From 2aa4a82499d4becd2284cdb482213d541b8804dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 28 Apr 2024 16:29:10 +0200 Subject: Adding upstream version 86.0.1. Signed-off-by: Daniel Baumann --- netwerk/protocol/about/moz.build | 35 + netwerk/protocol/about/nsAboutBlank.cpp | 52 + netwerk/protocol/about/nsAboutBlank.h | 32 + netwerk/protocol/about/nsAboutCache.cpp | 532 + netwerk/protocol/about/nsAboutCache.h | 193 + netwerk/protocol/about/nsAboutCacheEntry.cpp | 557 + netwerk/protocol/about/nsAboutCacheEntry.h | 85 + netwerk/protocol/about/nsAboutProtocolHandler.cpp | 445 + netwerk/protocol/about/nsAboutProtocolHandler.h | 146 + netwerk/protocol/about/nsAboutProtocolUtils.h | 57 + netwerk/protocol/about/nsIAboutModule.idl | 114 + netwerk/protocol/data/DataChannelChild.cpp | 58 + netwerk/protocol/data/DataChannelChild.h | 40 + netwerk/protocol/data/DataChannelParent.cpp | 105 + netwerk/protocol/data/DataChannelParent.h | 39 + netwerk/protocol/data/moz.build | 29 + netwerk/protocol/data/nsDataChannel.cpp | 109 + netwerk/protocol/data/nsDataChannel.h | 24 + netwerk/protocol/data/nsDataHandler.cpp | 270 + netwerk/protocol/data/nsDataHandler.h | 57 + netwerk/protocol/data/nsDataModule.cpp | 15 + netwerk/protocol/file/FileChannelChild.cpp | 52 + netwerk/protocol/file/FileChannelChild.h | 35 + netwerk/protocol/file/FileChannelParent.cpp | 105 + netwerk/protocol/file/FileChannelParent.h | 39 + netwerk/protocol/file/moz.build | 43 + netwerk/protocol/file/nsFileChannel.cpp | 511 + netwerk/protocol/file/nsFileChannel.h | 53 + netwerk/protocol/file/nsFileProtocolHandler.cpp | 277 + netwerk/protocol/file/nsFileProtocolHandler.h | 28 + netwerk/protocol/file/nsIFileChannel.idl | 17 + netwerk/protocol/file/nsIFileProtocolHandler.idl | 87 + netwerk/protocol/ftp/FTPChannelChild.cpp | 541 + netwerk/protocol/ftp/FTPChannelChild.h | 137 + netwerk/protocol/ftp/FTPChannelParent.cpp | 418 + netwerk/protocol/ftp/FTPChannelParent.h | 92 + netwerk/protocol/ftp/PFTPChannel.ipdl | 54 + netwerk/protocol/ftp/doc/testdoc | 4 + netwerk/protocol/ftp/ftpCore.h | 15 + netwerk/protocol/ftp/moz.build | 50 + netwerk/protocol/ftp/nsFTPChannel.cpp | 213 + netwerk/protocol/ftp/nsFTPChannel.h | 115 + netwerk/protocol/ftp/nsFtpConnectionThread.cpp | 1960 ++++ netwerk/protocol/ftp/nsFtpConnectionThread.h | 254 + netwerk/protocol/ftp/nsFtpControlConnection.cpp | 169 + netwerk/protocol/ftp/nsFtpControlConnection.h | 83 + netwerk/protocol/ftp/nsFtpProtocolHandler.cpp | 331 + netwerk/protocol/ftp/nsFtpProtocolHandler.h | 85 + netwerk/protocol/ftp/nsIFTPChannel.idl | 29 + .../protocol/ftp/nsIFTPChannelParentInternal.idl | 15 + netwerk/protocol/ftp/test/frametest/contents.html | 5 + netwerk/protocol/ftp/test/frametest/index.html | 12 + netwerk/protocol/ftp/test/frametest/menu.html | 371 + netwerk/protocol/gio/components.conf | 16 + netwerk/protocol/gio/moz.build | 24 + netwerk/protocol/gio/nsGIOProtocolHandler.cpp | 991 ++ netwerk/protocol/gio/nsGIOProtocolHandler.h | 33 + netwerk/protocol/http/ASpdySession.cpp | 113 + netwerk/protocol/http/ASpdySession.h | 126 + netwerk/protocol/http/AltDataOutputStreamChild.cpp | 196 + netwerk/protocol/http/AltDataOutputStreamChild.h | 51 + .../protocol/http/AltDataOutputStreamParent.cpp | 82 + netwerk/protocol/http/AltDataOutputStreamParent.h | 52 + netwerk/protocol/http/AltServiceChild.cpp | 114 + netwerk/protocol/http/AltServiceChild.h | 43 + netwerk/protocol/http/AltServiceParent.cpp | 52 + netwerk/protocol/http/AltServiceParent.h | 41 + netwerk/protocol/http/AltSvcTransactionChild.cpp | 75 + netwerk/protocol/http/AltSvcTransactionChild.h | 39 + netwerk/protocol/http/AltSvcTransactionParent.cpp | 64 + netwerk/protocol/http/AltSvcTransactionParent.h | 52 + netwerk/protocol/http/AlternateServices.cpp | 1330 +++ netwerk/protocol/http/AlternateServices.h | 277 + .../protocol/http/BackgroundChannelRegistrar.cpp | 103 + netwerk/protocol/http/BackgroundChannelRegistrar.h | 57 + .../protocol/http/BackgroundDataBridgeChild.cpp | 58 + netwerk/protocol/http/BackgroundDataBridgeChild.h | 40 + .../protocol/http/BackgroundDataBridgeParent.cpp | 57 + netwerk/protocol/http/BackgroundDataBridgeParent.h | 35 + netwerk/protocol/http/CacheControlParser.cpp | 135 + netwerk/protocol/http/CacheControlParser.h | 52 + netwerk/protocol/http/CachePushChecker.cpp | 249 + netwerk/protocol/http/CachePushChecker.h | 45 + netwerk/protocol/http/ClassifierDummyChannel.cpp | 775 ++ netwerk/protocol/http/ClassifierDummyChannel.h | 91 + .../protocol/http/ClassifierDummyChannelChild.cpp | 91 + .../protocol/http/ClassifierDummyChannelChild.h | 47 + .../protocol/http/ClassifierDummyChannelParent.cpp | 55 + .../protocol/http/ClassifierDummyChannelParent.h | 40 + netwerk/protocol/http/ConnectionDiagnostics.cpp | 236 + netwerk/protocol/http/ConnectionEntry.cpp | 962 ++ netwerk/protocol/http/ConnectionEntry.h | 210 + netwerk/protocol/http/ConnectionHandle.cpp | 87 + netwerk/protocol/http/ConnectionHandle.h | 38 + netwerk/protocol/http/DelayHttpChannelQueue.cpp | 122 + netwerk/protocol/http/DelayHttpChannelQueue.h | 46 + netwerk/protocol/http/HTTPSRecordResolver.cpp | 99 + netwerk/protocol/http/HTTPSRecordResolver.h | 41 + netwerk/protocol/http/HalfOpenSocket.cpp | 1307 +++ netwerk/protocol/http/HalfOpenSocket.h | 165 + netwerk/protocol/http/Http2Compression.cpp | 1443 +++ netwerk/protocol/http/Http2Compression.h | 206 + netwerk/protocol/http/Http2HuffmanIncoming.h | 709 ++ netwerk/protocol/http/Http2HuffmanOutgoing.h | 85 + netwerk/protocol/http/Http2Push.cpp | 498 + netwerk/protocol/http/Http2Push.h | 161 + netwerk/protocol/http/Http2Session.cpp | 4653 +++++++++ netwerk/protocol/http/Http2Session.h | 609 ++ netwerk/protocol/http/Http2Stream.cpp | 1678 +++ netwerk/protocol/http/Http2Stream.h | 395 + netwerk/protocol/http/Http3Session.cpp | 1715 ++++ netwerk/protocol/http/Http3Session.h | 218 + netwerk/protocol/http/Http3Stream.cpp | 489 + netwerk/protocol/http/Http3Stream.h | 158 + netwerk/protocol/http/HttpAuthUtils.cpp | 169 + netwerk/protocol/http/HttpAuthUtils.h | 33 + .../protocol/http/HttpBackgroundChannelChild.cpp | 504 + netwerk/protocol/http/HttpBackgroundChannelChild.h | 152 + .../protocol/http/HttpBackgroundChannelParent.cpp | 519 + .../protocol/http/HttpBackgroundChannelParent.h | 121 + netwerk/protocol/http/HttpBaseChannel.cpp | 5330 ++++++++++ netwerk/protocol/http/HttpBaseChannel.h | 1045 ++ netwerk/protocol/http/HttpChannelChild.cpp | 3156 ++++++ netwerk/protocol/http/HttpChannelChild.h | 451 + netwerk/protocol/http/HttpChannelParams.ipdlh | 57 + netwerk/protocol/http/HttpChannelParent.cpp | 2145 ++++ netwerk/protocol/http/HttpChannelParent.h | 327 + netwerk/protocol/http/HttpConnectionBase.cpp | 70 + netwerk/protocol/http/HttpConnectionBase.h | 208 + netwerk/protocol/http/HttpConnectionMgrChild.cpp | 182 + netwerk/protocol/http/HttpConnectionMgrChild.h | 53 + netwerk/protocol/http/HttpConnectionMgrParent.cpp | 273 + netwerk/protocol/http/HttpConnectionMgrParent.h | 34 + netwerk/protocol/http/HttpConnectionMgrShell.h | 232 + netwerk/protocol/http/HttpConnectionUDP.cpp | 772 ++ netwerk/protocol/http/HttpConnectionUDP.h | 131 + netwerk/protocol/http/HttpInfo.cpp | 16 + netwerk/protocol/http/HttpInfo.h | 24 + netwerk/protocol/http/HttpLog.h | 70 + netwerk/protocol/http/HttpTrafficAnalyzer.cpp | 269 + netwerk/protocol/http/HttpTrafficAnalyzer.h | 51 + netwerk/protocol/http/HttpTrafficAnalyzer.inc | 106 + netwerk/protocol/http/HttpTransactionChild.cpp | 646 ++ netwerk/protocol/http/HttpTransactionChild.h | 127 + netwerk/protocol/http/HttpTransactionParent.cpp | 909 ++ netwerk/protocol/http/HttpTransactionParent.h | 170 + netwerk/protocol/http/HttpTransactionShell.h | 227 + netwerk/protocol/http/InterceptedChannel.cpp | 232 + netwerk/protocol/http/InterceptedChannel.h | 155 + netwerk/protocol/http/InterceptedHttpChannel.cpp | 1311 +++ netwerk/protocol/http/InterceptedHttpChannel.h | 188 + netwerk/protocol/http/NullHttpChannel.cpp | 867 ++ netwerk/protocol/http/NullHttpChannel.h | 64 + netwerk/protocol/http/NullHttpTransaction.cpp | 220 + netwerk/protocol/http/NullHttpTransaction.h | 99 + netwerk/protocol/http/PAltDataOutputStream.ipdl | 41 + netwerk/protocol/http/PAltService.ipdl | 41 + netwerk/protocol/http/PAltSvcTransaction.ipdl | 23 + netwerk/protocol/http/PBackgroundDataBridge.ipdl | 32 + netwerk/protocol/http/PClassifierDummyChannel.ipdl | 47 + netwerk/protocol/http/PHttpBackgroundChannel.ipdl | 80 + netwerk/protocol/http/PHttpChannel.ipdl | 152 + netwerk/protocol/http/PHttpChannelParams.h | 280 + netwerk/protocol/http/PHttpConnectionMgr.ipdl | 40 + netwerk/protocol/http/PHttpTransaction.ipdl | 106 + netwerk/protocol/http/PSpdyPush.h | 56 + netwerk/protocol/http/ParentChannelListener.cpp | 282 + netwerk/protocol/http/ParentChannelListener.h | 92 + netwerk/protocol/http/PendingTransactionInfo.cpp | 135 + netwerk/protocol/http/PendingTransactionInfo.h | 60 + netwerk/protocol/http/PendingTransactionQueue.cpp | 290 + netwerk/protocol/http/PendingTransactionQueue.h | 92 + netwerk/protocol/http/QuicSocketControl.cpp | 138 + netwerk/protocol/http/QuicSocketControl.h | 62 + netwerk/protocol/http/README | 119 + netwerk/protocol/http/SpeculativeTransaction.cpp | 84 + netwerk/protocol/http/SpeculativeTransaction.h | 69 + netwerk/protocol/http/TRRServiceChannel.cpp | 1539 +++ netwerk/protocol/http/TRRServiceChannel.h | 169 + netwerk/protocol/http/TimingStruct.h | 44 + netwerk/protocol/http/TunnelUtils.cpp | 2172 ++++ netwerk/protocol/http/TunnelUtils.h | 302 + .../protocol/http/WellKnownOpportunisticUtils.jsm | 30 + netwerk/protocol/http/components.conf | 14 + netwerk/protocol/http/http2_huffman_table.txt | 257 + netwerk/protocol/http/make_incoming_tables.py | 202 + netwerk/protocol/http/make_outgoing_tables.py | 58 + netwerk/protocol/http/moz.build | 199 + netwerk/protocol/http/nsAHttpConnection.h | 248 + netwerk/protocol/http/nsAHttpTransaction.h | 307 + netwerk/protocol/http/nsCORSListenerProxy.cpp | 1599 +++ netwerk/protocol/http/nsCORSListenerProxy.h | 127 + netwerk/protocol/http/nsHttp.cpp | 1045 ++ netwerk/protocol/http/nsHttp.h | 388 + .../protocol/http/nsHttpActivityDistributor.cpp | 198 + netwerk/protocol/http/nsHttpActivityDistributor.h | 37 + netwerk/protocol/http/nsHttpAtomList.h | 104 + netwerk/protocol/http/nsHttpAuthCache.cpp | 462 + netwerk/protocol/http/nsHttpAuthCache.h | 232 + netwerk/protocol/http/nsHttpAuthManager.cpp | 113 + netwerk/protocol/http/nsHttpAuthManager.h | 34 + netwerk/protocol/http/nsHttpBasicAuth.cpp | 103 + netwerk/protocol/http/nsHttpBasicAuth.h | 39 + netwerk/protocol/http/nsHttpChannel.cpp | 10154 +++++++++++++++++++ netwerk/protocol/http/nsHttpChannel.h | 888 ++ .../protocol/http/nsHttpChannelAuthProvider.cpp | 1698 ++++ netwerk/protocol/http/nsHttpChannelAuthProvider.h | 186 + netwerk/protocol/http/nsHttpChunkedDecoder.cpp | 168 + netwerk/protocol/http/nsHttpChunkedDecoder.h | 54 + netwerk/protocol/http/nsHttpConnection.cpp | 2735 +++++ netwerk/protocol/http/nsHttpConnection.h | 359 + netwerk/protocol/http/nsHttpConnectionInfo.cpp | 580 ++ netwerk/protocol/http/nsHttpConnectionInfo.h | 280 + netwerk/protocol/http/nsHttpConnectionMgr.cpp | 3527 +++++++ netwerk/protocol/http/nsHttpConnectionMgr.h | 453 + netwerk/protocol/http/nsHttpDigestAuth.cpp | 662 ++ netwerk/protocol/http/nsHttpDigestAuth.h | 93 + netwerk/protocol/http/nsHttpHandler.cpp | 3053 ++++++ netwerk/protocol/http/nsHttpHandler.h | 939 ++ netwerk/protocol/http/nsHttpHeaderArray.cpp | 452 + netwerk/protocol/http/nsHttpHeaderArray.h | 289 + netwerk/protocol/http/nsHttpNTLMAuth.cpp | 404 + netwerk/protocol/http/nsHttpNTLMAuth.h | 36 + netwerk/protocol/http/nsHttpRequestHead.cpp | 360 + netwerk/protocol/http/nsHttpRequestHead.h | 147 + netwerk/protocol/http/nsHttpResponseHead.cpp | 1266 +++ netwerk/protocol/http/nsHttpResponseHead.h | 235 + netwerk/protocol/http/nsHttpTransaction.cpp | 3347 ++++++ netwerk/protocol/http/nsHttpTransaction.h | 545 + .../http/nsIBackgroundChannelRegistrar.idl | 63 + netwerk/protocol/http/nsICorsPreflightCallback.h | 31 + netwerk/protocol/http/nsIHttpActivityObserver.idl | 162 + netwerk/protocol/http/nsIHttpAuthManager.idl | 115 + .../protocol/http/nsIHttpAuthenticableChannel.idl | 122 + netwerk/protocol/http/nsIHttpAuthenticator.idl | 221 + netwerk/protocol/http/nsIHttpChannel.idl | 510 + .../protocol/http/nsIHttpChannelAuthProvider.idl | 86 + netwerk/protocol/http/nsIHttpChannelChild.idl | 38 + netwerk/protocol/http/nsIHttpChannelInternal.idl | 439 + netwerk/protocol/http/nsIHttpHeaderVisitor.idl | 26 + netwerk/protocol/http/nsIHttpProtocolHandler.idl | 195 + netwerk/protocol/http/nsIRaceCacheWithNetwork.idl | 55 + .../http/nsIWellKnownOpportunisticUtils.idl | 23 + netwerk/protocol/http/nsServerTiming.cpp | 110 + netwerk/protocol/http/nsServerTiming.h | 54 + netwerk/protocol/moz.build | 10 + netwerk/protocol/res/ExtensionProtocolHandler.cpp | 987 ++ netwerk/protocol/res/ExtensionProtocolHandler.h | 236 + netwerk/protocol/res/PageThumbProtocolHandler.cpp | 468 + netwerk/protocol/res/PageThumbProtocolHandler.h | 123 + netwerk/protocol/res/SubstitutingJARURI.h | 161 + .../protocol/res/SubstitutingProtocolHandler.cpp | 653 ++ netwerk/protocol/res/SubstitutingProtocolHandler.h | 134 + netwerk/protocol/res/SubstitutingURL.h | 66 + netwerk/protocol/res/moz.build | 40 + netwerk/protocol/res/nsIResProtocolHandler.idl | 15 + .../res/nsISubstitutingProtocolHandler.idl | 62 + netwerk/protocol/res/nsResProtocolHandler.cpp | 194 + netwerk/protocol/res/nsResProtocolHandler.h | 80 + netwerk/protocol/viewsource/moz.build | 29 + .../protocol/viewsource/nsIViewSourceChannel.idl | 43 + .../protocol/viewsource/nsViewSourceChannel.cpp | 1175 +++ netwerk/protocol/viewsource/nsViewSourceChannel.h | 104 + .../protocol/viewsource/nsViewSourceHandler.cpp | 153 + netwerk/protocol/viewsource/nsViewSourceHandler.h | 48 + .../protocol/websocket/BaseWebSocketChannel.cpp | 367 + netwerk/protocol/websocket/BaseWebSocketChannel.h | 127 + .../protocol/websocket/IPCTransportProvider.cpp | 82 + netwerk/protocol/websocket/IPCTransportProvider.h | 89 + netwerk/protocol/websocket/PTransportProvider.ipdl | 27 + netwerk/protocol/websocket/PWebSocket.ipdl | 66 + .../websocket/PWebSocketEventListener.ipdl | 52 + netwerk/protocol/websocket/WebSocketChannel.cpp | 4023 ++++++++ netwerk/protocol/websocket/WebSocketChannel.h | 315 + .../protocol/websocket/WebSocketChannelChild.cpp | 729 ++ netwerk/protocol/websocket/WebSocketChannelChild.h | 111 + .../protocol/websocket/WebSocketChannelParent.cpp | 351 + .../protocol/websocket/WebSocketChannelParent.h | 69 + .../websocket/WebSocketEventListenerChild.cpp | 109 + .../websocket/WebSocketEventListenerChild.h | 63 + .../websocket/WebSocketEventListenerParent.cpp | 119 + .../websocket/WebSocketEventListenerParent.h | 44 + .../protocol/websocket/WebSocketEventService.cpp | 563 + netwerk/protocol/websocket/WebSocketEventService.h | 122 + netwerk/protocol/websocket/WebSocketFrame.cpp | 158 + netwerk/protocol/websocket/WebSocketFrame.h | 100 + netwerk/protocol/websocket/WebSocketLog.h | 24 + netwerk/protocol/websocket/moz.build | 62 + .../protocol/websocket/nsITransportProvider.idl | 36 + netwerk/protocol/websocket/nsIWebSocketChannel.idl | 252 + .../websocket/nsIWebSocketEventService.idl | 87 + netwerk/protocol/websocket/nsIWebSocketImpl.idl | 18 + .../protocol/websocket/nsIWebSocketListener.idl | 96 + 293 files changed, 113485 insertions(+) create mode 100644 netwerk/protocol/about/moz.build create mode 100644 netwerk/protocol/about/nsAboutBlank.cpp create mode 100644 netwerk/protocol/about/nsAboutBlank.h create mode 100644 netwerk/protocol/about/nsAboutCache.cpp create mode 100644 netwerk/protocol/about/nsAboutCache.h create mode 100644 netwerk/protocol/about/nsAboutCacheEntry.cpp create mode 100644 netwerk/protocol/about/nsAboutCacheEntry.h create mode 100644 netwerk/protocol/about/nsAboutProtocolHandler.cpp create mode 100644 netwerk/protocol/about/nsAboutProtocolHandler.h create mode 100644 netwerk/protocol/about/nsAboutProtocolUtils.h create mode 100644 netwerk/protocol/about/nsIAboutModule.idl create mode 100644 netwerk/protocol/data/DataChannelChild.cpp create mode 100644 netwerk/protocol/data/DataChannelChild.h create mode 100644 netwerk/protocol/data/DataChannelParent.cpp create mode 100644 netwerk/protocol/data/DataChannelParent.h create mode 100644 netwerk/protocol/data/moz.build create mode 100644 netwerk/protocol/data/nsDataChannel.cpp create mode 100644 netwerk/protocol/data/nsDataChannel.h create mode 100644 netwerk/protocol/data/nsDataHandler.cpp create mode 100644 netwerk/protocol/data/nsDataHandler.h create mode 100644 netwerk/protocol/data/nsDataModule.cpp create mode 100644 netwerk/protocol/file/FileChannelChild.cpp create mode 100644 netwerk/protocol/file/FileChannelChild.h create mode 100644 netwerk/protocol/file/FileChannelParent.cpp create mode 100644 netwerk/protocol/file/FileChannelParent.h create mode 100644 netwerk/protocol/file/moz.build create mode 100644 netwerk/protocol/file/nsFileChannel.cpp create mode 100644 netwerk/protocol/file/nsFileChannel.h create mode 100644 netwerk/protocol/file/nsFileProtocolHandler.cpp create mode 100644 netwerk/protocol/file/nsFileProtocolHandler.h create mode 100644 netwerk/protocol/file/nsIFileChannel.idl create mode 100644 netwerk/protocol/file/nsIFileProtocolHandler.idl create mode 100644 netwerk/protocol/ftp/FTPChannelChild.cpp create mode 100644 netwerk/protocol/ftp/FTPChannelChild.h create mode 100644 netwerk/protocol/ftp/FTPChannelParent.cpp create mode 100644 netwerk/protocol/ftp/FTPChannelParent.h create mode 100644 netwerk/protocol/ftp/PFTPChannel.ipdl create mode 100644 netwerk/protocol/ftp/doc/testdoc create mode 100644 netwerk/protocol/ftp/ftpCore.h create mode 100644 netwerk/protocol/ftp/moz.build create mode 100644 netwerk/protocol/ftp/nsFTPChannel.cpp create mode 100644 netwerk/protocol/ftp/nsFTPChannel.h create mode 100644 netwerk/protocol/ftp/nsFtpConnectionThread.cpp create mode 100644 netwerk/protocol/ftp/nsFtpConnectionThread.h create mode 100644 netwerk/protocol/ftp/nsFtpControlConnection.cpp create mode 100644 netwerk/protocol/ftp/nsFtpControlConnection.h create mode 100644 netwerk/protocol/ftp/nsFtpProtocolHandler.cpp create mode 100644 netwerk/protocol/ftp/nsFtpProtocolHandler.h create mode 100644 netwerk/protocol/ftp/nsIFTPChannel.idl create mode 100644 netwerk/protocol/ftp/nsIFTPChannelParentInternal.idl create mode 100644 netwerk/protocol/ftp/test/frametest/contents.html create mode 100644 netwerk/protocol/ftp/test/frametest/index.html create mode 100644 netwerk/protocol/ftp/test/frametest/menu.html create mode 100644 netwerk/protocol/gio/components.conf create mode 100644 netwerk/protocol/gio/moz.build create mode 100644 netwerk/protocol/gio/nsGIOProtocolHandler.cpp create mode 100644 netwerk/protocol/gio/nsGIOProtocolHandler.h create mode 100644 netwerk/protocol/http/ASpdySession.cpp create mode 100644 netwerk/protocol/http/ASpdySession.h create mode 100644 netwerk/protocol/http/AltDataOutputStreamChild.cpp create mode 100644 netwerk/protocol/http/AltDataOutputStreamChild.h create mode 100644 netwerk/protocol/http/AltDataOutputStreamParent.cpp create mode 100644 netwerk/protocol/http/AltDataOutputStreamParent.h create mode 100644 netwerk/protocol/http/AltServiceChild.cpp create mode 100644 netwerk/protocol/http/AltServiceChild.h create mode 100644 netwerk/protocol/http/AltServiceParent.cpp create mode 100644 netwerk/protocol/http/AltServiceParent.h create mode 100644 netwerk/protocol/http/AltSvcTransactionChild.cpp create mode 100644 netwerk/protocol/http/AltSvcTransactionChild.h create mode 100644 netwerk/protocol/http/AltSvcTransactionParent.cpp create mode 100644 netwerk/protocol/http/AltSvcTransactionParent.h create mode 100644 netwerk/protocol/http/AlternateServices.cpp create mode 100644 netwerk/protocol/http/AlternateServices.h create mode 100644 netwerk/protocol/http/BackgroundChannelRegistrar.cpp create mode 100644 netwerk/protocol/http/BackgroundChannelRegistrar.h create mode 100644 netwerk/protocol/http/BackgroundDataBridgeChild.cpp create mode 100644 netwerk/protocol/http/BackgroundDataBridgeChild.h create mode 100644 netwerk/protocol/http/BackgroundDataBridgeParent.cpp create mode 100644 netwerk/protocol/http/BackgroundDataBridgeParent.h create mode 100644 netwerk/protocol/http/CacheControlParser.cpp create mode 100644 netwerk/protocol/http/CacheControlParser.h create mode 100644 netwerk/protocol/http/CachePushChecker.cpp create mode 100644 netwerk/protocol/http/CachePushChecker.h create mode 100644 netwerk/protocol/http/ClassifierDummyChannel.cpp create mode 100644 netwerk/protocol/http/ClassifierDummyChannel.h create mode 100644 netwerk/protocol/http/ClassifierDummyChannelChild.cpp create mode 100644 netwerk/protocol/http/ClassifierDummyChannelChild.h create mode 100644 netwerk/protocol/http/ClassifierDummyChannelParent.cpp create mode 100644 netwerk/protocol/http/ClassifierDummyChannelParent.h create mode 100644 netwerk/protocol/http/ConnectionDiagnostics.cpp create mode 100644 netwerk/protocol/http/ConnectionEntry.cpp create mode 100644 netwerk/protocol/http/ConnectionEntry.h create mode 100644 netwerk/protocol/http/ConnectionHandle.cpp create mode 100644 netwerk/protocol/http/ConnectionHandle.h create mode 100644 netwerk/protocol/http/DelayHttpChannelQueue.cpp create mode 100644 netwerk/protocol/http/DelayHttpChannelQueue.h create mode 100644 netwerk/protocol/http/HTTPSRecordResolver.cpp create mode 100644 netwerk/protocol/http/HTTPSRecordResolver.h create mode 100644 netwerk/protocol/http/HalfOpenSocket.cpp create mode 100644 netwerk/protocol/http/HalfOpenSocket.h create mode 100644 netwerk/protocol/http/Http2Compression.cpp create mode 100644 netwerk/protocol/http/Http2Compression.h create mode 100644 netwerk/protocol/http/Http2HuffmanIncoming.h create mode 100644 netwerk/protocol/http/Http2HuffmanOutgoing.h create mode 100644 netwerk/protocol/http/Http2Push.cpp create mode 100644 netwerk/protocol/http/Http2Push.h create mode 100644 netwerk/protocol/http/Http2Session.cpp create mode 100644 netwerk/protocol/http/Http2Session.h create mode 100644 netwerk/protocol/http/Http2Stream.cpp create mode 100644 netwerk/protocol/http/Http2Stream.h create mode 100644 netwerk/protocol/http/Http3Session.cpp create mode 100644 netwerk/protocol/http/Http3Session.h create mode 100644 netwerk/protocol/http/Http3Stream.cpp create mode 100644 netwerk/protocol/http/Http3Stream.h create mode 100644 netwerk/protocol/http/HttpAuthUtils.cpp create mode 100644 netwerk/protocol/http/HttpAuthUtils.h create mode 100644 netwerk/protocol/http/HttpBackgroundChannelChild.cpp create mode 100644 netwerk/protocol/http/HttpBackgroundChannelChild.h create mode 100644 netwerk/protocol/http/HttpBackgroundChannelParent.cpp create mode 100644 netwerk/protocol/http/HttpBackgroundChannelParent.h create mode 100644 netwerk/protocol/http/HttpBaseChannel.cpp create mode 100644 netwerk/protocol/http/HttpBaseChannel.h create mode 100644 netwerk/protocol/http/HttpChannelChild.cpp create mode 100644 netwerk/protocol/http/HttpChannelChild.h create mode 100644 netwerk/protocol/http/HttpChannelParams.ipdlh create mode 100644 netwerk/protocol/http/HttpChannelParent.cpp create mode 100644 netwerk/protocol/http/HttpChannelParent.h create mode 100644 netwerk/protocol/http/HttpConnectionBase.cpp create mode 100644 netwerk/protocol/http/HttpConnectionBase.h create mode 100644 netwerk/protocol/http/HttpConnectionMgrChild.cpp create mode 100644 netwerk/protocol/http/HttpConnectionMgrChild.h create mode 100644 netwerk/protocol/http/HttpConnectionMgrParent.cpp create mode 100644 netwerk/protocol/http/HttpConnectionMgrParent.h create mode 100644 netwerk/protocol/http/HttpConnectionMgrShell.h create mode 100644 netwerk/protocol/http/HttpConnectionUDP.cpp create mode 100644 netwerk/protocol/http/HttpConnectionUDP.h create mode 100644 netwerk/protocol/http/HttpInfo.cpp create mode 100644 netwerk/protocol/http/HttpInfo.h create mode 100644 netwerk/protocol/http/HttpLog.h create mode 100644 netwerk/protocol/http/HttpTrafficAnalyzer.cpp create mode 100644 netwerk/protocol/http/HttpTrafficAnalyzer.h create mode 100644 netwerk/protocol/http/HttpTrafficAnalyzer.inc create mode 100644 netwerk/protocol/http/HttpTransactionChild.cpp create mode 100644 netwerk/protocol/http/HttpTransactionChild.h create mode 100644 netwerk/protocol/http/HttpTransactionParent.cpp create mode 100644 netwerk/protocol/http/HttpTransactionParent.h create mode 100644 netwerk/protocol/http/HttpTransactionShell.h create mode 100644 netwerk/protocol/http/InterceptedChannel.cpp create mode 100644 netwerk/protocol/http/InterceptedChannel.h create mode 100644 netwerk/protocol/http/InterceptedHttpChannel.cpp create mode 100644 netwerk/protocol/http/InterceptedHttpChannel.h create mode 100644 netwerk/protocol/http/NullHttpChannel.cpp create mode 100644 netwerk/protocol/http/NullHttpChannel.h create mode 100644 netwerk/protocol/http/NullHttpTransaction.cpp create mode 100644 netwerk/protocol/http/NullHttpTransaction.h create mode 100644 netwerk/protocol/http/PAltDataOutputStream.ipdl create mode 100644 netwerk/protocol/http/PAltService.ipdl create mode 100644 netwerk/protocol/http/PAltSvcTransaction.ipdl create mode 100644 netwerk/protocol/http/PBackgroundDataBridge.ipdl create mode 100644 netwerk/protocol/http/PClassifierDummyChannel.ipdl create mode 100644 netwerk/protocol/http/PHttpBackgroundChannel.ipdl create mode 100644 netwerk/protocol/http/PHttpChannel.ipdl create mode 100644 netwerk/protocol/http/PHttpChannelParams.h create mode 100644 netwerk/protocol/http/PHttpConnectionMgr.ipdl create mode 100644 netwerk/protocol/http/PHttpTransaction.ipdl create mode 100644 netwerk/protocol/http/PSpdyPush.h create mode 100644 netwerk/protocol/http/ParentChannelListener.cpp create mode 100644 netwerk/protocol/http/ParentChannelListener.h create mode 100644 netwerk/protocol/http/PendingTransactionInfo.cpp create mode 100644 netwerk/protocol/http/PendingTransactionInfo.h create mode 100644 netwerk/protocol/http/PendingTransactionQueue.cpp create mode 100644 netwerk/protocol/http/PendingTransactionQueue.h create mode 100644 netwerk/protocol/http/QuicSocketControl.cpp create mode 100644 netwerk/protocol/http/QuicSocketControl.h create mode 100644 netwerk/protocol/http/README create mode 100644 netwerk/protocol/http/SpeculativeTransaction.cpp create mode 100644 netwerk/protocol/http/SpeculativeTransaction.h create mode 100644 netwerk/protocol/http/TRRServiceChannel.cpp create mode 100644 netwerk/protocol/http/TRRServiceChannel.h create mode 100644 netwerk/protocol/http/TimingStruct.h create mode 100644 netwerk/protocol/http/TunnelUtils.cpp create mode 100644 netwerk/protocol/http/TunnelUtils.h create mode 100644 netwerk/protocol/http/WellKnownOpportunisticUtils.jsm create mode 100644 netwerk/protocol/http/components.conf create mode 100644 netwerk/protocol/http/http2_huffman_table.txt create mode 100644 netwerk/protocol/http/make_incoming_tables.py create mode 100644 netwerk/protocol/http/make_outgoing_tables.py create mode 100644 netwerk/protocol/http/moz.build create mode 100644 netwerk/protocol/http/nsAHttpConnection.h create mode 100644 netwerk/protocol/http/nsAHttpTransaction.h create mode 100644 netwerk/protocol/http/nsCORSListenerProxy.cpp create mode 100644 netwerk/protocol/http/nsCORSListenerProxy.h create mode 100644 netwerk/protocol/http/nsHttp.cpp create mode 100644 netwerk/protocol/http/nsHttp.h create mode 100644 netwerk/protocol/http/nsHttpActivityDistributor.cpp create mode 100644 netwerk/protocol/http/nsHttpActivityDistributor.h create mode 100644 netwerk/protocol/http/nsHttpAtomList.h create mode 100644 netwerk/protocol/http/nsHttpAuthCache.cpp create mode 100644 netwerk/protocol/http/nsHttpAuthCache.h create mode 100644 netwerk/protocol/http/nsHttpAuthManager.cpp create mode 100644 netwerk/protocol/http/nsHttpAuthManager.h create mode 100644 netwerk/protocol/http/nsHttpBasicAuth.cpp create mode 100644 netwerk/protocol/http/nsHttpBasicAuth.h create mode 100644 netwerk/protocol/http/nsHttpChannel.cpp create mode 100644 netwerk/protocol/http/nsHttpChannel.h create mode 100644 netwerk/protocol/http/nsHttpChannelAuthProvider.cpp create mode 100644 netwerk/protocol/http/nsHttpChannelAuthProvider.h create mode 100644 netwerk/protocol/http/nsHttpChunkedDecoder.cpp create mode 100644 netwerk/protocol/http/nsHttpChunkedDecoder.h create mode 100644 netwerk/protocol/http/nsHttpConnection.cpp create mode 100644 netwerk/protocol/http/nsHttpConnection.h create mode 100644 netwerk/protocol/http/nsHttpConnectionInfo.cpp create mode 100644 netwerk/protocol/http/nsHttpConnectionInfo.h create mode 100644 netwerk/protocol/http/nsHttpConnectionMgr.cpp create mode 100644 netwerk/protocol/http/nsHttpConnectionMgr.h create mode 100644 netwerk/protocol/http/nsHttpDigestAuth.cpp create mode 100644 netwerk/protocol/http/nsHttpDigestAuth.h create mode 100644 netwerk/protocol/http/nsHttpHandler.cpp create mode 100644 netwerk/protocol/http/nsHttpHandler.h create mode 100644 netwerk/protocol/http/nsHttpHeaderArray.cpp create mode 100644 netwerk/protocol/http/nsHttpHeaderArray.h create mode 100644 netwerk/protocol/http/nsHttpNTLMAuth.cpp create mode 100644 netwerk/protocol/http/nsHttpNTLMAuth.h create mode 100644 netwerk/protocol/http/nsHttpRequestHead.cpp create mode 100644 netwerk/protocol/http/nsHttpRequestHead.h create mode 100644 netwerk/protocol/http/nsHttpResponseHead.cpp create mode 100644 netwerk/protocol/http/nsHttpResponseHead.h create mode 100644 netwerk/protocol/http/nsHttpTransaction.cpp create mode 100644 netwerk/protocol/http/nsHttpTransaction.h create mode 100644 netwerk/protocol/http/nsIBackgroundChannelRegistrar.idl create mode 100644 netwerk/protocol/http/nsICorsPreflightCallback.h create mode 100644 netwerk/protocol/http/nsIHttpActivityObserver.idl create mode 100644 netwerk/protocol/http/nsIHttpAuthManager.idl create mode 100644 netwerk/protocol/http/nsIHttpAuthenticableChannel.idl create mode 100644 netwerk/protocol/http/nsIHttpAuthenticator.idl create mode 100644 netwerk/protocol/http/nsIHttpChannel.idl create mode 100644 netwerk/protocol/http/nsIHttpChannelAuthProvider.idl create mode 100644 netwerk/protocol/http/nsIHttpChannelChild.idl create mode 100644 netwerk/protocol/http/nsIHttpChannelInternal.idl create mode 100644 netwerk/protocol/http/nsIHttpHeaderVisitor.idl create mode 100644 netwerk/protocol/http/nsIHttpProtocolHandler.idl create mode 100644 netwerk/protocol/http/nsIRaceCacheWithNetwork.idl create mode 100644 netwerk/protocol/http/nsIWellKnownOpportunisticUtils.idl create mode 100644 netwerk/protocol/http/nsServerTiming.cpp create mode 100644 netwerk/protocol/http/nsServerTiming.h create mode 100644 netwerk/protocol/moz.build create mode 100644 netwerk/protocol/res/ExtensionProtocolHandler.cpp create mode 100644 netwerk/protocol/res/ExtensionProtocolHandler.h create mode 100644 netwerk/protocol/res/PageThumbProtocolHandler.cpp create mode 100644 netwerk/protocol/res/PageThumbProtocolHandler.h create mode 100644 netwerk/protocol/res/SubstitutingJARURI.h create mode 100644 netwerk/protocol/res/SubstitutingProtocolHandler.cpp create mode 100644 netwerk/protocol/res/SubstitutingProtocolHandler.h create mode 100644 netwerk/protocol/res/SubstitutingURL.h create mode 100644 netwerk/protocol/res/moz.build create mode 100644 netwerk/protocol/res/nsIResProtocolHandler.idl create mode 100644 netwerk/protocol/res/nsISubstitutingProtocolHandler.idl create mode 100644 netwerk/protocol/res/nsResProtocolHandler.cpp create mode 100644 netwerk/protocol/res/nsResProtocolHandler.h create mode 100644 netwerk/protocol/viewsource/moz.build create mode 100644 netwerk/protocol/viewsource/nsIViewSourceChannel.idl create mode 100644 netwerk/protocol/viewsource/nsViewSourceChannel.cpp create mode 100644 netwerk/protocol/viewsource/nsViewSourceChannel.h create mode 100644 netwerk/protocol/viewsource/nsViewSourceHandler.cpp create mode 100644 netwerk/protocol/viewsource/nsViewSourceHandler.h create mode 100644 netwerk/protocol/websocket/BaseWebSocketChannel.cpp create mode 100644 netwerk/protocol/websocket/BaseWebSocketChannel.h create mode 100644 netwerk/protocol/websocket/IPCTransportProvider.cpp create mode 100644 netwerk/protocol/websocket/IPCTransportProvider.h create mode 100644 netwerk/protocol/websocket/PTransportProvider.ipdl create mode 100644 netwerk/protocol/websocket/PWebSocket.ipdl create mode 100644 netwerk/protocol/websocket/PWebSocketEventListener.ipdl create mode 100644 netwerk/protocol/websocket/WebSocketChannel.cpp create mode 100644 netwerk/protocol/websocket/WebSocketChannel.h create mode 100644 netwerk/protocol/websocket/WebSocketChannelChild.cpp create mode 100644 netwerk/protocol/websocket/WebSocketChannelChild.h create mode 100644 netwerk/protocol/websocket/WebSocketChannelParent.cpp create mode 100644 netwerk/protocol/websocket/WebSocketChannelParent.h create mode 100644 netwerk/protocol/websocket/WebSocketEventListenerChild.cpp create mode 100644 netwerk/protocol/websocket/WebSocketEventListenerChild.h create mode 100644 netwerk/protocol/websocket/WebSocketEventListenerParent.cpp create mode 100644 netwerk/protocol/websocket/WebSocketEventListenerParent.h create mode 100644 netwerk/protocol/websocket/WebSocketEventService.cpp create mode 100644 netwerk/protocol/websocket/WebSocketEventService.h create mode 100644 netwerk/protocol/websocket/WebSocketFrame.cpp create mode 100644 netwerk/protocol/websocket/WebSocketFrame.h create mode 100644 netwerk/protocol/websocket/WebSocketLog.h create mode 100644 netwerk/protocol/websocket/moz.build create mode 100644 netwerk/protocol/websocket/nsITransportProvider.idl create mode 100644 netwerk/protocol/websocket/nsIWebSocketChannel.idl create mode 100644 netwerk/protocol/websocket/nsIWebSocketEventService.idl create mode 100644 netwerk/protocol/websocket/nsIWebSocketImpl.idl create mode 100644 netwerk/protocol/websocket/nsIWebSocketListener.idl (limited to 'netwerk/protocol') diff --git a/netwerk/protocol/about/moz.build b/netwerk/protocol/about/moz.build new file mode 100644 index 0000000000..66bd1e4559 --- /dev/null +++ b/netwerk/protocol/about/moz.build @@ -0,0 +1,35 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +XPIDL_SOURCES += [ + "nsIAboutModule.idl", +] + +XPIDL_MODULE = "necko_about" + +EXPORTS += [ + "nsAboutProtocolHandler.h", + "nsAboutProtocolUtils.h", +] + +UNIFIED_SOURCES += [ + "nsAboutBlank.cpp", + "nsAboutCache.cpp", + "nsAboutCacheEntry.cpp", + "nsAboutProtocolHandler.cpp", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" + +LOCAL_INCLUDES += [ + "/netwerk/base", + "/netwerk/cache2", +] + +if CONFIG["CC_TYPE"] in ("clang", "gcc"): + CXXFLAGS += ["-Wno-error=shadow"] diff --git a/netwerk/protocol/about/nsAboutBlank.cpp b/netwerk/protocol/about/nsAboutBlank.cpp new file mode 100644 index 0000000000..4a5615b803 --- /dev/null +++ b/netwerk/protocol/about/nsAboutBlank.cpp @@ -0,0 +1,52 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 "nsAboutBlank.h" +#include "nsStringStream.h" +#include "nsNetUtil.h" +#include "nsContentUtils.h" + +NS_IMPL_ISUPPORTS(nsAboutBlank, nsIAboutModule) + +NS_IMETHODIMP +nsAboutBlank::NewChannel(nsIURI* aURI, nsILoadInfo* aLoadInfo, + nsIChannel** result) { + NS_ENSURE_ARG_POINTER(aURI); + + nsCOMPtr in; + nsresult rv = NS_NewCStringInputStream(getter_AddRefs(in), ""_ns); + if (NS_FAILED(rv)) return rv; + + nsCOMPtr channel; + rv = NS_NewInputStreamChannelInternal(getter_AddRefs(channel), aURI, + in.forget(), "text/html"_ns, "utf-8"_ns, + aLoadInfo); + if (NS_FAILED(rv)) return rv; + + channel.forget(result); + return rv; +} + +NS_IMETHODIMP +nsAboutBlank::GetURIFlags(nsIURI* aURI, uint32_t* result) { + *result = nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT | + nsIAboutModule::URI_CAN_LOAD_IN_CHILD | + nsIAboutModule::MAKE_LINKABLE | + nsIAboutModule::HIDE_FROM_ABOUTABOUT; + return NS_OK; +} + +NS_IMETHODIMP +nsAboutBlank::GetChromeURI(nsIURI* aURI, nsIURI** chromeURI) { + return NS_ERROR_ILLEGAL_VALUE; +} + +nsresult nsAboutBlank::Create(nsISupports* aOuter, REFNSIID aIID, + void** aResult) { + RefPtr about = new nsAboutBlank(); + return about->QueryInterface(aIID, aResult); +} + +//////////////////////////////////////////////////////////////////////////////// diff --git a/netwerk/protocol/about/nsAboutBlank.h b/netwerk/protocol/about/nsAboutBlank.h new file mode 100644 index 0000000000..4fcd90816c --- /dev/null +++ b/netwerk/protocol/about/nsAboutBlank.h @@ -0,0 +1,32 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef nsAboutBlank_h__ +#define nsAboutBlank_h__ + +#include "nsIAboutModule.h" + +class nsAboutBlank : public nsIAboutModule { + public: + NS_DECL_ISUPPORTS + + NS_DECL_NSIABOUTMODULE + + nsAboutBlank() = default; + + static nsresult Create(nsISupports* aOuter, REFNSIID aIID, void** aResult); + + private: + virtual ~nsAboutBlank() = default; +}; + +#define NS_ABOUT_BLANK_MODULE_CID \ + { /* 3decd6c8-30ef-11d3-8cd0-0060b0fc14a3 */ \ + 0x3decd6c8, 0x30ef, 0x11d3, { \ + 0x8c, 0xd0, 0x00, 0x60, 0xb0, 0xfc, 0x14, 0xa3 \ + } \ + } + +#endif // nsAboutBlank_h__ diff --git a/netwerk/protocol/about/nsAboutCache.cpp b/netwerk/protocol/about/nsAboutCache.cpp new file mode 100644 index 0000000000..d9c5c7733f --- /dev/null +++ b/netwerk/protocol/about/nsAboutCache.cpp @@ -0,0 +1,532 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 "nsAboutCache.h" +#include "nsIInputStream.h" +#include "nsIURI.h" +#include "nsCOMPtr.h" +#include "nsNetUtil.h" +#include "nsIPipe.h" +#include "nsContentUtils.h" +#include "nsEscape.h" +#include "nsAboutProtocolUtils.h" +#include "nsPrintfCString.h" + +#include "nsICacheStorageService.h" +#include "nsICacheStorage.h" +#include "CacheFileUtils.h" +#include "CacheObserver.h" + +#include "nsThreadUtils.h" + +using namespace mozilla::net; + +NS_IMPL_ISUPPORTS(nsAboutCache, nsIAboutModule) +NS_IMPL_ISUPPORTS(nsAboutCache::Channel, nsIChannel, nsIRequest, + nsICacheStorageVisitor) + +NS_IMETHODIMP +nsAboutCache::NewChannel(nsIURI* aURI, nsILoadInfo* aLoadInfo, + nsIChannel** result) { + nsresult rv; + + NS_ENSURE_ARG_POINTER(aURI); + + RefPtr channel = new Channel(); + rv = channel->Init(aURI, aLoadInfo); + if (NS_FAILED(rv)) return rv; + + channel.forget(result); + + return NS_OK; +} + +nsresult nsAboutCache::Channel::Init(nsIURI* aURI, nsILoadInfo* aLoadInfo) { + nsresult rv; + + mCancel = false; + + nsCOMPtr inputStream; + rv = NS_NewPipe(getter_AddRefs(inputStream), getter_AddRefs(mStream), 16384, + (uint32_t)-1, + true, // non-blocking input + false // blocking output + ); + if (NS_FAILED(rv)) return rv; + + nsAutoCString storageName; + rv = ParseURI(aURI, storageName); + if (NS_FAILED(rv)) return rv; + + mOverview = storageName.IsEmpty(); + if (mOverview) { + // ...and visit all we can + mStorageList.AppendElement("memory"_ns); + mStorageList.AppendElement("disk"_ns); + mStorageList.AppendElement("appcache"_ns); + } else { + // ...and visit just the specified storage, entries will output too + mStorageList.AppendElement(storageName); + } + + // The entries header is added on encounter of the first entry + mEntriesHeaderAdded = false; + + rv = NS_NewInputStreamChannelInternal(getter_AddRefs(mChannel), aURI, + inputStream.forget(), "text/html"_ns, + "utf-8"_ns, aLoadInfo); + if (NS_FAILED(rv)) return rv; + + mBuffer.AssignLiteral( + "\n" + "\n" + "\n" + " Network Cache Storage Information\n" + " \n" + " \n" + " \n" + " \n" + "\n" + "\n" + "

Information about the Network Cache Storage Service

\n"); + + if (!mOverview) { + mBuffer.AppendLiteral( + "Back to overview"); + } + + rv = FlushBuffer(); + if (NS_FAILED(rv)) { + NS_WARNING("Failed to flush buffer"); + } + + return NS_OK; +} + +NS_IMETHODIMP nsAboutCache::Channel::AsyncOpen(nsIStreamListener* aListener) { + nsresult rv; + + if (!mChannel) { + return NS_ERROR_UNEXPECTED; + } + + // Kick the walk loop. + rv = VisitNextStorage(); + if (NS_FAILED(rv)) return rv; + + rv = NS_MaybeOpenChannelUsingAsyncOpen(mChannel, aListener); + if (NS_FAILED(rv)) return rv; + + return NS_OK; +} + +NS_IMETHODIMP nsAboutCache::Channel::Open(nsIInputStream** _retval) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +nsresult nsAboutCache::Channel::ParseURI(nsIURI* uri, nsACString& storage) { + // + // about:cache[?storage=[&context=]] + // + nsresult rv; + + nsAutoCString path; + rv = uri->GetPathQueryRef(path); + if (NS_FAILED(rv)) return rv; + + storage.Truncate(); + + nsACString::const_iterator start, valueStart, end; + path.BeginReading(start); + path.EndReading(end); + + valueStart = end; + if (!FindInReadable("?storage="_ns, start, valueStart)) { + return NS_OK; + } + + storage.Assign(Substring(valueStart, end)); + + return NS_OK; +} + +nsresult nsAboutCache::Channel::VisitNextStorage() { + if (!mStorageList.Length()) return NS_ERROR_NOT_AVAILABLE; + + mStorageName = mStorageList[0]; + mStorageList.RemoveElementAt(0); + + // Must re-dispatch since we cannot start another visit cycle + // from visitor callback. The cache v1 service doesn't like it. + // TODO - mayhemer, bug 913828, remove this dispatch and call + // directly. + return NS_DispatchToMainThread(mozilla::NewRunnableMethod( + "nsAboutCache::Channel::FireVisitStorage", this, + &nsAboutCache::Channel::FireVisitStorage)); +} + +void nsAboutCache::Channel::FireVisitStorage() { + nsresult rv; + + rv = VisitStorage(mStorageName); + if (NS_FAILED(rv)) { + nsAutoCString escaped; + nsAppendEscapedHTML(mStorageName, escaped); + mBuffer.Append(nsPrintfCString( + "

Unrecognized storage name '%s' in about:cache URL

", + escaped.get())); + + rv = FlushBuffer(); + if (NS_FAILED(rv)) { + NS_WARNING("Failed to flush buffer"); + } + + // Simulate finish of a visit cycle, this tries the next storage + // or closes the output stream (i.e. the UI loader will stop spinning) + OnCacheEntryVisitCompleted(); + } +} + +nsresult nsAboutCache::Channel::VisitStorage(nsACString const& storageName) { + nsresult rv; + + rv = GetStorage(storageName, nullptr, getter_AddRefs(mStorage)); + if (NS_FAILED(rv)) return rv; + + rv = mStorage->AsyncVisitStorage(this, !mOverview); + if (NS_FAILED(rv)) return rv; + + return NS_OK; +} + +// static +nsresult nsAboutCache::GetStorage(nsACString const& storageName, + nsILoadContextInfo* loadInfo, + nsICacheStorage** storage) { + nsresult rv; + + nsCOMPtr cacheService = + do_GetService("@mozilla.org/netwerk/cache-storage-service;1", &rv); + if (NS_FAILED(rv)) return rv; + + nsCOMPtr cacheStorage; + if (storageName == "disk") { + rv = cacheService->DiskCacheStorage(loadInfo, false, + getter_AddRefs(cacheStorage)); + } else if (storageName == "memory") { + rv = cacheService->MemoryCacheStorage(loadInfo, + getter_AddRefs(cacheStorage)); + } else if (storageName == "appcache") { + rv = cacheService->AppCacheStorage(loadInfo, nullptr, + getter_AddRefs(cacheStorage)); + } else { + rv = NS_ERROR_UNEXPECTED; + } + if (NS_FAILED(rv)) return rv; + + cacheStorage.forget(storage); + return NS_OK; +} + +NS_IMETHODIMP +nsAboutCache::Channel::OnCacheStorageInfo(uint32_t aEntryCount, + uint64_t aConsumption, + uint64_t aCapacity, + nsIFile* aDirectory) { + // We need mStream for this + if (!mStream) { + return NS_ERROR_FAILURE; + } + + mBuffer.AssignLiteral("

"); + nsAppendEscapedHTML(mStorageName, mBuffer); + mBuffer.AppendLiteral( + "

\n" + "\n"); + + // Write out cache info + // Number of entries + mBuffer.AppendLiteral( + " \n" + " \n" + " \n" + " \n"); + + // Maximum storage size + mBuffer.AppendLiteral( + " \n" + " \n" + " \n" + " \n"); + + // Storage in use + mBuffer.AppendLiteral( + " \n" + " \n" + " \n" + " \n"); + + // Storage disk location + mBuffer.AppendLiteral( + " \n" + " \n" + " \n" + " \n"); + + if (mOverview) { // The about:cache case + if (aEntryCount != 0) { // Add the "List Cache Entries" link + mBuffer.AppendLiteral( + " \n" + " \n" + " \n"); + } + } + + mBuffer.AppendLiteral("
Number of entries:"); + mBuffer.AppendInt(aEntryCount); + mBuffer.AppendLiteral( + "
Maximum storage size:"); + mBuffer.AppendInt(aCapacity / 1024); + mBuffer.AppendLiteral( + " KiB
Storage in use:"); + mBuffer.AppendInt(aConsumption / 1024); + mBuffer.AppendLiteral( + " KiB
Storage disk location:"); + if (aDirectory) { + nsAutoString path; + aDirectory->GetPath(path); + mBuffer.Append(NS_ConvertUTF16toUTF8(path)); + } else { + mBuffer.AppendLiteral("none, only stored in memory"); + } + mBuffer.AppendLiteral( + "
List Cache Entries
\n"); + + // The entries header is added on encounter of the first entry + mEntriesHeaderAdded = false; + + nsresult rv = FlushBuffer(); + if (NS_FAILED(rv)) { + NS_WARNING("Failed to flush buffer"); + } + + if (mOverview) { + // OnCacheEntryVisitCompleted() is not called when we do not iterate + // cache entries. Since this moves forward to the next storage in + // the list we want to visit, artificially call it here. + OnCacheEntryVisitCompleted(); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsAboutCache::Channel::OnCacheEntryInfo(nsIURI* aURI, + const nsACString& aIdEnhance, + int64_t aDataSize, int32_t aFetchCount, + uint32_t aLastModified, + uint32_t aExpirationTime, bool aPinned, + nsILoadContextInfo* aInfo) { + // We need mStream for this + if (!mStream || mCancel) { + // Returning a failure from this callback stops the iteration + return NS_ERROR_FAILURE; + } + + if (!mEntriesHeaderAdded) { + mBuffer.AppendLiteral( + "
\n" + "\n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n"); + mEntriesHeaderAdded = true; + } + + // Generate a about:cache-entry URL for this entry... + + nsAutoCString url; + url.AssignLiteral("about:cache-entry?storage="); + nsAppendEscapedHTML(mStorageName, url); + + nsAutoCString context; + CacheFileUtils::AppendKeyPrefix(aInfo, context); + url.AppendLiteral("&context="); + nsAppendEscapedHTML(context, url); + + url.AppendLiteral("&eid="); + nsAppendEscapedHTML(aIdEnhance, url); + + nsAutoCString cacheUriSpec; + aURI->GetAsciiSpec(cacheUriSpec); + nsAutoCString escapedCacheURI; + nsAppendEscapedHTML(cacheUriSpec, escapedCacheURI); + url.AppendLiteral("&uri="); + url += escapedCacheURI; + + // Entry start... + mBuffer.AppendLiteral(" \n"); + + // URI + mBuffer.AppendLiteral(" \n"); + + // Content length + mBuffer.AppendLiteral(" \n"); + + // Number of accesses + mBuffer.AppendLiteral(" \n"); + + // vars for reporting time + char buf[255]; + + // Last modified time + mBuffer.AppendLiteral(" \n"); + + // Expires time + mBuffer.AppendLiteral(" \n"); + + // Pinning + mBuffer.AppendLiteral(" \n"); + + // Entry is done... + mBuffer.AppendLiteral(" \n"); + + return FlushBuffer(); +} + +NS_IMETHODIMP +nsAboutCache::Channel::OnCacheEntryVisitCompleted() { + if (!mStream) { + return NS_ERROR_FAILURE; + } + + if (mEntriesHeaderAdded) { + mBuffer.AppendLiteral("
KeyData sizeFetch countLast ModifedExpiresPinning
"); + if (!aIdEnhance.IsEmpty()) { + nsAppendEscapedHTML(aIdEnhance, mBuffer); + mBuffer.Append(':'); + } + mBuffer.Append(escapedCacheURI); + mBuffer.AppendLiteral(""); + + if (!context.IsEmpty()) { + mBuffer.AppendLiteral("
"); + nsAutoCString escapedContext; + nsAppendEscapedHTML(context, escapedContext); + mBuffer.Append(escapedContext); + mBuffer.AppendLiteral(""); + } + + mBuffer.AppendLiteral("
"); + mBuffer.AppendInt(aDataSize); + mBuffer.AppendLiteral(" bytes"); + mBuffer.AppendInt(aFetchCount); + mBuffer.AppendLiteral(""); + if (aLastModified) { + PrintTimeString(buf, sizeof(buf), aLastModified); + mBuffer.Append(buf); + } else { + mBuffer.AppendLiteral("No last modified time"); + } + mBuffer.AppendLiteral(""); + + // Bug - 633747. + // When expiration time is 0, we show 1970-01-01 01:00:00 which is confusing. + // So we check if time is 0, then we show a message, "Expired Immediately" + if (aExpirationTime == 0) { + mBuffer.AppendLiteral("Expired Immediately"); + } else if (aExpirationTime < 0xFFFFFFFF) { + PrintTimeString(buf, sizeof(buf), aExpirationTime); + mBuffer.Append(buf); + } else { + mBuffer.AppendLiteral("No expiration time"); + } + mBuffer.AppendLiteral(""); + if (aPinned) { + mBuffer.AppendLiteral("Pinned"); + } else { + mBuffer.AppendLiteral(" "); + } + mBuffer.AppendLiteral("
\n"); + } + + // Kick another storage visiting (from a storage that allows us.) + while (mStorageList.Length()) { + nsresult rv = VisitNextStorage(); + if (NS_SUCCEEDED(rv)) { + // Expecting new round of OnCache* calls. + return NS_OK; + } + } + + // We are done! + mBuffer.AppendLiteral( + "\n" + "\n"); + nsresult rv = FlushBuffer(); + if (NS_FAILED(rv)) { + NS_WARNING("Failed to flush buffer"); + } + mStream->Close(); + + return NS_OK; +} + +nsresult nsAboutCache::Channel::FlushBuffer() { + nsresult rv; + + uint32_t bytesWritten; + rv = mStream->Write(mBuffer.get(), mBuffer.Length(), &bytesWritten); + mBuffer.Truncate(); + + if (NS_FAILED(rv)) { + mCancel = true; + } + + return rv; +} + +NS_IMETHODIMP +nsAboutCache::GetURIFlags(nsIURI* aURI, uint32_t* result) { + *result = nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT; + return NS_OK; +} + +// static +nsresult nsAboutCache::Create(nsISupports* aOuter, REFNSIID aIID, + void** aResult) { + RefPtr about = new nsAboutCache(); + return about->QueryInterface(aIID, aResult); +} + +NS_IMETHODIMP +nsAboutCache::GetChromeURI(nsIURI* aURI, nsIURI** chromeURI) { + return NS_ERROR_ILLEGAL_VALUE; +} + +//////////////////////////////////////////////////////////////////////////////// diff --git a/netwerk/protocol/about/nsAboutCache.h b/netwerk/protocol/about/nsAboutCache.h new file mode 100644 index 0000000000..2c7064d89e --- /dev/null +++ b/netwerk/protocol/about/nsAboutCache.h @@ -0,0 +1,193 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef nsAboutCache_h__ +#define nsAboutCache_h__ + +#include "nsIAboutModule.h" +#include "nsICacheStorageVisitor.h" +#include "nsICacheStorage.h" + +#include "nsString.h" +#include "nsIChannel.h" +#include "nsIOutputStream.h" +#include "nsILoadContextInfo.h" + +#include "nsCOMPtr.h" +#include "nsTArray.h" + +#define NS_FORWARD_SAFE_NSICHANNEL_SUBSET(_to) \ + NS_IMETHOD GetOriginalURI(nsIURI** aOriginalURI) override { \ + return !_to ? NS_ERROR_NULL_POINTER : _to->GetOriginalURI(aOriginalURI); \ + } \ + NS_IMETHOD SetOriginalURI(nsIURI* aOriginalURI) override { \ + return !_to ? NS_ERROR_NULL_POINTER : _to->SetOriginalURI(aOriginalURI); \ + } \ + NS_IMETHOD GetURI(nsIURI** aURI) override { \ + return !_to ? NS_ERROR_NULL_POINTER : _to->GetURI(aURI); \ + } \ + NS_IMETHOD GetOwner(nsISupports** aOwner) override { \ + return !_to ? NS_ERROR_NULL_POINTER : _to->GetOwner(aOwner); \ + } \ + NS_IMETHOD SetOwner(nsISupports* aOwner) override { \ + return !_to ? NS_ERROR_NULL_POINTER : _to->SetOwner(aOwner); \ + } \ + NS_IMETHOD GetNotificationCallbacks( \ + nsIInterfaceRequestor** aNotificationCallbacks) override { \ + return !_to ? NS_ERROR_NULL_POINTER \ + : _to->GetNotificationCallbacks(aNotificationCallbacks); \ + } \ + NS_IMETHOD SetNotificationCallbacks( \ + nsIInterfaceRequestor* aNotificationCallbacks) override { \ + return !_to ? NS_ERROR_NULL_POINTER \ + : _to->SetNotificationCallbacks(aNotificationCallbacks); \ + } \ + NS_IMETHOD GetSecurityInfo(nsISupports** aSecurityInfo) override { \ + return !_to ? NS_ERROR_NULL_POINTER : _to->GetSecurityInfo(aSecurityInfo); \ + } \ + NS_IMETHOD GetContentType(nsACString& aContentType) override { \ + return !_to ? NS_ERROR_NULL_POINTER : _to->GetContentType(aContentType); \ + } \ + NS_IMETHOD SetContentType(const nsACString& aContentType) override { \ + return !_to ? NS_ERROR_NULL_POINTER : _to->SetContentType(aContentType); \ + } \ + NS_IMETHOD GetContentCharset(nsACString& aContentCharset) override { \ + return !_to ? NS_ERROR_NULL_POINTER \ + : _to->GetContentCharset(aContentCharset); \ + } \ + NS_IMETHOD SetContentCharset(const nsACString& aContentCharset) override { \ + return !_to ? NS_ERROR_NULL_POINTER \ + : _to->SetContentCharset(aContentCharset); \ + } \ + NS_IMETHOD GetContentLength(int64_t* aContentLength) override { \ + return !_to ? NS_ERROR_NULL_POINTER \ + : _to->GetContentLength(aContentLength); \ + } \ + NS_IMETHOD SetContentLength(int64_t aContentLength) override { \ + return !_to ? NS_ERROR_NULL_POINTER \ + : _to->SetContentLength(aContentLength); \ + } \ + NS_IMETHOD GetContentDisposition(uint32_t* aContentDisposition) override { \ + return !_to ? NS_ERROR_NULL_POINTER \ + : _to->GetContentDisposition(aContentDisposition); \ + } \ + NS_IMETHOD SetContentDisposition(uint32_t aContentDisposition) override { \ + return !_to ? NS_ERROR_NULL_POINTER \ + : _to->SetContentDisposition(aContentDisposition); \ + } \ + NS_IMETHOD GetContentDispositionFilename( \ + nsAString& aContentDispositionFilename) override { \ + return !_to ? NS_ERROR_NULL_POINTER \ + : _to->GetContentDispositionFilename( \ + aContentDispositionFilename); \ + } \ + NS_IMETHOD SetContentDispositionFilename( \ + const nsAString& aContentDispositionFilename) override { \ + return !_to ? NS_ERROR_NULL_POINTER \ + : _to->SetContentDispositionFilename( \ + aContentDispositionFilename); \ + } \ + NS_IMETHOD GetContentDispositionHeader( \ + nsACString& aContentDispositionHeader) override { \ + return !_to ? NS_ERROR_NULL_POINTER \ + : _to->GetContentDispositionHeader(aContentDispositionHeader); \ + } \ + NS_IMETHOD GetLoadInfo(nsILoadInfo** aLoadInfo) override { \ + return !_to ? NS_ERROR_NULL_POINTER : _to->GetLoadInfo(aLoadInfo); \ + } \ + NS_IMETHOD SetLoadInfo(nsILoadInfo* aLoadInfo) override { \ + return !_to ? NS_ERROR_NULL_POINTER : _to->SetLoadInfo(aLoadInfo); \ + } \ + NS_IMETHOD GetIsDocument(bool* aIsDocument) override { \ + return !_to ? NS_ERROR_NULL_POINTER : _to->GetIsDocument(aIsDocument); \ + } \ + NS_IMETHOD GetCanceled(bool* aCanceled) override { \ + return !_to ? NS_ERROR_NULL_POINTER : _to->GetCanceled(aCanceled); \ + }; + +class nsAboutCache final : public nsIAboutModule { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIABOUTMODULE + + nsAboutCache() = default; + + [[nodiscard]] static nsresult Create(nsISupports* aOuter, REFNSIID aIID, + void** aResult); + + [[nodiscard]] static nsresult GetStorage(nsACString const& storageName, + nsILoadContextInfo* loadInfo, + nsICacheStorage** storage); + + protected: + virtual ~nsAboutCache() = default; + + class Channel final : public nsIChannel, public nsICacheStorageVisitor { + NS_DECL_ISUPPORTS + NS_DECL_NSICACHESTORAGEVISITOR + NS_FORWARD_SAFE_NSIREQUEST(mChannel) + NS_FORWARD_SAFE_NSICHANNEL_SUBSET(mChannel) + NS_IMETHOD AsyncOpen(nsIStreamListener* aListener) override; + NS_IMETHOD Open(nsIInputStream** _retval) override; + + private: + virtual ~Channel() = default; + + public: + [[nodiscard]] nsresult Init(nsIURI* aURI, nsILoadInfo* aLoadInfo); + [[nodiscard]] nsresult ParseURI(nsIURI* uri, nsACString& storage); + + // Finds a next storage we wish to visit (we use this method + // even there is a specified storage name, which is the only + // one in the list then.) Posts FireVisitStorage() when found. + [[nodiscard]] nsresult VisitNextStorage(); + // Helper method that calls VisitStorage() for the current storage. + // When it fails, OnCacheEntryVisitCompleted is simulated to close + // the output stream and thus the about:cache channel. + void FireVisitStorage(); + // Kiks the visit cycle for the given storage, names can be: + // "disk", "memory", "appcache" + // Note: any newly added storage type has to be manually handled here. + [[nodiscard]] nsresult VisitStorage(nsACString const& storageName); + + // Writes content of mBuffer to mStream and truncates + // the buffer. It may fail when the input stream is closed by canceling + // the input stream channel. It can be used to stop the cache iteration + // process. + [[nodiscard]] nsresult FlushBuffer(); + + // Whether we are showing overview status of all available + // storages. + bool mOverview; + + // Flag initially false, that indicates the entries header has + // been added to the output HTML. + bool mEntriesHeaderAdded; + + // Cancelation flag + bool mCancel; + + // The list of all storage names we want to visit + nsTArray mStorageList; + nsCString mStorageName; + nsCOMPtr mStorage; + + // Output data buffering and streaming output + nsCString mBuffer; + nsCOMPtr mStream; + + // The input stream channel, the one that actually does the job + nsCOMPtr mChannel; + }; +}; + +#define NS_ABOUT_CACHE_MODULE_CID \ + { /* 9158c470-86e4-11d4-9be2-00e09872a416 */ \ + 0x9158c470, 0x86e4, 0x11d4, { \ + 0x9b, 0xe2, 0x00, 0xe0, 0x98, 0x72, 0xa4, 0x16 \ + } \ + } + +#endif // nsAboutCache_h__ diff --git a/netwerk/protocol/about/nsAboutCacheEntry.cpp b/netwerk/protocol/about/nsAboutCacheEntry.cpp new file mode 100644 index 0000000000..f416852a0e --- /dev/null +++ b/netwerk/protocol/about/nsAboutCacheEntry.cpp @@ -0,0 +1,557 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 "nsAboutCacheEntry.h" + +#include "mozilla/Sprintf.h" + +#include "nsAboutCache.h" +#include "nsICacheStorage.h" +#include "CacheObserver.h" +#include "nsNetUtil.h" +#include "nsEscape.h" +#include "nsIAsyncInputStream.h" +#include "nsIAsyncOutputStream.h" +#include "nsAboutProtocolUtils.h" +#include "nsContentUtils.h" +#include "nsInputStreamPump.h" +#include "CacheFileUtils.h" +#include + +using namespace mozilla::net; + +#define HEXDUMP_MAX_ROWS 16 + +static void HexDump(uint32_t* state, const char* buf, int32_t n, + nsCString& result) { + char temp[16]; + + const unsigned char* p; + while (n) { + SprintfLiteral(temp, "%08x: ", *state); + result.Append(temp); + *state += HEXDUMP_MAX_ROWS; + + p = (const unsigned char*)buf; + + int32_t i, row_max = std::min(HEXDUMP_MAX_ROWS, n); + + // print hex codes: + for (i = 0; i < row_max; ++i) { + SprintfLiteral(temp, "%02x ", *p++); + result.Append(temp); + } + for (i = row_max; i < HEXDUMP_MAX_ROWS; ++i) { + result.AppendLiteral(" "); + } + + // print ASCII glyphs if possible: + p = (const unsigned char*)buf; + for (i = 0; i < row_max; ++i, ++p) { + switch (*p) { + case '<': + result.AppendLiteral("<"); + break; + case '>': + result.AppendLiteral(">"); + break; + case '&': + result.AppendLiteral("&"); + break; + default: + if (*p < 0x7F && *p > 0x1F) { + result.Append(*p); + } else { + result.Append('.'); + } + } + } + + result.Append('\n'); + + buf += row_max; + n -= row_max; + } +} + +//----------------------------------------------------------------------------- +// nsAboutCacheEntry::nsISupports + +NS_IMPL_ISUPPORTS(nsAboutCacheEntry, nsIAboutModule) +NS_IMPL_ISUPPORTS(nsAboutCacheEntry::Channel, nsICacheEntryOpenCallback, + nsICacheEntryMetaDataVisitor, nsIStreamListener, nsIRequest, + nsIChannel) + +//----------------------------------------------------------------------------- +// nsAboutCacheEntry::nsIAboutModule + +NS_IMETHODIMP +nsAboutCacheEntry::NewChannel(nsIURI* uri, nsILoadInfo* aLoadInfo, + nsIChannel** result) { + NS_ENSURE_ARG_POINTER(uri); + nsresult rv; + + RefPtr channel = new Channel(); + rv = channel->Init(uri, aLoadInfo); + if (NS_FAILED(rv)) return rv; + + channel.forget(result); + + return NS_OK; +} + +NS_IMETHODIMP +nsAboutCacheEntry::GetURIFlags(nsIURI* aURI, uint32_t* result) { + *result = nsIAboutModule::HIDE_FROM_ABOUTABOUT | + nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT; + return NS_OK; +} + +NS_IMETHODIMP +nsAboutCacheEntry::GetChromeURI(nsIURI* aURI, nsIURI** chromeURI) { + return NS_ERROR_ILLEGAL_VALUE; +} + +//----------------------------------------------------------------------------- +// nsAboutCacheEntry::Channel + +nsresult nsAboutCacheEntry::Channel::Init(nsIURI* uri, nsILoadInfo* aLoadInfo) { + nsresult rv; + + nsCOMPtr stream; + rv = GetContentStream(uri, getter_AddRefs(stream)); + if (NS_FAILED(rv)) return rv; + + rv = NS_NewInputStreamChannelInternal(getter_AddRefs(mChannel), uri, + stream.forget(), "text/html"_ns, + "utf-8"_ns, aLoadInfo); + if (NS_FAILED(rv)) return rv; + + return NS_OK; +} + +nsresult nsAboutCacheEntry::Channel::GetContentStream(nsIURI* uri, + nsIInputStream** result) { + nsresult rv; + + // Init: (block size, maximum length) + nsCOMPtr inputStream; + rv = NS_NewPipe2(getter_AddRefs(inputStream), getter_AddRefs(mOutputStream), + true, false, 256, UINT32_MAX); + if (NS_FAILED(rv)) return rv; + + constexpr auto buffer = + "\n" + "\n" + "\n" + " \n" + " Cache entry information\n" + " \n" + " \n" + "\n" + "\n" + "

Cache entry information

\n"_ns; + uint32_t n; + rv = mOutputStream->Write(buffer.get(), buffer.Length(), &n); + if (NS_FAILED(rv)) return rv; + if (n != buffer.Length()) return NS_ERROR_UNEXPECTED; + + rv = OpenCacheEntry(uri); + if (NS_FAILED(rv)) return rv; + + inputStream.forget(result); + return NS_OK; +} + +nsresult nsAboutCacheEntry::Channel::OpenCacheEntry(nsIURI* uri) { + nsresult rv; + + rv = ParseURI(uri, mStorageName, getter_AddRefs(mLoadInfo), mEnhanceId, + getter_AddRefs(mCacheURI)); + if (NS_FAILED(rv)) return rv; + + return OpenCacheEntry(); +} + +nsresult nsAboutCacheEntry::Channel::OpenCacheEntry() { + nsresult rv; + + nsCOMPtr storage; + rv = nsAboutCache::GetStorage(mStorageName, mLoadInfo, + getter_AddRefs(storage)); + if (NS_FAILED(rv)) return rv; + + // Invokes OnCacheEntryAvailable() + rv = storage->AsyncOpenURI( + mCacheURI, mEnhanceId, + nsICacheStorage::OPEN_READONLY | nsICacheStorage::OPEN_SECRETLY, this); + if (NS_FAILED(rv)) return rv; + + return NS_OK; +} + +nsresult nsAboutCacheEntry::Channel::ParseURI(nsIURI* uri, + nsACString& storageName, + nsILoadContextInfo** loadInfo, + nsCString& enahnceID, + nsIURI** cacheUri) { + // + // about:cache-entry?storage=[string]&contenxt=[string]&eid=[string]&uri=[string] + // + nsresult rv; + + nsAutoCString path; + rv = uri->GetPathQueryRef(path); + if (NS_FAILED(rv)) return rv; + + nsACString::const_iterator keyBegin, keyEnd, valBegin, begin, end; + path.BeginReading(begin); + path.EndReading(end); + + keyBegin = begin; + keyEnd = end; + if (!FindInReadable("?storage="_ns, keyBegin, keyEnd)) + return NS_ERROR_FAILURE; + + valBegin = keyEnd; // the value of the storage key starts after the key + + keyBegin = keyEnd; + keyEnd = end; + if (!FindInReadable("&context="_ns, keyBegin, keyEnd)) + return NS_ERROR_FAILURE; + + storageName.Assign(Substring(valBegin, keyBegin)); + valBegin = keyEnd; // the value of the context key starts after the key + + keyBegin = keyEnd; + keyEnd = end; + if (!FindInReadable("&eid="_ns, keyBegin, keyEnd)) return NS_ERROR_FAILURE; + + nsAutoCString contextKey(Substring(valBegin, keyBegin)); + valBegin = keyEnd; // the value of the eid key starts after the key + + keyBegin = keyEnd; + keyEnd = end; + if (!FindInReadable("&uri="_ns, keyBegin, keyEnd)) return NS_ERROR_FAILURE; + + enahnceID.Assign(Substring(valBegin, keyBegin)); + + valBegin = keyEnd; // the value of the uri key starts after the key + nsAutoCString uriSpec(Substring(valBegin, end)); // uri is the last one + + // Uf... parsing done, now get some objects from it... + + nsCOMPtr info = CacheFileUtils::ParseKey(contextKey); + if (!info) return NS_ERROR_FAILURE; + info.forget(loadInfo); + + rv = NS_NewURI(cacheUri, uriSpec); + if (NS_FAILED(rv)) return rv; + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsICacheEntryOpenCallback implementation +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsAboutCacheEntry::Channel::OnCacheEntryCheck( + nsICacheEntry* aEntry, nsIApplicationCache* aApplicationCache, + uint32_t* result) { + *result = nsICacheEntryOpenCallback::ENTRY_WANTED; + return NS_OK; +} + +NS_IMETHODIMP +nsAboutCacheEntry::Channel::OnCacheEntryAvailable( + nsICacheEntry* entry, bool isNew, nsIApplicationCache* aApplicationCache, + nsresult status) { + nsresult rv; + + mWaitingForData = false; + if (entry) { + rv = WriteCacheEntryDescription(entry); + } else { + rv = WriteCacheEntryUnavailable(); + } + if (NS_FAILED(rv)) return rv; + + if (!mWaitingForData) { + // Data is not expected, close the output of content now. + CloseContent(); + } + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// Print-out helper methods +//----------------------------------------------------------------------------- + +#define APPEND_ROW(label, value) \ + PR_BEGIN_MACRO \ + buffer.AppendLiteral( \ + " \n" \ + " "); \ + buffer.AppendLiteral(label); \ + buffer.AppendLiteral( \ + ":\n" \ + " "); \ + buffer.Append(value); \ + buffer.AppendLiteral( \ + "\n" \ + " \n"); \ + PR_END_MACRO + +nsresult nsAboutCacheEntry::Channel::WriteCacheEntryDescription( + nsICacheEntry* entry) { + nsresult rv; + // This method appears to run in a situation where the run-time stack + // should have plenty of space, so allocating a large string on the + // stack is OK. + nsAutoCStringN<4097> buffer; + uint32_t n; + + nsAutoCString str; + + rv = entry->GetKey(str); + if (NS_FAILED(rv)) return rv; + + buffer.AssignLiteral( + "\n" + " \n" + " \n" + " \n" + " \n"); + + // temp vars for reporting + char timeBuf[255]; + uint32_t u = 0; + int32_t i = 0; + nsAutoCString s; + + // Fetch Count + s.Truncate(); + entry->GetFetchCount(&i); + s.AppendInt(i); + APPEND_ROW("fetch count", s); + + // Last Fetched + entry->GetLastFetched(&u); + if (u) { + PrintTimeString(timeBuf, sizeof(timeBuf), u); + APPEND_ROW("last fetched", timeBuf); + } else { + APPEND_ROW("last fetched", "No last fetch time (bug 1000338)"); + } + + // Last Modified + entry->GetLastModified(&u); + if (u) { + PrintTimeString(timeBuf, sizeof(timeBuf), u); + APPEND_ROW("last modified", timeBuf); + } else { + APPEND_ROW("last modified", "No last modified time (bug 1000338)"); + } + + // Expiration Time + entry->GetExpirationTime(&u); + + // Bug - 633747. + // When expiration time is 0, we show 1970-01-01 01:00:00 which is confusing. + // So we check if time is 0, then we show a message, "Expired Immediately" + if (u == 0) { + APPEND_ROW("expires", "Expired Immediately"); + } else if (u < 0xFFFFFFFF) { + PrintTimeString(timeBuf, sizeof(timeBuf), u); + APPEND_ROW("expires", timeBuf); + } else { + APPEND_ROW("expires", "No expiration time"); + } + + // Data Size + s.Truncate(); + uint32_t dataSize; + if (NS_FAILED(entry->GetStorageDataSize(&dataSize))) dataSize = 0; + s.AppendInt( + (int32_t)dataSize); // XXX nsICacheEntryInfo interfaces should be fixed. + s.AppendLiteral(" B"); + APPEND_ROW("Data size", s); + + // TODO - mayhemer + // Here used to be a link to the disk file (in the old cache for entries that + // did not fit any of the block files, in the new cache every time). + // I'd rather have a small set of buttons here to action on the entry: + // 1. save the content + // 2. save as a complete HTTP response (response head, headers, content) + // 3. doom the entry + // A new bug(s) should be filed here. + + // Security Info + nsCOMPtr securityInfo; + entry->GetSecurityInfo(getter_AddRefs(securityInfo)); + if (securityInfo) { + APPEND_ROW("Security", "This is a secure document."); + } else { + APPEND_ROW( + "Security", + "This document does not have any security info associated with it."); + } + + buffer.AppendLiteral( + "
key:"); + + // Test if the key is actually a URI + nsCOMPtr uri; + rv = NS_NewURI(getter_AddRefs(uri), str); + + nsAutoCString escapedStr; + nsAppendEscapedHTML(str, escapedStr); + + // javascript: and data: URLs should not be linkified + // since clicking them can cause scripts to run - bug 162584 + if (NS_SUCCEEDED(rv) && + !(uri->SchemeIs("javascript") || uri->SchemeIs("data"))) { + buffer.AppendLiteral(""); + buffer.Append(escapedStr); + buffer.AppendLiteral(""); + uri = nullptr; + } else { + buffer.Append(escapedStr); + } + buffer.AppendLiteral( + "
\n" + "
\n" + "\n"); + + mBuffer = &buffer; // make it available for OnMetaDataElement(). + entry->VisitMetaData(this); + mBuffer = nullptr; + + buffer.AppendLiteral("
\n"); + mOutputStream->Write(buffer.get(), buffer.Length(), &n); + buffer.Truncate(); + + // Provide a hexdump of the data + if (!dataSize) { + return NS_OK; + } + + nsCOMPtr stream; + entry->OpenInputStream(0, getter_AddRefs(stream)); + if (!stream) { + return NS_OK; + } + + RefPtr pump; + rv = nsInputStreamPump::Create(getter_AddRefs(pump), stream); + if (NS_FAILED(rv)) { + return NS_OK; // just ignore + } + + rv = pump->AsyncRead(this); + if (NS_FAILED(rv)) { + return NS_OK; // just ignore + } + + mWaitingForData = true; + return NS_OK; +} + +nsresult nsAboutCacheEntry::Channel::WriteCacheEntryUnavailable() { + uint32_t n; + constexpr auto buffer = "The cache entry you selected is not available."_ns; + mOutputStream->Write(buffer.get(), buffer.Length(), &n); + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsICacheEntryMetaDataVisitor implementation +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsAboutCacheEntry::Channel::OnMetaDataElement(char const* key, + char const* value) { + mBuffer->AppendLiteral( + " \n" + " "); + mBuffer->Append(key); + mBuffer->AppendLiteral( + ":\n" + " "); + nsAppendEscapedHTML(nsDependentCString(value), *mBuffer); + mBuffer->AppendLiteral( + "\n" + " \n"); + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsIStreamListener implementation +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsAboutCacheEntry::Channel::OnStartRequest(nsIRequest* request) { + mHexDumpState = 0; + + constexpr auto buffer = "
\n
"_ns;
+  uint32_t n;
+  return mOutputStream->Write(buffer.get(), buffer.Length(), &n);
+}
+
+NS_IMETHODIMP
+nsAboutCacheEntry::Channel::OnDataAvailable(nsIRequest* request,
+                                            nsIInputStream* aInputStream,
+                                            uint64_t aOffset, uint32_t aCount) {
+  uint32_t n;
+  return aInputStream->ReadSegments(&nsAboutCacheEntry::Channel::PrintCacheData,
+                                    this, aCount, &n);
+}
+
+/* static */
+nsresult nsAboutCacheEntry::Channel::PrintCacheData(
+    nsIInputStream* aInStream, void* aClosure, const char* aFromSegment,
+    uint32_t aToOffset, uint32_t aCount, uint32_t* aWriteCount) {
+  nsAboutCacheEntry::Channel* a =
+      static_cast(aClosure);
+
+  nsCString buffer;
+  HexDump(&a->mHexDumpState, aFromSegment, aCount, buffer);
+
+  uint32_t n;
+  a->mOutputStream->Write(buffer.get(), buffer.Length(), &n);
+
+  *aWriteCount = aCount;
+
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAboutCacheEntry::Channel::OnStopRequest(nsIRequest* request,
+                                          nsresult result) {
+  constexpr auto buffer = "
\n"_ns; + uint32_t n; + mOutputStream->Write(buffer.get(), buffer.Length(), &n); + + CloseContent(); + + return NS_OK; +} + +void nsAboutCacheEntry::Channel::CloseContent() { + constexpr auto buffer = "\n\n"_ns; + uint32_t n; + mOutputStream->Write(buffer.get(), buffer.Length(), &n); + + mOutputStream->Close(); + mOutputStream = nullptr; +} diff --git a/netwerk/protocol/about/nsAboutCacheEntry.h b/netwerk/protocol/about/nsAboutCacheEntry.h new file mode 100644 index 0000000000..4c4e6c2ee8 --- /dev/null +++ b/netwerk/protocol/about/nsAboutCacheEntry.h @@ -0,0 +1,85 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef nsAboutCacheEntry_h__ +#define nsAboutCacheEntry_h__ + +#include "nsIAboutModule.h" +#include "nsICacheEntryOpenCallback.h" +#include "nsICacheEntry.h" +#include "nsIStreamListener.h" +#include "nsString.h" +#include "nsCOMPtr.h" + +class nsIAsyncOutputStream; +class nsIInputStream; +class nsILoadContextInfo; +class nsIURI; + +class nsAboutCacheEntry final : public nsIAboutModule { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIABOUTMODULE + + private: + virtual ~nsAboutCacheEntry() = default; + + class Channel final : public nsICacheEntryOpenCallback, + public nsICacheEntryMetaDataVisitor, + public nsIStreamListener, + public nsIChannel { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSICACHEENTRYOPENCALLBACK + NS_DECL_NSICACHEENTRYMETADATAVISITOR + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMLISTENER + NS_FORWARD_SAFE_NSICHANNEL(mChannel) + NS_FORWARD_SAFE_NSIREQUEST(mChannel) + + Channel() : mBuffer(nullptr), mWaitingForData(false), mHexDumpState(0) {} + + private: + virtual ~Channel() = default; + + public: + [[nodiscard]] nsresult Init(nsIURI* uri, nsILoadInfo* aLoadInfo); + + [[nodiscard]] nsresult GetContentStream(nsIURI*, nsIInputStream**); + [[nodiscard]] nsresult OpenCacheEntry(nsIURI*); + [[nodiscard]] nsresult OpenCacheEntry(); + [[nodiscard]] nsresult WriteCacheEntryDescription(nsICacheEntry*); + [[nodiscard]] nsresult WriteCacheEntryUnavailable(); + [[nodiscard]] nsresult ParseURI(nsIURI* uri, nsACString& storageName, + nsILoadContextInfo** loadInfo, + nsCString& enahnceID, nsIURI** cacheUri); + void CloseContent(); + + [[nodiscard]] static nsresult PrintCacheData( + nsIInputStream* aInStream, void* aClosure, const char* aFromSegment, + uint32_t aToOffset, uint32_t aCount, uint32_t* aWriteCount); + + private: + nsCString mStorageName, mEnhanceId; + nsCOMPtr mLoadInfo; + nsCOMPtr mCacheURI; + + nsCString* mBuffer; + nsCOMPtr mOutputStream; + bool mWaitingForData; + uint32_t mHexDumpState; + + nsCOMPtr mChannel; + }; +}; + +#define NS_ABOUT_CACHE_ENTRY_MODULE_CID \ + { /* 7fa5237d-b0eb-438f-9e50-ca0166e63788 */ \ + 0x7fa5237d, 0xb0eb, 0x438f, { \ + 0x9e, 0x50, 0xca, 0x01, 0x66, 0xe6, 0x37, 0x88 \ + } \ + } + +#endif // nsAboutCacheEntry_h__ diff --git a/netwerk/protocol/about/nsAboutProtocolHandler.cpp b/netwerk/protocol/about/nsAboutProtocolHandler.cpp new file mode 100644 index 0000000000..de8a83a5df --- /dev/null +++ b/netwerk/protocol/about/nsAboutProtocolHandler.cpp @@ -0,0 +1,445 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 "base/basictypes.h" +#include "mozilla/ArrayUtils.h" + +#include "nsAboutProtocolHandler.h" +#include "nsIURI.h" +#include "nsIAboutModule.h" +#include "nsString.h" +#include "nsNetCID.h" +#include "nsAboutProtocolUtils.h" +#include "nsError.h" +#include "nsNetUtil.h" +#include "nsIObjectInputStream.h" +#include "nsIObjectOutputStream.h" +#include "nsIWritablePropertyBag2.h" +#include "nsIChannel.h" +#include "nsIScriptError.h" +#include "nsIClassInfoImpl.h" + +#include "mozilla/ipc/URIUtils.h" + +namespace mozilla { +namespace net { + +static NS_DEFINE_CID(kSimpleURICID, NS_SIMPLEURI_CID); +static NS_DEFINE_CID(kNestedAboutURICID, NS_NESTEDABOUTURI_CID); + +static bool IsSafeForUntrustedContent(nsIAboutModule* aModule, nsIURI* aURI) { + uint32_t flags; + nsresult rv = aModule->GetURIFlags(aURI, &flags); + NS_ENSURE_SUCCESS(rv, false); + + return (flags & nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT) != 0; +} + +static bool IsSafeToLinkForUntrustedContent(nsIURI* aURI) { + nsAutoCString path; + aURI->GetPathQueryRef(path); + + int32_t f = path.FindChar('#'); + if (f >= 0) { + path.SetLength(f); + } + + f = path.FindChar('?'); + if (f >= 0) { + path.SetLength(f); + } + + ToLowerCase(path); + + // The about modules for these URL types have the + // URI_SAFE_FOR_UNTRUSTED_CONTENT and MAKE_LINKABLE flags set. + if (path.EqualsLiteral("blank") || path.EqualsLiteral("logo") || + path.EqualsLiteral("srcdoc")) { + return true; + } + + return false; +} + +//////////////////////////////////////////////////////////////////////////////// + +NS_IMPL_ISUPPORTS(nsAboutProtocolHandler, nsIProtocolHandler, + nsIProtocolHandlerWithDynamicFlags, nsISupportsWeakReference) + +//////////////////////////////////////////////////////////////////////////////// +// nsIProtocolHandler methods: + +NS_IMETHODIMP +nsAboutProtocolHandler::GetScheme(nsACString& result) { + result.AssignLiteral("about"); + return NS_OK; +} + +NS_IMETHODIMP +nsAboutProtocolHandler::GetDefaultPort(int32_t* result) { + *result = -1; // no port for about: URLs + return NS_OK; +} + +NS_IMETHODIMP +nsAboutProtocolHandler::GetProtocolFlags(uint32_t* result) { + *result = URI_NORELATIVE | URI_NOAUTH | URI_DANGEROUS_TO_LOAD | + URI_SCHEME_NOT_SELF_LINKABLE; + return NS_OK; +} + +NS_IMETHODIMP +nsAboutProtocolHandler::GetFlagsForURI(nsIURI* aURI, uint32_t* aFlags) { + // First use the default (which is "unsafe for content"): + GetProtocolFlags(aFlags); + + // Now try to see if this URI overrides the default: + nsCOMPtr aboutMod; + nsresult rv = NS_GetAboutModule(aURI, getter_AddRefs(aboutMod)); + if (NS_FAILED(rv)) { + // Swallow this and just tell the consumer the default: + return NS_OK; + } + uint32_t aboutModuleFlags = 0; + rv = aboutMod->GetURIFlags(aURI, &aboutModuleFlags); + // This should never happen, so pass back the error: + NS_ENSURE_SUCCESS(rv, rv); + + // Secure (https) pages can load safe about pages without becoming + // mixed content. + if (aboutModuleFlags & nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT) { + *aFlags |= URI_IS_POTENTIALLY_TRUSTWORTHY; + // about: pages can only be loaded by unprivileged principals + // if they are marked as LINKABLE + if (aboutModuleFlags & nsIAboutModule::MAKE_LINKABLE) { + // Replace URI_DANGEROUS_TO_LOAD with URI_LOADABLE_BY_ANYONE. + *aFlags &= ~URI_DANGEROUS_TO_LOAD; + *aFlags |= URI_LOADABLE_BY_ANYONE; + } + } + return NS_OK; +} + +// static +nsresult nsAboutProtocolHandler::CreateNewURI(const nsACString& aSpec, + const char* aCharset, + nsIURI* aBaseURI, + nsIURI** aResult) { + *aResult = nullptr; + nsresult rv; + + // Use a simple URI to parse out some stuff first + nsCOMPtr url; + rv = NS_MutateURI(new nsSimpleURI::Mutator()).SetSpec(aSpec).Finalize(url); + + if (NS_FAILED(rv)) { + return rv; + } + + if (IsSafeToLinkForUntrustedContent(url)) { + // We need to indicate that this baby is safe. Use an inner URI that + // no one but the security manager will see. Make sure to preserve our + // path, in case someone decides to hardcode checks for particular + // about: URIs somewhere. + nsAutoCString spec; + rv = url->GetPathQueryRef(spec); + NS_ENSURE_SUCCESS(rv, rv); + + spec.InsertLiteral("moz-safe-about:", 0); + + nsCOMPtr inner; + rv = NS_NewURI(getter_AddRefs(inner), spec); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr base(aBaseURI); + rv = NS_MutateURI(new nsNestedAboutURI::Mutator()) + .Apply(NS_MutatorMethod(&nsINestedAboutURIMutator::InitWithBase, + inner, base)) + .SetSpec(aSpec) + .Finalize(url); + NS_ENSURE_SUCCESS(rv, rv); + } + + url.swap(*aResult); + return NS_OK; +} + +NS_IMETHODIMP +nsAboutProtocolHandler::NewChannel(nsIURI* uri, nsILoadInfo* aLoadInfo, + nsIChannel** result) { + NS_ENSURE_ARG_POINTER(uri); + + // about:what you ask? + nsCOMPtr aboutMod; + nsresult rv = NS_GetAboutModule(uri, getter_AddRefs(aboutMod)); + + nsAutoCString path; + nsresult rv2 = NS_GetAboutModuleName(uri, path); + if (NS_SUCCEEDED(rv2) && path.EqualsLiteral("srcdoc")) { + // about:srcdoc is meant to be unresolvable, yet is included in the + // about lookup tables so that it can pass security checks when used in + // a srcdoc iframe. To ensure that it stays unresolvable, we pretend + // that it doesn't exist. + rv = NS_ERROR_FACTORY_NOT_REGISTERED; + } + + if (NS_SUCCEEDED(rv)) { + // The standard return case: + rv = aboutMod->NewChannel(uri, aLoadInfo, result); + if (NS_SUCCEEDED(rv)) { + // Not all implementations of nsIAboutModule::NewChannel() + // set the LoadInfo on the newly created channel yet, as + // an interim solution we set the LoadInfo here if not + // available on the channel. Bug 1087720 + nsCOMPtr loadInfo = (*result)->LoadInfo(); + if (aLoadInfo != loadInfo) { + NS_ASSERTION(false, + "nsIAboutModule->newChannel(aURI, aLoadInfo) needs to " + "set LoadInfo"); + AutoTArray params = { + u"nsIAboutModule->newChannel(aURI)"_ns, + u"nsIAboutModule->newChannel(aURI, aLoadInfo)"_ns}; + nsContentUtils::ReportToConsole( + nsIScriptError::warningFlag, "Security by Default"_ns, + nullptr, // aDocument + nsContentUtils::eNECKO_PROPERTIES, "APIDeprecationWarning", params); + (*result)->SetLoadInfo(aLoadInfo); + } + + // If this URI is safe for untrusted content, enforce that its + // principal be based on the channel's originalURI by setting the + // owner to null. + // Note: this relies on aboutMod's newChannel implementation + // having set the proper originalURI, which probably isn't ideal. + if (IsSafeForUntrustedContent(aboutMod, uri)) { + (*result)->SetOwner(nullptr); + } + + RefPtr aboutURI; + nsresult rv2 = + uri->QueryInterface(kNestedAboutURICID, getter_AddRefs(aboutURI)); + if (NS_SUCCEEDED(rv2) && aboutURI->GetBaseURI()) { + nsCOMPtr writableBag = + do_QueryInterface(*result); + if (writableBag) { + writableBag->SetPropertyAsInterface(u"baseURI"_ns, + aboutURI->GetBaseURI()); + } + } + } + return rv; + } + + // mumble... + + if (rv == NS_ERROR_FACTORY_NOT_REGISTERED) { + // This looks like an about: we don't know about. Convert + // this to an invalid URI error. + rv = NS_ERROR_MALFORMED_URI; + } + + return rv; +} + +NS_IMETHODIMP +nsAboutProtocolHandler::AllowPort(int32_t port, const char* scheme, + bool* _retval) { + // don't override anything. + *_retval = false; + return NS_OK; +} + +//////////////////////////////////////////////////////////////////////////////// +// Safe about protocol handler impl + +NS_IMPL_ISUPPORTS(nsSafeAboutProtocolHandler, nsIProtocolHandler, + nsISupportsWeakReference) + +// nsIProtocolHandler methods: + +NS_IMETHODIMP +nsSafeAboutProtocolHandler::GetScheme(nsACString& result) { + result.AssignLiteral("moz-safe-about"); + return NS_OK; +} + +NS_IMETHODIMP +nsSafeAboutProtocolHandler::GetDefaultPort(int32_t* result) { + *result = -1; // no port for moz-safe-about: URLs + return NS_OK; +} + +NS_IMETHODIMP +nsSafeAboutProtocolHandler::GetProtocolFlags(uint32_t* result) { + *result = URI_NORELATIVE | URI_NOAUTH | URI_LOADABLE_BY_ANYONE | + URI_IS_POTENTIALLY_TRUSTWORTHY; + return NS_OK; +} + +NS_IMETHODIMP +nsSafeAboutProtocolHandler::NewChannel(nsIURI* uri, nsILoadInfo* aLoadInfo, + nsIChannel** result) { + *result = nullptr; + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP +nsSafeAboutProtocolHandler::AllowPort(int32_t port, const char* scheme, + bool* _retval) { + // don't override anything. + *_retval = false; + return NS_OK; +} + +//////////////////////////////////////////////////////////// +// nsNestedAboutURI implementation + +NS_IMPL_CLASSINFO(nsNestedAboutURI, nullptr, nsIClassInfo::THREADSAFE, + NS_NESTEDABOUTURI_CID); +// Empty CI getter. We only need nsIClassInfo for Serialization +NS_IMPL_CI_INTERFACE_GETTER0(nsNestedAboutURI) + +NS_INTERFACE_MAP_BEGIN(nsNestedAboutURI) + if (aIID.Equals(kNestedAboutURICID)) + foundInterface = static_cast(this); + else + NS_IMPL_QUERY_CLASSINFO(nsNestedAboutURI) +NS_INTERFACE_MAP_END_INHERITING(nsSimpleNestedURI) + +// nsISerializable + +NS_IMETHODIMP +nsNestedAboutURI::Read(nsIObjectInputStream* aStream) { + MOZ_ASSERT_UNREACHABLE("Use nsIURIMutator.read() instead"); + return NS_ERROR_NOT_IMPLEMENTED; +} + +nsresult nsNestedAboutURI::ReadPrivate(nsIObjectInputStream* aStream) { + nsresult rv = nsSimpleNestedURI::ReadPrivate(aStream); + if (NS_FAILED(rv)) return rv; + + bool haveBase; + rv = aStream->ReadBoolean(&haveBase); + if (NS_FAILED(rv)) return rv; + + if (haveBase) { + nsCOMPtr supports; + rv = aStream->ReadObject(true, getter_AddRefs(supports)); + if (NS_FAILED(rv)) return rv; + + mBaseURI = do_QueryInterface(supports, &rv); + if (NS_FAILED(rv)) return rv; + } + + return NS_OK; +} + +NS_IMETHODIMP +nsNestedAboutURI::Write(nsIObjectOutputStream* aStream) { + nsresult rv = nsSimpleNestedURI::Write(aStream); + if (NS_FAILED(rv)) return rv; + + rv = aStream->WriteBoolean(mBaseURI != nullptr); + if (NS_FAILED(rv)) return rv; + + if (mBaseURI) { + // A previous iteration of this code wrote out mBaseURI as nsISupports + // and then read it in as nsIURI, which is non-kosher when mBaseURI + // implements more than just a single line of interfaces and the + // canonical nsISupports* isn't the one a static_cast<> of mBaseURI + // would produce. For backwards compatibility with existing + // serializations we continue to write mBaseURI as nsISupports but + // switch to reading it as nsISupports, with a post-read QI to get to + // nsIURI. + rv = aStream->WriteCompoundObject(mBaseURI, NS_GET_IID(nsISupports), true); + if (NS_FAILED(rv)) return rv; + } + + return NS_OK; +} + +NS_IMETHODIMP_(void) +nsNestedAboutURI::Serialize(mozilla::ipc::URIParams& aParams) { + using namespace mozilla::ipc; + + NestedAboutURIParams params; + URIParams nestedParams; + + nsSimpleNestedURI::Serialize(nestedParams); + params.nestedParams() = nestedParams; + + if (mBaseURI) { + SerializeURI(mBaseURI, params.baseURI()); + } + + aParams = params; +} + +bool nsNestedAboutURI::Deserialize(const mozilla::ipc::URIParams& aParams) { + using namespace mozilla::ipc; + + if (aParams.type() != URIParams::TNestedAboutURIParams) { + NS_ERROR("Received unknown parameters from the other process!"); + return false; + } + + const NestedAboutURIParams& params = aParams.get_NestedAboutURIParams(); + if (!nsSimpleNestedURI::Deserialize(params.nestedParams())) { + return false; + } + + mBaseURI = nullptr; + if (params.baseURI()) { + mBaseURI = DeserializeURI(*params.baseURI()); + } + return true; +} + +// nsSimpleURI +/* virtual */ nsSimpleURI* nsNestedAboutURI::StartClone( + nsSimpleURI::RefHandlingEnum aRefHandlingMode, const nsACString& aNewRef) { + // Sadly, we can't make use of nsSimpleNestedURI::StartClone here. + // However, this function is expected to exactly match that function, + // aside from the "new ns***URI()" call. + NS_ENSURE_TRUE(mInnerURI, nullptr); + + nsCOMPtr innerClone; + nsresult rv = NS_OK; + if (aRefHandlingMode == eHonorRef) { + innerClone = mInnerURI; + } else if (aRefHandlingMode == eReplaceRef) { + rv = NS_GetURIWithNewRef(mInnerURI, aNewRef, getter_AddRefs(innerClone)); + } else { + rv = NS_GetURIWithoutRef(mInnerURI, getter_AddRefs(innerClone)); + } + + if (NS_FAILED(rv)) { + return nullptr; + } + + nsNestedAboutURI* url = new nsNestedAboutURI(innerClone, mBaseURI); + SetRefOnClone(url, aRefHandlingMode, aNewRef); + + return url; +} + +// Queries this list of interfaces. If none match, it queries mURI. +NS_IMPL_NSIURIMUTATOR_ISUPPORTS(nsNestedAboutURI::Mutator, nsIURISetters, + nsIURIMutator, nsISerializable, + nsINestedAboutURIMutator) + +NS_IMETHODIMP +nsNestedAboutURI::Mutate(nsIURIMutator** aMutator) { + RefPtr mutator = new nsNestedAboutURI::Mutator(); + nsresult rv = mutator->InitFromURI(this); + if (NS_FAILED(rv)) { + return rv; + } + mutator.forget(aMutator); + return NS_OK; +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/about/nsAboutProtocolHandler.h b/netwerk/protocol/about/nsAboutProtocolHandler.h new file mode 100644 index 0000000000..173742e458 --- /dev/null +++ b/netwerk/protocol/about/nsAboutProtocolHandler.h @@ -0,0 +1,146 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef nsAboutProtocolHandler_h___ +#define nsAboutProtocolHandler_h___ + +#include "nsIProtocolHandler.h" +#include "nsSimpleNestedURI.h" +#include "nsWeakReference.h" +#include "mozilla/Attributes.h" +#include "nsIURIMutator.h" + +class nsIURI; + +namespace mozilla { +namespace net { + +class nsAboutProtocolHandler : public nsIProtocolHandlerWithDynamicFlags, + public nsIProtocolHandler, + public nsSupportsWeakReference { + public: + NS_DECL_ISUPPORTS + + // nsIProtocolHandler methods: + NS_DECL_NSIPROTOCOLHANDLER + NS_DECL_NSIPROTOCOLHANDLERWITHDYNAMICFLAGS + + // nsAboutProtocolHandler methods: + nsAboutProtocolHandler() = default; + + static nsresult CreateNewURI(const nsACString& aSpec, const char* aCharset, + nsIURI* aBaseURI, nsIURI** result); + + private: + virtual ~nsAboutProtocolHandler() = default; +}; + +class nsSafeAboutProtocolHandler final : public nsIProtocolHandler, + public nsSupportsWeakReference { + public: + NS_DECL_ISUPPORTS + + // nsIProtocolHandler methods: + NS_DECL_NSIPROTOCOLHANDLER + + // nsSafeAboutProtocolHandler methods: + nsSafeAboutProtocolHandler() = default; + + private: + ~nsSafeAboutProtocolHandler() = default; +}; + +// Class to allow us to propagate the base URI to about:blank correctly +class nsNestedAboutURI final : public nsSimpleNestedURI { + private: + nsNestedAboutURI(nsIURI* aInnerURI, nsIURI* aBaseURI) + : nsSimpleNestedURI(aInnerURI), mBaseURI(aBaseURI) {} + nsNestedAboutURI() : nsSimpleNestedURI() {} + virtual ~nsNestedAboutURI() = default; + + public: + // Override QI so we can QI to our CID as needed + NS_IMETHOD QueryInterface(REFNSIID aIID, void** aInstancePtr) override; + + // Override StartClone(), the nsISerializable methods, and + virtual nsSimpleURI* StartClone(RefHandlingEnum aRefHandlingMode, + const nsACString& newRef) override; + NS_IMETHOD Mutate(nsIURIMutator** _retval) override; + NS_IMETHOD_(void) Serialize(ipc::URIParams& aParams) override; + + // nsISerializable + NS_IMETHOD Read(nsIObjectInputStream* aStream) override; + NS_IMETHOD Write(nsIObjectOutputStream* aStream) override; + + nsIURI* GetBaseURI() const { return mBaseURI; } + + protected: + nsCOMPtr mBaseURI; + bool Deserialize(const mozilla::ipc::URIParams&); + nsresult ReadPrivate(nsIObjectInputStream* stream); + + public: + class Mutator final : public nsIURIMutator, + public BaseURIMutator, + public nsISerializable, + public nsINestedAboutURIMutator { + NS_DECL_ISUPPORTS + NS_FORWARD_SAFE_NSIURISETTERS_RET(mURI) + + explicit Mutator() = default; + + private: + virtual ~Mutator() = default; + + [[nodiscard]] NS_IMETHOD Deserialize( + const mozilla::ipc::URIParams& aParams) override { + return InitFromIPCParams(aParams); + } + + NS_IMETHOD + Write(nsIObjectOutputStream* aOutputStream) override { + return NS_ERROR_NOT_IMPLEMENTED; + } + + [[nodiscard]] NS_IMETHOD Read(nsIObjectInputStream* aStream) override { + return InitFromInputStream(aStream); + } + + [[nodiscard]] NS_IMETHOD Finalize(nsIURI** aURI) override { + mURI->mMutable = false; + mURI.forget(aURI); + return NS_OK; + } + + [[nodiscard]] NS_IMETHOD SetSpec(const nsACString& aSpec, + nsIURIMutator** aMutator) override { + if (aMutator) { + NS_ADDREF(*aMutator = this); + } + return InitFromSpec(aSpec); + } + + [[nodiscard]] NS_IMETHOD InitWithBase(nsIURI* aInnerURI, + nsIURI* aBaseURI) override { + mURI = new nsNestedAboutURI(aInnerURI, aBaseURI); + return NS_OK; + } + + void ResetMutable() { + if (mURI) { + mURI->mMutable = true; + } + } + + friend class nsNestedAboutURI; + }; + + friend BaseURIMutator; +}; + +} // namespace net +} // namespace mozilla + +#endif /* nsAboutProtocolHandler_h___ */ diff --git a/netwerk/protocol/about/nsAboutProtocolUtils.h b/netwerk/protocol/about/nsAboutProtocolUtils.h new file mode 100644 index 0000000000..ef508dce2e --- /dev/null +++ b/netwerk/protocol/about/nsAboutProtocolUtils.h @@ -0,0 +1,57 @@ +/* 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/. */ + +#ifndef nsAboutProtocolUtils_h +#define nsAboutProtocolUtils_h + +#include "nsIURI.h" +#include "nsString.h" +#include "nsReadableUtils.h" +#include "nsIAboutModule.h" +#include "nsServiceManagerUtils.h" +#include "prtime.h" + +[[nodiscard]] inline nsresult NS_GetAboutModuleName(nsIURI* aAboutURI, + nsCString& aModule) { + NS_ASSERTION(aAboutURI->SchemeIs("about"), + "should be used only on about: URIs"); + + nsresult rv = aAboutURI->GetPathQueryRef(aModule); + NS_ENSURE_SUCCESS(rv, rv); + + int32_t f = aModule.FindCharInSet("#?"_ns); + if (f != kNotFound) { + aModule.Truncate(f); + } + + // convert to lowercase, as all about: modules are lowercase + ToLowerCase(aModule); + return NS_OK; +} + +inline nsresult NS_GetAboutModule(nsIURI* aAboutURI, nsIAboutModule** aModule) { + MOZ_ASSERT(aAboutURI, "Must have URI"); + + nsAutoCString contractID; + nsresult rv = NS_GetAboutModuleName(aAboutURI, contractID); + if (NS_FAILED(rv)) return rv; + + // look up a handler to deal with "what" + contractID.InsertLiteral(NS_ABOUT_MODULE_CONTRACTID_PREFIX, 0); + + return CallGetService(contractID.get(), aModule); +} + +inline PRTime SecondsToPRTime(uint32_t t_sec) { + return (PRTime)t_sec * PR_USEC_PER_SEC; +} + +inline void PrintTimeString(char* buf, uint32_t bufsize, uint32_t t_sec) { + PRExplodedTime et; + PRTime t_usec = SecondsToPRTime(t_sec); + PR_ExplodeTime(t_usec, PR_LocalTimeParameters, &et); + PR_FormatTime(buf, bufsize, "%Y-%m-%d %H:%M:%S", &et); +} + +#endif diff --git a/netwerk/protocol/about/nsIAboutModule.idl b/netwerk/protocol/about/nsIAboutModule.idl new file mode 100644 index 0000000000..b7ebec8f67 --- /dev/null +++ b/netwerk/protocol/about/nsIAboutModule.idl @@ -0,0 +1,114 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* 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 "nsISupports.idl" + +interface nsIURI; +interface nsIChannel; +interface nsILoadInfo; + +[scriptable, uuid(c0c19db9-1b5a-4ac5-b656-ed6f8149fa48)] +interface nsIAboutModule : nsISupports +{ + + /** + * Constructs a new channel for the about protocol module. + * + * @param aURI the uri of the new channel + * @param aLoadInfo the loadinfo of the new channel + */ + nsIChannel newChannel(in nsIURI aURI, + in nsILoadInfo aLoadInfo); + + /** + * A flag that indicates whether a URI should be run with content + * privileges. If it is, the about: protocol handler will enforce that + * the principal of channels created for it be based on their + * originalURI or URI (depending on the channel flags), by setting + * their "owner" to null. + * If content needs to be able to link to this URI, specify + * URI_CONTENT_LINKABLE as well. + */ + const unsigned long URI_SAFE_FOR_UNTRUSTED_CONTENT = (1 << 0); + + /** + * A flag that indicates whether script should be enabled for the + * given about: URI even if it's disabled in general. + */ + const unsigned long ALLOW_SCRIPT = (1 << 1); + + /** + * A flag that indicates whether this about: URI doesn't want to be listed + * in about:about, especially if it's not useful without a query string. + */ + const unsigned long HIDE_FROM_ABOUTABOUT = (1 << 2); + + /** + * A flag that indicates whether this about: URI wants Indexed DB enabled. + */ + const unsigned long ENABLE_INDEXED_DB = (1 << 3); + + /** + * A flag that indicates that this URI can be loaded in a child process + */ + const unsigned long URI_CAN_LOAD_IN_CHILD = (1 << 4); + + /** + * A flag that indicates that this URI must be loaded in a child process + */ + const unsigned long URI_MUST_LOAD_IN_CHILD = (1 << 5); + + /** + * Obsolete. This flag no longer has any effect and will be removed in future. + */ + const unsigned long MAKE_UNLINKABLE = (1 << 6); + + /** + * A flag that indicates that this URI should be linkable from content. + * Ignored unless URI_SAFE_FOR_UNTRUSTED_CONTENT is also specified. + * + * When adding a new about module with this flag make sure to also update + * IsSafeToLinkForUntrustedContent() in nsAboutProtocolHandler.cpp + */ + const unsigned long MAKE_LINKABLE = (1 << 7); + + /** + * A flag that indicates that this URI can be loaded in the privileged + * activity stream content process if said process is enabled. Ignored unless + * URI_MUST_LOAD_IN_CHILD is also specified. + */ + const unsigned long URI_CAN_LOAD_IN_PRIVILEGEDABOUT_PROCESS = (1 << 8); + + /** + * A flag that indicates that this URI must be loaded in an extension process (if available). + */ + const unsigned long URI_MUST_LOAD_IN_EXTENSION_PROCESS = (1 << 9); + + /** + * A flag that indicates that this about: URI needs to allow unsanitized content. + * Only to be used by about:home and about:newtab. + */ + const unsigned long ALLOW_UNSANITIZED_CONTENT = (1 << 10); + + /** + * A method to get the flags that apply to a given about: URI. The URI + * passed in is guaranteed to be one of the URIs that this module + * registered to deal with. + */ + unsigned long getURIFlags(in nsIURI aURI); + + /** + * A method to get the chrome URI that corresponds to a given about URI. + */ + nsIURI getChromeURI(in nsIURI aURI); +}; + +%{C++ + +#define NS_ABOUT_MODULE_CONTRACTID "@mozilla.org/network/protocol/about;1" +#define NS_ABOUT_MODULE_CONTRACTID_PREFIX NS_ABOUT_MODULE_CONTRACTID "?what=" +#define NS_ABOUT_MODULE_CONTRACTID_LENGTH 49 // strlen(NS_ABOUT_MODULE_CONTRACTID_PREFIX) + +%} diff --git a/netwerk/protocol/data/DataChannelChild.cpp b/netwerk/protocol/data/DataChannelChild.cpp new file mode 100644 index 0000000000..3ce6f02dd5 --- /dev/null +++ b/netwerk/protocol/data/DataChannelChild.cpp @@ -0,0 +1,58 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=4 sw=2 sts=2 et 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 "DataChannelChild.h" + +#include "mozilla/Unused.h" +#include "mozilla/dom/ContentChild.h" +#include "mozilla/net/NeckoChild.h" + +namespace mozilla { +namespace net { + +NS_IMPL_ISUPPORTS_INHERITED(DataChannelChild, nsDataChannel, nsIChildChannel) + +DataChannelChild::DataChannelChild(nsIURI* aURI) + : nsDataChannel(aURI), mIPCOpen(false) {} + +NS_IMETHODIMP +DataChannelChild::ConnectParent(uint32_t aId) { + mozilla::dom::ContentChild* cc = + static_cast(gNeckoChild->Manager()); + if (cc->IsShuttingDown()) { + return NS_ERROR_FAILURE; + } + + if (!gNeckoChild->SendPDataChannelConstructor(this, aId)) { + return NS_ERROR_FAILURE; + } + + // IPC now has a ref to us. + mIPCOpen = true; + return NS_OK; +} + +NS_IMETHODIMP +DataChannelChild::CompleteRedirectSetup(nsIStreamListener* aListener) { + nsresult rv; + rv = AsyncOpen(aListener); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (mIPCOpen) { + Unused << Send__delete__(this); + } + return NS_OK; +} + +void DataChannelChild::ActorDestroy(ActorDestroyReason why) { + MOZ_ASSERT(mIPCOpen); + mIPCOpen = false; +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/data/DataChannelChild.h b/netwerk/protocol/data/DataChannelChild.h new file mode 100644 index 0000000000..8e9f91ea60 --- /dev/null +++ b/netwerk/protocol/data/DataChannelChild.h @@ -0,0 +1,40 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=4 sw=2 sts=2 et 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/. */ + +#ifndef NS_DATACHANNELCHILD_H +#define NS_DATACHANNELCHILD_H + +#include "nsDataChannel.h" +#include "nsIChildChannel.h" +#include "nsISupportsImpl.h" + +#include "mozilla/net/PDataChannelChild.h" + +namespace mozilla { +namespace net { + +class DataChannelChild : public nsDataChannel, + public nsIChildChannel, + public PDataChannelChild { + public: + explicit DataChannelChild(nsIURI* uri); + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_NSICHILDCHANNEL + + protected: + virtual void ActorDestroy(ActorDestroyReason why) override; + + private: + ~DataChannelChild() = default; + + bool mIPCOpen; +}; + +} // namespace net +} // namespace mozilla + +#endif /* NS_DATACHANNELCHILD_H */ diff --git a/netwerk/protocol/data/DataChannelParent.cpp b/netwerk/protocol/data/DataChannelParent.cpp new file mode 100644 index 0000000000..025cc2060e --- /dev/null +++ b/netwerk/protocol/data/DataChannelParent.cpp @@ -0,0 +1,105 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=4 sw=2 sts=2 et 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 "DataChannelParent.h" +#include "mozilla/Assertions.h" +#include "mozilla/dom/ContentParent.h" +#include "mozilla/net/NeckoParent.h" +#include "nsNetUtil.h" +#include "nsIChannel.h" + +namespace mozilla { +namespace net { + +NS_IMPL_ISUPPORTS(DataChannelParent, nsIParentChannel, nsIStreamListener) + +bool DataChannelParent::Init(const uint64_t& aChannelId) { + nsCOMPtr channel; + MOZ_ALWAYS_SUCCEEDS( + NS_LinkRedirectChannels(aChannelId, this, getter_AddRefs(channel))); + + return true; +} + +NS_IMETHODIMP +DataChannelParent::SetParentListener(ParentChannelListener* aListener) { + // Nothing to do. + return NS_OK; +} + +NS_IMETHODIMP +DataChannelParent::NotifyClassificationFlags(uint32_t aClassificationFlags, + bool aIsThirdParty) { + // Nothing to do. + return NS_OK; +} + +NS_IMETHODIMP +DataChannelParent::NotifyFlashPluginStateChanged( + nsIHttpChannel::FlashPluginState aState) { + // Nothing to do. + return NS_OK; +} + +NS_IMETHODIMP +DataChannelParent::SetClassifierMatchedInfo(const nsACString& aList, + const nsACString& aProvider, + const nsACString& aFullHash) { + // nothing to do + return NS_OK; +} + +NS_IMETHODIMP +DataChannelParent::SetClassifierMatchedTrackingInfo( + const nsACString& aLists, const nsACString& aFullHashes) { + // nothing to do + return NS_OK; +} + +NS_IMETHODIMP +DataChannelParent::Delete() { + // Nothing to do. + return NS_OK; +} + +NS_IMETHODIMP +DataChannelParent::GetRemoteType(nsACString& aRemoteType) { + if (!CanSend()) { + return NS_ERROR_UNEXPECTED; + } + + dom::PContentParent* pcp = Manager()->Manager(); + aRemoteType = static_cast(pcp)->GetRemoteType(); + return NS_OK; +} + +void DataChannelParent::ActorDestroy(ActorDestroyReason why) {} + +NS_IMETHODIMP +DataChannelParent::OnStartRequest(nsIRequest* aRequest) { + // We don't have a way to prevent nsBaseChannel from calling AsyncOpen on + // the created nsDataChannel. We don't have anywhere to send the data in the + // parent, so abort the binding. + return NS_BINDING_ABORTED; +} + +NS_IMETHODIMP +DataChannelParent::OnStopRequest(nsIRequest* aRequest, nsresult aStatusCode) { + // See above. + MOZ_ASSERT(NS_FAILED(aStatusCode)); + return NS_OK; +} + +NS_IMETHODIMP +DataChannelParent::OnDataAvailable(nsIRequest* aRequest, + nsIInputStream* aInputStream, + uint64_t aOffset, uint32_t aCount) { + // See above. + MOZ_CRASH("Should never be called"); +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/data/DataChannelParent.h b/netwerk/protocol/data/DataChannelParent.h new file mode 100644 index 0000000000..122cdda182 --- /dev/null +++ b/netwerk/protocol/data/DataChannelParent.h @@ -0,0 +1,39 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=4 sw=2 sts=2 et 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/. */ + +#ifndef NS_DATACHANNELPARENT_H +#define NS_DATACHANNELPARENT_H + +#include "nsIParentChannel.h" +#include "nsISupportsImpl.h" + +#include "mozilla/net/PDataChannelParent.h" + +namespace mozilla { +namespace net { + +// In order to support HTTP redirects to data:, we need to implement the HTTP +// redirection API, which requires a class that implements nsIParentChannel +// and which calls NS_LinkRedirectChannels. +class DataChannelParent : public nsIParentChannel, public PDataChannelParent { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIPARENTCHANNEL + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMLISTENER + + [[nodiscard]] bool Init(const uint64_t& aChannelId); + + private: + ~DataChannelParent() = default; + + virtual void ActorDestroy(ActorDestroyReason why) override; +}; + +} // namespace net +} // namespace mozilla + +#endif /* NS_DATACHANNELPARENT_H */ diff --git a/netwerk/protocol/data/moz.build b/netwerk/protocol/data/moz.build new file mode 100644 index 0000000000..482c0b8a2f --- /dev/null +++ b/netwerk/protocol/data/moz.build @@ -0,0 +1,29 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +EXPORTS.mozilla.net += [ + "DataChannelChild.h", + "DataChannelParent.h", +] + +EXPORTS += [ + "nsDataChannel.h", + "nsDataHandler.h", +] + +UNIFIED_SOURCES += [ + "DataChannelChild.cpp", + "DataChannelParent.cpp", + "nsDataChannel.cpp", + "nsDataHandler.cpp", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" +LOCAL_INCLUDES += [ + "/netwerk/base", +] diff --git a/netwerk/protocol/data/nsDataChannel.cpp b/netwerk/protocol/data/nsDataChannel.cpp new file mode 100644 index 0000000000..2b96cb0853 --- /dev/null +++ b/netwerk/protocol/data/nsDataChannel.cpp @@ -0,0 +1,109 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +// data implementation + +#include "nsDataChannel.h" + +#include "mozilla/Base64.h" +#include "nsDataHandler.h" +#include "nsIInputStream.h" +#include "nsEscape.h" +#include "nsStringStream.h" + +using namespace mozilla; + +/** + * Helper for performing a fallible unescape. + * + * @param aStr The string to unescape. + * @param aBuffer Buffer to unescape into if necessary. + * @param rv Out: nsresult indicating success or failure of unescaping. + * @return Reference to the string containing the unescaped data. + */ +const nsACString& Unescape(const nsACString& aStr, nsACString& aBuffer, + nsresult* rv) { + MOZ_ASSERT(rv); + + bool appended = false; + *rv = NS_UnescapeURL(aStr.Data(), aStr.Length(), /* aFlags = */ 0, aBuffer, + appended, mozilla::fallible); + if (NS_FAILED(*rv) || !appended) { + return aStr; + } + + return aBuffer; +} + +nsresult nsDataChannel::OpenContentStream(bool async, nsIInputStream** result, + nsIChannel** channel) { + NS_ENSURE_TRUE(URI(), NS_ERROR_NOT_INITIALIZED); + + nsresult rv; + + // In order to avoid potentially building up a new path including the + // ref portion of the URI, which we don't care about, we clone a version + // of the URI that does not have a ref and in most cases should share + // string buffers with the original URI. + nsCOMPtr uri; + rv = NS_GetURIWithoutRef(URI(), getter_AddRefs(uri)); + if (NS_FAILED(rv)) return rv; + + nsAutoCString path; + rv = uri->GetPathQueryRef(path); + if (NS_FAILED(rv)) return rv; + + nsCString contentType, contentCharset; + nsDependentCSubstring dataRange; + bool lBase64; + rv = nsDataHandler::ParsePathWithoutRef(path, contentType, &contentCharset, + lBase64, &dataRange); + if (NS_FAILED(rv)) return rv; + + // This will avoid a copy if nothing needs to be unescaped. + nsAutoCString unescapedBuffer; + const nsACString& data = Unescape(dataRange, unescapedBuffer, &rv); + if (NS_FAILED(rv)) { + return rv; + } + + if (lBase64 && &data == &unescapedBuffer) { + // Don't allow spaces in base64-encoded content. This is only + // relevant for escaped spaces; other spaces are stripped in + // NewURI. We know there were no escaped spaces if the data buffer + // wasn't used in |Unescape|. + unescapedBuffer.StripWhitespace(); + } + + nsCOMPtr bufInStream; + uint32_t contentLen; + if (lBase64) { + nsAutoCString decodedData; + rv = Base64Decode(data, decodedData); + if (NS_FAILED(rv)) { + // Returning this error code instead of what Base64Decode returns + // (NS_ERROR_ILLEGAL_VALUE) will prevent rendering of redirect response + // content by HTTP channels. It's also more logical error to return. + // Here we know the URL is actually corrupted. + return NS_ERROR_MALFORMED_URI; + } + + contentLen = decodedData.Length(); + rv = NS_NewCStringInputStream(getter_AddRefs(bufInStream), decodedData); + } else { + contentLen = data.Length(); + rv = NS_NewCStringInputStream(getter_AddRefs(bufInStream), data); + } + + if (NS_FAILED(rv)) return rv; + + SetContentType(contentType); + SetContentCharset(contentCharset); + mContentLength = contentLen; + + bufInStream.forget(result); + + return NS_OK; +} diff --git a/netwerk/protocol/data/nsDataChannel.h b/netwerk/protocol/data/nsDataChannel.h new file mode 100644 index 0000000000..8ee759f1db --- /dev/null +++ b/netwerk/protocol/data/nsDataChannel.h @@ -0,0 +1,24 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +// data implementation header + +#ifndef nsDataChannel_h___ +#define nsDataChannel_h___ + +#include "nsBaseChannel.h" + +class nsIInputStream; + +class nsDataChannel : public nsBaseChannel { + public: + explicit nsDataChannel(nsIURI* uri) { SetURI(uri); } + + protected: + [[nodiscard]] virtual nsresult OpenContentStream( + bool async, nsIInputStream** result, nsIChannel** channel) override; +}; + +#endif /* nsDataChannel_h___ */ diff --git a/netwerk/protocol/data/nsDataHandler.cpp b/netwerk/protocol/data/nsDataHandler.cpp new file mode 100644 index 0000000000..01efa9b0f1 --- /dev/null +++ b/netwerk/protocol/data/nsDataHandler.cpp @@ -0,0 +1,270 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 "nsDataChannel.h" +#include "nsDataHandler.h" +#include "nsNetCID.h" +#include "nsError.h" +#include "nsIOService.h" +#include "DataChannelChild.h" +#include "plstr.h" +#include "nsSimpleURI.h" +#include "mozilla/dom/MimeType.h" + +#ifdef ANDROID +# include "mozilla/StaticPrefs_network.h" +#endif + +//////////////////////////////////////////////////////////////////////////////// + +NS_IMPL_ISUPPORTS(nsDataHandler, nsIProtocolHandler, nsISupportsWeakReference) + +nsresult nsDataHandler::Create(nsISupports* aOuter, const nsIID& aIID, + void** aResult) { + RefPtr ph = new nsDataHandler(); + return ph->QueryInterface(aIID, aResult); +} + +//////////////////////////////////////////////////////////////////////////////// +// nsIProtocolHandler methods: + +NS_IMETHODIMP +nsDataHandler::GetScheme(nsACString& result) { + result.AssignLiteral("data"); + return NS_OK; +} + +NS_IMETHODIMP +nsDataHandler::GetDefaultPort(int32_t* result) { + // no ports for data protocol + *result = -1; + return NS_OK; +} + +NS_IMETHODIMP +nsDataHandler::GetProtocolFlags(uint32_t* result) { + *result = URI_NORELATIVE | URI_NOAUTH | URI_INHERITS_SECURITY_CONTEXT | + URI_LOADABLE_BY_ANYONE | URI_NON_PERSISTABLE | + URI_IS_LOCAL_RESOURCE | URI_SYNC_LOAD_IS_OK; + return NS_OK; +} + +/* static */ nsresult nsDataHandler::CreateNewURI(const nsACString& aSpec, + const char* aCharset, + nsIURI* aBaseURI, + nsIURI** result) { + nsresult rv; + nsCOMPtr uri; + + nsCString spec(aSpec); + +#ifdef ANDROID + // Due to heap limitations on mobile, limits the size of data URL + if (spec.Length() > StaticPrefs::network_data_max_uri_length_mobile()) + return NS_ERROR_OUT_OF_MEMORY; +#endif + + if (aBaseURI && !spec.IsEmpty() && spec[0] == '#') { + // Looks like a reference instead of a fully-specified URI. + // --> initialize |uri| as a clone of |aBaseURI|, with ref appended. + rv = NS_MutateURI(aBaseURI).SetRef(spec).Finalize(uri); + } else { + // Otherwise, we'll assume |spec| is a fully-specified data URI + nsAutoCString contentType; + bool base64; + rv = ParseURI(spec, contentType, /* contentCharset = */ nullptr, base64, + /* dataBuffer = */ nullptr); + if (NS_FAILED(rv)) return rv; + + // Strip whitespace unless this is text, where whitespace is important + // Don't strip escaped whitespace though (bug 391951) + if (base64 || (strncmp(contentType.get(), "text/", 5) != 0 && + contentType.Find("xml") == kNotFound)) { + // it's ascii encoded binary, don't let any spaces in + if (!spec.StripWhitespace(mozilla::fallible)) { + return NS_ERROR_OUT_OF_MEMORY; + } + } + + rv = NS_MutateURI(new mozilla::net::nsSimpleURI::Mutator()) + .SetSpec(spec) + .Finalize(uri); + } + + if (NS_FAILED(rv)) return rv; + + uri.forget(result); + return rv; +} + +NS_IMETHODIMP +nsDataHandler::NewChannel(nsIURI* uri, nsILoadInfo* aLoadInfo, + nsIChannel** result) { + NS_ENSURE_ARG_POINTER(uri); + RefPtr channel; + if (XRE_IsParentProcess()) { + channel = new nsDataChannel(uri); + } else { + channel = new mozilla::net::DataChannelChild(uri); + } + + // set the loadInfo on the new channel + nsresult rv = channel->SetLoadInfo(aLoadInfo); + NS_ENSURE_SUCCESS(rv, rv); + + channel.forget(result); + return NS_OK; +} + +NS_IMETHODIMP +nsDataHandler::AllowPort(int32_t port, const char* scheme, bool* _retval) { + // don't override anything. + *_retval = false; + return NS_OK; +} + +/** + * Helper that performs a case insensitive match to find the offset of a given + * pattern in a nsACString. + * The search is performed starting from the end of the string; if the string + * contains more than one match, the rightmost (last) match will be returned. + */ +static bool FindOffsetOf(const nsACString& aPattern, const nsACString& aSrc, + nsACString::size_type& aOffset) { + nsACString::const_iterator begin, end; + aSrc.BeginReading(begin); + aSrc.EndReading(end); + if (!RFindInReadable(aPattern, begin, end, + nsCaseInsensitiveCStringComparator)) { + return false; + } + + // FindInReadable updates |begin| and |end| to the match coordinates. + aOffset = nsACString::size_type(begin.get() - aSrc.Data()); + return true; +} + +nsresult nsDataHandler::ParsePathWithoutRef( + const nsACString& aPath, nsCString& aContentType, + nsCString* aContentCharset, bool& aIsBase64, + nsDependentCSubstring* aDataBuffer) { + static constexpr auto kBase64 = "base64"_ns; + static constexpr auto kCharset = "charset"_ns; + + aIsBase64 = false; + + // First, find the start of the data + int32_t commaIdx = aPath.FindChar(','); + + // This is a hack! When creating a URL using the DOM API we want to ignore + // if a comma is missing. But if we're actually loading a data: URI, in which + // case aContentCharset is not null, then we want to return an error if a + // comma is missing. + if (aContentCharset && commaIdx == kNotFound) { + return NS_ERROR_MALFORMED_URI; + } + if (commaIdx == 0 || commaIdx == kNotFound) { + // Nothing but data. + aContentType.AssignLiteral("text/plain"); + if (aContentCharset) { + aContentCharset->AssignLiteral("US-ASCII"); + } + } else { + auto mediaType = Substring(aPath, 0, commaIdx); + + // Determine if the data is base64 encoded. + nsACString::size_type base64; + if (FindOffsetOf(kBase64, mediaType, base64) && base64 > 0) { + nsACString::size_type offset = base64 + kBase64.Length(); + // Per the RFC 2397 grammar, "base64" MUST be at the end of the + // non-data part. + // + // But we also allow it in between parameters so a subsequent ";" + // is ok as well (this deals with *broken* data URIs, see bug + // 781693 for an example). Anything after "base64" in the non-data + // part will be discarded in this case, however. + if (offset == mediaType.Length() || mediaType[offset] == ';' || + mediaType[offset] == ' ') { + MOZ_DIAGNOSTIC_ASSERT(base64 > 0, "Did someone remove the check?"); + // Index is on the first character of matched "base64" so we + // move to the preceding character + base64--; + // Skip any preceding spaces, searching for a semicolon + while (base64 > 0 && mediaType[base64] == ' ') { + base64--; + } + if (mediaType[base64] == ';') { + aIsBase64 = true; + // Trim the base64 part off. + mediaType.Rebind(aPath, 0, base64); + } + } + } + + // Skip any leading spaces + nsACString::size_type startIndex = 0; + while (startIndex < mediaType.Length() && mediaType[startIndex] == ' ') { + startIndex++; + } + + nsAutoCString mediaTypeBuf; + // If the mimetype starts with ';' we assume text/plain + if (startIndex < mediaType.Length() && mediaType[startIndex] == ';') { + mediaTypeBuf.AssignLiteral("text/plain"); + mediaTypeBuf.Append(mediaType); + mediaType.Rebind(mediaTypeBuf, 0, mediaTypeBuf.Length()); + } + + // Everything else is content type. + if (mozilla::UniquePtr parsed = CMimeType::Parse(mediaType)) { + parsed->GetFullType(aContentType); + if (aContentCharset) { + parsed->GetParameterValue(kCharset, *aContentCharset); + } + } else { + // Mime Type parsing failed + aContentType.AssignLiteral("text/plain"); + if (aContentCharset) { + aContentCharset->AssignLiteral("US-ASCII"); + } + } + } + + if (aDataBuffer) { + aDataBuffer->Rebind(aPath, commaIdx + 1); + } + + return NS_OK; +} + +nsresult nsDataHandler::ParseURI(nsCString& spec, nsCString& contentType, + nsCString* contentCharset, bool& isBase64, + nsCString* dataBuffer) { + static constexpr auto kDataScheme = "data:"_ns; + + // move past "data:" + int32_t scheme = spec.Find(kDataScheme, /* aIgnoreCase = */ true); + if (scheme == kNotFound) { + // malformed uri + return NS_ERROR_MALFORMED_URI; + } + + scheme += kDataScheme.Length(); + + // Find the start of the hash ref if present. + int32_t hash = spec.FindChar('#', scheme); + + auto pathWithoutRef = Substring(spec, scheme, hash != kNotFound ? hash : -1); + nsDependentCSubstring dataRange; + nsresult rv = ParsePathWithoutRef(pathWithoutRef, contentType, contentCharset, + isBase64, &dataRange); + if (NS_SUCCEEDED(rv) && dataBuffer) { + if (!dataBuffer->Assign(dataRange, mozilla::fallible)) { + rv = NS_ERROR_OUT_OF_MEMORY; + } + } + + return rv; +} diff --git a/netwerk/protocol/data/nsDataHandler.h b/netwerk/protocol/data/nsDataHandler.h new file mode 100644 index 0000000000..37d170f3e5 --- /dev/null +++ b/netwerk/protocol/data/nsDataHandler.h @@ -0,0 +1,57 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef nsDataHandler_h___ +#define nsDataHandler_h___ + +#include "nsIProtocolHandler.h" +#include "nsWeakReference.h" + +class nsDataHandler : public nsIProtocolHandler, + public nsSupportsWeakReference { + virtual ~nsDataHandler() = default; + + public: + NS_DECL_ISUPPORTS + + // nsIProtocolHandler methods: + NS_DECL_NSIPROTOCOLHANDLER + + // nsDataHandler methods: + nsDataHandler() = default; + + static nsresult CreateNewURI(const nsACString& aSpec, const char* aCharset, + nsIURI* aBaseURI, nsIURI** result); + + // Define a Create method to be used with a factory: + [[nodiscard]] static nsresult Create(nsISupports* aOuter, const nsIID& aIID, + void** aResult); + + // Parse a data: URI and return the individual parts + // (the given spec will temporarily be modified but will be returned + // to the original before returning) + // contentCharset and dataBuffer can be nullptr if they are not needed. + [[nodiscard]] static nsresult ParseURI(nsCString& spec, + nsCString& contentType, + nsCString* contentCharset, + bool& isBase64, nsCString* dataBuffer); + + // Parse the path portion of a data: URI and return the individual parts. + // + // Note: The path is assumed *not* to have a ref portion. + // + // @arg aPath The path portion of the spec. Must not have ref portion. + // @arg aContentType Out param, will hold the parsed content type. + // @arg aContentCharset Optional, will hold the charset if specified. + // @arg aIsBase64 Out param, indicates if the data is base64 encoded. + // @arg aDataBuffer Optional, will reference the substring in |aPath| that + // contains the data portion of the path. No copy is made. + [[nodiscard]] static nsresult ParsePathWithoutRef( + const nsACString& aPath, nsCString& aContentType, + nsCString* aContentCharset, bool& aIsBase64, + nsDependentCSubstring* aDataBuffer); +}; + +#endif /* nsDataHandler_h___ */ diff --git a/netwerk/protocol/data/nsDataModule.cpp b/netwerk/protocol/data/nsDataModule.cpp new file mode 100644 index 0000000000..8bcda94362 --- /dev/null +++ b/netwerk/protocol/data/nsDataModule.cpp @@ -0,0 +1,15 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 "nsIGenericFactory.h" +#include "nsDataHandler.h" + +// The list of components we register +static const nsModuleComponentInfo components[] = { + {"Data Protocol Handler", NS_DATAHANDLER_CID, + NS_NETWORK_PROTOCOL_CONTRACTID_PREFIX "data", nsDataHandler::Create}, +}; + +NS_IMPL_NSGETMODULE(nsDataProtocolModule, components) diff --git a/netwerk/protocol/file/FileChannelChild.cpp b/netwerk/protocol/file/FileChannelChild.cpp new file mode 100644 index 0000000000..fa9cc00584 --- /dev/null +++ b/netwerk/protocol/file/FileChannelChild.cpp @@ -0,0 +1,52 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 sw=2 sts=2 et 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 "FileChannelChild.h" + +#include "mozilla/Unused.h" +#include "mozilla/dom/ContentChild.h" +#include "mozilla/net/NeckoChild.h" + +namespace mozilla { +namespace net { + +NS_IMPL_ISUPPORTS_INHERITED(FileChannelChild, nsFileChannel, nsIChildChannel) + +FileChannelChild::FileChannelChild(nsIURI* uri) : nsFileChannel(uri) {} + +NS_IMETHODIMP +FileChannelChild::ConnectParent(uint32_t id) { + mozilla::dom::ContentChild* cc = + static_cast(gNeckoChild->Manager()); + if (cc->IsShuttingDown()) { + return NS_ERROR_FAILURE; + } + + if (!gNeckoChild->SendPFileChannelConstructor(this, id)) { + return NS_ERROR_FAILURE; + } + return NS_OK; +} + +NS_IMETHODIMP +FileChannelChild::CompleteRedirectSetup(nsIStreamListener* listener) { + nsresult rv; + + rv = AsyncOpen(listener); + + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (CanSend()) { + Unused << Send__delete__(this); + } + + return NS_OK; +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/file/FileChannelChild.h b/netwerk/protocol/file/FileChannelChild.h new file mode 100644 index 0000000000..e6d67b7d1c --- /dev/null +++ b/netwerk/protocol/file/FileChannelChild.h @@ -0,0 +1,35 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 sw=2 sts=2 et 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/. */ + +#ifndef mozilla__net__FileChannelChild_h +#define mozilla__net__FileChannelChild_h + +#include "nsFileChannel.h" +#include "nsIChildChannel.h" +#include "nsISupportsImpl.h" + +#include "mozilla/net/PFileChannelChild.h" + +namespace mozilla { +namespace net { + +class FileChannelChild : public nsFileChannel, + public nsIChildChannel, + public PFileChannelChild { + public: + explicit FileChannelChild(nsIURI* uri); + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_NSICHILDCHANNEL + + private: + ~FileChannelChild() = default; +}; + +} // namespace net +} // namespace mozilla + +#endif /* mozilla__net__FileChannelChild_h */ diff --git a/netwerk/protocol/file/FileChannelParent.cpp b/netwerk/protocol/file/FileChannelParent.cpp new file mode 100644 index 0000000000..904b9fa88f --- /dev/null +++ b/netwerk/protocol/file/FileChannelParent.cpp @@ -0,0 +1,105 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 sw=2 sts=2 et 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 "FileChannelParent.h" +#include "mozilla/Assertions.h" +#include "mozilla/dom/ContentParent.h" +#include "mozilla/net/NeckoParent.h" +#include "nsNetUtil.h" +#include "nsIChannel.h" + +namespace mozilla { +namespace net { + +NS_IMPL_ISUPPORTS(FileChannelParent, nsIParentChannel, nsIStreamListener) + +bool FileChannelParent::Init(const uint64_t& aChannelId) { + nsCOMPtr channel; + MOZ_ALWAYS_SUCCEEDS( + NS_LinkRedirectChannels(aChannelId, this, getter_AddRefs(channel))); + + return true; +} + +NS_IMETHODIMP +FileChannelParent::SetParentListener(ParentChannelListener* aListener) { + // Nothing to do. + return NS_OK; +} + +NS_IMETHODIMP +FileChannelParent::NotifyClassificationFlags(uint32_t aClassificationFlags, + bool aIsThirdParty) { + // Nothing to do. + return NS_OK; +} + +NS_IMETHODIMP +FileChannelParent::NotifyFlashPluginStateChanged( + nsIHttpChannel::FlashPluginState aState) { + // Nothing to do. + return NS_OK; +} + +NS_IMETHODIMP +FileChannelParent::SetClassifierMatchedInfo(const nsACString& aList, + const nsACString& aProvider, + const nsACString& aFullHash) { + // nothing to do + return NS_OK; +} + +NS_IMETHODIMP +FileChannelParent::SetClassifierMatchedTrackingInfo( + const nsACString& aLists, const nsACString& aFullHashes) { + // nothing to do + return NS_OK; +} + +NS_IMETHODIMP +FileChannelParent::Delete() { + // Nothing to do. + return NS_OK; +} + +NS_IMETHODIMP +FileChannelParent::GetRemoteType(nsACString& aRemoteType) { + if (!CanSend()) { + return NS_ERROR_UNEXPECTED; + } + + dom::PContentParent* pcp = Manager()->Manager(); + aRemoteType = static_cast(pcp)->GetRemoteType(); + return NS_OK; +} + +void FileChannelParent::ActorDestroy(ActorDestroyReason why) {} + +NS_IMETHODIMP +FileChannelParent::OnStartRequest(nsIRequest* aRequest) { + // We don't have a way to prevent nsBaseChannel from calling AsyncOpen on + // the created nsDataChannel. We don't have anywhere to send the data in the + // parent, so abort the binding. + return NS_BINDING_ABORTED; +} + +NS_IMETHODIMP +FileChannelParent::OnStopRequest(nsIRequest* aRequest, nsresult aStatusCode) { + // See above. + MOZ_ASSERT(NS_FAILED(aStatusCode)); + return NS_OK; +} + +NS_IMETHODIMP +FileChannelParent::OnDataAvailable(nsIRequest* aRequest, + nsIInputStream* aInputStream, + uint64_t aOffset, uint32_t aCount) { + // See above. + MOZ_CRASH("Should never be called"); +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/file/FileChannelParent.h b/netwerk/protocol/file/FileChannelParent.h new file mode 100644 index 0000000000..f2a10d8f87 --- /dev/null +++ b/netwerk/protocol/file/FileChannelParent.h @@ -0,0 +1,39 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=4 sw=2 sts=2 et 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/. */ + +#ifndef mozilla__net__FileChannelParent_h +#define mozilla__net__FileChannelParent_h + +#include "nsIParentChannel.h" +#include "nsISupportsImpl.h" + +#include "mozilla/net/PFileChannelParent.h" + +namespace mozilla { +namespace net { + +// In order to support HTTP redirects to file:, we need to implement the HTTP +// redirection API, which requires a class that implements nsIParentChannel +// and which calls NS_LinkRedirectChannels. +class FileChannelParent : public nsIParentChannel, public PFileChannelParent { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIPARENTCHANNEL + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMLISTENER + + [[nodiscard]] bool Init(const uint64_t& aChannelId); + + private: + ~FileChannelParent() = default; + + virtual void ActorDestroy(ActorDestroyReason why) override; +}; + +} // namespace net +} // namespace mozilla + +#endif /* mozilla__net__FileChannelParent_h */ diff --git a/netwerk/protocol/file/moz.build b/netwerk/protocol/file/moz.build new file mode 100644 index 0000000000..06a883f826 --- /dev/null +++ b/netwerk/protocol/file/moz.build @@ -0,0 +1,43 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +with Files("**"): + BUG_COMPONENT = ("Core", "Networking: File") + +EXPORTS.mozilla.net += [ + "FileChannelChild.h", + "FileChannelParent.h", + "nsFileProtocolHandler.h", +] + +EXPORTS += [ + "nsFileChannel.h", +] + +XPIDL_SOURCES += [ + "nsIFileChannel.idl", + "nsIFileProtocolHandler.idl", +] + +XPIDL_MODULE = "necko_file" + +UNIFIED_SOURCES += [ + "FileChannelChild.cpp", + "FileChannelParent.cpp", + "nsFileChannel.cpp", + "nsFileProtocolHandler.cpp", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" + +LOCAL_INCLUDES += [ + "/netwerk/base", +] + +if CONFIG["CC_TYPE"] in ("clang", "gcc"): + CXXFLAGS += ["-Wno-error=shadow"] diff --git a/netwerk/protocol/file/nsFileChannel.cpp b/netwerk/protocol/file/nsFileChannel.cpp new file mode 100644 index 0000000000..a94aa50599 --- /dev/null +++ b/netwerk/protocol/file/nsFileChannel.cpp @@ -0,0 +1,511 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cin: */ +/* 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 "nsIOService.h" +#include "nsFileChannel.h" +#include "nsBaseContentStream.h" +#include "nsDirectoryIndexStream.h" +#include "nsThreadUtils.h" +#include "nsTransportUtils.h" +#include "nsStreamUtils.h" +#include "nsMimeTypes.h" +#include "nsNetUtil.h" +#include "nsNetCID.h" +#include "nsIOutputStream.h" +#include "nsIFileStreams.h" +#include "nsFileProtocolHandler.h" +#include "nsProxyRelease.h" +#include "nsIContentPolicy.h" +#include "nsContentUtils.h" + +#include "nsIFileURL.h" +#include "nsIURIMutator.h" +#include "nsIFile.h" +#include "nsIMIMEService.h" +#include "prio.h" +#include + +#include "mozilla/TaskQueue.h" +#include "mozilla/Unused.h" + +using namespace mozilla; +using namespace mozilla::net; + +//----------------------------------------------------------------------------- + +class nsFileCopyEvent : public Runnable { + public: + nsFileCopyEvent(nsIOutputStream* dest, nsIInputStream* source, int64_t len) + : mozilla::Runnable("nsFileCopyEvent"), + mDest(dest), + mSource(source), + mLen(len), + mStatus(NS_OK), + mInterruptStatus(NS_OK) {} + + // Read the current status of the file copy operation. + nsresult Status() { return mStatus; } + + // Call this method to perform the file copy synchronously. + void DoCopy(); + + // Call this method to perform the file copy on a background thread. The + // callback is dispatched when the file copy completes. + nsresult Dispatch(nsIRunnable* callback, nsITransportEventSink* sink, + nsIEventTarget* target); + + // Call this method to interrupt a file copy operation that is occuring on + // a background thread. The status parameter passed to this function must + // be a failure code and is set as the status of this file copy operation. + void Interrupt(nsresult status) { + NS_ASSERTION(NS_FAILED(status), "must be a failure code"); + mInterruptStatus = status; + } + + NS_IMETHOD Run() override { + DoCopy(); + return NS_OK; + } + + private: + nsCOMPtr mCallbackTarget; + nsCOMPtr mCallback; + nsCOMPtr mSink; + nsCOMPtr mDest; + nsCOMPtr mSource; + int64_t mLen; + nsresult mStatus; // modified on i/o thread only + nsresult mInterruptStatus; // modified on main thread only +}; + +void nsFileCopyEvent::DoCopy() { + // We'll copy in chunks this large by default. This size affects how + // frequently we'll check for interrupts. + const int32_t chunk = + nsIOService::gDefaultSegmentSize * nsIOService::gDefaultSegmentCount; + + nsresult rv = NS_OK; + + int64_t len = mLen, progress = 0; + while (len) { + // If we've been interrupted, then stop copying. + rv = mInterruptStatus; + if (NS_FAILED(rv)) break; + + int32_t num = std::min((int32_t)len, chunk); + + uint32_t result; + rv = mSource->ReadSegments(NS_CopySegmentToStream, mDest, num, &result); + if (NS_FAILED(rv)) break; + if (result != (uint32_t)num) { + rv = NS_ERROR_FILE_DISK_FULL; // stopped prematurely (out of disk space) + break; + } + + // Dispatch progress notification + if (mSink) { + progress += num; + mSink->OnTransportStatus(nullptr, NS_NET_STATUS_WRITING, progress, mLen); + } + + len -= num; + } + + if (NS_FAILED(rv)) mStatus = rv; + + // Close the output stream before notifying our callback so that others may + // freely "play" with the file. + mDest->Close(); + + // Notify completion + if (mCallback) { + mCallbackTarget->Dispatch(mCallback, NS_DISPATCH_NORMAL); + + // Release the callback on the target thread to avoid destroying stuff on + // the wrong thread. + NS_ProxyRelease("nsFileCopyEvent::mCallback", mCallbackTarget, + mCallback.forget()); + } +} + +nsresult nsFileCopyEvent::Dispatch(nsIRunnable* callback, + nsITransportEventSink* sink, + nsIEventTarget* target) { + // Use the supplied event target for all asynchronous operations. + + mCallback = callback; + mCallbackTarget = target; + + // Build a coalescing proxy for progress events + nsresult rv = + net_NewTransportEventSinkProxy(getter_AddRefs(mSink), sink, target); + + if (NS_FAILED(rv)) return rv; + + // Dispatch ourselves to I/O thread pool... + nsCOMPtr pool = + do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID, &rv); + if (NS_FAILED(rv)) return rv; + + return pool->Dispatch(this, NS_DISPATCH_NORMAL); +} + +//----------------------------------------------------------------------------- + +// This is a dummy input stream that when read, performs the file copy. The +// copy happens on a background thread via mCopyEvent. + +class nsFileUploadContentStream : public nsBaseContentStream { + public: + NS_INLINE_DECL_REFCOUNTING_INHERITED(nsFileUploadContentStream, + nsBaseContentStream) + + nsFileUploadContentStream(bool nonBlocking, nsIOutputStream* dest, + nsIInputStream* source, int64_t len, + nsITransportEventSink* sink) + : nsBaseContentStream(nonBlocking), + mCopyEvent(new nsFileCopyEvent(dest, source, len)), + mSink(sink) {} + + bool IsInitialized() { return mCopyEvent != nullptr; } + + NS_IMETHOD ReadSegments(nsWriteSegmentFun fun, void* closure, uint32_t count, + uint32_t* result) override; + NS_IMETHOD AsyncWait(nsIInputStreamCallback* callback, uint32_t flags, + uint32_t count, nsIEventTarget* target) override; + + private: + virtual ~nsFileUploadContentStream() = default; + + void OnCopyComplete(); + + RefPtr mCopyEvent; + nsCOMPtr mSink; +}; + +NS_IMETHODIMP +nsFileUploadContentStream::ReadSegments(nsWriteSegmentFun fun, void* closure, + uint32_t count, uint32_t* result) { + *result = 0; // nothing is ever actually read from this stream + + if (IsClosed()) return NS_OK; + + if (IsNonBlocking()) { + // Inform the caller that they will have to wait for the copy operation to + // complete asynchronously. We'll kick of the copy operation once they + // call AsyncWait. + return NS_BASE_STREAM_WOULD_BLOCK; + } + + // Perform copy synchronously, and then close out the stream. + mCopyEvent->DoCopy(); + nsresult status = mCopyEvent->Status(); + CloseWithStatus(NS_FAILED(status) ? status : NS_BASE_STREAM_CLOSED); + return status; +} + +NS_IMETHODIMP +nsFileUploadContentStream::AsyncWait(nsIInputStreamCallback* callback, + uint32_t flags, uint32_t count, + nsIEventTarget* target) { + nsresult rv = nsBaseContentStream::AsyncWait(callback, flags, count, target); + if (NS_FAILED(rv) || IsClosed()) return rv; + + if (IsNonBlocking()) { + nsCOMPtr callback = + NewRunnableMethod("nsFileUploadContentStream::OnCopyComplete", this, + &nsFileUploadContentStream::OnCopyComplete); + mCopyEvent->Dispatch(callback, mSink, target); + } + + return NS_OK; +} + +void nsFileUploadContentStream::OnCopyComplete() { + // This method is being called to indicate that we are done copying. + nsresult status = mCopyEvent->Status(); + + CloseWithStatus(NS_FAILED(status) ? status : NS_BASE_STREAM_CLOSED); +} + +//----------------------------------------------------------------------------- + +nsFileChannel::nsFileChannel(nsIURI* uri) : mUploadLength(0), mFileURI(uri) {} + +nsresult nsFileChannel::Init() { + NS_ENSURE_STATE(mLoadInfo); + + // If we have a link file, we should resolve its target right away. + // This is to protect against a same origin attack where the same link file + // can point to different resources right after the first resource is loaded. + nsCOMPtr file; + nsCOMPtr targetURI; +#ifdef XP_WIN + nsAutoString fileTarget; +#else + nsAutoCString fileTarget; +#endif + nsCOMPtr resolvedFile; + bool symLink; + nsCOMPtr fileURL = do_QueryInterface(mFileURI); + if (fileURL && NS_SUCCEEDED(fileURL->GetFile(getter_AddRefs(file))) && + NS_SUCCEEDED(file->IsSymlink(&symLink)) && symLink && +#ifdef XP_WIN + NS_SUCCEEDED(file->GetTarget(fileTarget)) && + NS_SUCCEEDED( + NS_NewLocalFile(fileTarget, true, getter_AddRefs(resolvedFile))) && +#else + NS_SUCCEEDED(file->GetNativeTarget(fileTarget)) && + NS_SUCCEEDED(NS_NewNativeLocalFile(fileTarget, true, + getter_AddRefs(resolvedFile))) && +#endif + NS_SUCCEEDED( + NS_NewFileURI(getter_AddRefs(targetURI), resolvedFile, nullptr))) { + // Make an effort to match up the query strings. + nsCOMPtr origURL = do_QueryInterface(mFileURI); + nsCOMPtr targetURL = do_QueryInterface(targetURI); + nsAutoCString queryString; + if (origURL && targetURL && NS_SUCCEEDED(origURL->GetQuery(queryString))) { + Unused + << NS_MutateURI(targetURI).SetQuery(queryString).Finalize(targetURI); + } + + SetURI(targetURI); + SetOriginalURI(mFileURI); + mLoadInfo->SetResultPrincipalURI(targetURI); + } else { + SetURI(mFileURI); + } + + return NS_OK; +} + +nsresult nsFileChannel::MakeFileInputStream(nsIFile* file, + nsCOMPtr& stream, + nsCString& contentType, + bool async) { + // we accept that this might result in a disk hit to stat the file + bool isDir; + nsresult rv = file->IsDirectory(&isDir); + if (NS_FAILED(rv)) { + // canonicalize error message + if (rv == NS_ERROR_FILE_TARGET_DOES_NOT_EXIST) rv = NS_ERROR_FILE_NOT_FOUND; + + if (async && (NS_ERROR_FILE_NOT_FOUND == rv)) { + // We don't return "Not Found" errors here. Since we could not find + // the file, it's not a directory anyway. + isDir = false; + } else { + return rv; + } + } + + if (isDir) { + rv = nsDirectoryIndexStream::Create(file, getter_AddRefs(stream)); + if (NS_SUCCEEDED(rv) && !HasContentTypeHint()) + contentType.AssignLiteral(APPLICATION_HTTP_INDEX_FORMAT); + } else { + rv = NS_NewLocalFileInputStream(getter_AddRefs(stream), file, -1, -1, + async ? nsIFileInputStream::DEFER_OPEN : 0); + if (NS_SUCCEEDED(rv) && !HasContentTypeHint()) { + // Use file extension to infer content type + nsCOMPtr mime = do_GetService("@mozilla.org/mime;1", &rv); + if (NS_SUCCEEDED(rv)) { + mime->GetTypeFromFile(file, contentType); + } + } + } + return rv; +} + +nsresult nsFileChannel::OpenContentStream(bool async, nsIInputStream** result, + nsIChannel** channel) { + // NOTE: the resulting file is a clone, so it is safe to pass it to the + // file input stream which will be read on a background thread. + nsCOMPtr file; + nsresult rv = GetFile(getter_AddRefs(file)); + if (NS_FAILED(rv)) return rv; + + nsCOMPtr fileHandler; + rv = NS_GetFileProtocolHandler(getter_AddRefs(fileHandler)); + if (NS_FAILED(rv)) return rv; + + nsCOMPtr newURI; + if (NS_SUCCEEDED(fileHandler->ReadURLFile(file, getter_AddRefs(newURI))) || + NS_SUCCEEDED(fileHandler->ReadShellLink(file, getter_AddRefs(newURI)))) { + nsCOMPtr newChannel; + rv = NS_NewChannel(getter_AddRefs(newChannel), newURI, + nsContentUtils::GetSystemPrincipal(), + nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + nsIContentPolicy::TYPE_OTHER); + + if (NS_FAILED(rv)) return rv; + + *result = nullptr; + newChannel.forget(channel); + return NS_OK; + } + + nsCOMPtr stream; + + if (mUploadStream) { + // Pass back a nsFileUploadContentStream instance that knows how to perform + // the file copy when "read" (the resulting stream in this case does not + // actually return any data). + + nsCOMPtr fileStream; + rv = NS_NewLocalFileOutputStream(getter_AddRefs(fileStream), file, + PR_WRONLY | PR_CREATE_FILE | PR_TRUNCATE, + PR_IRUSR | PR_IWUSR); + if (NS_FAILED(rv)) return rv; + + RefPtr uploadStream = + new nsFileUploadContentStream(async, fileStream, mUploadStream, + mUploadLength, this); + if (!uploadStream || !uploadStream->IsInitialized()) { + return NS_ERROR_OUT_OF_MEMORY; + } + stream = std::move(uploadStream); + + mContentLength = 0; + + // Since there isn't any content to speak of we just set the content-type + // to something other than "unknown" to avoid triggering the content-type + // sniffer code in nsBaseChannel. + // However, don't override explicitly set types. + if (!HasContentTypeHint()) + SetContentType(nsLiteralCString(APPLICATION_OCTET_STREAM)); + } else { + nsAutoCString contentType; + rv = MakeFileInputStream(file, stream, contentType, async); + if (NS_FAILED(rv)) return rv; + + EnableSynthesizedProgressEvents(true); + + // fixup content length and type + + // when we are called from asyncOpen, the content length fixup will be + // performed on a background thread and block the listener invocation via + // ListenerBlockingPromise method + if (!async && mContentLength < 0) { + rv = FixupContentLength(false); + if (NS_FAILED(rv)) { + return rv; + } + } + + if (!contentType.IsEmpty()) { + SetContentType(contentType); + } + } + + *result = nullptr; + stream.swap(*result); + return NS_OK; +} + +nsresult nsFileChannel::ListenerBlockingPromise(BlockingPromise** aPromise) { + NS_ENSURE_ARG(aPromise); + *aPromise = nullptr; + + if (mContentLength >= 0) { + return NS_OK; + } + + nsCOMPtr sts( + do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID)); + if (!sts) { + return FixupContentLength(true); + } + + RefPtr taskQueue = new TaskQueue(sts.forget()); + RefPtr self = this; + RefPtr promise = + mozilla::InvokeAsync(taskQueue, __func__, [self{std::move(self)}]() { + nsresult rv = self->FixupContentLength(true); + if (NS_FAILED(rv)) { + return BlockingPromise::CreateAndReject(rv, __func__); + } + return BlockingPromise::CreateAndResolve(NS_OK, __func__); + }); + + promise.forget(aPromise); + return NS_OK; +} + +nsresult nsFileChannel::FixupContentLength(bool async) { + MOZ_ASSERT(mContentLength < 0); + + nsCOMPtr file; + nsresult rv = GetFile(getter_AddRefs(file)); + if (NS_FAILED(rv)) { + return rv; + } + + int64_t size; + rv = file->GetFileSize(&size); + if (NS_FAILED(rv)) { + if (async && (NS_ERROR_FILE_NOT_FOUND == rv || + NS_ERROR_FILE_TARGET_DOES_NOT_EXIST == rv)) { + size = 0; + } else { + return rv; + } + } + mContentLength = size; + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsFileChannel::nsISupports + +NS_IMPL_ISUPPORTS_INHERITED(nsFileChannel, nsBaseChannel, nsIUploadChannel, + nsIFileChannel) + +//----------------------------------------------------------------------------- +// nsFileChannel::nsIFileChannel + +NS_IMETHODIMP +nsFileChannel::GetFile(nsIFile** file) { + nsCOMPtr fileURL = do_QueryInterface(URI()); + NS_ENSURE_STATE(fileURL); + + // This returns a cloned nsIFile + return fileURL->GetFile(file); +} + +//----------------------------------------------------------------------------- +// nsFileChannel::nsIUploadChannel + +NS_IMETHODIMP +nsFileChannel::SetUploadStream(nsIInputStream* stream, + const nsACString& contentType, + int64_t contentLength) { + NS_ENSURE_TRUE(!Pending(), NS_ERROR_IN_PROGRESS); + + if ((mUploadStream = stream)) { + mUploadLength = contentLength; + if (mUploadLength < 0) { + // Make sure we know how much data we are uploading. + uint64_t avail; + nsresult rv = mUploadStream->Available(&avail); + if (NS_FAILED(rv)) return rv; + // if this doesn't fit in the javascript MAX_SAFE_INTEGER + // pretend we don't know the size + mUploadLength = InScriptableRange(avail) ? avail : -1; + } + } else { + mUploadLength = -1; + } + return NS_OK; +} + +NS_IMETHODIMP +nsFileChannel::GetUploadStream(nsIInputStream** result) { + NS_IF_ADDREF(*result = mUploadStream); + return NS_OK; +} diff --git a/netwerk/protocol/file/nsFileChannel.h b/netwerk/protocol/file/nsFileChannel.h new file mode 100644 index 0000000000..dcf0646f03 --- /dev/null +++ b/netwerk/protocol/file/nsFileChannel.h @@ -0,0 +1,53 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cin: */ +/* 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/. */ + +#ifndef nsFileChannel_h__ +#define nsFileChannel_h__ + +#include "nsBaseChannel.h" +#include "nsIFileChannel.h" +#include "nsIUploadChannel.h" + +class nsFileChannel : public nsBaseChannel, + public nsIFileChannel, + public nsIUploadChannel { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_NSIFILECHANNEL + NS_DECL_NSIUPLOADCHANNEL + + explicit nsFileChannel(nsIURI* uri); + + nsresult Init(); + + protected: + ~nsFileChannel() = default; + + // Called to construct a blocking file input stream for the given file. This + // method also returns a best guess at the content-type for the data stream. + // NOTE: If the channel has a type hint set, contentType will be left + // untouched. The caller should not use it in that case. + [[nodiscard]] nsresult MakeFileInputStream(nsIFile* file, + nsCOMPtr& stream, + nsCString& contentType, + bool async); + + [[nodiscard]] virtual nsresult OpenContentStream( + bool async, nsIInputStream** result, nsIChannel** channel) override; + + // Implementing the pump blocking promise to fixup content length on a + // background thread prior to calling on mListener + virtual nsresult ListenerBlockingPromise(BlockingPromise** promise) override; + + private: + nsresult FixupContentLength(bool async); + + nsCOMPtr mUploadStream; + int64_t mUploadLength; + nsCOMPtr mFileURI; +}; + +#endif // !nsFileChannel_h__ diff --git a/netwerk/protocol/file/nsFileProtocolHandler.cpp b/netwerk/protocol/file/nsFileProtocolHandler.cpp new file mode 100644 index 0000000000..d1ab886a0f --- /dev/null +++ b/netwerk/protocol/file/nsFileProtocolHandler.cpp @@ -0,0 +1,277 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +// vim:ts=4 sw=2 sts=2 et cin: +/* 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 "nsIFile.h" +#include "nsFileProtocolHandler.h" +#include "nsFileChannel.h" +#include "nsStandardURL.h" +#include "nsURLHelper.h" +#include "nsIURIMutator.h" + +#include "nsNetUtil.h" + +#include "FileChannelChild.h" + +#include "mozilla/ResultExtensions.h" + +// URL file handling, copied and modified from +// xpfe/components/bookmarks/src/nsBookmarksService.cpp +#ifdef XP_WIN +# include +# include +# include "nsIFileURL.h" +# ifdef CompareString +# undef CompareString +# endif +#endif + +// URL file handling for freedesktop.org +#ifdef XP_UNIX +# include "nsINIParser.h" +# define DESKTOP_ENTRY_SECTION "Desktop Entry" +#endif + +//----------------------------------------------------------------------------- + +nsresult nsFileProtocolHandler::Init() { return NS_OK; } + +NS_IMPL_ISUPPORTS(nsFileProtocolHandler, nsIFileProtocolHandler, + nsIProtocolHandler, nsISupportsWeakReference) + +//----------------------------------------------------------------------------- +// nsIProtocolHandler methods: + +#if defined(XP_WIN) +NS_IMETHODIMP +nsFileProtocolHandler::ReadURLFile(nsIFile* aFile, nsIURI** aURI) { + nsAutoString path; + nsresult rv = aFile->GetPath(path); + if (NS_FAILED(rv)) return rv; + + if (path.Length() < 4) return NS_ERROR_NOT_AVAILABLE; + if (!StringTail(path, 4).LowerCaseEqualsLiteral(".url")) + return NS_ERROR_NOT_AVAILABLE; + + HRESULT result; + + rv = NS_ERROR_NOT_AVAILABLE; + + IUniformResourceLocatorW* urlLink = nullptr; + result = + ::CoCreateInstance(CLSID_InternetShortcut, nullptr, CLSCTX_INPROC_SERVER, + IID_IUniformResourceLocatorW, (void**)&urlLink); + if (SUCCEEDED(result) && urlLink) { + IPersistFile* urlFile = nullptr; + result = urlLink->QueryInterface(IID_IPersistFile, (void**)&urlFile); + if (SUCCEEDED(result) && urlFile) { + result = urlFile->Load(path.get(), STGM_READ); + if (SUCCEEDED(result)) { + LPWSTR lpTemp = nullptr; + + // The URL this method will give us back seems to be already + // escaped. Hence, do not do escaping of our own. + result = urlLink->GetURL(&lpTemp); + if (SUCCEEDED(result) && lpTemp) { + rv = NS_NewURI(aURI, nsDependentString(lpTemp)); + // free the string that GetURL alloc'd + CoTaskMemFree(lpTemp); + } + } + urlFile->Release(); + } + urlLink->Release(); + } + return rv; +} + +#elif defined(XP_UNIX) +NS_IMETHODIMP +nsFileProtocolHandler::ReadURLFile(nsIFile* aFile, nsIURI** aURI) { + // We only support desktop files that end in ".desktop" like the spec says: + // http://standards.freedesktop.org/desktop-entry-spec/latest/ar01s02.html + nsAutoCString leafName; + nsresult rv = aFile->GetNativeLeafName(leafName); + if (NS_FAILED(rv) || !StringEndsWith(leafName, ".desktop"_ns)) + return NS_ERROR_NOT_AVAILABLE; + + bool isFile = false; + rv = aFile->IsFile(&isFile); + if (NS_FAILED(rv) || !isFile) { + return NS_ERROR_NOT_AVAILABLE; + } + + nsINIParser parser; + rv = parser.Init(aFile); + if (NS_FAILED(rv)) return rv; + + nsAutoCString type; + parser.GetString(DESKTOP_ENTRY_SECTION, "Type", type); + if (!type.EqualsLiteral("Link")) return NS_ERROR_NOT_AVAILABLE; + + nsAutoCString url; + rv = parser.GetString(DESKTOP_ENTRY_SECTION, "URL", url); + if (NS_FAILED(rv) || url.IsEmpty()) return NS_ERROR_NOT_AVAILABLE; + + return NS_NewURI(aURI, url); +} + +#else // other platforms +NS_IMETHODIMP +nsFileProtocolHandler::ReadURLFile(nsIFile* aFile, nsIURI** aURI) { + return NS_ERROR_NOT_AVAILABLE; +} +#endif // ReadURLFile() + +NS_IMETHODIMP +nsFileProtocolHandler::ReadShellLink(nsIFile* aFile, nsIURI** aURI) { +#if defined(XP_WIN) + nsAutoString path; + MOZ_TRY(aFile->GetPath(path)); + + if (path.Length() < 4 || + !StringTail(path, 4).LowerCaseEqualsLiteral(".lnk")) { + return NS_ERROR_NOT_AVAILABLE; + } + RefPtr persistFile; + RefPtr shellLink; + WCHAR lpTemp[MAX_PATH]; + // Get a pointer to the IPersistFile interface. + if (FAILED(CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER, + IID_IShellLinkW, getter_AddRefs(shellLink))) || + FAILED(shellLink->QueryInterface(IID_IPersistFile, + getter_AddRefs(persistFile))) || + FAILED(persistFile->Load(path.get(), STGM_READ)) || + FAILED(shellLink->Resolve(nullptr, SLR_NO_UI)) || + FAILED(shellLink->GetPath(lpTemp, MAX_PATH, nullptr, SLGP_UNCPRIORITY))) { + return NS_ERROR_FAILURE; + } + nsCOMPtr linkedFile; + MOZ_TRY(NS_NewLocalFile(nsDependentString(lpTemp), false, + getter_AddRefs(linkedFile))); + return NS_NewFileURI(aURI, linkedFile); +#else + return NS_ERROR_NOT_AVAILABLE; +#endif +} + +NS_IMETHODIMP +nsFileProtocolHandler::GetScheme(nsACString& result) { + result.AssignLiteral("file"); + return NS_OK; +} + +NS_IMETHODIMP +nsFileProtocolHandler::GetDefaultPort(int32_t* result) { + *result = -1; // no port for file: URLs + return NS_OK; +} + +NS_IMETHODIMP +nsFileProtocolHandler::GetProtocolFlags(uint32_t* result) { + *result = URI_NOAUTH | URI_IS_LOCAL_FILE | URI_IS_LOCAL_RESOURCE | + URI_IS_POTENTIALLY_TRUSTWORTHY; + return NS_OK; +} + +NS_IMETHODIMP +nsFileProtocolHandler::NewChannel(nsIURI* uri, nsILoadInfo* aLoadInfo, + nsIChannel** result) { + nsresult rv; + + RefPtr chan; + if (IsNeckoChild()) { + chan = new mozilla::net::FileChannelChild(uri); + } else { + chan = new nsFileChannel(uri); + } + + // set the loadInfo on the new channel ; must do this + // before calling Init() on it, since it needs the load + // info be already set. + rv = chan->SetLoadInfo(aLoadInfo); + if (NS_FAILED(rv)) { + return rv; + } + + rv = chan->Init(); + if (NS_FAILED(rv)) { + return rv; + } + + chan.forget(result); + return NS_OK; +} + +NS_IMETHODIMP +nsFileProtocolHandler::AllowPort(int32_t port, const char* scheme, + bool* result) { + // don't override anything. + *result = false; + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsIFileProtocolHandler methods: + +NS_IMETHODIMP +nsFileProtocolHandler::NewFileURI(nsIFile* aFile, nsIURI** aResult) { + NS_ENSURE_ARG_POINTER(aFile); + + RefPtr file(aFile); + // NOTE: the origin charset is assigned the value of the platform + // charset by the SetFile method. + return NS_MutateURI(new nsStandardURL::Mutator()) + .Apply(NS_MutatorMethod(&nsIFileURLMutator::SetFile, file)) + .Finalize(aResult); +} + +NS_IMETHODIMP +nsFileProtocolHandler::NewFileURIMutator(nsIFile* aFile, + nsIURIMutator** aResult) { + NS_ENSURE_ARG_POINTER(aFile); + nsresult rv; + + nsCOMPtr mutator = new nsStandardURL::Mutator(); + nsCOMPtr fileMutator = do_QueryInterface(mutator, &rv); + if (NS_FAILED(rv)) { + return rv; + } + + // NOTE: the origin charset is assigned the value of the platform + // charset by the SetFile method. + rv = fileMutator->SetFile(aFile); + if (NS_FAILED(rv)) { + return rv; + } + + mutator.forget(aResult); + return NS_OK; +} + +NS_IMETHODIMP +nsFileProtocolHandler::GetURLSpecFromFile(nsIFile* file, nsACString& result) { + NS_ENSURE_ARG_POINTER(file); + return net_GetURLSpecFromFile(file, result); +} + +NS_IMETHODIMP +nsFileProtocolHandler::GetURLSpecFromActualFile(nsIFile* file, + nsACString& result) { + NS_ENSURE_ARG_POINTER(file); + return net_GetURLSpecFromActualFile(file, result); +} + +NS_IMETHODIMP +nsFileProtocolHandler::GetURLSpecFromDir(nsIFile* file, nsACString& result) { + NS_ENSURE_ARG_POINTER(file); + return net_GetURLSpecFromDir(file, result); +} + +NS_IMETHODIMP +nsFileProtocolHandler::GetFileFromURLSpec(const nsACString& spec, + nsIFile** result) { + return net_GetFileFromURLSpec(spec, result); +} diff --git a/netwerk/protocol/file/nsFileProtocolHandler.h b/netwerk/protocol/file/nsFileProtocolHandler.h new file mode 100644 index 0000000000..ff2b30634a --- /dev/null +++ b/netwerk/protocol/file/nsFileProtocolHandler.h @@ -0,0 +1,28 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef nsFileProtocolHandler_h__ +#define nsFileProtocolHandler_h__ + +#include "nsIFileProtocolHandler.h" +#include "nsWeakReference.h" + +class nsIURIMutator; + +class nsFileProtocolHandler : public nsIFileProtocolHandler, + public nsSupportsWeakReference { + virtual ~nsFileProtocolHandler() = default; + + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIPROTOCOLHANDLER + NS_DECL_NSIFILEPROTOCOLHANDLER + + nsFileProtocolHandler() = default; + + [[nodiscard]] nsresult Init(); +}; + +#endif // !nsFileProtocolHandler_h__ diff --git a/netwerk/protocol/file/nsIFileChannel.idl b/netwerk/protocol/file/nsIFileChannel.idl new file mode 100644 index 0000000000..e5fcdecb4c --- /dev/null +++ b/netwerk/protocol/file/nsIFileChannel.idl @@ -0,0 +1,17 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* 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 "nsISupports.idl" + +interface nsIFile; + +/** + * nsIFileChannel + */ +[scriptable, uuid(06169120-136d-45a5-b535-498f1f755ab7)] +interface nsIFileChannel : nsISupports +{ + readonly attribute nsIFile file; +}; diff --git a/netwerk/protocol/file/nsIFileProtocolHandler.idl b/netwerk/protocol/file/nsIFileProtocolHandler.idl new file mode 100644 index 0000000000..7ca9f6cf29 --- /dev/null +++ b/netwerk/protocol/file/nsIFileProtocolHandler.idl @@ -0,0 +1,87 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* 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 "nsIProtocolHandler.idl" + +interface nsIFile; +interface nsIURIMutator; + +[scriptable, uuid(1fb25bd5-4354-4dcd-8d97-621b7b3ed2e4)] +interface nsIFileProtocolHandler : nsIProtocolHandler +{ + /** + * This method constructs a new file URI + * + * @param aFile nsIFile + * @return reference to a new nsIURI object + */ + nsIURI newFileURI(in nsIFile aFile); + + /** + * This method constructs a new file URI, and returns a URI mutator + * that has not yet been finalized, allowing the URI to be changed without + * being cloned. + * + * @param aFile nsIFile + * @return reference to a new nsIURIMutator object + */ + nsIURIMutator newFileURIMutator(in nsIFile file); + + /** + * Converts the nsIFile to the corresponding URL string. NOTE: under + * some platforms this is a lossy conversion (e.g., Mac Carbon build). + * If the nsIFile is a local file, then the result will be a file:// + * URL string. + * + * The resulting string may contain URL-escaped characters. + * NOTE: Callers should use getURLSpecFromActualFile or + * getURLSpecFromDirFile if possible, for performance reasons. + */ + AUTF8String getURLSpecFromFile(in nsIFile file); + + /** + * Converts the nsIFile to the corresponding URL string. Should + * only be called on files which are not directories. Otherwise + * identical to getURLSpecFromFile, but is usually more efficient. + * WARNING: This restriction may not be enforced at runtime! + */ + AUTF8String getURLSpecFromActualFile(in nsIFile file); + + /** + * Converts the nsIFile to the corresponding URL string. Should + * only be called on files which are directories. Otherwise + * identical to getURLSpecFromFile, but is usually more efficient. + * WARNING: This restriction may not be enforced at runtime! + */ + AUTF8String getURLSpecFromDir(in nsIFile file); + + /** + * Converts the URL string into the corresponding nsIFile if possible. + * A local file will be created if the URL string begins with file://. + */ + nsIFile getFileFromURLSpec(in AUTF8String url); + + /** + * Takes a local file and tries to interpret it as an internet shortcut + * (e.g. .url files on windows). + * @param file The local file to read + * @return The URI the file refers to + * + * @throw NS_ERROR_NOT_AVAILABLE if the OS does not support such files. + * @throw NS_ERROR_NOT_AVAILABLE if this file is not an internet shortcut. + */ + nsIURI readURLFile(in nsIFile file); + + /** + * Takes a local file and tries to interpret it as a shell link file + * (.lnk files on Windows) + * @param file The local file to read + * @return The URI the file refers to + * + * @throw NS_ERROR_NOT_AVAILABLE if the OS does not support such files. + * @throw NS_ERROR_NOT_AVAILABLE if this file is not a shell link. + */ + nsIURI readShellLink(in nsIFile file); +}; diff --git a/netwerk/protocol/ftp/FTPChannelChild.cpp b/netwerk/protocol/ftp/FTPChannelChild.cpp new file mode 100644 index 0000000000..63edaff4cd --- /dev/null +++ b/netwerk/protocol/ftp/FTPChannelChild.cpp @@ -0,0 +1,541 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et 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/net/NeckoChild.h" +#include "mozilla/net/FTPChannelChild.h" +#include "mozilla/dom/ContentChild.h" +#include "mozilla/dom/DocGroup.h" +#include "mozilla/dom/BrowserChild.h" +#include "nsContentUtils.h" +#include "nsFtpProtocolHandler.h" +#include "nsIBrowserChild.h" +#include "nsStringStream.h" +#include "nsNetUtil.h" +#include "base/compiler_specific.h" +#include "mozilla/ipc/IPCStreamUtils.h" +#include "mozilla/ipc/URIUtils.h" +#include "SerializedLoadContext.h" +#include "mozilla/ipc/BackgroundUtils.h" +#include "nsIURIMutator.h" +#include "nsContentSecurityManager.h" +#include "mozilla/ScopeExit.h" + +using mozilla::dom::ContentChild; +using namespace mozilla::ipc; + +#undef LOG +#define LOG(args) MOZ_LOG(gFTPLog, mozilla::LogLevel::Debug, args) + +namespace mozilla { +namespace net { + +FTPChannelChild::FTPChannelChild(nsIURI* aUri) + : mIPCOpen(false), + mEventQ(new ChannelEventQueue(static_cast(this))), + mCanceled(false), + mSuspendCount(0), + mIsPending(false), + mLastModifiedTime(0), + mStartPos(0), + mSuspendSent(false) { + LOG(("Creating FTPChannelChild @%p\n", this)); + // grab a reference to the handler to ensure that it doesn't go away. + NS_ADDREF(gFtpHandler); + SetURI(aUri); + + // We could support thread retargeting, but as long as we're being driven by + // IPDL on the main thread it doesn't buy us anything. + DisallowThreadRetargeting(); +} + +FTPChannelChild::~FTPChannelChild() { + LOG(("Destroying FTPChannelChild @%p\n", this)); + gFtpHandler->Release(); +} + +void FTPChannelChild::AddIPDLReference() { + MOZ_ASSERT(!mIPCOpen, "Attempt to retain more than one IPDL reference"); + mIPCOpen = true; + AddRef(); +} + +void FTPChannelChild::ReleaseIPDLReference() { + MOZ_ASSERT(mIPCOpen, "Attempt to release nonexistent IPDL reference"); + mIPCOpen = false; + Release(); +} + +//----------------------------------------------------------------------------- +// FTPChannelChild::nsISupports +//----------------------------------------------------------------------------- + +NS_IMPL_ISUPPORTS_INHERITED(FTPChannelChild, nsBaseChannel, nsIFTPChannel, + nsIUploadChannel, nsIResumableChannel, + nsIProxiedChannel, nsIChildChannel) + +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +FTPChannelChild::GetLastModifiedTime(PRTime* aLastModifiedTime) { + *aLastModifiedTime = mLastModifiedTime; + return NS_OK; +} + +NS_IMETHODIMP +FTPChannelChild::SetLastModifiedTime(PRTime aLastModifiedTime) { + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP +FTPChannelChild::ResumeAt(uint64_t aStartPos, const nsACString& aEntityID) { + NS_ENSURE_TRUE(!mIsPending, NS_ERROR_IN_PROGRESS); + mStartPos = aStartPos; + mEntityID = aEntityID; + return NS_OK; +} + +NS_IMETHODIMP +FTPChannelChild::GetEntityID(nsACString& aEntityID) { + aEntityID = mEntityID; + return NS_OK; +} + +NS_IMETHODIMP +FTPChannelChild::GetProxyInfo(nsIProxyInfo** aProxyInfo) { DROP_DEAD(); } + +NS_IMETHODIMP FTPChannelChild::GetHttpProxyConnectResponseCode( + int32_t* aResponseCode) { + DROP_DEAD(); +} + +NS_IMETHODIMP +FTPChannelChild::SetUploadStream(nsIInputStream* aStream, + const nsACString& aContentType, + int64_t aContentLength) { + NS_ENSURE_TRUE(!mIsPending, NS_ERROR_IN_PROGRESS); + mUploadStream = aStream; + // NOTE: contentLength is intentionally ignored here. + return NS_OK; +} + +NS_IMETHODIMP +FTPChannelChild::GetUploadStream(nsIInputStream** aStream) { + NS_ENSURE_ARG_POINTER(aStream); + *aStream = mUploadStream; + NS_IF_ADDREF(*aStream); + return NS_OK; +} + +NS_IMETHODIMP +FTPChannelChild::AsyncOpen(nsIStreamListener* aListener) { + nsCOMPtr listener = aListener; + nsresult rv = + nsContentSecurityManager::doContentSecurityCheck(this, listener); + NS_ENSURE_SUCCESS(rv, rv); + + LOG(("FTPChannelChild::AsyncOpen [this=%p]\n", this)); + + NS_ENSURE_TRUE((gNeckoChild), NS_ERROR_FAILURE); + NS_ENSURE_TRUE( + !static_cast(gNeckoChild->Manager())->IsShuttingDown(), + NS_ERROR_FAILURE); + NS_ENSURE_ARG_POINTER(listener); + NS_ENSURE_TRUE(!mIsPending, NS_ERROR_IN_PROGRESS); + NS_ENSURE_TRUE(!mWasOpened, NS_ERROR_ALREADY_OPENED); + + // Port checked in parent, but duplicate here so we can return with error + // immediately, as we've done since before e10s. + rv = NS_CheckPortSafety(nsBaseChannel::URI()); // Need to disambiguate, + // because in the child ipdl, + // a typedef URI is defined... + if (NS_FAILED(rv)) return rv; + + mozilla::dom::BrowserChild* browserChild = nullptr; + nsCOMPtr iBrowserChild; + NS_QueryNotificationCallbacks(mCallbacks, mLoadGroup, + NS_GET_IID(nsIBrowserChild), + getter_AddRefs(iBrowserChild)); + GetCallback(iBrowserChild); + if (iBrowserChild) { + browserChild = + static_cast(iBrowserChild.get()); + } + + mListener = listener; + + // add ourselves to the load group. + if (mLoadGroup) mLoadGroup->AddRequest(this, nullptr); + + mozilla::ipc::AutoIPCStream autoStream; + autoStream.Serialize(mUploadStream, + static_cast(gNeckoChild->Manager())); + + uint32_t loadFlags = 0; + GetLoadFlags(&loadFlags); + + FTPChannelOpenArgs openArgs; + SerializeURI(nsBaseChannel::URI(), openArgs.uri()); + openArgs.startPos() = mStartPos; + openArgs.entityID() = mEntityID; + openArgs.uploadStream() = autoStream.TakeOptionalValue(); + openArgs.loadFlags() = loadFlags; + + nsCOMPtr loadInfo = LoadInfo(); + rv = mozilla::ipc::LoadInfoToLoadInfoArgs(loadInfo, &openArgs.loadInfo()); + NS_ENSURE_SUCCESS(rv, rv); + + // This must happen before the constructor message is sent. + SetupNeckoTarget(); + + gNeckoChild->SendPFTPChannelConstructor( + this, browserChild, IPC::SerializedLoadContext(this), openArgs); + + // The socket transport layer in the chrome process now has a logical ref to + // us until OnStopRequest is called. + AddIPDLReference(); + + mIsPending = true; + mWasOpened = true; + + return rv; +} + +NS_IMETHODIMP +FTPChannelChild::IsPending(bool* aResult) { + *aResult = mIsPending; + return NS_OK; +} + +nsresult FTPChannelChild::OpenContentStream(bool aAsync, + nsIInputStream** aStream, + nsIChannel** aChannel) { + MOZ_CRASH("FTPChannel*Child* should never have OpenContentStream called!"); + return NS_OK; +} + +//----------------------------------------------------------------------------- +// FTPChannelChild::PFTPChannelChild +//----------------------------------------------------------------------------- + +mozilla::ipc::IPCResult FTPChannelChild::RecvOnStartRequest( + const nsresult& aChannelStatus, const int64_t& aContentLength, + const nsCString& aContentType, const PRTime& aLastModified, + const nsCString& aEntityID, const URIParams& aURI) { + LOG(("FTPChannelChild::RecvOnStartRequest [this=%p]\n", this)); + + mEventQ->RunOrEnqueue(new NeckoTargetChannelFunctionEvent( + this, [self = UnsafePtr(this), aChannelStatus, + aContentLength, aContentType, aLastModified, aEntityID, aURI]() { + self->DoOnStartRequest(aChannelStatus, aContentLength, aContentType, + aLastModified, aEntityID, aURI); + })); + return IPC_OK(); +} + +void FTPChannelChild::DoOnStartRequest(const nsresult& aChannelStatus, + const int64_t& aContentLength, + const nsCString& aContentType, + const PRTime& aLastModified, + const nsCString& aEntityID, + const URIParams& aURI) { + mDuringOnStart = true; + RefPtr self = this; + auto clearDuringFlag = + mozilla::MakeScopeExit([self] { self->mDuringOnStart = false; }); + + LOG(("FTPChannelChild::DoOnStartRequest [this=%p]\n", this)); + + if (!mCanceled && NS_SUCCEEDED(mStatus)) { + mStatus = aChannelStatus; + } + + mContentLength = aContentLength; + SetContentType(aContentType); + mLastModifiedTime = aLastModified; + mEntityID = aEntityID; + + nsCString spec; + nsCOMPtr uri = DeserializeURI(aURI); + nsresult rv = uri->GetSpec(spec); + if (NS_SUCCEEDED(rv)) { + // Changes nsBaseChannel::URI() + rv = NS_MutateURI(mURI).SetSpec(spec).Finalize(mURI); + if (NS_FAILED(rv)) { + Cancel(rv); + } + } else { + Cancel(rv); + } + + AutoEventEnqueuer ensureSerialDispatch(mEventQ); + rv = mListener->OnStartRequest(this); + if (NS_FAILED(rv)) Cancel(rv); +} + +mozilla::ipc::IPCResult FTPChannelChild::RecvOnDataAvailable( + const nsresult& aChannelStatus, const nsCString& aData, + const uint64_t& aOffset, const uint32_t& aCount) { + LOG(("FTPChannelChild::RecvOnDataAvailable [this=%p]\n", this)); + + mEventQ->RunOrEnqueue(new NeckoTargetChannelFunctionEvent( + this, [self = UnsafePtr(this), aChannelStatus, aData, + aOffset, aCount]() { + self->DoOnDataAvailable(aChannelStatus, aData, aOffset, aCount); + })); + + return IPC_OK(); +} + +void FTPChannelChild::DoOnDataAvailable(const nsresult& aChannelStatus, + const nsCString& aData, + const uint64_t& aOffset, + const uint32_t& aCount) { + LOG(("FTPChannelChild::DoOnDataAvailable [this=%p]\n", this)); + + if (!mCanceled && NS_SUCCEEDED(mStatus)) { + mStatus = aChannelStatus; + } + + if (mCanceled) { + return; + } + + // NOTE: the OnDataAvailable contract requires the client to read all the data + // in the inputstream. This code relies on that ('data' will go away after + // this function). Apparently the previous, non-e10s behavior was to actually + // support only reading part of the data, allowing later calls to read the + // rest. + nsCOMPtr stringStream; + nsresult rv = + NS_NewByteInputStream(getter_AddRefs(stringStream), + Span(aData).To(aCount), NS_ASSIGNMENT_DEPEND); + if (NS_FAILED(rv)) { + Cancel(rv); + return; + } + + AutoEventEnqueuer ensureSerialDispatch(mEventQ); + rv = mListener->OnDataAvailable(this, stringStream, aOffset, aCount); + if (NS_FAILED(rv)) { + Cancel(rv); + } + stringStream->Close(); +} + +mozilla::ipc::IPCResult FTPChannelChild::RecvOnStopRequest( + const nsresult& aChannelStatus, const nsCString& aErrorMsg, + const bool& aUseUTF8) { + LOG(("FTPChannelChild::RecvOnStopRequest [this=%p status=%" PRIx32 "]\n", + this, static_cast(aChannelStatus))); + + mEventQ->RunOrEnqueue(new NeckoTargetChannelFunctionEvent( + this, [self = UnsafePtr(this), aChannelStatus, aErrorMsg, + aUseUTF8]() { + self->DoOnStopRequest(aChannelStatus, aErrorMsg, aUseUTF8); + })); + return IPC_OK(); +} + +void FTPChannelChild::DoOnStopRequest(const nsresult& aChannelStatus, + const nsCString& aErrorMsg, + bool aUseUTF8) { + LOG(("FTPChannelChild::DoOnStopRequest [this=%p status=%" PRIx32 "]\n", this, + static_cast(aChannelStatus))); + + if (!mCanceled) mStatus = aChannelStatus; + + { // Ensure that all queued ipdl events are dispatched before + // we initiate protocol deletion below. + mIsPending = false; + AutoEventEnqueuer ensureSerialDispatch(mEventQ); + (void)mListener->OnStopRequest(this, aChannelStatus); + + mListener = nullptr; + + if (mLoadGroup) { + mLoadGroup->RemoveRequest(this, nullptr, aChannelStatus); + } + } + + // This calls NeckoChild::DeallocPFTPChannelChild(), which deletes |this| if + // IPDL holds the last reference. Don't rely on |this| existing after here! + Send__delete__(this); +} + +mozilla::ipc::IPCResult FTPChannelChild::RecvFailedAsyncOpen( + const nsresult& aStatusCode) { + LOG(("FTPChannelChild::RecvFailedAsyncOpen [this=%p status=%" PRIx32 "]\n", + this, static_cast(aStatusCode))); + mEventQ->RunOrEnqueue(new NeckoTargetChannelFunctionEvent( + this, [self = UnsafePtr(this), aStatusCode]() { + self->DoFailedAsyncOpen(aStatusCode); + })); + return IPC_OK(); +} + +void FTPChannelChild::DoFailedAsyncOpen(const nsresult& aStatusCode) { + LOG(("FTPChannelChild::DoFailedAsyncOpen [this=%p status=%" PRIx32 "]\n", + this, static_cast(aStatusCode))); + mStatus = aStatusCode; + + if (mLoadGroup) { + mLoadGroup->RemoveRequest(this, nullptr, aStatusCode); + } + + if (mListener) { + mListener->OnStartRequest(this); + mIsPending = false; + mListener->OnStopRequest(this, aStatusCode); + } else { + mIsPending = false; + } + + mListener = nullptr; + + if (mIPCOpen) { + Send__delete__(this); + } +} + +mozilla::ipc::IPCResult FTPChannelChild::RecvDeleteSelf() { + mEventQ->RunOrEnqueue(new NeckoTargetChannelFunctionEvent( + this, + [self = UnsafePtr(this)]() { self->DoDeleteSelf(); })); + return IPC_OK(); +} + +void FTPChannelChild::DoDeleteSelf() { + if (mIPCOpen) { + Send__delete__(this); + } +} + +NS_IMETHODIMP +FTPChannelChild::Cancel(nsresult aStatus) { + LOG(("FTPChannelChild::Cancel [this=%p]\n", this)); + if (mCanceled) { + return NS_OK; + } + + mCanceled = true; + mStatus = aStatus; + if (mIPCOpen) { + SendCancel(aStatus); + } + return NS_OK; +} + +NS_IMETHODIMP +FTPChannelChild::Suspend() { + NS_ENSURE_TRUE(mIPCOpen, NS_ERROR_NOT_AVAILABLE); + + LOG(("FTPChannelChild::Suspend [this=%p]\n", this)); + + // SendSuspend only once, when suspend goes from 0 to 1. + if (!mSuspendCount++) { + SendSuspend(); + mSuspendSent = true; + } + mEventQ->Suspend(); + + return NS_OK; +} + +NS_IMETHODIMP +FTPChannelChild::Resume() { + NS_ENSURE_TRUE(mIPCOpen, NS_ERROR_NOT_AVAILABLE); + + LOG(("FTPChannelChild::Resume [this=%p]\n", this)); + + // SendResume only once, when suspend count drops to 0. + if (!--mSuspendCount && mSuspendSent) { + SendResume(); + } + mEventQ->Resume(); + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// FTPChannelChild::nsIChildChannel +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +FTPChannelChild::ConnectParent(uint32_t aId) { + NS_ENSURE_TRUE((gNeckoChild), NS_ERROR_FAILURE); + NS_ENSURE_TRUE( + !static_cast(gNeckoChild->Manager())->IsShuttingDown(), + NS_ERROR_FAILURE); + + LOG(("FTPChannelChild::ConnectParent [this=%p]\n", this)); + + mozilla::dom::BrowserChild* browserChild = nullptr; + nsCOMPtr iBrowserChild; + NS_QueryNotificationCallbacks(mCallbacks, mLoadGroup, + NS_GET_IID(nsIBrowserChild), + getter_AddRefs(iBrowserChild)); + GetCallback(iBrowserChild); + if (iBrowserChild) { + browserChild = + static_cast(iBrowserChild.get()); + } + + // This must happen before the constructor message is sent. + SetupNeckoTarget(); + + // The socket transport in the chrome process now holds a logical ref to us + // until OnStopRequest, or we do a redirect, or we hit an IPDL error. + AddIPDLReference(); + + FTPChannelConnectArgs connectArgs(aId); + + if (!gNeckoChild->SendPFTPChannelConstructor( + this, browserChild, IPC::SerializedLoadContext(this), connectArgs)) { + return NS_ERROR_FAILURE; + } + + return NS_OK; +} + +NS_IMETHODIMP +FTPChannelChild::CompleteRedirectSetup(nsIStreamListener* aListener) { + LOG(("FTPChannelChild::CompleteRedirectSetup [this=%p]\n", this)); + + NS_ENSURE_TRUE(!mIsPending, NS_ERROR_IN_PROGRESS); + NS_ENSURE_TRUE(!mWasOpened, NS_ERROR_ALREADY_OPENED); + + mIsPending = true; + mWasOpened = true; + mListener = aListener; + + // add ourselves to the load group. + if (mLoadGroup) { + mLoadGroup->AddRequest(this, nullptr); + } + + // We already have an open IPDL connection to the parent. If on-modify-request + // listeners or load group observers canceled us, let the parent handle it + // and send it back to us naturally. + return NS_OK; +} + +void FTPChannelChild::SetupNeckoTarget() { + if (mNeckoTarget) { + return; + } + nsCOMPtr loadInfo = LoadInfo(); + mNeckoTarget = + nsContentUtils::GetEventTargetByLoadInfo(loadInfo, TaskCategory::Network); + if (!mNeckoTarget) { + return; + } + + gNeckoChild->SetEventTargetForActor(this, mNeckoTarget); +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/ftp/FTPChannelChild.h b/netwerk/protocol/ftp/FTPChannelChild.h new file mode 100644 index 0000000000..e9c7dc9f6f --- /dev/null +++ b/netwerk/protocol/ftp/FTPChannelChild.h @@ -0,0 +1,137 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et 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/. */ + +#ifndef mozilla_net_FTPChannelChild_h +#define mozilla_net_FTPChannelChild_h + +#include "mozilla/UniquePtr.h" +#include "mozilla/net/PFTPChannelChild.h" +#include "mozilla/net/ChannelEventQueue.h" +#include "nsBaseChannel.h" +#include "nsIFTPChannel.h" +#include "nsIUploadChannel.h" +#include "nsIProxiedChannel.h" +#include "nsIResumableChannel.h" +#include "nsIChildChannel.h" +#include "nsIEventTarget.h" + +#include "nsIStreamListener.h" +#include "mozilla/net/PrivateBrowsingChannel.h" + +class nsIEventTarget; + +namespace mozilla { + +namespace net { + +// This class inherits logic from nsBaseChannel that is not needed for an +// e10s child channel, but it works. At some point we could slice up +// nsBaseChannel and have a new class that has only the common logic for +// nsFTPChannel/FTPChannelChild. + +class FTPChannelChild final : public PFTPChannelChild, + public nsBaseChannel, + public nsIFTPChannel, + public nsIUploadChannel, + public nsIResumableChannel, + public nsIProxiedChannel, + public nsIChildChannel { + public: + typedef ::nsIStreamListener nsIStreamListener; + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_NSIFTPCHANNEL + NS_DECL_NSIUPLOADCHANNEL + NS_DECL_NSIRESUMABLECHANNEL + NS_DECL_NSIPROXIEDCHANNEL + NS_DECL_NSICHILDCHANNEL + + NS_IMETHOD Cancel(nsresult aStatus) override; + NS_IMETHOD Suspend() override; + NS_IMETHOD Resume() override; + + explicit FTPChannelChild(nsIURI* aUri); + + void AddIPDLReference(); + void ReleaseIPDLReference(); + + NS_IMETHOD AsyncOpen(nsIStreamListener* aListener) override; + + // Note that we handle this ourselves, overriding the nsBaseChannel + // default behavior, in order to be e10s-friendly. + NS_IMETHOD IsPending(bool* aResult) override; + + nsresult OpenContentStream(bool aAsync, nsIInputStream** aStream, + nsIChannel** aChannel) override; + + bool IsSuspended() const; + + protected: + virtual ~FTPChannelChild(); + + mozilla::ipc::IPCResult RecvOnStartRequest(const nsresult& aChannelStatus, + const int64_t& aContentLength, + const nsCString& aContentType, + const PRTime& aLastModified, + const nsCString& aEntityID, + const URIParams& aURI) override; + mozilla::ipc::IPCResult RecvOnDataAvailable(const nsresult& aChannelStatus, + const nsCString& aData, + const uint64_t& aOffset, + const uint32_t& aCount) override; + mozilla::ipc::IPCResult RecvOnStopRequest(const nsresult& aChannelStatus, + const nsCString& aErrorMsg, + const bool& aUseUTF8) override; + mozilla::ipc::IPCResult RecvFailedAsyncOpen( + const nsresult& aStatusCode) override; + mozilla::ipc::IPCResult RecvDeleteSelf() override; + + void DoOnStartRequest(const nsresult& aChannelStatus, + const int64_t& aContentLength, + const nsCString& aContentType, + const PRTime& aLastModified, const nsCString& aEntityID, + const URIParams& aURI); + void DoOnDataAvailable(const nsresult& aChannelStatus, const nsCString& aData, + const uint64_t& aOffset, const uint32_t& aCount); + void DoOnStopRequest(const nsresult& StatusCode, const nsCString& aErrorMsg, + bool aUseUTF8); + void DoFailedAsyncOpen(const nsresult& aStatusCode); + void DoDeleteSelf(); + + void SetupNeckoTarget() override; + + friend class NeckoTargetChannelFunctionEvent; + + private: + nsCOMPtr mUploadStream; + + bool mIPCOpen; + const RefPtr mEventQ; + + bool mCanceled; + uint32_t mSuspendCount; + bool mIsPending; + + // This will only be true while DoOnStartRequest is in progress. + // It is used to enforce that DivertToParent is only called during that time. + bool mDuringOnStart = false; + + PRTime mLastModifiedTime; + uint64_t mStartPos; + nsCString mEntityID; + + // Set if SendSuspend is called. Determines if SendResume is needed when + // diverting callbacks to parent. + bool mSuspendSent; +}; + +inline bool FTPChannelChild::IsSuspended() const { return mSuspendCount != 0; } + +} // namespace net +} // namespace mozilla + +#endif // mozilla_net_FTPChannelChild_h diff --git a/netwerk/protocol/ftp/FTPChannelParent.cpp b/netwerk/protocol/ftp/FTPChannelParent.cpp new file mode 100644 index 0000000000..3bf94fea70 --- /dev/null +++ b/netwerk/protocol/ftp/FTPChannelParent.cpp @@ -0,0 +1,418 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et 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/net/FTPChannelParent.h" +#include "nsStringStream.h" +#include "mozilla/net/ChannelEventQueue.h" +#include "mozilla/dom/BrowserParent.h" +#include "nsFTPChannel.h" +#include "nsNetCID.h" +#include "nsNetUtil.h" +#include "nsQueryObject.h" +#include "nsFtpProtocolHandler.h" +#include "nsIAuthPrompt.h" +#include "nsIAuthPromptProvider.h" +#include "nsIHttpChannelInternal.h" +#include "nsISecureBrowserUI.h" +#include "nsIForcePendingChannel.h" +#include "mozilla/ipc/IPCStreamUtils.h" +#include "mozilla/ipc/URIUtils.h" +#include "mozilla/Unused.h" +#include "SerializedLoadContext.h" +#include "mozilla/ipc/BackgroundUtils.h" +#include "mozilla/LoadInfo.h" +#include "mozilla/dom/ContentParent.h" + +using namespace mozilla::dom; +using namespace mozilla::ipc; + +#undef LOG +#define LOG(args) MOZ_LOG(gFTPLog, mozilla::LogLevel::Debug, args) + +namespace mozilla { +namespace net { + +FTPChannelParent::FTPChannelParent(dom::BrowserParent* aIframeEmbedding, + nsILoadContext* aLoadContext, + PBOverrideStatus aOverrideStatus) + : mIPCClosed(false), + mLoadContext(aLoadContext), + mPBOverride(aOverrideStatus), + mStatus(NS_OK), + mBrowserParent(aIframeEmbedding), + mUseUTF8(false) { + nsIProtocolHandler* handler; + CallGetService(NS_NETWORK_PROTOCOL_CONTRACTID_PREFIX "ftp", &handler); + MOZ_ASSERT(handler, "no ftp handler"); + + mEventQ = new ChannelEventQueue(static_cast(this)); +} + +FTPChannelParent::~FTPChannelParent() { gFtpHandler->Release(); } + +void FTPChannelParent::ActorDestroy(ActorDestroyReason why) { + // We may still have refcount>0 if the channel hasn't called OnStopRequest + // yet, but we must not send any more msgs to child. + mIPCClosed = true; +} + +//----------------------------------------------------------------------------- +// FTPChannelParent::nsISupports +//----------------------------------------------------------------------------- + +NS_IMPL_ISUPPORTS(FTPChannelParent, nsIStreamListener, nsIParentChannel, + nsIInterfaceRequestor, nsIRequestObserver, + nsIChannelEventSink, nsIFTPChannelParentInternal) + +//----------------------------------------------------------------------------- +// FTPChannelParent::PFTPChannelParent +//----------------------------------------------------------------------------- + +//----------------------------------------------------------------------------- +// FTPChannelParent methods +//----------------------------------------------------------------------------- + +bool FTPChannelParent::Init(const FTPChannelCreationArgs& aArgs) { + switch (aArgs.type()) { + case FTPChannelCreationArgs::TFTPChannelOpenArgs: { + const FTPChannelOpenArgs& a = aArgs.get_FTPChannelOpenArgs(); + return DoAsyncOpen(a.uri(), a.startPos(), a.entityID(), a.uploadStream(), + a.loadInfo(), a.loadFlags()); + } + case FTPChannelCreationArgs::TFTPChannelConnectArgs: { + const FTPChannelConnectArgs& cArgs = aArgs.get_FTPChannelConnectArgs(); + return ConnectChannel(cArgs.channelId()); + } + default: + MOZ_ASSERT_UNREACHABLE("unknown open type"); + return false; + } +} + +bool FTPChannelParent::DoAsyncOpen(const URIParams& aURI, + const uint64_t& aStartPos, + const nsCString& aEntityID, + const Maybe& aUploadStream, + const Maybe& aLoadInfoArgs, + const uint32_t& aLoadFlags) { + nsresult rv; + + nsCOMPtr uri = DeserializeURI(aURI); + if (!uri) return false; + +#ifdef DEBUG + LOG(("FTPChannelParent DoAsyncOpen [this=%p uri=%s]\n", this, + uri->GetSpecOrDefault().get())); +#endif + + nsCOMPtr ios(do_GetIOService(&rv)); + if (NS_FAILED(rv)) { + return SendFailedAsyncOpen(rv); + } + + nsCOMPtr loadInfo; + rv = mozilla::ipc::LoadInfoArgsToLoadInfo(aLoadInfoArgs, + getter_AddRefs(loadInfo)); + if (NS_FAILED(rv)) { + return SendFailedAsyncOpen(rv); + } + + OriginAttributes attrs; + rv = loadInfo->GetOriginAttributes(&attrs); + if (NS_FAILED(rv)) { + return SendFailedAsyncOpen(rv); + } + + nsCOMPtr chan; + rv = NS_NewChannelInternal(getter_AddRefs(chan), uri, loadInfo, nullptr, + nullptr, nullptr, aLoadFlags, ios); + + if (NS_FAILED(rv)) return SendFailedAsyncOpen(rv); + + mChannel = chan; + + // later on mChannel may become an HTTP channel (we'll be redirected to one + // if we're using a proxy), but for now this is safe + nsFtpChannel* ftpChan = static_cast(mChannel.get()); + + if (mPBOverride != kPBOverride_Unset) { + ftpChan->SetPrivate(mPBOverride == kPBOverride_Private ? true : false); + } + rv = ftpChan->SetNotificationCallbacks(this); + if (NS_FAILED(rv)) return SendFailedAsyncOpen(rv); + + nsCOMPtr upload = DeserializeIPCStream(aUploadStream); + if (upload) { + // contentType and contentLength are ignored + rv = ftpChan->SetUploadStream(upload, ""_ns, 0); + if (NS_FAILED(rv)) return SendFailedAsyncOpen(rv); + } + + rv = ftpChan->ResumeAt(aStartPos, aEntityID); + if (NS_FAILED(rv)) return SendFailedAsyncOpen(rv); + + rv = ftpChan->AsyncOpen(this); + + if (NS_FAILED(rv)) return SendFailedAsyncOpen(rv); + + return true; +} + +bool FTPChannelParent::ConnectChannel(const uint64_t& channelId) { + nsresult rv; + + LOG(("Looking for a registered channel [this=%p, id=%" PRIx64 "]", this, + channelId)); + + nsCOMPtr channel; + rv = NS_LinkRedirectChannels(channelId, this, getter_AddRefs(channel)); + if (NS_SUCCEEDED(rv)) mChannel = channel; + + LOG((" found channel %p, rv=%08" PRIx32, mChannel.get(), + static_cast(rv))); + + return true; +} + +mozilla::ipc::IPCResult FTPChannelParent::RecvCancel(const nsresult& status) { + if (mChannel) mChannel->Cancel(status); + + return IPC_OK(); +} + +mozilla::ipc::IPCResult FTPChannelParent::RecvSuspend() { + if (mChannel) { + mChannel->Suspend(); + } + return IPC_OK(); +} + +mozilla::ipc::IPCResult FTPChannelParent::RecvResume() { + if (mChannel) { + mChannel->Resume(); + } + return IPC_OK(); +} + +//----------------------------------------------------------------------------- +// FTPChannelParent::nsIRequestObserver +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +FTPChannelParent::OnStartRequest(nsIRequest* aRequest) { + LOG(("FTPChannelParent::OnStartRequest [this=%p]\n", this)); + + nsCOMPtr chan = do_QueryInterface(aRequest); + MOZ_ASSERT(chan); + NS_ENSURE_TRUE(chan, NS_ERROR_UNEXPECTED); + + // Send down any permissions which are relevant to this URL if we are + // performing a document load. + if (!mIPCClosed) { + PContentParent* pcp = Manager()->Manager(); + MOZ_ASSERT(pcp, "We should have a manager if our IPC isn't closed"); + DebugOnly rv = + static_cast(pcp)->AboutToLoadHttpFtpDocumentForChild( + chan); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + + int64_t contentLength; + chan->GetContentLength(&contentLength); + nsCString contentType; + chan->GetContentType(contentType); + nsresult channelStatus = NS_OK; + chan->GetStatus(&channelStatus); + + nsCString entityID; + nsCOMPtr resChan = do_QueryInterface(aRequest); + MOZ_ASSERT( + resChan); // both FTP and HTTP should implement nsIResumableChannel + if (resChan) { + resChan->GetEntityID(entityID); + } + + PRTime lastModified = 0; + nsCOMPtr ftpChan = do_QueryInterface(aRequest); + if (ftpChan) { + ftpChan->GetLastModifiedTime(&lastModified); + } + nsCOMPtr httpChan = do_QueryInterface(aRequest); + if (httpChan) { + Unused << httpChan->GetLastModifiedTime(&lastModified); + } + + URIParams uriparam; + nsCOMPtr uri; + chan->GetURI(getter_AddRefs(uri)); + SerializeURI(uri, uriparam); + + if (mIPCClosed || + !SendOnStartRequest(channelStatus, contentLength, contentType, + lastModified, entityID, uriparam)) { + return NS_ERROR_UNEXPECTED; + } + + return NS_OK; +} + +NS_IMETHODIMP +FTPChannelParent::OnStopRequest(nsIRequest* aRequest, nsresult aStatusCode) { + LOG(("FTPChannelParent::OnStopRequest: [this=%p status=%" PRIu32 "]\n", this, + static_cast(aStatusCode))); + + if (mIPCClosed || !SendOnStopRequest(aStatusCode, mErrorMsg, mUseUTF8)) { + return NS_ERROR_UNEXPECTED; + } + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// FTPChannelParent::nsIStreamListener +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +FTPChannelParent::OnDataAvailable(nsIRequest* aRequest, + nsIInputStream* aInputStream, + uint64_t aOffset, uint32_t aCount) { + LOG(("FTPChannelParent::OnDataAvailable [this=%p]\n", this)); + + nsCString data; + nsresult rv = NS_ReadInputStreamToString(aInputStream, data, aCount); + if (NS_FAILED(rv)) return rv; + + nsresult channelStatus = NS_OK; + mChannel->GetStatus(&channelStatus); + + if (mIPCClosed || !SendOnDataAvailable(channelStatus, data, aOffset, aCount)) + return NS_ERROR_UNEXPECTED; + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// FTPChannelParent::nsIParentChannel +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +FTPChannelParent::SetParentListener(ParentChannelListener* aListener) { + // Do not need ptr to ParentChannelListener. + return NS_OK; +} + +NS_IMETHODIMP +FTPChannelParent::NotifyClassificationFlags(uint32_t aClassificationFlags, + bool aIsThirdParty) { + // One day, this should probably be filled in. + return NS_OK; +} + +NS_IMETHODIMP +FTPChannelParent::NotifyFlashPluginStateChanged( + nsIHttpChannel::FlashPluginState aState) { + // One day, this should probably be filled in. + return NS_OK; +} + +NS_IMETHODIMP +FTPChannelParent::SetClassifierMatchedInfo(const nsACString& aList, + const nsACString& aProvider, + const nsACString& aFullHash) { + // One day, this should probably be filled in. + return NS_OK; +} + +NS_IMETHODIMP +FTPChannelParent::SetClassifierMatchedTrackingInfo( + const nsACString& aLists, const nsACString& aFullHashes) { + // One day, this should probably be filled in. + return NS_OK; +} + +NS_IMETHODIMP +FTPChannelParent::Delete() { + if (mIPCClosed || !SendDeleteSelf()) return NS_ERROR_UNEXPECTED; + + return NS_OK; +} + +NS_IMETHODIMP +FTPChannelParent::GetRemoteType(nsACString& aRemoteType) { + if (!CanSend()) { + return NS_ERROR_UNEXPECTED; + } + + dom::PContentParent* pcp = Manager()->Manager(); + aRemoteType = static_cast(pcp)->GetRemoteType(); + return NS_OK; +} + +//----------------------------------------------------------------------------- +// FTPChannelParent::nsIInterfaceRequestor +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +FTPChannelParent::GetInterface(const nsIID& uuid, void** result) { + if (uuid.Equals(NS_GET_IID(nsIAuthPromptProvider)) || + uuid.Equals(NS_GET_IID(nsISecureBrowserUI))) { + if (mBrowserParent) { + return mBrowserParent->QueryInterface(uuid, result); + } + } else if (uuid.Equals(NS_GET_IID(nsIAuthPrompt)) || + uuid.Equals(NS_GET_IID(nsIAuthPrompt2))) { + nsCOMPtr provider(do_QueryObject(mBrowserParent)); + if (provider) { + nsresult rv = provider->GetAuthPrompt( + nsIAuthPromptProvider::PROMPT_NORMAL, uuid, result); + if (NS_FAILED(rv)) { + return NS_ERROR_NO_INTERFACE; + } + return NS_OK; + } + } + + // Only support nsILoadContext if child channel's callbacks did too + if (uuid.Equals(NS_GET_IID(nsILoadContext)) && mLoadContext) { + nsCOMPtr copy = mLoadContext; + copy.forget(result); + return NS_OK; + } + + return QueryInterface(uuid, result); +} + +//----------------------------------------------------------------------------- +// FTPChannelParent::nsIChannelEventSink +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +FTPChannelParent::AsyncOnChannelRedirect( + nsIChannel* oldChannel, nsIChannel* newChannel, uint32_t redirectFlags, + nsIAsyncVerifyRedirectCallback* callback) { + nsCOMPtr ftpChan = do_QueryInterface(newChannel); + if (!ftpChan) { + // when FTP is set to use HTTP proxying, we wind up getting redirected to an + // HTTP channel. + nsCOMPtr httpChan = do_QueryInterface(newChannel); + if (!httpChan) return NS_ERROR_UNEXPECTED; + } + mChannel = newChannel; + callback->OnRedirectVerifyCallback(NS_OK); + return NS_OK; +} + +NS_IMETHODIMP +FTPChannelParent::SetErrorMsg(const char* aMsg, bool aUseUTF8) { + mErrorMsg = aMsg; + mUseUTF8 = aUseUTF8; + return NS_OK; +} + +//--------------------- +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/ftp/FTPChannelParent.h b/netwerk/protocol/ftp/FTPChannelParent.h new file mode 100644 index 0000000000..21ef7590bb --- /dev/null +++ b/netwerk/protocol/ftp/FTPChannelParent.h @@ -0,0 +1,92 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et 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/. */ + +#ifndef mozilla_net_FTPChannelParent_h +#define mozilla_net_FTPChannelParent_h + +#include "mozilla/net/PFTPChannelParent.h" +#include "mozilla/net/NeckoParent.h" +#include "nsIParentChannel.h" +#include "nsIInterfaceRequestor.h" +#include "nsIChannelEventSink.h" +#include "nsIFTPChannelParentInternal.h" + +class nsILoadContext; + +namespace mozilla { + +namespace dom { +class BrowserParent; +} // namespace dom + +namespace net { +class ChannelEventQueue; + +class FTPChannelParent final : public PFTPChannelParent, + public nsIParentChannel, + public nsIInterfaceRequestor, + public nsIChannelEventSink, + public nsIFTPChannelParentInternal { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMLISTENER + NS_DECL_NSIPARENTCHANNEL + NS_DECL_NSIINTERFACEREQUESTOR + NS_DECL_NSICHANNELEVENTSINK + + FTPChannelParent(dom::BrowserParent* aIframeEmbedding, + nsILoadContext* aLoadContext, + PBOverrideStatus aOverrideStatus); + + bool Init(const FTPChannelCreationArgs& aOpenArgs); + + NS_IMETHOD SetErrorMsg(const char* aMsg, bool aUseUTF8) override; + + protected: + virtual ~FTPChannelParent(); + + bool DoAsyncOpen(const URIParams& aURI, const uint64_t& aStartPos, + const nsCString& aEntityID, + const Maybe& aUploadStream, + const Maybe& aLoadInfoArgs, + const uint32_t& aLoadFlags); + + // used to connect redirected-to channel in parent with just created + // ChildChannel. Used during HTTP->FTP redirects. + bool ConnectChannel(const uint64_t& channelId); + + virtual mozilla::ipc::IPCResult RecvCancel(const nsresult& status) override; + virtual mozilla::ipc::IPCResult RecvSuspend() override; + virtual mozilla::ipc::IPCResult RecvResume() override; + + virtual void ActorDestroy(ActorDestroyReason why) override; + + // if configured to use HTTP proxy for FTP, this can an an HTTP channel. + nsCOMPtr mChannel; + + bool mIPCClosed; + + nsCOMPtr mLoadContext; + + PBOverrideStatus mPBOverride; + + // Set to the canceled status value if the main channel was canceled. + nsresult mStatus; + + RefPtr mBrowserParent; + + RefPtr mEventQ; + + nsCString mErrorMsg; + bool mUseUTF8; +}; + +} // namespace net +} // namespace mozilla + +#endif // mozilla_net_FTPChannelParent_h diff --git a/netwerk/protocol/ftp/PFTPChannel.ipdl b/netwerk/protocol/ftp/PFTPChannel.ipdl new file mode 100644 index 0000000000..be0d8abaac --- /dev/null +++ b/netwerk/protocol/ftp/PFTPChannel.ipdl @@ -0,0 +1,54 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et tw=80 ft=cpp : */ + +/* 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 protocol PNecko; +include URIParams; + +//FIXME: bug #792908 (NeckoChannelParams already included by PNecko) +include NeckoChannelParams; + +using PRTime from "prtime.h"; + +namespace mozilla { +namespace net { + +async protocol PFTPChannel +{ + manager PNecko; + +parent: + // Note: channels are opened during construction, so no open method here: + // see PNecko.ipdl + + async __delete__(); + + async Cancel(nsresult status); + async Suspend(); + async Resume(); + +child: + async OnStartRequest(nsresult aChannelStatus, + int64_t aContentLength, + nsCString aContentType, + PRTime aLastModified, + nsCString aEntityID, + URIParams aURI); + async OnDataAvailable(nsresult channelStatus, + nsCString data, + uint64_t offset, + uint32_t count); + async OnStopRequest(nsresult channelStatus, + nsCString aErrorMsg, + bool aUseUTF8); + async FailedAsyncOpen(nsresult statusCode); + + async DeleteSelf(); +}; + +} // namespace net +} // namespace mozilla + diff --git a/netwerk/protocol/ftp/doc/testdoc b/netwerk/protocol/ftp/doc/testdoc new file mode 100644 index 0000000000..61fda16fcc --- /dev/null +++ b/netwerk/protocol/ftp/doc/testdoc @@ -0,0 +1,4 @@ +Test +here +there +everywhere diff --git a/netwerk/protocol/ftp/ftpCore.h b/netwerk/protocol/ftp/ftpCore.h new file mode 100644 index 0000000000..5c7433495b --- /dev/null +++ b/netwerk/protocol/ftp/ftpCore.h @@ -0,0 +1,15 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef __ftpCore_h___ +#define __ftpCore_h___ + +#include "nsError.h" + +/** + * Status nsresult codes + */ + +#endif // __ftpCore_h___ diff --git a/netwerk/protocol/ftp/moz.build b/netwerk/protocol/ftp/moz.build new file mode 100644 index 0000000000..914916bf29 --- /dev/null +++ b/netwerk/protocol/ftp/moz.build @@ -0,0 +1,50 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +with Files("**"): + BUG_COMPONENT = ("Core", "Networking: FTP") + +XPIDL_SOURCES += [ + "nsIFTPChannel.idl", + "nsIFTPChannelParentInternal.idl", +] + +XPIDL_MODULE = "necko_ftp" + +EXPORTS += [ + "ftpCore.h", +] + +EXPORTS.mozilla.net += [ + "FTPChannelChild.h", + "FTPChannelParent.h", +] + +UNIFIED_SOURCES += [ + "FTPChannelChild.cpp", + "FTPChannelParent.cpp", + "nsFTPChannel.cpp", + "nsFtpConnectionThread.cpp", + "nsFtpControlConnection.cpp", + "nsFtpProtocolHandler.cpp", +] + +IPDL_SOURCES += [ + "PFTPChannel.ipdl", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" + +LOCAL_INCLUDES += [ + "/netwerk/base", +] + +if CONFIG["CC_TYPE"] in ("clang", "gcc"): + CXXFLAGS += ["-Wno-error=shadow"] + +include("/tools/fuzzing/libfuzzer-config.mozbuild") diff --git a/netwerk/protocol/ftp/nsFTPChannel.cpp b/netwerk/protocol/ftp/nsFTPChannel.cpp new file mode 100644 index 0000000000..97a7be1e68 --- /dev/null +++ b/netwerk/protocol/ftp/nsFTPChannel.cpp @@ -0,0 +1,213 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=4 sts=2 sw=2 et cin: */ +/* 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 "nsFTPChannel.h" +#include "nsFtpConnectionThread.h" // defines nsFtpState + +#include "nsThreadUtils.h" +#include "mozilla/Attributes.h" + +using namespace mozilla; +using namespace mozilla::net; +extern LazyLogModule gFTPLog; + +// There are two transport connections established for an +// ftp connection. One is used for the command channel , and +// the other for the data channel. The command channel is the first +// connection made and is used to negotiate the second, data, channel. +// The data channel is driven by the command channel and is either +// initiated by the server (PORT command) or by the client (PASV command). +// Client initiation is the most common case and is attempted first. + +//----------------------------------------------------------------------------- + +NS_IMPL_ISUPPORTS_INHERITED(nsFtpChannel, nsBaseChannel, nsIUploadChannel, + nsIResumableChannel, nsIFTPChannel, + nsIProxiedChannel, nsIForcePendingChannel, + nsISupportsWeakReference) + +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsFtpChannel::SetUploadStream(nsIInputStream* stream, + const nsACString& contentType, + int64_t contentLength) { + NS_ENSURE_TRUE(!Pending(), NS_ERROR_IN_PROGRESS); + + mUploadStream = stream; + + // NOTE: contentLength is intentionally ignored here. + + return NS_OK; +} + +NS_IMETHODIMP +nsFtpChannel::GetUploadStream(nsIInputStream** aStream) { + NS_ENSURE_ARG_POINTER(aStream); + nsCOMPtr stream = mUploadStream; + stream.forget(aStream); + return NS_OK; +} + +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsFtpChannel::ResumeAt(uint64_t aStartPos, const nsACString& aEntityID) { + NS_ENSURE_TRUE(!Pending(), NS_ERROR_IN_PROGRESS); + mEntityID = aEntityID; + mStartPos = aStartPos; + mResumeRequested = (mStartPos || !mEntityID.IsEmpty()); + return NS_OK; +} + +NS_IMETHODIMP +nsFtpChannel::GetEntityID(nsACString& entityID) { + if (mEntityID.IsEmpty()) return NS_ERROR_NOT_RESUMABLE; + + entityID = mEntityID; + return NS_OK; +} + +//----------------------------------------------------------------------------- +NS_IMETHODIMP +nsFtpChannel::GetProxyInfo(nsIProxyInfo** aProxyInfo) { + nsCOMPtr info = ProxyInfo(); + info.forget(aProxyInfo); + return NS_OK; +} + +NS_IMETHODIMP nsFtpChannel::GetHttpProxyConnectResponseCode( + int32_t* aResponseCode) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +//----------------------------------------------------------------------------- + +nsresult nsFtpChannel::OpenContentStream(bool async, nsIInputStream** result, + nsIChannel** channel) { + if (!async) return NS_ERROR_NOT_IMPLEMENTED; + + RefPtr state = new nsFtpState(); + + nsresult rv = state->Init(this); + if (NS_FAILED(rv)) { + return rv; + } + + state.forget(result); + return NS_OK; +} + +bool nsFtpChannel::GetStatusArg(nsresult status, nsString& statusArg) { + nsAutoCString host; + URI()->GetHost(host); + CopyUTF8toUTF16(host, statusArg); + return true; +} + +void nsFtpChannel::OnCallbacksChanged() { mFTPEventSink = nullptr; } + +//----------------------------------------------------------------------------- + +namespace { + +class FTPEventSinkProxy final : public nsIFTPEventSink { + ~FTPEventSinkProxy() = default; + + public: + explicit FTPEventSinkProxy(nsIFTPEventSink* aTarget) + : mTarget(aTarget), mEventTarget(GetCurrentEventTarget()) {} + + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIFTPEVENTSINK + + class OnFTPControlLogRunnable : public Runnable { + public: + OnFTPControlLogRunnable(nsIFTPEventSink* aTarget, bool aServer, + const char* aMessage) + : mozilla::Runnable("FTPEventSinkProxy::OnFTPControlLogRunnable"), + mTarget(aTarget), + mServer(aServer), + mMessage(aMessage) {} + + NS_DECL_NSIRUNNABLE + + private: + nsCOMPtr mTarget; + bool mServer; + nsCString mMessage; + }; + + private: + nsCOMPtr mTarget; + nsCOMPtr mEventTarget; +}; + +NS_IMPL_ISUPPORTS(FTPEventSinkProxy, nsIFTPEventSink) + +NS_IMETHODIMP +FTPEventSinkProxy::OnFTPControlLog(bool aServer, const char* aMsg) { + RefPtr r = + new OnFTPControlLogRunnable(mTarget, aServer, aMsg); + return mEventTarget->Dispatch(r, NS_DISPATCH_NORMAL); +} + +NS_IMETHODIMP +FTPEventSinkProxy::OnFTPControlLogRunnable::Run() { + mTarget->OnFTPControlLog(mServer, mMessage.get()); + return NS_OK; +} + +} // namespace + +void nsFtpChannel::GetFTPEventSink(nsCOMPtr& aResult) { + if (!mFTPEventSink) { + nsCOMPtr ftpSink; + GetCallback(ftpSink); + if (ftpSink) { + mFTPEventSink = new FTPEventSinkProxy(ftpSink); + } + } + aResult = mFTPEventSink; +} + +NS_IMETHODIMP +nsFtpChannel::ForcePending(bool aForcePending) { + // Set true here so IsPending will return true. + // Required for callback diversion from child back to parent. In such cases + // OnStopRequest can be called in the parent before callbacks are diverted + // back from the child to the listener in the parent. + mForcePending = aForcePending; + + return NS_OK; +} + +NS_IMETHODIMP +nsFtpChannel::IsPending(bool* result) { + *result = Pending(); + return NS_OK; +} + +bool nsFtpChannel::Pending() const { + return nsBaseChannel::Pending() || mForcePending; +} + +NS_IMETHODIMP +nsFtpChannel::Suspend() { + LOG(("nsFtpChannel::Suspend [this=%p]\n", this)); + NS_ENSURE_TRUE(Pending(), NS_ERROR_NOT_AVAILABLE); + + ++mSuspendCount; + return nsBaseChannel::Suspend(); +} + +NS_IMETHODIMP +nsFtpChannel::Resume() { + LOG(("nsFtpChannel::Resume [this=%p]\n", this)); + NS_ENSURE_TRUE(mSuspendCount > 0, NS_ERROR_UNEXPECTED); + --mSuspendCount; + return nsBaseChannel::Resume(); +} diff --git a/netwerk/protocol/ftp/nsFTPChannel.h b/netwerk/protocol/ftp/nsFTPChannel.h new file mode 100644 index 0000000000..52dc23506a --- /dev/null +++ b/netwerk/protocol/ftp/nsFTPChannel.h @@ -0,0 +1,115 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=4 sw=2 sts=2 et cindent: */ +/* 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/. */ + +#ifndef nsFTPChannel_h___ +#define nsFTPChannel_h___ + +#include "nsBaseChannel.h" + +#include "nsString.h" +#include "nsCOMPtr.h" +#include "nsIFTPChannel.h" +#include "nsIForcePendingChannel.h" +#include "nsIUploadChannel.h" +#include "nsIProxyInfo.h" +#include "nsIProxiedChannel.h" +#include "nsIResumableChannel.h" +#include "nsWeakReference.h" + +class nsIURI; + +class nsFtpChannel final : public nsBaseChannel, + public nsIFTPChannel, + public nsIUploadChannel, + public nsIResumableChannel, + public nsIProxiedChannel, + public nsIForcePendingChannel, + public nsSupportsWeakReference { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_NSIUPLOADCHANNEL + NS_DECL_NSIRESUMABLECHANNEL + NS_DECL_NSIPROXIEDCHANNEL + + nsFtpChannel(nsIURI* uri, nsIProxyInfo* pi) + : mProxyInfo(pi), + mStartPos(0), + mResumeRequested(false), + mLastModifiedTime(0), + mForcePending(false), + mSuspendCount(0) { + SetURI(uri); + } + + void UpdateURI(nsIURI* aURI) { + MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread(), "Not thread-safe."); + mURI = aURI; + } + + nsIProxyInfo* ProxyInfo() { return mProxyInfo; } + + void SetProxyInfo(nsIProxyInfo* pi) { mProxyInfo = pi; } + + NS_IMETHOD IsPending(bool* result) override; + + // This is a short-cut to calling nsIRequest::IsPending(). + // Overrides Pending in nsBaseChannel. + bool Pending() const override; + + // Were we asked to resume a download? + bool ResumeRequested() { return mResumeRequested; } + + // Download from this byte offset + uint64_t StartPos() { return mStartPos; } + + // ID of the entity to resume downloading + const nsCString& EntityID() { return mEntityID; } + void SetEntityID(const nsACString& entityID) { mEntityID = entityID; } + + NS_IMETHOD GetLastModifiedTime(PRTime* lastModifiedTime) override { + *lastModifiedTime = mLastModifiedTime; + return NS_OK; + } + + NS_IMETHOD SetLastModifiedTime(PRTime lastModifiedTime) override { + mLastModifiedTime = lastModifiedTime; + return NS_OK; + } + + // Data stream to upload + nsIInputStream* UploadStream() { return mUploadStream; } + + // Helper function for getting the nsIFTPEventSink. + void GetFTPEventSink(nsCOMPtr& aResult); + + NS_IMETHOD Suspend() override; + NS_IMETHOD Resume() override; + + public: + NS_IMETHOD ForcePending(bool aForcePending) override; + + protected: + virtual ~nsFtpChannel() = default; + virtual nsresult OpenContentStream(bool async, nsIInputStream** result, + nsIChannel** channel) override; + virtual bool GetStatusArg(nsresult status, nsString& statusArg) override; + virtual void OnCallbacksChanged() override; + + private: + nsCOMPtr mProxyInfo; + nsCOMPtr mFTPEventSink; + nsCOMPtr mUploadStream; + uint64_t mStartPos; + nsCString mEntityID; + bool mResumeRequested; + PRTime mLastModifiedTime; + bool mForcePending; + + // Current suspension depth for this channel object + uint32_t mSuspendCount; +}; + +#endif /* nsFTPChannel_h___ */ diff --git a/netwerk/protocol/ftp/nsFtpConnectionThread.cpp b/netwerk/protocol/ftp/nsFtpConnectionThread.cpp new file mode 100644 index 0000000000..c93a2c604b --- /dev/null +++ b/netwerk/protocol/ftp/nsFtpConnectionThread.cpp @@ -0,0 +1,1960 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set tw=80 ts=4 sts=2 sw=2 et cin: */ +/* 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 + +#include "prprf.h" +#include "mozilla/Logging.h" +#include "mozilla/NullPrincipal.h" +#include "mozilla/TextUtils.h" +#include "prtime.h" + +#include "nsIOService.h" +#include "nsFTPChannel.h" +#include "nsFtpConnectionThread.h" +#include "nsFtpControlConnection.h" +#include "nsFtpProtocolHandler.h" +#include "netCore.h" +#include "nsCRT.h" +#include "nsEscape.h" +#include "nsMimeTypes.h" +#include "nsNetCID.h" +#include "nsNetUtil.h" +#include "nsIAsyncStreamCopier.h" +#include "nsThreadUtils.h" +#include "nsStreamUtils.h" +#include "nsIURL.h" +#include "nsISocketTransport.h" +#include "nsIPrefBranch.h" +#include "nsAuthInformationHolder.h" +#include "nsIProtocolProxyService.h" +#include "nsICancelable.h" +#include "nsIOutputStream.h" +#include "nsIProtocolHandler.h" +#include "nsIProxyInfo.h" +#include "nsISocketTransportService.h" +#include "nsIURI.h" +#include "nsILoadInfo.h" +#include "nsIAuthPrompt2.h" +#include "nsIFTPChannelParentInternal.h" +#include "mozilla/Telemetry.h" + +using namespace mozilla; +using namespace mozilla::net; + +extern LazyLogModule gFTPLog; +#define LOG(args) MOZ_LOG(gFTPLog, mozilla::LogLevel::Debug, args) +#define LOG_INFO(args) MOZ_LOG(gFTPLog, mozilla::LogLevel::Info, args) + +// remove FTP parameters (starting with ";") from the path +static void removeParamsFromPath(nsCString& path) { + int32_t index = path.FindChar(';'); + if (index >= 0) { + path.SetLength(index); + } +} + +NS_IMPL_ISUPPORTS_INHERITED(nsFtpState, nsBaseContentStream, + nsIInputStreamCallback, nsITransportEventSink, + nsIRequestObserver, nsIProtocolProxyCallback) + +nsFtpState::nsFtpState() + : nsBaseContentStream(true), + mState(FTP_INIT), + mNextState(FTP_S_USER), + mKeepRunning(true), + mResponseCode(0), + mReceivedControlData(false), + mTryingCachedControl(false), + mRETRFailed(false), + mFileSize(kJS_MAX_SAFE_UINTEGER), + mServerType(FTP_GENERIC_TYPE), + mAction(GET), + mAnonymous(true), + mRetryPass(false), + mStorReplyReceived(false), + mRlist1xxReceived(false), + mRretr1xxReceived(false), + mRstor1xxReceived(false), + mInternalError(NS_OK), + mReconnectAndLoginAgain(false), + mCacheConnection(true), + mPort(21), + mAddressChecked(false), + mServerIsIPv6(false), + mUseUTF8(false), + mControlStatus(NS_OK), + mDeferredCallbackPending(false) { + this->mServerAddress.raw.family = 0; + this->mServerAddress.inet = {}; + LOG_INFO(("FTP:(%p) nsFtpState created", this)); + + // make sure handler stays around + mHandler = gFtpHandler; +} + +nsFtpState::~nsFtpState() { + LOG_INFO(("FTP:(%p) nsFtpState destroyed", this)); + + if (mProxyRequest) mProxyRequest->Cancel(NS_ERROR_FAILURE); + + // release reference to handler + mHandler = nullptr; +} + +// nsIInputStreamCallback implementation +NS_IMETHODIMP +nsFtpState::OnInputStreamReady(nsIAsyncInputStream* aInStream) { + LOG(("FTP:(%p) data stream ready\n", this)); + + // We are receiving a notification from our data stream, so just forward it + // on to our stream callback. + if (HasPendingCallback()) DispatchCallbackSync(); + + return NS_OK; +} + +void nsFtpState::OnControlDataAvailable(const char* aData, uint32_t aDataLen) { + LOG(("FTP:(%p) control data available [%u]\n", this, aDataLen)); + mControlConnection->WaitData(this); // queue up another call + + if (!mReceivedControlData) { + // parameter can be null cause the channel fills them in. + OnTransportStatus(nullptr, NS_NET_STATUS_BEGIN_FTP_TRANSACTION, 0, 0); + mReceivedControlData = true; + } + + // Sometimes we can get two responses in the same packet, eg from LIST. + // So we need to parse the response line by line + + nsCString buffer = mControlReadCarryOverBuf; + + // Clear the carryover buf - if we still don't have a line, then it will + // be reappended below + mControlReadCarryOverBuf.Truncate(); + + buffer.Append(aData, aDataLen); + + const char* currLine = buffer.get(); + while (*currLine && mKeepRunning) { + int32_t eolLength = strcspn(currLine, CRLF); + int32_t currLineLength = strlen(currLine); + + // if currLine is empty or only contains CR or LF, then bail. we can + // sometimes get an ODA event with the full response line + CR without + // the trailing LF. the trailing LF might come in the next ODA event. + // because we are happy enough to process a response line ending only + // in CR, we need to take care to discard the extra LF (bug 191220). + if (eolLength == 0 && currLineLength <= 1) break; + + if (eolLength == currLineLength) { + mControlReadCarryOverBuf.Assign(currLine); + break; + } + + // Append the current segment, including the LF + nsAutoCString line; + int32_t crlfLength = 0; + + if ((currLineLength > eolLength) && (currLine[eolLength] == nsCRT::CR) && + (currLine[eolLength + 1] == nsCRT::LF)) { + crlfLength = 2; // CR +LF + } else { + crlfLength = 1; // + LF or CR + } + + line.Assign(currLine, eolLength + crlfLength); + + // Does this start with a response code? + bool startNum = (line.Length() >= 3 && IsAsciiDigit(line[0]) && + IsAsciiDigit(line[1]) && IsAsciiDigit(line[2])); + + if (mResponseMsg.IsEmpty()) { + // If we get here, then we know that we have a complete line, and + // that it is the first one + + NS_ASSERTION(line.Length() > 4 && startNum, + "Read buffer doesn't include response code"); + + mResponseCode = atoi(PromiseFlatCString(Substring(line, 0, 3)).get()); + } + + mResponseMsg.Append(line); + + // This is the last line if its 3 numbers followed by a space + if (startNum && line[3] == ' ') { + // yup. last line, let's move on. + if (mState == mNextState) { + NS_ERROR("ftp read state mixup"); + mInternalError = NS_ERROR_FAILURE; + mState = FTP_ERROR; + } else { + mState = mNextState; + } + + nsCOMPtr ftpSink; + mChannel->GetFTPEventSink(ftpSink); + if (ftpSink) ftpSink->OnFTPControlLog(true, mResponseMsg.get()); + + nsresult rv = Process(); + mResponseMsg.Truncate(); + if (NS_FAILED(rv)) { + CloseWithStatus(rv); + return; + } + } + + currLine = currLine + eolLength + crlfLength; + } +} + +void nsFtpState::OnControlError(nsresult status) { + NS_ASSERTION(NS_FAILED(status), "expecting error condition"); + + LOG(("FTP:(%p) CC(%p) error [%" PRIx32 " was-cached=%u]\n", this, + mControlConnection.get(), static_cast(status), + mTryingCachedControl)); + + mControlStatus = status; + if (mReconnectAndLoginAgain && NS_SUCCEEDED(mInternalError)) { + mReconnectAndLoginAgain = false; + mAnonymous = false; + mControlStatus = NS_OK; + Connect(); + } else if (mTryingCachedControl && NS_SUCCEEDED(mInternalError)) { + mTryingCachedControl = false; + Connect(); + } else { + CloseWithStatus(status); + } +} + +nsresult nsFtpState::EstablishControlConnection() { + NS_ASSERTION(!mControlConnection, "we already have a control connection"); + + nsresult rv; + + LOG(("FTP:(%p) trying cached control\n", this)); + + // Look to see if we can use a cached control connection: + RefPtr connection; + // Don't use cached control if anonymous (bug #473371) + if (!mChannel->HasLoadFlag(nsIRequest::LOAD_ANONYMOUS)) + gFtpHandler->RemoveConnection(mChannel->URI(), getter_AddRefs(connection)); + + if (connection) { + mControlConnection.swap(connection); + if (mControlConnection->IsAlive()) { + // set stream listener of the control connection to be us. + mControlConnection->WaitData(this); + + // read cached variables into us. + mServerType = mControlConnection->mServerType; + mPassword = mControlConnection->mPassword; + mPwd = mControlConnection->mPwd; + mUseUTF8 = mControlConnection->mUseUTF8; + mTryingCachedControl = true; + + // we have to set charset to connection if server supports utf-8 + if (mUseUTF8) mChannel->SetContentCharset("UTF-8"_ns); + + // we're already connected to this server, skip login. + mState = FTP_S_PASV; + mResponseCode = 530; // assume the control connection was dropped. + mControlStatus = NS_OK; + mReceivedControlData = false; // For this request, we have not. + + // if we succeed, return. Otherwise, we need to create a transport + rv = mControlConnection->Connect(mChannel->ProxyInfo(), this); + if (NS_SUCCEEDED(rv)) return rv; + } + LOG(("FTP:(%p) cached CC(%p) is unusable\n", this, + mControlConnection.get())); + + mControlConnection->WaitData(nullptr); + mControlConnection = nullptr; + } + + LOG(("FTP:(%p) creating CC\n", this)); + + mState = FTP_READ_BUF; + mNextState = FTP_S_USER; + + nsAutoCString host; + rv = mChannel->URI()->GetAsciiHost(host); + if (NS_FAILED(rv)) return rv; + + mControlConnection = new nsFtpControlConnection(host, mPort); + if (!mControlConnection) return NS_ERROR_OUT_OF_MEMORY; + + rv = mControlConnection->Connect(mChannel->ProxyInfo(), this); + if (NS_FAILED(rv)) { + LOG(("FTP:(%p) CC(%p) failed to connect [rv=%" PRIx32 "]\n", this, + mControlConnection.get(), static_cast(rv))); + mControlConnection = nullptr; + return rv; + } + + return mControlConnection->WaitData(this); +} + +void nsFtpState::MoveToNextState(FTP_STATE nextState) { + if (NS_FAILED(mInternalError)) { + mState = FTP_ERROR; + LOG(("FTP:(%p) FAILED (%" PRIx32 ")\n", this, + static_cast(mInternalError))); + } else { + mState = FTP_READ_BUF; + mNextState = nextState; + } +} + +nsresult nsFtpState::Process() { + nsresult rv = NS_OK; + bool processingRead = true; + + while (mKeepRunning && processingRead) { + switch (mState) { + case FTP_COMMAND_CONNECT: + KillControlConnection(); + LOG(("FTP:(%p) establishing CC", this)); + mInternalError = EstablishControlConnection(); // sets mState + if (NS_FAILED(mInternalError)) { + mState = FTP_ERROR; + LOG(("FTP:(%p) FAILED\n", this)); + } else { + LOG(("FTP:(%p) SUCCEEDED\n", this)); + } + break; + + case FTP_READ_BUF: + LOG(("FTP:(%p) Waiting for CC(%p)\n", this, mControlConnection.get())); + processingRead = false; + break; + + case FTP_ERROR: // xx needs more work to handle dropped control + // connection cases + if ((mTryingCachedControl && mResponseCode == 530 && + mInternalError == NS_ERROR_FTP_PASV) || + (mResponseCode == 425 && mInternalError == NS_ERROR_FTP_PASV)) { + // The user was logged out during an pasv operation + // we want to restart this request with a new control + // channel. + mState = FTP_COMMAND_CONNECT; + } else if (mResponseCode == 421 && + mInternalError != NS_ERROR_FTP_LOGIN) { + // The command channel dropped for some reason. + // Fire it back up, unless we were trying to login + // in which case the server might just be telling us + // that the max number of users has been reached... + mState = FTP_COMMAND_CONNECT; + } else if (mAnonymous && mInternalError == NS_ERROR_FTP_LOGIN) { + // If the login was anonymous, and it failed, try again with a + // username Don't reuse old control connection, see #386167 + mAnonymous = false; + mState = FTP_COMMAND_CONNECT; + } else { + LOG(("FTP:(%p) FTP_ERROR - calling StopProcessing\n", this)); + rv = StopProcessing(); + NS_ASSERTION(NS_SUCCEEDED(rv), "StopProcessing failed."); + processingRead = false; + } + break; + + case FTP_COMPLETE: + LOG(("FTP:(%p) COMPLETE\n", this)); + rv = StopProcessing(); + NS_ASSERTION(NS_SUCCEEDED(rv), "StopProcessing failed."); + processingRead = false; + break; + + // USER + case FTP_S_USER: + rv = S_user(); + + if (NS_FAILED(rv)) mInternalError = NS_ERROR_FTP_LOGIN; + + MoveToNextState(FTP_R_USER); + break; + + case FTP_R_USER: + mState = R_user(); + + if (FTP_ERROR == mState) mInternalError = NS_ERROR_FTP_LOGIN; + + break; + // PASS + case FTP_S_PASS: + rv = S_pass(); + + if (NS_FAILED(rv)) mInternalError = NS_ERROR_FTP_LOGIN; + + MoveToNextState(FTP_R_PASS); + break; + + case FTP_R_PASS: + mState = R_pass(); + + if (FTP_ERROR == mState) mInternalError = NS_ERROR_FTP_LOGIN; + + break; + // ACCT + case FTP_S_ACCT: + rv = S_acct(); + + if (NS_FAILED(rv)) mInternalError = NS_ERROR_FTP_LOGIN; + + MoveToNextState(FTP_R_ACCT); + break; + + case FTP_R_ACCT: + mState = R_acct(); + + if (FTP_ERROR == mState) mInternalError = NS_ERROR_FTP_LOGIN; + + break; + + // SYST + case FTP_S_SYST: + rv = S_syst(); + + if (NS_FAILED(rv)) mInternalError = NS_ERROR_FTP_LOGIN; + + MoveToNextState(FTP_R_SYST); + break; + + case FTP_R_SYST: + mState = R_syst(); + + if (FTP_ERROR == mState) mInternalError = NS_ERROR_FTP_LOGIN; + + break; + + // TYPE + case FTP_S_TYPE: + rv = S_type(); + + if (NS_FAILED(rv)) mInternalError = rv; + + MoveToNextState(FTP_R_TYPE); + break; + + case FTP_R_TYPE: + mState = R_type(); + + if (FTP_ERROR == mState) mInternalError = NS_ERROR_FAILURE; + + break; + // CWD + case FTP_S_CWD: + rv = S_cwd(); + + if (NS_FAILED(rv)) mInternalError = NS_ERROR_FTP_CWD; + + MoveToNextState(FTP_R_CWD); + break; + + case FTP_R_CWD: + mState = R_cwd(); + + if (FTP_ERROR == mState) mInternalError = NS_ERROR_FTP_CWD; + break; + + // LIST + case FTP_S_LIST: + rv = S_list(); + + if (rv == NS_ERROR_NOT_RESUMABLE) { + mInternalError = rv; + } else if (NS_FAILED(rv)) { + mInternalError = NS_ERROR_FTP_CWD; + } + + MoveToNextState(FTP_R_LIST); + break; + + case FTP_R_LIST: + mState = R_list(); + + if (FTP_ERROR == mState) mInternalError = NS_ERROR_FAILURE; + + break; + + // SIZE + case FTP_S_SIZE: + rv = S_size(); + + if (NS_FAILED(rv)) mInternalError = rv; + + MoveToNextState(FTP_R_SIZE); + break; + + case FTP_R_SIZE: + mState = R_size(); + + if (FTP_ERROR == mState) mInternalError = NS_ERROR_FAILURE; + + break; + + // REST + case FTP_S_REST: + rv = S_rest(); + + if (NS_FAILED(rv)) mInternalError = rv; + + MoveToNextState(FTP_R_REST); + break; + + case FTP_R_REST: + mState = R_rest(); + + if (FTP_ERROR == mState) mInternalError = NS_ERROR_FAILURE; + + break; + + // MDTM + case FTP_S_MDTM: + rv = S_mdtm(); + if (NS_FAILED(rv)) mInternalError = rv; + MoveToNextState(FTP_R_MDTM); + break; + + case FTP_R_MDTM: + mState = R_mdtm(); + + // Don't want to overwrite a more explicit status code + if (FTP_ERROR == mState && NS_SUCCEEDED(mInternalError)) + mInternalError = NS_ERROR_FAILURE; + + break; + + // RETR + case FTP_S_RETR: + rv = S_retr(); + + if (NS_FAILED(rv)) mInternalError = rv; + + MoveToNextState(FTP_R_RETR); + break; + + case FTP_R_RETR: + + mState = R_retr(); + + if (FTP_ERROR == mState) mInternalError = NS_ERROR_FAILURE; + + break; + + // STOR + case FTP_S_STOR: + rv = S_stor(); + + if (NS_FAILED(rv)) mInternalError = rv; + + MoveToNextState(FTP_R_STOR); + break; + + case FTP_R_STOR: + mState = R_stor(); + + if (FTP_ERROR == mState) mInternalError = NS_ERROR_FAILURE; + + break; + + // PASV + case FTP_S_PASV: + rv = S_pasv(); + + if (NS_FAILED(rv)) mInternalError = NS_ERROR_FTP_PASV; + + MoveToNextState(FTP_R_PASV); + break; + + case FTP_R_PASV: + mState = R_pasv(); + + if (FTP_ERROR == mState) mInternalError = NS_ERROR_FTP_PASV; + + break; + + // PWD + case FTP_S_PWD: + rv = S_pwd(); + + if (NS_FAILED(rv)) mInternalError = NS_ERROR_FTP_PWD; + + MoveToNextState(FTP_R_PWD); + break; + + case FTP_R_PWD: + mState = R_pwd(); + + if (FTP_ERROR == mState) mInternalError = NS_ERROR_FTP_PWD; + + break; + + // FEAT for RFC2640 support + case FTP_S_FEAT: + rv = S_feat(); + + if (NS_FAILED(rv)) mInternalError = rv; + + MoveToNextState(FTP_R_FEAT); + break; + + case FTP_R_FEAT: + mState = R_feat(); + + // Don't want to overwrite a more explicit status code + if (FTP_ERROR == mState && NS_SUCCEEDED(mInternalError)) + mInternalError = NS_ERROR_FAILURE; + break; + + // OPTS for some non-RFC2640-compliant servers support + case FTP_S_OPTS: + rv = S_opts(); + + if (NS_FAILED(rv)) mInternalError = rv; + + MoveToNextState(FTP_R_OPTS); + break; + + case FTP_R_OPTS: + mState = R_opts(); + + // Don't want to overwrite a more explicit status code + if (FTP_ERROR == mState && NS_SUCCEEDED(mInternalError)) + mInternalError = NS_ERROR_FAILURE; + break; + + default:; + } + } + + return rv; +} + +/////////////////////////////////// +// STATE METHODS +/////////////////////////////////// +nsresult nsFtpState::S_user() { + // some servers on connect send us a 421 or 521. (84525) (141784) + if ((mResponseCode == 421) || (mResponseCode == 521)) return NS_ERROR_FAILURE; + + nsresult rv; + nsAutoCString usernameStr("USER "); + + mResponseMsg = ""; + + if (mAnonymous) { + mReconnectAndLoginAgain = true; + usernameStr.AppendLiteral("anonymous"); + } else { + mReconnectAndLoginAgain = false; + if (mUsername.IsEmpty()) { + // No prompt for anonymous requests (bug #473371) + if (mChannel->HasLoadFlag(nsIRequest::LOAD_ANONYMOUS)) + return NS_ERROR_FAILURE; + + nsCOMPtr prompter; + NS_QueryAuthPrompt2(static_cast(mChannel), + getter_AddRefs(prompter)); + if (!prompter) return NS_ERROR_NOT_INITIALIZED; + + RefPtr info = new nsAuthInformationHolder( + nsIAuthInformation::AUTH_HOST, u""_ns, ""_ns); + + bool retval; + rv = prompter->PromptAuth(mChannel, nsIAuthPrompt2::LEVEL_NONE, info, + &retval); + + // if the user canceled or didn't supply a username we want to fail + if (NS_FAILED(rv) || !retval || info->User().IsEmpty()) + return NS_ERROR_FAILURE; + + mUsername = info->User(); + mPassword = info->Password(); + } + // XXX Is UTF-8 the best choice? + AppendUTF16toUTF8(mUsername, usernameStr); + } + + usernameStr.AppendLiteral(CRLF); + + return SendFTPCommand(usernameStr); +} + +FTP_STATE +nsFtpState::R_user() { + mReconnectAndLoginAgain = false; + if (mResponseCode / 100 == 3) { + // send off the password + return FTP_S_PASS; + } + if (mResponseCode / 100 == 2) { + // no password required, we're already logged in + return FTP_S_SYST; + } + if (mResponseCode / 100 == 5) { + // problem logging in. typically this means the server + // has reached it's user limit. + return FTP_ERROR; + } + // LOGIN FAILED + return FTP_ERROR; +} + +nsresult nsFtpState::S_pass() { + nsresult rv; + nsAutoCString passwordStr("PASS "); + + mResponseMsg = ""; + + if (mAnonymous) { + if (!mPassword.IsEmpty()) { + // XXX Is UTF-8 the best choice? + AppendUTF16toUTF8(mPassword, passwordStr); + } else { + nsAutoCString anonPassword; + bool useRealEmail = false; + nsCOMPtr prefs = do_GetService(NS_PREFSERVICE_CONTRACTID); + if (prefs) { + rv = prefs->GetBoolPref("advanced.mailftp", &useRealEmail); + if (NS_SUCCEEDED(rv) && useRealEmail) { + prefs->GetCharPref("network.ftp.anonymous_password", anonPassword); + } + } + if (!anonPassword.IsEmpty()) { + passwordStr.AppendASCII(anonPassword.get()); + } else { + // We need to default to a valid email address - bug 101027 + // example.com is reserved (rfc2606), so use that + passwordStr.AppendLiteral("mozilla@example.com"); + } + } + } else { + if (mPassword.IsEmpty() || mRetryPass) { + // No prompt for anonymous requests (bug #473371) + if (mChannel->HasLoadFlag(nsIRequest::LOAD_ANONYMOUS)) + return NS_ERROR_FAILURE; + + nsCOMPtr prompter; + NS_QueryAuthPrompt2(static_cast(mChannel), + getter_AddRefs(prompter)); + if (!prompter) return NS_ERROR_NOT_INITIALIZED; + + RefPtr info = new nsAuthInformationHolder( + nsIAuthInformation::AUTH_HOST | nsIAuthInformation::ONLY_PASSWORD, + u""_ns, ""_ns); + + info->SetUserInternal(mUsername); + + bool retval; + rv = prompter->PromptAuth(mChannel, nsIAuthPrompt2::LEVEL_NONE, info, + &retval); + + // we want to fail if the user canceled. Note here that if they want + // a blank password, we will pass it along. + if (NS_FAILED(rv) || !retval) return NS_ERROR_FAILURE; + + mPassword = info->Password(); + } + // XXX Is UTF-8 the best choice? + AppendUTF16toUTF8(mPassword, passwordStr); + } + + passwordStr.AppendLiteral(CRLF); + + return SendFTPCommand(passwordStr); +} + +FTP_STATE +nsFtpState::R_pass() { + if (mResponseCode / 100 == 3) { + // send account info + return FTP_S_ACCT; + } + if (mResponseCode / 100 == 2) { + // logged in + return FTP_S_SYST; + } + if (mResponseCode == 503) { + // start over w/ the user command. + // note: the password was successful, and it's stored in mPassword + mRetryPass = false; + return FTP_S_USER; + } + if (mResponseCode / 100 == 5 || mResponseCode == 421) { + // There is no difference between a too-many-users error, + // a wrong-password error, or any other sort of error + + if (!mAnonymous) mRetryPass = true; + + return FTP_ERROR; + } + // unexpected response code + return FTP_ERROR; +} + +nsresult nsFtpState::S_pwd() { + return SendFTPCommand(nsLiteralCString("PWD" CRLF)); +} + +FTP_STATE +nsFtpState::R_pwd() { + // Error response to PWD command isn't fatal, but don't cache the connection + // if CWD command is sent since correct mPwd is needed for further requests. + if (mResponseCode / 100 != 2) return FTP_S_TYPE; + + nsAutoCString respStr(mResponseMsg); + int32_t pos = respStr.FindChar('"'); + if (pos > -1) { + respStr.Cut(0, pos + 1); + pos = respStr.FindChar('"'); + if (pos > -1) { + respStr.Truncate(pos); + if (mServerType == FTP_VMS_TYPE) ConvertDirspecFromVMS(respStr); + if (respStr.IsEmpty() || respStr.Last() != '/') respStr.Append('/'); + mPwd = respStr; + } + } + return FTP_S_TYPE; +} + +nsresult nsFtpState::S_syst() { + return SendFTPCommand(nsLiteralCString("SYST" CRLF)); +} + +FTP_STATE +nsFtpState::R_syst() { + if (mResponseCode / 100 == 2) { + if ((mResponseMsg.Find("L8") > -1) || (mResponseMsg.Find("UNIX") > -1) || + (mResponseMsg.Find("BSD") > -1) || + (mResponseMsg.Find("MACOS Peter's Server") > -1) || + (mResponseMsg.Find("MACOS WebSTAR FTP") > -1) || + (mResponseMsg.Find("MVS") > -1) || (mResponseMsg.Find("OS/390") > -1) || + (mResponseMsg.Find("OS/400") > -1)) { + mServerType = FTP_UNIX_TYPE; + } else if ((mResponseMsg.Find("WIN32", true) > -1) || + (mResponseMsg.Find("windows", true) > -1)) { + mServerType = FTP_NT_TYPE; + } else if (mResponseMsg.Find("OS/2", true) > -1) { + mServerType = FTP_OS2_TYPE; + } else if (mResponseMsg.Find("VMS", true) > -1) { + mServerType = FTP_VMS_TYPE; + } else { + NS_ERROR("Server type list format unrecognized."); + + // clear mResponseMsg, which is displayed to the user. + mResponseMsg = ""; + return FTP_ERROR; + } + + return FTP_S_FEAT; + } + + if (mResponseCode / 100 == 5) { + // server didn't like the SYST command. Probably (500, 501, 502) + // No clue. We will just hope it is UNIX type server. + mServerType = FTP_UNIX_TYPE; + + return FTP_S_FEAT; + } + return FTP_ERROR; +} + +nsresult nsFtpState::S_acct() { + return SendFTPCommand(nsLiteralCString("ACCT noaccount" CRLF)); +} + +FTP_STATE +nsFtpState::R_acct() { + if (mResponseCode / 100 == 2) return FTP_S_SYST; + + return FTP_ERROR; +} + +nsresult nsFtpState::S_type() { + return SendFTPCommand(nsLiteralCString("TYPE I" CRLF)); +} + +FTP_STATE +nsFtpState::R_type() { + if (mResponseCode / 100 != 2) return FTP_ERROR; + + return FTP_S_PASV; +} + +nsresult nsFtpState::S_cwd() { + // Don't cache the connection if PWD command failed + if (mPwd.IsEmpty()) mCacheConnection = false; + + nsAutoCString cwdStr; + if (mAction != PUT) cwdStr = mPath; + if (cwdStr.IsEmpty() || cwdStr.First() != '/') cwdStr.Insert(mPwd, 0); + if (mServerType == FTP_VMS_TYPE) ConvertDirspecToVMS(cwdStr); + cwdStr.InsertLiteral("CWD ", 0); + cwdStr.AppendLiteral(CRLF); + + return SendFTPCommand(cwdStr); +} + +FTP_STATE +nsFtpState::R_cwd() { + if (mResponseCode / 100 == 2) { + if (mAction == PUT) return FTP_S_STOR; + + return FTP_S_LIST; + } + + return FTP_ERROR; +} + +nsresult nsFtpState::S_size() { + nsAutoCString sizeBuf(mPath); + if (sizeBuf.IsEmpty() || sizeBuf.First() != '/') sizeBuf.Insert(mPwd, 0); + if (mServerType == FTP_VMS_TYPE) ConvertFilespecToVMS(sizeBuf); + sizeBuf.InsertLiteral("SIZE ", 0); + sizeBuf.AppendLiteral(CRLF); + + return SendFTPCommand(sizeBuf); +} + +FTP_STATE +nsFtpState::R_size() { + if (mResponseCode / 100 == 2) { + PR_sscanf(mResponseMsg.get() + 4, "%llu", &mFileSize); + mChannel->SetContentLength(mFileSize); + } + + // We may want to be able to resume this + return FTP_S_MDTM; +} + +nsresult nsFtpState::S_mdtm() { + nsAutoCString mdtmBuf(mPath); + if (mdtmBuf.IsEmpty() || mdtmBuf.First() != '/') mdtmBuf.Insert(mPwd, 0); + if (mServerType == FTP_VMS_TYPE) ConvertFilespecToVMS(mdtmBuf); + mdtmBuf.InsertLiteral("MDTM ", 0); + mdtmBuf.AppendLiteral(CRLF); + + return SendFTPCommand(mdtmBuf); +} + +FTP_STATE +nsFtpState::R_mdtm() { + if (mResponseCode == 213) { + mResponseMsg.Cut(0, 4); + mResponseMsg.Trim(" \t\r\n"); + // yyyymmddhhmmss + if (mResponseMsg.Length() != 14) { + NS_ASSERTION(mResponseMsg.Length() == 14, "Unknown MDTM response"); + } else { + mModTime = mResponseMsg; + + // Save lastModified time for downloaded files. + nsAutoCString timeString; + nsresult error; + PRExplodedTime exTime; + + mResponseMsg.Mid(timeString, 0, 4); + exTime.tm_year = timeString.ToInteger(&error); + mResponseMsg.Mid(timeString, 4, 2); + exTime.tm_month = timeString.ToInteger(&error) - 1; // january = 0 + mResponseMsg.Mid(timeString, 6, 2); + exTime.tm_mday = timeString.ToInteger(&error); + mResponseMsg.Mid(timeString, 8, 2); + exTime.tm_hour = timeString.ToInteger(&error); + mResponseMsg.Mid(timeString, 10, 2); + exTime.tm_min = timeString.ToInteger(&error); + mResponseMsg.Mid(timeString, 12, 2); + exTime.tm_sec = timeString.ToInteger(&error); + exTime.tm_usec = 0; + + exTime.tm_params.tp_gmt_offset = 0; + exTime.tm_params.tp_dst_offset = 0; + + PR_NormalizeTime(&exTime, PR_GMTParameters); + exTime.tm_params = PR_LocalTimeParameters(&exTime); + + PRTime time = PR_ImplodeTime(&exTime); + (void)mChannel->SetLastModifiedTime(time); + } + } + + nsCString entityID; + entityID.Truncate(); + entityID.AppendInt(int64_t(mFileSize)); + entityID.Append('/'); + entityID.Append(mModTime); + mChannel->SetEntityID(entityID); + + // We weren't asked to resume + if (!mChannel->ResumeRequested()) return FTP_S_RETR; + + // if (our entityID == supplied one (if any)) + if (mSuppliedEntityID.IsEmpty() || entityID.Equals(mSuppliedEntityID)) + return FTP_S_REST; + + mInternalError = NS_ERROR_ENTITY_CHANGED; + mResponseMsg.Truncate(); + return FTP_ERROR; +} + +nsresult nsFtpState::SetContentType() { + // FTP directory URLs don't always end in a slash. Make sure they do. + // This check needs to be here rather than a more obvious place + // (e.g. LIST command processing) so that it ensures the terminating + // slash is appended for the new request case. + + if (!mPath.IsEmpty() && mPath.Last() != '/') { + nsCOMPtr url = (do_QueryInterface(mChannel->URI())); + nsAutoCString filePath; + if (NS_SUCCEEDED(url->GetFilePath(filePath))) { + filePath.Append('/'); + nsresult rv = NS_MutateURI(url).SetFilePath(filePath).Finalize(url); + if (NS_SUCCEEDED(rv)) { + mChannel->UpdateURI(url); + } + } + } + return mChannel->SetContentType( + nsLiteralCString(APPLICATION_HTTP_INDEX_FORMAT)); +} + +nsresult nsFtpState::S_list() { + nsresult rv = SetContentType(); + if (NS_FAILED(rv)) + // XXX Invalid cast of FTP_STATE to nsresult -- FTP_ERROR has + // value < 0x80000000 and will pass NS_SUCCEEDED() (bug 778109) + return (nsresult)FTP_ERROR; + + rv = mChannel->PushStreamConverter("text/ftp-dir", + APPLICATION_HTTP_INDEX_FORMAT); + if (NS_FAILED(rv)) { + // clear mResponseMsg which is displayed to the user. + // TODO: we should probably set this to something meaningful. + mResponseMsg = ""; + return rv; + } + + // dir listings aren't resumable + NS_ENSURE_TRUE(!mChannel->ResumeRequested(), NS_ERROR_NOT_RESUMABLE); + + mChannel->SetEntityID(""_ns); + + const char* listString; + if (mServerType == FTP_VMS_TYPE) { + listString = "LIST *.*;0" CRLF; + } else { + listString = "LIST" CRLF; + } + + return SendFTPCommand(nsDependentCString(listString)); +} + +FTP_STATE +nsFtpState::R_list() { + if (mResponseCode / 100 == 1) { + Telemetry::ScalarAdd( + Telemetry::ScalarID::NETWORKING_FTP_OPENED_CHANNELS_LISTINGS, 1); + + mRlist1xxReceived = true; + + // OK, time to start reading from the data connection. + if (mDataStream && HasPendingCallback()) + mDataStream->AsyncWait(this, 0, 0, CallbackTarget()); + return FTP_READ_BUF; + } + + if (mResponseCode / 100 == 2 && mRlist1xxReceived) { + //(DONE) + mNextState = FTP_COMPLETE; + mRlist1xxReceived = false; + return FTP_COMPLETE; + } + return FTP_ERROR; +} + +nsresult nsFtpState::S_retr() { + nsAutoCString retrStr(mPath); + if (retrStr.IsEmpty() || retrStr.First() != '/') retrStr.Insert(mPwd, 0); + if (mServerType == FTP_VMS_TYPE) ConvertFilespecToVMS(retrStr); + retrStr.InsertLiteral("RETR ", 0); + retrStr.AppendLiteral(CRLF); + + return SendFTPCommand(retrStr); +} + +FTP_STATE +nsFtpState::R_retr() { + if (mResponseCode / 100 == 2) { + if (!mRretr1xxReceived) return FTP_ERROR; + + //(DONE) + mNextState = FTP_COMPLETE; + mRretr1xxReceived = false; + return FTP_COMPLETE; + } + + if (mResponseCode / 100 == 1) { + mChannel->SetContentType(nsLiteralCString(APPLICATION_OCTET_STREAM)); + + Telemetry::ScalarAdd( + Telemetry::ScalarID::NETWORKING_FTP_OPENED_CHANNELS_FILES, 1); + + mRretr1xxReceived = true; + + if (mDataStream && HasPendingCallback()) + mDataStream->AsyncWait(this, 0, 0, CallbackTarget()); + return FTP_READ_BUF; + } + + // These error codes are related to problems with the connection. + // If we encounter any at this point, do not try CWD and abort. + if (mResponseCode == 421 || mResponseCode == 425 || mResponseCode == 426) + return FTP_ERROR; + + if (mResponseCode / 100 == 5) { + mRETRFailed = true; + return FTP_S_PASV; + } + + return FTP_S_CWD; +} + +nsresult nsFtpState::S_rest() { + nsAutoCString restString("REST "); + // The int64_t cast is needed to avoid ambiguity + restString.AppendInt(int64_t(mChannel->StartPos()), 10); + restString.AppendLiteral(CRLF); + + return SendFTPCommand(restString); +} + +FTP_STATE +nsFtpState::R_rest() { + if (mResponseCode / 100 == 4) { + // If REST fails, then we can't resume + mChannel->SetEntityID(""_ns); + + mInternalError = NS_ERROR_NOT_RESUMABLE; + mResponseMsg.Truncate(); + + return FTP_ERROR; + } + + return FTP_S_RETR; +} + +nsresult nsFtpState::S_stor() { + NS_ENSURE_STATE(mChannel->UploadStream()); + + NS_ASSERTION(mAction == PUT, "Wrong state to be here"); + + nsCOMPtr url = do_QueryInterface(mChannel->URI()); + NS_ASSERTION(url, "I thought you were a nsStandardURL"); + + nsAutoCString storStr; + url->GetFilePath(storStr); + NS_ASSERTION(!storStr.IsEmpty(), "What does it mean to store a empty path"); + + // kill the first slash since we want to be relative to CWD. + if (storStr.First() == '/') storStr.Cut(0, 1); + + if (mServerType == FTP_VMS_TYPE) ConvertFilespecToVMS(storStr); + + NS_UnescapeURL(storStr); + storStr.InsertLiteral("STOR ", 0); + storStr.AppendLiteral(CRLF); + + return SendFTPCommand(storStr); +} + +FTP_STATE +nsFtpState::R_stor() { + if (mResponseCode / 100 == 2 && mRstor1xxReceived) { + //(DONE) + mNextState = FTP_COMPLETE; + mStorReplyReceived = true; + mRstor1xxReceived = false; + + // Call Close() if it was not called in nsFtpState::OnStoprequest() + if (!mUploadRequest && !IsClosed()) Close(); + + return FTP_COMPLETE; + } + + if (mResponseCode / 100 == 1) { + Telemetry::ScalarAdd( + Telemetry::ScalarID::NETWORKING_FTP_OPENED_CHANNELS_FILES, 1); + + LOG(("FTP:(%p) writing on DT\n", this)); + mRstor1xxReceived = true; + return FTP_READ_BUF; + } + + mStorReplyReceived = true; + return FTP_ERROR; +} + +nsresult nsFtpState::S_pasv() { + if (!mAddressChecked) { + // Find socket address + mAddressChecked = true; + mServerAddress.raw.family = AF_INET; + mServerAddress.inet.ip = htonl(INADDR_ANY); + mServerAddress.inet.port = htons(0); + + nsITransport* controlSocket = mControlConnection->Transport(); + if (!controlSocket) + // XXX Invalid cast of FTP_STATE to nsresult -- FTP_ERROR has + // value < 0x80000000 and will pass NS_SUCCEEDED() (bug 778109) + return (nsresult)FTP_ERROR; + + nsCOMPtr sTrans = do_QueryInterface(controlSocket); + if (sTrans) { + nsresult rv = sTrans->GetPeerAddr(&mServerAddress); + if (NS_SUCCEEDED(rv)) { + if (!mServerAddress.IsIPAddrAny()) + mServerIsIPv6 = (mServerAddress.raw.family == AF_INET6) && + !mServerAddress.IsIPAddrV4Mapped(); + else { + /* + * In case of SOCKS5 remote DNS resolution, we do + * not know the remote IP address. Still, if it is + * an IPV6 host, then the external address of the + * socks server should also be IPv6, and this is the + * self address of the transport. + */ + NetAddr selfAddress; + rv = sTrans->GetSelfAddr(&selfAddress); + if (NS_SUCCEEDED(rv)) + mServerIsIPv6 = (selfAddress.raw.family == AF_INET6) && + !selfAddress.IsIPAddrV4Mapped(); + } + } + } + } + + const char* string; + if (mServerIsIPv6) { + string = "EPSV" CRLF; + } else { + string = "PASV" CRLF; + } + + return SendFTPCommand(nsDependentCString(string)); +} + +FTP_STATE +nsFtpState::R_pasv() { + if (mResponseCode / 100 != 2) return FTP_ERROR; + + nsresult rv; + int32_t port; + + nsAutoCString responseCopy(mResponseMsg); + char* response = responseCopy.BeginWriting(); + + char* ptr = response; + + // Make sure to ignore the address in the PASV response (bug 370559) + + if (mServerIsIPv6) { + // The returned string is of the form + // text (|||ppp|) + // Where '|' can be any single character + char delim; + while (*ptr && *ptr != '(') ptr++; + if (*ptr++ != '(') return FTP_ERROR; + delim = *ptr++; + if (!delim || *ptr++ != delim || *ptr++ != delim || *ptr < '0' || + *ptr > '9') + return FTP_ERROR; + port = 0; + do { + port = port * 10 + *ptr++ - '0'; + } while (*ptr >= '0' && *ptr <= '9'); + if (*ptr++ != delim || *ptr != ')') return FTP_ERROR; + } else { + // The returned address string can be of the form + // (xxx,xxx,xxx,xxx,ppp,ppp) or + // xxx,xxx,xxx,xxx,ppp,ppp (without parens) + int32_t h0, h1, h2, h3, p0, p1; + + int32_t fields = 0; + // First try with parens + while (*ptr && *ptr != '(') ++ptr; + if (*ptr) { + ++ptr; + fields = PR_sscanf(ptr, "%ld,%ld,%ld,%ld,%ld,%ld", &h0, &h1, &h2, &h3, + &p0, &p1); + } + if (!*ptr || fields < 6) { + // OK, lets try w/o parens + ptr = response; + while (*ptr && *ptr != ',') ++ptr; + if (*ptr) { + // backup to the start of the digits + do { + ptr--; + } while ((ptr >= response) && (*ptr >= '0') && (*ptr <= '9')); + ptr++; // get back onto the numbers + fields = PR_sscanf(ptr, "%ld,%ld,%ld,%ld,%ld,%ld", &h0, &h1, &h2, &h3, + &p0, &p1); + } + } + + NS_ASSERTION(fields == 6, "Can't parse PASV response"); + if (fields < 6) return FTP_ERROR; + + port = ((int32_t)(p0 << 8)) + p1; + } + + bool newDataConn = true; + if (mDataTransport) { + // Reuse this connection only if its still alive, and the port + // is the same + nsCOMPtr strans = do_QueryInterface(mDataTransport); + if (strans) { + int32_t oldPort; + nsresult rv = strans->GetPort(&oldPort); + if (NS_SUCCEEDED(rv)) { + if (oldPort == port) { + bool isAlive; + if (NS_SUCCEEDED(strans->IsAlive(&isAlive)) && isAlive) + newDataConn = false; + } + } + } + + if (newDataConn) { + mDataTransport->Close(NS_ERROR_ABORT); + mDataTransport = nullptr; + if (mDataStream) { + mDataStream->CloseWithStatus(NS_ERROR_ABORT); + mDataStream = nullptr; + } + } + } + + if (newDataConn) { + // now we know where to connect our data channel + nsCOMPtr sts = + do_GetService(NS_SOCKETTRANSPORTSERVICE_CONTRACTID); + if (!sts) return FTP_ERROR; + + nsCOMPtr strans; + + nsAutoCString host; + if (!mServerAddress.IsIPAddrAny()) { + char buf[kIPv6CStrBufSize]; + mServerAddress.ToStringBuffer(buf, sizeof(buf)); + host.Assign(buf); + } else { + /* + * In case of SOCKS5 remote DNS resolving, the peer address + * fetched previously will be invalid (0.0.0.0): it is unknown + * to us. But we can pass on the original hostname to the + * connect for the data connection. + */ + rv = mChannel->URI()->GetAsciiHost(host); + if (NS_FAILED(rv)) return FTP_ERROR; + } + + rv = sts->CreateTransport(nsTArray(), host, port, + mChannel->ProxyInfo(), + getter_AddRefs(strans)); // the data socket + if (NS_FAILED(rv)) return FTP_ERROR; + mDataTransport = strans; + + strans->SetQoSBits(gFtpHandler->GetDataQoSBits()); + + LOG(("FTP:(%p) created DT (%s:%x)\n", this, host.get(), port)); + + // hook ourself up as a proxy for status notifications + rv = mDataTransport->SetEventSink(this, GetCurrentEventTarget()); + NS_ENSURE_SUCCESS(rv, FTP_ERROR); + + if (mAction == PUT) { + NS_ASSERTION(!mRETRFailed, "Failed before uploading"); + + // nsIUploadChannel requires the upload stream to support ReadSegments. + // therefore, we can open an unbuffered socket output stream. + nsCOMPtr output; + rv = mDataTransport->OpenOutputStream(nsITransport::OPEN_UNBUFFERED, 0, 0, + getter_AddRefs(output)); + if (NS_FAILED(rv)) return FTP_ERROR; + + // perform the data copy on the socket transport thread. we do this + // because "output" is a socket output stream, so the result is that + // all work will be done on the socket transport thread. + nsCOMPtr stEventTarget = + do_GetService(NS_SOCKETTRANSPORTSERVICE_CONTRACTID); + if (!stEventTarget) return FTP_ERROR; + + nsCOMPtr copier = + do_CreateInstance(NS_ASYNCSTREAMCOPIER_CONTRACTID, &rv); + if (NS_SUCCEEDED(rv)) { + rv = copier->Init(mChannel->UploadStream(), output, stEventTarget, true, + false /* output is NOT buffered */, 0, true, true); + } + if (NS_FAILED(rv)) return FTP_ERROR; + + rv = copier->AsyncCopy(this, nullptr); + if (NS_FAILED(rv)) return FTP_ERROR; + + // hold a reference to the copier so we can cancel it if necessary. + mUploadRequest = copier; + + // update the current working directory before sending the STOR + // command. this is needed since we might be reusing a control + // connection. + return FTP_S_CWD; + } + + // + // else, we are reading from the data connection... + // + + // open a buffered, asynchronous socket input stream + nsCOMPtr input; + rv = mDataTransport->OpenInputStream(0, nsIOService::gDefaultSegmentSize, + nsIOService::gDefaultSegmentCount, + getter_AddRefs(input)); + NS_ENSURE_SUCCESS(rv, FTP_ERROR); + mDataStream = do_QueryInterface(input); + } + + if (mRETRFailed || mPath.IsEmpty() || mPath.Last() == '/') return FTP_S_CWD; + return FTP_S_SIZE; +} + +nsresult nsFtpState::S_feat() { + return SendFTPCommand(nsLiteralCString("FEAT" CRLF)); +} + +FTP_STATE +nsFtpState::R_feat() { + if (mResponseCode / 100 == 2) { + if (mResponseMsg.Find(nsLiteralCString(CRLF " UTF8" CRLF), true) > -1) { + // This FTP server supports UTF-8 encoding + mChannel->SetContentCharset("UTF-8"_ns); + mUseUTF8 = true; + return FTP_S_OPTS; + } + } + + mUseUTF8 = false; + return FTP_S_PWD; +} + +nsresult nsFtpState::S_opts() { + // This command is for compatibility of old FTP spec (IETF Draft) + return SendFTPCommand(nsLiteralCString("OPTS UTF8 ON" CRLF)); +} + +FTP_STATE +nsFtpState::R_opts() { + // Ignore error code because "OPTS UTF8 ON" is for compatibility of + // FTP server using IETF draft + return FTP_S_PWD; +} + +//////////////////////////////////////////////////////////////////////////////// +// nsIRequest methods: + +nsresult nsFtpState::Init(nsFtpChannel* channel) { + // parameter validation + NS_ASSERTION(channel, "FTP: needs a channel"); + + mChannel = channel; // a straight ref ptr to the channel + + mKeepRunning = true; + mSuppliedEntityID = channel->EntityID(); + + if (channel->UploadStream()) mAction = PUT; + + nsresult rv; + nsCOMPtr url = do_QueryInterface(mChannel->URI()); + + nsAutoCString host; + if (url) { + rv = url->GetAsciiHost(host); + } else { + rv = mChannel->URI()->GetAsciiHost(host); + } + if (NS_FAILED(rv) || host.IsEmpty()) { + return NS_ERROR_MALFORMED_URI; + } + + nsAutoCString path; + if (url) { + rv = url->GetFilePath(path); + } else { + rv = mChannel->URI()->GetPathQueryRef(path); + } + if (NS_FAILED(rv)) return rv; + + removeParamsFromPath(path); + + nsCOMPtr outURI; + // FTP parameters such as type=i are ignored + if (url) { + rv = NS_MutateURI(url).SetFilePath(path).Finalize(outURI); + } else { + rv = NS_MutateURI(mChannel->URI()).SetPathQueryRef(path).Finalize(outURI); + } + if (NS_SUCCEEDED(rv)) { + mChannel->UpdateURI(outURI); + } + + // Skip leading slash + char* fwdPtr = path.BeginWriting(); + if (!fwdPtr) return NS_ERROR_OUT_OF_MEMORY; + if (*fwdPtr == '/') fwdPtr++; + if (*fwdPtr != '\0') { + // now unescape it... %xx reduced inline to resulting character + int32_t len = NS_UnescapeURL(fwdPtr); + mPath.Assign(fwdPtr, len); + +#ifdef DEBUG + if (mPath.FindCharInSet(CRLF) >= 0) + NS_ERROR("NewURI() should've prevented this!!!"); +#endif + } + + // pull any username and/or password out of the uri + nsAutoCString uname; + rv = mChannel->URI()->GetUsername(uname); + if (NS_FAILED(rv)) return rv; + + if (!uname.IsEmpty() && !uname.EqualsLiteral("anonymous")) { + mAnonymous = false; + CopyUTF8toUTF16(NS_UnescapeURL(uname), mUsername); + + // return an error if we find a CR or LF in the username + if (uname.FindCharInSet(CRLF) >= 0) return NS_ERROR_MALFORMED_URI; + } + + nsAutoCString password; + rv = mChannel->URI()->GetPassword(password); + if (NS_FAILED(rv)) return rv; + + CopyUTF8toUTF16(NS_UnescapeURL(password), mPassword); + + // return an error if we find a CR or LF in the password + if (mPassword.FindCharInSet(CRLF) >= 0) return NS_ERROR_MALFORMED_URI; + + int32_t port; + rv = mChannel->URI()->GetPort(&port); + if (NS_FAILED(rv)) return rv; + + if (port > 0) mPort = port; + + // Lookup Proxy information asynchronously if it isn't already set + // on the channel and if we aren't configured explicitly to go directly + nsCOMPtr pps = + do_GetService(NS_PROTOCOLPROXYSERVICE_CONTRACTID); + + if (pps && !mChannel->ProxyInfo()) { + pps->AsyncResolve(static_cast(mChannel), 0, this, nullptr, + getter_AddRefs(mProxyRequest)); + } + + return NS_OK; +} + +void nsFtpState::Connect() { + mState = FTP_COMMAND_CONNECT; + mNextState = FTP_S_USER; + + nsresult rv = Process(); + + // check for errors. + if (NS_FAILED(rv)) { + LOG(("FTP:Process() failed: %" PRIx32 "\n", static_cast(rv))); + mInternalError = NS_ERROR_FAILURE; + mState = FTP_ERROR; + CloseWithStatus(mInternalError); + } +} + +void nsFtpState::KillControlConnection() { + mControlReadCarryOverBuf.Truncate(0); + + mAddressChecked = false; + mServerIsIPv6 = false; + + // if everything went okay, save the connection. + // FIX: need a better way to determine if we can cache the connections. + // there are some errors which do not mean that we need to kill the + // connection e.g. fnf. + + if (!mControlConnection) return; + + // kill the reference to ourselves in the control connection. + mControlConnection->WaitData(nullptr); + + if (NS_SUCCEEDED(mInternalError) && NS_SUCCEEDED(mControlStatus) && + mControlConnection->IsAlive() && mCacheConnection) { + LOG_INFO(("FTP:(%p) caching CC(%p)", this, mControlConnection.get())); + + // Store connection persistent data + mControlConnection->mServerType = mServerType; + mControlConnection->mPassword = mPassword; + mControlConnection->mPwd = mPwd; + mControlConnection->mUseUTF8 = mUseUTF8; + + nsresult rv = NS_OK; + // Don't cache controlconnection if anonymous (bug #473371) + if (!mChannel->HasLoadFlag(nsIRequest::LOAD_ANONYMOUS)) + rv = gFtpHandler->InsertConnection(mChannel->URI(), mControlConnection); + // Can't cache it? Kill it then. + mControlConnection->Disconnect(rv); + } else { + mControlConnection->Disconnect(NS_BINDING_ABORTED); + } + + mControlConnection = nullptr; +} + +nsresult nsFtpState::StopProcessing() { + // Only do this function once. + if (!mKeepRunning) return NS_OK; + mKeepRunning = false; + + LOG_INFO(("FTP:(%p) nsFtpState stopping", this)); + + if (NS_FAILED(mInternalError) && !mResponseMsg.IsEmpty()) { + // check to see if the control status is bad, forward the error message. + nsCOMPtr ftpChanP; + mChannel->GetCallback(ftpChanP); + if (ftpChanP) { + ftpChanP->SetErrorMsg(mResponseMsg.get(), mUseUTF8); + } + } + + nsresult broadcastErrorCode = mControlStatus; + if (NS_SUCCEEDED(broadcastErrorCode)) broadcastErrorCode = mInternalError; + + mInternalError = broadcastErrorCode; + + KillControlConnection(); + + // XXX This can fire before we are done loading data. Is that a problem? + OnTransportStatus(nullptr, NS_NET_STATUS_END_FTP_TRANSACTION, 0, 0); + + if (NS_FAILED(broadcastErrorCode)) CloseWithStatus(broadcastErrorCode); + + return NS_OK; +} + +nsresult nsFtpState::SendFTPCommand(const nsACString& command) { + NS_ASSERTION(mControlConnection, "null control connection"); + + // we don't want to log the password: + nsAutoCString logcmd(command); + if (StringBeginsWith(command, "PASS "_ns)) logcmd = "PASS xxxxx"; + + LOG(("FTP:(%p) writing \"%s\"\n", this, logcmd.get())); + + nsCOMPtr ftpSink; + mChannel->GetFTPEventSink(ftpSink); + if (ftpSink) ftpSink->OnFTPControlLog(false, logcmd.get()); + + if (mControlConnection) return mControlConnection->Write(command); + + return NS_ERROR_FAILURE; +} + +// Convert a unix-style filespec to VMS format +// /foo/fred/barney/file.txt -> foo:[fred.barney]file.txt +// /foo/file.txt -> foo:[000000]file.txt +void nsFtpState::ConvertFilespecToVMS(nsCString& fileString) { + int ntok = 1; + char *t, *nextToken; + nsAutoCString fileStringCopy; + + // Get a writeable copy we can strtok with. + fileStringCopy = fileString; + t = nsCRT::strtok(fileStringCopy.BeginWriting(), "/", &nextToken); + if (t) + while (nsCRT::strtok(nextToken, "/", &nextToken)) + ntok++; // count number of terms (tokens) + LOG(("FTP:(%p) ConvertFilespecToVMS ntok: %d\n", this, ntok)); + LOG(("FTP:(%p) ConvertFilespecToVMS from: \"%s\"\n", this, fileString.get())); + + if (fileString.First() == '/') { + // absolute filespec + // / -> [] + // /a -> a (doesn't really make much sense) + // /a/b -> a:[000000]b + // /a/b/c -> a:[b]c + // /a/b/c/d -> a:[b.c]d + if (ntok == 1) { + if (fileString.Length() == 1) { + // Just a slash + fileString.Truncate(); + fileString.AppendLiteral("[]"); + } else { + // just copy the name part (drop the leading slash) + fileStringCopy = fileString; + fileString = Substring(fileStringCopy, 1, fileStringCopy.Length() - 1); + } + } else { + // Get another copy since the last one was written to. + fileStringCopy = fileString; + fileString.Truncate(); + fileString.Append( + nsCRT::strtok(fileStringCopy.BeginWriting(), "/", &nextToken)); + fileString.AppendLiteral(":["); + if (ntok > 2) { + for (int i = 2; i < ntok; i++) { + if (i > 2) fileString.Append('.'); + fileString.Append(nsCRT::strtok(nextToken, "/", &nextToken)); + } + } else { + fileString.AppendLiteral("000000"); + } + fileString.Append(']'); + fileString.Append(nsCRT::strtok(nextToken, "/", &nextToken)); + } + } else { + // relative filespec + // a -> a + // a/b -> [.a]b + // a/b/c -> [.a.b]c + if (ntok == 1) { + // no slashes, just use the name as is + } else { + // Get another copy since the last one was written to. + fileStringCopy = fileString; + fileString.Truncate(); + fileString.AppendLiteral("[."); + fileString.Append( + nsCRT::strtok(fileStringCopy.BeginWriting(), "/", &nextToken)); + if (ntok > 2) { + for (int i = 2; i < ntok; i++) { + fileString.Append('.'); + fileString.Append(nsCRT::strtok(nextToken, "/", &nextToken)); + } + } + fileString.Append(']'); + fileString.Append(nsCRT::strtok(nextToken, "/", &nextToken)); + } + } + LOG(("FTP:(%p) ConvertFilespecToVMS to: \"%s\"\n", this, fileString.get())); +} + +// Convert a unix-style dirspec to VMS format +// /foo/fred/barney/rubble -> foo:[fred.barney.rubble] +// /foo/fred -> foo:[fred] +// /foo -> foo:[000000] +// (null) -> (null) +void nsFtpState::ConvertDirspecToVMS(nsCString& dirSpec) { + LOG(("FTP:(%p) ConvertDirspecToVMS from: \"%s\"\n", this, dirSpec.get())); + if (!dirSpec.IsEmpty()) { + if (dirSpec.Last() != '/') dirSpec.Append('/'); + // we can use the filespec routine if we make it look like a file name + dirSpec.Append('x'); + ConvertFilespecToVMS(dirSpec); + dirSpec.Truncate(dirSpec.Length() - 1); + } + LOG(("FTP:(%p) ConvertDirspecToVMS to: \"%s\"\n", this, dirSpec.get())); +} + +// Convert an absolute VMS style dirspec to UNIX format +void nsFtpState::ConvertDirspecFromVMS(nsCString& dirSpec) { + LOG(("FTP:(%p) ConvertDirspecFromVMS from: \"%s\"\n", this, dirSpec.get())); + if (dirSpec.IsEmpty()) { + dirSpec.Insert('.', 0); + } else { + dirSpec.Insert('/', 0); + dirSpec.ReplaceSubstring(":[", "/"); + dirSpec.ReplaceChar('.', '/'); + dirSpec.ReplaceChar(']', '/'); + } + LOG(("FTP:(%p) ConvertDirspecFromVMS to: \"%s\"\n", this, dirSpec.get())); +} + +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsFtpState::OnTransportStatus(nsITransport* transport, nsresult status, + int64_t progress, int64_t progressMax) { + // Mix signals from both the control and data connections. + + // Ignore data transfer events on the control connection. + if (mControlConnection && transport == mControlConnection->Transport()) { + switch (status) { + case NS_NET_STATUS_RESOLVING_HOST: + case NS_NET_STATUS_RESOLVED_HOST: + case NS_NET_STATUS_CONNECTING_TO: + case NS_NET_STATUS_CONNECTED_TO: + case NS_NET_STATUS_TLS_HANDSHAKE_STARTING: + case NS_NET_STATUS_TLS_HANDSHAKE_ENDED: + break; + default: + return NS_OK; + } + } + + // Ignore the progressMax value from the socket. We know the true size of + // the file based on the response from our SIZE request. Additionally, only + // report the max progress based on where we started/resumed. + mChannel->OnTransportStatus(nullptr, status, progress, + mFileSize - mChannel->StartPos()); + return NS_OK; +} + +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsFtpState::OnStartRequest(nsIRequest* request) { + mStorReplyReceived = false; + return NS_OK; +} + +NS_IMETHODIMP +nsFtpState::OnStopRequest(nsIRequest* request, nsresult status) { + mUploadRequest = nullptr; + + // Close() will be called when reply to STOR command is received + // see bug #389394 + if (!mStorReplyReceived) return NS_OK; + + // We're done uploading. Let our consumer know that we're done. + Close(); + return NS_OK; +} + +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsFtpState::Available(uint64_t* result) { + if (mDataStream) return mDataStream->Available(result); + + return nsBaseContentStream::Available(result); +} + +NS_IMETHODIMP +nsFtpState::ReadSegments(nsWriteSegmentFun writer, void* closure, + uint32_t count, uint32_t* result) { + // Insert a thunk here so that the input stream passed to the writer is this + // input stream instead of mDataStream. + + if (mDataStream) { + nsWriteSegmentThunk thunk = {this, writer, closure}; + nsresult rv; + rv = mDataStream->ReadSegments(NS_WriteSegmentThunk, &thunk, count, result); + return rv; + } + + return nsBaseContentStream::ReadSegments(writer, closure, count, result); +} + +NS_IMETHODIMP +nsFtpState::CloseWithStatus(nsresult status) { + LOG(("FTP:(%p) close [%" PRIx32 "]\n", this, static_cast(status))); + + // Shutdown the control connection processing if we are being closed with an + // error. Note: This method may be called several times. + if (!IsClosed() && NS_FAILED(status)) { + if (NS_SUCCEEDED(mInternalError)) mInternalError = status; + StopProcessing(); + } + + if (mUploadRequest) { + mUploadRequest->Cancel(NS_ERROR_ABORT); + mUploadRequest = nullptr; + } + + if (mDataTransport) { + // Shutdown the data transport. + mDataTransport->Close(NS_ERROR_ABORT); + mDataTransport = nullptr; + } + + if (mDataStream) { + mDataStream->CloseWithStatus(NS_ERROR_ABORT); + mDataStream = nullptr; + } + + return nsBaseContentStream::CloseWithStatus(status); +} + +static nsresult CreateHTTPProxiedChannel(nsIChannel* channel, nsIProxyInfo* pi, + nsIChannel** newChannel) { + nsresult rv; + nsCOMPtr ioService = do_GetIOService(&rv); + if (NS_FAILED(rv)) return rv; + + nsCOMPtr handler; + rv = ioService->GetProtocolHandler("http", getter_AddRefs(handler)); + if (NS_FAILED(rv)) return rv; + + nsCOMPtr pph = do_QueryInterface(handler, &rv); + if (NS_FAILED(rv)) return rv; + + nsCOMPtr uri; + channel->GetURI(getter_AddRefs(uri)); + + nsCOMPtr loadInfo = channel->LoadInfo(); + + return pph->NewProxiedChannel(uri, pi, 0, nullptr, loadInfo, newChannel); +} + +NS_IMETHODIMP +nsFtpState::OnProxyAvailable(nsICancelable* request, nsIChannel* channel, + nsIProxyInfo* pi, nsresult status) { + mProxyRequest = nullptr; + + // failed status code just implies DIRECT processing + + if (NS_SUCCEEDED(status)) { + nsAutoCString type; + if (pi && NS_SUCCEEDED(pi->GetType(type)) && type.EqualsLiteral("http")) { + // Proxy the FTP url via HTTP + // This would have been easier to just return a HTTP channel directly + // from nsIIOService::NewChannelFromURI(), but the proxy type cannot + // be reliabliy determined synchronously without jank due to pac, etc.. + LOG(("FTP:(%p) Configured to use a HTTP proxy channel\n", this)); + + nsCOMPtr newChannel; + if (NS_SUCCEEDED(CreateHTTPProxiedChannel(channel, pi, + getter_AddRefs(newChannel))) && + NS_SUCCEEDED(mChannel->Redirect( + newChannel, nsIChannelEventSink::REDIRECT_INTERNAL, true))) { + LOG(("FTP:(%p) Redirected to use a HTTP proxy channel\n", this)); + return NS_OK; + } + } else if (pi) { + // Proxy using the FTP protocol routed through a socks proxy + LOG(("FTP:(%p) Configured to use a SOCKS proxy channel\n", this)); + mChannel->SetProxyInfo(pi); + } + } + + if (mDeferredCallbackPending) { + mDeferredCallbackPending = false; + OnCallbackPending(); + } + return NS_OK; +} + +void nsFtpState::OnCallbackPending() { + if (mState == FTP_INIT) { + if (mProxyRequest) { + mDeferredCallbackPending = true; + return; + } + Connect(); + } else if (mDataStream) { + mDataStream->AsyncWait(this, 0, 0, CallbackTarget()); + } +} diff --git a/netwerk/protocol/ftp/nsFtpConnectionThread.h b/netwerk/protocol/ftp/nsFtpConnectionThread.h new file mode 100644 index 0000000000..ec1cf999c7 --- /dev/null +++ b/netwerk/protocol/ftp/nsFtpConnectionThread.h @@ -0,0 +1,254 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef __nsFtpConnectionThread__h_ +#define __nsFtpConnectionThread__h_ + +#include "nsBaseContentStream.h" + +#include "nsString.h" +#include "nsCOMPtr.h" +#include "nsIAsyncInputStream.h" +#include "nsITransport.h" +#include "mozilla/net/DNS.h" +#include "nsFtpControlConnection.h" +#include "nsIProtocolProxyCallback.h" + +// ftp server types +#define FTP_GENERIC_TYPE 0 +#define FTP_UNIX_TYPE 1 +#define FTP_VMS_TYPE 8 +#define FTP_NT_TYPE 9 +#define FTP_OS2_TYPE 11 + +// ftp states +typedef enum _FTP_STATE { + /////////////////////// + //// Internal states + FTP_INIT, + FTP_COMMAND_CONNECT, + FTP_READ_BUF, + FTP_ERROR, + FTP_COMPLETE, + + /////////////////////// + //// Command channel connection setup states + FTP_S_USER, + FTP_R_USER, + FTP_S_PASS, + FTP_R_PASS, + FTP_S_SYST, + FTP_R_SYST, + FTP_S_ACCT, + FTP_R_ACCT, + FTP_S_TYPE, + FTP_R_TYPE, + FTP_S_CWD, + FTP_R_CWD, + FTP_S_SIZE, + FTP_R_SIZE, + FTP_S_MDTM, + FTP_R_MDTM, + FTP_S_REST, + FTP_R_REST, + FTP_S_RETR, + FTP_R_RETR, + FTP_S_STOR, + FTP_R_STOR, + FTP_S_LIST, + FTP_R_LIST, + FTP_S_PASV, + FTP_R_PASV, + FTP_S_PWD, + FTP_R_PWD, + FTP_S_FEAT, + FTP_R_FEAT, + FTP_S_OPTS, + FTP_R_OPTS +} FTP_STATE; + +// higher level ftp actions +typedef enum _FTP_ACTION { GET, PUT } FTP_ACTION; + +class nsFtpChannel; +class nsICancelable; +class nsIProxyInfo; +class nsIStreamListener; + +// The nsFtpState object is the content stream for the channel. It implements +// nsIInputStreamCallback, so it can read data from the control connection. It +// implements nsITransportEventSink so it can mix status events from both the +// control connection and the data connection. + +class nsFtpState final : public nsBaseContentStream, + public nsIInputStreamCallback, + public nsITransportEventSink, + public nsIRequestObserver, + public nsFtpControlConnectionListener, + public nsIProtocolProxyCallback { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_NSIINPUTSTREAMCALLBACK + NS_DECL_NSITRANSPORTEVENTSINK + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSIPROTOCOLPROXYCALLBACK + + // Override input stream methods: + NS_IMETHOD CloseWithStatus(nsresult status) override; + NS_IMETHOD Available(uint64_t* result) override; + NS_IMETHOD ReadSegments(nsWriteSegmentFun fun, void* closure, uint32_t count, + uint32_t* result) override; + + // nsFtpControlConnectionListener methods: + virtual void OnControlDataAvailable(const char* data, + uint32_t dataLen) override; + virtual void OnControlError(nsresult status) override; + + nsFtpState(); + nsresult Init(nsFtpChannel* channel); + + protected: + // Notification from nsBaseContentStream::AsyncWait + virtual void OnCallbackPending() override; + + private: + virtual ~nsFtpState(); + + /////////////////////////////////// + // BEGIN: STATE METHODS + nsresult S_user(); + FTP_STATE R_user(); + nsresult S_pass(); + FTP_STATE R_pass(); + nsresult S_syst(); + FTP_STATE R_syst(); + nsresult S_acct(); + FTP_STATE R_acct(); + + nsresult S_type(); + FTP_STATE R_type(); + nsresult S_cwd(); + FTP_STATE R_cwd(); + + nsresult S_size(); + FTP_STATE R_size(); + nsresult S_mdtm(); + FTP_STATE R_mdtm(); + nsresult S_list(); + FTP_STATE R_list(); + + nsresult S_rest(); + FTP_STATE R_rest(); + nsresult S_retr(); + FTP_STATE R_retr(); + nsresult S_stor(); + FTP_STATE R_stor(); + nsresult S_pasv(); + FTP_STATE R_pasv(); + nsresult S_pwd(); + FTP_STATE R_pwd(); + nsresult S_feat(); + FTP_STATE R_feat(); + nsresult S_opts(); + FTP_STATE R_opts(); + // END: STATE METHODS + /////////////////////////////////// + + // internal methods + void MoveToNextState(FTP_STATE nextState); + nsresult Process(); + + void KillControlConnection(); + nsresult StopProcessing(); + nsresult EstablishControlConnection(); + nsresult SendFTPCommand(const nsACString& command); + void ConvertFilespecToVMS(nsCString& fileSpec); + void ConvertDirspecToVMS(nsCString& fileSpec); + void ConvertDirspecFromVMS(nsCString& fileSpec); + nsresult BuildStreamConverter(nsIStreamListener** convertStreamListener); + nsresult SetContentType(); + + /** + * This method is called to kick-off the FTP state machine. mState is + * reset to FTP_COMMAND_CONNECT, and the FTP state machine progresses from + * there. This method is initially called (indirectly) from the channel's + * AsyncOpen implementation. + */ + void Connect(); + + /////////////////////////////////// + // Private members + + nsCOMPtr mHandler; // Ref to gFtpHandler + + // ****** state machine vars + FTP_STATE mState; // the current state + FTP_STATE mNextState; // the next state + bool mKeepRunning; // thread event loop boolean + int32_t mResponseCode; // the last command response code + nsCString mResponseMsg; // the last command response text + + // ****** channel/transport/stream vars + RefPtr + mControlConnection; // cacheable control connection (owns mCPipe) + bool mReceivedControlData; + bool mTryingCachedControl; // retrying the password + bool mRETRFailed; // Did we already try a RETR and it failed? + uint64_t mFileSize; + nsCString mModTime; + + // ****** consumer vars + RefPtr + mChannel; // our owning FTP channel we pass through our events + nsCOMPtr mProxyInfo; + + // ****** connection cache vars + int32_t mServerType; // What kind of server are we talking to + + // ****** protocol interpretation related state vars + nsString mUsername; // username + nsString mPassword; // password + FTP_ACTION mAction; // the higher level action (GET/PUT) + bool mAnonymous; // try connecting anonymous (default) + bool mRetryPass; // retrying the password + bool mStorReplyReceived; // FALSE if waiting for STOR + // completion status from server + bool mRlist1xxReceived; // TRUE if we have received a LIST 1xx + // response from the server + bool mRretr1xxReceived; // TRUE if we have received a RETR 1xx + // response from the server + bool mRstor1xxReceived; // TRUE if we have received a STOR 1xx + // response from the server + nsresult mInternalError; // represents internal state errors + bool mReconnectAndLoginAgain; + bool mCacheConnection; + + // ****** URI vars + int32_t mPort; // the port to connect to + nsString mFilename; // url filename (if any) + nsCString mPath; // the url's path + nsCString mPwd; // login Path + + // ****** other vars + nsCOMPtr mDataTransport; + nsCOMPtr mDataStream; + nsCOMPtr mUploadRequest; + bool mAddressChecked; + bool mServerIsIPv6; + bool mUseUTF8; + + mozilla::net::NetAddr mServerAddress; + + // ***** control read gvars + nsresult mControlStatus; + nsCString mControlReadCarryOverBuf; + + nsCString mSuppliedEntityID; + + nsCOMPtr mProxyRequest; + bool mDeferredCallbackPending; +}; + +#endif //__nsFtpConnectionThread__h_ diff --git a/netwerk/protocol/ftp/nsFtpControlConnection.cpp b/netwerk/protocol/ftp/nsFtpControlConnection.cpp new file mode 100644 index 0000000000..1d54ed9de2 --- /dev/null +++ b/netwerk/protocol/ftp/nsFtpControlConnection.cpp @@ -0,0 +1,169 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 "nsIOService.h" +#include "nsFtpControlConnection.h" +#include "nsFtpProtocolHandler.h" +#include "mozilla/Logging.h" +#include "nsIInputStream.h" +#include "nsISocketTransportService.h" +#include "nsISocketTransport.h" +#include "nsThreadUtils.h" +#include "nsNetCID.h" +#include "nsTArray.h" +#include + +using namespace mozilla; +using namespace mozilla::net; + +extern LazyLogModule gFTPLog; +#define LOG(args) MOZ_LOG(gFTPLog, mozilla::LogLevel::Debug, args) +#define LOG_INFO(args) MOZ_LOG(gFTPLog, mozilla::LogLevel::Info, args) + +// +// nsFtpControlConnection implementation ... +// + +NS_IMPL_ISUPPORTS(nsFtpControlConnection, nsIInputStreamCallback) + +NS_IMETHODIMP +nsFtpControlConnection::OnInputStreamReady(nsIAsyncInputStream* stream) { + char data[4096]; + + // Consume data whether we have a listener or not. + uint64_t avail64; + uint32_t avail = 0; + nsresult rv = stream->Available(&avail64); + if (NS_SUCCEEDED(rv)) { + avail = (uint32_t)std::min(avail64, (uint64_t)sizeof(data)); + + uint32_t n; + rv = stream->Read(data, avail, &n); + if (NS_SUCCEEDED(rv)) avail = n; + } + + // It's important that we null out mListener before calling one of its + // methods as it may call WaitData, which would queue up another read. + + RefPtr listener; + listener.swap(mListener); + + if (!listener) return NS_OK; + + if (NS_FAILED(rv)) { + listener->OnControlError(rv); + } else { + listener->OnControlDataAvailable(data, avail); + } + + return NS_OK; +} + +nsFtpControlConnection::nsFtpControlConnection(const nsACString& host, + uint32_t port) + : mServerType(0), + mSuspendedWrite(0), + mSessionId(gFtpHandler->GetSessionId()), + mUseUTF8(false), + mHost(host), + mPort(port) { + LOG_INFO(("FTP:CC created @%p", this)); +} + +nsFtpControlConnection::~nsFtpControlConnection() { + LOG_INFO(("FTP:CC destroyed @%p", this)); +} + +bool nsFtpControlConnection::IsAlive() { + if (!mSocket) return false; + + bool isAlive = false; + mSocket->IsAlive(&isAlive); + return isAlive; +} +nsresult nsFtpControlConnection::Connect(nsIProxyInfo* proxyInfo, + nsITransportEventSink* eventSink) { + if (mSocket) return NS_OK; + + // build our own + nsresult rv; + nsCOMPtr sts = + do_GetService(NS_SOCKETTRANSPORTSERVICE_CONTRACTID, &rv); + if (NS_FAILED(rv)) return rv; + + rv = sts->CreateTransport(nsTArray(), mHost, mPort, proxyInfo, + getter_AddRefs(mSocket)); // the command transport + if (NS_FAILED(rv)) return rv; + + mSocket->SetQoSBits(gFtpHandler->GetControlQoSBits()); + + // proxy transport events back to current thread + if (eventSink) mSocket->SetEventSink(eventSink, GetCurrentEventTarget()); + + // open buffered, blocking output stream to socket. so long as commands + // do not exceed 1024 bytes in length, the writing thread (the main thread) + // will not block. this should be OK. + rv = mSocket->OpenOutputStream(nsITransport::OPEN_BLOCKING, 1024, 1, + getter_AddRefs(mSocketOutput)); + if (NS_FAILED(rv)) return rv; + + // open buffered, non-blocking/asynchronous input stream to socket. + nsCOMPtr inStream; + rv = mSocket->OpenInputStream(0, nsIOService::gDefaultSegmentSize, + nsIOService::gDefaultSegmentCount, + getter_AddRefs(inStream)); + if (NS_SUCCEEDED(rv)) mSocketInput = do_QueryInterface(inStream); + + return rv; +} + +nsresult nsFtpControlConnection::WaitData( + nsFtpControlConnectionListener* listener) { + LOG(("FTP:(%p) wait data [listener=%p]\n", this, listener)); + + // If listener is null, then simply disconnect the listener. Otherwise, + // ensure that we are listening. + if (!listener) { + mListener = nullptr; + return NS_OK; + } + + NS_ENSURE_STATE(mSocketInput); + + mListener = listener; + return mSocketInput->AsyncWait(this, 0, 0, GetCurrentEventTarget()); +} + +nsresult nsFtpControlConnection::Disconnect(nsresult status) { + if (!mSocket) return NS_OK; // already disconnected + + LOG_INFO(("FTP:(%p) CC disconnecting (%" PRIx32 ")", this, + static_cast(status))); + + if (NS_FAILED(status)) { + // break cyclic reference! + mSocket->Close(status); + mSocket = nullptr; + mSocketInput->AsyncWait(nullptr, 0, 0, nullptr); // clear any observer + mSocketInput = nullptr; + mSocketOutput = nullptr; + } + + return NS_OK; +} + +nsresult nsFtpControlConnection::Write(const nsACString& command) { + NS_ENSURE_STATE(mSocketOutput); + + uint32_t len = command.Length(); + uint32_t cnt; + nsresult rv = mSocketOutput->Write(command.Data(), len, &cnt); + + if (NS_FAILED(rv)) return rv; + + if (len != cnt) return NS_ERROR_FAILURE; + + return NS_OK; +} diff --git a/netwerk/protocol/ftp/nsFtpControlConnection.h b/netwerk/protocol/ftp/nsFtpControlConnection.h new file mode 100644 index 0000000000..c29e23eeed --- /dev/null +++ b/netwerk/protocol/ftp/nsFtpControlConnection.h @@ -0,0 +1,83 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set et ts=4 sts=2 sw=2 cin: */ +/* 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/. */ + +#ifndef nsFtpControlConnection_h___ +#define nsFtpControlConnection_h___ + +#include "nsCOMPtr.h" + +#include "nsISocketTransport.h" +#include "nsIAsyncInputStream.h" +#include "nsString.h" +#include "mozilla/Attributes.h" + +class nsIOutputStream; +class nsIProxyInfo; +class nsITransportEventSink; + +class nsFtpControlConnectionListener : public nsISupports { + public: + /** + * Called when a chunk of data arrives on the control connection. + * @param data + * The new data or null if an error occurred. + * @param dataLen + * The data length in bytes. + */ + virtual void OnControlDataAvailable(const char* data, uint32_t dataLen) = 0; + + /** + * Called when an error occurs on the control connection. + * @param status + * A failure code providing more info about the error. + */ + virtual void OnControlError(nsresult status) = 0; +}; + +class nsFtpControlConnection final : public nsIInputStreamCallback { + ~nsFtpControlConnection(); + + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIINPUTSTREAMCALLBACK + + nsFtpControlConnection(const nsACString& host, uint32_t port); + + nsresult Connect(nsIProxyInfo* proxyInfo, nsITransportEventSink* eventSink); + nsresult Disconnect(nsresult status); + nsresult Write(const nsACString& command); + + bool IsAlive(); + + nsITransport* Transport() { return mSocket; } + + /** + * Call this function to be notified asynchronously when there is data + * available for the socket. The listener passed to this method replaces + * any existing listener, and the listener can be null to disconnect the + * previous listener. + */ + nsresult WaitData(nsFtpControlConnectionListener* listener); + + uint32_t mServerType; // what kind of server is it. + nsString mPassword; + int32_t mSuspendedWrite; + nsCString mPwd; + uint32_t mSessionId; + bool mUseUTF8; + + private: + nsCString mHost; + uint32_t mPort; + + nsCOMPtr mSocket; + nsCOMPtr mSocketOutput; + nsCOMPtr mSocketInput; + + RefPtr mListener; +}; + +#endif diff --git a/netwerk/protocol/ftp/nsFtpProtocolHandler.cpp b/netwerk/protocol/ftp/nsFtpProtocolHandler.cpp new file mode 100644 index 0000000000..99c8a19693 --- /dev/null +++ b/netwerk/protocol/ftp/nsFtpProtocolHandler.cpp @@ -0,0 +1,331 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * 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/net/NeckoChild.h" +#include "mozilla/net/FTPChannelChild.h" +using namespace mozilla; +using namespace mozilla::net; + +#include "nsFtpProtocolHandler.h" +#include "nsFTPChannel.h" +#include "mozilla/Logging.h" +#include "nsIPrefBranch.h" +#include "nsIObserverService.h" +#include "nsEscape.h" +#include "nsAlgorithm.h" + +//----------------------------------------------------------------------------- + +// +// Log module for FTP Protocol logging... +// +// To enable logging (see prlog.h for full details): +// +// set MOZ_LOG=nsFtp:5 +// set MOZ_LOG_FILE=ftp.log +// +// This enables LogLevel::Debug level information and places all output in +// the file ftp.log. +// +LazyLogModule gFTPLog("nsFtp"); +#undef LOG +#define LOG(args) MOZ_LOG(gFTPLog, mozilla::LogLevel::Debug, args) + +//----------------------------------------------------------------------------- + +#define IDLE_TIMEOUT_PREF "network.ftp.idleConnectionTimeout" +#define IDLE_CONNECTION_LIMIT 8 /* TODO pref me */ + +#define ENABLED_PREF "network.ftp.enabled" +#define QOS_DATA_PREF "network.ftp.data.qos" +#define QOS_CONTROL_PREF "network.ftp.control.qos" + +nsFtpProtocolHandler* gFtpHandler = nullptr; + +//----------------------------------------------------------------------------- + +nsFtpProtocolHandler::nsFtpProtocolHandler() + : mIdleTimeout(-1), + mEnabled(true), + mSessionId(0), + mControlQoSBits(0x00), + mDataQoSBits(0x00) { + LOG(("FTP:creating handler @%p\n", this)); + + gFtpHandler = this; +} + +nsFtpProtocolHandler::~nsFtpProtocolHandler() { + LOG(("FTP:destroying handler @%p\n", this)); + + NS_ASSERTION(mRootConnectionList.Length() == 0, "why wasn't Observe called?"); + + gFtpHandler = nullptr; +} + +NS_IMPL_ISUPPORTS(nsFtpProtocolHandler, nsIProtocolHandler, + nsIProxiedProtocolHandler, nsIObserver, + nsISupportsWeakReference) + +nsresult nsFtpProtocolHandler::Init() { + if (IsNeckoChild()) NeckoChild::InitNeckoChild(); + + if (mIdleTimeout == -1) { + nsresult rv; + nsCOMPtr branch = + do_GetService(NS_PREFSERVICE_CONTRACTID, &rv); + if (NS_FAILED(rv)) return rv; + + rv = branch->GetIntPref(IDLE_TIMEOUT_PREF, &mIdleTimeout); + if (NS_FAILED(rv)) mIdleTimeout = 5 * 60; // 5 minute default + + rv = branch->AddObserver(IDLE_TIMEOUT_PREF, this, true); + if (NS_FAILED(rv)) return rv; + + rv = branch->GetBoolPref(ENABLED_PREF, &mEnabled); + if (NS_FAILED(rv)) mEnabled = true; + + rv = branch->AddObserver(ENABLED_PREF, this, true); + if (NS_FAILED(rv)) return rv; + + int32_t val; + rv = branch->GetIntPref(QOS_DATA_PREF, &val); + if (NS_SUCCEEDED(rv)) mDataQoSBits = (uint8_t)clamped(val, 0, 0xff); + + rv = branch->AddObserver(QOS_DATA_PREF, this, true); + if (NS_FAILED(rv)) return rv; + + rv = branch->GetIntPref(QOS_CONTROL_PREF, &val); + if (NS_SUCCEEDED(rv)) mControlQoSBits = (uint8_t)clamped(val, 0, 0xff); + + rv = branch->AddObserver(QOS_CONTROL_PREF, this, true); + if (NS_FAILED(rv)) return rv; + } + + nsCOMPtr observerService = + mozilla::services::GetObserverService(); + if (observerService) { + observerService->AddObserver(this, "network:offline-about-to-go-offline", + true); + + observerService->AddObserver(this, "net:clear-active-logins", true); + } + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsIProtocolHandler methods: + +NS_IMETHODIMP +nsFtpProtocolHandler::GetScheme(nsACString& result) { + result.AssignLiteral("ftp"); + return NS_OK; +} + +NS_IMETHODIMP +nsFtpProtocolHandler::GetDefaultPort(int32_t* result) { + *result = 21; + return NS_OK; +} + +NS_IMETHODIMP +nsFtpProtocolHandler::GetProtocolFlags(uint32_t* result) { + *result = URI_STD | ALLOWS_PROXY | ALLOWS_PROXY_HTTP | URI_LOADABLE_BY_ANYONE; + return NS_OK; +} + +NS_IMETHODIMP +nsFtpProtocolHandler::NewChannel(nsIURI* url, nsILoadInfo* aLoadInfo, + nsIChannel** result) { + return NewProxiedChannel(url, nullptr, 0, nullptr, aLoadInfo, result); +} + +NS_IMETHODIMP +nsFtpProtocolHandler::NewProxiedChannel(nsIURI* uri, nsIProxyInfo* proxyInfo, + uint32_t proxyResolveFlags, + nsIURI* proxyURI, + nsILoadInfo* aLoadInfo, + nsIChannel** result) { + if (!mEnabled) { + return NS_ERROR_UNKNOWN_PROTOCOL; + } + + NS_ENSURE_ARG_POINTER(uri); + RefPtr channel; + if (IsNeckoChild()) + channel = new FTPChannelChild(uri); + else + channel = new nsFtpChannel(uri, proxyInfo); + + // set the loadInfo on the new channel + nsresult rv = channel->SetLoadInfo(aLoadInfo); + NS_ENSURE_SUCCESS(rv, rv); + + channel.forget(result); + return rv; +} + +NS_IMETHODIMP +nsFtpProtocolHandler::AllowPort(int32_t port, const char* scheme, + bool* _retval) { + *_retval = (port == 21 || port == 22); + return NS_OK; +} + +// connection cache methods + +void nsFtpProtocolHandler::Timeout(nsITimer* aTimer, void* aClosure) { + LOG(("FTP:timeout reached for %p\n", aClosure)); + + bool found = gFtpHandler->mRootConnectionList.RemoveElement(aClosure); + if (!found) { + NS_ERROR("timerStruct not found"); + return; + } + + timerStruct* s = (timerStruct*)aClosure; + delete s; +} + +nsresult nsFtpProtocolHandler::RemoveConnection( + nsIURI* aKey, nsFtpControlConnection** _retval) { + NS_ASSERTION(_retval, "null pointer"); + NS_ASSERTION(aKey, "null pointer"); + + *_retval = nullptr; + + nsAutoCString spec; + aKey->GetPrePath(spec); + + LOG(("FTP:removing connection for %s\n", spec.get())); + + timerStruct* ts = nullptr; + uint32_t i; + bool found = false; + + for (i = 0; i < mRootConnectionList.Length(); ++i) { + ts = mRootConnectionList[i]; + if (strcmp(spec.get(), ts->key) == 0) { + found = true; + mRootConnectionList.RemoveElementAt(i); + break; + } + } + + if (!found) return NS_ERROR_FAILURE; + + // swap connection ownership + ts->conn.forget(_retval); + delete ts; + + return NS_OK; +} + +nsresult nsFtpProtocolHandler::InsertConnection(nsIURI* aKey, + nsFtpControlConnection* aConn) { + NS_ASSERTION(aConn, "null pointer"); + NS_ASSERTION(aKey, "null pointer"); + + if (aConn->mSessionId != mSessionId) return NS_ERROR_FAILURE; + + nsAutoCString spec; + aKey->GetPrePath(spec); + + LOG(("FTP:inserting connection for %s\n", spec.get())); + + timerStruct* ts = new timerStruct(); + if (!ts) return NS_ERROR_OUT_OF_MEMORY; + + nsCOMPtr timer; + nsresult rv = NS_NewTimerWithFuncCallback( + getter_AddRefs(timer), nsFtpProtocolHandler::Timeout, ts, + mIdleTimeout * 1000, nsITimer::TYPE_REPEATING_SLACK, + "nsFtpProtocolHandler::InsertConnection"); + if (NS_FAILED(rv)) { + delete ts; + return rv; + } + + ts->key = ToNewCString(spec, mozilla::fallible); + if (!ts->key) { + delete ts; + return NS_ERROR_OUT_OF_MEMORY; + } + + // ts->conn is a RefPtr + ts->conn = aConn; + ts->timer = timer; + + // + // limit number of idle connections. if limit is reached, then prune + // eldest connection with matching key. if none matching, then prune + // eldest connection. + // + if (mRootConnectionList.Length() == IDLE_CONNECTION_LIMIT) { + uint32_t i; + for (i = 0; i < mRootConnectionList.Length(); ++i) { + timerStruct* candidate = mRootConnectionList[i]; + if (strcmp(candidate->key, ts->key) == 0) { + mRootConnectionList.RemoveElementAt(i); + delete candidate; + break; + } + } + if (mRootConnectionList.Length() == IDLE_CONNECTION_LIMIT) { + timerStruct* eldest = mRootConnectionList[0]; + mRootConnectionList.RemoveElementAt(0); + delete eldest; + } + } + + mRootConnectionList.AppendElement(ts); + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsIObserver + +NS_IMETHODIMP +nsFtpProtocolHandler::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) { + LOG(("FTP:observing [%s]\n", aTopic)); + + if (!strcmp(aTopic, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID)) { + nsCOMPtr branch = do_QueryInterface(aSubject); + if (!branch) { + NS_ERROR("no prefbranch"); + return NS_ERROR_UNEXPECTED; + } + int32_t val; + nsresult rv = branch->GetIntPref(IDLE_TIMEOUT_PREF, &val); + if (NS_SUCCEEDED(rv)) mIdleTimeout = val; + bool enabled; + rv = branch->GetBoolPref(ENABLED_PREF, &enabled); + if (NS_SUCCEEDED(rv)) mEnabled = enabled; + + rv = branch->GetIntPref(QOS_DATA_PREF, &val); + if (NS_SUCCEEDED(rv)) mDataQoSBits = (uint8_t)clamped(val, 0, 0xff); + + rv = branch->GetIntPref(QOS_CONTROL_PREF, &val); + if (NS_SUCCEEDED(rv)) mControlQoSBits = (uint8_t)clamped(val, 0, 0xff); + } else if (!strcmp(aTopic, "network:offline-about-to-go-offline")) { + ClearAllConnections(); + } else if (!strcmp(aTopic, "net:clear-active-logins")) { + ClearAllConnections(); + mSessionId++; + } else { + MOZ_ASSERT_UNREACHABLE("unexpected topic"); + } + + return NS_OK; +} + +void nsFtpProtocolHandler::ClearAllConnections() { + uint32_t i; + for (i = 0; i < mRootConnectionList.Length(); ++i) + delete mRootConnectionList[i]; + mRootConnectionList.Clear(); +} diff --git a/netwerk/protocol/ftp/nsFtpProtocolHandler.h b/netwerk/protocol/ftp/nsFtpProtocolHandler.h new file mode 100644 index 0000000000..38ce33f8b7 --- /dev/null +++ b/netwerk/protocol/ftp/nsFtpProtocolHandler.h @@ -0,0 +1,85 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef nsFtpProtocolHandler_h__ +#define nsFtpProtocolHandler_h__ + +#include "nsFtpControlConnection.h" +#include "nsIProxiedProtocolHandler.h" +#include "nsTArray.h" +#include "nsITimer.h" +#include "nsIObserver.h" +#include "nsWeakReference.h" + +//----------------------------------------------------------------------------- + +class nsFtpProtocolHandler final : public nsIProxiedProtocolHandler, + public nsIObserver, + public nsSupportsWeakReference { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIPROTOCOLHANDLER + NS_DECL_NSIPROXIEDPROTOCOLHANDLER + NS_DECL_NSIOBSERVER + + nsFtpProtocolHandler(); + + nsresult Init(); + + // FTP Connection list access + nsresult InsertConnection(nsIURI* aKey, nsFtpControlConnection* aConn); + nsresult RemoveConnection(nsIURI* aKey, nsFtpControlConnection** aConn); + uint32_t GetSessionId() { return mSessionId; } + + uint8_t GetDataQoSBits() { return mDataQoSBits; } + uint8_t GetControlQoSBits() { return mControlQoSBits; } + + private: + virtual ~nsFtpProtocolHandler(); + + // Stuff for the timer callback function + struct timerStruct { + nsCOMPtr timer; + RefPtr conn; + char* key; + + timerStruct() : key(nullptr) {} + + ~timerStruct() { + if (timer) timer->Cancel(); + if (key) free(key); + if (conn) { + conn->Disconnect(NS_ERROR_ABORT); + } + } + }; + + static void Timeout(nsITimer* aTimer, void* aClosure); + void ClearAllConnections(); + + nsTArray mRootConnectionList; + + int32_t mIdleTimeout; + bool mEnabled; + + // When "clear active logins" is performed, all idle connection are dropped + // and mSessionId is incremented. When nsFtpState wants to insert idle + // connection we refuse to cache if its mSessionId is different (i.e. + // control connection had been created before last "clear active logins" was + // performed. + uint32_t mSessionId; + + uint8_t mControlQoSBits; + uint8_t mDataQoSBits; +}; + +//----------------------------------------------------------------------------- + +extern nsFtpProtocolHandler* gFtpHandler; + +#include "mozilla/Logging.h" +extern mozilla::LazyLogModule gFTPLog; + +#endif // !nsFtpProtocolHandler_h__ diff --git a/netwerk/protocol/ftp/nsIFTPChannel.idl b/netwerk/protocol/ftp/nsIFTPChannel.idl new file mode 100644 index 0000000000..de82983841 --- /dev/null +++ b/netwerk/protocol/ftp/nsIFTPChannel.idl @@ -0,0 +1,29 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* 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 "nsISupports.idl" + +/** + * This interface may be used to determine if a channel is a FTP channel. + */ +[scriptable, uuid(07f0d5cd-1fd5-4aa3-b6fc-665bdc5dbf9f)] +interface nsIFTPChannel : nsISupports +{ + attribute PRTime lastModifiedTime; +}; + +/** + * This interface may be defined as a notification callback on the FTP + * channel. It allows a consumer to receive a log of the FTP control + * connection conversation. + */ +[scriptable, uuid(455d4234-0330-43d2-bbfb-99afbecbfeb0)] +interface nsIFTPEventSink : nsISupports +{ + /** + * XXX document this method! (see bug 328915) + */ + void OnFTPControlLog(in boolean server, in string msg); +}; diff --git a/netwerk/protocol/ftp/nsIFTPChannelParentInternal.idl b/netwerk/protocol/ftp/nsIFTPChannelParentInternal.idl new file mode 100644 index 0000000000..2642c804b1 --- /dev/null +++ b/netwerk/protocol/ftp/nsIFTPChannelParentInternal.idl @@ -0,0 +1,15 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 "nsISupports.idl" + +/** + * This is an internal interface for FTP parent channel. + */ +[builtinclass, uuid(87b58410-83cb-42a7-b57b-27c07ef828d7)] +interface nsIFTPChannelParentInternal : nsISupports +{ + void setErrorMsg(in string msg, in boolean useUTF8); +}; diff --git a/netwerk/protocol/ftp/test/frametest/contents.html b/netwerk/protocol/ftp/test/frametest/contents.html new file mode 100644 index 0000000000..077b8d8f75 --- /dev/null +++ b/netwerk/protocol/ftp/test/frametest/contents.html @@ -0,0 +1,5 @@ + + +

Click a link to the left

+ + diff --git a/netwerk/protocol/ftp/test/frametest/index.html b/netwerk/protocol/ftp/test/frametest/index.html new file mode 100644 index 0000000000..6a8a736251 --- /dev/null +++ b/netwerk/protocol/ftp/test/frametest/index.html @@ -0,0 +1,12 @@ + + +FTP Frameset Test + + + + + + + + + diff --git a/netwerk/protocol/ftp/test/frametest/menu.html b/netwerk/protocol/ftp/test/frametest/menu.html new file mode 100644 index 0000000000..9f43f00ee8 --- /dev/null +++ b/netwerk/protocol/ftp/test/frametest/menu.html @@ -0,0 +1,371 @@ + + + + +


+ + diff --git a/netwerk/protocol/gio/components.conf b/netwerk/protocol/gio/components.conf new file mode 100644 index 0000000000..2db452b649 --- /dev/null +++ b/netwerk/protocol/gio/components.conf @@ -0,0 +1,16 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +Classes = [ + { + 'cid': '{ee706783-3af8-4d19-9e84-e2ebfe213480}', + 'contract_ids': ['@mozilla.org/network/protocol;1?name=moz-gio'], + 'singleton': True, + 'type': 'nsGIOProtocolHandler', + 'headers': ['nsGIOProtocolHandler.h'], + 'constructor': 'nsGIOProtocolHandler::GetSingleton', + }, +] diff --git a/netwerk/protocol/gio/moz.build b/netwerk/protocol/gio/moz.build new file mode 100644 index 0000000000..cd659096bf --- /dev/null +++ b/netwerk/protocol/gio/moz.build @@ -0,0 +1,24 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +SOURCES += [ + "nsGIOProtocolHandler.cpp", +] + +XPCOM_MANIFESTS += [ + "components.conf", +] + +EXPORTS += [ + "nsGIOProtocolHandler.h", +] + +FINAL_LIBRARY = "xul" + +CXXFLAGS += CONFIG["TK_CFLAGS"] + +with Files("**"): + BUG_COMPONENT = ("Core", "Widget: Gtk") diff --git a/netwerk/protocol/gio/nsGIOProtocolHandler.cpp b/netwerk/protocol/gio/nsGIOProtocolHandler.cpp new file mode 100644 index 0000000000..61c642d8f1 --- /dev/null +++ b/netwerk/protocol/gio/nsGIOProtocolHandler.cpp @@ -0,0 +1,991 @@ +/* vim:set ts=2 sw=2 et cindent: */ +/* 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/. */ + +/* + * This code is based on original Mozilla gnome-vfs extension. It implements + * input stream provided by GVFS/GIO. + */ +#include "nsGIOProtocolHandler.h" +#include "mozilla/Components.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/Logging.h" +#include "mozilla/NullPrincipal.h" +#include "nsIPrefBranch.h" +#include "nsIPrefService.h" +#include "nsIObserver.h" +#include "nsThreadUtils.h" +#include "nsProxyRelease.h" +#include "nsIStringBundle.h" +#include "nsMimeTypes.h" +#include "nsNetCID.h" +#include "nsNetUtil.h" +#include "nsServiceManagerUtils.h" +#include "nsIURI.h" +#include "nsIAuthPrompt.h" +#include "nsIChannel.h" +#include "nsIInputStream.h" +#include "nsIProtocolHandler.h" +#include "mozilla/Monitor.h" +#include "plstr.h" +#include "prtime.h" +#include +#include + +using namespace mozilla; + +#define MOZ_GIO_SCHEME "moz-gio" +#define MOZ_GIO_SUPPORTED_PROTOCOLS "network.gio.supported-protocols" + +//----------------------------------------------------------------------------- + +// NSPR_LOG_MODULES=gio:5 +static mozilla::LazyLogModule sGIOLog("gio"); +#define LOG(args) MOZ_LOG(sGIOLog, mozilla::LogLevel::Debug, args) + +//----------------------------------------------------------------------------- +static nsresult MapGIOResult(gint code) { + switch (code) { + case G_IO_ERROR_NOT_FOUND: + return NS_ERROR_FILE_NOT_FOUND; // shows error + case G_IO_ERROR_INVALID_ARGUMENT: + return NS_ERROR_INVALID_ARG; + case G_IO_ERROR_NOT_SUPPORTED: + return NS_ERROR_NOT_AVAILABLE; + case G_IO_ERROR_NO_SPACE: + return NS_ERROR_FILE_NO_DEVICE_SPACE; + case G_IO_ERROR_READ_ONLY: + return NS_ERROR_FILE_READ_ONLY; + case G_IO_ERROR_PERMISSION_DENIED: + return NS_ERROR_FILE_ACCESS_DENIED; // wrong password/login + case G_IO_ERROR_CLOSED: + return NS_BASE_STREAM_CLOSED; // was EOF + case G_IO_ERROR_NOT_DIRECTORY: + return NS_ERROR_FILE_NOT_DIRECTORY; + case G_IO_ERROR_PENDING: + return NS_ERROR_IN_PROGRESS; + case G_IO_ERROR_EXISTS: + return NS_ERROR_FILE_ALREADY_EXISTS; + case G_IO_ERROR_IS_DIRECTORY: + return NS_ERROR_FILE_IS_DIRECTORY; + case G_IO_ERROR_NOT_MOUNTED: + return NS_ERROR_NOT_CONNECTED; // shows error + case G_IO_ERROR_HOST_NOT_FOUND: + return NS_ERROR_UNKNOWN_HOST; // shows error + case G_IO_ERROR_CANCELLED: + return NS_ERROR_ABORT; + case G_IO_ERROR_NOT_EMPTY: + return NS_ERROR_FILE_DIR_NOT_EMPTY; + case G_IO_ERROR_FILENAME_TOO_LONG: + return NS_ERROR_FILE_NAME_TOO_LONG; + case G_IO_ERROR_INVALID_FILENAME: + return NS_ERROR_FILE_INVALID_PATH; + case G_IO_ERROR_TIMED_OUT: + return NS_ERROR_NET_TIMEOUT; // shows error + case G_IO_ERROR_WOULD_BLOCK: + return NS_BASE_STREAM_WOULD_BLOCK; + case G_IO_ERROR_FAILED_HANDLED: + return NS_ERROR_ABORT; // Cancel on login dialog + + /* unhandled: + G_IO_ERROR_NOT_REGULAR_FILE, + G_IO_ERROR_NOT_SYMBOLIC_LINK, + G_IO_ERROR_NOT_MOUNTABLE_FILE, + G_IO_ERROR_TOO_MANY_LINKS, + G_IO_ERROR_ALREADY_MOUNTED, + G_IO_ERROR_CANT_CREATE_BACKUP, + G_IO_ERROR_WRONG_ETAG, + G_IO_ERROR_WOULD_RECURSE, + G_IO_ERROR_BUSY, + G_IO_ERROR_WOULD_MERGE, + G_IO_ERROR_TOO_MANY_OPEN_FILES + */ + // Make GCC happy + default: + return NS_ERROR_FAILURE; + } +} + +static nsresult MapGIOResult(GError* result) { + if (!result) return NS_OK; + return MapGIOResult(result->code); +} +/** Return values for mount operation. + * These enums are used as mount operation return values. + */ +typedef enum { + MOUNT_OPERATION_IN_PROGRESS, /** \enum operation in progress */ + MOUNT_OPERATION_SUCCESS, /** \enum operation successful */ + MOUNT_OPERATION_FAILED /** \enum operation not successful */ +} MountOperationResult; +//----------------------------------------------------------------------------- +/** + * Sort function compares according to file type (directory/file) + * and alphabethical order + * @param a pointer to GFileInfo object to compare + * @param b pointer to GFileInfo object to compare + * @return -1 when first object should be before the second, 0 when equal, + * +1 when second object should be before the first + */ +static gint FileInfoComparator(gconstpointer a, gconstpointer b) { + GFileInfo* ia = (GFileInfo*)a; + GFileInfo* ib = (GFileInfo*)b; + if (g_file_info_get_file_type(ia) == G_FILE_TYPE_DIRECTORY && + g_file_info_get_file_type(ib) != G_FILE_TYPE_DIRECTORY) + return -1; + if (g_file_info_get_file_type(ib) == G_FILE_TYPE_DIRECTORY && + g_file_info_get_file_type(ia) != G_FILE_TYPE_DIRECTORY) + return 1; + + return strcasecmp(g_file_info_get_name(ia), g_file_info_get_name(ib)); +} + +/* Declaration of mount callback functions */ +static void mount_enclosing_volume_finished(GObject* source_object, + GAsyncResult* res, + gpointer user_data); +static void mount_operation_ask_password( + GMountOperation* mount_op, const char* message, const char* default_user, + const char* default_domain, GAskPasswordFlags flags, gpointer user_data); +//----------------------------------------------------------------------------- + +class nsGIOInputStream final : public nsIInputStream { + ~nsGIOInputStream() { Close(); } + + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIINPUTSTREAM + + explicit nsGIOInputStream(const nsCString& uriSpec) + : mSpec(uriSpec), + mChannel(nullptr), + mHandle(nullptr), + mStream(nullptr), + mBytesRemaining(UINT64_MAX), + mStatus(NS_OK), + mDirList(nullptr), + mDirListPtr(nullptr), + mDirBufCursor(0), + mDirOpen(false), + mMonitorMountInProgress("GIOInputStream::MountFinished") {} + + void SetChannel(nsIChannel* channel) { + // We need to hold an owning reference to our channel. This is done + // so we can access the channel's notification callbacks to acquire + // a reference to a nsIAuthPrompt if we need to handle an interactive + // mount operation. + // + // However, the channel can only be accessed on the main thread, so + // we have to be very careful with ownership. Moreover, it doesn't + // support threadsafe addref/release, so proxying is the answer. + // + // Also, it's important to note that this likely creates a reference + // cycle since the channel likely owns this stream. This reference + // cycle is broken in our Close method. + + NS_ADDREF(mChannel = channel); + } + void SetMountResult(MountOperationResult result, gint error_code); + + private: + nsresult DoOpen(); + nsresult DoRead(char* aBuf, uint32_t aCount, uint32_t* aCountRead); + nsresult SetContentTypeOfChannel(const char* contentType); + nsresult MountVolume(); + nsresult DoOpenDirectory(); + nsresult DoOpenFile(GFileInfo* info); + nsCString mSpec; + nsIChannel* mChannel; // manually refcounted + GFile* mHandle; + GFileInputStream* mStream; + uint64_t mBytesRemaining; + nsresult mStatus; + GList* mDirList; + GList* mDirListPtr; + nsCString mDirBuf; + uint32_t mDirBufCursor; + bool mDirOpen; + MountOperationResult mMountRes; + mozilla::Monitor mMonitorMountInProgress; + gint mMountErrorCode; +}; +/** + * Set result of mount operation and notify monitor waiting for results. + * This method is called in main thread as long as it is used only + * in mount_enclosing_volume_finished function. + * @param result Result of mount operation + */ +void nsGIOInputStream::SetMountResult(MountOperationResult result, + gint error_code) { + mozilla::MonitorAutoLock mon(mMonitorMountInProgress); + mMountRes = result; + mMountErrorCode = error_code; + mon.Notify(); +} + +/** + * Start mount operation and wait in loop until it is finished. This method is + * called from thread which is trying to read from location. + */ +nsresult nsGIOInputStream::MountVolume() { + GMountOperation* mount_op = g_mount_operation_new(); + g_signal_connect(mount_op, "ask-password", + G_CALLBACK(mount_operation_ask_password), mChannel); + mMountRes = MOUNT_OPERATION_IN_PROGRESS; + /* g_file_mount_enclosing_volume uses a dbus request to mount the volume. + Callback mount_enclosing_volume_finished is called in main thread + (not this thread on which this method is called). */ + g_file_mount_enclosing_volume(mHandle, G_MOUNT_MOUNT_NONE, mount_op, nullptr, + mount_enclosing_volume_finished, this); + mozilla::MonitorAutoLock mon(mMonitorMountInProgress); + /* Waiting for finish of mount operation thread */ + while (mMountRes == MOUNT_OPERATION_IN_PROGRESS) mon.Wait(); + + g_object_unref(mount_op); + + if (mMountRes == MOUNT_OPERATION_FAILED) { + return MapGIOResult(mMountErrorCode); + } + return NS_OK; +} + +/** + * Create list of infos about objects in opened directory + * Return: NS_OK when list obtained, otherwise error code according + * to failed operation. + */ +nsresult nsGIOInputStream::DoOpenDirectory() { + GError* error = nullptr; + + GFileEnumerator* f_enum = g_file_enumerate_children( + mHandle, "standard::*,time::*", G_FILE_QUERY_INFO_NONE, nullptr, &error); + if (!f_enum) { + nsresult rv = MapGIOResult(error); + g_warning("Cannot read from directory: %s", error->message); + g_error_free(error); + return rv; + } + // fill list of file infos + GFileInfo* info = g_file_enumerator_next_file(f_enum, nullptr, &error); + while (info) { + mDirList = g_list_append(mDirList, info); + info = g_file_enumerator_next_file(f_enum, nullptr, &error); + } + g_object_unref(f_enum); + if (error) { + g_warning("Error reading directory content: %s", error->message); + nsresult rv = MapGIOResult(error); + g_error_free(error); + return rv; + } + mDirOpen = true; + + // Sort list of file infos by using FileInfoComparator function + mDirList = g_list_sort(mDirList, FileInfoComparator); + mDirListPtr = mDirList; + + // Write base URL (make sure it ends with a '/') + mDirBuf.AppendLiteral("300: "); + mDirBuf.Append(mSpec); + if (mSpec.get()[mSpec.Length() - 1] != '/') mDirBuf.Append('/'); + mDirBuf.Append('\n'); + + // Write column names + mDirBuf.AppendLiteral( + "200: filename content-length last-modified file-type\n"); + + // Write charset (assume UTF-8) + // XXX is this correct? + mDirBuf.AppendLiteral("301: UTF-8\n"); + SetContentTypeOfChannel(APPLICATION_HTTP_INDEX_FORMAT); + return NS_OK; +} + +/** + * Create file stream and set mime type for channel + * @param info file info used to determine mime type + * @return NS_OK when file stream created successfuly, error code otherwise + */ +nsresult nsGIOInputStream::DoOpenFile(GFileInfo* info) { + GError* error = nullptr; + + mStream = g_file_read(mHandle, nullptr, &error); + if (!mStream) { + nsresult rv = MapGIOResult(error); + g_warning("Cannot read from file: %s", error->message); + g_error_free(error); + return rv; + } + + const char* content_type = g_file_info_get_content_type(info); + if (content_type) { + char* mime_type = g_content_type_get_mime_type(content_type); + if (mime_type) { + if (strcmp(mime_type, APPLICATION_OCTET_STREAM) != 0) { + SetContentTypeOfChannel(mime_type); + } + g_free(mime_type); + } + } else { + g_warning("Missing content type."); + } + + mBytesRemaining = g_file_info_get_size(info); + // Update the content length attribute on the channel. We do this + // synchronously without proxying. This hack is not as bad as it looks! + mChannel->SetContentLength(mBytesRemaining); + + return NS_OK; +} + +/** + * Start file open operation, mount volume when needed and according to file + * type create file output stream or read directory content. + * @return NS_OK when file or directory opened successfully, error code + * otherwise + */ +nsresult nsGIOInputStream::DoOpen() { + nsresult rv; + GError* error = nullptr; + + NS_ASSERTION(mHandle == nullptr, "already open"); + + mHandle = g_file_new_for_uri(mSpec.get()); + + GFileInfo* info = g_file_query_info(mHandle, "standard::*", + G_FILE_QUERY_INFO_NONE, nullptr, &error); + + if (error) { + if (error->domain == G_IO_ERROR && error->code == G_IO_ERROR_NOT_MOUNTED) { + // location is not yet mounted, try to mount + g_error_free(error); + if (NS_IsMainThread()) return NS_ERROR_NOT_CONNECTED; + error = nullptr; + rv = MountVolume(); + if (rv != NS_OK) { + return rv; + } + // get info again + info = g_file_query_info(mHandle, "standard::*", G_FILE_QUERY_INFO_NONE, + nullptr, &error); + // second try to get file info from remote files after media mount + if (!info) { + g_warning("Unable to get file info: %s", error->message); + rv = MapGIOResult(error); + g_error_free(error); + return rv; + } + } else { + g_warning("Unable to get file info: %s", error->message); + rv = MapGIOResult(error); + g_error_free(error); + return rv; + } + } + // Get file type to handle directories and file differently + GFileType f_type = g_file_info_get_file_type(info); + if (f_type == G_FILE_TYPE_DIRECTORY) { + // directory + rv = DoOpenDirectory(); + } else if (f_type != G_FILE_TYPE_UNKNOWN) { + // file + rv = DoOpenFile(info); + } else { + g_warning("Unable to get file type."); + rv = NS_ERROR_FILE_NOT_FOUND; + } + if (info) g_object_unref(info); + return rv; +} + +/** + * Read content of file or create file list from directory + * @param aBuf read destination buffer + * @param aCount length of destination buffer + * @param aCountRead number of read characters + * @return NS_OK when read successfully, NS_BASE_STREAM_CLOSED when end of file, + * error code otherwise + */ +nsresult nsGIOInputStream::DoRead(char* aBuf, uint32_t aCount, + uint32_t* aCountRead) { + nsresult rv = NS_ERROR_NOT_AVAILABLE; + if (mStream) { + // file read + GError* error = nullptr; + uint32_t bytes_read = g_input_stream_read(G_INPUT_STREAM(mStream), aBuf, + aCount, nullptr, &error); + if (error) { + rv = MapGIOResult(error); + *aCountRead = 0; + g_warning("Cannot read from file: %s", error->message); + g_error_free(error); + return rv; + } + *aCountRead = bytes_read; + mBytesRemaining -= *aCountRead; + return NS_OK; + } + if (mDirOpen) { + // directory read + while (aCount && rv != NS_BASE_STREAM_CLOSED) { + // Copy data out of our buffer + uint32_t bufLen = mDirBuf.Length() - mDirBufCursor; + if (bufLen) { + uint32_t n = std::min(bufLen, aCount); + memcpy(aBuf, mDirBuf.get() + mDirBufCursor, n); + *aCountRead += n; + aBuf += n; + aCount -= n; + mDirBufCursor += n; + } + + if (!mDirListPtr) // Are we at the end of the directory list? + { + rv = NS_BASE_STREAM_CLOSED; + } else if (aCount) // Do we need more data? + { + GFileInfo* info = (GFileInfo*)mDirListPtr->data; + + // Prune '.' and '..' from directory listing. + const char* fname = g_file_info_get_name(info); + if (fname && fname[0] == '.' && + (fname[1] == '\0' || (fname[1] == '.' && fname[2] == '\0'))) { + mDirListPtr = mDirListPtr->next; + continue; + } + + mDirBuf.AssignLiteral("201: "); + + // The "filename" field + nsCString escName; + nsCOMPtr nu = do_GetService(NS_NETUTIL_CONTRACTID); + if (nu && fname) { + nu->EscapeString(nsDependentCString(fname), + nsINetUtil::ESCAPE_URL_PATH, escName); + + mDirBuf.Append(escName); + mDirBuf.Append(' '); + } + + // The "content-length" field + // XXX truncates size from 64-bit to 32-bit + mDirBuf.AppendInt(int32_t(g_file_info_get_size(info))); + mDirBuf.Append(' '); + + // The "last-modified" field + // + // NSPR promises: PRTime is compatible with time_t + // we just need to convert from seconds to microseconds + GTimeVal gtime; + g_file_info_get_modification_time(info, >ime); + + PRExplodedTime tm; + PRTime pt = ((PRTime)gtime.tv_sec) * 1000000; + PR_ExplodeTime(pt, PR_GMTParameters, &tm); + { + char buf[64]; + PR_FormatTimeUSEnglish(buf, sizeof(buf), + "%a,%%20%d%%20%b%%20%Y%%20%H:%M:%S%%20GMT ", + &tm); + mDirBuf.Append(buf); + } + + // The "file-type" field + switch (g_file_info_get_file_type(info)) { + case G_FILE_TYPE_REGULAR: + mDirBuf.AppendLiteral("FILE "); + break; + case G_FILE_TYPE_DIRECTORY: + mDirBuf.AppendLiteral("DIRECTORY "); + break; + case G_FILE_TYPE_SYMBOLIC_LINK: + mDirBuf.AppendLiteral("SYMBOLIC-LINK "); + break; + default: + break; + } + mDirBuf.Append('\n'); + + mDirBufCursor = 0; + mDirListPtr = mDirListPtr->next; + } + } + } + return rv; +} + +/** + * This class is used to implement SetContentTypeOfChannel. + */ +class nsGIOSetContentTypeEvent : public mozilla::Runnable { + public: + nsGIOSetContentTypeEvent(nsIChannel* channel, const char* contentType) + : mozilla::Runnable("nsGIOSetContentTypeEvent"), + mChannel(channel), + mContentType(contentType) { + // stash channel reference in mChannel. no AddRef here! see note + // in SetContentTypeOfchannel. + } + + NS_IMETHOD Run() override { + mChannel->SetContentType(mContentType); + return NS_OK; + } + + private: + nsIChannel* mChannel; + nsCString mContentType; +}; + +nsresult nsGIOInputStream::SetContentTypeOfChannel(const char* contentType) { + // We need to proxy this call over to the main thread. We post an + // asynchronous event in this case so that we don't delay reading data, and + // we know that this is safe to do since the channel's reference will be + // released asynchronously as well. We trust the ordering of the main + // thread's event queue to protect us against memory corruption. + + nsresult rv; + nsCOMPtr ev = + new nsGIOSetContentTypeEvent(mChannel, contentType); + if (!ev) { + rv = NS_ERROR_OUT_OF_MEMORY; + } else { + rv = NS_DispatchToMainThread(ev); + } + return rv; +} + +NS_IMPL_ISUPPORTS(nsGIOInputStream, nsIInputStream) + +/** + * Free all used memory and close stream. + */ +NS_IMETHODIMP +nsGIOInputStream::Close() { + if (mStream) { + g_object_unref(mStream); + mStream = nullptr; + } + + if (mHandle) { + g_object_unref(mHandle); + mHandle = nullptr; + } + + if (mDirList) { + // Destroy the list of GIOFileInfo objects... + g_list_foreach(mDirList, (GFunc)g_object_unref, nullptr); + g_list_free(mDirList); + mDirList = nullptr; + mDirListPtr = nullptr; + } + + if (mChannel) { + NS_ReleaseOnMainThread("nsGIOInputStream::mChannel", dont_AddRef(mChannel)); + + mChannel = nullptr; + } + + mSpec.Truncate(); // free memory + + // Prevent future reads from re-opening the handle. + if (NS_SUCCEEDED(mStatus)) mStatus = NS_BASE_STREAM_CLOSED; + + return NS_OK; +} + +/** + * Return number of remaining bytes available on input + * @param aResult remaining bytes + */ +NS_IMETHODIMP +nsGIOInputStream::Available(uint64_t* aResult) { + if (NS_FAILED(mStatus)) return mStatus; + + *aResult = mBytesRemaining; + + return NS_OK; +} + +/** + * Trying to read from stream. When location is not available it tries to mount + * it. + * @param aBuf buffer to put read data + * @param aCount length of aBuf + * @param aCountRead number of bytes actually read + */ +NS_IMETHODIMP +nsGIOInputStream::Read(char* aBuf, uint32_t aCount, uint32_t* aCountRead) { + *aCountRead = 0; + // Check if file is already opened, otherwise open it + if (!mStream && !mDirOpen && mStatus == NS_OK) { + mStatus = DoOpen(); + if (NS_FAILED(mStatus)) { + return mStatus; + } + } + + mStatus = DoRead(aBuf, aCount, aCountRead); + // Check if all data has been read + if (mStatus == NS_BASE_STREAM_CLOSED) return NS_OK; + + // Check whenever any error appears while reading + return mStatus; +} + +NS_IMETHODIMP +nsGIOInputStream::ReadSegments(nsWriteSegmentFun aWriter, void* aClosure, + uint32_t aCount, uint32_t* aResult) { + // There is no way to implement this using GnomeVFS, but fortunately + // that doesn't matter. Because we are a blocking input stream, Necko + // isn't going to call our ReadSegments method. + MOZ_ASSERT_UNREACHABLE("nsGIOInputStream::ReadSegments"); + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsGIOInputStream::IsNonBlocking(bool* aResult) { + *aResult = false; + return NS_OK; +} + +//----------------------------------------------------------------------------- + +/** + * Called when finishing mount operation. Result of operation is set in + * nsGIOInputStream. This function is called in main thread as an async request + * typically from dbus. + * @param source_object GFile object which requested the mount + * @param res result object + * @param user_data pointer to nsGIOInputStream + */ +static void mount_enclosing_volume_finished(GObject* source_object, + GAsyncResult* res, + gpointer user_data) { + GError* error = nullptr; + + nsGIOInputStream* istream = static_cast(user_data); + + g_file_mount_enclosing_volume_finish(G_FILE(source_object), res, &error); + + if (error) { + g_warning("Mount failed: %s %d", error->message, error->code); + istream->SetMountResult(MOUNT_OPERATION_FAILED, error->code); + g_error_free(error); + } else { + istream->SetMountResult(MOUNT_OPERATION_SUCCESS, 0); + } +} + +/** + * This function is called when username or password are requested from user. + * This function is called in main thread as async request from dbus. + * @param mount_op mount operation + * @param message message to show to user + * @param default_user preffered user + * @param default_domain domain name + * @param flags what type of information is required + * @param user_data nsIChannel + */ +static void mount_operation_ask_password( + GMountOperation* mount_op, const char* message, const char* default_user, + const char* default_domain, GAskPasswordFlags flags, gpointer user_data) { + nsIChannel* channel = (nsIChannel*)user_data; + if (!channel) { + g_mount_operation_reply(mount_op, G_MOUNT_OPERATION_ABORTED); + return; + } + // We can't handle request for domain + if (flags & G_ASK_PASSWORD_NEED_DOMAIN) { + g_mount_operation_reply(mount_op, G_MOUNT_OPERATION_ABORTED); + return; + } + + nsCOMPtr prompt; + NS_QueryNotificationCallbacks(channel, prompt); + + // If no auth prompt, then give up. We could failover to using the + // WindowWatcher service, but that might defeat a consumer's purposeful + // attempt to disable authentication (for whatever reason). + if (!prompt) { + g_mount_operation_reply(mount_op, G_MOUNT_OPERATION_ABORTED); + return; + } + // Parse out the host and port... + nsCOMPtr uri; + channel->GetURI(getter_AddRefs(uri)); + if (!uri) { + g_mount_operation_reply(mount_op, G_MOUNT_OPERATION_ABORTED); + return; + } + + nsAutoCString scheme, hostPort; + uri->GetScheme(scheme); + uri->GetHostPort(hostPort); + + // It doesn't make sense for either of these strings to be empty. What kind + // of funky URI is this? + if (scheme.IsEmpty() || hostPort.IsEmpty()) { + g_mount_operation_reply(mount_op, G_MOUNT_OPERATION_ABORTED); + return; + } + // Construct the single signon key. Altering the value of this key will + // cause people's remembered passwords to be forgotten. Think carefully + // before changing the way this key is constructed. + nsAutoString key, realm; + + NS_ConvertUTF8toUTF16 dispHost(scheme); + dispHost.AppendLiteral("://"); + dispHost.Append(NS_ConvertUTF8toUTF16(hostPort)); + + key = dispHost; + if (*default_domain != '\0') { + // We assume the realm string is ASCII. That might be a bogus assumption, + // but we have no idea what encoding GnomeVFS is using, so for now we'll + // limit ourselves to ISO-Latin-1. XXX What is a better solution? + realm.Append('"'); + realm.Append(NS_ConvertASCIItoUTF16(default_domain)); + realm.Append('"'); + key.Append(' '); + key.Append(realm); + } + // Construct the message string... + // + // We use Necko's string bundle here. This code really should be encapsulated + // behind some Necko API, after all this code is based closely on the code in + // nsHttpChannel.cpp. + nsCOMPtr bundleSvc = + do_GetService(NS_STRINGBUNDLE_CONTRACTID); + if (!bundleSvc) { + g_mount_operation_reply(mount_op, G_MOUNT_OPERATION_ABORTED); + return; + } + nsCOMPtr bundle; + bundleSvc->CreateBundle("chrome://global/locale/commonDialogs.properties", + getter_AddRefs(bundle)); + if (!bundle) { + g_mount_operation_reply(mount_op, G_MOUNT_OPERATION_ABORTED); + return; + } + nsAutoString nsmessage; + + if (flags & G_ASK_PASSWORD_NEED_PASSWORD) { + if (flags & G_ASK_PASSWORD_NEED_USERNAME) { + if (!realm.IsEmpty()) { + AutoTArray strings = {realm, dispHost}; + bundle->FormatStringFromName("EnterLoginForRealm3", strings, nsmessage); + } else { + AutoTArray strings = {dispHost}; + bundle->FormatStringFromName("EnterUserPasswordFor2", strings, + nsmessage); + } + } else { + NS_ConvertUTF8toUTF16 userName(default_user); + AutoTArray strings = {userName, dispHost}; + bundle->FormatStringFromName("EnterPasswordFor", strings, nsmessage); + } + } else { + g_warning("Unknown mount operation request (flags: %x)", flags); + } + + if (nsmessage.IsEmpty()) { + g_mount_operation_reply(mount_op, G_MOUNT_OPERATION_ABORTED); + return; + } + // Prompt the user... + nsresult rv; + bool retval = false; + char16_t *user = nullptr, *pass = nullptr; + if (default_user) { + // user will be freed by PromptUsernameAndPassword + user = ToNewUnicode(NS_ConvertUTF8toUTF16(default_user)); + } + if (flags & G_ASK_PASSWORD_NEED_USERNAME) { + rv = prompt->PromptUsernameAndPassword( + nullptr, nsmessage.get(), key.get(), + nsIAuthPrompt::SAVE_PASSWORD_PERMANENTLY, &user, &pass, &retval); + } else { + rv = prompt->PromptPassword(nullptr, nsmessage.get(), key.get(), + nsIAuthPrompt::SAVE_PASSWORD_PERMANENTLY, &pass, + &retval); + } + if (NS_FAILED(rv) || !retval) { // was || user == '\0' || pass == '\0' + g_mount_operation_reply(mount_op, G_MOUNT_OPERATION_ABORTED); + free(user); + free(pass); + return; + } + /* GIO should accept UTF8 */ + g_mount_operation_set_username(mount_op, NS_ConvertUTF16toUTF8(user).get()); + g_mount_operation_set_password(mount_op, NS_ConvertUTF16toUTF8(pass).get()); + free(user); + free(pass); + g_mount_operation_reply(mount_op, G_MOUNT_OPERATION_HANDLED); +} + +//----------------------------------------------------------------------------- + +mozilla::StaticRefPtr nsGIOProtocolHandler::sSingleton; + +already_AddRefed nsGIOProtocolHandler::GetSingleton() { + if (!sSingleton) { + sSingleton = new nsGIOProtocolHandler(); + sSingleton->Init(); + ClearOnShutdown(&sSingleton); + } + return do_AddRef(sSingleton); +} + +NS_IMPL_ISUPPORTS(nsGIOProtocolHandler, nsIProtocolHandler, nsIObserver) + +nsresult nsGIOProtocolHandler::Init() { + nsCOMPtr prefs = do_GetService(NS_PREFSERVICE_CONTRACTID); + if (prefs) { + InitSupportedProtocolsPref(prefs); + prefs->AddObserver(MOZ_GIO_SUPPORTED_PROTOCOLS, this, false); + } + + return NS_OK; +} + +void nsGIOProtocolHandler::InitSupportedProtocolsPref(nsIPrefBranch* prefs) { + // Get user preferences to determine which protocol is supported. + // Gvfs/GIO has a set of supported protocols like obex, network, archive, + // computer, dav, cdda, gphoto2, trash, etc. Some of these seems to be + // irrelevant to process by browser. By default accept only smb and sftp + // protocols so far. + nsresult rv = + prefs->GetCharPref(MOZ_GIO_SUPPORTED_PROTOCOLS, mSupportedProtocols); + if (NS_SUCCEEDED(rv)) { + mSupportedProtocols.StripWhitespace(); + ToLowerCase(mSupportedProtocols); + } else { + mSupportedProtocols.AssignLiteral( +#ifdef MOZ_PROXY_BYPASS_PROTECTION + "" // use none +#else + "smb:,sftp:" // use defaults +#endif + ); + } + LOG(("gio: supported protocols \"%s\"\n", mSupportedProtocols.get())); +} + +bool nsGIOProtocolHandler::IsSupportedProtocol(const nsCString& aSpec) { + const char* specString = aSpec.get(); + const char* colon = strchr(specString, ':'); + if (!colon) return false; + + uint32_t length = colon - specString + 1; + + // + ':' + nsCString scheme(specString, length); + + char* found = PL_strcasestr(mSupportedProtocols.get(), scheme.get()); + if (!found) return false; + + if (found[length] != ',' && found[length] != '\0') return false; + + return true; +} + +NS_IMETHODIMP +nsGIOProtocolHandler::GetScheme(nsACString& aScheme) { + aScheme.AssignLiteral(MOZ_GIO_SCHEME); + return NS_OK; +} + +NS_IMETHODIMP +nsGIOProtocolHandler::GetDefaultPort(int32_t* aDefaultPort) { + *aDefaultPort = -1; + return NS_OK; +} + +NS_IMETHODIMP +nsGIOProtocolHandler::GetProtocolFlags(uint32_t* aProtocolFlags) { + // Is URI_STD true of all GnomeVFS URI types? + *aProtocolFlags = URI_STD | URI_DANGEROUS_TO_LOAD; + return NS_OK; +} + +static bool IsValidGIOScheme(const nsACString& aScheme) { + // Verify that GIO supports this URI scheme. + GVfs* gvfs = g_vfs_get_default(); + + if (!gvfs) { + g_warning("Cannot get GVfs object."); + return false; + } + + const gchar* const* uri_schemes = g_vfs_get_supported_uri_schemes(gvfs); + + while (*uri_schemes != nullptr) { + // While flatSpec ends with ':' the uri_scheme does not. Therefore do not + // compare last character. + if (aScheme.Equals(*uri_schemes)) { + return true; + } + uri_schemes++; + } + + return false; +} + +NS_IMETHODIMP +nsGIOProtocolHandler::NewChannel(nsIURI* aURI, nsILoadInfo* aLoadInfo, + nsIChannel** aResult) { + NS_ENSURE_ARG_POINTER(aURI); + nsresult rv; + + nsAutoCString spec; + rv = aURI->GetSpec(spec); + if (NS_FAILED(rv)) return rv; + + if (!IsSupportedProtocol(spec)) { + return NS_ERROR_UNKNOWN_PROTOCOL; + } + + nsAutoCString scheme; + rv = aURI->GetScheme(scheme); + if (NS_FAILED(rv)) { + return rv; + } + + if (!IsValidGIOScheme(scheme)) { + return NS_ERROR_UNKNOWN_PROTOCOL; + } + + RefPtr stream = new nsGIOInputStream(spec); + if (!stream) { + return NS_ERROR_OUT_OF_MEMORY; + } + + RefPtr tmpStream = stream; + rv = NS_NewInputStreamChannelInternal(aResult, aURI, tmpStream.forget(), + nsLiteralCString(UNKNOWN_CONTENT_TYPE), + ""_ns, // aContentCharset + aLoadInfo); + if (NS_SUCCEEDED(rv)) { + stream->SetChannel(*aResult); + } + return rv; +} + +NS_IMETHODIMP +nsGIOProtocolHandler::AllowPort(int32_t aPort, const char* aScheme, + bool* aResult) { + // Don't override anything. + *aResult = false; + return NS_OK; +} + +NS_IMETHODIMP +nsGIOProtocolHandler::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) { + if (strcmp(aTopic, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID) == 0) { + nsCOMPtr prefs = do_QueryInterface(aSubject); + InitSupportedProtocolsPref(prefs); + } + return NS_OK; +} diff --git a/netwerk/protocol/gio/nsGIOProtocolHandler.h b/netwerk/protocol/gio/nsGIOProtocolHandler.h new file mode 100644 index 0000000000..4c8d936402 --- /dev/null +++ b/netwerk/protocol/gio/nsGIOProtocolHandler.h @@ -0,0 +1,33 @@ +/* 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/. */ + +#ifndef nsGIOProtocolHandler_h___ +#define nsGIOProtocolHandler_h___ + +#include "nsIProtocolHandler.h" +#include "nsIObserver.h" +#include "nsIPrefBranch.h" +#include "nsStringFwd.h" + +class nsGIOProtocolHandler final : public nsIProtocolHandler, + public nsIObserver { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIPROTOCOLHANDLER + NS_DECL_NSIOBSERVER + + static already_AddRefed GetSingleton(); + bool IsSupportedProtocol(const nsCString& spec); + + private: + nsresult Init(); + ~nsGIOProtocolHandler() = default; + + void InitSupportedProtocolsPref(nsIPrefBranch* prefs); + + static mozilla::StaticRefPtr sSingleton; + nsCString mSupportedProtocols; +}; + +#endif // nsGIOProtocolHandler_h___ diff --git a/netwerk/protocol/http/ASpdySession.cpp b/netwerk/protocol/http/ASpdySession.cpp new file mode 100644 index 0000000000..cb91af13fb --- /dev/null +++ b/netwerk/protocol/http/ASpdySession.cpp @@ -0,0 +1,113 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et 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/. */ + +// HttpLog.h should generally be included first +#include "HttpLog.h" + +/* + Currently supported is h2 +*/ + +#include "nsHttp.h" +#include "nsHttpHandler.h" + +#include "ASpdySession.h" +#include "PSpdyPush.h" +#include "Http2Push.h" +#include "Http2Session.h" + +#include "mozilla/Telemetry.h" + +namespace mozilla { +namespace net { + +ASpdySession* ASpdySession::NewSpdySession(net::SpdyVersion version, + nsISocketTransport* aTransport, + bool attemptingEarlyData) { + // This is a necko only interface, so we can enforce version + // requests as a precondition + MOZ_ASSERT(version == SpdyVersion::HTTP_2, "Unsupported spdy version"); + + // Don't do a runtime check of IsSpdyV?Enabled() here because pref value + // may have changed since starting negotiation. The selected protocol comes + // from a list provided in the SERVER HELLO filtered by our acceptable + // versions, so there is no risk of the server ignoring our prefs. + + return Http2Session::CreateSession(aTransport, version, attemptingEarlyData); +} + +SpdyInformation::SpdyInformation() { + // highest index of enabled protocols is the + // most preferred for ALPN negotiaton + Version[0] = SpdyVersion::HTTP_2; + VersionString[0] = "h2"_ns; + ALPNCallbacks[0] = Http2Session::ALPNCallback; +} + +bool SpdyInformation::ProtocolEnabled(uint32_t index) const { + MOZ_ASSERT(index < kCount, "index out of range"); + + return gHttpHandler->IsHttp2Enabled(); +} + +nsresult SpdyInformation::GetNPNIndex(const nsACString& npnString, + uint32_t* result) const { + if (npnString.IsEmpty()) return NS_ERROR_FAILURE; + + for (uint32_t index = 0; index < kCount; ++index) { + if (npnString.Equals(VersionString[index])) { + *result = index; + return NS_OK; + } + } + + return NS_ERROR_FAILURE; +} + +////////////////////////////////////////// +// SpdyPushCache +////////////////////////////////////////// + +SpdyPushCache::~SpdyPushCache() { mHashHttp2.Clear(); } + +bool SpdyPushCache::RegisterPushedStreamHttp2(const nsCString& key, + Http2PushedStream* stream) { + LOG3(("SpdyPushCache::RegisterPushedStreamHttp2 %s 0x%X\n", key.get(), + stream->StreamID())); + if (mHashHttp2.Get(key)) { + LOG3(("SpdyPushCache::RegisterPushedStreamHttp2 %s 0x%X duplicate key\n", + key.get(), stream->StreamID())); + return false; + } + mHashHttp2.Put(key, stream); + return true; +} + +Http2PushedStream* SpdyPushCache::RemovePushedStreamHttp2( + const nsCString& key) { + Http2PushedStream* rv = mHashHttp2.Get(key); + LOG3(("SpdyPushCache::RemovePushedStreamHttp2 %s 0x%X\n", key.get(), + rv ? rv->StreamID() : 0)); + if (rv) mHashHttp2.Remove(key); + return rv; +} + +Http2PushedStream* SpdyPushCache::RemovePushedStreamHttp2ByID( + const nsCString& key, const uint32_t& streamID) { + Http2PushedStream* rv = mHashHttp2.Get(key); + LOG3(("SpdyPushCache::RemovePushedStreamHttp2ByID %s 0x%X 0x%X", key.get(), + rv ? rv->StreamID() : 0, streamID)); + if (rv && streamID == rv->StreamID()) { + mHashHttp2.Remove(key); + } else { + // Ensure we overwrite our rv with null in case the stream IDs don't match + rv = nullptr; + } + return rv; +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/ASpdySession.h b/netwerk/protocol/http/ASpdySession.h new file mode 100644 index 0000000000..ab2dbc722b --- /dev/null +++ b/netwerk/protocol/http/ASpdySession.h @@ -0,0 +1,126 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ + +/* 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/. */ + +#ifndef mozilla_net_ASpdySession_h +#define mozilla_net_ASpdySession_h + +#include "nsAHttpTransaction.h" +#include "prinrval.h" +#include "nsString.h" + +class nsISocketTransport; + +namespace mozilla { +namespace net { + +class ASpdySession : public nsAHttpTransaction { + public: + ASpdySession() = default; + virtual ~ASpdySession() = default; + + [[nodiscard]] virtual bool AddStream(nsAHttpTransaction*, int32_t, bool, bool, + nsIInterfaceRequestor*) = 0; + virtual bool CanReuse() = 0; + virtual bool RoomForMoreStreams() = 0; + virtual PRIntervalTime IdleTime() = 0; + virtual uint32_t ReadTimeoutTick(PRIntervalTime now) = 0; + virtual void DontReuse() = 0; + virtual enum SpdyVersion SpdyVersion() = 0; + + static ASpdySession* NewSpdySession(net::SpdyVersion version, + nsISocketTransport*, bool); + + virtual bool TestJoinConnection(const nsACString& hostname, int32_t port) = 0; + virtual bool JoinConnection(const nsACString& hostname, int32_t port) = 0; + + // MaybeReTunnel() is called by the connection manager when it cannot + // dispatch a tunneled transaction. That might be because the tunnels it + // expects to see are dead (and we may or may not be able to make more), + // or it might just need to wait longer for one of them to become free. + // + // return true if the session takes back ownership of the transaction from + // the connection manager. + virtual bool MaybeReTunnel(nsAHttpTransaction*) = 0; + + virtual void PrintDiagnostics(nsCString& log) = 0; + + bool ResponseTimeoutEnabled() const final { return true; } + + virtual void SendPing() = 0; + + const static uint32_t kSendingChunkSize = 4095; + const static uint32_t kTCPSendBufferSize = 131072; + + // This is roughly the amount of data a suspended channel will have to + // buffer before h2 flow control kicks in. + const static uint32_t kInitialRwin = 12 * 1024 * 1024; // 12MB + + const static uint32_t kDefaultMaxConcurrent = 100; + + // soft errors are errors that terminate a stream without terminating the + // connection. In general non-network errors are stream errors as well + // as network specific items like cancels. + static bool SoftStreamError(nsresult code) { + if (NS_SUCCEEDED(code) || code == NS_BASE_STREAM_WOULD_BLOCK) { + return false; + } + + // this could go either way, but because there are network instances of + // it being a hard error we should consider it hard. + if (code == NS_ERROR_FAILURE || code == NS_ERROR_OUT_OF_MEMORY) { + return false; + } + + if (NS_ERROR_GET_MODULE(code) != NS_ERROR_MODULE_NETWORK) { + return true; + } + + // these are network specific soft errors + return (code == NS_BASE_STREAM_CLOSED || code == NS_BINDING_FAILED || + code == NS_BINDING_ABORTED || code == NS_BINDING_REDIRECTED || + code == NS_ERROR_INVALID_CONTENT_ENCODING || + code == NS_BINDING_RETARGETED || + code == NS_ERROR_CORRUPTED_CONTENT); + } + + virtual void SetCleanShutdown(bool) = 0; + virtual bool CanAcceptWebsocket() = 0; +}; + +typedef bool (*ALPNCallback)(nsISupports*); // nsISSLSocketControl is typical + +// this is essentially a single instantiation as a member of nsHttpHandler. +// It could be all static except using static ctors of XPCOM objects is a +// bad idea. +class SpdyInformation { + public: + SpdyInformation(); + ~SpdyInformation() = default; + + static const uint32_t kCount = 1; + + // determine the index (0..kCount-1) of the spdy information that + // correlates to the npn string. NS_FAILED() if no match is found. + [[nodiscard]] nsresult GetNPNIndex(const nsACString& npnString, + uint32_t* result) const; + + // determine if a version of the protocol is enabled for index < kCount + bool ProtocolEnabled(uint32_t index) const; + + SpdyVersion Version[kCount]; // telemetry enum e.g. SPDY_VERSION_31 + nsCString VersionString[kCount]; // npn string e.g. "spdy/3.1" + + // the ALPNCallback function allows the protocol stack to decide whether or + // not to offer a particular protocol based on the known TLS information + // that we will offer in the client hello (such as version). There has + // not been a Server Hello received yet, so not much else can be considered. + ALPNCallback ALPNCallbacks[kCount]; +}; + +} // namespace net +} // namespace mozilla + +#endif // mozilla_net_ASpdySession_h diff --git a/netwerk/protocol/http/AltDataOutputStreamChild.cpp b/netwerk/protocol/http/AltDataOutputStreamChild.cpp new file mode 100644 index 0000000000..1c52cf74aa --- /dev/null +++ b/netwerk/protocol/http/AltDataOutputStreamChild.cpp @@ -0,0 +1,196 @@ +/* -*- 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/net/AltDataOutputStreamChild.h" +#include "mozilla/Unused.h" +#include "nsIInputStream.h" +#include "nsStreamUtils.h" + +namespace mozilla { +namespace net { + +NS_IMPL_ADDREF(AltDataOutputStreamChild) + +NS_IMETHODIMP_(MozExternalRefCountType) AltDataOutputStreamChild::Release() { + MOZ_ASSERT(0 != mRefCnt, "dup release"); + MOZ_ASSERT(NS_IsMainThread(), "Main thread only"); + --mRefCnt; + NS_LOG_RELEASE(this, mRefCnt, "AltDataOutputStreamChild"); + + if (mRefCnt == 1 && mIPCOpen) { + // The only reference left is the IPDL one. After the parent replies back + // with a DeleteSelf message, the child will call Send__delete__(this), + // decrementing the ref count and triggering the destructor. + SendDeleteSelf(); + return 1; + } + + if (mRefCnt == 0) { + mRefCnt = 1; /* stabilize */ + delete this; + return 0; + } + return mRefCnt; +} + +NS_INTERFACE_MAP_BEGIN(AltDataOutputStreamChild) + NS_INTERFACE_MAP_ENTRY(nsIAsyncOutputStream) + NS_INTERFACE_MAP_ENTRY(nsIOutputStream) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +AltDataOutputStreamChild::AltDataOutputStreamChild() + : mIPCOpen(false), mError(NS_OK), mCallbackFlags(0) { + MOZ_ASSERT(NS_IsMainThread(), "Main thread only"); +} + +void AltDataOutputStreamChild::AddIPDLReference() { + MOZ_ASSERT(!mIPCOpen, "Attempt to retain more than one IPDL reference"); + mIPCOpen = true; + AddRef(); +} + +void AltDataOutputStreamChild::ReleaseIPDLReference() { + MOZ_ASSERT(mIPCOpen, "Attempt to release nonexistent IPDL reference"); + mIPCOpen = false; + + if (mCallback) { + NotifyListener(); + } + + Release(); +} + +bool AltDataOutputStreamChild::WriteDataInChunks( + const nsDependentCSubstring& data) { + const uint32_t kChunkSize = 128 * 1024; + uint32_t next = std::min(data.Length(), kChunkSize); + for (uint32_t i = 0; i < data.Length(); + i = next, next = std::min(data.Length(), next + kChunkSize)) { + nsCString chunk(Substring(data, i, kChunkSize)); + if (mIPCOpen && !SendWriteData(chunk)) { + mIPCOpen = false; + return false; + } + } + return true; +} + +NS_IMETHODIMP +AltDataOutputStreamChild::Close() { return CloseWithStatus(NS_OK); } + +NS_IMETHODIMP +AltDataOutputStreamChild::Flush() { + if (!mIPCOpen) { + return NS_ERROR_NOT_AVAILABLE; + } + if (NS_FAILED(mError)) { + return mError; + } + + // This is a no-op + return NS_OK; +} + +NS_IMETHODIMP +AltDataOutputStreamChild::Write(const char* aBuf, uint32_t aCount, + uint32_t* _retval) { + if (!mIPCOpen) { + return NS_ERROR_NOT_AVAILABLE; + } + if (NS_FAILED(mError)) { + return mError; + } + if (WriteDataInChunks(nsDependentCSubstring(aBuf, aCount))) { + *_retval = aCount; + return NS_OK; + } + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +AltDataOutputStreamChild::WriteFrom(nsIInputStream* aFromStream, + uint32_t aCount, uint32_t* _retval) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +AltDataOutputStreamChild::WriteSegments(nsReadSegmentFun aReader, + void* aClosure, uint32_t aCount, + uint32_t* _retval) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +AltDataOutputStreamChild::IsNonBlocking(bool* _retval) { + *_retval = false; + return NS_OK; +} + +mozilla::ipc::IPCResult AltDataOutputStreamChild::RecvError( + const nsresult& err) { + mError = err; + return IPC_OK(); +} + +mozilla::ipc::IPCResult AltDataOutputStreamChild::RecvDeleteSelf() { + PAltDataOutputStreamChild::Send__delete__(this); + return IPC_OK(); +} + +// nsIAsyncOutputStream + +NS_IMETHODIMP +AltDataOutputStreamChild::CloseWithStatus(nsresult aStatus) { + if (!mIPCOpen) { + return NS_ERROR_NOT_AVAILABLE; + } + if (NS_FAILED(mError)) { + return mError; + } + Unused << SendClose(aStatus); + + return NS_OK; +} + +NS_IMETHODIMP +AltDataOutputStreamChild::AsyncWait(nsIOutputStreamCallback* aCallback, + uint32_t aFlags, uint32_t aRequestedCount, + nsIEventTarget* aEventTarget) { + mCallback = aCallback; + mCallbackFlags = aFlags; + mCallbackTarget = aEventTarget; + + if (!mCallback) { + return NS_OK; + } + + // The stream is blocking so it is writable at any time + if (!mIPCOpen || !(aFlags & WAIT_CLOSURE_ONLY)) { + NotifyListener(); + } + + return NS_OK; +} + +void AltDataOutputStreamChild::NotifyListener() { + MOZ_ASSERT(mCallback); + + if (!mCallbackTarget) { + mCallbackTarget = GetMainThreadEventTarget(); + } + + nsCOMPtr asyncCallback = + NS_NewOutputStreamReadyEvent(mCallback, mCallbackTarget); + + mCallback = nullptr; + mCallbackTarget = nullptr; + + asyncCallback->OnOutputStreamReady(this); +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/AltDataOutputStreamChild.h b/netwerk/protocol/http/AltDataOutputStreamChild.h new file mode 100644 index 0000000000..1a6d5143dd --- /dev/null +++ b/netwerk/protocol/http/AltDataOutputStreamChild.h @@ -0,0 +1,51 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et 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/. */ + +#ifndef mozilla_net_AltDataOutputStreamChild_h +#define mozilla_net_AltDataOutputStreamChild_h + +#include "mozilla/net/PAltDataOutputStreamChild.h" +#include "nsIAsyncOutputStream.h" + +namespace mozilla { +namespace net { + +class AltDataOutputStreamChild : public PAltDataOutputStreamChild, + public nsIAsyncOutputStream { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIASYNCOUTPUTSTREAM + NS_DECL_NSIOUTPUTSTREAM + explicit AltDataOutputStreamChild(); + + void AddIPDLReference(); + void ReleaseIPDLReference(); + // Saves an error code which will be reported to the writer on the next call. + virtual mozilla::ipc::IPCResult RecvError(const nsresult& err); + virtual mozilla::ipc::IPCResult RecvDeleteSelf(); + + private: + virtual ~AltDataOutputStreamChild() = default; + // Sends data to the parent process in 256k chunks. + bool WriteDataInChunks(const nsDependentCSubstring& data); + void NotifyListener(); + + bool mIPCOpen; + // If there was an error opening the output stream or writing to it on the + // parent side, this will be set to the error code. We check it before we + // write so we can report an error to the consumer. + nsresult mError; + + nsCOMPtr mCallback; + uint32_t mCallbackFlags; + nsCOMPtr mCallbackTarget; +}; + +} // namespace net +} // namespace mozilla + +#endif // mozilla_net_AltDataOutputStreamChild_h diff --git a/netwerk/protocol/http/AltDataOutputStreamParent.cpp b/netwerk/protocol/http/AltDataOutputStreamParent.cpp new file mode 100644 index 0000000000..e7e044e05e --- /dev/null +++ b/netwerk/protocol/http/AltDataOutputStreamParent.cpp @@ -0,0 +1,82 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et 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/net/AltDataOutputStreamParent.h" +#include "mozilla/Unused.h" + +namespace mozilla { +namespace net { + +NS_IMPL_ISUPPORTS0(AltDataOutputStreamParent) + +AltDataOutputStreamParent::AltDataOutputStreamParent(nsIOutputStream* aStream) + : mOutputStream(aStream), mStatus(NS_OK), mIPCOpen(true) { + MOZ_ASSERT(NS_IsMainThread(), "Main thread only"); +} + +AltDataOutputStreamParent::~AltDataOutputStreamParent() { + MOZ_ASSERT(NS_IsMainThread(), "Main thread only"); +} + +mozilla::ipc::IPCResult AltDataOutputStreamParent::RecvWriteData( + const nsCString& data) { + if (NS_FAILED(mStatus)) { + if (mIPCOpen) { + Unused << SendError(mStatus); + } + return IPC_OK(); + } + nsresult rv; + uint32_t n; + if (mOutputStream) { + rv = mOutputStream->Write(data.BeginReading(), data.Length(), &n); + MOZ_ASSERT(n == data.Length() || NS_FAILED(rv)); + if (NS_FAILED(rv) && mIPCOpen) { + Unused << SendError(rv); + } + } + return IPC_OK(); +} + +mozilla::ipc::IPCResult AltDataOutputStreamParent::RecvClose( + const nsresult& aStatus) { + if (NS_FAILED(mStatus)) { + if (mIPCOpen) { + Unused << SendError(mStatus); + } + return IPC_OK(); + } + + if (!mOutputStream) { + return IPC_OK(); + } + + nsCOMPtr asyncOutputStream = + do_QueryInterface(mOutputStream); + MOZ_ASSERT(asyncOutputStream); + + nsresult rv = asyncOutputStream->CloseWithStatus(aStatus); + if (NS_FAILED(rv) && mIPCOpen) { + Unused << SendError(rv); + } + + mOutputStream = nullptr; + return IPC_OK(); +} + +void AltDataOutputStreamParent::ActorDestroy(ActorDestroyReason aWhy) { + mIPCOpen = false; +} + +mozilla::ipc::IPCResult AltDataOutputStreamParent::RecvDeleteSelf() { + mIPCOpen = false; + Unused << SendDeleteSelf(); + return IPC_OK(); +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/AltDataOutputStreamParent.h b/netwerk/protocol/http/AltDataOutputStreamParent.h new file mode 100644 index 0000000000..a9642b9e45 --- /dev/null +++ b/netwerk/protocol/http/AltDataOutputStreamParent.h @@ -0,0 +1,52 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et 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/. */ + +#ifndef mozilla_net_AltDataOutputStreamParent_h +#define mozilla_net_AltDataOutputStreamParent_h + +#include "mozilla/net/PAltDataOutputStreamParent.h" +#include "nsIOutputStream.h" + +namespace mozilla { +namespace net { + +// Forwards data received from the content process to an output stream. +class AltDataOutputStreamParent : public PAltDataOutputStreamParent, + public nsISupports { + public: + NS_DECL_ISUPPORTS + + // Called from NeckoParent::AllocPAltDataOutputStreamParent which also opens + // the output stream. + // aStream may be null + explicit AltDataOutputStreamParent(nsIOutputStream* aStream); + + // Called when data is received from the content process. + // We proceed to write that data to the output stream. + mozilla::ipc::IPCResult RecvWriteData(const nsCString& data); + // Called when AltDataOutputStreamChild::Close() is + // Closes and nulls the output stream. + mozilla::ipc::IPCResult RecvClose(const nsresult& aStatus); + virtual void ActorDestroy(ActorDestroyReason aWhy) override; + + // Sets an error that will be reported to the content process. + void SetError(nsresult status) { mStatus = status; } + mozilla::ipc::IPCResult RecvDeleteSelf(); + + private: + virtual ~AltDataOutputStreamParent(); + nsCOMPtr mOutputStream; + // In case any error occurs mStatus will be != NS_OK, and this status code + // will be sent to the content process asynchronously. + nsresult mStatus; + bool mIPCOpen; +}; + +} // namespace net +} // namespace mozilla + +#endif // mozilla_net_AltDataOutputStreamParent_h diff --git a/netwerk/protocol/http/AltServiceChild.cpp b/netwerk/protocol/http/AltServiceChild.cpp new file mode 100644 index 0000000000..b42e1cb267 --- /dev/null +++ b/netwerk/protocol/http/AltServiceChild.cpp @@ -0,0 +1,114 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* vim:set ts=4 sw=4 sts=4 et cin: */ +/* 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/. */ + +// HttpLog.h should generally be included first +#include "HttpLog.h" + +#include "AltServiceChild.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/StaticPtr.h" +#include "mozilla/net/SocketProcessChild.h" +#include "nsHttpConnectionInfo.h" +#include "nsProxyInfo.h" +#include "nsThreadUtils.h" + +namespace mozilla { +namespace net { + +StaticRefPtr sAltServiceChild; + +AltServiceChild::AltServiceChild() { + LOG(("AltServiceChild ctor [%p]\n", this)); +} + +AltServiceChild::~AltServiceChild() { + LOG(("AltServiceChild dtor [%p]\n", this)); +} + +// static +bool AltServiceChild::EnsureAltServiceChild() { + MOZ_ASSERT(XRE_IsSocketProcess()); + MOZ_ASSERT(NS_IsMainThread()); + + if (sAltServiceChild) { + return true; + } + + SocketProcessChild* socketChild = SocketProcessChild::GetSingleton(); + if (!socketChild || socketChild->IsShuttingDown()) { + return false; + } + + sAltServiceChild = new AltServiceChild(); + ClearOnShutdown(&sAltServiceChild); + + if (!socketChild->SendPAltServiceConstructor(sAltServiceChild)) { + sAltServiceChild = nullptr; + return false; + } + + return true; +} + +// static +void AltServiceChild::ClearHostMapping(nsHttpConnectionInfo* aCi) { + LOG(("AltServiceChild::ClearHostMapping ci=%s", aCi->HashKey().get())); + MOZ_ASSERT(aCi); + + RefPtr ci = aCi->Clone(); + auto task = [ci{std::move(ci)}]() { + if (!EnsureAltServiceChild()) { + return; + } + + if (!ci->GetOrigin().IsEmpty() && sAltServiceChild->CanSend()) { + Unused << sAltServiceChild->SendClearHostMapping( + ci->GetOrigin(), ci->OriginPort(), ci->GetOriginAttributes(), + ci->GetTopWindowOrigin()); + } + }; + + if (!NS_IsMainThread()) { + MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(NS_NewRunnableFunction( + "net::AltServiceChild::ClearHostMapping", task))); + return; + } + + task(); +} + +// static +void AltServiceChild::ProcessHeader( + const nsCString& aBuf, const nsCString& aOriginScheme, + const nsCString& aOriginHost, int32_t aOriginPort, + const nsCString& aUsername, const nsCString& aTopWindowOrigin, + bool aPrivateBrowsing, bool aIsolated, nsIInterfaceRequestor* aCallbacks, + nsProxyInfo* aProxyInfo, uint32_t aCaps, + const OriginAttributes& aOriginAttributes) { + LOG(("AltServiceChild::ProcessHeader")); + MOZ_ASSERT(NS_IsMainThread()); + + if (!EnsureAltServiceChild()) { + return; + } + + if (!sAltServiceChild->CanSend()) { + return; + } + + nsTArray proxyInfoArray; + if (aProxyInfo) { + nsProxyInfo::SerializeProxyInfo(aProxyInfo, proxyInfoArray); + } + + Unused << sAltServiceChild->SendProcessHeader( + aBuf, aOriginScheme, aOriginHost, aOriginPort, aUsername, + aTopWindowOrigin, aPrivateBrowsing, aIsolated, proxyInfoArray, aCaps, + aOriginAttributes); +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/AltServiceChild.h b/netwerk/protocol/http/AltServiceChild.h new file mode 100644 index 0000000000..7bf4cead50 --- /dev/null +++ b/netwerk/protocol/http/AltServiceChild.h @@ -0,0 +1,43 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* 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/. */ + +#ifndef AltServiceChild_h__ +#define AltServiceChild_h__ + +#include "mozilla/net/PAltServiceChild.h" + +class nsIInterfaceRequestor; + +namespace mozilla { +namespace net { + +class nsHttpConnectionInfo; +class nsProxyInfo; + +class AltServiceChild final : public PAltServiceChild { + public: + NS_INLINE_DECL_REFCOUNTING(AltServiceChild, override) + + static bool EnsureAltServiceChild(); + static void ClearHostMapping(nsHttpConnectionInfo* aCi); + static void ProcessHeader(const nsCString& aBuf, + const nsCString& aOriginScheme, + const nsCString& aOriginHost, int32_t aOriginPort, + const nsCString& aUsername, + const nsCString& aTopWindowOrigin, + bool aPrivateBrowsing, bool aIsolated, + nsIInterfaceRequestor* aCallbacks, + nsProxyInfo* aProxyInfo, uint32_t aCaps, + const OriginAttributes& aOriginAttributes); + + private: + AltServiceChild(); + virtual ~AltServiceChild(); +}; + +} // namespace net +} // namespace mozilla + +#endif // AltServiceChild_h__ diff --git a/netwerk/protocol/http/AltServiceParent.cpp b/netwerk/protocol/http/AltServiceParent.cpp new file mode 100644 index 0000000000..c5a9efe332 --- /dev/null +++ b/netwerk/protocol/http/AltServiceParent.cpp @@ -0,0 +1,52 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* vim:set ts=4 sw=4 sts=4 et cin: */ +/* 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/. */ + +// HttpLog.h should generally be included first +#include "HttpLog.h" + +#include "AltServiceParent.h" +#include "AlternateServices.h" +#include "nsHttpHandler.h" + +namespace mozilla { +namespace net { + +mozilla::ipc::IPCResult AltServiceParent::RecvClearHostMapping( + const nsCString& aHost, const int32_t& aPort, + const OriginAttributes& aOriginAttributes, + const nsCString& aTopWindowOrigin) { + LOG(("AltServiceParent::RecvClearHostMapping [this=%p]\n", this)); + if (gHttpHandler) { + gHttpHandler->AltServiceCache()->ClearHostMapping( + aHost, aPort, aOriginAttributes, aTopWindowOrigin); + } + return IPC_OK(); +} + +mozilla::ipc::IPCResult AltServiceParent::RecvProcessHeader( + const nsCString& aBuf, const nsCString& aOriginScheme, + const nsCString& aOriginHost, const int32_t& aOriginPort, + const nsACString& aUsername, const nsACString& aTopWindowOrigin, + const bool& aPrivateBrowsing, const bool& aIsolated, + nsTArray&& aProxyInfo, const uint32_t& aCaps, + const OriginAttributes& aOriginAttributes) { + LOG(("AltServiceParent::RecvProcessHeader [this=%p]\n", this)); + nsProxyInfo* pi = aProxyInfo.IsEmpty() + ? nullptr + : nsProxyInfo::DeserializeProxyInfo(aProxyInfo); + AltSvcMapping::ProcessHeader(aBuf, aOriginScheme, aOriginHost, aOriginPort, + aUsername, aTopWindowOrigin, aPrivateBrowsing, + aIsolated, nullptr, pi, aCaps, + aOriginAttributes); + return IPC_OK(); +} + +void AltServiceParent::ActorDestroy(ActorDestroyReason aWhy) { + LOG(("AltServiceParent::ActorDestroy [this=%p]\n", this)); +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/AltServiceParent.h b/netwerk/protocol/http/AltServiceParent.h new file mode 100644 index 0000000000..7cde277d37 --- /dev/null +++ b/netwerk/protocol/http/AltServiceParent.h @@ -0,0 +1,41 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* 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/. */ + +#ifndef AltServiceParent_h__ +#define AltServiceParent_h__ + +#include "mozilla/net/PAltServiceParent.h" + +namespace mozilla { +namespace net { + +class AltServiceParent final : public PAltServiceParent { + public: + NS_INLINE_DECL_REFCOUNTING(AltServiceParent, override) + AltServiceParent() = default; + + mozilla::ipc::IPCResult RecvClearHostMapping( + const nsCString& aHost, const int32_t& aPort, + const OriginAttributes& aOriginAttributes, + const nsCString& aTopWindowOrigin); + + mozilla::ipc::IPCResult RecvProcessHeader( + const nsCString& aBuf, const nsCString& aOriginScheme, + const nsCString& aOriginHost, const int32_t& aOriginPort, + const nsACString& aUsername, const nsACString& aTopWindowOrigin, + const bool& aPrivateBrowsing, const bool& aIsolated, + nsTArray&& aProxyInfo, const uint32_t& aCaps, + const OriginAttributes& aOriginAttributes); + + void ActorDestroy(ActorDestroyReason aWhy) override; + + private: + virtual ~AltServiceParent() = default; +}; + +} // namespace net +} // namespace mozilla + +#endif // AltServiceParent_h__ diff --git a/netwerk/protocol/http/AltSvcTransactionChild.cpp b/netwerk/protocol/http/AltSvcTransactionChild.cpp new file mode 100644 index 0000000000..096d584c0e --- /dev/null +++ b/netwerk/protocol/http/AltSvcTransactionChild.cpp @@ -0,0 +1,75 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* 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/. */ + +// HttpLog.h should generally be included first +#include "HttpLog.h" + +#include "AltSvcTransactionChild.h" +#include "AlternateServices.h" +#include "nsHttpConnectionInfo.h" + +namespace mozilla { +namespace net { + +AltSvcTransactionChild::AltSvcTransactionChild(nsHttpConnectionInfo* aConnInfo, + uint32_t aCaps) + : mConnInfo(aConnInfo), mCaps(aCaps) { + LOG(("AltSvcTransactionChild %p ctor", this)); +} + +AltSvcTransactionChild::~AltSvcTransactionChild() { + LOG(("AltSvcTransactionChild %p dtor", this)); +} + +void AltSvcTransactionChild::OnTransactionDestroy(bool aValidateResult) { + LOG(("AltSvcTransactionChild::OnTransactionDestroy %p aValidateResult=%d", + this, aValidateResult)); + RefPtr self = this; + auto task = [self, aValidateResult]() { + if (self->CanSend()) { + self->Send__delete__(self, aValidateResult); + } + }; + if (!NS_IsMainThread()) { + NS_DispatchToMainThread( + NS_NewRunnableFunction("AltSvcTransactionChild::OnTransactionClose", + std::move(task)), + NS_DISPATCH_NORMAL); + return; + } + + task(); +} + +void AltSvcTransactionChild::OnTransactionClose(bool aValidateResult) { + LOG(("AltSvcTransactionChild::OnTransactionClose %p aValidateResult=%d", this, + aValidateResult)); + RefPtr self = this; + auto task = [self, aValidateResult]() { + if (self->CanSend()) { + self->SendOnTransactionClose(aValidateResult); + } + }; + if (!NS_IsMainThread()) { + NS_DispatchToMainThread( + NS_NewRunnableFunction("AltSvcTransactionChild::OnTransactionClose", + std::move(task)), + NS_DISPATCH_NORMAL); + return; + } + + task(); +} + +already_AddRefed +AltSvcTransactionChild::CreateTransaction() { + RefPtr transaction = + new AltSvcTransaction(mConnInfo, nullptr, mCaps, + this, mConnInfo->IsHttp3()); + return transaction.forget(); +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/AltSvcTransactionChild.h b/netwerk/protocol/http/AltSvcTransactionChild.h new file mode 100644 index 0000000000..eb3b4aa580 --- /dev/null +++ b/netwerk/protocol/http/AltSvcTransactionChild.h @@ -0,0 +1,39 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* 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/. */ + +#ifndef AltSvcTransactionChild_h__ +#define AltSvcTransactionChild_h__ + +#include "mozilla/net/PAltSvcTransactionChild.h" + +namespace mozilla { +namespace net { + +class nsHttpConnectionInfo; +class SpeculativeTransaction; + +class AltSvcTransactionChild final : public PAltSvcTransactionChild { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(AltSvcTransactionChild, override) + + explicit AltSvcTransactionChild(nsHttpConnectionInfo* aConnInfo, + uint32_t aCaps); + + void OnTransactionDestroy(bool aValidateResult); + void OnTransactionClose(bool aValidateResult); + + already_AddRefed CreateTransaction(); + + private: + virtual ~AltSvcTransactionChild(); + + RefPtr mConnInfo; + uint32_t mCaps; +}; + +} // namespace net +} // namespace mozilla + +#endif // AltSvcTransactionChild_h__ diff --git a/netwerk/protocol/http/AltSvcTransactionParent.cpp b/netwerk/protocol/http/AltSvcTransactionParent.cpp new file mode 100644 index 0000000000..494efa7f65 --- /dev/null +++ b/netwerk/protocol/http/AltSvcTransactionParent.cpp @@ -0,0 +1,64 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* 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/. */ + +// HttpLog.h should generally be included first +#include "HttpLog.h" + +#include "AltSvcTransactionParent.h" +#include "AlternateServices.h" +#include "mozilla/net/SocketProcessParent.h" +#include "nsHttpConnectionInfo.h" + +namespace mozilla { +namespace net { + +NS_IMPL_ADDREF_INHERITED(AltSvcTransactionParent, NullHttpTransaction) +NS_IMPL_RELEASE_INHERITED(AltSvcTransactionParent, NullHttpTransaction) + +NS_INTERFACE_MAP_BEGIN(AltSvcTransactionParent) + NS_INTERFACE_MAP_ENTRY_CONCRETE(AltSvcTransactionParent) +NS_INTERFACE_MAP_END_INHERITING(NullHttpTransaction) + +AltSvcTransactionParent::AltSvcTransactionParent( + nsHttpConnectionInfo* aConnInfo, nsIInterfaceRequestor* aCallbacks, + uint32_t aCaps, AltSvcMappingValidator* aValidator) + : SpeculativeTransaction(aConnInfo, aCallbacks, + aCaps & ~NS_HTTP_ALLOW_KEEPALIVE), + mValidator(aValidator) { + LOG(("AltSvcTransactionParent %p ctor", this)); + MOZ_ASSERT(mValidator); +} + +AltSvcTransactionParent::~AltSvcTransactionParent() { + LOG(("AltSvcTransactionParent %p dtor", this)); +} + +bool AltSvcTransactionParent::Init() { + SocketProcessParent* parent = SocketProcessParent::GetSingleton(); + if (!parent) { + return false; + } + + HttpConnectionInfoCloneArgs connInfo; + nsHttpConnectionInfo::SerializeHttpConnectionInfo(mConnectionInfo, connInfo); + return parent->SendPAltSvcTransactionConstructor(this, connInfo, mCaps); +} + +mozilla::ipc::IPCResult AltSvcTransactionParent::Recv__delete__( + const bool& aValidateResult) { + LOG(("AltSvcTransactionParent::Recv__delete__ this=%p", this)); + mValidator->OnTransactionDestroy(aValidateResult); + return IPC_OK(); +} + +mozilla::ipc::IPCResult AltSvcTransactionParent::RecvOnTransactionClose( + const bool& aValidateResult) { + LOG(("AltSvcTransactionParent::RecvOnTransactionClose this=%p", this)); + mValidator->OnTransactionClose(aValidateResult); + return IPC_OK(); +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/AltSvcTransactionParent.h b/netwerk/protocol/http/AltSvcTransactionParent.h new file mode 100644 index 0000000000..dc910ad301 --- /dev/null +++ b/netwerk/protocol/http/AltSvcTransactionParent.h @@ -0,0 +1,52 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* 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/. */ + +#ifndef AltSvcTransactionParent_h__ +#define AltSvcTransactionParent_h__ + +#include "mozilla/net/PAltSvcTransactionParent.h" +#include "mozilla/net/SpeculativeTransaction.h" + +namespace mozilla { +namespace net { + +class AltSvcMappingValidator; + +// 03d22e57-c364-4871-989a-6593eb909d24 +#define ALTSVCTRANSACTIONPARENT_IID \ + { \ + 0x03d22e57, 0xc364, 0x4871, { \ + 0x98, 0x9a, 0x65, 0x93, 0xeb, 0x90, 0x9d, 0x24 \ + } \ + } + +class AltSvcTransactionParent final : public PAltSvcTransactionParent, + public SpeculativeTransaction { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECLARE_STATIC_IID_ACCESSOR(ALTSVCTRANSACTIONPARENT_IID) + + explicit AltSvcTransactionParent(nsHttpConnectionInfo* aConnInfo, + nsIInterfaceRequestor* aCallbacks, + uint32_t aCaps, + AltSvcMappingValidator* aValidator); + + bool Init(); + mozilla::ipc::IPCResult Recv__delete__(const bool& aValidateResult); + mozilla::ipc::IPCResult RecvOnTransactionClose(const bool& aValidateResult); + + private: + virtual ~AltSvcTransactionParent(); + + RefPtr mValidator; +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(AltSvcTransactionParent, + ALTSVCTRANSACTIONPARENT_IID) + +} // namespace net +} // namespace mozilla + +#endif // AltSvcTransactionParent_h__ diff --git a/netwerk/protocol/http/AlternateServices.cpp b/netwerk/protocol/http/AlternateServices.cpp new file mode 100644 index 0000000000..1bc38b273d --- /dev/null +++ b/netwerk/protocol/http/AlternateServices.cpp @@ -0,0 +1,1330 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et 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 "HttpLog.h" + +#include "AlternateServices.h" +#include "LoadInfo.h" +#include "nsComponentManagerUtils.h" +#include "nsEscape.h" +#include "nsHttpConnectionInfo.h" +#include "nsHttpChannel.h" +#include "nsHttpHandler.h" +#include "nsIOService.h" +#include "nsThreadUtils.h" +#include "nsHttpTransaction.h" +#include "nsISSLSocketControl.h" +#include "nsIWellKnownOpportunisticUtils.h" +#include "mozilla/StaticPrefs_network.h" +#include "mozilla/dom/PContent.h" +#include "mozilla/SyncRunnable.h" +#include "mozilla/net/AltSvcTransactionParent.h" + +/* RFC 7838 Alternative Services + http://httpwg.org/http-extensions/opsec.html + note that connections currently do not do mixed-scheme (the I attribute + in the ConnectionInfo prevents it) but could, do not honor tls-commit and + should not, and always require authentication +*/ + +namespace mozilla { +namespace net { + +// function places true in outIsHTTPS if scheme is https, false if +// http, and returns an error if neither. originScheme passed into +// alternate service should already be normalized to those lower case +// strings by the URI parser (and so there is an assert)- this is an extra +// check. +static nsresult SchemeIsHTTPS(const nsACString& originScheme, + bool& outIsHTTPS) { + outIsHTTPS = originScheme.EqualsLiteral("https"); + + if (!outIsHTTPS && !originScheme.EqualsLiteral("http")) { + MOZ_ASSERT(!originScheme.LowerCaseEqualsLiteral("https") && + !originScheme.LowerCaseEqualsLiteral("http"), + "The scheme should already be lowercase"); + return NS_ERROR_UNEXPECTED; + } + return NS_OK; +} + +bool AltSvcMapping::AcceptableProxy(nsProxyInfo* proxyInfo) { + return !proxyInfo || proxyInfo->IsDirect() || proxyInfo->IsSOCKS(); +} + +void AltSvcMapping::ProcessHeader( + const nsCString& buf, const nsCString& originScheme, + const nsCString& originHost, int32_t originPort, const nsACString& username, + const nsACString& topWindowOrigin, bool privateBrowsing, bool isolated, + nsIInterfaceRequestor* callbacks, nsProxyInfo* proxyInfo, uint32_t caps, + const OriginAttributes& originAttributes, + bool aDontValidate /* = false */) { // aDontValidate is only used for + // testing + MOZ_ASSERT(NS_IsMainThread()); + LOG(("AltSvcMapping::ProcessHeader: %s\n", buf.get())); + + if (StaticPrefs::network_http_altsvc_proxy_checks() && + !AcceptableProxy(proxyInfo)) { + LOG(("AltSvcMapping::ProcessHeader ignoring due to proxy\n")); + return; + } + + bool isHTTPS; + if (NS_FAILED(SchemeIsHTTPS(originScheme, isHTTPS))) { + return; + } + if (!isHTTPS && !gHttpHandler->AllowAltSvcOE()) { + LOG(("Alt-Svc Response Header for http:// origin but OE disabled\n")); + return; + } + + LOG(("Alt-Svc Response Header %s\n", buf.get())); + ParsedHeaderValueListList parsedAltSvc(buf); + int32_t numEntriesInHeader = parsedAltSvc.mValues.Length(); + + // Only use one http3 version. + bool http3Found = false; + + for (uint32_t index = 0; index < parsedAltSvc.mValues.Length(); ++index) { + uint32_t maxage = 86400; // default + nsAutoCString hostname; + nsAutoCString npnToken; + int32_t portno = originPort; + bool clearEntry = false; + bool isHttp3 = false; + + for (uint32_t pairIndex = 0; + pairIndex < parsedAltSvc.mValues[index].mValues.Length(); + ++pairIndex) { + nsDependentCSubstring& currentName = + parsedAltSvc.mValues[index].mValues[pairIndex].mName; + nsDependentCSubstring& currentValue = + parsedAltSvc.mValues[index].mValues[pairIndex].mValue; + + if (!pairIndex) { + if (currentName.EqualsLiteral("clear")) { + clearEntry = true; + --numEntriesInHeader; // Only want to keep track of actual alt-svc + // maps, not clearing + break; + } + + // h2=[hostname]:443 or h3-xx=[hostname]:port + // XX is current version we support and it is define in nsHttp.h. + isHttp3 = gHttpHandler->IsHttp3VersionSupported(currentName); + npnToken = currentName; + + int32_t colonIndex = currentValue.FindChar(':'); + if (colonIndex >= 0) { + portno = + atoi(PromiseFlatCString(currentValue).get() + colonIndex + 1); + } else { + colonIndex = 0; + } + hostname.Assign(currentValue.BeginReading(), colonIndex); + } else if (currentName.EqualsLiteral("ma")) { + maxage = atoi(PromiseFlatCString(currentValue).get()); + } else { + LOG(("Alt Svc ignoring parameter %s", currentName.BeginReading())); + } + } + + if (clearEntry) { + nsCString suffix; + originAttributes.CreateSuffix(suffix); + LOG(("Alt Svc clearing mapping for %s:%d:%s", originHost.get(), + originPort, suffix.get())); + gHttpHandler->AltServiceCache()->ClearHostMapping( + originHost, originPort, originAttributes, topWindowOrigin); + continue; + } + + if (NS_FAILED(NS_CheckPortSafety(portno, originScheme.get()))) { + LOG(("Alt Svc doesn't allow port %d, ignoring", portno)); + continue; + } + + // unescape modifies a c string in place, so afterwards + // update nsCString length + nsUnescape(npnToken.BeginWriting()); + npnToken.SetLength(strlen(npnToken.BeginReading())); + + if (http3Found && isHttp3) { + LOG(("Alt Svc ignore multiple Http3 options (%s)", npnToken.get())); + continue; + } + + uint32_t spdyIndex; + SpdyInformation* spdyInfo = gHttpHandler->SpdyInfo(); + if (!(NS_SUCCEEDED(spdyInfo->GetNPNIndex(npnToken, &spdyIndex)) && + spdyInfo->ProtocolEnabled(spdyIndex)) && + !(isHttp3 && gHttpHandler->IsHttp3Enabled() && + !gHttpHandler->IsHttp3Excluded(hostname.IsEmpty() ? originHost + : hostname))) { + LOG(("Alt Svc unknown protocol %s, ignoring", npnToken.get())); + continue; + } + + if (isHttp3) { + http3Found = true; + } + + RefPtr mapping = new AltSvcMapping( + gHttpHandler->AltServiceCache()->GetStoragePtr(), + gHttpHandler->AltServiceCache()->StorageEpoch(), originScheme, + originHost, originPort, username, topWindowOrigin, privateBrowsing, + isolated, NowInSeconds() + maxage, hostname, portno, npnToken, + originAttributes, isHttp3); + if (mapping->TTL() <= 0) { + LOG(("Alt Svc invalid map")); + mapping = nullptr; + // since this isn't a parse error, let's clear any existing mapping + // as that would have happened if we had accepted the parameters. + gHttpHandler->AltServiceCache()->ClearHostMapping( + originHost, originPort, originAttributes, topWindowOrigin); + } else if (!aDontValidate) { + gHttpHandler->UpdateAltServiceMapping(mapping, proxyInfo, callbacks, caps, + originAttributes); + } else { + gHttpHandler->UpdateAltServiceMappingWithoutValidation( + mapping, proxyInfo, callbacks, caps, originAttributes); + } + } + + if (numEntriesInHeader) { // Ignore headers that were just "alt-svc: clear" + Telemetry::Accumulate(Telemetry::HTTP_ALTSVC_ENTRIES_PER_HEADER, + numEntriesInHeader); + } +} + +AltSvcMapping::AltSvcMapping( + DataStorage* storage, int32_t epoch, const nsACString& originScheme, + const nsACString& originHost, int32_t originPort, + const nsACString& username, const nsACString& topWindowOrigin, + bool privateBrowsing, bool isolated, uint32_t expiresAt, + const nsACString& alternateHost, int32_t alternatePort, + const nsACString& npnToken, const OriginAttributes& originAttributes, + bool aIsHttp3) + : mStorage(storage), + mStorageEpoch(epoch), + mAlternateHost(alternateHost), + mAlternatePort(alternatePort), + mOriginHost(originHost), + mOriginPort(originPort), + mUsername(username), + mTopWindowOrigin(topWindowOrigin), + mPrivate(privateBrowsing), + mIsolated(isolated), + mExpiresAt(expiresAt), + mValidated(false), + mMixedScheme(false), + mNPNToken(npnToken), + mOriginAttributes(originAttributes), + mSyncOnlyOnSuccess(false), + mIsHttp3(aIsHttp3) { + MOZ_ASSERT(NS_IsMainThread()); + + if (NS_FAILED(SchemeIsHTTPS(originScheme, mHttps))) { + LOG(("AltSvcMapping ctor %p invalid scheme\n", this)); + mExpiresAt = 0; // invalid + } + + if (mAlternatePort == -1) { + mAlternatePort = mHttps ? NS_HTTPS_DEFAULT_PORT : NS_HTTP_DEFAULT_PORT; + } + if (mOriginPort == -1) { + mOriginPort = mHttps ? NS_HTTPS_DEFAULT_PORT : NS_HTTP_DEFAULT_PORT; + } + + LOG(("AltSvcMapping ctor %p %s://%s:%d to %s:%d\n", this, + nsCString(originScheme).get(), mOriginHost.get(), mOriginPort, + mAlternateHost.get(), mAlternatePort)); + + if (mAlternateHost.IsEmpty()) { + mAlternateHost = mOriginHost; + } + + if ((mAlternatePort == mOriginPort) && + mAlternateHost.EqualsIgnoreCase(mOriginHost.get()) && !mIsHttp3) { + // Http2 on the same host:port does not make sense because we are + // connecting to the same end point over the same protocol (TCP) as with + // original host. On the other hand, for Http3 alt-svc can be hosted on + // the same host:port because protocol(UDP vs. TCP) is always different and + // we are not connecting to the same end point. + LOG(("Alt Svc is also origin Svc - ignoring\n")); + mExpiresAt = 0; // invalid + } + + if (mExpiresAt) { + MakeHashKey(mHashKey, originScheme, mOriginHost, mOriginPort, mPrivate, + mIsolated, mTopWindowOrigin, mOriginAttributes, mIsHttp3); + } +} + +void AltSvcMapping::MakeHashKey( + nsCString& outKey, const nsACString& originScheme, + const nsACString& originHost, int32_t originPort, bool privateBrowsing, + bool isolated, const nsACString& topWindowOrigin, + const OriginAttributes& originAttributes, bool aHttp3) { + outKey.Truncate(); + + if (originPort == -1) { + bool isHttps = originScheme.EqualsLiteral("https"); + originPort = isHttps ? NS_HTTPS_DEFAULT_PORT : NS_HTTP_DEFAULT_PORT; + } + + outKey.Append(originScheme); + outKey.Append(':'); + outKey.Append(originHost); + outKey.Append(':'); + outKey.AppendInt(originPort); + outKey.Append(':'); + outKey.Append(privateBrowsing ? 'P' : '.'); + outKey.Append(':'); + nsAutoCString suffix; + originAttributes.CreateSuffix(suffix); + outKey.Append(suffix); + outKey.Append(':'); + + if (isolated) { + outKey.Append('I'); + outKey.Append(':'); + outKey.Append(topWindowOrigin); + outKey.Append( + '|'); // Be careful, the top window origin may contain colons! + } + outKey.Append(aHttp3 ? '3' : '.'); +} + +int32_t AltSvcMapping::TTL() { return mExpiresAt - NowInSeconds(); } + +void AltSvcMapping::SyncString(const nsCString& str) { + MOZ_ASSERT(NS_IsMainThread()); + mStorage->Put(HashKey(), str, + mPrivate ? DataStorage_Private : DataStorage_Persistent); +} + +void AltSvcMapping::Sync() { + if (!mStorage) { + return; + } + if (mSyncOnlyOnSuccess && !mValidated) { + return; + } + nsCString value; + Serialize(value); + + if (!NS_IsMainThread()) { + nsCOMPtr r; + r = NewRunnableMethod("net::AltSvcMapping::SyncString", this, + &AltSvcMapping::SyncString, value); + NS_DispatchToMainThread(r, NS_DISPATCH_NORMAL); + return; + } + + mStorage->Put(HashKey(), value, + mPrivate ? DataStorage_Private : DataStorage_Persistent); +} + +void AltSvcMapping::SetValidated(bool val) { + mValidated = val; + Sync(); +} + +void AltSvcMapping::SetMixedScheme(bool val) { + mMixedScheme = val; + Sync(); +} + +void AltSvcMapping::SetExpiresAt(int32_t val) { + mExpiresAt = val; + Sync(); +} + +void AltSvcMapping::SetExpired() { + LOG(("AltSvcMapping SetExpired %p origin %s alternate %s\n", this, + mOriginHost.get(), mAlternateHost.get())); + mExpiresAt = NowInSeconds() - 1; + Sync(); +} + +bool AltSvcMapping::RouteEquals(AltSvcMapping* map) { + MOZ_ASSERT(map->mHashKey.Equals(mHashKey)); + return mAlternateHost.Equals(map->mAlternateHost) && + (mAlternatePort == map->mAlternatePort) && + mNPNToken.Equals(map->mNPNToken); +} + +void AltSvcMapping::GetConnectionInfo( + nsHttpConnectionInfo** outCI, nsProxyInfo* pi, + const OriginAttributes& originAttributes) { + RefPtr ci = new nsHttpConnectionInfo( + mOriginHost, mOriginPort, mNPNToken, mUsername, mTopWindowOrigin, pi, + originAttributes, mAlternateHost, mAlternatePort, mIsHttp3); + + // http:// without the mixed-scheme attribute needs to be segmented in the + // connection manager connection information hash with this attribute + if (!mHttps && !mMixedScheme) { + ci->SetInsecureScheme(true); + } + ci->SetPrivate(mPrivate); + ci->SetIsolated(mIsolated); + ci.forget(outCI); +} + +void AltSvcMapping::Serialize(nsCString& out) { + // Be careful, when serializing new members, add them to the end of this list. + out = mHttps ? "https:"_ns : "http:"_ns; + out.Append(mOriginHost); + out.Append(':'); + out.AppendInt(mOriginPort); + out.Append(':'); + out.Append(mAlternateHost); + out.Append(':'); + out.AppendInt(mAlternatePort); + out.Append(':'); + out.Append(mUsername); + out.Append(':'); + out.Append(mPrivate ? 'y' : 'n'); + out.Append(':'); + out.AppendInt(mExpiresAt); + out.Append(':'); + out.Append(mNPNToken); + out.Append(':'); + out.Append(mValidated ? 'y' : 'n'); + out.Append(':'); + out.AppendInt(mStorageEpoch); + out.Append(':'); + out.Append(mMixedScheme ? 'y' : 'n'); + out.Append(':'); + nsAutoCString suffix; + mOriginAttributes.CreateSuffix(suffix); + out.Append(suffix); + out.Append(':'); + out.Append(mTopWindowOrigin); + out.Append('|'); // Be careful, the top window origin may contain colons! + out.Append(mIsolated ? 'y' : 'n'); + out.Append(':'); + out.Append(mIsHttp3 ? 'y' : 'n'); + out.Append(':'); + // Add code to serialize new members here! +} + +AltSvcMapping::AltSvcMapping(DataStorage* storage, int32_t epoch, + const nsCString& str) + : mStorage(storage), + mStorageEpoch(epoch), + mSyncOnlyOnSuccess(false), + mIsHttp3(false) { + mValidated = false; + nsresult code; + char separator = ':'; + + // The the do {} while(0) loop acts like try/catch(e){} with the break in + // _NS_NEXT_TOKEN + do { +#ifdef _NS_NEXT_TOKEN + COMPILER ERROR +#endif +#define _NS_NEXT_TOKEN \ + start = idx + 1; \ + idx = str.FindChar(separator, start); \ + if (idx < 0) break; + int32_t start = 0; + int32_t idx; + idx = str.FindChar(separator, start); + if (idx < 0) break; + // Be careful, when deserializing new members, add them to the end of this + // list. + mHttps = Substring(str, start, idx - start).EqualsLiteral("https"); + _NS_NEXT_TOKEN; + mOriginHost = Substring(str, start, idx - start); + _NS_NEXT_TOKEN; + mOriginPort = + nsCString(Substring(str, start, idx - start)).ToInteger(&code); + _NS_NEXT_TOKEN; + mAlternateHost = Substring(str, start, idx - start); + _NS_NEXT_TOKEN; + mAlternatePort = + nsCString(Substring(str, start, idx - start)).ToInteger(&code); + _NS_NEXT_TOKEN; + mUsername = Substring(str, start, idx - start); + _NS_NEXT_TOKEN; + mPrivate = Substring(str, start, idx - start).EqualsLiteral("y"); + _NS_NEXT_TOKEN; + mExpiresAt = nsCString(Substring(str, start, idx - start)).ToInteger(&code); + _NS_NEXT_TOKEN; + mNPNToken = Substring(str, start, idx - start); + _NS_NEXT_TOKEN; + mValidated = Substring(str, start, idx - start).EqualsLiteral("y"); + _NS_NEXT_TOKEN; + mStorageEpoch = + nsCString(Substring(str, start, idx - start)).ToInteger(&code); + _NS_NEXT_TOKEN; + mMixedScheme = Substring(str, start, idx - start).EqualsLiteral("y"); + _NS_NEXT_TOKEN; + Unused << mOriginAttributes.PopulateFromSuffix( + Substring(str, start, idx - start)); + // The separator after the top window origin is a pipe character since the + // origin string can contain colons. + separator = '|'; + _NS_NEXT_TOKEN; + mTopWindowOrigin = Substring(str, start, idx - start); + separator = ':'; + _NS_NEXT_TOKEN; + mIsolated = Substring(str, start, idx - start).EqualsLiteral("y"); + _NS_NEXT_TOKEN; + mIsHttp3 = Substring(str, start, idx - start).EqualsLiteral("y"); + // Add code to deserialize new members here! +#undef _NS_NEXT_TOKEN + + MakeHashKey(mHashKey, mHttps ? "https"_ns : "http"_ns, mOriginHost, + mOriginPort, mPrivate, mIsolated, mTopWindowOrigin, + mOriginAttributes, mIsHttp3); + } while (false); +} + +AltSvcMappingValidator::AltSvcMappingValidator(AltSvcMapping* aMap) + : mMapping(aMap) { + LOG(("AltSvcMappingValidator ctor %p map %p [%s -> %s]", this, aMap, + aMap->OriginHost().get(), aMap->AlternateHost().get())); + MOZ_ASSERT(mMapping); + MOZ_ASSERT(mMapping->HTTPS()); // http:// uses the .wk path +} + +void AltSvcMappingValidator::OnTransactionDestroy(bool aValidateResult) { + mMapping->SetValidated(aValidateResult); + if (!mMapping->Validated()) { + // try again later + mMapping->SetExpiresAt(NowInSeconds() + 2); + } + LOG( + ("AltSvcMappingValidator::OnTransactionDestroy %p map %p validated %d " + "[%s]", + this, mMapping.get(), mMapping->Validated(), mMapping->HashKey().get())); +} + +void AltSvcMappingValidator::OnTransactionClose(bool aValidateResult) { + mMapping->SetValidated(aValidateResult); + LOG( + ("AltSvcMappingValidator::OnTransactionClose %p map %p validated %d " + "[%s]", + this, mMapping.get(), mMapping->Validated(), mMapping->HashKey().get())); +} + +template +AltSvcTransaction::AltSvcTransaction( + nsHttpConnectionInfo* ci, nsIInterfaceRequestor* callbacks, uint32_t caps, + Validator* aValidator, bool aIsHttp3) + : SpeculativeTransaction(ci, callbacks, caps & ~NS_HTTP_ALLOW_KEEPALIVE), + mValidator(aValidator), + mIsHttp3(aIsHttp3), + mRunning(true), + mTriedToValidate(false), + mTriedToWrite(false), + mValidatedResult(false) { + MOZ_ASSERT_IF(nsIOService::UseSocketProcess(), XRE_IsSocketProcess()); + MOZ_ASSERT_IF(!nsIOService::UseSocketProcess(), XRE_IsParentProcess()); +} + +template +AltSvcTransaction::~AltSvcTransaction() { + LOG(("AltSvcTransaction dtor %p running %d", this, mRunning)); + + if (mRunning) { + mValidatedResult = MaybeValidate(NS_OK); + mValidator->OnTransactionDestroy(mValidatedResult); + } +} + +template +bool AltSvcTransaction::MaybeValidate(nsresult reason) { + if (mTriedToValidate) { + return mValidatedResult; + } + mTriedToValidate = true; + + LOG(("AltSvcTransaction::MaybeValidate() %p reason=%" PRIx32 + " running=%d conn=%p write=%d", + this, static_cast(reason), mRunning, mConnection.get(), + mTriedToWrite)); + + if (mTriedToWrite && reason == NS_BASE_STREAM_CLOSED) { + // The normal course of events is to cause the transaction to fail with + // CLOSED on a write - so that's a success that means the HTTP/2 session + // is setup. + reason = NS_OK; + } + + if (NS_FAILED(reason) || !mRunning || !mConnection) { + LOG(("AltSvcTransaction::MaybeValidate %p Failed due to precondition", + this)); + return false; + } + + // insist on >= http/2 + HttpVersion version = mConnection->Version(); + LOG(("AltSvcTransaction::MaybeValidate() %p version %d\n", this, + static_cast(version))); + if ((!mIsHttp3 && (version != HttpVersion::v2_0)) || + (mIsHttp3 && (version != HttpVersion::v3_0))) { + LOG( + ("AltSvcTransaction::MaybeValidate %p Failed due to protocol version" + " expacted %s.", + this, mIsHttp3 ? "Http3" : "Http2")); + return false; + } + + nsCOMPtr secInfo; + mConnection->GetSecurityInfo(getter_AddRefs(secInfo)); + nsCOMPtr socketControl = do_QueryInterface(secInfo); + + LOG(("AltSvcTransaction::MaybeValidate() %p socketControl=%p\n", this, + socketControl.get())); + + if (socketControl->GetFailedVerification()) { + LOG( + ("AltSvcTransaction::MaybeValidate() %p " + "not validated due to auth error", + this)); + return false; + } + + LOG( + ("AltSvcTransaction::MaybeValidate() %p " + "validating alternate service with successful auth check", + this)); + + return true; +} + +template +void AltSvcTransaction::Close(nsresult reason) { + LOG(("AltSvcTransaction::Close() %p reason=%" PRIx32 " running %d", this, + static_cast(reason), mRunning)); + + mValidatedResult = MaybeValidate(reason); + mValidator->OnTransactionClose(mValidatedResult); + if (!mValidatedResult && mConnection) { + mConnection->DontReuse(); + } + NullHttpTransaction::Close(reason); +} + +template +nsresult AltSvcTransaction::ReadSegments( + nsAHttpSegmentReader* reader, uint32_t count, uint32_t* countRead) { + LOG(("AltSvcTransaction::ReadSegements() %p\n", this)); + mTriedToWrite = true; + return NullHttpTransaction::ReadSegments(reader, count, countRead); +} + +class WellKnownChecker { + public: + WellKnownChecker(nsIURI* uri, const nsCString& origin, uint32_t caps, + nsHttpConnectionInfo* ci, AltSvcMapping* mapping) + : mWaiting( + 2) // waiting for 2 channels (default and alternate) to complete + , + mOrigin(origin), + mAlternatePort(ci->RoutedPort()), + mMapping(mapping), + mCI(ci), + mURI(uri), + mCaps(caps) { + LOG(("WellKnownChecker ctor %p\n", this)); + MOZ_ASSERT(!mMapping->HTTPS()); + } + + nsresult Start() { + LOG(("WellKnownChecker::Start %p\n", this)); + nsCOMPtr loadInfo = + new LoadInfo(nsContentUtils::GetSystemPrincipal(), nullptr, nullptr, + nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + nsIContentPolicy::TYPE_OTHER); + loadInfo->SetOriginAttributes(mCI->GetOriginAttributes()); + // allow deprecated HTTP request from SystemPrincipal + loadInfo->SetAllowDeprecatedSystemRequests(true); + + RefPtr chan = new nsHttpChannel(); + nsresult rv; + + mTransactionAlternate = new TransactionObserver(chan, this); + RefPtr newCI = mCI->Clone(); + rv = MakeChannel(chan, mTransactionAlternate, newCI, mURI, mCaps, loadInfo); + if (NS_FAILED(rv)) { + return rv; + } + chan = new nsHttpChannel(); + mTransactionOrigin = new TransactionObserver(chan, this); + newCI = nullptr; + return MakeChannel(chan, mTransactionOrigin, newCI, mURI, mCaps, loadInfo); + } + + void Done(TransactionObserver* finished) { + MOZ_ASSERT(NS_IsMainThread()); + LOG(("WellKnownChecker::Done %p waiting for %d\n", this, mWaiting)); + + mWaiting--; // another channel is complete + if (!mWaiting) { // there are all complete! + nsAutoCString mAlternateCT, mOriginCT; + mTransactionOrigin->mChannel->GetContentType(mOriginCT); + mTransactionAlternate->mChannel->GetContentType(mAlternateCT); + nsCOMPtr uu = + do_CreateInstance(NS_WELLKNOWNOPPORTUNISTICUTILS_CONTRACTID); + bool accepted = false; + + if (!mTransactionOrigin->mStatusOK) { + LOG(("WellKnownChecker::Done %p origin was not 200 response code\n", + this)); + } else if (!mTransactionAlternate->mAuthOK) { + LOG(("WellKnownChecker::Done %p alternate was not TLS authenticated\n", + this)); + } else if (!mTransactionAlternate->mStatusOK) { + LOG(("WellKnownChecker::Done %p alternate was not 200 response code\n", + this)); + } else if (!mTransactionAlternate->mVersionOK) { + LOG(("WellKnownChecker::Done %p alternate was not at least h2 or h3\n", + this)); + } else if (!mTransactionAlternate->mWKResponse.Equals( + mTransactionOrigin->mWKResponse)) { + LOG( + ("WellKnownChecker::Done %p alternate and origin " + ".wk representations don't match\norigin: %s\alternate:%s\n", + this, mTransactionOrigin->mWKResponse.get(), + mTransactionAlternate->mWKResponse.get())); + } else if (!mAlternateCT.Equals(mOriginCT)) { + LOG( + ("WellKnownChecker::Done %p alternate and origin content types " + "dont match\n", + this)); + } else if (!mAlternateCT.EqualsLiteral("application/json")) { + LOG(("WellKnownChecker::Done %p .wk content type is %s\n", this, + mAlternateCT.get())); + } else if (!uu) { + LOG(("WellKnownChecker::Done %p json parser service unavailable\n", + this)); + } else { + accepted = true; + } + + if (accepted) { + MOZ_ASSERT(!mMapping->HTTPS()); // https:// does not use .wk + + nsresult rv = uu->Verify(mTransactionAlternate->mWKResponse, mOrigin); + if (NS_SUCCEEDED(rv)) { + bool validWK = false; + Unused << uu->GetValid(&validWK); + if (!validWK) { + LOG(("WellKnownChecker::Done %p json parser declares invalid\n%s\n", + this, mTransactionAlternate->mWKResponse.get())); + accepted = false; + } + } else { + LOG(("WellKnownChecker::Done %p .wk jason eval failed to run\n", + this)); + accepted = false; + } + } + + MOZ_ASSERT(!mMapping->Validated()); + if (accepted) { + LOG(("WellKnownChecker::Done %p Alternate for %s ACCEPTED\n", this, + mOrigin.get())); + mMapping->SetValidated(true); + } else { + LOG(("WellKnownChecker::Done %p Alternate for %s FAILED\n", this, + mOrigin.get())); + // try again soon + mMapping->SetExpiresAt(NowInSeconds() + 2); + } + + delete this; + } + } + + ~WellKnownChecker() { LOG(("WellKnownChecker dtor %p\n", this)); } + + private: + nsresult MakeChannel(nsHttpChannel* chan, TransactionObserver* obs, + nsHttpConnectionInfo* ci, nsIURI* uri, uint32_t caps, + nsILoadInfo* loadInfo) { + uint64_t channelId; + nsLoadFlags flags; + + ExtContentPolicyType contentPolicyType = + loadInfo->GetExternalContentPolicyType(); + + if (NS_FAILED(gHttpHandler->NewChannelId(channelId)) || + NS_FAILED(chan->Init(uri, caps, nullptr, 0, nullptr, channelId, + contentPolicyType)) || + NS_FAILED(chan->SetAllowAltSvc(false)) || + NS_FAILED(chan->SetRedirectMode( + nsIHttpChannelInternal::REDIRECT_MODE_ERROR)) || + NS_FAILED(chan->SetLoadInfo(loadInfo)) || + NS_FAILED(chan->GetLoadFlags(&flags))) { + return NS_ERROR_FAILURE; + } + flags |= HttpBaseChannel::LOAD_BYPASS_CACHE; + if (NS_FAILED(chan->SetLoadFlags(flags))) { + return NS_ERROR_FAILURE; + } + chan->SetTransactionObserver(obs); + chan->SetConnectionInfo(ci); + return chan->AsyncOpen(obs); + } + + RefPtr mTransactionAlternate; + RefPtr mTransactionOrigin; + uint32_t mWaiting; // semaphore + nsCString mOrigin; + int32_t mAlternatePort; + RefPtr mMapping; + RefPtr mCI; + nsCOMPtr mURI; + uint32_t mCaps; +}; + +NS_IMPL_ISUPPORTS(TransactionObserver, nsIStreamListener) + +TransactionObserver::TransactionObserver(nsHttpChannel* channel, + WellKnownChecker* checker) + : mChannel(channel), + mChecker(checker), + mRanOnce(false), + mStatusOK(false), + mAuthOK(false), + mVersionOK(false) { + LOG(("TransactionObserver ctor %p channel %p checker %p\n", this, channel, + checker)); + mChannelRef = do_QueryInterface((nsIHttpChannel*)channel); +} + +void TransactionObserver::Complete(bool versionOK, bool authOK, + nsresult reason) { + if (mRanOnce) { + return; + } + mRanOnce = true; + + mVersionOK = versionOK; + mAuthOK = authOK; + + LOG( + ("TransactionObserve::Complete %p authOK %d versionOK %d" + " reason %" PRIx32, + this, authOK, versionOK, static_cast(reason))); +} + +#define MAX_WK 32768 + +NS_IMETHODIMP +TransactionObserver::OnStartRequest(nsIRequest* aRequest) { + MOZ_ASSERT(NS_IsMainThread()); + // only consider the first 32KB.. because really. + mWKResponse.SetCapacity(MAX_WK); + return NS_OK; +} + +NS_IMETHODIMP +TransactionObserver::OnDataAvailable(nsIRequest* aRequest, + nsIInputStream* aStream, uint64_t aOffset, + uint32_t aCount) { + MOZ_ASSERT(NS_IsMainThread()); + uint32_t oldLen = mWKResponse.Length(); + uint64_t newLen = aCount + oldLen; + if (newLen < MAX_WK) { + auto handleOrErr = mWKResponse.BulkWrite(newLen, oldLen, false); + if (handleOrErr.isErr()) { + return handleOrErr.unwrapErr(); + } + auto handle = handleOrErr.unwrap(); + uint32_t amtRead; + if (NS_SUCCEEDED( + aStream->Read(handle.Elements() + oldLen, aCount, &amtRead))) { + MOZ_ASSERT(oldLen + amtRead <= newLen); + handle.Finish(oldLen + amtRead, false); + LOG(("TransactionObserver onDataAvailable %p read %d of .wk [%d]\n", this, + amtRead, mWKResponse.Length())); + } else { + LOG(("TransactionObserver onDataAvailable %p read error\n", this)); + } + } + return NS_OK; +} + +NS_IMETHODIMP +TransactionObserver::OnStopRequest(nsIRequest* aRequest, nsresult code) { + MOZ_ASSERT(NS_IsMainThread()); + LOG(("TransactionObserver onStopRequest %p code %" PRIx32 "\n", this, + static_cast(code))); + if (NS_SUCCEEDED(code)) { + nsHttpResponseHead* hdrs = mChannel->GetResponseHead(); + LOG(("TransactionObserver onStopRequest %p http resp %d\n", this, + hdrs ? hdrs->Status() : -1)); + mStatusOK = hdrs && (hdrs->Status() == 200); + } + if (mChecker) { + mChecker->Done(this); + } + return NS_OK; +} + +void AltSvcCache::EnsureStorageInited() { + if (mStorage) { + return; + } + + auto initTask = [&]() { + MOZ_ASSERT(NS_IsMainThread()); + + // DataStorage gives synchronous access to a memory based hash table + // that is backed by disk where those writes are done asynchronously + // on another thread + mStorage = DataStorage::Get(DataStorageClass::AlternateServices); + if (!mStorage) { + LOG(("AltSvcCache::EnsureStorageInited WARN NO STORAGE\n")); + return; + } + + if (NS_FAILED(mStorage->Init(nullptr))) { + mStorage = nullptr; + } + + mStorageEpoch = NowInSeconds(); + }; + + if (NS_IsMainThread()) { + initTask(); + return; + } + + nsCOMPtr main = GetMainThreadEventTarget(); + if (!main) { + return; + } + + SyncRunnable::DispatchToThread( + main, new SyncRunnable(NS_NewRunnableFunction( + "AltSvcCache::EnsureStorageInited", initTask))); +} + +already_AddRefed AltSvcCache::LookupMapping( + const nsCString& key, bool privateBrowsing) { + LOG(("AltSvcCache::LookupMapping %p %s\n", this, key.get())); + if (!mStorage) { + LOG(("AltSvcCache::LookupMapping %p no backing store\n", this)); + return nullptr; + } + + if (NS_IsMainThread() && !mStorage->IsReady()) { + LOG(("AltSvcCache::LookupMapping %p skip when storage is not ready\n", + this)); + return nullptr; + } + + nsCString val(mStorage->Get( + key, privateBrowsing ? DataStorage_Private : DataStorage_Persistent)); + if (val.IsEmpty()) { + LOG(("AltSvcCache::LookupMapping %p MISS\n", this)); + return nullptr; + } + RefPtr rv = new AltSvcMapping(mStorage, mStorageEpoch, val); + if (!rv->Validated() && (rv->StorageEpoch() != mStorageEpoch)) { + // this was an in progress validation abandoned in a different session + // rare edge case will not detect session change - that's ok as only impact + // will be loss of alt-svc to this origin for this session. + LOG(("AltSvcCache::LookupMapping %p invalid hit - MISS\n", this)); + mStorage->Remove( + key, rv->Private() ? DataStorage_Private : DataStorage_Persistent); + return nullptr; + } + + if (rv->IsHttp3() && + (!gHttpHandler->IsHttp3Enabled() || + !gHttpHandler->IsHttp3VersionSupported(rv->NPNToken()) || + gHttpHandler->IsHttp3Excluded(rv->AlternateHost()))) { + // If Http3 is disabled or the version not supported anymore, remove the + // mapping. + mStorage->Remove( + key, rv->Private() ? DataStorage_Private : DataStorage_Persistent); + return nullptr; + } + + if (rv->TTL() <= 0) { + LOG(("AltSvcCache::LookupMapping %p expired hit - MISS\n", this)); + mStorage->Remove( + key, rv->Private() ? DataStorage_Private : DataStorage_Persistent); + return nullptr; + } + + MOZ_ASSERT(rv->Private() == privateBrowsing); + LOG(("AltSvcCache::LookupMapping %p HIT %p\n", this, rv.get())); + return rv.forget(); +} + +// This is only used for testing! +void AltSvcCache::UpdateAltServiceMappingWithoutValidation( + AltSvcMapping* map, nsProxyInfo* pi, nsIInterfaceRequestor* aCallbacks, + uint32_t caps, const OriginAttributes& originAttributes) { + MOZ_ASSERT(NS_IsMainThread()); + if (!mStorage) { + return; + } + RefPtr existing = + LookupMapping(map->HashKey(), map->Private()); + LOG( + ("AltSvcCache::UpdateAltServiceMappingWithoutValidation %p map %p " + "existing %p %s", + this, map, existing.get(), map->AlternateHost().get())); + if (!existing) { + map->SetValidated(true); + } +} + +void AltSvcCache::UpdateAltServiceMapping( + AltSvcMapping* map, nsProxyInfo* pi, nsIInterfaceRequestor* aCallbacks, + uint32_t caps, const OriginAttributes& originAttributes) { + MOZ_ASSERT(NS_IsMainThread()); + if (!mStorage) { + return; + } + RefPtr existing = + LookupMapping(map->HashKey(), map->Private()); + LOG( + ("AltSvcCache::UpdateAltServiceMapping %p map %p existing %p %s " + "validated=%d", + this, map, existing.get(), map->AlternateHost().get(), + existing ? existing->Validated() : 0)); + + if (existing && existing->Validated()) { + if (existing->RouteEquals(map)) { + // update expires in storage + // if this is http:// then a ttl can only be extended via .wk, so ignore + // this header path unless it is making things shorter + if (existing->HTTPS()) { + LOG( + ("AltSvcCache::UpdateAltServiceMapping %p map %p updates ttl of " + "%p\n", + this, map, existing.get())); + existing->SetExpiresAt(map->GetExpiresAt()); + } else { + if (map->GetExpiresAt() < existing->GetExpiresAt()) { + LOG( + ("AltSvcCache::UpdateAltServiceMapping %p map %p reduces ttl of " + "%p\n", + this, map, existing.get())); + existing->SetExpiresAt(map->GetExpiresAt()); + } else { + LOG( + ("AltSvcCache::UpdateAltServiceMapping %p map %p tries to extend " + "%p but" + " cannot as without .wk\n", + this, map, existing.get())); + } + } + Telemetry::Accumulate(Telemetry::HTTP_ALTSVC_MAPPING_CHANGED_TARGET, + false); + return; + } + + if (map->GetExpiresAt() < existing->GetExpiresAt()) { + LOG( + ("AltSvcCache::UpdateAltServiceMapping %p map %p ttl shorter than " + "%p, ignoring", + this, map, existing.get())); + return; + } + + // new alternate. start new validation + LOG(("AltSvcCache::UpdateAltServiceMapping %p map %p may overwrite %p\n", + this, map, existing.get())); + Telemetry::Accumulate(Telemetry::HTTP_ALTSVC_MAPPING_CHANGED_TARGET, true); + } + + if (existing && !existing->Validated()) { + LOG( + ("AltSvcCache::UpdateAltServiceMapping %p map %p ignored because %p " + "still in progress\n", + this, map, existing.get())); + return; + } + + // start new validation, but don't overwrite a valid existing mapping unless + // this completes successfully + MOZ_ASSERT(!map->Validated()); + if (!existing) { + map->Sync(); + } else { + map->SetSyncOnlyOnSuccess(true); + } + + RefPtr ci; + map->GetConnectionInfo(getter_AddRefs(ci), pi, originAttributes); + caps |= ci->GetAnonymous() ? NS_HTTP_LOAD_ANONYMOUS : 0; + caps |= NS_HTTP_ERROR_SOFTLY; + + if (map->HTTPS()) { + LOG( + ("AltSvcCache::UpdateAltServiceMapping %p validation via " + "speculative connect started\n", + this)); + // for https resources we only establish a connection + nsCOMPtr callbacks = new AltSvcOverride(aCallbacks); + RefPtr validator = new AltSvcMappingValidator(map); + RefPtr transaction; + if (nsIOService::UseSocketProcess()) { + RefPtr parent = + new AltSvcTransactionParent(ci, aCallbacks, caps, validator); + if (!parent->Init()) { + return; + } + transaction = parent; + } else { + transaction = new AltSvcTransaction( + ci, aCallbacks, caps, validator, map->IsHttp3()); + } + + nsresult rv = + gHttpHandler->SpeculativeConnect(ci, callbacks, caps, transaction); + if (NS_FAILED(rv)) { + LOG( + ("AltSvcCache::UpdateAltServiceMapping %p " + "speculative connect failed with code %08x\n", + this, static_cast(rv))); + } + } else { + // for http:// resources we fetch .well-known too + nsAutoCString origin("http://"_ns); + + // Check whether origin is an ipv6 address. In that case we need to add + // '[]'. + if (map->OriginHost().FindChar(':') != kNotFound) { + origin.Append('['); + origin.Append(map->OriginHost()); + origin.Append(']'); + } else { + origin.Append(map->OriginHost()); + } + if (map->OriginPort() != NS_HTTP_DEFAULT_PORT) { + origin.Append(':'); + origin.AppendInt(map->OriginPort()); + } + + nsCOMPtr wellKnown; + nsAutoCString uri(origin); + uri.AppendLiteral("/.well-known/http-opportunistic"); + NS_NewURI(getter_AddRefs(wellKnown), uri); + + auto* checker = new WellKnownChecker(wellKnown, origin, caps, ci, map); + if (NS_FAILED(checker->Start())) { + LOG( + ("AltSvcCache::UpdateAltServiceMapping %p .wk checker failed to " + "start\n", + this)); + map->SetExpired(); + delete checker; + checker = nullptr; + } else { + // object deletes itself when done if started + LOG(("AltSvcCache::UpdateAltServiceMapping %p .wk checker started %p\n", + this, checker)); + } + } +} + +already_AddRefed AltSvcCache::GetAltServiceMapping( + const nsACString& scheme, const nsACString& host, int32_t port, + bool privateBrowsing, bool isolated, const nsACString& topWindowOrigin, + const OriginAttributes& originAttributes, bool aHttp2Allowed, + bool aHttp3Allowed) { + EnsureStorageInited(); + + bool isHTTPS; + if (NS_FAILED(SchemeIsHTTPS(scheme, isHTTPS))) { + return nullptr; + } + if (!gHttpHandler->AllowAltSvc()) { + return nullptr; + } + if (!gHttpHandler->AllowAltSvcOE() && !isHTTPS) { + return nullptr; + } + + // First look for HTTP3 + if (aHttp3Allowed) { + nsAutoCString key; + AltSvcMapping::MakeHashKey(key, scheme, host, port, privateBrowsing, + isolated, topWindowOrigin, originAttributes, + true); + RefPtr existing = LookupMapping(key, privateBrowsing); + LOG( + ("AltSvcCache::GetAltServiceMapping %p key=%s " + "existing=%p validated=%d ttl=%d", + this, key.get(), existing.get(), existing ? existing->Validated() : 0, + existing ? existing->TTL() : 0)); + if (existing && existing->Validated()) { + return existing.forget(); + } + } + + // Now look for HTTP2. + if (aHttp2Allowed) { + nsAutoCString key; + AltSvcMapping::MakeHashKey(key, scheme, host, port, privateBrowsing, + isolated, topWindowOrigin, originAttributes, + false); + RefPtr existing = LookupMapping(key, privateBrowsing); + LOG( + ("AltSvcCache::GetAltServiceMapping %p key=%s " + "existing=%p validated=%d ttl=%d", + this, key.get(), existing.get(), existing ? existing->Validated() : 0, + existing ? existing->TTL() : 0)); + if (existing && existing->Validated()) { + return existing.forget(); + } + } + + return nullptr; +} + +class ProxyClearHostMapping : public Runnable { + public: + explicit ProxyClearHostMapping(const nsACString& host, int32_t port, + const OriginAttributes& originAttributes, + const nsACString& topWindowOrigin) + : Runnable("net::ProxyClearHostMapping"), + mHost(host), + mPort(port), + mOriginAttributes(originAttributes), + mTopWindowOrigin(topWindowOrigin) {} + + NS_IMETHOD Run() override { + MOZ_ASSERT(NS_IsMainThread()); + gHttpHandler->AltServiceCache()->ClearHostMapping( + mHost, mPort, mOriginAttributes, mTopWindowOrigin); + return NS_OK; + } + + private: + nsCString mHost; + int32_t mPort; + OriginAttributes mOriginAttributes; + nsCString mTopWindowOrigin; +}; + +void AltSvcCache::ClearHostMapping(const nsACString& host, int32_t port, + const OriginAttributes& originAttributes, + const nsACString& topWindowOrigin) { + MOZ_ASSERT(XRE_IsParentProcess()); + + if (!NS_IsMainThread()) { + nsCOMPtr event = new ProxyClearHostMapping( + host, port, originAttributes, topWindowOrigin); + if (event) { + NS_DispatchToMainThread(event); + } + return; + } + nsAutoCString key; + for (int secure = 0; secure < 2; ++secure) { + constexpr auto http = "http"_ns; + constexpr auto https = "https"_ns; + const nsLiteralCString& scheme = secure ? https : http; + for (int pb = 1; pb >= 0; --pb) { + for (int isolate = 0; isolate < 2; ++isolate) { + AltSvcMapping::MakeHashKey(key, scheme, host, port, bool(pb), + bool(isolate), topWindowOrigin, + originAttributes, false); + RefPtr existing = LookupMapping(key, bool(pb)); + if (existing) { + existing->SetExpired(); + } + AltSvcMapping::MakeHashKey(key, scheme, host, port, bool(pb), + bool(isolate), topWindowOrigin, + originAttributes, true); + existing = LookupMapping(key, bool(pb)); + if (existing) { + existing->SetExpired(); + } + } + } + } +} + +void AltSvcCache::ClearHostMapping(nsHttpConnectionInfo* ci) { + if (!ci->GetOrigin().IsEmpty()) { + ClearHostMapping(ci->GetOrigin(), ci->OriginPort(), + ci->GetOriginAttributes(), ci->GetTopWindowOrigin()); + } +} + +void AltSvcCache::ClearAltServiceMappings() { + MOZ_ASSERT(NS_IsMainThread()); + if (mStorage) { + mStorage->Clear(); + } +} + +nsresult AltSvcCache::GetAltSvcCacheKeys(nsTArray& value) { + MOZ_ASSERT(NS_IsMainThread()); + if (gHttpHandler->AllowAltSvc() && mStorage) { + nsTArray items; + mStorage->GetAll(&items); + + for (const auto& item : items) { + value.AppendElement(item.key()); + } + } + return NS_OK; +} + +NS_IMETHODIMP +AltSvcOverride::GetInterface(const nsIID& iid, void** result) { + if (NS_SUCCEEDED(QueryInterface(iid, result)) && *result) { + return NS_OK; + } + + if (mCallbacks) { + return mCallbacks->GetInterface(iid, result); + } + + return NS_ERROR_NO_INTERFACE; +} + +NS_IMETHODIMP +AltSvcOverride::GetIgnoreIdle(bool* ignoreIdle) { + *ignoreIdle = true; + return NS_OK; +} + +NS_IMETHODIMP +AltSvcOverride::GetParallelSpeculativeConnectLimit( + uint32_t* parallelSpeculativeConnectLimit) { + *parallelSpeculativeConnectLimit = 32; + return NS_OK; +} + +NS_IMETHODIMP +AltSvcOverride::GetIsFromPredictor(bool* isFromPredictor) { + *isFromPredictor = false; + return NS_OK; +} + +NS_IMETHODIMP +AltSvcOverride::GetAllow1918(bool* allow) { + // normally we don't do speculative connects to 1918.. and we use + // speculative connects for the mapping validation, so override + // that default here for alt-svc + *allow = true; + return NS_OK; +} + +NS_IMPL_ISUPPORTS(AltSvcOverride, nsIInterfaceRequestor, + nsISpeculativeConnectionOverrider) + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/AlternateServices.h b/netwerk/protocol/http/AlternateServices.h new file mode 100644 index 0000000000..8288ad6095 --- /dev/null +++ b/netwerk/protocol/http/AlternateServices.h @@ -0,0 +1,277 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et 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/. */ + +/* +Alt-Svc allows separation of transport routing from the origin host without +using a proxy. See https://httpwg.github.io/http-extensions/alt-svc.html and +https://tools.ietf.org/html/draft-ietf-httpbis-alt-svc-06 + + Nice To Have Future Enhancements:: + * flush on network change event when we have an indicator + * use established https channel for http instead separate of conninfo hash + * pin via http-tls header + * clear based on origin when a random fail happens not just 421 + * upon establishment of channel, cancel and retry trans that have not yet + written anything + * persistent storage (including private browsing filter) + * memory reporter for cache, but this is rather tiny +*/ + +#ifndef mozilla_net_AlternateServices_h +#define mozilla_net_AlternateServices_h + +#include "mozilla/DataStorage.h" +#include "nsRefPtrHashtable.h" +#include "nsString.h" +#include "nsIInterfaceRequestor.h" +#include "nsIStreamListener.h" +#include "nsISpeculativeConnect.h" +#include "mozilla/BasePrincipal.h" +#include "SpeculativeTransaction.h" + +class nsILoadInfo; + +namespace mozilla { +namespace net { + +class nsProxyInfo; +class nsHttpConnectionInfo; +class nsHttpTransaction; +class nsHttpChannel; +class WellKnownChecker; + +class AltSvcMapping { + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(AltSvcMapping) + + private: // ctor from ProcessHeader + AltSvcMapping(DataStorage* storage, int32_t storageEpoch, + const nsACString& originScheme, const nsACString& originHost, + int32_t originPort, const nsACString& username, + const nsACString& topWindowOrigin, bool privateBrowsing, + bool isolated, uint32_t expiresAt, + const nsACString& alternateHost, int32_t alternatePort, + const nsACString& npnToken, + const OriginAttributes& originAttributes, bool aIsHttp3); + + public: + AltSvcMapping(DataStorage* storage, int32_t storageEpoch, + const nsCString& serialized); + + static void ProcessHeader( + const nsCString& buf, const nsCString& originScheme, + const nsCString& originHost, int32_t originPort, + const nsACString& username, const nsACString& topWindowOrigin, + bool privateBrowsing, bool isolated, nsIInterfaceRequestor* callbacks, + nsProxyInfo* proxyInfo, uint32_t caps, + const OriginAttributes& originAttributes, + bool aDontValidate = false); // aDontValidate is only used for testing! + + // AcceptableProxy() decides whether a particular proxy configuration (pi) is + // suitable for use with Alt-Svc. No proxy (including a null pi) is suitable. + static bool AcceptableProxy(nsProxyInfo* pi); + + const nsCString& AlternateHost() const { return mAlternateHost; } + const nsCString& OriginHost() const { return mOriginHost; } + uint32_t OriginPort() const { return mOriginPort; } + const nsCString& HashKey() const { return mHashKey; } + uint32_t AlternatePort() const { return mAlternatePort; } + bool Validated() { return mValidated; } + int32_t GetExpiresAt() { return mExpiresAt; } + bool RouteEquals(AltSvcMapping* map); + bool HTTPS() { return mHttps; } + + void GetConnectionInfo(nsHttpConnectionInfo** outCI, nsProxyInfo* pi, + const OriginAttributes& originAttributes); + + int32_t TTL(); + int32_t StorageEpoch() { return mStorageEpoch; } + bool Private() { return mPrivate; } + bool Isolated() { return mIsolated; } + + void SetValidated(bool val); + void SetMixedScheme(bool val); + void SetExpiresAt(int32_t val); + void SetExpired(); + void Sync(); + void SetSyncOnlyOnSuccess(bool aSOOS) { mSyncOnlyOnSuccess = aSOOS; } + + static void MakeHashKey(nsCString& outKey, const nsACString& originScheme, + const nsACString& originHost, int32_t originPort, + bool privateBrowsing, bool isolated, + const nsACString& topWindowOrigin, + const OriginAttributes& originAttributes, + bool aIsHttp3); + + bool IsHttp3() { return mIsHttp3; } + const nsCString& NPNToken() const { return mNPNToken; } + + private: + virtual ~AltSvcMapping() = default; + void SyncString(const nsCString& val); + RefPtr mStorage; + int32_t mStorageEpoch; + void Serialize(nsCString& out); + + nsCString mHashKey; + + // If you change any of these members, update Serialize() + nsCString mAlternateHost; + MOZ_INIT_OUTSIDE_CTOR int32_t mAlternatePort; + + nsCString mOriginHost; + MOZ_INIT_OUTSIDE_CTOR int32_t mOriginPort; + + nsCString mUsername; + nsCString mTopWindowOrigin; + MOZ_INIT_OUTSIDE_CTOR bool mPrivate; + MOZ_INIT_OUTSIDE_CTOR bool mIsolated; + + MOZ_INIT_OUTSIDE_CTOR uint32_t mExpiresAt; // alt-svc mappping + + MOZ_INIT_OUTSIDE_CTOR bool mValidated; + MOZ_INIT_OUTSIDE_CTOR bool mHttps; // origin is https:// + MOZ_INIT_OUTSIDE_CTOR bool + mMixedScheme; // .wk allows http and https on same con + + nsCString mNPNToken; + + OriginAttributes mOriginAttributes; + + bool mSyncOnlyOnSuccess; + bool mIsHttp3; +}; + +class AltSvcOverride : public nsIInterfaceRequestor, + public nsISpeculativeConnectionOverrider { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSISPECULATIVECONNECTIONOVERRIDER + NS_DECL_NSIINTERFACEREQUESTOR + + explicit AltSvcOverride(nsIInterfaceRequestor* aRequestor) + : mCallbacks(aRequestor) {} + + private: + virtual ~AltSvcOverride() = default; + nsCOMPtr mCallbacks; +}; + +class TransactionObserver final : public nsIStreamListener { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSISTREAMLISTENER + NS_DECL_NSIREQUESTOBSERVER + + TransactionObserver(nsHttpChannel* channel, WellKnownChecker* checker); + void Complete(bool versionOK, bool authOK, nsresult reason); + + private: + friend class WellKnownChecker; + virtual ~TransactionObserver() = default; + + nsCOMPtr mChannelRef; + nsHttpChannel* mChannel; + WellKnownChecker* mChecker; + nsCString mWKResponse; + + bool mRanOnce; + bool mStatusOK; // HTTP Status 200 + // These two values could be accessed on sts thread. + Atomic mAuthOK; // confirmed no TLS failure + Atomic mVersionOK; // connection h2 +}; + +class AltSvcCache { + public: + AltSvcCache() : mStorageEpoch(0) {} + virtual ~AltSvcCache() = default; + void UpdateAltServiceMapping( + AltSvcMapping* map, nsProxyInfo* pi, nsIInterfaceRequestor*, + uint32_t caps, + const OriginAttributes& originAttributes); // main thread + void UpdateAltServiceMappingWithoutValidation( + AltSvcMapping* map, nsProxyInfo* pi, nsIInterfaceRequestor*, + uint32_t caps, + const OriginAttributes& originAttributes); // main thread + already_AddRefed GetAltServiceMapping( + const nsACString& scheme, const nsACString& host, int32_t port, bool pb, + bool isolated, const nsACString& topWindowOrigin, + const OriginAttributes& originAttributes, bool aHttp2Allowed, + bool aHttp3Allowed); + void ClearAltServiceMappings(); + void ClearHostMapping(const nsACString& host, int32_t port, + const OriginAttributes& originAttributes, + const nsACString& topWindowOrigin); + void ClearHostMapping(nsHttpConnectionInfo* ci); + DataStorage* GetStoragePtr() { return mStorage.get(); } + int32_t StorageEpoch() { return mStorageEpoch; } + nsresult GetAltSvcCacheKeys(nsTArray& value); + + private: + void EnsureStorageInited(); + already_AddRefed LookupMapping(const nsCString& key, + bool privateBrowsing); + RefPtr mStorage; + int32_t mStorageEpoch; +}; + +// This class is used to write the validated result to AltSvcMapping when the +// AltSvcTransaction is closed and destroyed. +class AltSvcMappingValidator final { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(AltSvcMappingValidator) + + explicit AltSvcMappingValidator(AltSvcMapping* aMap); + + void OnTransactionDestroy(bool aValidateResult); + + void OnTransactionClose(bool aValidateResult); + + protected: + virtual ~AltSvcMappingValidator() = default; + + RefPtr mMapping; +}; + +// This is the asynchronous null transaction used to validate +// an alt-svc advertisement only for https:// +// When http over socket process is enabled, this class should live only in +// socket process. +template +class AltSvcTransaction final : public SpeculativeTransaction { + public: + AltSvcTransaction(nsHttpConnectionInfo* ci, nsIInterfaceRequestor* callbacks, + uint32_t caps, Validator* aValidator, bool aIsHttp3); + + ~AltSvcTransaction() override; + + // AltSvcTransaction is used to validate the alt-svc record, so we don't want + // to fetch HTTPS RR for this. + virtual nsresult FetchHTTPSRR() override { return NS_ERROR_NOT_IMPLEMENTED; } + + private: + // check on alternate route. + // also evaluate 'reasonable assurances' for opportunistic security + bool MaybeValidate(nsresult reason); + + public: + void Close(nsresult reason) override; + nsresult ReadSegments(nsAHttpSegmentReader* reader, uint32_t count, + uint32_t* countRead) override; + + private: + RefPtr mValidator; + uint32_t mIsHttp3 : 1; + uint32_t mRunning : 1; + uint32_t mTriedToValidate : 1; + uint32_t mTriedToWrite : 1; + uint32_t mValidatedResult : 1; +}; + +} // namespace net +} // namespace mozilla + +#endif // include guard diff --git a/netwerk/protocol/http/BackgroundChannelRegistrar.cpp b/netwerk/protocol/http/BackgroundChannelRegistrar.cpp new file mode 100644 index 0000000000..c78ba43fc4 --- /dev/null +++ b/netwerk/protocol/http/BackgroundChannelRegistrar.cpp @@ -0,0 +1,103 @@ +/* -*- 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 "BackgroundChannelRegistrar.h" + +#include "HttpBackgroundChannelParent.h" +#include "HttpChannelParent.h" +#include "nsXULAppAPI.h" +#include "mozilla/StaticPtr.h" + +namespace { +mozilla::StaticRefPtr gSingleton; +} + +namespace mozilla { +namespace net { + +NS_IMPL_ISUPPORTS(BackgroundChannelRegistrar, nsIBackgroundChannelRegistrar) + +BackgroundChannelRegistrar::BackgroundChannelRegistrar() { + // BackgroundChannelRegistrar is a main-thread-only object. + // All the operations should be run on main thread. + // It should be used on chrome process only. + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(NS_IsMainThread()); +} + +BackgroundChannelRegistrar::~BackgroundChannelRegistrar() { + MOZ_ASSERT(NS_IsMainThread()); +} + +// static +already_AddRefed +BackgroundChannelRegistrar::GetOrCreate() { + if (!gSingleton) { + gSingleton = new BackgroundChannelRegistrar(); + } + return do_AddRef(gSingleton); +} + +// static +void BackgroundChannelRegistrar::Shutdown() { + MOZ_ASSERT(NS_IsMainThread()); + gSingleton = nullptr; +} + +void BackgroundChannelRegistrar::NotifyChannelLinked( + HttpChannelParent* aChannelParent, HttpBackgroundChannelParent* aBgParent) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aChannelParent); + MOZ_ASSERT(aBgParent); + + aBgParent->LinkToChannel(aChannelParent); + aChannelParent->OnBackgroundParentReady(aBgParent); +} + +// nsIBackgroundChannelRegistrar +void BackgroundChannelRegistrar::DeleteChannel(uint64_t aKey) { + MOZ_ASSERT(NS_IsMainThread()); + + mChannels.Remove(aKey); + mBgChannels.Remove(aKey); +} + +void BackgroundChannelRegistrar::LinkHttpChannel(uint64_t aKey, + HttpChannelParent* aChannel) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aChannel); + + RefPtr bgParent; + bool found = mBgChannels.Remove(aKey, getter_AddRefs(bgParent)); + + if (!found) { + mChannels.Put(aKey, RefPtr{aChannel}); + return; + } + + MOZ_ASSERT(bgParent); + NotifyChannelLinked(aChannel, bgParent); +} + +void BackgroundChannelRegistrar::LinkBackgroundChannel( + uint64_t aKey, HttpBackgroundChannelParent* aBgChannel) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aBgChannel); + + RefPtr parent; + bool found = mChannels.Remove(aKey, getter_AddRefs(parent)); + + if (!found) { + mBgChannels.Put(aKey, RefPtr{aBgChannel}); + return; + } + + MOZ_ASSERT(parent); + NotifyChannelLinked(parent, aBgChannel); +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/BackgroundChannelRegistrar.h b/netwerk/protocol/http/BackgroundChannelRegistrar.h new file mode 100644 index 0000000000..fe6ba88ac4 --- /dev/null +++ b/netwerk/protocol/http/BackgroundChannelRegistrar.h @@ -0,0 +1,57 @@ +/* -*- 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/. */ + +#ifndef mozilla_net_BackgroundChannelRegistrar_h__ +#define mozilla_net_BackgroundChannelRegistrar_h__ + +#include "nsIBackgroundChannelRegistrar.h" +#include "nsRefPtrHashtable.h" +#include "mozilla/AlreadyAddRefed.h" + +namespace mozilla { +namespace net { + +class HttpBackgroundChannelParent; +class HttpChannelParent; + +class BackgroundChannelRegistrar final : public nsIBackgroundChannelRegistrar { + typedef nsRefPtrHashtable + ChannelHashtable; + typedef nsRefPtrHashtable + BackgroundChannelHashtable; + + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIBACKGROUNDCHANNELREGISTRAR + + explicit BackgroundChannelRegistrar(); + + // Singleton accessor + static already_AddRefed GetOrCreate(); + + static void Shutdown(); + + private: + virtual ~BackgroundChannelRegistrar(); + + // A helper function for BackgroundChannelRegistrar itself to callback + // HttpChannelParent and HttpBackgroundChannelParent when both objects are + // ready. aChannelParent and aBgParent is the pair of HttpChannelParent and + // HttpBackgroundChannelParent that should be linked together. + void NotifyChannelLinked(HttpChannelParent* aChannelParent, + HttpBackgroundChannelParent* aBgParent); + + // Store unlinked HttpChannelParent objects. + ChannelHashtable mChannels; + + // Store unlinked HttpBackgroundChannelParent objects. + BackgroundChannelHashtable mBgChannels; +}; + +} // namespace net +} // namespace mozilla + +#endif // mozilla_net_BackgroundChannelRegistrar_h__ diff --git a/netwerk/protocol/http/BackgroundDataBridgeChild.cpp b/netwerk/protocol/http/BackgroundDataBridgeChild.cpp new file mode 100644 index 0000000000..3bd302273f --- /dev/null +++ b/netwerk/protocol/http/BackgroundDataBridgeChild.cpp @@ -0,0 +1,58 @@ +/* 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/net/BackgroundDataBridgeChild.h" +#include "mozilla/net/HttpBackgroundChannelChild.h" + +namespace mozilla { +namespace net { + +BackgroundDataBridgeChild::BackgroundDataBridgeChild( + HttpBackgroundChannelChild* aBgChild) + : mBgChild(aBgChild) { + MOZ_ASSERT(aBgChild); +} + +BackgroundDataBridgeChild::~BackgroundDataBridgeChild() = default; + +void BackgroundDataBridgeChild::ActorDestroy(ActorDestroyReason aWhy) { + mBgChild = nullptr; +} + +mozilla::ipc::IPCResult BackgroundDataBridgeChild::RecvOnTransportAndData( + const uint64_t& offset, const uint32_t& count, + const nsDependentCSubstring& data) { + if (!mBgChild) { + return IPC_OK(); + } + + if (mBgChild->ChannelClosed()) { + Unused << Send__delete__(this); + return IPC_OK(); + } + + return mBgChild->RecvOnTransportAndData(NS_OK, NS_NET_STATUS_RECEIVING_FROM, + offset, count, data, true); +} + +mozilla::ipc::IPCResult BackgroundDataBridgeChild::RecvOnStopRequest( + nsresult aStatus, const ResourceTimingStructArgs& aTiming, + const TimeStamp& aLastActiveTabOptHit, + const nsHttpHeaderArray& aResponseTrailers) { + if (!mBgChild) { + return IPC_OK(); + } + + if (mBgChild->ChannelClosed()) { + Unused << Send__delete__(this); + return IPC_OK(); + } + + return mBgChild->RecvOnStopRequest(aStatus, aTiming, aLastActiveTabOptHit, + aResponseTrailers, + nsTArray(), true); +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/BackgroundDataBridgeChild.h b/netwerk/protocol/http/BackgroundDataBridgeChild.h new file mode 100644 index 0000000000..42df66ad68 --- /dev/null +++ b/netwerk/protocol/http/BackgroundDataBridgeChild.h @@ -0,0 +1,40 @@ +/* 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/. */ + +#ifndef mozilla_net_BackgroundDataBridgeChild_h +#define mozilla_net_BackgroundDataBridgeChild_h + +#include "mozilla/net/PBackgroundDataBridgeChild.h" +#include "mozilla/ipc/BackgroundChild.h" + +namespace mozilla { +namespace net { + +class HttpBackgroundChannelChild; + +class BackgroundDataBridgeChild final : public PBackgroundDataBridgeChild { + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(BackgroundDataBridgeChild, override) + + explicit BackgroundDataBridgeChild(HttpBackgroundChannelChild* aBgChild); + + protected: + virtual ~BackgroundDataBridgeChild(); + void ActorDestroy(ActorDestroyReason aWhy) override; + + RefPtr mBgChild; + + public: + mozilla::ipc::IPCResult RecvOnTransportAndData( + const uint64_t& offset, const uint32_t& count, + const nsDependentCSubstring& data); + mozilla::ipc::IPCResult RecvOnStopRequest( + nsresult aStatus, const ResourceTimingStructArgs& aTiming, + const TimeStamp& aLastActiveTabOptHit, + const nsHttpHeaderArray& aResponseTrailers); +}; + +} // namespace net +} // namespace mozilla + +#endif // mozilla_net_BackgroundDataBridgeChild_h diff --git a/netwerk/protocol/http/BackgroundDataBridgeParent.cpp b/netwerk/protocol/http/BackgroundDataBridgeParent.cpp new file mode 100644 index 0000000000..195af7b1c1 --- /dev/null +++ b/netwerk/protocol/http/BackgroundDataBridgeParent.cpp @@ -0,0 +1,57 @@ +/* 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/net/BackgroundDataBridgeParent.h" +#include "mozilla/net/SocketProcessChild.h" +#include "mozilla/Unused.h" + +namespace mozilla { +namespace net { + +BackgroundDataBridgeParent::BackgroundDataBridgeParent(uint64_t aChannelID) + : mChannelID(aChannelID), mBackgroundThread(NS_GetCurrentThread()) { + SocketProcessChild::GetSingleton()->AddDataBridgeToMap(aChannelID, this); +} + +void BackgroundDataBridgeParent::ActorDestroy(ActorDestroyReason aWhy) { + SocketProcessChild::GetSingleton()->RemoveDataBridgeFromMap(mChannelID); +} + +already_AddRefed BackgroundDataBridgeParent::GetBackgroundThread() { + nsCOMPtr thread = mBackgroundThread; + return thread.forget(); +} + +void BackgroundDataBridgeParent::Destroy() { + RefPtr self = this; + MOZ_ALWAYS_SUCCEEDS(mBackgroundThread->Dispatch( + NS_NewRunnableFunction("BackgroundDataBridgeParent::Destroy", + [self]() { + if (self->CanSend()) { + Unused << self->Send__delete__(self); + } + }), + NS_DISPATCH_NORMAL)); +} + +void BackgroundDataBridgeParent::OnStopRequest( + nsresult aStatus, const ResourceTimingStructArgs& aTiming, + const TimeStamp& aLastActiveTabOptHit, + const nsHttpHeaderArray& aResponseTrailers) { + RefPtr self = this; + MOZ_ALWAYS_SUCCEEDS(mBackgroundThread->Dispatch( + NS_NewRunnableFunction( + "BackgroundDataBridgeParent::OnStopRequest", + [self, aStatus, aTiming, aLastActiveTabOptHit, aResponseTrailers]() { + if (self->CanSend()) { + Unused << self->SendOnStopRequest( + aStatus, aTiming, aLastActiveTabOptHit, aResponseTrailers); + Unused << self->Send__delete__(self); + } + }), + NS_DISPATCH_NORMAL)); +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/BackgroundDataBridgeParent.h b/netwerk/protocol/http/BackgroundDataBridgeParent.h new file mode 100644 index 0000000000..15fd4ca4ec --- /dev/null +++ b/netwerk/protocol/http/BackgroundDataBridgeParent.h @@ -0,0 +1,35 @@ +/* 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/. */ + +#ifndef mozilla_net_BackgroundDataBridgeParent_h +#define mozilla_net_BackgroundDataBridgeParent_h + +#include "mozilla/net/PBackgroundDataBridgeParent.h" + +namespace mozilla { +namespace net { + +class BackgroundDataBridgeParent final : public PBackgroundDataBridgeParent { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(BackgroundDataBridgeParent, override) + + explicit BackgroundDataBridgeParent(uint64_t aChannelID); + void ActorDestroy(ActorDestroyReason aWhy) override; + already_AddRefed GetBackgroundThread(); + void Destroy(); + void OnStopRequest(nsresult aStatus, const ResourceTimingStructArgs& aTiming, + const TimeStamp& aLastActiveTabOptHit, + const nsHttpHeaderArray& aResponseTrailers); + + private: + virtual ~BackgroundDataBridgeParent() = default; + + uint64_t mChannelID; + nsCOMPtr mBackgroundThread; +}; + +} // namespace net +} // namespace mozilla + +#endif // mozilla_net_BackgroundDataBridgeParent_h diff --git a/netwerk/protocol/http/CacheControlParser.cpp b/netwerk/protocol/http/CacheControlParser.cpp new file mode 100644 index 0000000000..00d5b42d4f --- /dev/null +++ b/netwerk/protocol/http/CacheControlParser.cpp @@ -0,0 +1,135 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et 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 "CacheControlParser.h" + +namespace mozilla { +namespace net { + +CacheControlParser::CacheControlParser(nsACString const& aHeader) + : Tokenizer(aHeader, nullptr, "-_"), + mMaxAgeSet(false), + mMaxAge(0), + mMaxStaleSet(false), + mMaxStale(0), + mMinFreshSet(false), + mMinFresh(0), + mStaleWhileRevalidateSet(false), + mStaleWhileRevalidate(0), + mNoCache(false), + mNoStore(false), + mPublic(false), + mPrivate(false), + mImmutable(false) { + SkipWhites(); + if (!CheckEOF()) { + Directive(); + } +} + +void CacheControlParser::Directive() { + if (CheckWord("no-cache")) { + mNoCache = true; + IgnoreDirective(); // ignore any optionally added values + } else if (CheckWord("no-store")) { + mNoStore = true; + } else if (CheckWord("max-age")) { + mMaxAgeSet = SecondsValue(&mMaxAge); + } else if (CheckWord("max-stale")) { + mMaxStaleSet = SecondsValue(&mMaxStale, PR_UINT32_MAX); + } else if (CheckWord("min-fresh")) { + mMinFreshSet = SecondsValue(&mMinFresh); + } else if (CheckWord("stale-while-revalidate")) { + mStaleWhileRevalidateSet = SecondsValue(&mStaleWhileRevalidate); + } else if (CheckWord("public")) { + mPublic = true; + } else if (CheckWord("private")) { + mPrivate = true; + } else if (CheckWord("immutable")) { + mImmutable = true; + } else { + IgnoreDirective(); + } + + SkipWhites(); + if (CheckEOF()) { + return; + } + if (CheckChar(',')) { + SkipWhites(); + Directive(); + return; + } + + NS_WARNING("Unexpected input in Cache-control header value"); +} + +bool CacheControlParser::SecondsValue(uint32_t* seconds, uint32_t defaultVal) { + SkipWhites(); + if (!CheckChar('=')) { + *seconds = defaultVal; + return !!defaultVal; + } + + SkipWhites(); + if (!ReadInteger(seconds)) { + NS_WARNING("Unexpected value in Cache-control header value"); + return false; + } + + return true; +} + +void CacheControlParser::IgnoreDirective() { + Token t; + while (Next(t)) { + if (t.Equals(Token::Char(',')) || t.Equals(Token::EndOfFile())) { + Rollback(); + break; + } + if (t.Equals(Token::Char('"'))) { + SkipUntil(Token::Char('"')); + if (!CheckChar('"')) { + NS_WARNING( + "Missing quoted string expansion in Cache-control header value"); + break; + } + } + } +} + +bool CacheControlParser::MaxAge(uint32_t* seconds) { + *seconds = mMaxAge; + return mMaxAgeSet; +} + +bool CacheControlParser::MaxStale(uint32_t* seconds) { + *seconds = mMaxStale; + return mMaxStaleSet; +} + +bool CacheControlParser::MinFresh(uint32_t* seconds) { + *seconds = mMinFresh; + return mMinFreshSet; +} + +bool CacheControlParser::StaleWhileRevalidate(uint32_t* seconds) { + *seconds = mStaleWhileRevalidate; + return mStaleWhileRevalidateSet; +} + +bool CacheControlParser::NoCache() { return mNoCache; } + +bool CacheControlParser::NoStore() { return mNoStore; } + +bool CacheControlParser::Public() { return mPublic; } + +bool CacheControlParser::Private() { return mPrivate; } + +bool CacheControlParser::Immutable() { return mImmutable; } + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/CacheControlParser.h b/netwerk/protocol/http/CacheControlParser.h new file mode 100644 index 0000000000..6a6588be0c --- /dev/null +++ b/netwerk/protocol/http/CacheControlParser.h @@ -0,0 +1,52 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et 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/. */ + +#ifndef CacheControlParser_h__ +#define CacheControlParser_h__ + +#include "mozilla/Tokenizer.h" + +namespace mozilla { +namespace net { + +class CacheControlParser final : Tokenizer { + public: + explicit CacheControlParser(nsACString const& header); + + [[nodiscard]] bool MaxAge(uint32_t* seconds); + [[nodiscard]] bool MaxStale(uint32_t* seconds); + [[nodiscard]] bool MinFresh(uint32_t* seconds); + [[nodiscard]] bool StaleWhileRevalidate(uint32_t* seconds); + bool NoCache(); + bool NoStore(); + bool Public(); + bool Private(); + bool Immutable(); + + private: + void Directive(); + void IgnoreDirective(); + [[nodiscard]] bool SecondsValue(uint32_t* seconds, uint32_t defaultVal = 0); + + bool mMaxAgeSet; + uint32_t mMaxAge; + bool mMaxStaleSet; + uint32_t mMaxStale; + bool mMinFreshSet; + uint32_t mMinFresh; + bool mStaleWhileRevalidateSet; + uint32_t mStaleWhileRevalidate; + bool mNoCache; + bool mNoStore; + bool mPublic; + bool mPrivate; + bool mImmutable; +}; + +} // namespace net +} // namespace mozilla + +#endif diff --git a/netwerk/protocol/http/CachePushChecker.cpp b/netwerk/protocol/http/CachePushChecker.cpp new file mode 100644 index 0000000000..11dd509846 --- /dev/null +++ b/netwerk/protocol/http/CachePushChecker.cpp @@ -0,0 +1,249 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et 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 "CachePushChecker.h" + +#include "LoadContextInfo.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/net/SocketProcessChild.h" +#include "nsICacheEntry.h" +#include "nsICacheStorageService.h" +#include "nsICacheStorage.h" +#include "nsThreadUtils.h" + +namespace mozilla { +namespace net { + +NS_IMPL_ISUPPORTS(CachePushChecker, nsICacheEntryOpenCallback); + +CachePushChecker::CachePushChecker(nsIURI* aPushedURL, + const OriginAttributes& aOriginAttributes, + const nsACString& aRequestString, + std::function&& aCallback) + : mPushedURL(aPushedURL), + mOriginAttributes(aOriginAttributes), + mRequestString(aRequestString), + mCallback(std::move(aCallback)), + mCurrentEventTarget(GetCurrentEventTarget()) {} + +nsresult CachePushChecker::DoCheck() { + if (XRE_IsSocketProcess()) { + RefPtr self = this; + return NS_DispatchToMainThread( + NS_NewRunnableFunction( + "CachePushChecker::DoCheck", + [self]() { + if (SocketProcessChild* child = + SocketProcessChild::GetSingleton()) { + child + ->SendCachePushCheck(self->mPushedURL, + self->mOriginAttributes, + self->mRequestString) + ->Then( + GetCurrentSerialEventTarget(), __func__, + [self](bool aResult) { self->InvokeCallback(aResult); }, + [](const mozilla::ipc::ResponseRejectReason) {}); + } + }), + NS_DISPATCH_NORMAL); + } + + nsresult rv; + nsCOMPtr css = + do_GetService("@mozilla.org/netwerk/cache-storage-service;1", &rv); + if (NS_FAILED(rv)) { + return rv; + } + + RefPtr lci = GetLoadContextInfo(false, mOriginAttributes); + nsCOMPtr ds; + rv = css->DiskCacheStorage(lci, false, getter_AddRefs(ds)); + if (NS_FAILED(rv)) { + return rv; + } + + return ds->AsyncOpenURI( + mPushedURL, ""_ns, + nsICacheStorage::OPEN_READONLY | nsICacheStorage::OPEN_SECRETLY, this); +} + +NS_IMETHODIMP +CachePushChecker::OnCacheEntryCheck(nsICacheEntry* entry, + nsIApplicationCache* appCache, + uint32_t* result) { + MOZ_ASSERT(XRE_IsParentProcess()); + + // We never care to fully open the entry, since we won't actually use it. + // We just want to be able to do all our checks to see if a future channel can + // use this entry, or if we need to accept the push. + *result = nsICacheEntryOpenCallback::ENTRY_NOT_WANTED; + + bool isForcedValid = false; + entry->GetIsForcedValid(&isForcedValid); + + nsHttpRequestHead requestHead; + requestHead.ParseHeaderSet(mRequestString.BeginReading()); + nsHttpResponseHead cachedResponseHead; + bool acceptPush = true; + auto onExitGuard = MakeScopeExit([&] { InvokeCallback(acceptPush); }); + + nsresult rv = + nsHttp::GetHttpResponseHeadFromCacheEntry(entry, &cachedResponseHead); + if (NS_FAILED(rv)) { + // Couldn't make sense of what's in the cache entry, go ahead and accept + // the push. + return NS_OK; + } + + if ((cachedResponseHead.Status() / 100) != 2) { + // Assume the push is sending us a success, while we don't have one in the + // cache, so we'll accept the push. + return NS_OK; + } + + // Get the method that was used to generate the cached response + nsCString buf; + rv = entry->GetMetaDataElement("request-method", getter_Copies(buf)); + if (NS_FAILED(rv)) { + // Can't check request method, accept the push + return NS_OK; + } + nsAutoCString pushedMethod; + requestHead.Method(pushedMethod); + if (!buf.Equals(pushedMethod)) { + // Methods don't match, accept the push + return NS_OK; + } + + int64_t size, contentLength; + rv = nsHttp::CheckPartial(entry, &size, &contentLength, &cachedResponseHead); + if (NS_FAILED(rv)) { + // Couldn't figure out if this was partial or not, accept the push. + return NS_OK; + } + + if (size == int64_t(-1) || contentLength != size) { + // This is partial content in the cache, accept the push. + return NS_OK; + } + + nsAutoCString requestedETag; + if (NS_FAILED(requestHead.GetHeader(nsHttp::If_Match, requestedETag))) { + // Can't check etag + return NS_OK; + } + if (!requestedETag.IsEmpty()) { + nsAutoCString cachedETag; + if (NS_FAILED(cachedResponseHead.GetHeader(nsHttp::ETag, cachedETag))) { + // Can't check etag + return NS_OK; + } + if (!requestedETag.Equals(cachedETag)) { + // ETags don't match, accept the push. + return NS_OK; + } + } + + nsAutoCString imsString; + Unused << requestHead.GetHeader(nsHttp::If_Modified_Since, imsString); + if (!buf.IsEmpty()) { + uint32_t ims = buf.ToInteger(&rv); + uint32_t lm; + rv = cachedResponseHead.GetLastModifiedValue(&lm); + if (NS_SUCCEEDED(rv) && lm && lm < ims) { + // The push appears to be newer than what's in our cache, accept it. + return NS_OK; + } + } + + nsAutoCString cacheControlRequestHeader; + Unused << requestHead.GetHeader(nsHttp::Cache_Control, + cacheControlRequestHeader); + CacheControlParser cacheControlRequest(cacheControlRequestHeader); + if (cacheControlRequest.NoStore()) { + // Don't use a no-store cache entry, accept the push. + return NS_OK; + } + + nsCString cachedAuth; + rv = entry->GetMetaDataElement("auth", getter_Copies(cachedAuth)); + if (NS_SUCCEEDED(rv)) { + uint32_t lastModifiedTime; + rv = entry->GetLastModified(&lastModifiedTime); + if (NS_SUCCEEDED(rv)) { + if ((gHttpHandler->SessionStartTime() > lastModifiedTime) && + !cachedAuth.IsEmpty()) { + // Need to revalidate this, as the auth is old. Accept the push. + return NS_OK; + } + + if (cachedAuth.IsEmpty() && + requestHead.HasHeader(nsHttp::Authorization)) { + // They're pushing us something with auth, but we didn't cache anything + // with auth. Accept the push. + return NS_OK; + } + } + } + + bool weaklyFramed, isImmutable; + nsHttp::DetermineFramingAndImmutability(entry, &cachedResponseHead, true, + &weaklyFramed, &isImmutable); + + // We'll need this value in later computations... + uint32_t lastModifiedTime; + rv = entry->GetLastModified(&lastModifiedTime); + if (NS_FAILED(rv)) { + // Ugh, this really sucks. OK, accept the push. + return NS_OK; + } + + // Determine if this is the first time that this cache entry + // has been accessed during this session. + bool fromPreviousSession = + (gHttpHandler->SessionStartTime() > lastModifiedTime); + + bool validationRequired = nsHttp::ValidationRequired( + isForcedValid, &cachedResponseHead, 0 /*NWGH: ??? - loadFlags*/, false, + isImmutable, false, requestHead, entry, cacheControlRequest, + fromPreviousSession); + + if (validationRequired) { + // A real channel would most likely hit the net at this point, so let's + // accept the push. + return NS_OK; + } + + // If we get here, then we would be able to use this cache entry. Cancel the + // push so as not to waste any more bandwidth. + acceptPush = false; + return NS_OK; +} + +NS_IMETHODIMP +CachePushChecker::OnCacheEntryAvailable(nsICacheEntry* entry, bool isNew, + nsIApplicationCache* appCache, + nsresult result) { + // Nothing to do here, all the work is in OnCacheEntryCheck. + return NS_OK; +} + +void CachePushChecker::InvokeCallback(bool aResult) { + RefPtr self = this; + auto task = [self, aResult]() { self->mCallback(aResult); }; + if (!mCurrentEventTarget->IsOnCurrentThread()) { + mCurrentEventTarget->Dispatch( + NS_NewRunnableFunction("CachePushChecker::InvokeCallback", + std::move(task)), + NS_DISPATCH_NORMAL); + return; + } + + task(); +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/CachePushChecker.h b/netwerk/protocol/http/CachePushChecker.h new file mode 100644 index 0000000000..72855443a0 --- /dev/null +++ b/netwerk/protocol/http/CachePushChecker.h @@ -0,0 +1,45 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et 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/. */ + +#ifndef CachePushChecker_h__ +#define CachePushChecker_h__ + +#include +#include "mozilla/OriginAttributes.h" +#include "nsICacheEntryOpenCallback.h" +#include "nsIEventTarget.h" +#include "nsString.h" +#include "nsIURI.h" + +namespace mozilla { +namespace net { + +class CachePushChecker final : public nsICacheEntryOpenCallback { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSICACHEENTRYOPENCALLBACK + + explicit CachePushChecker(nsIURI* aPushedURL, + const OriginAttributes& aOriginAttributes, + const nsACString& aRequestString, + std::function&& aCallback); + nsresult DoCheck(); + + private: + ~CachePushChecker() = default; + void InvokeCallback(bool aResult); + + nsCOMPtr mPushedURL; + OriginAttributes mOriginAttributes; + nsCString mRequestString; + std::function mCallback; + nsCOMPtr mCurrentEventTarget; +}; + +} // namespace net +} // namespace mozilla + +#endif diff --git a/netwerk/protocol/http/ClassifierDummyChannel.cpp b/netwerk/protocol/http/ClassifierDummyChannel.cpp new file mode 100644 index 0000000000..8306c43936 --- /dev/null +++ b/netwerk/protocol/http/ClassifierDummyChannel.cpp @@ -0,0 +1,775 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et 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 "ClassifierDummyChannel.h" + +#include "mozilla/ContentBlocking.h" +#include "mozilla/net/ClassifierDummyChannelChild.h" +#include "mozilla/net/UrlClassifierCommon.h" +#include "mozilla/dom/ContentChild.h" +#include "mozilla/Preferences.h" +#include "mozilla/StaticPrefs_privacy.h" +#include "nsContentSecurityManager.h" +#include "nsIChannel.h" +#include "nsIURI.h" +#include "nsProxyRelease.h" +#include "nsQueryObject.h" + +namespace mozilla { +namespace net { + +/* static */ ClassifierDummyChannel::StorageAllowedState +ClassifierDummyChannel::StorageAllowed( + nsIChannel* aChannel, const std::function& aCallback) { + MOZ_ASSERT(!XRE_IsParentProcess()); + + nsCOMPtr httpChannel = do_QueryInterface(aChannel); + if (!httpChannel) { + // Any non-http channel is allowed. + return eStorageGranted; + } + + if (!nsContentUtils::IsNonSubresourceRequest(aChannel)) { + // If this is a sub-resource, we don't need to check if the channel is + // annotated as tracker because: + // - if the SW doesn't respond, or it sends us back to the network, the + // channel will be annotated on the parent process. + // - if the SW answers, the content is taken from the cache, which is + // considered trusted. + return eStorageGranted; + } + + nsCOMPtr uri; + aChannel->GetURI(getter_AddRefs(uri)); + + if (StaticPrefs::privacy_trackingprotection_annotate_channels()) { + dom::ContentChild* cc = + static_cast(gNeckoChild->Manager()); + if (cc->IsShuttingDown()) { + return eStorageDenied; + } + + if (!ClassifierDummyChannelChild::Create(httpChannel, uri, aCallback)) { + return eStorageDenied; + } + + return eAsyncNeeded; + } + + if (ContentBlocking::ShouldAllowAccessFor(httpChannel, uri, nullptr)) { + return eStorageGranted; + } + + return eStorageDenied; +} + +NS_IMPL_ADDREF(ClassifierDummyChannel) +NS_IMPL_RELEASE(ClassifierDummyChannel) + +NS_INTERFACE_MAP_BEGIN(ClassifierDummyChannel) + NS_INTERFACE_MAP_ENTRY(nsIRequest) + NS_INTERFACE_MAP_ENTRY(nsIChannel) + NS_INTERFACE_MAP_ENTRY(nsIHttpChannelInternal) + NS_INTERFACE_MAP_ENTRY(nsIClassifiedChannel) + NS_INTERFACE_MAP_ENTRY_CONCRETE(ClassifierDummyChannel) +NS_INTERFACE_MAP_END + +ClassifierDummyChannel::ClassifierDummyChannel(nsIURI* aURI, + nsIURI* aTopWindowURI, + nsresult aTopWindowURIResult, + nsILoadInfo* aLoadInfo) + : mTopWindowURI(aTopWindowURI), + mTopWindowURIResult(aTopWindowURIResult), + mFirstPartyClassificationFlags(0), + mThirdPartyClassificationFlags(0) { + MOZ_ASSERT(XRE_IsParentProcess()); + + SetOriginalURI(aURI); + SetLoadInfo(aLoadInfo); +} + +ClassifierDummyChannel::~ClassifierDummyChannel() { + NS_ReleaseOnMainThread("ClassifierDummyChannel::mLoadInfo", + mLoadInfo.forget()); + NS_ReleaseOnMainThread("ClassifierDummyChannel::mURI", mURI.forget()); + NS_ReleaseOnMainThread("ClassifierDummyChannel::mTopWindowURI", + mTopWindowURI.forget()); +} + +void ClassifierDummyChannel::AddClassificationFlags( + uint32_t aClassificationFlags, bool aThirdParty) { + if (aThirdParty) { + mThirdPartyClassificationFlags |= aClassificationFlags; + } else { + mFirstPartyClassificationFlags |= aClassificationFlags; + } +} + +//----------------------------------------------------------------------------- +// ClassifierDummyChannel::nsIChannel +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +ClassifierDummyChannel::GetOriginalURI(nsIURI** aOriginalURI) { + NS_IF_ADDREF(*aOriginalURI = mURI); + return NS_OK; +} + +NS_IMETHODIMP +ClassifierDummyChannel::SetOriginalURI(nsIURI* aOriginalURI) { + mURI = aOriginalURI; + return NS_OK; +} + +NS_IMETHODIMP +ClassifierDummyChannel::GetURI(nsIURI** aURI) { + NS_IF_ADDREF(*aURI = mURI); + return NS_OK; +} + +NS_IMETHODIMP +ClassifierDummyChannel::GetOwner(nsISupports** aOwner) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ClassifierDummyChannel::SetOwner(nsISupports* aOwner) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ClassifierDummyChannel::GetNotificationCallbacks( + nsIInterfaceRequestor** aNotificationCallbacks) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ClassifierDummyChannel::SetNotificationCallbacks( + nsIInterfaceRequestor* aNotificationCallbacks) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ClassifierDummyChannel::GetSecurityInfo(nsISupports** aSecurityInfo) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ClassifierDummyChannel::GetContentType(nsACString& aContentType) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ClassifierDummyChannel::SetContentType(const nsACString& aContentType) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ClassifierDummyChannel::GetContentCharset(nsACString& aContentCharset) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ClassifierDummyChannel::SetContentCharset(const nsACString& aContentCharset) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ClassifierDummyChannel::GetContentLength(int64_t* aContentLength) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ClassifierDummyChannel::SetContentLength(int64_t aContentLength) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ClassifierDummyChannel::Open(nsIInputStream** aStream) { + nsCOMPtr listener; + nsresult rv = + nsContentSecurityManager::doContentSecurityCheck(this, listener); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ClassifierDummyChannel::AsyncOpen(nsIStreamListener* aListener) { + nsCOMPtr listener = aListener; + nsresult rv = + nsContentSecurityManager::doContentSecurityCheck(this, listener); + NS_ENSURE_SUCCESS(rv, rv); + return AsyncOpen(listener); +} + +NS_IMETHODIMP +ClassifierDummyChannel::GetContentDisposition(uint32_t* aContentDisposition) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ClassifierDummyChannel::SetContentDisposition(uint32_t aContentDisposition) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ClassifierDummyChannel::GetContentDispositionFilename( + nsAString& aContentDispositionFilename) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ClassifierDummyChannel::SetContentDispositionFilename( + const nsAString& aContentDispositionFilename) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ClassifierDummyChannel::GetContentDispositionHeader( + nsACString& aContentDispositionHeader) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ClassifierDummyChannel::GetLoadInfo(nsILoadInfo** aLoadInfo) { + NS_IF_ADDREF(*aLoadInfo = mLoadInfo); + return NS_OK; +} + +NS_IMETHODIMP +ClassifierDummyChannel::SetLoadInfo(nsILoadInfo* aLoadInfo) { + MOZ_RELEASE_ASSERT(aLoadInfo, "loadinfo can't be null"); + mLoadInfo = aLoadInfo; + return NS_OK; +} + +//----------------------------------------------------------------------------- +// ClassifierDummyChannel::nsIRequest +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +ClassifierDummyChannel::GetName(nsACString& aName) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ClassifierDummyChannel::IsPending(bool* aRetval) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ClassifierDummyChannel::GetStatus(nsresult* aStatus) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ClassifierDummyChannel::Cancel(nsresult aStatus) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ClassifierDummyChannel::Suspend() { return NS_ERROR_NOT_IMPLEMENTED; } + +NS_IMETHODIMP +ClassifierDummyChannel::Resume() { return NS_ERROR_NOT_IMPLEMENTED; } + +NS_IMETHODIMP +ClassifierDummyChannel::GetLoadGroup(nsILoadGroup** aLoadGroup) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ClassifierDummyChannel::SetLoadGroup(nsILoadGroup* aLoadGroup) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ClassifierDummyChannel::GetLoadFlags(nsLoadFlags* aLoadFlags) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ClassifierDummyChannel::GetTRRMode(nsIRequest::TRRMode* aTRRMode) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ClassifierDummyChannel::SetTRRMode(nsIRequest::TRRMode aTRRMode) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ClassifierDummyChannel::SetLoadFlags(nsLoadFlags aLoadFlags) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ClassifierDummyChannel::GetIsDocument(bool* aIsDocument) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +//----------------------------------------------------------------------------- +// ClassifierDummyChannel::nsIHttpChannelInternal +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +ClassifierDummyChannel::GetDocumentURI(nsIURI** aDocumentURI) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ClassifierDummyChannel::SetDocumentURI(nsIURI* aDocumentURI) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ClassifierDummyChannel::GetRequestVersion(uint32_t* aMajor, uint32_t* aMinor) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ClassifierDummyChannel::GetResponseVersion(uint32_t* aMajor, uint32_t* aMinor) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ClassifierDummyChannel::TakeAllSecurityMessages( + nsCOMArray& aMessages) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ClassifierDummyChannel::SetCookie(const nsACString& aCookieHeader) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ClassifierDummyChannel::SetupFallbackChannel(const char* aFallbackKey) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ClassifierDummyChannel::GetIsAuthChannel(bool* aIsAuthChannel) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ClassifierDummyChannel::GetThirdPartyFlags(uint32_t* aThirdPartyFlags) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ClassifierDummyChannel::SetThirdPartyFlags(uint32_t aThirdPartyFlags) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ClassifierDummyChannel::GetForceAllowThirdPartyCookie( + bool* aForceAllowThirdPartyCookie) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ClassifierDummyChannel::SetForceAllowThirdPartyCookie( + bool aForceAllowThirdPartyCookie) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ClassifierDummyChannel::GetCanceled(bool* aCanceled) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ClassifierDummyChannel::GetChannelIsForDownload(bool* aChannlIsForDownload) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ClassifierDummyChannel::SetChannelIsForDownload(bool aChannlIsForDownload) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ClassifierDummyChannel::GetLocalAddress(nsACString& aLocalAddress) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ClassifierDummyChannel::GetLocalPort(int32_t* aLocalPort) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ClassifierDummyChannel::GetRemoteAddress(nsACString& aRemoteAddress) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ClassifierDummyChannel::GetRemotePort(int32_t* aRemotePort) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ClassifierDummyChannel::SetCacheKeysRedirectChain( + nsTArray* aCacheKeys) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ClassifierDummyChannel::HTTPUpgrade(const nsACString& aProtocolName, + nsIHttpUpgradeListener* aListener) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ClassifierDummyChannel::GetOnlyConnect(bool* aOnlyConnect) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ClassifierDummyChannel::SetConnectOnly() { return NS_ERROR_NOT_IMPLEMENTED; } + +NS_IMETHODIMP +ClassifierDummyChannel::GetAllowSpdy(bool* aAllowSpdy) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ClassifierDummyChannel::SetAllowSpdy(bool aAllowSpdy) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ClassifierDummyChannel::GetAllowHttp3(bool* aAllowHttp3) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ClassifierDummyChannel::SetAllowHttp3(bool aAllowHttp3) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ClassifierDummyChannel::GetResponseTimeoutEnabled( + bool* aResponseTimeoutEnabled) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ClassifierDummyChannel::SetResponseTimeoutEnabled( + bool aResponseTimeoutEnabled) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ClassifierDummyChannel::GetInitialRwin(uint32_t* aInitialRwin) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ClassifierDummyChannel::SetInitialRwin(uint32_t aInitialRwin) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ClassifierDummyChannel::GetApiRedirectToURI(nsIURI** aApiRedirectToURI) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ClassifierDummyChannel::GetAllowAltSvc(bool* aAllowAltSvc) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ClassifierDummyChannel::SetAllowAltSvc(bool aAllowAltSvc) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ClassifierDummyChannel::GetBeConservative(bool* aBeConservative) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ClassifierDummyChannel::SetBeConservative(bool aBeConservative) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ClassifierDummyChannel::GetIsTRRServiceChannel(bool* aTrr) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ClassifierDummyChannel::SetIsTRRServiceChannel(bool aTrr) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ClassifierDummyChannel::GetIsResolvedByTRR(bool* aResolvedByTRR) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ClassifierDummyChannel::GetTlsFlags(uint32_t* aTlsFlags) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ClassifierDummyChannel::SetTlsFlags(uint32_t aTlsFlags) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ClassifierDummyChannel::GetLastModifiedTime(PRTime* aLastModifiedTime) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ClassifierDummyChannel::GetCorsIncludeCredentials( + bool* aCorsIncludeCredentials) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ClassifierDummyChannel::SetCorsIncludeCredentials( + bool aCorsIncludeCredentials) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ClassifierDummyChannel::GetCorsMode(uint32_t* aCorsMode) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ClassifierDummyChannel::SetCorsMode(uint32_t aCorsMode) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ClassifierDummyChannel::GetRedirectMode(uint32_t* aRedirectMode) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ClassifierDummyChannel::SetRedirectMode(uint32_t aRedirectMode) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ClassifierDummyChannel::GetFetchCacheMode(uint32_t* aFetchCacheMode) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ClassifierDummyChannel::SetFetchCacheMode(uint32_t aFetchCacheMode) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ClassifierDummyChannel::GetTopWindowURI(nsIURI** aTopWindowURI) { + nsCOMPtr topWindowURI = mTopWindowURI; + topWindowURI.forget(aTopWindowURI); + return mTopWindowURIResult; +} + +NS_IMETHODIMP +ClassifierDummyChannel::SetTopWindowURIIfUnknown(nsIURI* aTopWindowURI) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ClassifierDummyChannel::GetProxyURI(nsIURI** aProxyURI) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +void ClassifierDummyChannel::SetCorsPreflightParameters( + const nsTArray& aUnsafeHeaders, + bool aShouldStripRequestBodyHeader) {} + +void ClassifierDummyChannel::SetAltDataForChild(bool aIsForChild) {} +void ClassifierDummyChannel::DisableAltDataCache() {} + +NS_IMETHODIMP +ClassifierDummyChannel::GetBlockAuthPrompt(bool* aBlockAuthPrompt) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ClassifierDummyChannel::SetBlockAuthPrompt(bool aBlockAuthPrompt) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ClassifierDummyChannel::GetIntegrityMetadata(nsAString& aIntegrityMetadata) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ClassifierDummyChannel::SetIntegrityMetadata( + const nsAString& aIntegrityMetadata) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ClassifierDummyChannel::GetConnectionInfoHashKey( + nsACString& aConnectionInfoHashKey) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ClassifierDummyChannel::GetLastRedirectFlags(uint32_t* aLastRedirectFlags) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ClassifierDummyChannel::SetLastRedirectFlags(uint32_t aLastRedirectFlags) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ClassifierDummyChannel::GetNavigationStartTimeStamp( + TimeStamp* aNavigationStartTimeStamp) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ClassifierDummyChannel::SetNavigationStartTimeStamp( + TimeStamp aNavigationStartTimeStamp) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ClassifierDummyChannel::CancelByURLClassifier(nsresult aErrorCode) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +void ClassifierDummyChannel::SetIPv4Disabled() {} + +void ClassifierDummyChannel::SetIPv6Disabled() {} + +bool ClassifierDummyChannel::GetHasNonEmptySandboxingFlag() { return false; } + +void ClassifierDummyChannel::SetHasNonEmptySandboxingFlag( + bool aHasNonEmptySandboxingFlag) {} + +NS_IMETHODIMP ClassifierDummyChannel::ComputeCrossOriginOpenerPolicy( + nsILoadInfo::CrossOriginOpenerPolicy aInitiatorPolicy, + nsILoadInfo::CrossOriginOpenerPolicy* aOutPolicy) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ClassifierDummyChannel::GetCrossOriginOpenerPolicy( + nsILoadInfo::CrossOriginOpenerPolicy* aPolicy) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP ClassifierDummyChannel::HasCrossOriginOpenerPolicyMismatch( + bool* aIsMismatch) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP ClassifierDummyChannel::SetMatchedInfo( + const nsACString& aList, const nsACString& aProvider, + const nsACString& aFullHash) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP ClassifierDummyChannel::GetMatchedList(nsACString& aMatchedList) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP ClassifierDummyChannel::GetMatchedProvider( + nsACString& aMatchedProvider) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP ClassifierDummyChannel::GetMatchedFullHash( + nsACString& aMatchedFullHash) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP ClassifierDummyChannel::SetMatchedTrackingInfo( + const nsTArray& aLists, const nsTArray& aFullHashes) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP ClassifierDummyChannel::GetMatchedTrackingLists( + nsTArray& aMatchedTrackingLists) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP ClassifierDummyChannel::GetMatchedTrackingFullHashes( + nsTArray& aMatchedTrackingFullHashes) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP ClassifierDummyChannel::GetFirstPartyClassificationFlags( + uint32_t* aFirstPartyClassificationFlags) { + *aFirstPartyClassificationFlags = mFirstPartyClassificationFlags; + return NS_OK; +} + +NS_IMETHODIMP ClassifierDummyChannel::GetThirdPartyClassificationFlags( + uint32_t* aThirdPartyClassificationFlags) { + *aThirdPartyClassificationFlags = mThirdPartyClassificationFlags; + return NS_OK; +} + +NS_IMETHODIMP ClassifierDummyChannel::GetClassificationFlags( + uint32_t* aClassificationFlags) { + if (mThirdPartyClassificationFlags) { + *aClassificationFlags = mThirdPartyClassificationFlags; + } else { + *aClassificationFlags = mFirstPartyClassificationFlags; + } + return NS_OK; +} + +NS_IMETHODIMP ClassifierDummyChannel::IsThirdPartyTrackingResource( + bool* aIsTrackingResource) { + MOZ_ASSERT( + !(mFirstPartyClassificationFlags && mThirdPartyClassificationFlags)); + *aIsTrackingResource = UrlClassifierCommon::IsTrackingClassificationFlag( + mThirdPartyClassificationFlags); + return NS_OK; +} + +NS_IMETHODIMP ClassifierDummyChannel::IsThirdPartySocialTrackingResource( + bool* aIsThirdPartySocialTrackingResource) { + MOZ_ASSERT(!mFirstPartyClassificationFlags || + !mThirdPartyClassificationFlags); + *aIsThirdPartySocialTrackingResource = + UrlClassifierCommon::IsSocialTrackingClassificationFlag( + mThirdPartyClassificationFlags); + return NS_OK; +} + +void ClassifierDummyChannel::DoDiagnosticAssertWhenOnStopNotCalledOnDestroy() {} + +NS_IMETHODIMP ClassifierDummyChannel::GetResponseEmbedderPolicy( + nsILoadInfo::CrossOriginEmbedderPolicy* aOutPolicy) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP ClassifierDummyChannel::SetWaitForHTTPSSVCRecord() { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ClassifierDummyChannel::GetSupportsHTTP3(bool* aSupportsHTTP3) { + *aSupportsHTTP3 = false; + return NS_ERROR_NOT_IMPLEMENTED; +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/ClassifierDummyChannel.h b/netwerk/protocol/http/ClassifierDummyChannel.h new file mode 100644 index 0000000000..ed97cddf0f --- /dev/null +++ b/netwerk/protocol/http/ClassifierDummyChannel.h @@ -0,0 +1,91 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et 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/. */ + +#ifndef mozilla_net_ClassifierDummyChannel_h +#define mozilla_net_ClassifierDummyChannel_h + +#include "nsIChannel.h" +#include "nsIClassifiedChannel.h" +#include "nsIHttpChannelInternal.h" +#include + +#define CLASSIFIER_DUMMY_CHANNEL_IID \ + { \ + 0x70ceb97d, 0xbfa6, 0x4255, { \ + 0xb7, 0x08, 0xe1, 0xb4, 0x4a, 0x1e, 0x0e, 0x9a \ + } \ + } + +namespace mozilla { +namespace net { + +/** + * In child intercept mode, the decision to intercept a channel is made in the + * child process without consulting the parent process. The decision is based + * on whether there is a ServiceWorker with a scope covering the URL in question + * and whether storage is allowed for the origin/URL. When the + * "network.cookie.cookieBehavior" preference is set to BEHAVIOR_REJECT_TRACKER, + * annotated channels are denied storage which means that the ServiceWorker + * should not intercept the channel. However, the decision for tracking + * protection to annotate a channel only occurs in the parent process. The + * dummy channel is a hack to allow the intercept decision process to ask the + * parent process if the channel should be annotated. Because this round-trip + * to the parent has overhead, the dummy channel is only created 1) if the + * ServiceWorker initially determines that the channel should be intercepted and + * 2) it's a navigation request. + * + * This hack can be removed once Bug 1231208's new "parent intercept" mechanism + * fully lands, the pref is enabled by default it stays enabled for long enough + * to be confident we will never need/want to turn it off. Then as part of bug + * 1496997 we can remove this implementation. Bug 1498259 covers removing this + * hack in particular. + */ +class ClassifierDummyChannel final : public nsIChannel, + public nsIHttpChannelInternal, + public nsIClassifiedChannel { + public: + NS_DECLARE_STATIC_IID_ACCESSOR(CLASSIFIER_DUMMY_CHANNEL_IID) + + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIREQUEST + NS_DECL_NSICHANNEL + NS_DECL_NSIHTTPCHANNELINTERNAL + NS_DECL_NSICLASSIFIEDCHANNEL + + enum StorageAllowedState { + eStorageGranted, + eStorageDenied, + eAsyncNeeded, + }; + + static StorageAllowedState StorageAllowed( + nsIChannel* aChannel, const std::function& aCallback); + + ClassifierDummyChannel(nsIURI* aURI, nsIURI* aTopWindowURI, + nsresult aTopWindowURIResult, nsILoadInfo* aLoadInfo); + + void AddClassificationFlags(uint32_t aClassificationFlags, bool aThirdParty); + + private: + ~ClassifierDummyChannel(); + + nsCOMPtr mLoadInfo; + nsCOMPtr mURI; + nsCOMPtr mTopWindowURI; + nsresult mTopWindowURIResult; + + uint32_t mFirstPartyClassificationFlags; + uint32_t mThirdPartyClassificationFlags; +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(ClassifierDummyChannel, + CLASSIFIER_DUMMY_CHANNEL_IID) + +} // namespace net +} // namespace mozilla + +#endif // mozilla_net_ClassifierDummyChannel_h diff --git a/netwerk/protocol/http/ClassifierDummyChannelChild.cpp b/netwerk/protocol/http/ClassifierDummyChannelChild.cpp new file mode 100644 index 0000000000..de96216b0e --- /dev/null +++ b/netwerk/protocol/http/ClassifierDummyChannelChild.cpp @@ -0,0 +1,91 @@ +/* -*- 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 "ClassifierDummyChannelChild.h" +#include "mozilla/ContentBlocking.h" +#include "mozilla/ipc/BackgroundUtils.h" +#include "mozilla/ipc/URIUtils.h" +#include "nsIURI.h" + +namespace mozilla { +namespace net { + +/* static */ +bool ClassifierDummyChannelChild::Create( + nsIHttpChannel* aChannel, nsIURI* aURI, + const std::function& aCallback) { + MOZ_ASSERT(NS_IsMainThread()); + + MOZ_ASSERT(aChannel); + MOZ_ASSERT(aURI); + + nsCOMPtr httpChannelInternal = + do_QueryInterface(aChannel); + if (!httpChannelInternal) { + // Any non-http channel is allowed. + return true; + } + + nsCOMPtr topWindowURI; + nsresult topWindowURIResult = + httpChannelInternal->GetTopWindowURI(getter_AddRefs(topWindowURI)); + + nsCOMPtr loadInfo = aChannel->LoadInfo(); + Maybe loadInfoArgs; + mozilla::ipc::LoadInfoToLoadInfoArgs(loadInfo, &loadInfoArgs); + + PClassifierDummyChannelChild* actor = + gNeckoChild->SendPClassifierDummyChannelConstructor( + aURI, topWindowURI, topWindowURIResult, loadInfoArgs); + if (!actor) { + return false; + } + + bool isThirdParty = + nsContentUtils::IsThirdPartyWindowOrChannel(nullptr, aChannel, aURI); + + static_cast(actor)->Initialize( + aChannel, aURI, isThirdParty, aCallback); + return true; +} + +ClassifierDummyChannelChild::ClassifierDummyChannelChild() + : mIsThirdParty(false) {} + +ClassifierDummyChannelChild::~ClassifierDummyChannelChild() = default; + +void ClassifierDummyChannelChild::Initialize( + nsIHttpChannel* aChannel, nsIURI* aURI, bool aIsThirdParty, + const std::function& aCallback) { + MOZ_ASSERT(NS_IsMainThread()); + + mChannel = aChannel; + mURI = aURI; + mIsThirdParty = aIsThirdParty; + mCallback = aCallback; +} + +mozilla::ipc::IPCResult ClassifierDummyChannelChild::Recv__delete__( + const uint32_t& aClassificationFlags) { + MOZ_ASSERT(NS_IsMainThread()); + + if (!mChannel) { + return IPC_OK(); + } + + nsCOMPtr channel = std::move(mChannel); + + RefPtr httpChannel = do_QueryObject(channel); + httpChannel->AddClassificationFlags(aClassificationFlags, mIsThirdParty); + + bool storageGranted = + ContentBlocking::ShouldAllowAccessFor(httpChannel, mURI, nullptr); + mCallback(storageGranted); + return IPC_OK(); +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/ClassifierDummyChannelChild.h b/netwerk/protocol/http/ClassifierDummyChannelChild.h new file mode 100644 index 0000000000..7af23d7ae8 --- /dev/null +++ b/netwerk/protocol/http/ClassifierDummyChannelChild.h @@ -0,0 +1,47 @@ +/* -*- 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/. */ + +#ifndef mozilla_net_ClassifierDummyChannelChild_h +#define mozilla_net_ClassifierDummyChannelChild_h + +#include "mozilla/net/PClassifierDummyChannelChild.h" +#include "nsCOMPtr.h" + +#include + +class nsIHttpChannel; +class nsIURI; + +namespace mozilla { +namespace net { + +class ClassifierDummyChannelChild final : public PClassifierDummyChannelChild { + friend class PClassifierDummyChannelChild; + + public: + static bool Create(nsIHttpChannel* aChannel, nsIURI* aURI, + const std::function& aCallback); + + // Used by PNeckoChild only! + ClassifierDummyChannelChild(); + ~ClassifierDummyChannelChild(); + + private: + void Initialize(nsIHttpChannel* aChannel, nsIURI* aURI, bool aIsThirdParty, + const std::function& aCallback); + + mozilla::ipc::IPCResult Recv__delete__(const uint32_t& aClassificationFlags); + + nsCOMPtr mChannel; + nsCOMPtr mURI; + std::function mCallback; + bool mIsThirdParty; +}; + +} // namespace net +} // namespace mozilla + +#endif // mozilla_net_ClassifierDummyChannelChild_h diff --git a/netwerk/protocol/http/ClassifierDummyChannelParent.cpp b/netwerk/protocol/http/ClassifierDummyChannelParent.cpp new file mode 100644 index 0000000000..043b739044 --- /dev/null +++ b/netwerk/protocol/http/ClassifierDummyChannelParent.cpp @@ -0,0 +1,55 @@ +/* -*- 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 "ClassifierDummyChannelParent.h" +#include "mozilla/net/AsyncUrlChannelClassifier.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/Unused.h" +#include "nsIPrincipal.h" +#include "nsNetUtil.h" + +namespace mozilla { +namespace net { + +ClassifierDummyChannelParent::ClassifierDummyChannelParent() + : mIPCActive(true) {} + +ClassifierDummyChannelParent::~ClassifierDummyChannelParent() = default; + +void ClassifierDummyChannelParent::Init(nsIURI* aURI, nsIURI* aTopWindowURI, + nsresult aTopWindowURIResult, + nsILoadInfo* aLoadInfo) { + MOZ_ASSERT(mIPCActive); + + RefPtr self = this; + auto onExit = + MakeScopeExit([self] { Unused << Send__delete__(self, false); }); + + if (!aURI) { + return; + } + + RefPtr channel = new ClassifierDummyChannel( + aURI, aTopWindowURI, aTopWindowURIResult, aLoadInfo); + + bool willCallback = NS_SUCCEEDED(AsyncUrlChannelClassifier::CheckChannel( + channel, [self = std::move(self), channel]() { + if (self->mIPCActive) { + Unused << Send__delete__(self, channel->GetClassificationFlags()); + } + })); + + if (willCallback) { + onExit.release(); + } +} + +void ClassifierDummyChannelParent::ActorDestroy(ActorDestroyReason aWhy) { + mIPCActive = false; +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/ClassifierDummyChannelParent.h b/netwerk/protocol/http/ClassifierDummyChannelParent.h new file mode 100644 index 0000000000..fe9397e4f7 --- /dev/null +++ b/netwerk/protocol/http/ClassifierDummyChannelParent.h @@ -0,0 +1,40 @@ +/* -*- 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/. */ + +#ifndef mozilla_net_ClassifierDummyChannelParent_h +#define mozilla_net_ClassifierDummyChannelParent_h + +#include "mozilla/net/PClassifierDummyChannelParent.h" +#include "nsISupportsImpl.h" + +class nsILoadInfo; +class nsIURI; + +namespace mozilla { +namespace net { + +class ClassifierDummyChannelParent final + : public PClassifierDummyChannelParent { + public: + NS_INLINE_DECL_REFCOUNTING(ClassifierDummyChannelParent) + + ClassifierDummyChannelParent(); + + void Init(nsIURI* aURI, nsIURI* aTopWindowURI, nsresult aTopWindowURIResult, + nsILoadInfo* aLoadInfo); + + private: + ~ClassifierDummyChannelParent(); + + void ActorDestroy(ActorDestroyReason aWhy) override; + + bool mIPCActive; +}; + +} // namespace net +} // namespace mozilla + +#endif // mozilla_net_ClassifierDummyChannelParent_h diff --git a/netwerk/protocol/http/ConnectionDiagnostics.cpp b/netwerk/protocol/http/ConnectionDiagnostics.cpp new file mode 100644 index 0000000000..7dc8550b0b --- /dev/null +++ b/netwerk/protocol/http/ConnectionDiagnostics.cpp @@ -0,0 +1,236 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et 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/. */ + +// HttpLog.h should generally be included first +#include "HttpLog.h" + +#include "nsHttpConnectionMgr.h" +#include "nsHttpConnection.h" +#include "HttpConnectionUDP.h" +#include "Http2Session.h" +#include "nsHttpHandler.h" +#include "nsIConsoleService.h" +#include "nsHttpRequestHead.h" +#include "nsServiceManagerUtils.h" +#include "nsSocketTransportService2.h" + +#include "mozilla/IntegerPrintfMacros.h" + +namespace mozilla { +namespace net { + +void nsHttpConnectionMgr::PrintDiagnostics() { + nsresult rv = + PostEvent(&nsHttpConnectionMgr::OnMsgPrintDiagnostics, 0, nullptr); + if (NS_FAILED(rv)) { + LOG( + ("nsHttpConnectionMgr::PrintDiagnostics\n" + " failed to post OnMsgPrintDiagnostics event")); + } +} + +void nsHttpConnectionMgr::OnMsgPrintDiagnostics(int32_t, ARefBase*) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + nsCOMPtr consoleService = + do_GetService(NS_CONSOLESERVICE_CONTRACTID); + if (!consoleService) return; + + mLogData.AppendPrintf("HTTP Connection Diagnostics\n---------------------\n"); + mLogData.AppendPrintf("IsSpdyEnabled() = %d\n", + gHttpHandler->IsSpdyEnabled()); + mLogData.AppendPrintf("MaxSocketCount() = %d\n", + gHttpHandler->MaxSocketCount()); + mLogData.AppendPrintf("mNumActiveConns = %d\n", mNumActiveConns); + mLogData.AppendPrintf("mNumIdleConns = %d\n", mNumIdleConns); + + for (auto iter = mCT.Iter(); !iter.Done(); iter.Next()) { + RefPtr ent = iter.Data(); + + mLogData.AppendPrintf( + " AtActiveConnectionLimit = %d\n", + AtActiveConnectionLimit(ent, NS_HTTP_ALLOW_KEEPALIVE)); + + ent->PrintDiagnostics(mLogData, MaxPersistConnections(ent)); + } + + consoleService->LogStringMessage(NS_ConvertUTF8toUTF16(mLogData).Data()); + mLogData.Truncate(); +} + +void ConnectionEntry::PrintDiagnostics(nsCString& log, + uint32_t aMaxPersistConns) { + log.AppendPrintf(" ent host = %s hashkey = %s\n", mConnInfo->Origin(), + mConnInfo->HashKey().get()); + + log.AppendPrintf(" RestrictConnections = %d\n", RestrictConnections()); + log.AppendPrintf(" Pending Q Length = %zu\n", PendingQueueLength()); + log.AppendPrintf(" Active Conns Length = %zu\n", mActiveConns.Length()); + log.AppendPrintf(" Idle Conns Length = %zu\n", mIdleConns.Length()); + log.AppendPrintf(" Half Opens Length = %zu\n", mHalfOpens.Length()); + log.AppendPrintf(" Coalescing Keys Length = %zu\n", + mCoalescingKeys.Length()); + log.AppendPrintf(" Spdy using = %d\n", mUsingSpdy); + + uint32_t i; + for (i = 0; i < mActiveConns.Length(); ++i) { + log.AppendPrintf(" :: Active Connection #%u\n", i); + mActiveConns[i]->PrintDiagnostics(log); + } + for (i = 0; i < mIdleConns.Length(); ++i) { + log.AppendPrintf(" :: Idle Connection #%u\n", i); + mIdleConns[i]->PrintDiagnostics(log); + } + for (i = 0; i < mHalfOpens.Length(); ++i) { + log.AppendPrintf(" :: Half Open #%u\n", i); + mHalfOpens[i]->PrintDiagnostics(log); + } + + mPendingQ.PrintDiagnostics(log); + + for (i = 0; i < mCoalescingKeys.Length(); ++i) { + log.AppendPrintf(" :: Coalescing Key #%u %s\n", i, + mCoalescingKeys[i].get()); + } +} + +void PendingTransactionQueue::PrintDiagnostics(nsCString& log) { + uint32_t i = 0; + for (auto it = mPendingTransactionTable.Iter(); !it.Done(); it.Next()) { + log.AppendPrintf( + " :: Pending Transactions with Window ID = %" PRIu64 "\n", it.Key()); + for (uint32_t j = 0; j < it.UserData()->Length(); ++j) { + log.AppendPrintf(" ::: Pending Transaction #%u\n", i); + it.UserData()->ElementAt(j)->PrintDiagnostics(log); + ++i; + } + } +} + +void HalfOpenSocket::PrintDiagnostics(nsCString& log) { + log.AppendPrintf(" has connected = %d, isSpeculative = %d\n", + HasConnected(), IsSpeculative()); + + TimeStamp now = TimeStamp::Now(); + + if (mPrimarySynStarted.IsNull()) + log.AppendPrintf(" primary not started\n"); + else + log.AppendPrintf(" primary started %.2fms ago\n", + (now - mPrimarySynStarted).ToMilliseconds()); + + if (mBackupSynStarted.IsNull()) + log.AppendPrintf(" backup not started\n"); + else + log.AppendPrintf(" backup started %.2f ago\n", + (now - mBackupSynStarted).ToMilliseconds()); + + log.AppendPrintf(" primary transport %d, backup transport %d\n", + !!mSocketTransport.get(), !!mBackupTransport.get()); +} + +void nsHttpConnection::PrintDiagnostics(nsCString& log) { + log.AppendPrintf(" CanDirectlyActivate = %d\n", CanDirectlyActivate()); + + log.AppendPrintf(" npncomplete = %d setupSSLCalled = %d\n", mNPNComplete, + mSetupSSLCalled); + + log.AppendPrintf(" spdyVersion = %d reportedSpdy = %d everspdy = %d\n", + static_cast(mUsingSpdyVersion), mReportedSpdy, + mEverUsedSpdy); + + log.AppendPrintf(" iskeepalive = %d dontReuse = %d isReused = %d\n", + IsKeepAlive(), mDontReuse, mIsReused); + + log.AppendPrintf(" mTransaction = %d mSpdySession = %d\n", + !!mTransaction.get(), !!mSpdySession.get()); + + PRIntervalTime now = PR_IntervalNow(); + log.AppendPrintf(" time since last read = %ums\n", + PR_IntervalToMilliseconds(now - mLastReadTime)); + + log.AppendPrintf(" max-read/read/written %" PRId64 "/%" PRId64 "/%" PRId64 + "\n", + mMaxBytesRead, mTotalBytesRead, mTotalBytesWritten); + + log.AppendPrintf(" rtt = %ums\n", PR_IntervalToMilliseconds(mRtt)); + + log.AppendPrintf(" idlemonitoring = %d transactionCount=%d\n", + mIdleMonitoring, mHttp1xTransactionCount); + + if (mSpdySession) mSpdySession->PrintDiagnostics(log); +} + +void HttpConnectionUDP::PrintDiagnostics(nsCString& log) { + log.AppendPrintf(" CanDirectlyActivate = %d\n", CanDirectlyActivate()); + + log.AppendPrintf(" dontReuse = %d isReused = %d\n", mDontReuse, mIsReused); + + PRIntervalTime now = PR_IntervalNow(); + log.AppendPrintf(" time since last read = %ums\n", + PR_IntervalToMilliseconds(now - mLastReadTime)); + + log.AppendPrintf(" read/written %" PRId64 "/%" PRId64 "\n", + mTotalBytesRead, mTotalBytesWritten); + + log.AppendPrintf(" rtt = %ums\n", PR_IntervalToMilliseconds(mRtt)); +} + +void Http2Session::PrintDiagnostics(nsCString& log) { + log.AppendPrintf(" ::: HTTP2\n"); + log.AppendPrintf( + " shouldgoaway = %d mClosed = %d CanReuse = %d nextID=0x%X\n", + mShouldGoAway, mClosed, CanReuse(), mNextStreamID); + + log.AppendPrintf(" concurrent = %d maxconcurrent = %d\n", mConcurrent, + mMaxConcurrent); + + log.AppendPrintf(" roomformorestreams = %d roomformoreconcurrent = %d\n", + RoomForMoreStreams(), RoomForMoreConcurrent()); + + log.AppendPrintf(" transactionHashCount = %d streamIDHashCount = %d\n", + mStreamTransactionHash.Count(), mStreamIDHash.Count()); + + log.AppendPrintf(" Queued Stream Size = %zu\n", mQueuedStreams.GetSize()); + + PRIntervalTime now = PR_IntervalNow(); + log.AppendPrintf(" Ping Threshold = %ums\n", + PR_IntervalToMilliseconds(mPingThreshold)); + log.AppendPrintf(" Ping Timeout = %ums\n", + PR_IntervalToMilliseconds(gHttpHandler->SpdyPingTimeout())); + log.AppendPrintf(" Idle for Any Activity (ping) = %ums\n", + PR_IntervalToMilliseconds(now - mLastReadEpoch)); + log.AppendPrintf(" Idle for Data Activity = %ums\n", + PR_IntervalToMilliseconds(now - mLastDataReadEpoch)); + if (mPingSentEpoch) + log.AppendPrintf(" Ping Outstanding (ping) = %ums, expired = %d\n", + PR_IntervalToMilliseconds(now - mPingSentEpoch), + now - mPingSentEpoch >= gHttpHandler->SpdyPingTimeout()); + else + log.AppendPrintf(" No Ping Outstanding\n"); +} + +void nsHttpTransaction::PrintDiagnostics(nsCString& log) { + if (!mRequestHead) return; + + nsAutoCString requestURI; + mRequestHead->RequestURI(requestURI); + log.AppendPrintf(" :::: uri = %s\n", requestURI.get()); + log.AppendPrintf(" caps = 0x%x\n", mCaps); + log.AppendPrintf(" priority = %d\n", mPriority); + log.AppendPrintf(" restart count = %u\n", mRestartCount); +} + +void PendingTransactionInfo::PrintDiagnostics(nsCString& log) { + log.AppendPrintf(" ::: Pending transaction\n"); + mTransaction->PrintDiagnostics(log); + RefPtr halfOpen = do_QueryReferent(mHalfOpen); + log.AppendPrintf(" Waiting for half open sock: %p or connection: %p\n", + halfOpen.get(), mActiveConn.get()); +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/ConnectionEntry.cpp b/netwerk/protocol/http/ConnectionEntry.cpp new file mode 100644 index 0000000000..b7651fb169 --- /dev/null +++ b/netwerk/protocol/http/ConnectionEntry.cpp @@ -0,0 +1,962 @@ +/* vim:set ts=4 sw=2 sts=2 et cin: */ +/* 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/. */ + +// HttpLog.h should generally be included first +#include "HttpLog.h" + +// Log on level :5, instead of default :4. +#undef LOG +#define LOG(args) LOG5(args) +#undef LOG_ENABLED +#define LOG_ENABLED() LOG5_ENABLED() + +#include "ConnectionEntry.h" +#include "nsQueryObject.h" +#include "mozilla/ChaosMode.h" + +namespace mozilla { +namespace net { + +// ConnectionEntry +ConnectionEntry::~ConnectionEntry() { + LOG(("ConnectionEntry::~ConnectionEntry this=%p", this)); + + MOZ_ASSERT(!mIdleConns.Length()); + MOZ_ASSERT(!mActiveConns.Length()); + MOZ_ASSERT(!mHalfOpens.Length()); + MOZ_ASSERT(!PendingQueueLength()); + MOZ_ASSERT(!UrgentStartQueueLength()); + MOZ_ASSERT(!mHalfOpenFastOpenBackups.Length()); + MOZ_ASSERT(!mDoNotDestroy); +} + +ConnectionEntry::ConnectionEntry(nsHttpConnectionInfo* ci) + : mConnInfo(ci), + mUsingSpdy(false), + mCanUseSpdy(true), + mPreferIPv4(false), + mPreferIPv6(false), + mUsedForConnection(false), + mDoNotDestroy(false) { + if (mConnInfo->FirstHopSSL() && !mConnInfo->IsHttp3()) { + mUseFastOpen = gHttpHandler->UseFastOpen(); + } else { + // Only allow the TCP fast open on a secure connection. + mUseFastOpen = false; + } + + LOG(("ConnectionEntry::ConnectionEntry this=%p key=%s", this, + ci->HashKey().get())); +} + +bool ConnectionEntry::AvailableForDispatchNow() { + if (mIdleConns.Length() && mIdleConns[0]->CanReuse()) { + return true; + } + + return gHttpHandler->ConnMgr()->GetH2orH3ActiveConn(this, false, false) + ? true + : false; +} + +uint32_t ConnectionEntry::UnconnectedHalfOpens() const { + uint32_t unconnectedHalfOpens = 0; + for (uint32_t i = 0; i < mHalfOpens.Length(); ++i) { + if (!mHalfOpens[i]->HasConnected()) { + ++unconnectedHalfOpens; + } + } + return unconnectedHalfOpens; +} + +bool ConnectionEntry::RemoveHalfOpen(HalfOpenSocket* halfOpen) { + bool isPrimary = false; + // A failure to create the transport object at all + // will result in it not being present in the halfopen table. That's expected. + if (mHalfOpens.RemoveElement(halfOpen)) { + isPrimary = true; + if (halfOpen->IsSpeculative()) { + Telemetry::AutoCounter + unusedSpeculativeConn; + ++unusedSpeculativeConn; + + if (halfOpen->IsFromPredictor()) { + Telemetry::AutoCounter + totalPreconnectsUnused; + ++totalPreconnectsUnused; + } + } + + gHttpHandler->ConnMgr()->DecreaseNumHalfOpenConns(); + + } else { + mHalfOpenFastOpenBackups.RemoveElement(halfOpen); + } + + if (!UnconnectedHalfOpens()) { + // perhaps this reverted RestrictConnections() + // use the PostEvent version of processpendingq to avoid + // altering the pending q vector from an arbitrary stack + nsresult rv = gHttpHandler->ConnMgr()->ProcessPendingQ(mConnInfo); + if (NS_FAILED(rv)) { + LOG( + ("ConnectionEntry::RemoveHalfOpen\n" + " failed to process pending queue\n")); + } + } + + return isPrimary; +} + +void ConnectionEntry::DisallowHttp2() { + mCanUseSpdy = false; + + // If we have any spdy connections, we want to go ahead and close them when + // they're done so we can free up some connections. + for (uint32_t i = 0; i < mActiveConns.Length(); ++i) { + if (mActiveConns[i]->UsingSpdy()) { + mActiveConns[i]->DontReuse(); + } + } + for (uint32_t i = 0; i < mIdleConns.Length(); ++i) { + if (mIdleConns[i]->UsingSpdy()) { + mIdleConns[i]->DontReuse(); + } + } + + // Can't coalesce if we're not using spdy + mCoalescingKeys.Clear(); +} + +void ConnectionEntry::DontReuseHttp3Conn() { + MOZ_ASSERT(mConnInfo->IsHttp3()); + + // If we have any spdy connections, we want to go ahead and close them when + // they're done so we can free up some connections. + for (uint32_t i = 0; i < mActiveConns.Length(); ++i) { + mActiveConns[i]->DontReuse(); + } + + // Can't coalesce if we're not using http3 + mCoalescingKeys.Clear(); +} + +void ConnectionEntry::RecordIPFamilyPreference(uint16_t family) { + LOG(("ConnectionEntry::RecordIPFamilyPreference %p, af=%u", this, family)); + + if (family == PR_AF_INET && !mPreferIPv6) { + mPreferIPv4 = true; + } + + if (family == PR_AF_INET6 && !mPreferIPv4) { + mPreferIPv6 = true; + } + + LOG((" %p prefer ipv4=%d, ipv6=%d", this, (bool)mPreferIPv4, + (bool)mPreferIPv6)); +} + +void ConnectionEntry::ResetIPFamilyPreference() { + LOG(("ConnectionEntry::ResetIPFamilyPreference %p", this)); + + mPreferIPv4 = false; + mPreferIPv6 = false; +} + +bool net::ConnectionEntry::PreferenceKnown() const { + return (bool)mPreferIPv4 || (bool)mPreferIPv6; +} + +size_t ConnectionEntry::PendingQueueLength() const { + return mPendingQ.PendingQueueLength(); +} + +size_t ConnectionEntry::PendingQueueLengthForWindow(uint64_t windowId) const { + return mPendingQ.PendingQueueLengthForWindow(windowId); +} + +void ConnectionEntry::AppendPendingUrgentStartQ( + nsTArray>& result) { + mPendingQ.AppendPendingUrgentStartQ(result); +} + +void ConnectionEntry::AppendPendingQForFocusedWindow( + uint64_t windowId, nsTArray>& result, + uint32_t maxCount) { + mPendingQ.AppendPendingQForFocusedWindow(windowId, result, maxCount); + LOG( + ("ConnectionEntry::AppendPendingQForFocusedWindow [ci=%s], " + "pendingQ count=%zu for focused window (id=%" PRIu64 ")\n", + mConnInfo->HashKey().get(), result.Length(), windowId)); +} + +void ConnectionEntry::AppendPendingQForNonFocusedWindows( + uint64_t windowId, nsTArray>& result, + uint32_t maxCount) { + mPendingQ.AppendPendingQForNonFocusedWindows(windowId, result, maxCount); + LOG( + ("ConnectionEntry::AppendPendingQForNonFocusedWindows [ci=%s], " + "pendingQ count=%zu for non focused window\n", + mConnInfo->HashKey().get(), result.Length())); +} + +void ConnectionEntry::RemoveEmptyPendingQ() { mPendingQ.RemoveEmptyPendingQ(); } + +void ConnectionEntry::InsertTransactionSorted( + nsTArray>& pendingQ, + PendingTransactionInfo* pendingTransInfo, + bool aInsertAsFirstForTheSamePriority /*= false*/) { + mPendingQ.InsertTransactionSorted(pendingQ, pendingTransInfo, + aInsertAsFirstForTheSamePriority); +} + +void ConnectionEntry::ReschedTransaction(nsHttpTransaction* aTrans) { + mPendingQ.ReschedTransaction(aTrans); +} + +void ConnectionEntry::InsertTransaction( + PendingTransactionInfo* pendingTransInfo, + bool aInsertAsFirstForTheSamePriority /* = false */) { + mPendingQ.InsertTransaction(pendingTransInfo, + aInsertAsFirstForTheSamePriority); + pendingTransInfo->Transaction()->OnPendingQueueInserted(); +} + +nsTArray>* +ConnectionEntry::GetTransactionPendingQHelper(nsAHttpTransaction* trans) { + return mPendingQ.GetTransactionPendingQHelper(trans); +} + +bool ConnectionEntry::RestrictConnections() { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + if (AvailableForDispatchNow()) { + // this might be a h2/spdy connection in this connection entry that + // is able to be immediately muxxed, or it might be one that + // was found in the same state through a coalescing hash + LOG( + ("ConnectionEntry::RestrictConnections %p %s restricted due to " + "active >=h2\n", + this, mConnInfo->HashKey().get())); + return true; + } + + // If this host is trying to negotiate a SPDY session right now, + // don't create any new ssl connections until the result of the + // negotiation is known. + + bool doRestrict = mConnInfo->FirstHopSSL() && gHttpHandler->IsSpdyEnabled() && + mUsingSpdy && + (mHalfOpens.Length() || mActiveConns.Length()); + + // If there are no restrictions, we are done + if (!doRestrict) { + return false; + } + + // If the restriction is based on a tcp handshake in progress + // let that connect and then see if it was SPDY or not + if (UnconnectedHalfOpens()) { + return true; + } + + // There is a concern that a host is using a mix of HTTP/1 and SPDY. + // In that case we don't want to restrict connections just because + // there is a single active HTTP/1 session in use. + if (mUsingSpdy && mActiveConns.Length()) { + bool confirmedRestrict = false; + for (uint32_t index = 0; index < mActiveConns.Length(); ++index) { + HttpConnectionBase* conn = mActiveConns[index]; + RefPtr connTCP = do_QueryObject(conn); + if ((connTCP && !connTCP->ReportedNPN()) || conn->CanDirectlyActivate()) { + confirmedRestrict = true; + break; + } + } + doRestrict = confirmedRestrict; + if (!confirmedRestrict) { + LOG( + ("nsHttpConnectionMgr spdy connection restriction to " + "%s bypassed.\n", + mConnInfo->Origin())); + } + } + return doRestrict; +} + +uint32_t ConnectionEntry::TotalActiveConnections() const { + // Add in the in-progress tcp connections, we will assume they are + // keepalive enabled. + // Exclude half-open's that has already created a usable connection. + // This prevents the limit being stuck on ipv6 connections that + // eventually time out after typical 21 seconds of no ACK+SYN reply. + return mActiveConns.Length() + UnconnectedHalfOpens(); +} + +size_t ConnectionEntry::UrgentStartQueueLength() { + return mPendingQ.UrgentStartQueueLength(); +} + +void ConnectionEntry::PrintPendingQ() { mPendingQ.PrintPendingQ(); } + +void ConnectionEntry::Compact() { + mIdleConns.Compact(); + mActiveConns.Compact(); + mPendingQ.Compact(); +} + +void ConnectionEntry::RemoveFromIdleConnectionsIndex(size_t inx) { + mIdleConns.RemoveElementAt(inx); + gHttpHandler->ConnMgr()->DecrementNumIdleConns(); +} + +bool ConnectionEntry::RemoveFromIdleConnections(nsHttpConnection* conn) { + if (!mIdleConns.RemoveElement(conn)) { + return false; + } + + gHttpHandler->ConnMgr()->DecrementNumIdleConns(); + return true; +} + +void ConnectionEntry::CancelAllTransactions(nsresult reason) { + mPendingQ.CancelAllTransactions(reason); +} + +nsresult ConnectionEntry::CloseIdleConnection(nsHttpConnection* conn) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + RefPtr deleteProtector(conn); + if (!RemoveFromIdleConnections(conn)) { + return NS_ERROR_UNEXPECTED; + } + + // The connection is closed immediately no need to call EndIdleMonitoring. + conn->Close(NS_ERROR_ABORT); + return NS_OK; +} + +void ConnectionEntry::CloseIdleConnections() { + while (mIdleConns.Length()) { + RefPtr conn(mIdleConns[0]); + RemoveFromIdleConnectionsIndex(0); + // The connection is closed immediately no need to call EndIdleMonitoring. + conn->Close(NS_ERROR_ABORT); + } +} + +void ConnectionEntry::CloseIdleConnections(uint32_t maxToClose) { + uint32_t closed = 0; + while (mIdleConns.Length() && (closed < maxToClose)) { + RefPtr conn(mIdleConns[0]); + RemoveFromIdleConnectionsIndex(0); + // The connection is closed immediately no need to call EndIdleMonitoring. + conn->Close(NS_ERROR_ABORT); + closed++; + } +} + +nsresult ConnectionEntry::RemoveIdleConnection(nsHttpConnection* conn) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + if (!RemoveFromIdleConnections(conn)) { + return NS_ERROR_UNEXPECTED; + } + + conn->EndIdleMonitoring(); + return NS_OK; +} + +bool ConnectionEntry::IsInIdleConnections(HttpConnectionBase* conn) { + RefPtr connTCP = do_QueryObject(conn); + return connTCP && mIdleConns.Contains(connTCP); +} + +already_AddRefed ConnectionEntry::GetIdleConnection( + bool respectUrgency, bool urgentTrans, bool* onlyUrgent) { + RefPtr conn; + size_t index = 0; + while (!conn && (mIdleConns.Length() > index)) { + conn = mIdleConns[index]; + + if (!conn->CanReuse()) { + RemoveFromIdleConnectionsIndex(index); + LOG((" dropping stale connection: [conn=%p]\n", conn.get())); + conn->Close(NS_ERROR_ABORT); + conn = nullptr; + continue; + } + + // non-urgent transactions can only be dispatched on non-urgent + // started or used connections. + if (respectUrgency && conn->IsUrgentStartPreferred() && !urgentTrans) { + LOG((" skipping urgent: [conn=%p]", conn.get())); + conn = nullptr; + ++index; + continue; + } + + *onlyUrgent = false; + + RemoveFromIdleConnectionsIndex(index); + conn->EndIdleMonitoring(); + LOG((" reusing connection: [conn=%p]\n", conn.get())); + } + + return conn.forget(); +} + +nsresult ConnectionEntry::RemoveActiveConnection(HttpConnectionBase* conn) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + if (!mActiveConns.RemoveElement(conn)) { + return NS_ERROR_UNEXPECTED; + } + gHttpHandler->ConnMgr()->DecrementActiveConnCount(conn); + + return NS_OK; +} + +void ConnectionEntry::ClosePersistentConnections() { + LOG(("ConnectionEntry::ClosePersistentConnections [ci=%s]\n", + mConnInfo->HashKey().get())); + CloseIdleConnections(); + + int32_t activeCount = mActiveConns.Length(); + for (int32_t i = 0; i < activeCount; i++) { + mActiveConns[i]->DontReuse(); + } + + for (int32_t index = mHalfOpenFastOpenBackups.Length() - 1; index >= 0; + --index) { + RefPtr half = mHalfOpenFastOpenBackups[index]; + half->CancelFastOpenConnection(); + } +} + +uint32_t ConnectionEntry::PruneDeadConnections() { + uint32_t timeToNextExpire = UINT32_MAX; + + for (int32_t len = mIdleConns.Length(); len > 0; --len) { + int32_t idx = len - 1; + RefPtr conn(mIdleConns[idx]); + if (!conn->CanReuse()) { + RemoveFromIdleConnectionsIndex(idx); + // The connection is closed immediately no need to call + // EndIdleMonitoring. + conn->Close(NS_ERROR_ABORT); + } else { + timeToNextExpire = std::min(timeToNextExpire, conn->TimeToLive()); + } + } + + if (mUsingSpdy) { + for (uint32_t i = 0; i < mActiveConns.Length(); ++i) { + RefPtr connTCP = do_QueryObject(mActiveConns[i]); + // Http3 has its own timers, it is not using this one. + if (connTCP && connTCP->UsingSpdy()) { + if (!connTCP->CanReuse()) { + // Marking it don't-reuse will create an active + // tear down if the spdy session is idle. + connTCP->DontReuse(); + } else { + timeToNextExpire = std::min(timeToNextExpire, connTCP->TimeToLive()); + } + } + } + } + + return timeToNextExpire; +} + +void ConnectionEntry::VerifyTraffic() { + if (!mConnInfo->IsHttp3()) { + // Iterate over all active connections and check them. + for (uint32_t index = 0; index < mActiveConns.Length(); ++index) { + RefPtr conn = do_QueryObject(mActiveConns[index]); + if (conn) { + conn->CheckForTraffic(true); + } + } + // Iterate the idle connections and unmark them for traffic checks. + for (uint32_t index = 0; index < mIdleConns.Length(); ++index) { + RefPtr conn = do_QueryObject(mIdleConns[index]); + if (conn) { + conn->CheckForTraffic(false); + } + } + } +} + +void ConnectionEntry::InsertIntoIdleConnections_internal( + nsHttpConnection* conn) { + uint32_t idx; + for (idx = 0; idx < mIdleConns.Length(); idx++) { + nsHttpConnection* idleConn = mIdleConns[idx]; + if (idleConn->MaxBytesRead() < conn->MaxBytesRead()) { + break; + } + } + + mIdleConns.InsertElementAt(idx, conn); +} + +void ConnectionEntry::InsertIntoIdleConnections(nsHttpConnection* conn) { + InsertIntoIdleConnections_internal(conn); + gHttpHandler->ConnMgr()->NewIdleConnectionAdded(conn->TimeToLive()); + conn->BeginIdleMonitoring(); +} + +bool ConnectionEntry::IsInActiveConns(HttpConnectionBase* conn) { + return mActiveConns.Contains(conn); +} + +void ConnectionEntry::InsertIntoActiveConns(HttpConnectionBase* conn) { + mActiveConns.AppendElement(conn); + gHttpHandler->ConnMgr()->IncrementActiveConnCount(); +} + +void ConnectionEntry::MakeAllDontReuseExcept(HttpConnectionBase* conn) { + for (uint32_t index = 0; index < mActiveConns.Length(); ++index) { + HttpConnectionBase* otherConn = mActiveConns[index]; + if (otherConn != conn) { + LOG( + ("ConnectionEntry::MakeAllDontReuseExcept shutting down old " + "connection (%p) " + "because new " + "spdy connection (%p) takes precedence\n", + otherConn, conn)); + otherConn->DontReuse(); + } + } + + // Cancel any other pending connections - their associated transactions + // are in the pending queue and will be dispatched onto this new connection + for (int32_t index = HalfOpensLength() - 1; index >= 0; --index) { + RefPtr half = mHalfOpens[index]; + LOG(( + "ConnectionEntry::MakeAllDontReuseExcept forcing halfopen abandon %p\n", + half.get())); + mHalfOpens[index]->Abandon(); + } + + for (int32_t index = mHalfOpenFastOpenBackups.Length() - 1; index >= 0; + --index) { + LOG( + ("ConnectionEntry::MakeAllDontReuseExcept shutting down connection in " + "fast " + "open state (%p) because new spdy connection (%p) takes " + "precedence\n", + mHalfOpenFastOpenBackups[index].get(), conn)); + RefPtr half = mHalfOpenFastOpenBackups[index]; + half->CancelFastOpenConnection(); + } +} + +bool ConnectionEntry::FindConnToClaim( + PendingTransactionInfo* pendingTransInfo) { + nsHttpTransaction* trans = pendingTransInfo->Transaction(); + + uint32_t halfOpenLength = HalfOpensLength(); + for (uint32_t i = 0; i < halfOpenLength; i++) { + auto halfOpen = mHalfOpens[i]; + if (halfOpen->AcceptsTransaction(trans) && + pendingTransInfo->TryClaimingHalfOpen(halfOpen)) { + // We've found a speculative connection or a connection that + // is free to be used in the half open list. + // A free to be used connection is a connection that was + // open for a concrete transaction, but that trunsaction + // ended up using another connection. + LOG( + ("ConnectionEntry::FindConnToClaim [ci = %s]\n" + "Found a speculative or a free-to-use half open connection\n", + mConnInfo->HashKey().get())); + + // return OK because we have essentially opened a new connection + // by converting a speculative half-open to general use + return true; + } + } + + // consider null transactions that are being used to drive the ssl handshake + // if the transaction creating this connection can re-use persistent + // connections + if (trans->Caps() & NS_HTTP_ALLOW_KEEPALIVE) { + uint32_t activeLength = mActiveConns.Length(); + for (uint32_t i = 0; i < activeLength; i++) { + if (pendingTransInfo->TryClaimingActiveConn(mActiveConns[i])) { + LOG( + ("ConnectionEntry::FindConnectingSocket [ci = %s] " + "Claiming a null transaction for later use\n", + mConnInfo->HashKey().get())); + return true; + } + } + } + return false; +} + +bool ConnectionEntry::MakeFirstActiveSpdyConnDontReuse() { + if (!mUsingSpdy) { + return false; + } + + for (uint32_t index = 0; index < mActiveConns.Length(); ++index) { + HttpConnectionBase* conn = mActiveConns[index]; + if (conn->UsingSpdy() && conn->CanReuse()) { + conn->DontReuse(); + return true; + } + } + return false; +} + +// Return an active h2 or h3 connection +// that can be directly activated or null. +HttpConnectionBase* ConnectionEntry::GetH2orH3ActiveConn() { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + HttpConnectionBase* experienced = nullptr; + HttpConnectionBase* noExperience = nullptr; + uint32_t activeLen = mActiveConns.Length(); + + // activeLen should generally be 1.. this is a setup race being resolved + // take a conn who can activate and is experienced + for (uint32_t index = 0; index < activeLen; ++index) { + HttpConnectionBase* tmp = mActiveConns[index]; + if (tmp->CanDirectlyActivate()) { + if (tmp->IsExperienced()) { + experienced = tmp; + break; + } + noExperience = tmp; // keep looking for a better option + } + } + + // if that worked, cleanup anything else and exit + if (experienced) { + for (uint32_t index = 0; index < activeLen; ++index) { + HttpConnectionBase* tmp = mActiveConns[index]; + // in the case where there is a functional h2 session, drop the others + if (tmp != experienced) { + tmp->DontReuse(); + } + } + for (int32_t index = mHalfOpenFastOpenBackups.Length() - 1; index >= 0; + --index) { + LOG( + ("GetH2orH3ActiveConn() shutting down connection in fast " + "open state (%p) because we have an experienced spdy " + "connection (%p).\n", + mHalfOpenFastOpenBackups[index].get(), experienced)); + RefPtr half = mHalfOpenFastOpenBackups[index]; + half->CancelFastOpenConnection(); + } + + LOG( + ("GetH2orH3ActiveConn() request for ent %p %s " + "found an active experienced connection %p in native connection " + "entry\n", + this, mConnInfo->HashKey().get(), experienced)); + return experienced; + } + + if (noExperience) { + LOG( + ("GetH2orH3ActiveConn() request for ent %p %s " + "found an active but inexperienced connection %p in native connection " + "entry\n", + this, mConnInfo->HashKey().get(), noExperience)); + return noExperience; + } + + return nullptr; +} + +void ConnectionEntry::CloseActiveConnections() { + while (mActiveConns.Length()) { + RefPtr conn(mActiveConns[0]); + mActiveConns.RemoveElementAt(0); + gHttpHandler->ConnMgr()->DecrementActiveConnCount(conn); + + // Since HttpConnectionBase::Close doesn't break the bond with + // the connection's transaction, we must explicitely tell it + // to close its transaction and not just self. + conn->CloseTransaction(conn->Transaction(), NS_ERROR_ABORT, true); + } +} + +void ConnectionEntry::CloseAllActiveConnsWithNullTransactcion( + nsresult aCloseCode) { + for (uint32_t index = 0; index < mActiveConns.Length(); ++index) { + RefPtr activeConn = mActiveConns[index]; + nsAHttpTransaction* liveTransaction = activeConn->Transaction(); + if (liveTransaction && liveTransaction->IsNullTransaction()) { + LOG( + ("ConnectionEntry::CloseAllActiveConnsWithNullTransactcion " + "also canceling Null Transaction %p on conn %p\n", + liveTransaction, activeConn.get())); + activeConn->CloseTransaction(liveTransaction, aCloseCode); + } + } +} + +void ConnectionEntry::PruneNoTraffic() { + LOG((" pruning no traffic [ci=%s]\n", mConnInfo->HashKey().get())); + if (mConnInfo->IsHttp3()) { + return; + } + + uint32_t numConns = mActiveConns.Length(); + if (numConns) { + // Walk the list backwards to allow us to remove entries easily. + for (int index = numConns - 1; index >= 0; index--) { + RefPtr conn = do_QueryObject(mActiveConns[index]); + if (conn && conn->NoTraffic()) { + mActiveConns.RemoveElementAt(index); + gHttpHandler->ConnMgr()->DecrementActiveConnCount(conn); + conn->Close(NS_ERROR_ABORT); + LOG( + (" closed active connection due to no traffic " + "[conn=%p]\n", + conn.get())); + } + } + } +} + +uint32_t ConnectionEntry::TimeoutTick() { + uint32_t timeoutTickNext = 3600; // 1hr + + if (mConnInfo->IsHttp3()) { + return timeoutTickNext; + } + + LOG( + ("ConnectionEntry::TimeoutTick() this=%p host=%s " + "idle=%zu active=%zu" + " half-len=%zu pending=%zu" + " urgentStart pending=%zu\n", + this, mConnInfo->Origin(), IdleConnectionsLength(), ActiveConnsLength(), + mHalfOpens.Length(), PendingQueueLength(), UrgentStartQueueLength())); + + // First call the tick handler for each active connection. + PRIntervalTime tickTime = PR_IntervalNow(); + for (uint32_t index = 0; index < mActiveConns.Length(); ++index) { + RefPtr conn = do_QueryObject(mActiveConns[index]); + if (conn) { + uint32_t connNextTimeout = conn->ReadTimeoutTick(tickTime); + timeoutTickNext = std::min(timeoutTickNext, connNextTimeout); + } + } + + // Now check for any stalled half open sockets. + if (mHalfOpens.Length()) { + TimeStamp currentTime = TimeStamp::Now(); + double maxConnectTime_ms = gHttpHandler->ConnectTimeout(); + + for (uint32_t index = mHalfOpens.Length(); index > 0;) { + index--; + + HalfOpenSocket* half = mHalfOpens[index]; + double delta = half->Duration(currentTime); + // If the socket has timed out, close it so the waiting + // transaction will get the proper signal. + if (delta > maxConnectTime_ms) { + LOG(("Force timeout of half open to %s after %.2fms.\n", + mConnInfo->HashKey().get(), delta)); + if (half->SocketTransport()) { + half->SocketTransport()->Close(NS_ERROR_NET_TIMEOUT); + } + if (half->BackupTransport()) { + half->BackupTransport()->Close(NS_ERROR_NET_TIMEOUT); + } + } + + // If this half open hangs around for 5 seconds after we've + // closed() it then just abandon the socket. + if (delta > maxConnectTime_ms + 5000) { + LOG(("Abandon half open to %s after %.2fms.\n", + mConnInfo->HashKey().get(), delta)); + half->Abandon(); + } + } + } + if (mHalfOpens.Length()) { + timeoutTickNext = 1; + } + + return timeoutTickNext; +} + +void ConnectionEntry::MoveConnection(HttpConnectionBase* proxyConn, + ConnectionEntry* otherEnt) { + // To avoid changing mNumActiveConns/mNumIdleConns counter use internal + // functions. + RefPtr deleteProtector(proxyConn); + if (mActiveConns.RemoveElement(proxyConn)) { + otherEnt->mActiveConns.AppendElement(proxyConn); + return; + } + + RefPtr proxyConnTCP = do_QueryObject(proxyConn); + if (proxyConnTCP) { + if (mIdleConns.RemoveElement(proxyConnTCP)) { + otherEnt->InsertIntoIdleConnections_internal(proxyConnTCP); + return; + } + } +} + +void ConnectionEntry::InsertIntoHalfOpens(HalfOpenSocket* sock) { + mHalfOpens.AppendElement(sock); + gHttpHandler->ConnMgr()->IncreaseNumHalfOpenConns(); +} + +void ConnectionEntry::InsertIntoHalfOpenFastOpenBackups(HalfOpenSocket* sock) { + mHalfOpenFastOpenBackups.AppendElement(sock); +} + +void ConnectionEntry::CloseAllHalfOpens() { + for (int32_t i = int32_t(HalfOpensLength()) - 1; i >= 0; i--) { + mHalfOpens[i]->Abandon(); + } +} + +void ConnectionEntry::RemoveHalfOpenFastOpenBackups(HalfOpenSocket* sock) { + mHalfOpenFastOpenBackups.RemoveElement(sock); +} + +bool ConnectionEntry::IsInHalfOpens(HalfOpenSocket* sock) { + return mHalfOpens.Contains(sock); +} + +HttpRetParams ConnectionEntry::GetConnectionData() { + HttpRetParams data; + data.host = mConnInfo->Origin(); + data.port = mConnInfo->OriginPort(); + for (uint32_t i = 0; i < mActiveConns.Length(); i++) { + HttpConnInfo info; + RefPtr connTCP = do_QueryObject(mActiveConns[i]); + if (connTCP) { + info.ttl = connTCP->TimeToLive(); + } else { + info.ttl = 0; + } + info.rtt = mActiveConns[i]->Rtt(); + info.SetHTTPProtocolVersion(mActiveConns[i]->Version()); + data.active.AppendElement(info); + } + for (uint32_t i = 0; i < mIdleConns.Length(); i++) { + HttpConnInfo info; + info.ttl = mIdleConns[i]->TimeToLive(); + info.rtt = mIdleConns[i]->Rtt(); + info.SetHTTPProtocolVersion(mIdleConns[i]->Version()); + data.idle.AppendElement(info); + } + for (uint32_t i = 0; i < mHalfOpens.Length(); i++) { + HalfOpenSockets hSocket; + hSocket.speculative = mHalfOpens[i]->IsSpeculative(); + data.halfOpens.AppendElement(hSocket); + } + if (mConnInfo->IsHttp3()) { + data.httpVersion = "HTTP/3"_ns; + } else if (mUsingSpdy) { + data.httpVersion = "HTTP/2"_ns; + } else { + data.httpVersion = "HTTP <= 1.1"_ns; + } + data.ssl = mConnInfo->EndToEndSSL(); + return data; +} + +void ConnectionEntry::LogConnections() { + if (!mConnInfo->IsHttp3()) { + LOG(("active urgent conns [")); + for (HttpConnectionBase* conn : mActiveConns) { + RefPtr connTCP = do_QueryObject(conn); + MOZ_ASSERT(connTCP); + if (connTCP->IsUrgentStartPreferred()) { + LOG((" %p", conn)); + } + } + LOG(("] active regular conns [")); + for (HttpConnectionBase* conn : mActiveConns) { + RefPtr connTCP = do_QueryObject(conn); + MOZ_ASSERT(connTCP); + if (!connTCP->IsUrgentStartPreferred()) { + LOG((" %p", conn)); + } + } + + LOG(("] idle urgent conns [")); + for (nsHttpConnection* conn : mIdleConns) { + if (conn->IsUrgentStartPreferred()) { + LOG((" %p", conn)); + } + } + LOG(("] idle regular conns [")); + for (nsHttpConnection* conn : mIdleConns) { + if (!conn->IsUrgentStartPreferred()) { + LOG((" %p", conn)); + } + } + } else { + LOG(("active conns [")); + for (HttpConnectionBase* conn : mActiveConns) { + LOG((" %p", conn)); + } + MOZ_ASSERT(mIdleConns.Length() == 0); + } + LOG(("]")); +} + +bool ConnectionEntry::RemoveTransFromPendingQ(nsHttpTransaction* aTrans) { + // We will abandon all half-open sockets belonging to the given + // transaction. + nsTArray>* infoArray = + GetTransactionPendingQHelper(aTrans); + + RefPtr pendingTransInfo; + int32_t transIndex = + infoArray ? infoArray->IndexOf(aTrans, 0, PendingComparator()) : -1; + if (transIndex >= 0) { + pendingTransInfo = (*infoArray)[transIndex]; + infoArray->RemoveElementAt(transIndex); + } + + if (!pendingTransInfo) { + return false; + } + + // Abandon all half-open sockets belonging to the given transaction. + pendingTransInfo->AbandonHalfOpenAndForgetActiveConn(); + return true; +} + +void ConnectionEntry::MaybeUpdateEchConfig(nsHttpConnectionInfo* aConnInfo) { + if (!mConnInfo->HashKey().Equals(aConnInfo->HashKey())) { + return; + } + + const nsCString& echConfig = aConnInfo->GetEchConfig(); + if (mConnInfo->GetEchConfig().Equals(echConfig)) { + return; + } + + LOG(("ConnectionEntry::MaybeUpdateEchConfig [ci=%s]\n", + mConnInfo->HashKey().get())); + + mConnInfo->SetEchConfig(echConfig); + + // If echConfig is changed, we should close all half opens and idle + // connections. This is to make sure the new echConfig will be used for the + // next connection. + CloseAllHalfOpens(); + CloseIdleConnections(); +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/ConnectionEntry.h b/netwerk/protocol/http/ConnectionEntry.h new file mode 100644 index 0000000000..b654889af7 --- /dev/null +++ b/netwerk/protocol/http/ConnectionEntry.h @@ -0,0 +1,210 @@ +/* vim:set ts=4 sw=2 sts=2 et cin: */ +/* 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/. */ + +#ifndef ConnectionEntry_h__ +#define ConnectionEntry_h__ + +#include "PendingTransactionInfo.h" +#include "PendingTransactionQueue.h" +#include "HalfOpenSocket.h" +#include "DashboardTypes.h" + +namespace mozilla { +namespace net { + +// ConnectionEntry +// +// nsHttpConnectionMgr::mCT maps connection info hash key to ConnectionEntry +// object, which contains list of active and idle connections as well as the +// list of pending transactions. +class ConnectionEntry { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(ConnectionEntry) + explicit ConnectionEntry(nsHttpConnectionInfo* ci); + + void ReschedTransaction(nsHttpTransaction* aTrans); + + nsTArray>* GetTransactionPendingQHelper( + nsAHttpTransaction* trans); + + void InsertTransactionSorted( + nsTArray>& pendingQ, + PendingTransactionInfo* pendingTransInfo, + bool aInsertAsFirstForTheSamePriority = false); + + void InsertTransaction(PendingTransactionInfo* aPendingTransInfo, + bool aInsertAsFirstForTheSamePriority = false); + + size_t UrgentStartQueueLength(); + + void PrintPendingQ(); + + void Compact(); + + void CancelAllTransactions(nsresult reason); + + nsresult CloseIdleConnection(nsHttpConnection* conn); + void CloseIdleConnections(); + void CloseIdleConnections(uint32_t maxToClose); + nsresult RemoveIdleConnection(nsHttpConnection* conn); + bool IsInIdleConnections(HttpConnectionBase* conn); + size_t IdleConnectionsLength() const { return mIdleConns.Length(); } + void InsertIntoIdleConnections(nsHttpConnection* conn); + already_AddRefed GetIdleConnection(bool respectUrgency, + bool urgentTrans, + bool* onlyUrgent); + + size_t ActiveConnsLength() const { return mActiveConns.Length(); } + void InsertIntoActiveConns(HttpConnectionBase* conn); + bool IsInActiveConns(HttpConnectionBase* conn); + nsresult RemoveActiveConnection(HttpConnectionBase* conn); + void MakeAllDontReuseExcept(HttpConnectionBase* conn); + bool FindConnToClaim(PendingTransactionInfo* pendingTransInfo); + void CloseActiveConnections(); + void CloseAllActiveConnsWithNullTransactcion(nsresult aCloseCode); + + HttpConnectionBase* GetH2orH3ActiveConn(); + // Make an active spdy connection DontReuse. + // TODO: this is a helper function and should nbe improved. + bool MakeFirstActiveSpdyConnDontReuse(); + + void ClosePersistentConnections(); + + uint32_t PruneDeadConnections(); + void VerifyTraffic(); + void PruneNoTraffic(); + uint32_t TimeoutTick(); + + void MoveConnection(HttpConnectionBase* proxyConn, ConnectionEntry* otherEnt); + + size_t HalfOpensLength() const { return mHalfOpens.Length(); } + size_t HalfOpenFastOpenBackupsLength() const { + return mHalfOpenFastOpenBackups.Length(); + } + void InsertIntoHalfOpens(HalfOpenSocket* sock); + void InsertIntoHalfOpenFastOpenBackups(HalfOpenSocket* sock); + void CloseAllHalfOpens(); + void RemoveHalfOpenFastOpenBackups(HalfOpenSocket* sock); + bool IsInHalfOpens(HalfOpenSocket* sock); + + HttpRetParams GetConnectionData(); + void LogConnections(); + + RefPtr mConnInfo; + + bool AvailableForDispatchNow(); + + // calculate the number of half open sockets that have not had at least 1 + // connection complete + uint32_t UnconnectedHalfOpens() const; + + // Remove a particular half open socket from the mHalfOpens array + bool RemoveHalfOpen(HalfOpenSocket*); + + // Spdy sometimes resolves the address in the socket manager in order + // to re-coalesce sharded HTTP hosts. The dotted decimal address is + // combined with the Anonymous flag and OA from the connection information + // to build the hash key for hosts in the same ip pool. + // + nsTArray mCoalescingKeys; + + // To have the UsingSpdy flag means some host with the same connection + // entry has done NPN=spdy/* at some point. It does not mean every + // connection is currently using spdy. + bool mUsingSpdy : 1; + + // Determines whether or not we can continue to use spdy-enabled + // connections in the future. This is generally set to false either when + // some connection here encounters connection-based auth (such as NTLM) + // or when some connection here encounters a server that is totally + // busted (such as it fails to properly perform the h2 handshake). + bool mCanUseSpdy : 1; + + // Flags to remember our happy-eyeballs decision. + // Reset only by Ctrl-F5 reload. + // True when we've first connected an IPv4 server for this host, + // initially false. + bool mPreferIPv4 : 1; + // True when we've first connected an IPv6 server for this host, + // initially false. + bool mPreferIPv6 : 1; + + // True if this connection entry has initiated a socket + bool mUsedForConnection : 1; + + // Try using TCP Fast Open. + bool mUseFastOpen : 1; + + bool mDoNotDestroy : 1; + + bool IsHttp3() const { return mConnInfo->IsHttp3(); } + bool AllowHttp2() const { return mCanUseSpdy; } + void DisallowHttp2(); + void DontReuseHttp3Conn(); + + // Set the IP family preference flags according the connected family + void RecordIPFamilyPreference(uint16_t family); + // Resets all flags to their default values + void ResetIPFamilyPreference(); + // True iff there is currently an established IP family preference + bool PreferenceKnown() const; + + // Return the count of pending transactions for all window ids. + size_t PendingQueueLength() const; + size_t PendingQueueLengthForWindow(uint64_t windowId) const; + + void AppendPendingUrgentStartQ( + nsTArray>& result); + + // Append transactions to the |result| whose window id + // is equal to |windowId|. + // NOTE: maxCount == 0 will get all transactions in the queue. + void AppendPendingQForFocusedWindow( + uint64_t windowId, nsTArray>& result, + uint32_t maxCount = 0); + + // Append transactions whose window id isn't equal to |windowId|. + // NOTE: windowId == 0 will get all transactions for both + // focused and non-focused windows. + void AppendPendingQForNonFocusedWindows( + uint64_t windowId, nsTArray>& result, + uint32_t maxCount = 0); + + // Remove the empty pendingQ in |mPendingTransactionTable|. + void RemoveEmptyPendingQ(); + + void PrintDiagnostics(nsCString& log, uint32_t aMaxPersistConns); + + bool RestrictConnections(); + + // Return total active connection count, which is the sum of + // active connections and unconnected half open connections. + uint32_t TotalActiveConnections() const; + + bool RemoveTransFromPendingQ(nsHttpTransaction* aTrans); + + void MaybeUpdateEchConfig(nsHttpConnectionInfo* aConnInfo); + + private: + void InsertIntoIdleConnections_internal(nsHttpConnection* conn); + void RemoveFromIdleConnectionsIndex(size_t inx); + bool RemoveFromIdleConnections(nsHttpConnection* conn); + + nsTArray> mIdleConns; // idle persistent connections + nsTArray> mActiveConns; // active connections + + nsTArray mHalfOpens; // half open connections + nsTArray> + mHalfOpenFastOpenBackups; // backup half open connections for + // connection in fast open phase + + PendingTransactionQueue mPendingQ; + ~ConnectionEntry(); +}; + +} // namespace net +} // namespace mozilla + +#endif // !ConnectionEntry_h__ diff --git a/netwerk/protocol/http/ConnectionHandle.cpp b/netwerk/protocol/http/ConnectionHandle.cpp new file mode 100644 index 0000000000..aa231e0b3e --- /dev/null +++ b/netwerk/protocol/http/ConnectionHandle.cpp @@ -0,0 +1,87 @@ +/* vim:set ts=4 sw=2 sts=2 et cin: */ +/* 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/. */ + +// HttpLog.h should generally be included first +#include "HttpLog.h" + +// Log on level :5, instead of default :4. +#undef LOG +#define LOG(args) LOG5(args) +#undef LOG_ENABLED +#define LOG_ENABLED() LOG5_ENABLED() + +#include "ConnectionHandle.h" + +namespace mozilla { +namespace net { + +ConnectionHandle::~ConnectionHandle() { + if (mConn) { + nsresult rv = gHttpHandler->ReclaimConnection(mConn); + if (NS_FAILED(rv)) { + LOG( + ("ConnectionHandle::~ConnectionHandle\n" + " failed to reclaim connection\n")); + } + } +} + +NS_IMPL_ISUPPORTS0(ConnectionHandle) + +nsresult ConnectionHandle::OnHeadersAvailable(nsAHttpTransaction* trans, + nsHttpRequestHead* req, + nsHttpResponseHead* resp, + bool* reset) { + return mConn->OnHeadersAvailable(trans, req, resp, reset); +} + +void ConnectionHandle::CloseTransaction(nsAHttpTransaction* trans, + nsresult reason) { + mConn->CloseTransaction(trans, reason); +} + +nsresult ConnectionHandle::TakeTransport(nsISocketTransport** aTransport, + nsIAsyncInputStream** aInputStream, + nsIAsyncOutputStream** aOutputStream) { + return mConn->TakeTransport(aTransport, aInputStream, aOutputStream); +} + +bool ConnectionHandle::IsPersistent() { + MOZ_ASSERT(OnSocketThread()); + return mConn->IsPersistent(); +} + +bool ConnectionHandle::IsReused() { + MOZ_ASSERT(OnSocketThread()); + return mConn->IsReused(); +} + +void ConnectionHandle::DontReuse() { + MOZ_ASSERT(OnSocketThread()); + mConn->DontReuse(); +} + +nsresult ConnectionHandle::PushBack(const char* buf, uint32_t bufLen) { + return mConn->PushBack(buf, bufLen); +} + +already_AddRefed ConnectionHandle::TakeHttpConnection() { + // return our connection object to the caller and clear it internally + // do not drop our reference - the caller now owns it. + MOZ_ASSERT(mConn); + return mConn.forget(); +} + +already_AddRefed ConnectionHandle::HttpConnection() { + RefPtr rv(mConn); + return rv.forget(); +} + +void ConnectionHandle::TopLevelOuterContentWindowIdChanged(uint64_t windowId) { + // Do nothing. +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/ConnectionHandle.h b/netwerk/protocol/http/ConnectionHandle.h new file mode 100644 index 0000000000..098c0adc17 --- /dev/null +++ b/netwerk/protocol/http/ConnectionHandle.h @@ -0,0 +1,38 @@ +/* vim:set ts=4 sw=2 sts=2 et cin: */ +/* 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/. */ + +#ifndef ConnectionHandle_h__ +#define ConnectionHandle_h__ + +namespace mozilla { +namespace net { + +//----------------------------------------------------------------------------- +// ConnectionHandle +// +// thin wrapper around a real connection, used to keep track of references +// to the connection to determine when the connection may be reused. the +// transaction owns a reference to this handle. this extra +// layer of indirection greatly simplifies consumer code, avoiding the +// need for consumer code to know when to give the connection back to the +// connection manager. +// +class ConnectionHandle : public nsAHttpConnection { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSAHTTPCONNECTION(mConn) + + explicit ConnectionHandle(HttpConnectionBase* conn) : mConn(conn) {} + void Reset() { mConn = nullptr; } + + private: + virtual ~ConnectionHandle(); + RefPtr mConn; +}; + +} // namespace net +} // namespace mozilla + +#endif // ConnectionHandle_h__ diff --git a/netwerk/protocol/http/DelayHttpChannelQueue.cpp b/netwerk/protocol/http/DelayHttpChannelQueue.cpp new file mode 100644 index 0000000000..03500ef943 --- /dev/null +++ b/netwerk/protocol/http/DelayHttpChannelQueue.cpp @@ -0,0 +1,122 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set et cin ts=4 sw=2 sts=2: */ +/* 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 "DelayHttpChannelQueue.h" +#include "mozilla/Services.h" +#include "mozilla/StaticPtr.h" +#include "mozilla/TimeStamp.h" +#include "nsIObserverService.h" +#include "nsHttpChannel.h" +#include "nsThreadManager.h" + +using namespace mozilla; +using namespace mozilla::net; + +namespace { +StaticRefPtr sDelayHttpChannelQueue; +} + +bool DelayHttpChannelQueue::AttemptQueueChannel(nsHttpChannel* aChannel) { + MOZ_ASSERT(aChannel); + MOZ_ASSERT(NS_IsMainThread()); + + if (!TimeStamp::GetFuzzyfoxEnabled()) { + return false; + } + + if (!sDelayHttpChannelQueue) { + RefPtr queue = new DelayHttpChannelQueue(); + if (!queue->Initialize()) { + return false; + } + + sDelayHttpChannelQueue = queue; + } + + if (NS_WARN_IF( + !sDelayHttpChannelQueue->mQueue.AppendElement(aChannel, fallible))) { + return false; + } + + return true; +} + +DelayHttpChannelQueue::DelayHttpChannelQueue() { + MOZ_ASSERT(NS_IsMainThread()); +} + +DelayHttpChannelQueue::~DelayHttpChannelQueue() { + MOZ_ASSERT(NS_IsMainThread()); +} + +bool DelayHttpChannelQueue::Initialize() { + MOZ_ASSERT(NS_IsMainThread()); + + nsCOMPtr obs = services::GetObserverService(); + if (NS_WARN_IF(!obs)) { + return false; + } + + nsresult rv = obs->AddObserver(this, "fuzzyfox-fire-outbound", false); + if (NS_WARN_IF(NS_FAILED(rv))) { + return false; + } + + rv = obs->AddObserver(this, "xpcom-shutdown", false); + if (NS_WARN_IF(NS_FAILED(rv))) { + return false; + } + + return true; +} + +NS_IMETHODIMP +DelayHttpChannelQueue::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) { + MOZ_ASSERT(NS_IsMainThread()); + + if (!strcmp(aTopic, "fuzzyfox-fire-outbound")) { + FireQueue(); + return NS_OK; + } + + MOZ_ASSERT(!strcmp(aTopic, "xpcom-shutdown")); + + nsCOMPtr obs = services::GetObserverService(); + if (NS_WARN_IF(!obs)) { + return NS_OK; + } + + obs->RemoveObserver(this, "fuzzyfox-fire-outbound"); + obs->RemoveObserver(this, "xpcom-shutdown"); + + return NS_OK; +} + +void DelayHttpChannelQueue::FireQueue() { + MOZ_ASSERT(NS_IsMainThread()); + + if (mQueue.IsEmpty()) { + return; + } + + // TODO: get this from the DOM clock? + TimeStamp ts = TimeStamp::Now(); + + FallibleTArray> queue = std::move(mQueue); + + for (RefPtr& channel : queue) { + channel->AsyncOpenFinal(ts); + } +} + +NS_INTERFACE_MAP_BEGIN(DelayHttpChannelQueue) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIObserver) + NS_INTERFACE_MAP_ENTRY(nsIObserver) +NS_INTERFACE_MAP_END + +NS_IMPL_ADDREF(DelayHttpChannelQueue) +NS_IMPL_RELEASE(DelayHttpChannelQueue) diff --git a/netwerk/protocol/http/DelayHttpChannelQueue.h b/netwerk/protocol/http/DelayHttpChannelQueue.h new file mode 100644 index 0000000000..491160734d --- /dev/null +++ b/netwerk/protocol/http/DelayHttpChannelQueue.h @@ -0,0 +1,46 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set et cin ts=4 sw=2 sts=2: */ +/* 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/. */ + +#ifndef mozilla_net_DelayHttpChannelQueue_h +#define mozilla_net_DelayHttpChannelQueue_h + +#include "nsIObserver.h" +#include "nsTArray.h" + +namespace mozilla { +namespace net { + +class nsHttpChannel; + +/** + * DelayHttpChannelQueue stores a set of nsHttpChannels that + * are ready to fire out onto the network. However, with FuzzyFox, + * we can only fire those events at a specific interval, so we + * delay them here, in an instance of this class, until we observe + * the topic notificaion that we can send them outbound. + */ +class DelayHttpChannelQueue final : public nsIObserver { + public: + static bool AttemptQueueChannel(nsHttpChannel* aChannel); + + NS_DECL_ISUPPORTS + NS_DECL_NSIOBSERVER + + private: + DelayHttpChannelQueue(); + ~DelayHttpChannelQueue(); + + bool Initialize(); + + void FireQueue(); + + FallibleTArray> mQueue; +}; + +} // namespace net +} // namespace mozilla + +#endif // mozilla_net_DelayHttpChannelQueue_h diff --git a/netwerk/protocol/http/HTTPSRecordResolver.cpp b/netwerk/protocol/http/HTTPSRecordResolver.cpp new file mode 100644 index 0000000000..d83212eca6 --- /dev/null +++ b/netwerk/protocol/http/HTTPSRecordResolver.cpp @@ -0,0 +1,99 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +// HttpLog.h should generally be included first +#include "HttpLog.h" + +#include "HTTPSRecordResolver.h" +#include "nsIDNSByTypeRecord.h" +#include "nsIDNSService.h" +#include "nsHttpConnectionInfo.h" + +namespace mozilla { +namespace net { + +NS_IMPL_ISUPPORTS(HTTPSRecordResolver, nsIDNSListener) + +HTTPSRecordResolver::HTTPSRecordResolver(nsAHttpTransaction* aTransaction) + : mTransaction(aTransaction), + mConnInfo(aTransaction->ConnectionInfo()), + mCaps(aTransaction->Caps()) {} + +HTTPSRecordResolver::~HTTPSRecordResolver() = default; + +nsresult HTTPSRecordResolver::FetchHTTPSRRInternal( + nsIEventTarget* aTarget, nsICancelable** aDNSRequest) { + NS_ENSURE_ARG_POINTER(aTarget); + + // Only fetch HTTPS RR for https. + if (!mConnInfo->FirstHopSSL()) { + return NS_ERROR_FAILURE; + } + + nsCOMPtr dns = do_GetService(NS_DNSSERVICE_CONTRACTID); + if (!dns) { + return NS_ERROR_NOT_AVAILABLE; + } + + uint32_t flags = nsIDNSService::GetFlagsFromTRRMode(mConnInfo->GetTRRMode()); + if (mCaps & NS_HTTP_REFRESH_DNS) { + flags |= nsIDNSService::RESOLVE_BYPASS_CACHE; + } + + return dns->AsyncResolveNative( + mConnInfo->GetOrigin(), nsIDNSService::RESOLVE_TYPE_HTTPSSVC, flags, + nullptr, this, aTarget, mConnInfo->GetOriginAttributes(), aDNSRequest); +} + +NS_IMETHODIMP HTTPSRecordResolver::OnLookupComplete(nsICancelable* aRequest, + nsIDNSRecord* aRecord, + nsresult aStatus) { + nsCOMPtr addrRecord = do_QueryInterface(aRecord); + // This will be called again when receving the result of speculatively loading + // the addr records of the target name. In this case, just return NS_OK. + if (addrRecord) { + return NS_OK; + } + + nsCOMPtr record = do_QueryInterface(aRecord); + if (!record || NS_FAILED(aStatus)) { + return mTransaction->OnHTTPSRRAvailable(nullptr, nullptr); + } + + nsCOMPtr svcbRecord; + if (NS_FAILED(record->GetServiceModeRecord(mCaps & NS_HTTP_DISALLOW_SPDY, + mCaps & NS_HTTP_DISALLOW_HTTP3, + getter_AddRefs(svcbRecord)))) { + return mTransaction->OnHTTPSRRAvailable(record, nullptr); + } + + return mTransaction->OnHTTPSRRAvailable(record, svcbRecord); +} + +void HTTPSRecordResolver::PrefetchAddrRecord(const nsACString& aTargetName, + bool aRefreshDNS) { + nsCOMPtr dns = do_GetService(NS_DNSSERVICE_CONTRACTID); + if (!dns) { + return; + } + + uint32_t flags = nsIDNSService::GetFlagsFromTRRMode( + mTransaction->ConnectionInfo()->GetTRRMode()); + if (aRefreshDNS) { + flags |= nsIDNSService::RESOLVE_BYPASS_CACHE; + } + + nsCOMPtr tmpOutstanding; + + Unused << dns->AsyncResolveNative( + aTargetName, nsIDNSService::RESOLVE_TYPE_DEFAULT, + flags | nsIDNSService::RESOLVE_SPECULATE, nullptr, this, + GetCurrentEventTarget(), + mTransaction->ConnectionInfo()->GetOriginAttributes(), + getter_AddRefs(tmpOutstanding)); +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/HTTPSRecordResolver.h b/netwerk/protocol/http/HTTPSRecordResolver.h new file mode 100644 index 0000000000..15eb997634 --- /dev/null +++ b/netwerk/protocol/http/HTTPSRecordResolver.h @@ -0,0 +1,41 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* vim:set ts=4 sw=4 sts=4 et cin: */ +/* 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/. */ + +#ifndef HTTPSRecordResolver_h__ +#define HTTPSRecordResolver_h__ + +#include "nsICancelable.h" +#include "nsIDNSListener.h" + +namespace mozilla { +namespace net { + +class nsAHttpTransaction; + +// This class is the place to put some common code about fetching HTTPS RR. +class HTTPSRecordResolver : public nsIDNSListener { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIDNSLISTENER + + explicit HTTPSRecordResolver(nsAHttpTransaction* aTransaction); + nsresult FetchHTTPSRRInternal(nsIEventTarget* aTarget, + nsICancelable** aDNSRequest); + void PrefetchAddrRecord(const nsACString& aTargetName, bool aRefreshDNS); + + protected: + virtual ~HTTPSRecordResolver(); + + private: + RefPtr mTransaction; + RefPtr mConnInfo; + uint32_t mCaps; +}; + +} // namespace net +} // namespace mozilla + +#endif // HTTPSRecordResolver_h__ diff --git a/netwerk/protocol/http/HalfOpenSocket.cpp b/netwerk/protocol/http/HalfOpenSocket.cpp new file mode 100644 index 0000000000..815f2f41c4 --- /dev/null +++ b/netwerk/protocol/http/HalfOpenSocket.cpp @@ -0,0 +1,1307 @@ +/* vim:set ts=4 sw=2 sts=2 et cin: */ +/* 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/. */ + +// HttpLog.h should generally be included first +#include "HttpLog.h" + +#include "ConnectionHandle.h" +#include "HalfOpenSocket.h" +#include "TCPFastOpenLayer.h" +#include "nsHttpConnection.h" +#include "nsIDNSRecord.h" +#include "nsIDNSService.h" +#include "nsQueryObject.h" + +// Log on level :5, instead of default :4. +#undef LOG +#define LOG(args) LOG5(args) +#undef LOG_ENABLED +#define LOG_ENABLED() LOG5_ENABLED() + +namespace mozilla { +namespace net { + +//////////////////////// HalfOpenSocket +NS_IMPL_ADDREF(HalfOpenSocket) +NS_IMPL_RELEASE(HalfOpenSocket) + +NS_INTERFACE_MAP_BEGIN(HalfOpenSocket) + NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference) + NS_INTERFACE_MAP_ENTRY(nsIOutputStreamCallback) + NS_INTERFACE_MAP_ENTRY(nsITransportEventSink) + NS_INTERFACE_MAP_ENTRY(nsIInterfaceRequestor) + NS_INTERFACE_MAP_ENTRY(nsITimerCallback) + NS_INTERFACE_MAP_ENTRY(nsINamed) + NS_INTERFACE_MAP_ENTRY_CONCRETE(HalfOpenSocket) +NS_INTERFACE_MAP_END + +HalfOpenSocket::HalfOpenSocket(ConnectionEntry* ent, nsAHttpTransaction* trans, + uint32_t caps, bool speculative, + bool isFromPredictor, bool urgentStart) + : mTransaction(trans), + mDispatchedMTransaction(false), + mCaps(caps), + mSpeculative(speculative), + mUrgentStart(urgentStart), + mIsFromPredictor(isFromPredictor), + mAllow1918(true), + mHasConnected(false), + mPrimaryConnectedOK(false), + mBackupConnectedOK(false), + mBackupConnStatsSet(false), + mFreeToUse(true), + mPrimaryStreamStatus(NS_OK), + mFastOpenInProgress(false), + mEnt(ent) { + MOZ_ASSERT(ent && trans, "constructor with null arguments"); + LOG(("Creating HalfOpenSocket [this=%p trans=%p ent=%s key=%s]\n", this, + trans, ent->mConnInfo->Origin(), ent->mConnInfo->HashKey().get())); + + mIsHttp3 = mEnt->mConnInfo->IsHttp3(); + if (speculative) { + Telemetry::AutoCounter + totalSpeculativeConn; + ++totalSpeculativeConn; + + if (isFromPredictor) { + Telemetry::AutoCounter + totalPreconnectsCreated; + ++totalPreconnectsCreated; + } + } + + if (mEnt->mConnInfo->FirstHopSSL()) { + mFastOpenStatus = TFO_UNKNOWN; + } else { + mFastOpenStatus = TFO_HTTP; + } + MOZ_ASSERT(mEnt); +} + +HalfOpenSocket::~HalfOpenSocket() { + MOZ_ASSERT(!mStreamOut); + MOZ_ASSERT(!mBackupStreamOut); + LOG(("Destroying HalfOpenSocket [this=%p]\n", this)); + + if (mEnt) { + bool inqueue = mEnt->RemoveHalfOpen(this); + LOG(("Destroying HalfOpenSocket was in the HalfOpenList=%d [this=%p]\n", + inqueue, this)); + } +} + +nsresult HalfOpenSocket::SetupStreams(nsISocketTransport** transport, + nsIAsyncInputStream** instream, + nsIAsyncOutputStream** outstream, + bool isBackup) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + MOZ_ASSERT(mEnt); + nsresult rv; + nsTArray socketTypes; + const nsHttpConnectionInfo* ci = mEnt->mConnInfo; + if (mIsHttp3) { + socketTypes.AppendElement("quic"_ns); + } else { + if (ci->FirstHopSSL()) { + socketTypes.AppendElement("ssl"_ns); + } else { + const nsCString& defaultType = gHttpHandler->DefaultSocketType(); + if (!defaultType.IsVoid()) { + socketTypes.AppendElement(defaultType); + } + } + } + + nsCOMPtr socketTransport; + nsCOMPtr sts; + + sts = services::GetSocketTransportService(); + if (!sts) { + return NS_ERROR_NOT_AVAILABLE; + } + + LOG( + ("HalfOpenSocket::SetupStreams [this=%p ent=%s] " + "setup routed transport to origin %s:%d via %s:%d\n", + this, ci->HashKey().get(), ci->Origin(), ci->OriginPort(), + ci->RoutedHost(), ci->RoutedPort())); + + nsCOMPtr routedSTS(do_QueryInterface(sts)); + if (routedSTS) { + rv = routedSTS->CreateRoutedTransport( + socketTypes, ci->GetOrigin(), ci->OriginPort(), ci->GetRoutedHost(), + ci->RoutedPort(), ci->ProxyInfo(), getter_AddRefs(socketTransport)); + } else { + if (!ci->GetRoutedHost().IsEmpty()) { + // There is a route requested, but the legacy nsISocketTransportService + // can't handle it. + // Origin should be reachable on origin host name, so this should + // not be a problem - but log it. + LOG( + ("HalfOpenSocket this=%p using legacy nsISocketTransportService " + "means explicit route %s:%d will be ignored.\n", + this, ci->RoutedHost(), ci->RoutedPort())); + } + + rv = sts->CreateTransport(socketTypes, ci->GetOrigin(), ci->OriginPort(), + ci->ProxyInfo(), getter_AddRefs(socketTransport)); + } + NS_ENSURE_SUCCESS(rv, rv); + + uint32_t tmpFlags = 0; + if (mCaps & NS_HTTP_REFRESH_DNS) { + tmpFlags = nsISocketTransport::BYPASS_CACHE; + } + + tmpFlags |= nsISocketTransport::GetFlagsFromTRRMode( + NS_HTTP_TRR_MODE_FROM_FLAGS(mCaps)); + + if (mCaps & NS_HTTP_LOAD_ANONYMOUS) { + tmpFlags |= nsISocketTransport::ANONYMOUS_CONNECT; + } + + if (ci->GetPrivate() || ci->GetIsolated()) { + tmpFlags |= nsISocketTransport::NO_PERMANENT_STORAGE; + } + + Unused << socketTransport->SetIsPrivate(ci->GetPrivate()); + + if (ci->GetLessThanTls13()) { + tmpFlags |= nsISocketTransport::DONT_TRY_ECH; + } + + if (((mCaps & NS_HTTP_BE_CONSERVATIVE) || ci->GetBeConservative()) && + gHttpHandler->ConnMgr()->BeConservativeIfProxied(ci->ProxyInfo())) { + LOG(("Setting Socket to BE_CONSERVATIVE")); + tmpFlags |= nsISocketTransport::BE_CONSERVATIVE; + } + + if (ci->HasIPHintAddress()) { + nsCOMPtr dns = + do_GetService("@mozilla.org/network/dns-service;1", &rv); + if (NS_FAILED(rv)) { + return rv; + } + + // The spec says: "If A and AAAA records for TargetName are locally + // available, the client SHOULD ignore these hints.", so we check if the DNS + // record is in cache before setting USE_IP_HINT_ADDRESS. + nsCOMPtr record; + rv = dns->ResolveNative(ci->GetRoutedHost(), nsIDNSService::RESOLVE_OFFLINE, + mEnt->mConnInfo->GetOriginAttributes(), + getter_AddRefs(record)); + if (NS_FAILED(rv) || !record) { + LOG(("Setting Socket to use IP hint address")); + tmpFlags |= nsISocketTransport::USE_IP_HINT_ADDRESS; + } + } + + if (mCaps & NS_HTTP_DISABLE_IPV4) { + tmpFlags |= nsISocketTransport::DISABLE_IPV4; + } else if (mCaps & NS_HTTP_DISABLE_IPV6) { + tmpFlags |= nsISocketTransport::DISABLE_IPV6; + } else if (mEnt->PreferenceKnown()) { + if (mEnt->mPreferIPv6) { + tmpFlags |= nsISocketTransport::DISABLE_IPV4; + } else if (mEnt->mPreferIPv4) { + tmpFlags |= nsISocketTransport::DISABLE_IPV6; + } + + // In case the host is no longer accessible via the preferred IP family, + // try the opposite one and potentially restate the preference. + tmpFlags |= nsISocketTransport::RETRY_WITH_DIFFERENT_IP_FAMILY; + + // From the same reason, let the backup socket fail faster to try the other + // family. + uint16_t fallbackTimeout = + isBackup ? gHttpHandler->GetFallbackSynTimeout() : 0; + if (fallbackTimeout) { + socketTransport->SetTimeout(nsISocketTransport::TIMEOUT_CONNECT, + fallbackTimeout); + } + } else if (isBackup && gHttpHandler->FastFallbackToIPv4()) { + // For backup connections, we disable IPv6. That's because some users have + // broken IPv6 connectivity (leading to very long timeouts), and disabling + // IPv6 on the backup connection gives them a much better user experience + // with dual-stack hosts, though they still pay the 250ms delay for each new + // connection. This strategy is also known as "happy eyeballs". + tmpFlags |= nsISocketTransport::DISABLE_IPV6; + } + + if (!Allow1918()) { + tmpFlags |= nsISocketTransport::DISABLE_RFC1918; + } + + if ((mFastOpenStatus != TFO_HTTP) && !isBackup) { + if (mEnt->mUseFastOpen) { + socketTransport->SetFastOpenCallback(this); + } else { + mFastOpenStatus = TFO_DISABLED; + } + } + + MOZ_ASSERT(!(tmpFlags & nsISocketTransport::DISABLE_IPV4) || + !(tmpFlags & nsISocketTransport::DISABLE_IPV6), + "Both types should not be disabled at the same time."); + socketTransport->SetConnectionFlags(tmpFlags); + socketTransport->SetTlsFlags(ci->GetTlsFlags()); + + const OriginAttributes& originAttributes = + mEnt->mConnInfo->GetOriginAttributes(); + if (originAttributes != OriginAttributes()) { + socketTransport->SetOriginAttributes(originAttributes); + } + + socketTransport->SetQoSBits(gHttpHandler->GetQoSBits()); + + rv = socketTransport->SetEventSink(this, nullptr); + NS_ENSURE_SUCCESS(rv, rv); + + rv = socketTransport->SetSecurityCallbacks(this); + NS_ENSURE_SUCCESS(rv, rv); + + if (gHttpHandler->EchConfigEnabled()) { + rv = socketTransport->SetEchConfig(ci->GetEchConfig()); + NS_ENSURE_SUCCESS(rv, rv); + } + + Telemetry::Accumulate(Telemetry::HTTP_CONNECTION_ENTRY_CACHE_HIT_1, + mEnt->mUsedForConnection); + mEnt->mUsedForConnection = true; + + nsCOMPtr sout; + rv = socketTransport->OpenOutputStream(nsITransport::OPEN_UNBUFFERED, 0, 0, + getter_AddRefs(sout)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr sin; + rv = socketTransport->OpenInputStream(nsITransport::OPEN_UNBUFFERED, 0, 0, + getter_AddRefs(sin)); + NS_ENSURE_SUCCESS(rv, rv); + + socketTransport.forget(transport); + CallQueryInterface(sin, instream); + CallQueryInterface(sout, outstream); + + rv = (*outstream)->AsyncWait(this, 0, 0, nullptr); + if (NS_SUCCEEDED(rv)) { + gHttpHandler->ConnMgr()->StartedConnect(); + } + + return rv; +} + +nsresult HalfOpenSocket::SetupPrimaryStreams() { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + nsresult rv; + + mPrimarySynStarted = TimeStamp::Now(); + rv = SetupStreams(getter_AddRefs(mSocketTransport), getter_AddRefs(mStreamIn), + getter_AddRefs(mStreamOut), false); + + LOG(("HalfOpenSocket::SetupPrimaryStream [this=%p ent=%s rv=%" PRIx32 "]", + this, mEnt->mConnInfo->Origin(), static_cast(rv))); + if (NS_FAILED(rv)) { + if (mStreamOut) { + mStreamOut->AsyncWait(nullptr, 0, 0, nullptr); + } + if (mSocketTransport) { + mSocketTransport->SetFastOpenCallback(nullptr); + } + mStreamOut = nullptr; + mStreamIn = nullptr; + mSocketTransport = nullptr; + } + return rv; +} + +nsresult HalfOpenSocket::SetupBackupStreams() { + MOZ_ASSERT(mTransaction); + + mBackupSynStarted = TimeStamp::Now(); + nsresult rv = SetupStreams(getter_AddRefs(mBackupTransport), + getter_AddRefs(mBackupStreamIn), + getter_AddRefs(mBackupStreamOut), true); + + LOG(("HalfOpenSocket::SetupBackupStream [this=%p ent=%s rv=%" PRIx32 "]", + this, mEnt->mConnInfo->Origin(), static_cast(rv))); + if (NS_FAILED(rv)) { + if (mBackupStreamOut) { + mBackupStreamOut->AsyncWait(nullptr, 0, 0, nullptr); + } + mBackupStreamOut = nullptr; + mBackupStreamIn = nullptr; + mBackupTransport = nullptr; + } + return rv; +} + +void HalfOpenSocket::SetupBackupTimer() { + MOZ_ASSERT(mEnt); + uint16_t timeout = gHttpHandler->GetIdleSynTimeout(); + MOZ_ASSERT(!mSynTimer, "timer already initd"); + if (!timeout && mFastOpenInProgress) { + timeout = 250; + } + // When using Fast Open the correct transport will be setup for sure (it is + // guaranteed), but it can be that it will happened a bit later. + if (mFastOpenInProgress || (timeout && !mSpeculative)) { + // Setup the timer that will establish a backup socket + // if we do not get a writable event on the main one. + // We do this because a lost SYN takes a very long time + // to repair at the TCP level. + // + // Failure to setup the timer is something we can live with, + // so don't return an error in that case. + NS_NewTimerWithCallback(getter_AddRefs(mSynTimer), this, timeout, + nsITimer::TYPE_ONE_SHOT); + LOG(("HalfOpenSocket::SetupBackupTimer() [this=%p]", this)); + } else if (timeout) { + LOG(("HalfOpenSocket::SetupBackupTimer() [this=%p], did not arm\n", this)); + } +} + +void HalfOpenSocket::CancelBackupTimer() { + // If the syntimer is still armed, we can cancel it because no backup + // socket should be formed at this point + if (!mSynTimer) { + return; + } + + LOG(("HalfOpenSocket::CancelBackupTimer()")); + mSynTimer->Cancel(); + + // Keeping the reference to the timer to remember we have already + // performed the backup connection. +} + +void HalfOpenSocket::Abandon() { + LOG(("HalfOpenSocket::Abandon [this=%p ent=%s] %p %p %p %p", this, + mEnt->mConnInfo->Origin(), mSocketTransport.get(), + mBackupTransport.get(), mStreamOut.get(), mBackupStreamOut.get())); + + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + RefPtr deleteProtector(this); + + // Tell socket (and backup socket) to forget the half open socket. + if (mSocketTransport) { + mSocketTransport->SetEventSink(nullptr, nullptr); + mSocketTransport->SetSecurityCallbacks(nullptr); + mSocketTransport->SetFastOpenCallback(nullptr); + mSocketTransport = nullptr; + } + if (mBackupTransport) { + mBackupTransport->SetEventSink(nullptr, nullptr); + mBackupTransport->SetSecurityCallbacks(nullptr); + mBackupTransport = nullptr; + } + + // Tell output stream (and backup) to forget the half open socket. + if (mStreamOut) { + if (!mFastOpenInProgress) { + // If mFastOpenInProgress is true HalfOpen are not in mHalfOpen + // list and are not counted so we do not need to decrease counter. + gHttpHandler->ConnMgr()->RecvdConnect(); + } + mStreamOut->AsyncWait(nullptr, 0, 0, nullptr); + mStreamOut = nullptr; + } + if (mBackupStreamOut) { + gHttpHandler->ConnMgr()->RecvdConnect(); + mBackupStreamOut->AsyncWait(nullptr, 0, 0, nullptr); + mBackupStreamOut = nullptr; + } + + // Lose references to input stream (and backup). + if (mStreamIn) { + mStreamIn->AsyncWait(nullptr, 0, 0, nullptr); + mStreamIn = nullptr; + } + if (mBackupStreamIn) { + mBackupStreamIn->AsyncWait(nullptr, 0, 0, nullptr); + mBackupStreamIn = nullptr; + } + + // Stop the timer - we don't want any new backups. + CancelBackupTimer(); + + // Remove the half open from the connection entry. + if (mEnt) { + mEnt->mDoNotDestroy = false; + mEnt->RemoveHalfOpen(this); + } + mEnt = nullptr; +} + +double HalfOpenSocket::Duration(TimeStamp epoch) { + if (mPrimarySynStarted.IsNull()) { + return 0; + } + + return (epoch - mPrimarySynStarted).ToMilliseconds(); +} + +NS_IMETHODIMP // method for nsITimerCallback +HalfOpenSocket::Notify(nsITimer* timer) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + MOZ_ASSERT(timer == mSynTimer, "wrong timer"); + + MOZ_ASSERT(!mBackupTransport); + MOZ_ASSERT(mSynTimer); + MOZ_ASSERT(mEnt); + + DebugOnly rv = SetupBackupStreams(); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + // Keeping the reference to the timer to remember we have already + // performed the backup connection. + + return NS_OK; +} + +NS_IMETHODIMP // method for nsINamed +HalfOpenSocket::GetName(nsACString& aName) { + aName.AssignLiteral("HalfOpenSocket"); + return NS_OK; +} + +// method for nsIAsyncOutputStreamCallback +NS_IMETHODIMP +HalfOpenSocket::OnOutputStreamReady(nsIAsyncOutputStream* out) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + MOZ_ASSERT(mStreamOut || mBackupStreamOut); + MOZ_ASSERT(out == mStreamOut || out == mBackupStreamOut, "stream mismatch"); + MOZ_ASSERT(mEnt); + + LOG(("HalfOpenSocket::OnOutputStreamReady [this=%p ent=%s %s]\n", this, + mEnt->mConnInfo->Origin(), out == mStreamOut ? "primary" : "backup")); + + mEnt->mDoNotDestroy = true; + gHttpHandler->ConnMgr()->RecvdConnect(); + + CancelBackupTimer(); + + if (mFastOpenInProgress) { + LOG( + ("HalfOpenSocket::OnOutputStreamReady backup stream is ready, " + "close the fast open socket %p [this=%p ent=%s]\n", + mSocketTransport.get(), this, mEnt->mConnInfo->Origin())); + // If fast open is used, right after a socket for the primary stream is + // created a HttpConnectionBase is created for that socket. The connection + // listens for OnOutputStreamReady not HalfOpenSocket. So this stream + // cannot be mStreamOut. + MOZ_ASSERT((out == mBackupStreamOut) && mConnectionNegotiatingFastOpen); + // Here the backup, non-TFO connection has connected successfully, + // before the TFO connection. + // + // The primary, TFO connection will be cancelled and the transaction + // will be rewind. CloseConnectionFastOpenTakesTooLongOrError will + // return the rewind transaction. The transaction will be put back to + // the pending queue and as well connected to this halfOpenSocket. + // SetupConn should set up a new HttpConnectionBase with the backup + // socketTransport and the rewind transaction. + mSocketTransport->SetFastOpenCallback(nullptr); + mConnectionNegotiatingFastOpen->SetFastOpen(false); + mEnt->RemoveHalfOpenFastOpenBackups(this); + RefPtr trans = + mConnectionNegotiatingFastOpen + ->CloseConnectionFastOpenTakesTooLongOrError(true); + mSocketTransport = nullptr; + mStreamOut = nullptr; + mStreamIn = nullptr; + + if (trans && trans->QueryHttpTransaction()) { + RefPtr pendingTransInfo = + new PendingTransactionInfo(trans->QueryHttpTransaction()); + pendingTransInfo->AddHalfOpen(this); + mEnt->InsertTransaction(pendingTransInfo, true); + } + if (mEnt->mUseFastOpen) { + gHttpHandler->IncrementFastOpenConsecutiveFailureCounter(); + mEnt->mUseFastOpen = false; + } + + mFastOpenInProgress = false; + mConnectionNegotiatingFastOpen = nullptr; + if (mFastOpenStatus == TFO_NOT_TRIED) { + mFastOpenStatus = TFO_FAILED_BACKUP_CONNECTION_TFO_NOT_TRIED; + } else if (mFastOpenStatus == TFO_TRIED) { + mFastOpenStatus = TFO_FAILED_BACKUP_CONNECTION_TFO_TRIED; + } else if (mFastOpenStatus == TFO_DATA_SENT) { + mFastOpenStatus = TFO_FAILED_BACKUP_CONNECTION_TFO_DATA_SENT; + } else { + // This is TFO_DATA_COOKIE_NOT_ACCEPTED (I think this cannot + // happened, because the primary connection will be already + // connected or in recovery and mFastOpenInProgress==false). + mFastOpenStatus = + TFO_FAILED_BACKUP_CONNECTION_TFO_DATA_COOKIE_NOT_ACCEPTED; + } + } + + if (((mFastOpenStatus == TFO_DISABLED) || (mFastOpenStatus == TFO_HTTP)) && + !mBackupConnStatsSet) { + // Collect telemetry for backup connection being faster than primary + // connection. We want to collect this telemetry only for cases where + // TFO is not used. + mBackupConnStatsSet = true; + Telemetry::Accumulate(Telemetry::NETWORK_HTTP_BACKUP_CONN_WON_1, + (out == mBackupStreamOut)); + } + + if (mFastOpenStatus == TFO_UNKNOWN) { + MOZ_ASSERT(out == mStreamOut); + if (mPrimaryStreamStatus == NS_NET_STATUS_RESOLVING_HOST) { + mFastOpenStatus = TFO_UNKNOWN_RESOLVING; + } else if (mPrimaryStreamStatus == NS_NET_STATUS_RESOLVED_HOST) { + mFastOpenStatus = TFO_UNKNOWN_RESOLVED; + } else if (mPrimaryStreamStatus == NS_NET_STATUS_CONNECTING_TO) { + mFastOpenStatus = TFO_UNKNOWN_CONNECTING; + } else if (mPrimaryStreamStatus == NS_NET_STATUS_CONNECTED_TO) { + mFastOpenStatus = TFO_UNKNOWN_CONNECTED; + } + } + nsresult rv = SetupConn(out, false); + if (mEnt) { + mEnt->mDoNotDestroy = false; + } + return rv; +} + +bool HalfOpenSocket::FastOpenEnabled() { + LOG(("HalfOpenSocket::FastOpenEnabled [this=%p]\n", this)); + + MOZ_ASSERT(mEnt); + + if (!mEnt) { + return false; + } + + MOZ_ASSERT(mEnt->mConnInfo->FirstHopSSL()); + + // If mEnt is present this HalfOpen must be in the mHalfOpens, + // but we want to be sure!!! + if (!mEnt->IsInHalfOpens(this)) { + return false; + } + + if (!gHttpHandler->UseFastOpen()) { + // fast open was turned off. + LOG(("HalfOpenSocket::FastEnabled - fast open was turned off.\n")); + mEnt->mUseFastOpen = false; + mFastOpenStatus = TFO_DISABLED; + return false; + } + // We can use FastOpen if we have a transaction or if it is ssl + // connection. For ssl we will use a null transaction to drive the SSL + // handshake to completion if there is not a pending transaction. Afterwards + // the connection will be 100% ready for the next transaction to use it. + // Make an exception for SSL tunneled HTTP proxy as the NullHttpTransaction + // does not know how to drive Connect. + if (mEnt->mConnInfo->UsingConnect()) { + LOG(("HalfOpenSocket::FastOpenEnabled - It is using Connect.")); + mFastOpenStatus = TFO_DISABLED_CONNECT; + return false; + } + return true; +} + +nsresult HalfOpenSocket::StartFastOpen() { + MOZ_ASSERT(mStreamOut); + MOZ_ASSERT(!mBackupTransport); + MOZ_ASSERT(mEnt); + MOZ_ASSERT(mFastOpenStatus == TFO_UNKNOWN); + + LOG(("HalfOpenSocket::StartFastOpen [this=%p]\n", this)); + + RefPtr deleteProtector(this); + + mFastOpenInProgress = true; + mEnt->mDoNotDestroy = true; + // Remove this HalfOpen from mEnt->mHalfOpens. + // The new connection will take care of closing this HalfOpen from now on! + if (!mEnt->RemoveHalfOpen(this)) { + MOZ_ASSERT(false, "HalfOpen is not in mHalfOpens!"); + mSocketTransport->SetFastOpenCallback(nullptr); + CancelBackupTimer(); + mFastOpenInProgress = false; + Abandon(); + mFastOpenStatus = TFO_INIT_FAILED; + return NS_ERROR_ABORT; + } + + gHttpHandler->ConnMgr()->DecreaseNumHalfOpenConns(); + + // Count this socketTransport as connected. + gHttpHandler->ConnMgr()->RecvdConnect(); + + // Remove HalfOpen from callbacks, the new connection will take them. + mSocketTransport->SetEventSink(nullptr, nullptr); + mSocketTransport->SetSecurityCallbacks(nullptr); + mStreamOut->AsyncWait(nullptr, 0, 0, nullptr); + + nsresult rv = SetupConn(mStreamOut, true); + if (!mConnectionNegotiatingFastOpen) { + LOG( + ("HalfOpenSocket::StartFastOpen SetupConn failed " + "[this=%p rv=%x]\n", + this, static_cast(rv))); + if (NS_SUCCEEDED(rv)) { + rv = NS_ERROR_ABORT; + } + // If SetupConn failed this will CloseTransaction and socketTransport + // with an error, therefore we can close this HalfOpen. socketTransport + // will remove reference to this HalfOpen as well. + mSocketTransport->SetFastOpenCallback(nullptr); + CancelBackupTimer(); + mFastOpenInProgress = false; + + // The connection is responsible to take care of the halfOpen so we + // need to clean it up. + Abandon(); + mFastOpenStatus = TFO_INIT_FAILED; + } else { + LOG(("HalfOpenSocket::StartFastOpen [this=%p conn=%p]\n", this, + mConnectionNegotiatingFastOpen.get())); + + mEnt->InsertIntoHalfOpenFastOpenBackups(this); + // SetupBackupTimer should setup timer which will hold a ref to this + // halfOpen. It will failed only if it cannot create timer. Anyway just + // to be sure I will add this deleteProtector!!! + if (!mSynTimer) { + // For Fast Open we will setup backup timer also for + // NullTransaction. + // So maybe it is not set and we need to set it here. + SetupBackupTimer(); + } + } + if (mEnt) { + mEnt->mDoNotDestroy = false; + } + return rv; +} + +void HalfOpenSocket::SetFastOpenConnected(nsresult aError, bool aWillRetry) { + MOZ_ASSERT(mFastOpenInProgress); + MOZ_ASSERT(mEnt); + + LOG(("HalfOpenSocket::SetFastOpenConnected [this=%p conn=%p error=%x]\n", + this, mConnectionNegotiatingFastOpen.get(), + static_cast(aError))); + + // mConnectionNegotiatingFastOpen is set after a StartFastOpen creates + // and activates a HttpConnectionBase successfully (SetupConn calls + // DispatchTransaction and DispatchAbstractTransaction which calls + // conn->Activate). + // HttpConnectionBase::Activate can fail which will close socketTransport + // and socketTransport will call this function. The FastOpen clean up + // in case HttpConnectionBase::Activate fails will be done in StartFastOpen. + // Also OnMsgReclaimConnection can decided that we do not need this + // transaction and cancel it as well. + // In all other cases mConnectionNegotiatingFastOpen must not be nullptr. + if (!mConnectionNegotiatingFastOpen) { + return; + } + + MOZ_ASSERT((mFastOpenStatus == TFO_NOT_TRIED) || + (mFastOpenStatus == TFO_DATA_SENT) || + (mFastOpenStatus == TFO_TRIED) || + (mFastOpenStatus == TFO_DATA_COOKIE_NOT_ACCEPTED) || + (mFastOpenStatus == TFO_DISABLED)); + + RefPtr deleteProtector(this); + + mEnt->mDoNotDestroy = true; + + // Delete 2 points of entry to FastOpen function so that we do not reenter. + mEnt->RemoveHalfOpenFastOpenBackups(this); + mSocketTransport->SetFastOpenCallback(nullptr); + + mConnectionNegotiatingFastOpen->SetFastOpen(false); + + // Check if we want to restart connection! + if (aWillRetry && ((aError == NS_ERROR_CONNECTION_REFUSED) || +#if defined(_WIN64) && defined(WIN95) + // On Windows PR_ContinueConnect can return + // NS_ERROR_FAILURE. This will be fixed in bug 1386719 and + // this is just a temporary work around. + (aError == NS_ERROR_FAILURE) || +#endif + (aError == NS_ERROR_PROXY_CONNECTION_REFUSED) || + (aError == NS_ERROR_NET_TIMEOUT))) { + if (mEnt->mUseFastOpen) { + gHttpHandler->IncrementFastOpenConsecutiveFailureCounter(); + mEnt->mUseFastOpen = false; + } + // This is called from nsSocketTransport::RecoverFromError. The + // socket will try connect and we need to rewind nsHttpTransaction. + + RefPtr trans = + mConnectionNegotiatingFastOpen + ->CloseConnectionFastOpenTakesTooLongOrError(false); + if (trans && trans->QueryHttpTransaction()) { + RefPtr pendingTransInfo = + new PendingTransactionInfo(trans->QueryHttpTransaction()); + pendingTransInfo->AddHalfOpen(this); + mEnt->InsertTransaction(pendingTransInfo, true); + } + // We are doing a restart without fast open, so the easiest way is to + // return mSocketTransport to the halfOpenSock and destroy connection. + // This makes http2 implemenntation easier. + // mConnectionNegotiatingFastOpen is going away and halfOpen is taking + // this mSocketTransport so add halfOpen to mEnt and update + // mNumActiveConns. + mEnt->InsertIntoHalfOpens(this); + gHttpHandler->ConnMgr()->StartedConnect(); + + // Restore callbacks. + mStreamOut->AsyncWait(this, 0, 0, nullptr); + mSocketTransport->SetEventSink(this, nullptr); + mSocketTransport->SetSecurityCallbacks(this); + mStreamIn->AsyncWait(nullptr, 0, 0, nullptr); + + if ((aError == NS_ERROR_CONNECTION_REFUSED) || + (aError == NS_ERROR_PROXY_CONNECTION_REFUSED)) { + mFastOpenStatus = TFO_FAILED_CONNECTION_REFUSED; + } else { + mFastOpenStatus = TFO_FAILED_NET_TIMEOUT; + } + + } else { + // On success or other error we proceed with connection, we just need + // to close backup timer and halfOpenSock. + CancelBackupTimer(); + if (NS_SUCCEEDED(aError)) { + NetAddr peeraddr; + if (NS_SUCCEEDED(mSocketTransport->GetPeerAddr(&peeraddr))) { + mEnt->RecordIPFamilyPreference(peeraddr.raw.family); + } + gHttpHandler->ResetFastOpenConsecutiveFailureCounter(); + } + mSocketTransport = nullptr; + mStreamOut = nullptr; + mStreamIn = nullptr; + + // If backup transport has already started put this HalfOpen back to + // mEnt list. + if (mBackupTransport) { + mFastOpenStatus = TFO_BACKUP_CONN; + mEnt->InsertIntoHalfOpens(this); + } + } + + mFastOpenInProgress = false; + mConnectionNegotiatingFastOpen = nullptr; + if (mEnt) { + mEnt->mDoNotDestroy = false; + } else { + MOZ_ASSERT(!mBackupTransport); + MOZ_ASSERT(!mBackupStreamOut); + } +} + +void HalfOpenSocket::SetFastOpenStatus(uint8_t tfoStatus) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + MOZ_ASSERT(mFastOpenInProgress); + + mFastOpenStatus = tfoStatus; + mConnectionNegotiatingFastOpen->SetFastOpenStatus(tfoStatus); + if (mConnectionNegotiatingFastOpen->Transaction()) { + // The transaction could already be canceled in the meantime, hence + // nullified. + mConnectionNegotiatingFastOpen->Transaction()->SetFastOpenStatus(tfoStatus); + } +} + +void HalfOpenSocket::CancelFastOpenConnection() { + MOZ_ASSERT(mFastOpenInProgress); + + LOG(("HalfOpenSocket::CancelFastOpenConnection [this=%p conn=%p]\n", this, + mConnectionNegotiatingFastOpen.get())); + + RefPtr deleteProtector(this); + mEnt->RemoveHalfOpenFastOpenBackups(this); + mSocketTransport->SetFastOpenCallback(nullptr); + mConnectionNegotiatingFastOpen->SetFastOpen(false); + RefPtr trans = + mConnectionNegotiatingFastOpen + ->CloseConnectionFastOpenTakesTooLongOrError(true); + mSocketTransport = nullptr; + mStreamOut = nullptr; + mStreamIn = nullptr; + + if (trans && trans->QueryHttpTransaction()) { + RefPtr pendingTransInfo = + new PendingTransactionInfo(trans->QueryHttpTransaction()); + + mEnt->InsertTransaction(pendingTransInfo, true); + } + + mFastOpenInProgress = false; + mConnectionNegotiatingFastOpen = nullptr; + Abandon(); + + MOZ_ASSERT(!mBackupTransport); + MOZ_ASSERT(!mBackupStreamOut); +} + +void HalfOpenSocket::FastOpenNotSupported() { + MOZ_ASSERT(mFastOpenInProgress); + gHttpHandler->SetFastOpenNotSupported(); +} + +nsresult HalfOpenSocket::SetupConn(nsIAsyncOutputStream* out, bool aFastOpen) { + MOZ_ASSERT(!aFastOpen || (out == mStreamOut)); + // We cannot ask for a connection for TFO and Http3 ata the same time. + MOZ_ASSERT(!(mIsHttp3 && aFastOpen)); + // assign the new socket to the http connection + RefPtr conn; + if (!mIsHttp3) { + conn = new nsHttpConnection(); + } else { + conn = new HttpConnectionUDP(); + } + + LOG( + ("HalfOpenSocket::SetupConn " + "Created new nshttpconnection %p %s\n", + conn.get(), mIsHttp3 ? "using http3" : "")); + + NullHttpTransaction* nullTrans = mTransaction->QueryNullTransaction(); + if (nullTrans) { + conn->BootstrapTimings(nullTrans->Timings()); + } + + // Some capabilities are needed before a transaciton actually gets + // scheduled (e.g. how to negotiate false start) + conn->SetTransactionCaps(mTransaction->Caps()); + + NetAddr peeraddr; + nsCOMPtr callbacks; + mTransaction->GetSecurityCallbacks(getter_AddRefs(callbacks)); + nsresult rv; + if (out == mStreamOut) { + TimeDuration rtt = TimeStamp::Now() - mPrimarySynStarted; + rv = conn->Init( + mEnt->mConnInfo, gHttpHandler->ConnMgr()->mMaxRequestDelay, + mSocketTransport, mStreamIn, mStreamOut, + mPrimaryConnectedOK || aFastOpen, callbacks, + PR_MillisecondsToInterval(static_cast(rtt.ToMilliseconds()))); + + bool resetPreference = false; + mSocketTransport->GetResetIPFamilyPreference(&resetPreference); + if (resetPreference) { + mEnt->ResetIPFamilyPreference(); + } + + if (!aFastOpen && NS_SUCCEEDED(mSocketTransport->GetPeerAddr(&peeraddr))) { + mEnt->RecordIPFamilyPreference(peeraddr.raw.family); + } + + // The nsHttpConnection object now owns these streams and sockets + if (!aFastOpen) { + mStreamOut = nullptr; + mStreamIn = nullptr; + mSocketTransport = nullptr; + } else { + RefPtr connTCP = do_QueryObject(conn); + MOZ_ASSERT(connTCP); + if (connTCP) { + connTCP->SetFastOpen(true); + } + } + } else if (out == mBackupStreamOut) { + TimeDuration rtt = TimeStamp::Now() - mBackupSynStarted; + rv = conn->Init( + mEnt->mConnInfo, gHttpHandler->ConnMgr()->mMaxRequestDelay, + mBackupTransport, mBackupStreamIn, mBackupStreamOut, mBackupConnectedOK, + callbacks, + PR_MillisecondsToInterval(static_cast(rtt.ToMilliseconds()))); + + bool resetPreference = false; + mBackupTransport->GetResetIPFamilyPreference(&resetPreference); + if (resetPreference) { + mEnt->ResetIPFamilyPreference(); + } + + if (NS_SUCCEEDED(mBackupTransport->GetPeerAddr(&peeraddr))) { + mEnt->RecordIPFamilyPreference(peeraddr.raw.family); + } + + // The nsHttpConnection object now owns these streams and sockets + mBackupStreamOut = nullptr; + mBackupStreamIn = nullptr; + mBackupTransport = nullptr; + } else { + MOZ_ASSERT(false, "unexpected stream"); + rv = NS_ERROR_UNEXPECTED; + } + + if (NS_FAILED(rv)) { + LOG( + ("HalfOpenSocket::SetupConn " + "conn->init (%p) failed %" PRIx32 "\n", + conn.get(), static_cast(rv))); + + RefPtr connTCP = do_QueryObject(conn); + if (connTCP) { + // Set TFO status. + if ((mFastOpenStatus == TFO_HTTP) || (mFastOpenStatus == TFO_DISABLED) || + (mFastOpenStatus == TFO_DISABLED_CONNECT)) { + connTCP->SetFastOpenStatus(mFastOpenStatus); + } else { + connTCP->SetFastOpenStatus(TFO_INIT_FAILED); + } + } + + if (nsHttpTransaction* trans = mTransaction->QueryHttpTransaction()) { + if (mIsHttp3) { + trans->DisableHttp3(); + gHttpHandler->ExcludeHttp3(mEnt->mConnInfo); + } + // The transaction's connection info is changed after DisableHttp3(), so + // this is the only point we can remove this transaction from its conn + // entry. + mEnt->RemoveTransFromPendingQ(trans); + } + mTransaction->Close(rv); + + return rv; + } + + // This half-open socket has created a connection. This flag excludes it + // from counter of actual connections used for checking limits. + if (!aFastOpen) { + mHasConnected = true; + } + + // if this is still in the pending list, remove it and dispatch it + RefPtr pendingTransInfo = + gHttpHandler->ConnMgr()->FindTransactionHelper(true, mEnt, mTransaction); + if (pendingTransInfo) { + MOZ_ASSERT(!mSpeculative, "Speculative Half Open found mTransaction"); + + mEnt->InsertIntoActiveConns(conn); + if (mIsHttp3) { + // Each connection must have a ConnectionHandle wrapper. + // In case of Http < 2 the a ConnectionHandle is created for each + // transaction in DispatchAbstractTransaction. + // In case of Http2/ and Http3, ConnectionHandle is created only once. + // Http2 connection always starts as http1 connection and the first + // transaction use DispatchAbstractTransaction to be dispatched and + // a ConnectionHandle is created. All consecutive transactions for + // Http2 use a short-cut in DispatchTransaction and call + // HttpConnectionBase::Activate (DispatchAbstractTransaction is never + // called). + // In case of Http3 the short-cut HttpConnectionBase::Activate is always + // used also for the first transaction, therefore we need to create + // ConnectionHandle here. + RefPtr handle = new ConnectionHandle(conn); + pendingTransInfo->Transaction()->SetConnection(handle); + } + rv = gHttpHandler->ConnMgr()->DispatchTransaction( + mEnt, pendingTransInfo->Transaction(), conn); + } else { + // this transaction was dispatched off the pending q before all the + // sockets established themselves. + + // After about 1 second allow for the possibility of restarting a + // transaction due to server close. Keep at sub 1 second as that is the + // minimum granularity we can expect a server to be timing out with. + RefPtr connTCP = do_QueryObject(conn); + if (connTCP) { + connTCP->SetIsReusedAfter(950); + } + + // if we are using ssl and no other transactions are waiting right now, + // then form a null transaction to drive the SSL handshake to + // completion. Afterwards the connection will be 100% ready for the next + // transaction to use it. Make an exception for SSL tunneled HTTP proxy as + // the NullHttpTransaction does not know how to drive Connect + // Http3 cannot be dispatched using OnMsgReclaimConnection (see below), + // therefore we need to use a Nulltransaction. + if (!connTCP || + (mEnt->mConnInfo->FirstHopSSL() && !mEnt->UrgentStartQueueLength() && + !mEnt->PendingQueueLength() && !mEnt->mConnInfo->UsingConnect())) { + LOG( + ("HalfOpenSocket::SetupConn null transaction will " + "be used to finish SSL handshake on conn %p\n", + conn.get())); + RefPtr trans; + if (mTransaction->IsNullTransaction() && !mDispatchedMTransaction) { + // null transactions cannot be put in the entry queue, so that + // explains why it is not present. + mDispatchedMTransaction = true; + trans = mTransaction; + } else { + trans = new NullHttpTransaction(mEnt->mConnInfo, callbacks, mCaps); + } + + mEnt->InsertIntoActiveConns(conn); + rv = gHttpHandler->ConnMgr()->DispatchAbstractTransaction(mEnt, trans, + mCaps, conn, 0); + } else { + // otherwise just put this in the persistent connection pool + LOG( + ("HalfOpenSocket::SetupConn no transaction match " + "returning conn %p to pool\n", + conn.get())); + gHttpHandler->ConnMgr()->OnMsgReclaimConnection(conn); + + // We expect that there is at least one tranasction in the pending + // queue that can take this connection, but it can happened that + // all transactions are blocked or they have took other idle + // connections. In that case the connection has been added to the + // idle queue. + // If the connection is in the idle queue but it is using ssl, make + // a nulltransaction for it to finish ssl handshake! + + // !!! It can be that mEnt is null after OnMsgReclaimConnection.!!! + if (mEnt && mEnt->mConnInfo->FirstHopSSL() && + !mEnt->mConnInfo->UsingConnect()) { + RefPtr connTCP = do_QueryObject(conn); + // If RemoveIdleConnection succeeds that means that conn is in the + // idle queue. + if (connTCP && NS_SUCCEEDED(mEnt->RemoveIdleConnection(connTCP))) { + RefPtr trans; + if (mTransaction->IsNullTransaction() && !mDispatchedMTransaction) { + mDispatchedMTransaction = true; + trans = mTransaction; + } else { + trans = new NullHttpTransaction(mEnt->mConnInfo, callbacks, mCaps); + } + mEnt->InsertIntoActiveConns(conn); + rv = gHttpHandler->ConnMgr()->DispatchAbstractTransaction( + mEnt, trans, mCaps, conn, 0); + } + } + } + } + + // If this connection has a transaction get reference to its + // ConnectionHandler. + RefPtr connTCP = do_QueryObject(conn); + if (connTCP) { + if (aFastOpen) { + MOZ_ASSERT(mEnt); + MOZ_ASSERT(!mEnt->IsInIdleConnections(connTCP)); + if (NS_SUCCEEDED(rv) && mEnt->IsInActiveConns(conn)) { + mConnectionNegotiatingFastOpen = connTCP; + } else { + connTCP->SetFastOpen(false); + connTCP->SetFastOpenStatus(TFO_INIT_FAILED); + } + } else { + connTCP->SetFastOpenStatus(mFastOpenStatus); + if ((mFastOpenStatus != TFO_HTTP) && (mFastOpenStatus != TFO_DISABLED) && + (mFastOpenStatus != TFO_DISABLED_CONNECT)) { + mFastOpenStatus = TFO_BACKUP_CONN; // Set this to TFO_BACKUP_CONN + // so that if a backup + // connection is established we + // do not report values twice. + } + } + } + + // If this halfOpenConn was speculative, but at the end the conn got a + // non-null transaction than this halfOpen is not speculative anymore! + if (conn->Transaction() && !conn->Transaction()->IsNullTransaction()) { + Claim(); + } + + return rv; +} + +// method for nsITransportEventSink +NS_IMETHODIMP +HalfOpenSocket::OnTransportStatus(nsITransport* trans, nsresult status, + int64_t progress, int64_t progressMax) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + MOZ_ASSERT((trans == mSocketTransport) || (trans == mBackupTransport)); + MOZ_ASSERT(mEnt); + if (mTransaction) { + if ((trans == mSocketTransport) || + ((trans == mBackupTransport) && + (status == NS_NET_STATUS_CONNECTED_TO) && mSocketTransport)) { + // Send this status event only if the transaction is still pending, + // i.e. it has not found a free already connected socket. + // Sockets in halfOpen state can only get following events: + // NS_NET_STATUS_RESOLVING_HOST, NS_NET_STATUS_RESOLVED_HOST, + // NS_NET_STATUS_CONNECTING_TO and NS_NET_STATUS_CONNECTED_TO. + // mBackupTransport is only started after + // NS_NET_STATUS_CONNECTING_TO of mSocketTransport, so ignore all + // mBackupTransport events until NS_NET_STATUS_CONNECTED_TO. + // mBackupTransport must be connected before mSocketTransport. + mTransaction->OnTransportStatus(trans, status, progress); + } + } + + MOZ_ASSERT(trans == mSocketTransport || trans == mBackupTransport); + if (status == NS_NET_STATUS_CONNECTED_TO) { + if (trans == mSocketTransport) { + mPrimaryConnectedOK = true; + } else { + mBackupConnectedOK = true; + } + } + + // The rest of this method only applies to the primary transport + if (trans != mSocketTransport) { + return NS_OK; + } + + mPrimaryStreamStatus = status; + + // if we are doing spdy coalescing and haven't recorded the ip address + // for this entry before then make the hash key if our dns lookup + // just completed. We can't do coalescing if using a proxy because the + // ip addresses are not available to the client. + + if (status == NS_NET_STATUS_CONNECTING_TO && gHttpHandler->IsSpdyEnabled() && + gHttpHandler->CoalesceSpdy() && mEnt && mEnt->mConnInfo && + mEnt->mConnInfo->EndToEndSSL() && mEnt->AllowHttp2() && + !mEnt->mConnInfo->UsingProxy() && mEnt->mCoalescingKeys.IsEmpty()) { + nsCOMPtr dnsRecord(do_GetInterface(mSocketTransport)); + nsTArray addressSet; + nsresult rv = NS_ERROR_NOT_AVAILABLE; + if (dnsRecord) { + rv = dnsRecord->GetAddresses(addressSet); + } + + if (NS_SUCCEEDED(rv) && !addressSet.IsEmpty()) { + for (uint32_t i = 0; i < addressSet.Length(); ++i) { + if ((addressSet[i].raw.family == AF_INET && + addressSet[i].inet.ip == 0) || + (addressSet[i].raw.family == AF_INET6 && + addressSet[i].inet6.ip.u64[0] == 0 && + addressSet[i].inet6.ip.u64[1] == 0)) { + // Bug 1680249 - Don't create the coalescing key if the ip address is + // `0.0.0.0` or `::`. + LOG(("HalfOpenSocket: skip creating Coalescing Key for host [%s]", + mEnt->mConnInfo->Origin())); + continue; + } + nsCString* newKey = mEnt->mCoalescingKeys.AppendElement(nsCString()); + newKey->SetLength(kIPv6CStrBufSize + 26); + addressSet[i].ToStringBuffer(newKey->BeginWriting(), kIPv6CStrBufSize); + newKey->SetLength(strlen(newKey->BeginReading())); + if (mEnt->mConnInfo->GetAnonymous()) { + newKey->AppendLiteral("~A:"); + } else { + newKey->AppendLiteral("~.:"); + } + newKey->AppendInt(mEnt->mConnInfo->OriginPort()); + newKey->AppendLiteral("/["); + nsAutoCString suffix; + mEnt->mConnInfo->GetOriginAttributes().CreateSuffix(suffix); + newKey->Append(suffix); + newKey->AppendLiteral("]viaDNS"); + LOG(( + "HalfOpenSocket::OnTransportStatus " + "STATUS_CONNECTING_TO Established New Coalescing Key # %d for host " + "%s [%s]", + i, mEnt->mConnInfo->Origin(), newKey->get())); + } + gHttpHandler->ConnMgr()->ProcessSpdyPendingQ(mEnt); + } + } + + switch (status) { + case NS_NET_STATUS_CONNECTING_TO: + // Passed DNS resolution, now trying to connect, start the backup timer + // only prevent creating another backup transport. + // We also check for mEnt presence to not instantiate the timer after + // this half open socket has already been abandoned. It may happen + // when we get this notification right between main-thread calls to + // nsHttpConnectionMgr::Shutdown and nsSocketTransportService::Shutdown + // where the first abandons all half open socket instances and only + // after that the second stops the socket thread. + // Http3 has its own syn-retransmission, therefore it does not need a + // backup connection. + if (mEnt && !mBackupTransport && !mSynTimer && !mIsHttp3) { + SetupBackupTimer(); + } + break; + + case NS_NET_STATUS_CONNECTED_TO: + // TCP connection's up, now transfer or SSL negotiantion starts, + // no need for backup socket + CancelBackupTimer(); + break; + + default: + break; + } + + return NS_OK; +} + +// method for nsIInterfaceRequestor +NS_IMETHODIMP +HalfOpenSocket::GetInterface(const nsIID& iid, void** result) { + if (mTransaction) { + nsCOMPtr callbacks; + mTransaction->GetSecurityCallbacks(getter_AddRefs(callbacks)); + if (callbacks) { + return callbacks->GetInterface(iid, result); + } + } + return NS_ERROR_NO_INTERFACE; +} + +bool HalfOpenSocket::AcceptsTransaction(nsHttpTransaction* trans) { + // When marked as urgent start, only accept urgent start marked transactions. + // Otherwise, accept any kind of transaction. + return !mUrgentStart || (trans->Caps() & nsIClassOfService::UrgentStart); +} + +bool HalfOpenSocket::Claim() { + if (mSpeculative) { + mSpeculative = false; + uint32_t flags; + if (mSocketTransport && + NS_SUCCEEDED(mSocketTransport->GetConnectionFlags(&flags))) { + flags &= ~nsISocketTransport::DISABLE_RFC1918; + mSocketTransport->SetConnectionFlags(flags); + } + + Telemetry::AutoCounter + usedSpeculativeConn; + ++usedSpeculativeConn; + + if (mIsFromPredictor) { + Telemetry::AutoCounter + totalPreconnectsUsed; + ++totalPreconnectsUsed; + } + + // Http3 has its own syn-retransmission, therefore it does not need a + // backup connection. + if ((mPrimaryStreamStatus == NS_NET_STATUS_CONNECTING_TO) && mEnt && + !mBackupTransport && !mSynTimer && !mIsHttp3) { + SetupBackupTimer(); + } + } + + if (mFreeToUse) { + mFreeToUse = false; + return true; + } + return false; +} + +void HalfOpenSocket::Unclaim() { + MOZ_ASSERT(!mSpeculative && !mFreeToUse); + // We will keep the backup-timer running. Most probably this halfOpen will + // be used by a transaction from which this transaction took the halfOpen. + // (this is happening because of the transaction priority.) + mFreeToUse = true; +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/HalfOpenSocket.h b/netwerk/protocol/http/HalfOpenSocket.h new file mode 100644 index 0000000000..d1a753da12 --- /dev/null +++ b/netwerk/protocol/http/HalfOpenSocket.h @@ -0,0 +1,165 @@ +/* vim:set ts=4 sw=2 sts=2 et cin: */ +/* 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/. */ + +#ifndef HalfOpenSocket_h__ +#define HalfOpenSocket_h__ + +#include "mozilla/TimeStamp.h" +#include "nsAHttpConnection.h" +#include "nsHttpConnection.h" +#include "nsHttpTransaction.h" +#include "nsIAsyncOutputStream.h" +#include "nsINamed.h" +#include "nsITransport.h" +#include "nsWeakReference.h" +#include "TCPFastOpen.h" + +namespace mozilla { +namespace net { + +// 8d411b53-54bc-4a99-8b78-ff125eab1564 +#define NS_HALFOPENSOCKET_IID \ + { \ + 0x8d411b53, 0x54bc, 0x4a99, { \ + 0x8b, 0x78, 0xff, 0x12, 0x5e, 0xab, 0x15, 0x64 \ + } \ + } + +class PendingTransactionInfo; +class ConnectionEntry; + +class HalfOpenSocket final : public nsIOutputStreamCallback, + public nsITransportEventSink, + public nsIInterfaceRequestor, + public nsITimerCallback, + public nsINamed, + public nsSupportsWeakReference, + public TCPFastOpen { + ~HalfOpenSocket(); + + public: + NS_DECLARE_STATIC_IID_ACCESSOR(NS_HALFOPENSOCKET_IID) + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIOUTPUTSTREAMCALLBACK + NS_DECL_NSITRANSPORTEVENTSINK + NS_DECL_NSIINTERFACEREQUESTOR + NS_DECL_NSITIMERCALLBACK + NS_DECL_NSINAMED + + HalfOpenSocket(ConnectionEntry* ent, nsAHttpTransaction* trans, uint32_t caps, + bool speculative, bool isFromPredictor, bool urgentStart); + + [[nodiscard]] nsresult SetupStreams(nsISocketTransport**, + nsIAsyncInputStream**, + nsIAsyncOutputStream**, bool isBackup); + [[nodiscard]] nsresult SetupPrimaryStreams(); + [[nodiscard]] nsresult SetupBackupStreams(); + void SetupBackupTimer(); + void CancelBackupTimer(); + void Abandon(); + double Duration(TimeStamp epoch); + nsISocketTransport* SocketTransport() { return mSocketTransport; } + nsISocketTransport* BackupTransport() { return mBackupTransport; } + + nsAHttpTransaction* Transaction() { return mTransaction; } + + bool IsSpeculative() { return mSpeculative; } + + bool IsFromPredictor() { return mIsFromPredictor; } + + bool Allow1918() { return mAllow1918; } + void SetAllow1918(bool val) { mAllow1918 = val; } + + bool HasConnected() { return mHasConnected; } + + void PrintDiagnostics(nsCString& log); + + // Checks whether the transaction can be dispatched using this + // half-open's connection. If this half-open is marked as urgent-start, + // it only accepts urgent start transactions. Call only before Claim(). + bool AcceptsTransaction(nsHttpTransaction* trans); + bool Claim(); + void Unclaim(); + + bool FastOpenEnabled() override; + nsresult StartFastOpen() override; + void SetFastOpenConnected(nsresult, bool aWillRetry) override; + void FastOpenNotSupported() override; + void SetFastOpenStatus(uint8_t tfoStatus) override; + void CancelFastOpenConnection(); + + private: + nsresult SetupConn(nsIAsyncOutputStream* out, bool aFastOpen); + + // To find out whether |mTransaction| is still in the connection entry's + // pending queue. If the transaction is found and |removeWhenFound| is + // true, the transaction will be removed from the pending queue. + already_AddRefed FindTransactionHelper( + bool removeWhenFound); + + RefPtr mTransaction; + bool mDispatchedMTransaction; + nsCOMPtr mSocketTransport; + nsCOMPtr mStreamOut; + nsCOMPtr mStreamIn; + uint32_t mCaps; + + // mSpeculative is set if the socket was created from + // SpeculativeConnect(). It is cleared when a transaction would normally + // start a new connection from scratch but instead finds this one in + // the half open list and claims it for its own use. (which due to + // the vagaries of scheduling from the pending queue might not actually + // match up - but it prevents a speculative connection from opening + // more connections that are needed.) + bool mSpeculative; + + // If created with a non-null urgent transaction, remember it, so we can + // mark the connection as urgent rightaway it's created. + bool mUrgentStart; + + // mIsFromPredictor is set if the socket originated from the network + // Predictor. It is used to gather telemetry data on used speculative + // connections from the predictor. + bool mIsFromPredictor; + + bool mAllow1918; + + TimeStamp mPrimarySynStarted; + TimeStamp mBackupSynStarted; + + // mHasConnected tracks whether one of the sockets has completed the + // connection process. It may have completed unsuccessfully. + bool mHasConnected; + + bool mPrimaryConnectedOK; + bool mBackupConnectedOK; + bool mBackupConnStatsSet; + + // A HalfOpenSocket can be made for a concrete non-null transaction, + // but the transaction can be dispatch to another connection. In that + // case we can free this transaction to be claimed by other + // transactions. + bool mFreeToUse; + nsresult mPrimaryStreamStatus; + + bool mFastOpenInProgress; + RefPtr mConnectionNegotiatingFastOpen; + uint8_t mFastOpenStatus; + + RefPtr mEnt; + nsCOMPtr mSynTimer; + nsCOMPtr mBackupTransport; + nsCOMPtr mBackupStreamOut; + nsCOMPtr mBackupStreamIn; + + bool mIsHttp3; +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(HalfOpenSocket, NS_HALFOPENSOCKET_IID) + +} // namespace net +} // namespace mozilla + +#endif // HalfOpenSocket_h__ diff --git a/netwerk/protocol/http/Http2Compression.cpp b/netwerk/protocol/http/Http2Compression.cpp new file mode 100644 index 0000000000..8dba007688 --- /dev/null +++ b/netwerk/protocol/http/Http2Compression.cpp @@ -0,0 +1,1443 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et 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/. */ + +// HttpLog.h should generally be included first +#include "HttpLog.h" + +// Log on level :5, instead of default :4. +#undef LOG +#define LOG(args) LOG5(args) +#undef LOG_ENABLED +#define LOG_ENABLED() LOG5_ENABLED() + +#include "Http2Compression.h" +#include "Http2HuffmanIncoming.h" +#include "Http2HuffmanOutgoing.h" +#include "mozilla/StaticPtr.h" +#include "nsCharSeparatedTokenizer.h" +#include "nsIMemoryReporter.h" +#include "nsHttpHandler.h" + +namespace mozilla { +namespace net { + +static nsDeque* gStaticHeaders = nullptr; + +class HpackStaticTableReporter final : public nsIMemoryReporter { + public: + NS_DECL_THREADSAFE_ISUPPORTS + + HpackStaticTableReporter() = default; + + NS_IMETHOD + CollectReports(nsIHandleReportCallback* aHandleReport, nsISupports* aData, + bool aAnonymize) override { + MOZ_COLLECT_REPORT("explicit/network/hpack/static-table", KIND_HEAP, + UNITS_BYTES, + gStaticHeaders->SizeOfIncludingThis(MallocSizeOf), + "Memory usage of HPACK static table."); + + return NS_OK; + } + + private: + MOZ_DEFINE_MALLOC_SIZE_OF(MallocSizeOf) + + ~HpackStaticTableReporter() = default; +}; + +NS_IMPL_ISUPPORTS(HpackStaticTableReporter, nsIMemoryReporter) + +class HpackDynamicTableReporter final : public nsIMemoryReporter { + public: + NS_DECL_THREADSAFE_ISUPPORTS + + explicit HpackDynamicTableReporter(Http2BaseCompressor* aCompressor) + : mCompressor(aCompressor) {} + + NS_IMETHOD + CollectReports(nsIHandleReportCallback* aHandleReport, nsISupports* aData, + bool aAnonymize) override { + if (mCompressor) { + MOZ_COLLECT_REPORT("explicit/network/hpack/dynamic-tables", KIND_HEAP, + UNITS_BYTES, + mCompressor->SizeOfExcludingThis(MallocSizeOf), + "Aggregate memory usage of HPACK dynamic tables."); + } + return NS_OK; + } + + private: + MOZ_DEFINE_MALLOC_SIZE_OF(MallocSizeOf) + + ~HpackDynamicTableReporter() = default; + + Http2BaseCompressor* mCompressor; + + friend class Http2BaseCompressor; +}; + +NS_IMPL_ISUPPORTS(HpackDynamicTableReporter, nsIMemoryReporter) + +StaticRefPtr gStaticReporter; + +void Http2CompressionCleanup() { + // this happens after the socket thread has been destroyed + delete gStaticHeaders; + gStaticHeaders = nullptr; + UnregisterStrongMemoryReporter(gStaticReporter); + gStaticReporter = nullptr; +} + +static void AddStaticElement(const nsCString& name, const nsCString& value) { + nvPair* pair = new nvPair(name, value); + gStaticHeaders->Push(pair); +} + +static void AddStaticElement(const nsCString& name) { + AddStaticElement(name, ""_ns); +} + +static void InitializeStaticHeaders() { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + if (!gStaticHeaders) { + gStaticHeaders = new nsDeque(); + gStaticReporter = new HpackStaticTableReporter(); + RegisterStrongMemoryReporter(gStaticReporter); + AddStaticElement(":authority"_ns); + AddStaticElement(":method"_ns, "GET"_ns); + AddStaticElement(":method"_ns, "POST"_ns); + AddStaticElement(":path"_ns, "/"_ns); + AddStaticElement(":path"_ns, "/index.html"_ns); + AddStaticElement(":scheme"_ns, "http"_ns); + AddStaticElement(":scheme"_ns, "https"_ns); + AddStaticElement(":status"_ns, "200"_ns); + AddStaticElement(":status"_ns, "204"_ns); + AddStaticElement(":status"_ns, "206"_ns); + AddStaticElement(":status"_ns, "304"_ns); + AddStaticElement(":status"_ns, "400"_ns); + AddStaticElement(":status"_ns, "404"_ns); + AddStaticElement(":status"_ns, "500"_ns); + AddStaticElement("accept-charset"_ns); + AddStaticElement("accept-encoding"_ns, "gzip, deflate"_ns); + AddStaticElement("accept-language"_ns); + AddStaticElement("accept-ranges"_ns); + AddStaticElement("accept"_ns); + AddStaticElement("access-control-allow-origin"_ns); + AddStaticElement("age"_ns); + AddStaticElement("allow"_ns); + AddStaticElement("authorization"_ns); + AddStaticElement("cache-control"_ns); + AddStaticElement("content-disposition"_ns); + AddStaticElement("content-encoding"_ns); + AddStaticElement("content-language"_ns); + AddStaticElement("content-length"_ns); + AddStaticElement("content-location"_ns); + AddStaticElement("content-range"_ns); + AddStaticElement("content-type"_ns); + AddStaticElement("cookie"_ns); + AddStaticElement("date"_ns); + AddStaticElement("etag"_ns); + AddStaticElement("expect"_ns); + AddStaticElement("expires"_ns); + AddStaticElement("from"_ns); + AddStaticElement("host"_ns); + AddStaticElement("if-match"_ns); + AddStaticElement("if-modified-since"_ns); + AddStaticElement("if-none-match"_ns); + AddStaticElement("if-range"_ns); + AddStaticElement("if-unmodified-since"_ns); + AddStaticElement("last-modified"_ns); + AddStaticElement("link"_ns); + AddStaticElement("location"_ns); + AddStaticElement("max-forwards"_ns); + AddStaticElement("proxy-authenticate"_ns); + AddStaticElement("proxy-authorization"_ns); + AddStaticElement("range"_ns); + AddStaticElement("referer"_ns); + AddStaticElement("refresh"_ns); + AddStaticElement("retry-after"_ns); + AddStaticElement("server"_ns); + AddStaticElement("set-cookie"_ns); + AddStaticElement("strict-transport-security"_ns); + AddStaticElement("transfer-encoding"_ns); + AddStaticElement("user-agent"_ns); + AddStaticElement("vary"_ns); + AddStaticElement("via"_ns); + AddStaticElement("www-authenticate"_ns); + } +} + +size_t nvPair::SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const { + return mName.SizeOfExcludingThisIfUnshared(aMallocSizeOf) + + mValue.SizeOfExcludingThisIfUnshared(aMallocSizeOf); +} + +size_t nvPair::SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const { + return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf); +} + +nvFIFO::nvFIFO() : mByteCount(0), mTable() { InitializeStaticHeaders(); } + +nvFIFO::~nvFIFO() { Clear(); } + +void nvFIFO::AddElement(const nsCString& name, const nsCString& value) { + nvPair* pair = new nvPair(name, value); + mByteCount += pair->Size(); + mTable.PushFront(pair); +} + +void nvFIFO::AddElement(const nsCString& name) { AddElement(name, ""_ns); } + +void nvFIFO::RemoveElement() { + nvPair* pair = mTable.Pop(); + if (pair) { + mByteCount -= pair->Size(); + delete pair; + } +} + +uint32_t nvFIFO::ByteCount() const { return mByteCount; } + +uint32_t nvFIFO::Length() const { + return mTable.GetSize() + gStaticHeaders->GetSize(); +} + +uint32_t nvFIFO::VariableLength() const { return mTable.GetSize(); } + +size_t nvFIFO::StaticLength() const { return gStaticHeaders->GetSize(); } + +void nvFIFO::Clear() { + mByteCount = 0; + while (mTable.GetSize()) { + delete mTable.Pop(); + } +} + +const nvPair* nvFIFO::operator[](size_t index) const { + // NWGH - ensure index > 0 + // NWGH - subtract 1 from index here + if (index >= (mTable.GetSize() + gStaticHeaders->GetSize())) { + MOZ_ASSERT(false); + NS_WARNING("nvFIFO Table Out of Range"); + return nullptr; + } + if (index >= gStaticHeaders->GetSize()) { + return mTable.ObjectAt(index - gStaticHeaders->GetSize()); + } + return gStaticHeaders->ObjectAt(index); +} + +Http2BaseCompressor::Http2BaseCompressor() + : mOutput(nullptr), + mMaxBuffer(kDefaultMaxBuffer), + mMaxBufferSetting(kDefaultMaxBuffer), + mSetInitialMaxBufferSizeAllowed(true), + mPeakSize(0), + mPeakCount(0), + mDumpTables(false) { + mDynamicReporter = new HpackDynamicTableReporter(this); + RegisterStrongMemoryReporter(mDynamicReporter); +} + +Http2BaseCompressor::~Http2BaseCompressor() { + if (mPeakSize) { + Telemetry::Accumulate(mPeakSizeID, mPeakSize); + } + if (mPeakCount) { + Telemetry::Accumulate(mPeakCountID, mPeakCount); + } + UnregisterStrongMemoryReporter(mDynamicReporter); + mDynamicReporter->mCompressor = nullptr; + mDynamicReporter = nullptr; +} + +void Http2BaseCompressor::ClearHeaderTable() { mHeaderTable.Clear(); } + +size_t Http2BaseCompressor::SizeOfExcludingThis( + mozilla::MallocSizeOf aMallocSizeOf) const { + size_t size = 0; + for (uint32_t i = mHeaderTable.StaticLength(); i < mHeaderTable.Length(); + ++i) { + size += mHeaderTable[i]->SizeOfIncludingThis(aMallocSizeOf); + } + return size; +} + +void Http2BaseCompressor::MakeRoom(uint32_t amount, const char* direction) { + uint32_t countEvicted = 0; + uint32_t bytesEvicted = 0; + + // make room in the header table + while (mHeaderTable.VariableLength() && + ((mHeaderTable.ByteCount() + amount) > mMaxBuffer)) { + // NWGH - remove the "- 1" here + uint32_t index = mHeaderTable.Length() - 1; + LOG(("HTTP %s header table index %u %s %s removed for size.\n", direction, + index, mHeaderTable[index]->mName.get(), + mHeaderTable[index]->mValue.get())); + ++countEvicted; + bytesEvicted += mHeaderTable[index]->Size(); + mHeaderTable.RemoveElement(); + } + + if (!strcmp(direction, "decompressor")) { + Telemetry::Accumulate(Telemetry::HPACK_ELEMENTS_EVICTED_DECOMPRESSOR, + countEvicted); + Telemetry::Accumulate(Telemetry::HPACK_BYTES_EVICTED_DECOMPRESSOR, + bytesEvicted); + Telemetry::Accumulate( + Telemetry::HPACK_BYTES_EVICTED_RATIO_DECOMPRESSOR, + (uint32_t)((100.0 * (double)bytesEvicted) / (double)amount)); + } else { + Telemetry::Accumulate(Telemetry::HPACK_ELEMENTS_EVICTED_COMPRESSOR, + countEvicted); + Telemetry::Accumulate(Telemetry::HPACK_BYTES_EVICTED_COMPRESSOR, + bytesEvicted); + Telemetry::Accumulate( + Telemetry::HPACK_BYTES_EVICTED_RATIO_COMPRESSOR, + (uint32_t)((100.0 * (double)bytesEvicted) / (double)amount)); + } +} + +void Http2BaseCompressor::DumpState(const char* preamble) { + if (!LOG_ENABLED()) { + return; + } + + if (!mDumpTables) { + return; + } + + LOG(("%s", preamble)); + + LOG(("Header Table")); + uint32_t i; + uint32_t length = mHeaderTable.Length(); + uint32_t staticLength = mHeaderTable.StaticLength(); + // NWGH - make i = 1; i <= length; ++i + for (i = 0; i < length; ++i) { + const nvPair* pair = mHeaderTable[i]; + // NWGH - make this <= staticLength + LOG(("%sindex %u: %s %s", i < staticLength ? "static " : "", i, + pair->mName.get(), pair->mValue.get())); + } +} + +void Http2BaseCompressor::SetMaxBufferSizeInternal(uint32_t maxBufferSize) { + MOZ_ASSERT(maxBufferSize <= mMaxBufferSetting); + + uint32_t removedCount = 0; + + LOG(("Http2BaseCompressor::SetMaxBufferSizeInternal %u called", + maxBufferSize)); + + while (mHeaderTable.VariableLength() && + (mHeaderTable.ByteCount() > maxBufferSize)) { + mHeaderTable.RemoveElement(); + ++removedCount; + } + + mMaxBuffer = maxBufferSize; +} + +nsresult Http2BaseCompressor::SetInitialMaxBufferSize(uint32_t maxBufferSize) { + MOZ_ASSERT(mSetInitialMaxBufferSizeAllowed); + + if (mSetInitialMaxBufferSizeAllowed) { + mMaxBufferSetting = maxBufferSize; + return NS_OK; + } + + return NS_ERROR_FAILURE; +} + +void Http2BaseCompressor::SetDumpTables(bool dumpTables) { + mDumpTables = dumpTables; +} + +nsresult Http2Decompressor::DecodeHeaderBlock(const uint8_t* data, + uint32_t datalen, + nsACString& output, bool isPush) { + mSetInitialMaxBufferSizeAllowed = false; + mOffset = 0; + mData = data; + mDataLen = datalen; + mOutput = &output; + // Add in some space to hopefully not have to reallocate while decompressing + // the headers. 512 bytes seems like a good enough number. + mOutput->Truncate(); + mOutput->SetCapacity(datalen + 512); + mHeaderStatus.Truncate(); + mHeaderHost.Truncate(); + mHeaderScheme.Truncate(); + mHeaderPath.Truncate(); + mHeaderMethod.Truncate(); + mSeenNonColonHeader = false; + mIsPush = isPush; + + nsresult rv = NS_OK; + nsresult softfail_rv = NS_OK; + while (NS_SUCCEEDED(rv) && (mOffset < mDataLen)) { + bool modifiesTable = true; + const char* preamble = "Decompressor state after ?"; + if (mData[mOffset] & 0x80) { + rv = DoIndexed(); + preamble = "Decompressor state after indexed"; + } else if (mData[mOffset] & 0x40) { + rv = DoLiteralWithIncremental(); + preamble = "Decompressor state after literal with incremental"; + } else if (mData[mOffset] & 0x20) { + rv = DoContextUpdate(); + preamble = "Decompressor state after context update"; + } else if (mData[mOffset] & 0x10) { + modifiesTable = false; + rv = DoLiteralNeverIndexed(); + preamble = "Decompressor state after literal never index"; + } else { + modifiesTable = false; + rv = DoLiteralWithoutIndex(); + preamble = "Decompressor state after literal without index"; + } + DumpState(preamble); + if (rv == NS_ERROR_ILLEGAL_VALUE) { + if (modifiesTable) { + // Unfortunately, we can't count on our peer now having the same state + // as us, so let's terminate the session and we can try again later. + return NS_ERROR_FAILURE; + } + + // This is an http-level error that we can handle by resetting the stream + // in the upper layers. Let's note that we saw this, then continue + // decompressing until we either hit the end of the header block or find a + // hard failure. That way we won't get an inconsistent compression state + // with the server. + softfail_rv = rv; + rv = NS_OK; + } else if (rv == NS_ERROR_NET_RESET) { + // This happens when we detect connection-based auth being requested in + // the response headers. We'll paper over it for now, and the session will + // handle this as if it received RST_STREAM with HTTP_1_1_REQUIRED. + softfail_rv = rv; + rv = NS_OK; + } + } + + if (NS_FAILED(rv)) { + return rv; + } + + return softfail_rv; +} + +nsresult Http2Decompressor::DecodeInteger(uint32_t prefixLen, uint32_t& accum) { + accum = 0; + + if (prefixLen) { + uint32_t mask = (1 << prefixLen) - 1; + + accum = mData[mOffset] & mask; + ++mOffset; + + if (accum != mask) { + // the simple case for small values + return NS_OK; + } + } + + uint32_t factor = 1; // 128 ^ 0 + + // we need a series of bytes. The high bit signifies if we need another one. + // The first one is a a factor of 128 ^ 0, the next 128 ^1, the next 128 ^2, + // .. + + if (mOffset >= mDataLen) { + NS_WARNING("Ran out of data to decode integer"); + // This is session-fatal. + return NS_ERROR_FAILURE; + } + bool chainBit = mData[mOffset] & 0x80; + accum += (mData[mOffset] & 0x7f) * factor; + + ++mOffset; + factor = factor * 128; + + while (chainBit) { + // really big offsets are just trawling for overflows + if (accum >= 0x800000) { + NS_WARNING("Decoding integer >= 0x800000"); + // This is not strictly fatal to the session, but given the fact that + // the value is way to large to be reasonable, let's just tell our peer + // to go away. + return NS_ERROR_FAILURE; + } + + if (mOffset >= mDataLen) { + NS_WARNING("Ran out of data to decode integer"); + // This is session-fatal. + return NS_ERROR_FAILURE; + } + chainBit = mData[mOffset] & 0x80; + accum += (mData[mOffset] & 0x7f) * factor; + ++mOffset; + factor = factor * 128; + } + return NS_OK; +} + +static bool HasConnectionBasedAuth(const nsACString& headerValue) { + for (const nsACString& authMethod : + nsCCharSeparatedTokenizer(headerValue, '\n').ToRange()) { + if (authMethod.LowerCaseEqualsLiteral("ntlm")) { + return true; + } + if (authMethod.LowerCaseEqualsLiteral("negotiate")) { + return true; + } + } + + return false; +} + +nsresult Http2Decompressor::OutputHeader(const nsACString& name, + const nsACString& value) { + // exclusions + if (!mIsPush && + (name.EqualsLiteral("connection") || name.EqualsLiteral("host") || + name.EqualsLiteral("keep-alive") || + name.EqualsLiteral("proxy-connection") || name.EqualsLiteral("te") || + name.EqualsLiteral("transfer-encoding") || + name.EqualsLiteral("upgrade") || name.Equals(("accept-encoding")))) { + nsCString toLog(name); + LOG(("HTTP Decompressor illegal response header found, not gatewaying: %s", + toLog.get())); + return NS_OK; + } + + // Look for upper case characters in the name. + for (const char* cPtr = name.BeginReading(); cPtr && cPtr < name.EndReading(); + ++cPtr) { + if (*cPtr <= 'Z' && *cPtr >= 'A') { + nsCString toLog(name); + LOG(("HTTP Decompressor upper case response header found. [%s]\n", + toLog.get())); + return NS_ERROR_ILLEGAL_VALUE; + } + } + + // Look for CR OR LF in value - could be smuggling Sec 10.3 + // can map to space safely + for (const char* cPtr = value.BeginReading(); + cPtr && cPtr < value.EndReading(); ++cPtr) { + if (*cPtr == '\r' || *cPtr == '\n') { + char* wPtr = const_cast(cPtr); + *wPtr = ' '; + } + } + + // Status comes first + if (name.EqualsLiteral(":status")) { + nsAutoCString status("HTTP/2 "_ns); + status.Append(value); + status.AppendLiteral("\r\n"); + mOutput->Insert(status, 0); + mHeaderStatus = value; + } else if (name.EqualsLiteral(":authority")) { + mHeaderHost = value; + } else if (name.EqualsLiteral(":scheme")) { + mHeaderScheme = value; + } else if (name.EqualsLiteral(":path")) { + mHeaderPath = value; + } else if (name.EqualsLiteral(":method")) { + mHeaderMethod = value; + } + + // http/2 transport level headers shouldn't be gatewayed into http/1 + bool isColonHeader = false; + for (const char* cPtr = name.BeginReading(); cPtr && cPtr < name.EndReading(); + ++cPtr) { + if (*cPtr == ':') { + isColonHeader = true; + break; + } else if (*cPtr != ' ' && *cPtr != '\t') { + isColonHeader = false; + break; + } + } + + if (isColonHeader) { + // :status is the only pseudo-header field allowed in received HEADERS + // frames, PUSH_PROMISE allows the other pseudo-header fields + if (!name.EqualsLiteral(":status") && !mIsPush) { + LOG(("HTTP Decompressor found illegal response pseudo-header %s", + name.BeginReading())); + return NS_ERROR_ILLEGAL_VALUE; + } + if (mSeenNonColonHeader) { + LOG(("HTTP Decompressor found illegal : header %s", name.BeginReading())); + return NS_ERROR_ILLEGAL_VALUE; + } + LOG(("HTTP Decompressor not gatewaying %s into http/1", + name.BeginReading())); + return NS_OK; + } + + LOG(("Http2Decompressor::OutputHeader %s %s", name.BeginReading(), + value.BeginReading())); + mSeenNonColonHeader = true; + mOutput->Append(name); + mOutput->AppendLiteral(": "); + mOutput->Append(value); + mOutput->AppendLiteral("\r\n"); + + // Need to check if the server is going to try to speak connection-based auth + // with us. If so, we need to kill this via h2, and dial back with http/1.1. + // Technically speaking, the server should've just reset or goaway'd us with + // HTTP_1_1_REQUIRED, but there are some busted servers out there, so we need + // to check on our own to work around them. + if (name.EqualsLiteral("www-authenticate") || + name.EqualsLiteral("proxy-authenticate")) { + if (HasConnectionBasedAuth(value)) { + LOG3(("Http2Decompressor %p connection-based auth found in %s", this, + name.BeginReading())); + return NS_ERROR_NET_RESET; + } + } + return NS_OK; +} + +nsresult Http2Decompressor::OutputHeader(uint32_t index) { + // NWGH - make this < index + // bounds check + if (mHeaderTable.Length() <= index) { + LOG(("Http2Decompressor::OutputHeader index too large %u", index)); + // This is session-fatal. + return NS_ERROR_FAILURE; + } + + return OutputHeader(mHeaderTable[index]->mName, mHeaderTable[index]->mValue); +} + +nsresult Http2Decompressor::CopyHeaderString(uint32_t index, nsACString& name) { + // NWGH - make this < index + // bounds check + if (mHeaderTable.Length() <= index) { + // This is session-fatal. + return NS_ERROR_FAILURE; + } + + name = mHeaderTable[index]->mName; + return NS_OK; +} + +nsresult Http2Decompressor::CopyStringFromInput(uint32_t bytes, + nsACString& val) { + if (mOffset + bytes > mDataLen) { + // This is session-fatal. + return NS_ERROR_FAILURE; + } + + val.Assign(reinterpret_cast(mData) + mOffset, bytes); + mOffset += bytes; + return NS_OK; +} + +nsresult Http2Decompressor::DecodeFinalHuffmanCharacter( + const HuffmanIncomingTable* table, uint8_t& c, uint8_t& bitsLeft) { + MOZ_ASSERT(mOffset <= mDataLen); + if (mOffset > mDataLen) { + NS_WARNING("DecodeFinalHuffmanCharacter would read beyond end of buffer"); + return NS_ERROR_FAILURE; + } + uint8_t mask = (1 << bitsLeft) - 1; + uint8_t idx = mData[mOffset - 1] & mask; + idx <<= (8 - bitsLeft); + // Don't update bitsLeft yet, because we need to check that value against the + // number of bits used by our encoding later on. We'll update when we are sure + // how many bits we've actually used. + + if (table->IndexHasANextTable(idx)) { + // Can't chain to another table when we're all out of bits in the encoding + LOG(("DecodeFinalHuffmanCharacter trying to chain when we're out of bits")); + return NS_ERROR_FAILURE; + } + + const HuffmanIncomingEntry* entry = table->Entry(idx); + + if (bitsLeft < entry->mPrefixLen) { + // We don't have enough bits to actually make a match, this is some sort of + // invalid coding + LOG(("DecodeFinalHuffmanCharacter does't have enough bits to match")); + return NS_ERROR_FAILURE; + } + + // This is a character! + if (entry->mValue == 256) { + // EOS + LOG(("DecodeFinalHuffmanCharacter actually decoded an EOS")); + return NS_ERROR_FAILURE; + } + c = static_cast(entry->mValue & 0xFF); + bitsLeft -= entry->mPrefixLen; + + return NS_OK; +} + +uint8_t Http2Decompressor::ExtractByte(uint8_t bitsLeft, + uint32_t& bytesConsumed) { + MOZ_DIAGNOSTIC_ASSERT(mOffset < mDataLen); + uint8_t rv; + + if (bitsLeft) { + // Need to extract bitsLeft bits from the previous byte, and 8 - bitsLeft + // bits from the current byte + uint8_t mask = (1 << bitsLeft) - 1; + rv = (mData[mOffset - 1] & mask) << (8 - bitsLeft); + rv |= (mData[mOffset] & ~mask) >> bitsLeft; + } else { + rv = mData[mOffset]; + } + + // We always update these here, under the assumption that all 8 bits we got + // here will be used. These may be re-adjusted later in the case that we don't + // use up all 8 bits of the byte. + ++mOffset; + ++bytesConsumed; + + return rv; +} + +nsresult Http2Decompressor::DecodeHuffmanCharacter( + const HuffmanIncomingTable* table, uint8_t& c, uint32_t& bytesConsumed, + uint8_t& bitsLeft) { + uint8_t idx = ExtractByte(bitsLeft, bytesConsumed); + + if (table->IndexHasANextTable(idx)) { + if (mOffset >= mDataLen) { + if (!bitsLeft || (mOffset > mDataLen)) { + // TODO - does this get me into trouble in the new world? + // No info left in input to try to consume, we're done + LOG(("DecodeHuffmanCharacter all out of bits to consume, can't chain")); + return NS_ERROR_FAILURE; + } + + // We might get lucky here! + return DecodeFinalHuffmanCharacter(table->NextTable(idx), c, bitsLeft); + } + + // We're sorry, Mario, but your princess is in another castle + return DecodeHuffmanCharacter(table->NextTable(idx), c, bytesConsumed, + bitsLeft); + } + + const HuffmanIncomingEntry* entry = table->Entry(idx); + if (entry->mValue == 256) { + LOG(("DecodeHuffmanCharacter found an actual EOS")); + return NS_ERROR_FAILURE; + } + c = static_cast(entry->mValue & 0xFF); + + // Need to adjust bitsLeft (and possibly other values) because we may not have + // consumed all of the bits of the byte we extracted. + if (entry->mPrefixLen <= bitsLeft) { + bitsLeft -= entry->mPrefixLen; + --mOffset; + --bytesConsumed; + } else { + bitsLeft = 8 - (entry->mPrefixLen - bitsLeft); + } + MOZ_ASSERT(bitsLeft < 8); + + return NS_OK; +} + +nsresult Http2Decompressor::CopyHuffmanStringFromInput(uint32_t bytes, + nsACString& val) { + if (mOffset + bytes > mDataLen) { + LOG(("CopyHuffmanStringFromInput not enough data")); + return NS_ERROR_FAILURE; + } + + uint32_t bytesRead = 0; + uint8_t bitsLeft = 0; + nsAutoCString buf; + nsresult rv; + uint8_t c; + + while (bytesRead < bytes) { + uint32_t bytesConsumed = 0; + rv = DecodeHuffmanCharacter(&HuffmanIncomingRoot, c, bytesConsumed, + bitsLeft); + if (NS_FAILED(rv)) { + LOG(("CopyHuffmanStringFromInput failed to decode a character")); + return rv; + } + + bytesRead += bytesConsumed; + buf.Append(c); + } + + if (bytesRead > bytes) { + LOG(("CopyHuffmanStringFromInput read more bytes than was allowed!")); + return NS_ERROR_FAILURE; + } + + if (bitsLeft) { + // The shortest valid code is 4 bits, so we know there can be at most one + // character left that our loop didn't decode. Check to see if that's the + // case, and if so, add it to our output. + rv = DecodeFinalHuffmanCharacter(&HuffmanIncomingRoot, c, bitsLeft); + if (NS_SUCCEEDED(rv)) { + buf.Append(c); + } + } + + if (bitsLeft > 7) { + LOG(("CopyHuffmanStringFromInput more than 7 bits of padding")); + return NS_ERROR_FAILURE; + } + + if (bitsLeft) { + // Any bits left at this point must belong to the EOS symbol, so make sure + // they make sense (ie, are all ones) + uint8_t mask = (1 << bitsLeft) - 1; + uint8_t bits = mData[mOffset - 1] & mask; + if (bits != mask) { + LOG( + ("CopyHuffmanStringFromInput ran out of data but found possible " + "non-EOS symbol")); + return NS_ERROR_FAILURE; + } + } + + val = buf; + LOG(("CopyHuffmanStringFromInput decoded a full string!")); + return NS_OK; +} + +nsresult Http2Decompressor::DoIndexed() { + // this starts with a 1 bit pattern + MOZ_ASSERT(mData[mOffset] & 0x80); + + // This is a 7 bit prefix + + uint32_t index; + nsresult rv = DecodeInteger(7, index); + if (NS_FAILED(rv)) { + return rv; + } + + LOG(("HTTP decompressor indexed entry %u\n", index)); + + if (index == 0) { + return NS_ERROR_FAILURE; + } + // NWGH - remove this line, since we'll keep everything 1-indexed + index--; // Internally, we 0-index everything, since this is, y'know, C++ + + return OutputHeader(index); +} + +nsresult Http2Decompressor::DoLiteralInternal(nsACString& name, + nsACString& value, + uint32_t namePrefixLen) { + // guts of doliteralwithoutindex and doliteralwithincremental + MOZ_ASSERT(((mData[mOffset] & 0xF0) == 0x00) || // withoutindex + ((mData[mOffset] & 0xF0) == 0x10) || // neverindexed + ((mData[mOffset] & 0xC0) == 0x40)); // withincremental + + // first let's get the name + uint32_t index; + nsresult rv = DecodeInteger(namePrefixLen, index); + if (NS_FAILED(rv)) { + return rv; + } + + // sanity check + if (mOffset >= mDataLen) { + NS_WARNING("Http2 Decompressor ran out of data"); + // This is session-fatal + return NS_ERROR_FAILURE; + } + + bool isHuffmanEncoded; + + if (!index) { + // name is embedded as a literal + uint32_t nameLen; + isHuffmanEncoded = mData[mOffset] & (1 << 7); + rv = DecodeInteger(7, nameLen); + if (NS_SUCCEEDED(rv)) { + if (isHuffmanEncoded) { + rv = CopyHuffmanStringFromInput(nameLen, name); + } else { + rv = CopyStringFromInput(nameLen, name); + } + } + LOG(("Http2Decompressor::DoLiteralInternal literal name %s", + name.BeginReading())); + } else { + // NWGH - make this index, not index - 1 + // name is from headertable + rv = CopyHeaderString(index - 1, name); + LOG(("Http2Decompressor::DoLiteralInternal indexed name %d %s", index, + name.BeginReading())); + } + if (NS_FAILED(rv)) { + return rv; + } + + // sanity check + if (mOffset >= mDataLen) { + NS_WARNING("Http2 Decompressor ran out of data"); + // This is session-fatal + return NS_ERROR_FAILURE; + } + + // now the value + uint32_t valueLen; + isHuffmanEncoded = mData[mOffset] & (1 << 7); + rv = DecodeInteger(7, valueLen); + if (NS_SUCCEEDED(rv)) { + if (isHuffmanEncoded) { + rv = CopyHuffmanStringFromInput(valueLen, value); + } else { + rv = CopyStringFromInput(valueLen, value); + } + } + if (NS_FAILED(rv)) { + return rv; + } + + int32_t newline = 0; + while ((newline = value.FindChar('\n', newline)) != -1) { + if (value[newline + 1] == ' ' || value[newline + 1] == '\t') { + LOG(("Http2Decompressor::Disallowing folded header value %s", + value.BeginReading())); + return NS_ERROR_ILLEGAL_VALUE; + } + // Increment this to avoid always finding the same newline and looping + // forever + ++newline; + } + + LOG(("Http2Decompressor::DoLiteralInternal value %s", value.BeginReading())); + return NS_OK; +} + +nsresult Http2Decompressor::DoLiteralWithoutIndex() { + // this starts with 0000 bit pattern + MOZ_ASSERT((mData[mOffset] & 0xF0) == 0x00); + + nsAutoCString name, value; + nsresult rv = DoLiteralInternal(name, value, 4); + + LOG(("HTTP decompressor literal without index %s %s\n", name.get(), + value.get())); + + if (NS_SUCCEEDED(rv)) { + rv = OutputHeader(name, value); + } + return rv; +} + +nsresult Http2Decompressor::DoLiteralWithIncremental() { + // this starts with 01 bit pattern + MOZ_ASSERT((mData[mOffset] & 0xC0) == 0x40); + + nsAutoCString name, value; + nsresult rv = DoLiteralInternal(name, value, 6); + if (NS_SUCCEEDED(rv)) { + rv = OutputHeader(name, value); + } + // Let NET_RESET continue on so that we don't get out of sync, as it is just + // used to kill the stream, not the session. + if (NS_FAILED(rv) && rv != NS_ERROR_NET_RESET) { + return rv; + } + + uint32_t room = nvPair(name, value).Size(); + if (room > mMaxBuffer) { + ClearHeaderTable(); + LOG( + ("HTTP decompressor literal with index not inserted due to size %u %s " + "%s\n", + room, name.get(), value.get())); + DumpState("Decompressor state after ClearHeaderTable"); + return rv; + } + + MakeRoom(room, "decompressor"); + + // Incremental Indexing implicitly adds a row to the header table. + mHeaderTable.AddElement(name, value); + + uint32_t currentSize = mHeaderTable.ByteCount(); + if (currentSize > mPeakSize) { + mPeakSize = currentSize; + } + + uint32_t currentCount = mHeaderTable.VariableLength(); + if (currentCount > mPeakCount) { + mPeakCount = currentCount; + } + + LOG(("HTTP decompressor literal with index 0 %s %s\n", name.get(), + value.get())); + + return rv; +} + +nsresult Http2Decompressor::DoLiteralNeverIndexed() { + // This starts with 0001 bit pattern + MOZ_ASSERT((mData[mOffset] & 0xF0) == 0x10); + + nsAutoCString name, value; + nsresult rv = DoLiteralInternal(name, value, 4); + + LOG(("HTTP decompressor literal never indexed %s %s\n", name.get(), + value.get())); + + if (NS_SUCCEEDED(rv)) { + rv = OutputHeader(name, value); + } + return rv; +} + +nsresult Http2Decompressor::DoContextUpdate() { + // This starts with 001 bit pattern + MOZ_ASSERT((mData[mOffset] & 0xE0) == 0x20); + + // Getting here means we have to adjust the max table size, because the + // compressor on the other end has signaled to us through HPACK (not H2) + // that it's using a size different from the currently-negotiated size. + // This change could either come about because we've sent a + // SETTINGS_HEADER_TABLE_SIZE, or because the encoder has decided that + // the current negotiated size doesn't fit its needs (for whatever reason) + // and so it needs to change it (either up to the max allowed by our SETTING, + // or down to some value below that) + uint32_t newMaxSize; + nsresult rv = DecodeInteger(5, newMaxSize); + LOG(("Http2Decompressor::DoContextUpdate new maximum size %u", newMaxSize)); + if (NS_FAILED(rv)) { + return rv; + } + + if (newMaxSize > mMaxBufferSetting) { + // This is fatal to the session - peer is trying to use a table larger + // than we have made available. + return NS_ERROR_FAILURE; + } + + SetMaxBufferSizeInternal(newMaxSize); + + return NS_OK; +} + +///////////////////////////////////////////////////////////////// + +nsresult Http2Compressor::EncodeHeaderBlock( + const nsCString& nvInput, const nsACString& method, const nsACString& path, + const nsACString& host, const nsACString& scheme, + const nsACString& protocol, bool simpleConnectForm, nsACString& output) { + mSetInitialMaxBufferSizeAllowed = false; + mOutput = &output; + output.Truncate(); + mParsedContentLength = -1; + + bool isWebsocket = (!simpleConnectForm && !protocol.IsEmpty()); + + // first thing's first - context size updates (if necessary) + if (mBufferSizeChangeWaiting) { + if (mLowestBufferSizeWaiting < mMaxBufferSetting) { + EncodeTableSizeChange(mLowestBufferSizeWaiting); + } + EncodeTableSizeChange(mMaxBufferSetting); + mBufferSizeChangeWaiting = false; + } + + // colon headers first + if (!simpleConnectForm) { + ProcessHeader(nvPair(":method"_ns, method), false, false); + ProcessHeader(nvPair(":path"_ns, path), true, false); + ProcessHeader(nvPair(":authority"_ns, host), false, false); + ProcessHeader(nvPair(":scheme"_ns, scheme), false, false); + if (isWebsocket) { + ProcessHeader(nvPair(":protocol"_ns, protocol), false, false); + } + } else { + ProcessHeader(nvPair(":method"_ns, method), false, false); + ProcessHeader(nvPair(":authority"_ns, host), false, false); + } + + // now the non colon headers + const char* beginBuffer = nvInput.BeginReading(); + + // This strips off the HTTP/1 method+path+version + int32_t crlfIndex = nvInput.Find("\r\n"); + while (true) { + int32_t startIndex = crlfIndex + 2; + + crlfIndex = nvInput.Find("\r\n", false, startIndex); + if (crlfIndex == -1) { + break; + } + + int32_t colonIndex = + nvInput.Find(":", false, startIndex, crlfIndex - startIndex); + if (colonIndex == -1) { + break; + } + + nsDependentCSubstring name = + Substring(beginBuffer + startIndex, beginBuffer + colonIndex); + // all header names are lower case in http/2 + ToLowerCase(name); + + // exclusions + if (name.EqualsLiteral("connection") || name.EqualsLiteral("host") || + name.EqualsLiteral("keep-alive") || + name.EqualsLiteral("proxy-connection") || name.EqualsLiteral("te") || + name.EqualsLiteral("transfer-encoding") || + name.EqualsLiteral("upgrade") || + name.EqualsLiteral("sec-websocket-key")) { + continue; + } + + // colon headers are for http/2 and this is http/1 input, so that + // is probably a smuggling attack of some kind + bool isColonHeader = false; + for (const char* cPtr = name.BeginReading(); + cPtr && cPtr < name.EndReading(); ++cPtr) { + if (*cPtr == ':') { + isColonHeader = true; + break; + } else if (*cPtr != ' ' && *cPtr != '\t') { + isColonHeader = false; + break; + } + } + if (isColonHeader) { + continue; + } + + int32_t valueIndex = colonIndex + 1; + + while (valueIndex < crlfIndex && beginBuffer[valueIndex] == ' ') + ++valueIndex; + + nsDependentCSubstring value = + Substring(beginBuffer + valueIndex, beginBuffer + crlfIndex); + + if (name.EqualsLiteral("content-length")) { + int64_t len; + nsCString tmp(value); + if (nsHttp::ParseInt64(tmp.get(), nullptr, &len)) { + mParsedContentLength = len; + } + } + + if (name.EqualsLiteral("cookie")) { + // cookie crumbling + bool haveMoreCookies = true; + int32_t nextCookie = valueIndex; + while (haveMoreCookies) { + int32_t semiSpaceIndex = + nvInput.Find("; ", false, nextCookie, crlfIndex - nextCookie); + if (semiSpaceIndex == -1) { + haveMoreCookies = false; + semiSpaceIndex = crlfIndex; + } + nsDependentCSubstring cookie = + Substring(beginBuffer + nextCookie, beginBuffer + semiSpaceIndex); + // cookies less than 20 bytes are not indexed + ProcessHeader(nvPair(name, cookie), false, cookie.Length() < 20); + nextCookie = semiSpaceIndex + 2; + } + } else { + // allow indexing of every non-cookie except authorization + ProcessHeader(nvPair(name, value), false, + name.EqualsLiteral("authorization")); + } + } + + // NB: This is a *really* ugly hack, but to do this in the right place (the + // transaction) would require totally reworking how/when the transaction + // creates its request stream, which is not worth the effort and risk of + // breakage just to add one header only to h2 connections. + if (!simpleConnectForm && !isWebsocket) { + // Add in TE: trailers for regular requests + nsAutoCString te("te"); + nsAutoCString trailers("trailers"); + ProcessHeader(nvPair(te, trailers), false, false); + } + + mOutput = nullptr; + DumpState("Compressor state after EncodeHeaderBlock"); + return NS_OK; +} + +void Http2Compressor::DoOutput(Http2Compressor::outputCode code, + const class nvPair* pair, uint32_t index) { + // start Byte needs to be calculated from the offset after + // the opcode has been written out in case the output stream + // buffer gets resized/relocated + uint32_t offset = mOutput->Length(); + uint8_t* startByte; + + switch (code) { + case kNeverIndexedLiteral: + LOG( + ("HTTP compressor %p neverindex literal with name reference %u %s " + "%s\n", + this, index, pair->mName.get(), pair->mValue.get())); + + // In this case, the index will have already been adjusted to be 1-based + // instead of 0-based. + EncodeInteger(4, index); // 0001 4 bit prefix + startByte = + reinterpret_cast(mOutput->BeginWriting()) + offset; + *startByte = (*startByte & 0x0f) | 0x10; + + if (!index) { + HuffmanAppend(pair->mName); + } + + HuffmanAppend(pair->mValue); + break; + + case kPlainLiteral: + LOG(("HTTP compressor %p noindex literal with name reference %u %s %s\n", + this, index, pair->mName.get(), pair->mValue.get())); + + // In this case, the index will have already been adjusted to be 1-based + // instead of 0-based. + EncodeInteger(4, index); // 0000 4 bit prefix + startByte = + reinterpret_cast(mOutput->BeginWriting()) + offset; + *startByte = *startByte & 0x0f; + + if (!index) { + HuffmanAppend(pair->mName); + } + + HuffmanAppend(pair->mValue); + break; + + case kIndexedLiteral: + LOG(("HTTP compressor %p literal with name reference %u %s %s\n", this, + index, pair->mName.get(), pair->mValue.get())); + + // In this case, the index will have already been adjusted to be 1-based + // instead of 0-based. + EncodeInteger(6, index); // 01 2 bit prefix + startByte = + reinterpret_cast(mOutput->BeginWriting()) + offset; + *startByte = (*startByte & 0x3f) | 0x40; + + if (!index) { + HuffmanAppend(pair->mName); + } + + HuffmanAppend(pair->mValue); + break; + + case kIndex: + LOG(("HTTP compressor %p index %u %s %s\n", this, index, + pair->mName.get(), pair->mValue.get())); + // NWGH - make this plain old index instead of index + 1 + // In this case, we are passed the raw 0-based C index, and need to + // increment to make it 1-based and comply with the spec + EncodeInteger(7, index + 1); + startByte = + reinterpret_cast(mOutput->BeginWriting()) + offset; + *startByte = *startByte | 0x80; // 1 1 bit prefix + break; + } +} + +// writes the encoded integer onto the output +void Http2Compressor::EncodeInteger(uint32_t prefixLen, uint32_t val) { + uint32_t mask = (1 << prefixLen) - 1; + uint8_t tmp; + + if (val < mask) { + // 1 byte encoding! + tmp = val; + mOutput->Append(reinterpret_cast(&tmp), 1); + return; + } + + if (mask) { + val -= mask; + tmp = mask; + mOutput->Append(reinterpret_cast(&tmp), 1); + } + + uint32_t q, r; + do { + q = val / 128; + r = val % 128; + tmp = r; + if (q) { + tmp |= 0x80; // chain bit + } + val = q; + mOutput->Append(reinterpret_cast(&tmp), 1); + } while (q); +} + +void Http2Compressor::HuffmanAppend(const nsCString& value) { + nsAutoCString buf; + uint8_t bitsLeft = 8; + uint32_t length = value.Length(); + uint32_t offset; + uint8_t* startByte; + + for (uint32_t i = 0; i < length; ++i) { + uint8_t idx = static_cast(value[i]); + uint8_t huffLength = HuffmanOutgoing[idx].mLength; + uint32_t huffValue = HuffmanOutgoing[idx].mValue; + + if (bitsLeft < 8) { + // Fill in the least significant bits of the previous byte + // first + uint32_t val; + if (huffLength >= bitsLeft) { + val = huffValue & ~((1 << (huffLength - bitsLeft)) - 1); + val >>= (huffLength - bitsLeft); + } else { + val = huffValue << (bitsLeft - huffLength); + } + val &= ((1 << bitsLeft) - 1); + offset = buf.Length() - 1; + startByte = reinterpret_cast(buf.BeginWriting()) + offset; + *startByte = *startByte | static_cast(val & 0xFF); + if (huffLength >= bitsLeft) { + huffLength -= bitsLeft; + bitsLeft = 8; + } else { + bitsLeft -= huffLength; + huffLength = 0; + } + } + + while (huffLength >= 8) { + uint32_t mask = ~((1 << (huffLength - 8)) - 1); + uint8_t val = ((huffValue & mask) >> (huffLength - 8)) & 0xFF; + buf.Append(reinterpret_cast(&val), 1); + huffLength -= 8; + } + + if (huffLength) { + // Fill in the most significant bits of the next byte + bitsLeft = 8 - huffLength; + uint8_t val = (huffValue & ((1 << huffLength) - 1)) << bitsLeft; + buf.Append(reinterpret_cast(&val), 1); + } + } + + if (bitsLeft != 8) { + // Pad the last bits with ones, which corresponds to the EOS + // encoding + uint8_t val = (1 << bitsLeft) - 1; + offset = buf.Length() - 1; + startByte = reinterpret_cast(buf.BeginWriting()) + offset; + *startByte = *startByte | val; + } + + // Now we know how long our encoded string is, we can fill in our length + uint32_t bufLength = buf.Length(); + offset = mOutput->Length(); + EncodeInteger(7, bufLength); + startByte = + reinterpret_cast(mOutput->BeginWriting()) + offset; + *startByte = *startByte | 0x80; + + // Finally, we can add our REAL data! + mOutput->Append(buf); + LOG( + ("Http2Compressor::HuffmanAppend %p encoded %d byte original on %d " + "bytes.\n", + this, length, bufLength)); +} + +void Http2Compressor::ProcessHeader(const nvPair inputPair, bool noLocalIndex, + bool neverIndex) { + uint32_t newSize = inputPair.Size(); + uint32_t headerTableSize = mHeaderTable.Length(); + uint32_t matchedIndex = 0u; + uint32_t nameReference = 0u; + bool match = false; + + LOG(("Http2Compressor::ProcessHeader %s %s", inputPair.mName.get(), + inputPair.mValue.get())); + + // NWGH - make this index = 1; index <= headerTableSize; ++index + for (uint32_t index = 0; index < headerTableSize; ++index) { + if (mHeaderTable[index]->mName.Equals(inputPair.mName)) { + // NWGH - make this nameReference = index + nameReference = index + 1; + if (mHeaderTable[index]->mValue.Equals(inputPair.mValue)) { + match = true; + matchedIndex = index; + break; + } + } + } + + // We need to emit a new literal + if (!match || noLocalIndex || neverIndex) { + if (neverIndex) { + DoOutput(kNeverIndexedLiteral, &inputPair, nameReference); + DumpState("Compressor state after literal never index"); + return; + } + + if (noLocalIndex || (newSize > (mMaxBuffer / 2)) || (mMaxBuffer < 128)) { + DoOutput(kPlainLiteral, &inputPair, nameReference); + DumpState("Compressor state after literal without index"); + return; + } + + // make sure to makeroom() first so that any implied items + // get preserved. + MakeRoom(newSize, "compressor"); + DoOutput(kIndexedLiteral, &inputPair, nameReference); + + mHeaderTable.AddElement(inputPair.mName, inputPair.mValue); + LOG(("HTTP compressor %p new literal placed at index 0\n", this)); + DumpState("Compressor state after literal with index"); + return; + } + + // emit an index + DoOutput(kIndex, &inputPair, matchedIndex); + + DumpState("Compressor state after index"); +} + +void Http2Compressor::EncodeTableSizeChange(uint32_t newMaxSize) { + uint32_t offset = mOutput->Length(); + EncodeInteger(5, newMaxSize); + uint8_t* startByte = + reinterpret_cast(mOutput->BeginWriting()) + offset; + *startByte = *startByte | 0x20; +} + +void Http2Compressor::SetMaxBufferSize(uint32_t maxBufferSize) { + mMaxBufferSetting = maxBufferSize; + SetMaxBufferSizeInternal(maxBufferSize); + if (!mBufferSizeChangeWaiting) { + mBufferSizeChangeWaiting = true; + mLowestBufferSizeWaiting = maxBufferSize; + } else if (maxBufferSize < mLowestBufferSizeWaiting) { + mLowestBufferSizeWaiting = maxBufferSize; + } +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/Http2Compression.h b/netwerk/protocol/http/Http2Compression.h new file mode 100644 index 0000000000..1ab6661449 --- /dev/null +++ b/netwerk/protocol/http/Http2Compression.h @@ -0,0 +1,206 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef mozilla_net_Http2Compression_Internal_h +#define mozilla_net_Http2Compression_Internal_h + +// HPACK - RFC 7541 +// https://www.rfc-editor.org/rfc/rfc7541.txt + +#include "mozilla/Attributes.h" +#include "nsDeque.h" +#include "nsString.h" +#include "mozilla/Telemetry.h" + +namespace mozilla { +namespace net { + +struct HuffmanIncomingTable; + +void Http2CompressionCleanup(); + +class nvPair { + public: + nvPair(const nsACString& name, const nsACString& value) + : mName(name), mValue(value) {} + + uint32_t Size() const { return mName.Length() + mValue.Length() + 32; } + size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const; + size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const; + + nsCString mName; + nsCString mValue; +}; + +class nvFIFO { + public: + nvFIFO(); + ~nvFIFO(); + void AddElement(const nsCString& name, const nsCString& value); + void AddElement(const nsCString& name); + void RemoveElement(); + uint32_t ByteCount() const; + uint32_t Length() const; + uint32_t VariableLength() const; + size_t StaticLength() const; + void Clear(); + const nvPair* operator[](size_t index) const; + + private: + uint32_t mByteCount; + nsDeque mTable; +}; + +class HpackDynamicTableReporter; + +class Http2BaseCompressor { + public: + Http2BaseCompressor(); + virtual ~Http2BaseCompressor(); + size_t SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const; + nsresult SetInitialMaxBufferSize(uint32_t maxBufferSize); + void SetDumpTables(bool dumpTables); + + protected: + const static uint32_t kDefaultMaxBuffer = 4096; + + virtual void ClearHeaderTable(); + virtual void MakeRoom(uint32_t amount, const char* direction); + virtual void DumpState(const char*); + virtual void SetMaxBufferSizeInternal(uint32_t maxBufferSize); + + nsACString* mOutput; + nvFIFO mHeaderTable; + + uint32_t mMaxBuffer; + uint32_t mMaxBufferSetting; + bool mSetInitialMaxBufferSizeAllowed; + + uint32_t mPeakSize; + uint32_t mPeakCount; + MOZ_INIT_OUTSIDE_CTOR + Telemetry::HistogramID mPeakSizeID; + MOZ_INIT_OUTSIDE_CTOR + Telemetry::HistogramID mPeakCountID; + + bool mDumpTables; + + private: + RefPtr mDynamicReporter; +}; + +class Http2Compressor; + +class Http2Decompressor final : public Http2BaseCompressor { + public: + Http2Decompressor() + : mOffset(0), + mData(nullptr), + mDataLen(0), + mSeenNonColonHeader(false), + mIsPush(false) { + mPeakSizeID = Telemetry::HPACK_PEAK_SIZE_DECOMPRESSOR; + mPeakCountID = Telemetry::HPACK_PEAK_COUNT_DECOMPRESSOR; + }; + virtual ~Http2Decompressor() = default; + + // NS_OK: Produces the working set of HTTP/1 formatted headers + [[nodiscard]] nsresult DecodeHeaderBlock(const uint8_t* data, + uint32_t datalen, nsACString& output, + bool isPush); + + void GetStatus(nsACString& hdr) { hdr = mHeaderStatus; } + void GetHost(nsACString& hdr) { hdr = mHeaderHost; } + void GetScheme(nsACString& hdr) { hdr = mHeaderScheme; } + void GetPath(nsACString& hdr) { hdr = mHeaderPath; } + void GetMethod(nsACString& hdr) { hdr = mHeaderMethod; } + + private: + [[nodiscard]] nsresult DoIndexed(); + [[nodiscard]] nsresult DoLiteralWithoutIndex(); + [[nodiscard]] nsresult DoLiteralWithIncremental(); + [[nodiscard]] nsresult DoLiteralInternal(nsACString&, nsACString&, uint32_t); + [[nodiscard]] nsresult DoLiteralNeverIndexed(); + [[nodiscard]] nsresult DoContextUpdate(); + + [[nodiscard]] nsresult DecodeInteger(uint32_t prefixLen, uint32_t& result); + [[nodiscard]] nsresult OutputHeader(uint32_t index); + [[nodiscard]] nsresult OutputHeader(const nsACString& name, + const nsACString& value); + + [[nodiscard]] nsresult CopyHeaderString(uint32_t index, nsACString& name); + [[nodiscard]] nsresult CopyStringFromInput(uint32_t index, nsACString& val); + uint8_t ExtractByte(uint8_t bitsLeft, uint32_t& bytesConsumed); + [[nodiscard]] nsresult CopyHuffmanStringFromInput(uint32_t index, + nsACString& val); + [[nodiscard]] nsresult DecodeHuffmanCharacter( + const HuffmanIncomingTable* table, uint8_t& c, uint32_t& bytesConsumed, + uint8_t& bitsLeft); + [[nodiscard]] nsresult DecodeFinalHuffmanCharacter( + const HuffmanIncomingTable* table, uint8_t& c, uint8_t& bitsLeft); + + nsCString mHeaderStatus; + nsCString mHeaderHost; + nsCString mHeaderScheme; + nsCString mHeaderPath; + nsCString mHeaderMethod; + + // state variables when DecodeBlock() is on the stack + uint32_t mOffset; + const uint8_t* mData; + uint32_t mDataLen; + bool mSeenNonColonHeader; + bool mIsPush; +}; + +class Http2Compressor final : public Http2BaseCompressor { + public: + Http2Compressor() + : mParsedContentLength(-1), + mBufferSizeChangeWaiting(false), + mLowestBufferSizeWaiting(0) { + mPeakSizeID = Telemetry::HPACK_PEAK_SIZE_COMPRESSOR; + mPeakCountID = Telemetry::HPACK_PEAK_COUNT_COMPRESSOR; + }; + virtual ~Http2Compressor() = default; + + // HTTP/1 formatted header block as input - HTTP/2 formatted + // header block as output + [[nodiscard]] nsresult EncodeHeaderBlock( + const nsCString& nvInput, const nsACString& method, + const nsACString& path, const nsACString& host, const nsACString& scheme, + const nsACString& protocol, bool simpleConnectForm, nsACString& output); + + int64_t GetParsedContentLength() { + return mParsedContentLength; + } // -1 on not found + + void SetMaxBufferSize(uint32_t maxBufferSize); + + private: + enum outputCode { + kNeverIndexedLiteral, + kPlainLiteral, + kIndexedLiteral, + kIndex + }; + + void DoOutput(Http2Compressor::outputCode code, const class nvPair* pair, + uint32_t index); + void EncodeInteger(uint32_t prefixLen, uint32_t val); + void ProcessHeader(const nvPair inputPair, bool noLocalIndex, + bool neverIndex); + void HuffmanAppend(const nsCString& value); + void EncodeTableSizeChange(uint32_t newMaxSize); + + int64_t mParsedContentLength; + bool mBufferSizeChangeWaiting; + uint32_t mLowestBufferSizeWaiting; +}; + +} // namespace net +} // namespace mozilla + +#endif // mozilla_net_Http2Compression_Internal_h diff --git a/netwerk/protocol/http/Http2HuffmanIncoming.h b/netwerk/protocol/http/Http2HuffmanIncoming.h new file mode 100644 index 0000000000..0b5a8e9a1f --- /dev/null +++ b/netwerk/protocol/http/Http2HuffmanIncoming.h @@ -0,0 +1,709 @@ +/* + * THIS FILE IS AUTO-GENERATED. DO NOT EDIT! + */ +#ifndef mozilla__net__Http2HuffmanIncoming_h +#define mozilla__net__Http2HuffmanIncoming_h + +namespace mozilla { +namespace net { + +struct HuffmanIncomingTable; + +struct HuffmanIncomingEntry { + uint16_t mValue : 9; // 9 bits so it can hold 0..256 + uint16_t mPrefixLen : 7; // only holds 1..8 +}; + +// The data members are public only so they can be statically constructed. All +// accesses should be done through the functions. +struct HuffmanIncomingTable { + // The normal entries, for indices in the range 0..(mNumEntries-1). + const HuffmanIncomingEntry* const mEntries; + + // The next tables, for indices in the range mNumEntries..255. Must be + // |nullptr| if mIndexOfFirstNextTable is 256. + const HuffmanIncomingTable** const mNextTables; + + // The index of the first next table (equal to the number of entries in + // mEntries). This cannot be a uint8_t because it can have the value 256, + // in which case there are no next tables and mNextTables must be |nullptr|. + const uint16_t mIndexOfFirstNextTable; + + const uint8_t mPrefixLen; + + bool IndexHasANextTable(uint8_t aIndex) const { + return aIndex >= mIndexOfFirstNextTable; + } + + const HuffmanIncomingEntry* Entry(uint8_t aIndex) const { + MOZ_ASSERT(aIndex < mIndexOfFirstNextTable); + return &mEntries[aIndex]; + } + + const HuffmanIncomingTable* NextTable(uint8_t aIndex) const { + MOZ_ASSERT(aIndex >= mIndexOfFirstNextTable); + return mNextTables[aIndex - mIndexOfFirstNextTable]; + } +}; + +static const HuffmanIncomingEntry HuffmanIncomingEntries_254[] = { + {33, 2}, {33, 2}, {33, 2}, {33, 2}, {33, 2}, {33, 2}, {33, 2}, {33, 2}, + {33, 2}, {33, 2}, {33, 2}, {33, 2}, {33, 2}, {33, 2}, {33, 2}, {33, 2}, + {33, 2}, {33, 2}, {33, 2}, {33, 2}, {33, 2}, {33, 2}, {33, 2}, {33, 2}, + {33, 2}, {33, 2}, {33, 2}, {33, 2}, {33, 2}, {33, 2}, {33, 2}, {33, 2}, + {33, 2}, {33, 2}, {33, 2}, {33, 2}, {33, 2}, {33, 2}, {33, 2}, {33, 2}, + {33, 2}, {33, 2}, {33, 2}, {33, 2}, {33, 2}, {33, 2}, {33, 2}, {33, 2}, + {33, 2}, {33, 2}, {33, 2}, {33, 2}, {33, 2}, {33, 2}, {33, 2}, {33, 2}, + {33, 2}, {33, 2}, {33, 2}, {33, 2}, {33, 2}, {33, 2}, {33, 2}, {33, 2}, + {34, 2}, {34, 2}, {34, 2}, {34, 2}, {34, 2}, {34, 2}, {34, 2}, {34, 2}, + {34, 2}, {34, 2}, {34, 2}, {34, 2}, {34, 2}, {34, 2}, {34, 2}, {34, 2}, + {34, 2}, {34, 2}, {34, 2}, {34, 2}, {34, 2}, {34, 2}, {34, 2}, {34, 2}, + {34, 2}, {34, 2}, {34, 2}, {34, 2}, {34, 2}, {34, 2}, {34, 2}, {34, 2}, + {34, 2}, {34, 2}, {34, 2}, {34, 2}, {34, 2}, {34, 2}, {34, 2}, {34, 2}, + {34, 2}, {34, 2}, {34, 2}, {34, 2}, {34, 2}, {34, 2}, {34, 2}, {34, 2}, + {34, 2}, {34, 2}, {34, 2}, {34, 2}, {34, 2}, {34, 2}, {34, 2}, {34, 2}, + {34, 2}, {34, 2}, {34, 2}, {34, 2}, {34, 2}, {34, 2}, {34, 2}, {34, 2}, + {40, 2}, {40, 2}, {40, 2}, {40, 2}, {40, 2}, {40, 2}, {40, 2}, {40, 2}, + {40, 2}, {40, 2}, {40, 2}, {40, 2}, {40, 2}, {40, 2}, {40, 2}, {40, 2}, + {40, 2}, {40, 2}, {40, 2}, {40, 2}, {40, 2}, {40, 2}, {40, 2}, {40, 2}, + {40, 2}, {40, 2}, {40, 2}, {40, 2}, {40, 2}, {40, 2}, {40, 2}, {40, 2}, + {40, 2}, {40, 2}, {40, 2}, {40, 2}, {40, 2}, {40, 2}, {40, 2}, {40, 2}, + {40, 2}, {40, 2}, {40, 2}, {40, 2}, {40, 2}, {40, 2}, {40, 2}, {40, 2}, + {40, 2}, {40, 2}, {40, 2}, {40, 2}, {40, 2}, {40, 2}, {40, 2}, {40, 2}, + {40, 2}, {40, 2}, {40, 2}, {40, 2}, {40, 2}, {40, 2}, {40, 2}, {40, 2}, + {41, 2}, {41, 2}, {41, 2}, {41, 2}, {41, 2}, {41, 2}, {41, 2}, {41, 2}, + {41, 2}, {41, 2}, {41, 2}, {41, 2}, {41, 2}, {41, 2}, {41, 2}, {41, 2}, + {41, 2}, {41, 2}, {41, 2}, {41, 2}, {41, 2}, {41, 2}, {41, 2}, {41, 2}, + {41, 2}, {41, 2}, {41, 2}, {41, 2}, {41, 2}, {41, 2}, {41, 2}, {41, 2}, + {41, 2}, {41, 2}, {41, 2}, {41, 2}, {41, 2}, {41, 2}, {41, 2}, {41, 2}, + {41, 2}, {41, 2}, {41, 2}, {41, 2}, {41, 2}, {41, 2}, {41, 2}, {41, 2}, + {41, 2}, {41, 2}, {41, 2}, {41, 2}, {41, 2}, {41, 2}, {41, 2}, {41, 2}, + {41, 2}, {41, 2}, {41, 2}, {41, 2}, {41, 2}, {41, 2}, {41, 2}, {41, 2}, +}; + +static const HuffmanIncomingTable HuffmanIncoming_254 = { + HuffmanIncomingEntries_254, nullptr, 256, 2}; + +static const HuffmanIncomingEntry HuffmanIncomingEntries_255_254[] = { + {92, 3}, {92, 3}, {92, 3}, {92, 3}, {92, 3}, {92, 3}, {92, 3}, + {92, 3}, {92, 3}, {92, 3}, {92, 3}, {92, 3}, {92, 3}, {92, 3}, + {92, 3}, {92, 3}, {92, 3}, {92, 3}, {92, 3}, {92, 3}, {92, 3}, + {92, 3}, {92, 3}, {92, 3}, {92, 3}, {92, 3}, {92, 3}, {92, 3}, + {92, 3}, {92, 3}, {92, 3}, {92, 3}, {195, 3}, {195, 3}, {195, 3}, + {195, 3}, {195, 3}, {195, 3}, {195, 3}, {195, 3}, {195, 3}, {195, 3}, + {195, 3}, {195, 3}, {195, 3}, {195, 3}, {195, 3}, {195, 3}, {195, 3}, + {195, 3}, {195, 3}, {195, 3}, {195, 3}, {195, 3}, {195, 3}, {195, 3}, + {195, 3}, {195, 3}, {195, 3}, {195, 3}, {195, 3}, {195, 3}, {195, 3}, + {195, 3}, {208, 3}, {208, 3}, {208, 3}, {208, 3}, {208, 3}, {208, 3}, + {208, 3}, {208, 3}, {208, 3}, {208, 3}, {208, 3}, {208, 3}, {208, 3}, + {208, 3}, {208, 3}, {208, 3}, {208, 3}, {208, 3}, {208, 3}, {208, 3}, + {208, 3}, {208, 3}, {208, 3}, {208, 3}, {208, 3}, {208, 3}, {208, 3}, + {208, 3}, {208, 3}, {208, 3}, {208, 3}, {208, 3}, {128, 4}, {128, 4}, + {128, 4}, {128, 4}, {128, 4}, {128, 4}, {128, 4}, {128, 4}, {128, 4}, + {128, 4}, {128, 4}, {128, 4}, {128, 4}, {128, 4}, {128, 4}, {128, 4}, + {130, 4}, {130, 4}, {130, 4}, {130, 4}, {130, 4}, {130, 4}, {130, 4}, + {130, 4}, {130, 4}, {130, 4}, {130, 4}, {130, 4}, {130, 4}, {130, 4}, + {130, 4}, {130, 4}, {131, 4}, {131, 4}, {131, 4}, {131, 4}, {131, 4}, + {131, 4}, {131, 4}, {131, 4}, {131, 4}, {131, 4}, {131, 4}, {131, 4}, + {131, 4}, {131, 4}, {131, 4}, {131, 4}, {162, 4}, {162, 4}, {162, 4}, + {162, 4}, {162, 4}, {162, 4}, {162, 4}, {162, 4}, {162, 4}, {162, 4}, + {162, 4}, {162, 4}, {162, 4}, {162, 4}, {162, 4}, {162, 4}, {184, 4}, + {184, 4}, {184, 4}, {184, 4}, {184, 4}, {184, 4}, {184, 4}, {184, 4}, + {184, 4}, {184, 4}, {184, 4}, {184, 4}, {184, 4}, {184, 4}, {184, 4}, + {184, 4}, {194, 4}, {194, 4}, {194, 4}, {194, 4}, {194, 4}, {194, 4}, + {194, 4}, {194, 4}, {194, 4}, {194, 4}, {194, 4}, {194, 4}, {194, 4}, + {194, 4}, {194, 4}, {194, 4}, {224, 4}, {224, 4}, {224, 4}, {224, 4}, + {224, 4}, {224, 4}, {224, 4}, {224, 4}, {224, 4}, {224, 4}, {224, 4}, + {224, 4}, {224, 4}, {224, 4}, {224, 4}, {224, 4}, {226, 4}, {226, 4}, + {226, 4}, {226, 4}, {226, 4}, {226, 4}, {226, 4}, {226, 4}, {226, 4}, + {226, 4}, {226, 4}, {226, 4}, {226, 4}, {226, 4}, {226, 4}, {226, 4}, + {153, 5}, {153, 5}, {153, 5}, {153, 5}, {153, 5}, {153, 5}, {153, 5}, + {153, 5}, {161, 5}, {161, 5}, {161, 5}, {161, 5}, {161, 5}, {161, 5}, + {161, 5}, {161, 5}, {167, 5}, {167, 5}, {167, 5}, {167, 5}, {167, 5}, + {167, 5}, {167, 5}, {167, 5}, {172, 5}, {172, 5}, {172, 5}, {172, 5}, + {172, 5}, {172, 5}, {172, 5}, {172, 5}, +}; + +static const HuffmanIncomingTable HuffmanIncoming_255_254 = { + HuffmanIncomingEntries_255_254, nullptr, 256, 5}; + +static const HuffmanIncomingEntry HuffmanIncomingEntries_255_255_246[] = { + {199, 1}, {199, 1}, {199, 1}, {199, 1}, {199, 1}, {199, 1}, {199, 1}, + {199, 1}, {199, 1}, {199, 1}, {199, 1}, {199, 1}, {199, 1}, {199, 1}, + {199, 1}, {199, 1}, {199, 1}, {199, 1}, {199, 1}, {199, 1}, {199, 1}, + {199, 1}, {199, 1}, {199, 1}, {199, 1}, {199, 1}, {199, 1}, {199, 1}, + {199, 1}, {199, 1}, {199, 1}, {199, 1}, {199, 1}, {199, 1}, {199, 1}, + {199, 1}, {199, 1}, {199, 1}, {199, 1}, {199, 1}, {199, 1}, {199, 1}, + {199, 1}, {199, 1}, {199, 1}, {199, 1}, {199, 1}, {199, 1}, {199, 1}, + {199, 1}, {199, 1}, {199, 1}, {199, 1}, {199, 1}, {199, 1}, {199, 1}, + {199, 1}, {199, 1}, {199, 1}, {199, 1}, {199, 1}, {199, 1}, {199, 1}, + {199, 1}, {199, 1}, {199, 1}, {199, 1}, {199, 1}, {199, 1}, {199, 1}, + {199, 1}, {199, 1}, {199, 1}, {199, 1}, {199, 1}, {199, 1}, {199, 1}, + {199, 1}, {199, 1}, {199, 1}, {199, 1}, {199, 1}, {199, 1}, {199, 1}, + {199, 1}, {199, 1}, {199, 1}, {199, 1}, {199, 1}, {199, 1}, {199, 1}, + {199, 1}, {199, 1}, {199, 1}, {199, 1}, {199, 1}, {199, 1}, {199, 1}, + {199, 1}, {199, 1}, {199, 1}, {199, 1}, {199, 1}, {199, 1}, {199, 1}, + {199, 1}, {199, 1}, {199, 1}, {199, 1}, {199, 1}, {199, 1}, {199, 1}, + {199, 1}, {199, 1}, {199, 1}, {199, 1}, {199, 1}, {199, 1}, {199, 1}, + {199, 1}, {199, 1}, {199, 1}, {199, 1}, {199, 1}, {199, 1}, {199, 1}, + {199, 1}, {199, 1}, {207, 1}, {207, 1}, {207, 1}, {207, 1}, {207, 1}, + {207, 1}, {207, 1}, {207, 1}, {207, 1}, {207, 1}, {207, 1}, {207, 1}, + {207, 1}, {207, 1}, {207, 1}, {207, 1}, {207, 1}, {207, 1}, {207, 1}, + {207, 1}, {207, 1}, {207, 1}, {207, 1}, {207, 1}, {207, 1}, {207, 1}, + {207, 1}, {207, 1}, {207, 1}, {207, 1}, {207, 1}, {207, 1}, {207, 1}, + {207, 1}, {207, 1}, {207, 1}, {207, 1}, {207, 1}, {207, 1}, {207, 1}, + {207, 1}, {207, 1}, {207, 1}, {207, 1}, {207, 1}, {207, 1}, {207, 1}, + {207, 1}, {207, 1}, {207, 1}, {207, 1}, {207, 1}, {207, 1}, {207, 1}, + {207, 1}, {207, 1}, {207, 1}, {207, 1}, {207, 1}, {207, 1}, {207, 1}, + {207, 1}, {207, 1}, {207, 1}, {207, 1}, {207, 1}, {207, 1}, {207, 1}, + {207, 1}, {207, 1}, {207, 1}, {207, 1}, {207, 1}, {207, 1}, {207, 1}, + {207, 1}, {207, 1}, {207, 1}, {207, 1}, {207, 1}, {207, 1}, {207, 1}, + {207, 1}, {207, 1}, {207, 1}, {207, 1}, {207, 1}, {207, 1}, {207, 1}, + {207, 1}, {207, 1}, {207, 1}, {207, 1}, {207, 1}, {207, 1}, {207, 1}, + {207, 1}, {207, 1}, {207, 1}, {207, 1}, {207, 1}, {207, 1}, {207, 1}, + {207, 1}, {207, 1}, {207, 1}, {207, 1}, {207, 1}, {207, 1}, {207, 1}, + {207, 1}, {207, 1}, {207, 1}, {207, 1}, {207, 1}, {207, 1}, {207, 1}, + {207, 1}, {207, 1}, {207, 1}, {207, 1}, {207, 1}, {207, 1}, {207, 1}, + {207, 1}, {207, 1}, {207, 1}, {207, 1}, +}; + +static const HuffmanIncomingTable HuffmanIncoming_255_255_246 = { + HuffmanIncomingEntries_255_255_246, nullptr, 256, 1}; + +static const HuffmanIncomingEntry HuffmanIncomingEntries_255_255_247[] = { + {234, 1}, {234, 1}, {234, 1}, {234, 1}, {234, 1}, {234, 1}, {234, 1}, + {234, 1}, {234, 1}, {234, 1}, {234, 1}, {234, 1}, {234, 1}, {234, 1}, + {234, 1}, {234, 1}, {234, 1}, {234, 1}, {234, 1}, {234, 1}, {234, 1}, + {234, 1}, {234, 1}, {234, 1}, {234, 1}, {234, 1}, {234, 1}, {234, 1}, + {234, 1}, {234, 1}, {234, 1}, {234, 1}, {234, 1}, {234, 1}, {234, 1}, + {234, 1}, {234, 1}, {234, 1}, {234, 1}, {234, 1}, {234, 1}, {234, 1}, + {234, 1}, {234, 1}, {234, 1}, {234, 1}, {234, 1}, {234, 1}, {234, 1}, + {234, 1}, {234, 1}, {234, 1}, {234, 1}, {234, 1}, {234, 1}, {234, 1}, + {234, 1}, {234, 1}, {234, 1}, {234, 1}, {234, 1}, {234, 1}, {234, 1}, + {234, 1}, {234, 1}, {234, 1}, {234, 1}, {234, 1}, {234, 1}, {234, 1}, + {234, 1}, {234, 1}, {234, 1}, {234, 1}, {234, 1}, {234, 1}, {234, 1}, + {234, 1}, {234, 1}, {234, 1}, {234, 1}, {234, 1}, {234, 1}, {234, 1}, + {234, 1}, {234, 1}, {234, 1}, {234, 1}, {234, 1}, {234, 1}, {234, 1}, + {234, 1}, {234, 1}, {234, 1}, {234, 1}, {234, 1}, {234, 1}, {234, 1}, + {234, 1}, {234, 1}, {234, 1}, {234, 1}, {234, 1}, {234, 1}, {234, 1}, + {234, 1}, {234, 1}, {234, 1}, {234, 1}, {234, 1}, {234, 1}, {234, 1}, + {234, 1}, {234, 1}, {234, 1}, {234, 1}, {234, 1}, {234, 1}, {234, 1}, + {234, 1}, {234, 1}, {234, 1}, {234, 1}, {234, 1}, {234, 1}, {234, 1}, + {234, 1}, {234, 1}, {235, 1}, {235, 1}, {235, 1}, {235, 1}, {235, 1}, + {235, 1}, {235, 1}, {235, 1}, {235, 1}, {235, 1}, {235, 1}, {235, 1}, + {235, 1}, {235, 1}, {235, 1}, {235, 1}, {235, 1}, {235, 1}, {235, 1}, + {235, 1}, {235, 1}, {235, 1}, {235, 1}, {235, 1}, {235, 1}, {235, 1}, + {235, 1}, {235, 1}, {235, 1}, {235, 1}, {235, 1}, {235, 1}, {235, 1}, + {235, 1}, {235, 1}, {235, 1}, {235, 1}, {235, 1}, {235, 1}, {235, 1}, + {235, 1}, {235, 1}, {235, 1}, {235, 1}, {235, 1}, {235, 1}, {235, 1}, + {235, 1}, {235, 1}, {235, 1}, {235, 1}, {235, 1}, {235, 1}, {235, 1}, + {235, 1}, {235, 1}, {235, 1}, {235, 1}, {235, 1}, {235, 1}, {235, 1}, + {235, 1}, {235, 1}, {235, 1}, {235, 1}, {235, 1}, {235, 1}, {235, 1}, + {235, 1}, {235, 1}, {235, 1}, {235, 1}, {235, 1}, {235, 1}, {235, 1}, + {235, 1}, {235, 1}, {235, 1}, {235, 1}, {235, 1}, {235, 1}, {235, 1}, + {235, 1}, {235, 1}, {235, 1}, {235, 1}, {235, 1}, {235, 1}, {235, 1}, + {235, 1}, {235, 1}, {235, 1}, {235, 1}, {235, 1}, {235, 1}, {235, 1}, + {235, 1}, {235, 1}, {235, 1}, {235, 1}, {235, 1}, {235, 1}, {235, 1}, + {235, 1}, {235, 1}, {235, 1}, {235, 1}, {235, 1}, {235, 1}, {235, 1}, + {235, 1}, {235, 1}, {235, 1}, {235, 1}, {235, 1}, {235, 1}, {235, 1}, + {235, 1}, {235, 1}, {235, 1}, {235, 1}, {235, 1}, {235, 1}, {235, 1}, + {235, 1}, {235, 1}, {235, 1}, {235, 1}, +}; + +static const HuffmanIncomingTable HuffmanIncoming_255_255_247 = { + HuffmanIncomingEntries_255_255_247, nullptr, 256, 1}; + +static const HuffmanIncomingEntry HuffmanIncomingEntries_255_255_248[] = { + {192, 2}, {192, 2}, {192, 2}, {192, 2}, {192, 2}, {192, 2}, {192, 2}, + {192, 2}, {192, 2}, {192, 2}, {192, 2}, {192, 2}, {192, 2}, {192, 2}, + {192, 2}, {192, 2}, {192, 2}, {192, 2}, {192, 2}, {192, 2}, {192, 2}, + {192, 2}, {192, 2}, {192, 2}, {192, 2}, {192, 2}, {192, 2}, {192, 2}, + {192, 2}, {192, 2}, {192, 2}, {192, 2}, {192, 2}, {192, 2}, {192, 2}, + {192, 2}, {192, 2}, {192, 2}, {192, 2}, {192, 2}, {192, 2}, {192, 2}, + {192, 2}, {192, 2}, {192, 2}, {192, 2}, {192, 2}, {192, 2}, {192, 2}, + {192, 2}, {192, 2}, {192, 2}, {192, 2}, {192, 2}, {192, 2}, {192, 2}, + {192, 2}, {192, 2}, {192, 2}, {192, 2}, {192, 2}, {192, 2}, {192, 2}, + {192, 2}, {193, 2}, {193, 2}, {193, 2}, {193, 2}, {193, 2}, {193, 2}, + {193, 2}, {193, 2}, {193, 2}, {193, 2}, {193, 2}, {193, 2}, {193, 2}, + {193, 2}, {193, 2}, {193, 2}, {193, 2}, {193, 2}, {193, 2}, {193, 2}, + {193, 2}, {193, 2}, {193, 2}, {193, 2}, {193, 2}, {193, 2}, {193, 2}, + {193, 2}, {193, 2}, {193, 2}, {193, 2}, {193, 2}, {193, 2}, {193, 2}, + {193, 2}, {193, 2}, {193, 2}, {193, 2}, {193, 2}, {193, 2}, {193, 2}, + {193, 2}, {193, 2}, {193, 2}, {193, 2}, {193, 2}, {193, 2}, {193, 2}, + {193, 2}, {193, 2}, {193, 2}, {193, 2}, {193, 2}, {193, 2}, {193, 2}, + {193, 2}, {193, 2}, {193, 2}, {193, 2}, {193, 2}, {193, 2}, {193, 2}, + {193, 2}, {193, 2}, {200, 2}, {200, 2}, {200, 2}, {200, 2}, {200, 2}, + {200, 2}, {200, 2}, {200, 2}, {200, 2}, {200, 2}, {200, 2}, {200, 2}, + {200, 2}, {200, 2}, {200, 2}, {200, 2}, {200, 2}, {200, 2}, {200, 2}, + {200, 2}, {200, 2}, {200, 2}, {200, 2}, {200, 2}, {200, 2}, {200, 2}, + {200, 2}, {200, 2}, {200, 2}, {200, 2}, {200, 2}, {200, 2}, {200, 2}, + {200, 2}, {200, 2}, {200, 2}, {200, 2}, {200, 2}, {200, 2}, {200, 2}, + {200, 2}, {200, 2}, {200, 2}, {200, 2}, {200, 2}, {200, 2}, {200, 2}, + {200, 2}, {200, 2}, {200, 2}, {200, 2}, {200, 2}, {200, 2}, {200, 2}, + {200, 2}, {200, 2}, {200, 2}, {200, 2}, {200, 2}, {200, 2}, {200, 2}, + {200, 2}, {200, 2}, {200, 2}, {201, 2}, {201, 2}, {201, 2}, {201, 2}, + {201, 2}, {201, 2}, {201, 2}, {201, 2}, {201, 2}, {201, 2}, {201, 2}, + {201, 2}, {201, 2}, {201, 2}, {201, 2}, {201, 2}, {201, 2}, {201, 2}, + {201, 2}, {201, 2}, {201, 2}, {201, 2}, {201, 2}, {201, 2}, {201, 2}, + {201, 2}, {201, 2}, {201, 2}, {201, 2}, {201, 2}, {201, 2}, {201, 2}, + {201, 2}, {201, 2}, {201, 2}, {201, 2}, {201, 2}, {201, 2}, {201, 2}, + {201, 2}, {201, 2}, {201, 2}, {201, 2}, {201, 2}, {201, 2}, {201, 2}, + {201, 2}, {201, 2}, {201, 2}, {201, 2}, {201, 2}, {201, 2}, {201, 2}, + {201, 2}, {201, 2}, {201, 2}, {201, 2}, {201, 2}, {201, 2}, {201, 2}, + {201, 2}, {201, 2}, {201, 2}, {201, 2}, +}; + +static const HuffmanIncomingTable HuffmanIncoming_255_255_248 = { + HuffmanIncomingEntries_255_255_248, nullptr, 256, 2}; + +static const HuffmanIncomingEntry HuffmanIncomingEntries_255_255_249[] = { + {202, 2}, {202, 2}, {202, 2}, {202, 2}, {202, 2}, {202, 2}, {202, 2}, + {202, 2}, {202, 2}, {202, 2}, {202, 2}, {202, 2}, {202, 2}, {202, 2}, + {202, 2}, {202, 2}, {202, 2}, {202, 2}, {202, 2}, {202, 2}, {202, 2}, + {202, 2}, {202, 2}, {202, 2}, {202, 2}, {202, 2}, {202, 2}, {202, 2}, + {202, 2}, {202, 2}, {202, 2}, {202, 2}, {202, 2}, {202, 2}, {202, 2}, + {202, 2}, {202, 2}, {202, 2}, {202, 2}, {202, 2}, {202, 2}, {202, 2}, + {202, 2}, {202, 2}, {202, 2}, {202, 2}, {202, 2}, {202, 2}, {202, 2}, + {202, 2}, {202, 2}, {202, 2}, {202, 2}, {202, 2}, {202, 2}, {202, 2}, + {202, 2}, {202, 2}, {202, 2}, {202, 2}, {202, 2}, {202, 2}, {202, 2}, + {202, 2}, {205, 2}, {205, 2}, {205, 2}, {205, 2}, {205, 2}, {205, 2}, + {205, 2}, {205, 2}, {205, 2}, {205, 2}, {205, 2}, {205, 2}, {205, 2}, + {205, 2}, {205, 2}, {205, 2}, {205, 2}, {205, 2}, {205, 2}, {205, 2}, + {205, 2}, {205, 2}, {205, 2}, {205, 2}, {205, 2}, {205, 2}, {205, 2}, + {205, 2}, {205, 2}, {205, 2}, {205, 2}, {205, 2}, {205, 2}, {205, 2}, + {205, 2}, {205, 2}, {205, 2}, {205, 2}, {205, 2}, {205, 2}, {205, 2}, + {205, 2}, {205, 2}, {205, 2}, {205, 2}, {205, 2}, {205, 2}, {205, 2}, + {205, 2}, {205, 2}, {205, 2}, {205, 2}, {205, 2}, {205, 2}, {205, 2}, + {205, 2}, {205, 2}, {205, 2}, {205, 2}, {205, 2}, {205, 2}, {205, 2}, + {205, 2}, {205, 2}, {210, 2}, {210, 2}, {210, 2}, {210, 2}, {210, 2}, + {210, 2}, {210, 2}, {210, 2}, {210, 2}, {210, 2}, {210, 2}, {210, 2}, + {210, 2}, {210, 2}, {210, 2}, {210, 2}, {210, 2}, {210, 2}, {210, 2}, + {210, 2}, {210, 2}, {210, 2}, {210, 2}, {210, 2}, {210, 2}, {210, 2}, + {210, 2}, {210, 2}, {210, 2}, {210, 2}, {210, 2}, {210, 2}, {210, 2}, + {210, 2}, {210, 2}, {210, 2}, {210, 2}, {210, 2}, {210, 2}, {210, 2}, + {210, 2}, {210, 2}, {210, 2}, {210, 2}, {210, 2}, {210, 2}, {210, 2}, + {210, 2}, {210, 2}, {210, 2}, {210, 2}, {210, 2}, {210, 2}, {210, 2}, + {210, 2}, {210, 2}, {210, 2}, {210, 2}, {210, 2}, {210, 2}, {210, 2}, + {210, 2}, {210, 2}, {210, 2}, {213, 2}, {213, 2}, {213, 2}, {213, 2}, + {213, 2}, {213, 2}, {213, 2}, {213, 2}, {213, 2}, {213, 2}, {213, 2}, + {213, 2}, {213, 2}, {213, 2}, {213, 2}, {213, 2}, {213, 2}, {213, 2}, + {213, 2}, {213, 2}, {213, 2}, {213, 2}, {213, 2}, {213, 2}, {213, 2}, + {213, 2}, {213, 2}, {213, 2}, {213, 2}, {213, 2}, {213, 2}, {213, 2}, + {213, 2}, {213, 2}, {213, 2}, {213, 2}, {213, 2}, {213, 2}, {213, 2}, + {213, 2}, {213, 2}, {213, 2}, {213, 2}, {213, 2}, {213, 2}, {213, 2}, + {213, 2}, {213, 2}, {213, 2}, {213, 2}, {213, 2}, {213, 2}, {213, 2}, + {213, 2}, {213, 2}, {213, 2}, {213, 2}, {213, 2}, {213, 2}, {213, 2}, + {213, 2}, {213, 2}, {213, 2}, {213, 2}, +}; + +static const HuffmanIncomingTable HuffmanIncoming_255_255_249 = { + HuffmanIncomingEntries_255_255_249, nullptr, 256, 2}; + +static const HuffmanIncomingEntry HuffmanIncomingEntries_255_255_250[] = { + {218, 2}, {218, 2}, {218, 2}, {218, 2}, {218, 2}, {218, 2}, {218, 2}, + {218, 2}, {218, 2}, {218, 2}, {218, 2}, {218, 2}, {218, 2}, {218, 2}, + {218, 2}, {218, 2}, {218, 2}, {218, 2}, {218, 2}, {218, 2}, {218, 2}, + {218, 2}, {218, 2}, {218, 2}, {218, 2}, {218, 2}, {218, 2}, {218, 2}, + {218, 2}, {218, 2}, {218, 2}, {218, 2}, {218, 2}, {218, 2}, {218, 2}, + {218, 2}, {218, 2}, {218, 2}, {218, 2}, {218, 2}, {218, 2}, {218, 2}, + {218, 2}, {218, 2}, {218, 2}, {218, 2}, {218, 2}, {218, 2}, {218, 2}, + {218, 2}, {218, 2}, {218, 2}, {218, 2}, {218, 2}, {218, 2}, {218, 2}, + {218, 2}, {218, 2}, {218, 2}, {218, 2}, {218, 2}, {218, 2}, {218, 2}, + {218, 2}, {219, 2}, {219, 2}, {219, 2}, {219, 2}, {219, 2}, {219, 2}, + {219, 2}, {219, 2}, {219, 2}, {219, 2}, {219, 2}, {219, 2}, {219, 2}, + {219, 2}, {219, 2}, {219, 2}, {219, 2}, {219, 2}, {219, 2}, {219, 2}, + {219, 2}, {219, 2}, {219, 2}, {219, 2}, {219, 2}, {219, 2}, {219, 2}, + {219, 2}, {219, 2}, {219, 2}, {219, 2}, {219, 2}, {219, 2}, {219, 2}, + {219, 2}, {219, 2}, {219, 2}, {219, 2}, {219, 2}, {219, 2}, {219, 2}, + {219, 2}, {219, 2}, {219, 2}, {219, 2}, {219, 2}, {219, 2}, {219, 2}, + {219, 2}, {219, 2}, {219, 2}, {219, 2}, {219, 2}, {219, 2}, {219, 2}, + {219, 2}, {219, 2}, {219, 2}, {219, 2}, {219, 2}, {219, 2}, {219, 2}, + {219, 2}, {219, 2}, {238, 2}, {238, 2}, {238, 2}, {238, 2}, {238, 2}, + {238, 2}, {238, 2}, {238, 2}, {238, 2}, {238, 2}, {238, 2}, {238, 2}, + {238, 2}, {238, 2}, {238, 2}, {238, 2}, {238, 2}, {238, 2}, {238, 2}, + {238, 2}, {238, 2}, {238, 2}, {238, 2}, {238, 2}, {238, 2}, {238, 2}, + {238, 2}, {238, 2}, {238, 2}, {238, 2}, {238, 2}, {238, 2}, {238, 2}, + {238, 2}, {238, 2}, {238, 2}, {238, 2}, {238, 2}, {238, 2}, {238, 2}, + {238, 2}, {238, 2}, {238, 2}, {238, 2}, {238, 2}, {238, 2}, {238, 2}, + {238, 2}, {238, 2}, {238, 2}, {238, 2}, {238, 2}, {238, 2}, {238, 2}, + {238, 2}, {238, 2}, {238, 2}, {238, 2}, {238, 2}, {238, 2}, {238, 2}, + {238, 2}, {238, 2}, {238, 2}, {240, 2}, {240, 2}, {240, 2}, {240, 2}, + {240, 2}, {240, 2}, {240, 2}, {240, 2}, {240, 2}, {240, 2}, {240, 2}, + {240, 2}, {240, 2}, {240, 2}, {240, 2}, {240, 2}, {240, 2}, {240, 2}, + {240, 2}, {240, 2}, {240, 2}, {240, 2}, {240, 2}, {240, 2}, {240, 2}, + {240, 2}, {240, 2}, {240, 2}, {240, 2}, {240, 2}, {240, 2}, {240, 2}, + {240, 2}, {240, 2}, {240, 2}, {240, 2}, {240, 2}, {240, 2}, {240, 2}, + {240, 2}, {240, 2}, {240, 2}, {240, 2}, {240, 2}, {240, 2}, {240, 2}, + {240, 2}, {240, 2}, {240, 2}, {240, 2}, {240, 2}, {240, 2}, {240, 2}, + {240, 2}, {240, 2}, {240, 2}, {240, 2}, {240, 2}, {240, 2}, {240, 2}, + {240, 2}, {240, 2}, {240, 2}, {240, 2}, +}; + +static const HuffmanIncomingTable HuffmanIncoming_255_255_250 = { + HuffmanIncomingEntries_255_255_250, nullptr, 256, 2}; + +static const HuffmanIncomingEntry HuffmanIncomingEntries_255_255_251[] = { + {242, 2}, {242, 2}, {242, 2}, {242, 2}, {242, 2}, {242, 2}, {242, 2}, + {242, 2}, {242, 2}, {242, 2}, {242, 2}, {242, 2}, {242, 2}, {242, 2}, + {242, 2}, {242, 2}, {242, 2}, {242, 2}, {242, 2}, {242, 2}, {242, 2}, + {242, 2}, {242, 2}, {242, 2}, {242, 2}, {242, 2}, {242, 2}, {242, 2}, + {242, 2}, {242, 2}, {242, 2}, {242, 2}, {242, 2}, {242, 2}, {242, 2}, + {242, 2}, {242, 2}, {242, 2}, {242, 2}, {242, 2}, {242, 2}, {242, 2}, + {242, 2}, {242, 2}, {242, 2}, {242, 2}, {242, 2}, {242, 2}, {242, 2}, + {242, 2}, {242, 2}, {242, 2}, {242, 2}, {242, 2}, {242, 2}, {242, 2}, + {242, 2}, {242, 2}, {242, 2}, {242, 2}, {242, 2}, {242, 2}, {242, 2}, + {242, 2}, {243, 2}, {243, 2}, {243, 2}, {243, 2}, {243, 2}, {243, 2}, + {243, 2}, {243, 2}, {243, 2}, {243, 2}, {243, 2}, {243, 2}, {243, 2}, + {243, 2}, {243, 2}, {243, 2}, {243, 2}, {243, 2}, {243, 2}, {243, 2}, + {243, 2}, {243, 2}, {243, 2}, {243, 2}, {243, 2}, {243, 2}, {243, 2}, + {243, 2}, {243, 2}, {243, 2}, {243, 2}, {243, 2}, {243, 2}, {243, 2}, + {243, 2}, {243, 2}, {243, 2}, {243, 2}, {243, 2}, {243, 2}, {243, 2}, + {243, 2}, {243, 2}, {243, 2}, {243, 2}, {243, 2}, {243, 2}, {243, 2}, + {243, 2}, {243, 2}, {243, 2}, {243, 2}, {243, 2}, {243, 2}, {243, 2}, + {243, 2}, {243, 2}, {243, 2}, {243, 2}, {243, 2}, {243, 2}, {243, 2}, + {243, 2}, {243, 2}, {255, 2}, {255, 2}, {255, 2}, {255, 2}, {255, 2}, + {255, 2}, {255, 2}, {255, 2}, {255, 2}, {255, 2}, {255, 2}, {255, 2}, + {255, 2}, {255, 2}, {255, 2}, {255, 2}, {255, 2}, {255, 2}, {255, 2}, + {255, 2}, {255, 2}, {255, 2}, {255, 2}, {255, 2}, {255, 2}, {255, 2}, + {255, 2}, {255, 2}, {255, 2}, {255, 2}, {255, 2}, {255, 2}, {255, 2}, + {255, 2}, {255, 2}, {255, 2}, {255, 2}, {255, 2}, {255, 2}, {255, 2}, + {255, 2}, {255, 2}, {255, 2}, {255, 2}, {255, 2}, {255, 2}, {255, 2}, + {255, 2}, {255, 2}, {255, 2}, {255, 2}, {255, 2}, {255, 2}, {255, 2}, + {255, 2}, {255, 2}, {255, 2}, {255, 2}, {255, 2}, {255, 2}, {255, 2}, + {255, 2}, {255, 2}, {255, 2}, {203, 3}, {203, 3}, {203, 3}, {203, 3}, + {203, 3}, {203, 3}, {203, 3}, {203, 3}, {203, 3}, {203, 3}, {203, 3}, + {203, 3}, {203, 3}, {203, 3}, {203, 3}, {203, 3}, {203, 3}, {203, 3}, + {203, 3}, {203, 3}, {203, 3}, {203, 3}, {203, 3}, {203, 3}, {203, 3}, + {203, 3}, {203, 3}, {203, 3}, {203, 3}, {203, 3}, {203, 3}, {203, 3}, + {204, 3}, {204, 3}, {204, 3}, {204, 3}, {204, 3}, {204, 3}, {204, 3}, + {204, 3}, {204, 3}, {204, 3}, {204, 3}, {204, 3}, {204, 3}, {204, 3}, + {204, 3}, {204, 3}, {204, 3}, {204, 3}, {204, 3}, {204, 3}, {204, 3}, + {204, 3}, {204, 3}, {204, 3}, {204, 3}, {204, 3}, {204, 3}, {204, 3}, + {204, 3}, {204, 3}, {204, 3}, {204, 3}, +}; + +static const HuffmanIncomingTable HuffmanIncoming_255_255_251 = { + HuffmanIncomingEntries_255_255_251, nullptr, 256, 3}; + +static const HuffmanIncomingEntry HuffmanIncomingEntries_255_255_252[] = { + {211, 3}, {211, 3}, {211, 3}, {211, 3}, {211, 3}, {211, 3}, {211, 3}, + {211, 3}, {211, 3}, {211, 3}, {211, 3}, {211, 3}, {211, 3}, {211, 3}, + {211, 3}, {211, 3}, {211, 3}, {211, 3}, {211, 3}, {211, 3}, {211, 3}, + {211, 3}, {211, 3}, {211, 3}, {211, 3}, {211, 3}, {211, 3}, {211, 3}, + {211, 3}, {211, 3}, {211, 3}, {211, 3}, {212, 3}, {212, 3}, {212, 3}, + {212, 3}, {212, 3}, {212, 3}, {212, 3}, {212, 3}, {212, 3}, {212, 3}, + {212, 3}, {212, 3}, {212, 3}, {212, 3}, {212, 3}, {212, 3}, {212, 3}, + {212, 3}, {212, 3}, {212, 3}, {212, 3}, {212, 3}, {212, 3}, {212, 3}, + {212, 3}, {212, 3}, {212, 3}, {212, 3}, {212, 3}, {212, 3}, {212, 3}, + {212, 3}, {214, 3}, {214, 3}, {214, 3}, {214, 3}, {214, 3}, {214, 3}, + {214, 3}, {214, 3}, {214, 3}, {214, 3}, {214, 3}, {214, 3}, {214, 3}, + {214, 3}, {214, 3}, {214, 3}, {214, 3}, {214, 3}, {214, 3}, {214, 3}, + {214, 3}, {214, 3}, {214, 3}, {214, 3}, {214, 3}, {214, 3}, {214, 3}, + {214, 3}, {214, 3}, {214, 3}, {214, 3}, {214, 3}, {221, 3}, {221, 3}, + {221, 3}, {221, 3}, {221, 3}, {221, 3}, {221, 3}, {221, 3}, {221, 3}, + {221, 3}, {221, 3}, {221, 3}, {221, 3}, {221, 3}, {221, 3}, {221, 3}, + {221, 3}, {221, 3}, {221, 3}, {221, 3}, {221, 3}, {221, 3}, {221, 3}, + {221, 3}, {221, 3}, {221, 3}, {221, 3}, {221, 3}, {221, 3}, {221, 3}, + {221, 3}, {221, 3}, {222, 3}, {222, 3}, {222, 3}, {222, 3}, {222, 3}, + {222, 3}, {222, 3}, {222, 3}, {222, 3}, {222, 3}, {222, 3}, {222, 3}, + {222, 3}, {222, 3}, {222, 3}, {222, 3}, {222, 3}, {222, 3}, {222, 3}, + {222, 3}, {222, 3}, {222, 3}, {222, 3}, {222, 3}, {222, 3}, {222, 3}, + {222, 3}, {222, 3}, {222, 3}, {222, 3}, {222, 3}, {222, 3}, {223, 3}, + {223, 3}, {223, 3}, {223, 3}, {223, 3}, {223, 3}, {223, 3}, {223, 3}, + {223, 3}, {223, 3}, {223, 3}, {223, 3}, {223, 3}, {223, 3}, {223, 3}, + {223, 3}, {223, 3}, {223, 3}, {223, 3}, {223, 3}, {223, 3}, {223, 3}, + {223, 3}, {223, 3}, {223, 3}, {223, 3}, {223, 3}, {223, 3}, {223, 3}, + {223, 3}, {223, 3}, {223, 3}, {241, 3}, {241, 3}, {241, 3}, {241, 3}, + {241, 3}, {241, 3}, {241, 3}, {241, 3}, {241, 3}, {241, 3}, {241, 3}, + {241, 3}, {241, 3}, {241, 3}, {241, 3}, {241, 3}, {241, 3}, {241, 3}, + {241, 3}, {241, 3}, {241, 3}, {241, 3}, {241, 3}, {241, 3}, {241, 3}, + {241, 3}, {241, 3}, {241, 3}, {241, 3}, {241, 3}, {241, 3}, {241, 3}, + {244, 3}, {244, 3}, {244, 3}, {244, 3}, {244, 3}, {244, 3}, {244, 3}, + {244, 3}, {244, 3}, {244, 3}, {244, 3}, {244, 3}, {244, 3}, {244, 3}, + {244, 3}, {244, 3}, {244, 3}, {244, 3}, {244, 3}, {244, 3}, {244, 3}, + {244, 3}, {244, 3}, {244, 3}, {244, 3}, {244, 3}, {244, 3}, {244, 3}, + {244, 3}, {244, 3}, {244, 3}, {244, 3}, +}; + +static const HuffmanIncomingTable HuffmanIncoming_255_255_252 = { + HuffmanIncomingEntries_255_255_252, nullptr, 256, 3}; + +static const HuffmanIncomingEntry HuffmanIncomingEntries_255_255_253[] = { + {245, 3}, {245, 3}, {245, 3}, {245, 3}, {245, 3}, {245, 3}, {245, 3}, + {245, 3}, {245, 3}, {245, 3}, {245, 3}, {245, 3}, {245, 3}, {245, 3}, + {245, 3}, {245, 3}, {245, 3}, {245, 3}, {245, 3}, {245, 3}, {245, 3}, + {245, 3}, {245, 3}, {245, 3}, {245, 3}, {245, 3}, {245, 3}, {245, 3}, + {245, 3}, {245, 3}, {245, 3}, {245, 3}, {246, 3}, {246, 3}, {246, 3}, + {246, 3}, {246, 3}, {246, 3}, {246, 3}, {246, 3}, {246, 3}, {246, 3}, + {246, 3}, {246, 3}, {246, 3}, {246, 3}, {246, 3}, {246, 3}, {246, 3}, + {246, 3}, {246, 3}, {246, 3}, {246, 3}, {246, 3}, {246, 3}, {246, 3}, + {246, 3}, {246, 3}, {246, 3}, {246, 3}, {246, 3}, {246, 3}, {246, 3}, + {246, 3}, {247, 3}, {247, 3}, {247, 3}, {247, 3}, {247, 3}, {247, 3}, + {247, 3}, {247, 3}, {247, 3}, {247, 3}, {247, 3}, {247, 3}, {247, 3}, + {247, 3}, {247, 3}, {247, 3}, {247, 3}, {247, 3}, {247, 3}, {247, 3}, + {247, 3}, {247, 3}, {247, 3}, {247, 3}, {247, 3}, {247, 3}, {247, 3}, + {247, 3}, {247, 3}, {247, 3}, {247, 3}, {247, 3}, {248, 3}, {248, 3}, + {248, 3}, {248, 3}, {248, 3}, {248, 3}, {248, 3}, {248, 3}, {248, 3}, + {248, 3}, {248, 3}, {248, 3}, {248, 3}, {248, 3}, {248, 3}, {248, 3}, + {248, 3}, {248, 3}, {248, 3}, {248, 3}, {248, 3}, {248, 3}, {248, 3}, + {248, 3}, {248, 3}, {248, 3}, {248, 3}, {248, 3}, {248, 3}, {248, 3}, + {248, 3}, {248, 3}, {250, 3}, {250, 3}, {250, 3}, {250, 3}, {250, 3}, + {250, 3}, {250, 3}, {250, 3}, {250, 3}, {250, 3}, {250, 3}, {250, 3}, + {250, 3}, {250, 3}, {250, 3}, {250, 3}, {250, 3}, {250, 3}, {250, 3}, + {250, 3}, {250, 3}, {250, 3}, {250, 3}, {250, 3}, {250, 3}, {250, 3}, + {250, 3}, {250, 3}, {250, 3}, {250, 3}, {250, 3}, {250, 3}, {251, 3}, + {251, 3}, {251, 3}, {251, 3}, {251, 3}, {251, 3}, {251, 3}, {251, 3}, + {251, 3}, {251, 3}, {251, 3}, {251, 3}, {251, 3}, {251, 3}, {251, 3}, + {251, 3}, {251, 3}, {251, 3}, {251, 3}, {251, 3}, {251, 3}, {251, 3}, + {251, 3}, {251, 3}, {251, 3}, {251, 3}, {251, 3}, {251, 3}, {251, 3}, + {251, 3}, {251, 3}, {251, 3}, {252, 3}, {252, 3}, {252, 3}, {252, 3}, + {252, 3}, {252, 3}, {252, 3}, {252, 3}, {252, 3}, {252, 3}, {252, 3}, + {252, 3}, {252, 3}, {252, 3}, {252, 3}, {252, 3}, {252, 3}, {252, 3}, + {252, 3}, {252, 3}, {252, 3}, {252, 3}, {252, 3}, {252, 3}, {252, 3}, + {252, 3}, {252, 3}, {252, 3}, {252, 3}, {252, 3}, {252, 3}, {252, 3}, + {253, 3}, {253, 3}, {253, 3}, {253, 3}, {253, 3}, {253, 3}, {253, 3}, + {253, 3}, {253, 3}, {253, 3}, {253, 3}, {253, 3}, {253, 3}, {253, 3}, + {253, 3}, {253, 3}, {253, 3}, {253, 3}, {253, 3}, {253, 3}, {253, 3}, + {253, 3}, {253, 3}, {253, 3}, {253, 3}, {253, 3}, {253, 3}, {253, 3}, + {253, 3}, {253, 3}, {253, 3}, {253, 3}, +}; + +static const HuffmanIncomingTable HuffmanIncoming_255_255_253 = { + HuffmanIncomingEntries_255_255_253, nullptr, 256, 3}; + +static const HuffmanIncomingEntry HuffmanIncomingEntries_255_255_254[] = { + {254, 3}, {254, 3}, {254, 3}, {254, 3}, {254, 3}, {254, 3}, {254, 3}, + {254, 3}, {254, 3}, {254, 3}, {254, 3}, {254, 3}, {254, 3}, {254, 3}, + {254, 3}, {254, 3}, {254, 3}, {254, 3}, {254, 3}, {254, 3}, {254, 3}, + {254, 3}, {254, 3}, {254, 3}, {254, 3}, {254, 3}, {254, 3}, {254, 3}, + {254, 3}, {254, 3}, {254, 3}, {254, 3}, {2, 4}, {2, 4}, {2, 4}, + {2, 4}, {2, 4}, {2, 4}, {2, 4}, {2, 4}, {2, 4}, {2, 4}, + {2, 4}, {2, 4}, {2, 4}, {2, 4}, {2, 4}, {2, 4}, {3, 4}, + {3, 4}, {3, 4}, {3, 4}, {3, 4}, {3, 4}, {3, 4}, {3, 4}, + {3, 4}, {3, 4}, {3, 4}, {3, 4}, {3, 4}, {3, 4}, {3, 4}, + {3, 4}, {4, 4}, {4, 4}, {4, 4}, {4, 4}, {4, 4}, {4, 4}, + {4, 4}, {4, 4}, {4, 4}, {4, 4}, {4, 4}, {4, 4}, {4, 4}, + {4, 4}, {4, 4}, {4, 4}, {5, 4}, {5, 4}, {5, 4}, {5, 4}, + {5, 4}, {5, 4}, {5, 4}, {5, 4}, {5, 4}, {5, 4}, {5, 4}, + {5, 4}, {5, 4}, {5, 4}, {5, 4}, {5, 4}, {6, 4}, {6, 4}, + {6, 4}, {6, 4}, {6, 4}, {6, 4}, {6, 4}, {6, 4}, {6, 4}, + {6, 4}, {6, 4}, {6, 4}, {6, 4}, {6, 4}, {6, 4}, {6, 4}, + {7, 4}, {7, 4}, {7, 4}, {7, 4}, {7, 4}, {7, 4}, {7, 4}, + {7, 4}, {7, 4}, {7, 4}, {7, 4}, {7, 4}, {7, 4}, {7, 4}, + {7, 4}, {7, 4}, {8, 4}, {8, 4}, {8, 4}, {8, 4}, {8, 4}, + {8, 4}, {8, 4}, {8, 4}, {8, 4}, {8, 4}, {8, 4}, {8, 4}, + {8, 4}, {8, 4}, {8, 4}, {8, 4}, {11, 4}, {11, 4}, {11, 4}, + {11, 4}, {11, 4}, {11, 4}, {11, 4}, {11, 4}, {11, 4}, {11, 4}, + {11, 4}, {11, 4}, {11, 4}, {11, 4}, {11, 4}, {11, 4}, {12, 4}, + {12, 4}, {12, 4}, {12, 4}, {12, 4}, {12, 4}, {12, 4}, {12, 4}, + {12, 4}, {12, 4}, {12, 4}, {12, 4}, {12, 4}, {12, 4}, {12, 4}, + {12, 4}, {14, 4}, {14, 4}, {14, 4}, {14, 4}, {14, 4}, {14, 4}, + {14, 4}, {14, 4}, {14, 4}, {14, 4}, {14, 4}, {14, 4}, {14, 4}, + {14, 4}, {14, 4}, {14, 4}, {15, 4}, {15, 4}, {15, 4}, {15, 4}, + {15, 4}, {15, 4}, {15, 4}, {15, 4}, {15, 4}, {15, 4}, {15, 4}, + {15, 4}, {15, 4}, {15, 4}, {15, 4}, {15, 4}, {16, 4}, {16, 4}, + {16, 4}, {16, 4}, {16, 4}, {16, 4}, {16, 4}, {16, 4}, {16, 4}, + {16, 4}, {16, 4}, {16, 4}, {16, 4}, {16, 4}, {16, 4}, {16, 4}, + {17, 4}, {17, 4}, {17, 4}, {17, 4}, {17, 4}, {17, 4}, {17, 4}, + {17, 4}, {17, 4}, {17, 4}, {17, 4}, {17, 4}, {17, 4}, {17, 4}, + {17, 4}, {17, 4}, {18, 4}, {18, 4}, {18, 4}, {18, 4}, {18, 4}, + {18, 4}, {18, 4}, {18, 4}, {18, 4}, {18, 4}, {18, 4}, {18, 4}, + {18, 4}, {18, 4}, {18, 4}, {18, 4}, +}; + +static const HuffmanIncomingTable HuffmanIncoming_255_255_254 = { + HuffmanIncomingEntries_255_255_254, nullptr, 256, 4}; + +static const HuffmanIncomingEntry HuffmanIncomingEntries_255_255_255[] = { + {19, 4}, {19, 4}, {19, 4}, {19, 4}, {19, 4}, {19, 4}, {19, 4}, + {19, 4}, {19, 4}, {19, 4}, {19, 4}, {19, 4}, {19, 4}, {19, 4}, + {19, 4}, {19, 4}, {20, 4}, {20, 4}, {20, 4}, {20, 4}, {20, 4}, + {20, 4}, {20, 4}, {20, 4}, {20, 4}, {20, 4}, {20, 4}, {20, 4}, + {20, 4}, {20, 4}, {20, 4}, {20, 4}, {21, 4}, {21, 4}, {21, 4}, + {21, 4}, {21, 4}, {21, 4}, {21, 4}, {21, 4}, {21, 4}, {21, 4}, + {21, 4}, {21, 4}, {21, 4}, {21, 4}, {21, 4}, {21, 4}, {23, 4}, + {23, 4}, {23, 4}, {23, 4}, {23, 4}, {23, 4}, {23, 4}, {23, 4}, + {23, 4}, {23, 4}, {23, 4}, {23, 4}, {23, 4}, {23, 4}, {23, 4}, + {23, 4}, {24, 4}, {24, 4}, {24, 4}, {24, 4}, {24, 4}, {24, 4}, + {24, 4}, {24, 4}, {24, 4}, {24, 4}, {24, 4}, {24, 4}, {24, 4}, + {24, 4}, {24, 4}, {24, 4}, {25, 4}, {25, 4}, {25, 4}, {25, 4}, + {25, 4}, {25, 4}, {25, 4}, {25, 4}, {25, 4}, {25, 4}, {25, 4}, + {25, 4}, {25, 4}, {25, 4}, {25, 4}, {25, 4}, {26, 4}, {26, 4}, + {26, 4}, {26, 4}, {26, 4}, {26, 4}, {26, 4}, {26, 4}, {26, 4}, + {26, 4}, {26, 4}, {26, 4}, {26, 4}, {26, 4}, {26, 4}, {26, 4}, + {27, 4}, {27, 4}, {27, 4}, {27, 4}, {27, 4}, {27, 4}, {27, 4}, + {27, 4}, {27, 4}, {27, 4}, {27, 4}, {27, 4}, {27, 4}, {27, 4}, + {27, 4}, {27, 4}, {28, 4}, {28, 4}, {28, 4}, {28, 4}, {28, 4}, + {28, 4}, {28, 4}, {28, 4}, {28, 4}, {28, 4}, {28, 4}, {28, 4}, + {28, 4}, {28, 4}, {28, 4}, {28, 4}, {29, 4}, {29, 4}, {29, 4}, + {29, 4}, {29, 4}, {29, 4}, {29, 4}, {29, 4}, {29, 4}, {29, 4}, + {29, 4}, {29, 4}, {29, 4}, {29, 4}, {29, 4}, {29, 4}, {30, 4}, + {30, 4}, {30, 4}, {30, 4}, {30, 4}, {30, 4}, {30, 4}, {30, 4}, + {30, 4}, {30, 4}, {30, 4}, {30, 4}, {30, 4}, {30, 4}, {30, 4}, + {30, 4}, {31, 4}, {31, 4}, {31, 4}, {31, 4}, {31, 4}, {31, 4}, + {31, 4}, {31, 4}, {31, 4}, {31, 4}, {31, 4}, {31, 4}, {31, 4}, + {31, 4}, {31, 4}, {31, 4}, {127, 4}, {127, 4}, {127, 4}, {127, 4}, + {127, 4}, {127, 4}, {127, 4}, {127, 4}, {127, 4}, {127, 4}, {127, 4}, + {127, 4}, {127, 4}, {127, 4}, {127, 4}, {127, 4}, {220, 4}, {220, 4}, + {220, 4}, {220, 4}, {220, 4}, {220, 4}, {220, 4}, {220, 4}, {220, 4}, + {220, 4}, {220, 4}, {220, 4}, {220, 4}, {220, 4}, {220, 4}, {220, 4}, + {249, 4}, {249, 4}, {249, 4}, {249, 4}, {249, 4}, {249, 4}, {249, 4}, + {249, 4}, {249, 4}, {249, 4}, {249, 4}, {249, 4}, {249, 4}, {249, 4}, + {249, 4}, {249, 4}, {10, 6}, {10, 6}, {10, 6}, {10, 6}, {13, 6}, + {13, 6}, {13, 6}, {13, 6}, {22, 6}, {22, 6}, {22, 6}, {22, 6}, + {256, 6}, {256, 6}, {256, 6}, {256, 6}, +}; + +static const HuffmanIncomingTable HuffmanIncoming_255_255_255 = { + HuffmanIncomingEntries_255_255_255, nullptr, 256, 6}; + +static const HuffmanIncomingEntry HuffmanIncomingEntries_255_255[] = { + {176, 5}, {176, 5}, {176, 5}, {176, 5}, {176, 5}, {176, 5}, {176, 5}, + {176, 5}, {177, 5}, {177, 5}, {177, 5}, {177, 5}, {177, 5}, {177, 5}, + {177, 5}, {177, 5}, {179, 5}, {179, 5}, {179, 5}, {179, 5}, {179, 5}, + {179, 5}, {179, 5}, {179, 5}, {209, 5}, {209, 5}, {209, 5}, {209, 5}, + {209, 5}, {209, 5}, {209, 5}, {209, 5}, {216, 5}, {216, 5}, {216, 5}, + {216, 5}, {216, 5}, {216, 5}, {216, 5}, {216, 5}, {217, 5}, {217, 5}, + {217, 5}, {217, 5}, {217, 5}, {217, 5}, {217, 5}, {217, 5}, {227, 5}, + {227, 5}, {227, 5}, {227, 5}, {227, 5}, {227, 5}, {227, 5}, {227, 5}, + {229, 5}, {229, 5}, {229, 5}, {229, 5}, {229, 5}, {229, 5}, {229, 5}, + {229, 5}, {230, 5}, {230, 5}, {230, 5}, {230, 5}, {230, 5}, {230, 5}, + {230, 5}, {230, 5}, {129, 6}, {129, 6}, {129, 6}, {129, 6}, {132, 6}, + {132, 6}, {132, 6}, {132, 6}, {133, 6}, {133, 6}, {133, 6}, {133, 6}, + {134, 6}, {134, 6}, {134, 6}, {134, 6}, {136, 6}, {136, 6}, {136, 6}, + {136, 6}, {146, 6}, {146, 6}, {146, 6}, {146, 6}, {154, 6}, {154, 6}, + {154, 6}, {154, 6}, {156, 6}, {156, 6}, {156, 6}, {156, 6}, {160, 6}, + {160, 6}, {160, 6}, {160, 6}, {163, 6}, {163, 6}, {163, 6}, {163, 6}, + {164, 6}, {164, 6}, {164, 6}, {164, 6}, {169, 6}, {169, 6}, {169, 6}, + {169, 6}, {170, 6}, {170, 6}, {170, 6}, {170, 6}, {173, 6}, {173, 6}, + {173, 6}, {173, 6}, {178, 6}, {178, 6}, {178, 6}, {178, 6}, {181, 6}, + {181, 6}, {181, 6}, {181, 6}, {185, 6}, {185, 6}, {185, 6}, {185, 6}, + {186, 6}, {186, 6}, {186, 6}, {186, 6}, {187, 6}, {187, 6}, {187, 6}, + {187, 6}, {189, 6}, {189, 6}, {189, 6}, {189, 6}, {190, 6}, {190, 6}, + {190, 6}, {190, 6}, {196, 6}, {196, 6}, {196, 6}, {196, 6}, {198, 6}, + {198, 6}, {198, 6}, {198, 6}, {228, 6}, {228, 6}, {228, 6}, {228, 6}, + {232, 6}, {232, 6}, {232, 6}, {232, 6}, {233, 6}, {233, 6}, {233, 6}, + {233, 6}, {1, 7}, {1, 7}, {135, 7}, {135, 7}, {137, 7}, {137, 7}, + {138, 7}, {138, 7}, {139, 7}, {139, 7}, {140, 7}, {140, 7}, {141, 7}, + {141, 7}, {143, 7}, {143, 7}, {147, 7}, {147, 7}, {149, 7}, {149, 7}, + {150, 7}, {150, 7}, {151, 7}, {151, 7}, {152, 7}, {152, 7}, {155, 7}, + {155, 7}, {157, 7}, {157, 7}, {158, 7}, {158, 7}, {165, 7}, {165, 7}, + {166, 7}, {166, 7}, {168, 7}, {168, 7}, {174, 7}, {174, 7}, {175, 7}, + {175, 7}, {180, 7}, {180, 7}, {182, 7}, {182, 7}, {183, 7}, {183, 7}, + {188, 7}, {188, 7}, {191, 7}, {191, 7}, {197, 7}, {197, 7}, {231, 7}, + {231, 7}, {239, 7}, {239, 7}, {9, 8}, {142, 8}, {144, 8}, {145, 8}, + {148, 8}, {159, 8}, {171, 8}, {206, 8}, {215, 8}, {225, 8}, {236, 8}, + {237, 8}, +}; + +static const HuffmanIncomingTable* HuffmanIncomingNextTables_255_255[] = { + &HuffmanIncoming_255_255_246, &HuffmanIncoming_255_255_247, + &HuffmanIncoming_255_255_248, &HuffmanIncoming_255_255_249, + &HuffmanIncoming_255_255_250, &HuffmanIncoming_255_255_251, + &HuffmanIncoming_255_255_252, &HuffmanIncoming_255_255_253, + &HuffmanIncoming_255_255_254, &HuffmanIncoming_255_255_255, +}; + +static const HuffmanIncomingTable HuffmanIncoming_255_255 = { + HuffmanIncomingEntries_255_255, HuffmanIncomingNextTables_255_255, 246, 8}; + +static const HuffmanIncomingEntry HuffmanIncomingEntries_255[] = { + {63, 2}, {63, 2}, {63, 2}, {63, 2}, {63, 2}, {63, 2}, {63, 2}, + {63, 2}, {63, 2}, {63, 2}, {63, 2}, {63, 2}, {63, 2}, {63, 2}, + {63, 2}, {63, 2}, {63, 2}, {63, 2}, {63, 2}, {63, 2}, {63, 2}, + {63, 2}, {63, 2}, {63, 2}, {63, 2}, {63, 2}, {63, 2}, {63, 2}, + {63, 2}, {63, 2}, {63, 2}, {63, 2}, {63, 2}, {63, 2}, {63, 2}, + {63, 2}, {63, 2}, {63, 2}, {63, 2}, {63, 2}, {63, 2}, {63, 2}, + {63, 2}, {63, 2}, {63, 2}, {63, 2}, {63, 2}, {63, 2}, {63, 2}, + {63, 2}, {63, 2}, {63, 2}, {63, 2}, {63, 2}, {63, 2}, {63, 2}, + {63, 2}, {63, 2}, {63, 2}, {63, 2}, {63, 2}, {63, 2}, {63, 2}, + {63, 2}, {39, 3}, {39, 3}, {39, 3}, {39, 3}, {39, 3}, {39, 3}, + {39, 3}, {39, 3}, {39, 3}, {39, 3}, {39, 3}, {39, 3}, {39, 3}, + {39, 3}, {39, 3}, {39, 3}, {39, 3}, {39, 3}, {39, 3}, {39, 3}, + {39, 3}, {39, 3}, {39, 3}, {39, 3}, {39, 3}, {39, 3}, {39, 3}, + {39, 3}, {39, 3}, {39, 3}, {39, 3}, {39, 3}, {43, 3}, {43, 3}, + {43, 3}, {43, 3}, {43, 3}, {43, 3}, {43, 3}, {43, 3}, {43, 3}, + {43, 3}, {43, 3}, {43, 3}, {43, 3}, {43, 3}, {43, 3}, {43, 3}, + {43, 3}, {43, 3}, {43, 3}, {43, 3}, {43, 3}, {43, 3}, {43, 3}, + {43, 3}, {43, 3}, {43, 3}, {43, 3}, {43, 3}, {43, 3}, {43, 3}, + {43, 3}, {43, 3}, {124, 3}, {124, 3}, {124, 3}, {124, 3}, {124, 3}, + {124, 3}, {124, 3}, {124, 3}, {124, 3}, {124, 3}, {124, 3}, {124, 3}, + {124, 3}, {124, 3}, {124, 3}, {124, 3}, {124, 3}, {124, 3}, {124, 3}, + {124, 3}, {124, 3}, {124, 3}, {124, 3}, {124, 3}, {124, 3}, {124, 3}, + {124, 3}, {124, 3}, {124, 3}, {124, 3}, {124, 3}, {124, 3}, {35, 4}, + {35, 4}, {35, 4}, {35, 4}, {35, 4}, {35, 4}, {35, 4}, {35, 4}, + {35, 4}, {35, 4}, {35, 4}, {35, 4}, {35, 4}, {35, 4}, {35, 4}, + {35, 4}, {62, 4}, {62, 4}, {62, 4}, {62, 4}, {62, 4}, {62, 4}, + {62, 4}, {62, 4}, {62, 4}, {62, 4}, {62, 4}, {62, 4}, {62, 4}, + {62, 4}, {62, 4}, {62, 4}, {0, 5}, {0, 5}, {0, 5}, {0, 5}, + {0, 5}, {0, 5}, {0, 5}, {0, 5}, {36, 5}, {36, 5}, {36, 5}, + {36, 5}, {36, 5}, {36, 5}, {36, 5}, {36, 5}, {64, 5}, {64, 5}, + {64, 5}, {64, 5}, {64, 5}, {64, 5}, {64, 5}, {64, 5}, {91, 5}, + {91, 5}, {91, 5}, {91, 5}, {91, 5}, {91, 5}, {91, 5}, {91, 5}, + {93, 5}, {93, 5}, {93, 5}, {93, 5}, {93, 5}, {93, 5}, {93, 5}, + {93, 5}, {126, 5}, {126, 5}, {126, 5}, {126, 5}, {126, 5}, {126, 5}, + {126, 5}, {126, 5}, {94, 6}, {94, 6}, {94, 6}, {94, 6}, {125, 6}, + {125, 6}, {125, 6}, {125, 6}, {60, 7}, {60, 7}, {96, 7}, {96, 7}, + {123, 7}, {123, 7}, +}; + +static const HuffmanIncomingTable* HuffmanIncomingNextTables_255[] = { + &HuffmanIncoming_255_254, + &HuffmanIncoming_255_255, +}; + +static const HuffmanIncomingTable HuffmanIncoming_255 = { + HuffmanIncomingEntries_255, HuffmanIncomingNextTables_255, 254, 7}; + +static const HuffmanIncomingEntry HuffmanIncomingEntriesRoot[] = { + {48, 5}, {48, 5}, {48, 5}, {48, 5}, {48, 5}, {48, 5}, {48, 5}, + {48, 5}, {49, 5}, {49, 5}, {49, 5}, {49, 5}, {49, 5}, {49, 5}, + {49, 5}, {49, 5}, {50, 5}, {50, 5}, {50, 5}, {50, 5}, {50, 5}, + {50, 5}, {50, 5}, {50, 5}, {97, 5}, {97, 5}, {97, 5}, {97, 5}, + {97, 5}, {97, 5}, {97, 5}, {97, 5}, {99, 5}, {99, 5}, {99, 5}, + {99, 5}, {99, 5}, {99, 5}, {99, 5}, {99, 5}, {101, 5}, {101, 5}, + {101, 5}, {101, 5}, {101, 5}, {101, 5}, {101, 5}, {101, 5}, {105, 5}, + {105, 5}, {105, 5}, {105, 5}, {105, 5}, {105, 5}, {105, 5}, {105, 5}, + {111, 5}, {111, 5}, {111, 5}, {111, 5}, {111, 5}, {111, 5}, {111, 5}, + {111, 5}, {115, 5}, {115, 5}, {115, 5}, {115, 5}, {115, 5}, {115, 5}, + {115, 5}, {115, 5}, {116, 5}, {116, 5}, {116, 5}, {116, 5}, {116, 5}, + {116, 5}, {116, 5}, {116, 5}, {32, 6}, {32, 6}, {32, 6}, {32, 6}, + {37, 6}, {37, 6}, {37, 6}, {37, 6}, {45, 6}, {45, 6}, {45, 6}, + {45, 6}, {46, 6}, {46, 6}, {46, 6}, {46, 6}, {47, 6}, {47, 6}, + {47, 6}, {47, 6}, {51, 6}, {51, 6}, {51, 6}, {51, 6}, {52, 6}, + {52, 6}, {52, 6}, {52, 6}, {53, 6}, {53, 6}, {53, 6}, {53, 6}, + {54, 6}, {54, 6}, {54, 6}, {54, 6}, {55, 6}, {55, 6}, {55, 6}, + {55, 6}, {56, 6}, {56, 6}, {56, 6}, {56, 6}, {57, 6}, {57, 6}, + {57, 6}, {57, 6}, {61, 6}, {61, 6}, {61, 6}, {61, 6}, {65, 6}, + {65, 6}, {65, 6}, {65, 6}, {95, 6}, {95, 6}, {95, 6}, {95, 6}, + {98, 6}, {98, 6}, {98, 6}, {98, 6}, {100, 6}, {100, 6}, {100, 6}, + {100, 6}, {102, 6}, {102, 6}, {102, 6}, {102, 6}, {103, 6}, {103, 6}, + {103, 6}, {103, 6}, {104, 6}, {104, 6}, {104, 6}, {104, 6}, {108, 6}, + {108, 6}, {108, 6}, {108, 6}, {109, 6}, {109, 6}, {109, 6}, {109, 6}, + {110, 6}, {110, 6}, {110, 6}, {110, 6}, {112, 6}, {112, 6}, {112, 6}, + {112, 6}, {114, 6}, {114, 6}, {114, 6}, {114, 6}, {117, 6}, {117, 6}, + {117, 6}, {117, 6}, {58, 7}, {58, 7}, {66, 7}, {66, 7}, {67, 7}, + {67, 7}, {68, 7}, {68, 7}, {69, 7}, {69, 7}, {70, 7}, {70, 7}, + {71, 7}, {71, 7}, {72, 7}, {72, 7}, {73, 7}, {73, 7}, {74, 7}, + {74, 7}, {75, 7}, {75, 7}, {76, 7}, {76, 7}, {77, 7}, {77, 7}, + {78, 7}, {78, 7}, {79, 7}, {79, 7}, {80, 7}, {80, 7}, {81, 7}, + {81, 7}, {82, 7}, {82, 7}, {83, 7}, {83, 7}, {84, 7}, {84, 7}, + {85, 7}, {85, 7}, {86, 7}, {86, 7}, {87, 7}, {87, 7}, {89, 7}, + {89, 7}, {106, 7}, {106, 7}, {107, 7}, {107, 7}, {113, 7}, {113, 7}, + {118, 7}, {118, 7}, {119, 7}, {119, 7}, {120, 7}, {120, 7}, {121, 7}, + {121, 7}, {122, 7}, {122, 7}, {38, 8}, {42, 8}, {44, 8}, {59, 8}, + {88, 8}, {90, 8}, +}; + +static const HuffmanIncomingTable* HuffmanIncomingNextTablesRoot[] = { + &HuffmanIncoming_254, + &HuffmanIncoming_255, +}; + +static const HuffmanIncomingTable HuffmanIncomingRoot = { + HuffmanIncomingEntriesRoot, HuffmanIncomingNextTablesRoot, 254, 8}; + +} // namespace net +} // namespace mozilla + +#endif // mozilla__net__Http2HuffmanIncoming_h diff --git a/netwerk/protocol/http/Http2HuffmanOutgoing.h b/netwerk/protocol/http/Http2HuffmanOutgoing.h new file mode 100644 index 0000000000..376313ea91 --- /dev/null +++ b/netwerk/protocol/http/Http2HuffmanOutgoing.h @@ -0,0 +1,85 @@ +/* + * THIS FILE IS AUTO-GENERATED. DO NOT EDIT! + */ +#ifndef mozilla__net__Http2HuffmanOutgoing_h +#define mozilla__net__Http2HuffmanOutgoing_h + +namespace mozilla { +namespace net { + +struct HuffmanOutgoingEntry { + uint32_t mValue; + uint8_t mLength; +}; + +static const HuffmanOutgoingEntry HuffmanOutgoing[] = { + {0x00001ff8, 13}, {0x007fffd8, 23}, {0x0fffffe2, 28}, {0x0fffffe3, 28}, + {0x0fffffe4, 28}, {0x0fffffe5, 28}, {0x0fffffe6, 28}, {0x0fffffe7, 28}, + {0x0fffffe8, 28}, {0x00ffffea, 24}, {0x3ffffffc, 30}, {0x0fffffe9, 28}, + {0x0fffffea, 28}, {0x3ffffffd, 30}, {0x0fffffeb, 28}, {0x0fffffec, 28}, + {0x0fffffed, 28}, {0x0fffffee, 28}, {0x0fffffef, 28}, {0x0ffffff0, 28}, + {0x0ffffff1, 28}, {0x0ffffff2, 28}, {0x3ffffffe, 30}, {0x0ffffff3, 28}, + {0x0ffffff4, 28}, {0x0ffffff5, 28}, {0x0ffffff6, 28}, {0x0ffffff7, 28}, + {0x0ffffff8, 28}, {0x0ffffff9, 28}, {0x0ffffffa, 28}, {0x0ffffffb, 28}, + {0x00000014, 6}, {0x000003f8, 10}, {0x000003f9, 10}, {0x00000ffa, 12}, + {0x00001ff9, 13}, {0x00000015, 6}, {0x000000f8, 8}, {0x000007fa, 11}, + {0x000003fa, 10}, {0x000003fb, 10}, {0x000000f9, 8}, {0x000007fb, 11}, + {0x000000fa, 8}, {0x00000016, 6}, {0x00000017, 6}, {0x00000018, 6}, + {0x00000000, 5}, {0x00000001, 5}, {0x00000002, 5}, {0x00000019, 6}, + {0x0000001a, 6}, {0x0000001b, 6}, {0x0000001c, 6}, {0x0000001d, 6}, + {0x0000001e, 6}, {0x0000001f, 6}, {0x0000005c, 7}, {0x000000fb, 8}, + {0x00007ffc, 15}, {0x00000020, 6}, {0x00000ffb, 12}, {0x000003fc, 10}, + {0x00001ffa, 13}, {0x00000021, 6}, {0x0000005d, 7}, {0x0000005e, 7}, + {0x0000005f, 7}, {0x00000060, 7}, {0x00000061, 7}, {0x00000062, 7}, + {0x00000063, 7}, {0x00000064, 7}, {0x00000065, 7}, {0x00000066, 7}, + {0x00000067, 7}, {0x00000068, 7}, {0x00000069, 7}, {0x0000006a, 7}, + {0x0000006b, 7}, {0x0000006c, 7}, {0x0000006d, 7}, {0x0000006e, 7}, + {0x0000006f, 7}, {0x00000070, 7}, {0x00000071, 7}, {0x00000072, 7}, + {0x000000fc, 8}, {0x00000073, 7}, {0x000000fd, 8}, {0x00001ffb, 13}, + {0x0007fff0, 19}, {0x00001ffc, 13}, {0x00003ffc, 14}, {0x00000022, 6}, + {0x00007ffd, 15}, {0x00000003, 5}, {0x00000023, 6}, {0x00000004, 5}, + {0x00000024, 6}, {0x00000005, 5}, {0x00000025, 6}, {0x00000026, 6}, + {0x00000027, 6}, {0x00000006, 5}, {0x00000074, 7}, {0x00000075, 7}, + {0x00000028, 6}, {0x00000029, 6}, {0x0000002a, 6}, {0x00000007, 5}, + {0x0000002b, 6}, {0x00000076, 7}, {0x0000002c, 6}, {0x00000008, 5}, + {0x00000009, 5}, {0x0000002d, 6}, {0x00000077, 7}, {0x00000078, 7}, + {0x00000079, 7}, {0x0000007a, 7}, {0x0000007b, 7}, {0x00007ffe, 15}, + {0x000007fc, 11}, {0x00003ffd, 14}, {0x00001ffd, 13}, {0x0ffffffc, 28}, + {0x000fffe6, 20}, {0x003fffd2, 22}, {0x000fffe7, 20}, {0x000fffe8, 20}, + {0x003fffd3, 22}, {0x003fffd4, 22}, {0x003fffd5, 22}, {0x007fffd9, 23}, + {0x003fffd6, 22}, {0x007fffda, 23}, {0x007fffdb, 23}, {0x007fffdc, 23}, + {0x007fffdd, 23}, {0x007fffde, 23}, {0x00ffffeb, 24}, {0x007fffdf, 23}, + {0x00ffffec, 24}, {0x00ffffed, 24}, {0x003fffd7, 22}, {0x007fffe0, 23}, + {0x00ffffee, 24}, {0x007fffe1, 23}, {0x007fffe2, 23}, {0x007fffe3, 23}, + {0x007fffe4, 23}, {0x001fffdc, 21}, {0x003fffd8, 22}, {0x007fffe5, 23}, + {0x003fffd9, 22}, {0x007fffe6, 23}, {0x007fffe7, 23}, {0x00ffffef, 24}, + {0x003fffda, 22}, {0x001fffdd, 21}, {0x000fffe9, 20}, {0x003fffdb, 22}, + {0x003fffdc, 22}, {0x007fffe8, 23}, {0x007fffe9, 23}, {0x001fffde, 21}, + {0x007fffea, 23}, {0x003fffdd, 22}, {0x003fffde, 22}, {0x00fffff0, 24}, + {0x001fffdf, 21}, {0x003fffdf, 22}, {0x007fffeb, 23}, {0x007fffec, 23}, + {0x001fffe0, 21}, {0x001fffe1, 21}, {0x003fffe0, 22}, {0x001fffe2, 21}, + {0x007fffed, 23}, {0x003fffe1, 22}, {0x007fffee, 23}, {0x007fffef, 23}, + {0x000fffea, 20}, {0x003fffe2, 22}, {0x003fffe3, 22}, {0x003fffe4, 22}, + {0x007ffff0, 23}, {0x003fffe5, 22}, {0x003fffe6, 22}, {0x007ffff1, 23}, + {0x03ffffe0, 26}, {0x03ffffe1, 26}, {0x000fffeb, 20}, {0x0007fff1, 19}, + {0x003fffe7, 22}, {0x007ffff2, 23}, {0x003fffe8, 22}, {0x01ffffec, 25}, + {0x03ffffe2, 26}, {0x03ffffe3, 26}, {0x03ffffe4, 26}, {0x07ffffde, 27}, + {0x07ffffdf, 27}, {0x03ffffe5, 26}, {0x00fffff1, 24}, {0x01ffffed, 25}, + {0x0007fff2, 19}, {0x001fffe3, 21}, {0x03ffffe6, 26}, {0x07ffffe0, 27}, + {0x07ffffe1, 27}, {0x03ffffe7, 26}, {0x07ffffe2, 27}, {0x00fffff2, 24}, + {0x001fffe4, 21}, {0x001fffe5, 21}, {0x03ffffe8, 26}, {0x03ffffe9, 26}, + {0x0ffffffd, 28}, {0x07ffffe3, 27}, {0x07ffffe4, 27}, {0x07ffffe5, 27}, + {0x000fffec, 20}, {0x00fffff3, 24}, {0x000fffed, 20}, {0x001fffe6, 21}, + {0x003fffe9, 22}, {0x001fffe7, 21}, {0x001fffe8, 21}, {0x007ffff3, 23}, + {0x003fffea, 22}, {0x003fffeb, 22}, {0x01ffffee, 25}, {0x01ffffef, 25}, + {0x00fffff4, 24}, {0x00fffff5, 24}, {0x03ffffea, 26}, {0x007ffff4, 23}, + {0x03ffffeb, 26}, {0x07ffffe6, 27}, {0x03ffffec, 26}, {0x03ffffed, 26}, + {0x07ffffe7, 27}, {0x07ffffe8, 27}, {0x07ffffe9, 27}, {0x07ffffea, 27}, + {0x07ffffeb, 27}, {0x0ffffffe, 28}, {0x07ffffec, 27}, {0x07ffffed, 27}, + {0x07ffffee, 27}, {0x07ffffef, 27}, {0x07fffff0, 27}, {0x03ffffee, 26}, + {0x3fffffff, 30}}; + +} // namespace net +} // namespace mozilla + +#endif // mozilla__net__Http2HuffmanOutgoing_h diff --git a/netwerk/protocol/http/Http2Push.cpp b/netwerk/protocol/http/Http2Push.cpp new file mode 100644 index 0000000000..702ccea895 --- /dev/null +++ b/netwerk/protocol/http/Http2Push.cpp @@ -0,0 +1,498 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et 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/. */ + +// HttpLog.h should generally be included first +#include "HttpLog.h" + +// Log on level :5, instead of default :4. +#undef LOG +#define LOG(args) LOG5(args) +#undef LOG_ENABLED +#define LOG_ENABLED() LOG5_ENABLED() + +#include + +#include "Http2Push.h" +#include "nsHttpHandler.h" +#include "nsHttpTransaction.h" +#include "nsIHttpPushListener.h" +#include "nsISocketTransport.h" +#include "nsSocketTransportService2.h" +#include "nsString.h" + +namespace mozilla { +namespace net { + +// Because WeakPtr isn't thread-safe we must ensure that the object is destroyed +// on the socket thread, so any Release() called on a different thread is +// dispatched to the socket thread. +bool Http2PushedStreamWrapper::DispatchRelease() { + if (OnSocketThread()) { + return false; + } + + gSocketTransportService->Dispatch( + NewNonOwningRunnableMethod("net::Http2PushedStreamWrapper::Release", this, + &Http2PushedStreamWrapper::Release), + NS_DISPATCH_NORMAL); + + return true; +} + +NS_IMPL_ADDREF(Http2PushedStreamWrapper) +NS_IMETHODIMP_(MozExternalRefCountType) +Http2PushedStreamWrapper::Release() { + nsrefcnt count = mRefCnt - 1; + if (DispatchRelease()) { + // Redispatched to the socket thread. + return count; + } + + MOZ_ASSERT(0 != mRefCnt, "dup release"); + count = --mRefCnt; + NS_LOG_RELEASE(this, count, "Http2PushedStreamWrapper"); + + if (0 == count) { + mRefCnt = 1; + delete (this); + return 0; + } + + return count; +} + +NS_INTERFACE_MAP_BEGIN(Http2PushedStreamWrapper) +NS_INTERFACE_MAP_END + +Http2PushedStreamWrapper::Http2PushedStreamWrapper( + Http2PushedStream* aPushStream) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + mStream = aPushStream; + mRequestString = aPushStream->GetRequestString(); + mResourceUrl = aPushStream->GetResourceUrl(); + mStreamID = aPushStream->StreamID(); +} + +Http2PushedStreamWrapper::~Http2PushedStreamWrapper() { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); +} + +Http2PushedStream* Http2PushedStreamWrapper::GetStream() { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + if (mStream) { + Http2Stream* stream = mStream; + return static_cast(stream); + } + return nullptr; +} + +void Http2PushedStreamWrapper::OnPushFailed() { + if (OnSocketThread()) { + if (mStream) { + Http2Stream* stream = mStream; + static_cast(stream)->OnPushFailed(); + } + } else { + gSocketTransportService->Dispatch( + NewRunnableMethod("net::Http2PushedStreamWrapper::OnPushFailed", this, + &Http2PushedStreamWrapper::OnPushFailed), + NS_DISPATCH_NORMAL); + } +} + +////////////////////////////////////////// +// Http2PushedStream +////////////////////////////////////////// + +Http2PushedStream::Http2PushedStream( + Http2PushTransactionBuffer* aTransaction, Http2Session* aSession, + Http2Stream* aAssociatedStream, uint32_t aID, + uint64_t aCurrentForegroundTabOuterContentWindowId) + : Http2Stream(aTransaction, aSession, 0, + aCurrentForegroundTabOuterContentWindowId), + mConsumerStream(nullptr), + mAssociatedTransaction(aAssociatedStream->Transaction()), + mBufferedPush(aTransaction), + mStatus(NS_OK), + mPushCompleted(false), + mDeferCleanupOnSuccess(true), + mDeferCleanupOnPush(false), + mOnPushFailed(false) { + LOG3(("Http2PushedStream ctor this=%p 0x%X\n", this, aID)); + mStreamID = aID; + MOZ_ASSERT(!(aID & 1)); // must be even to be a pushed stream + mBufferedPush->SetPushStream(this); + mRequestContext = aAssociatedStream->RequestContext(); + mLastRead = TimeStamp::Now(); + mPriorityDependency = aAssociatedStream->PriorityDependency(); + if (mPriorityDependency == Http2Session::kUrgentStartGroupID || + mPriorityDependency == Http2Session::kLeaderGroupID) { + mPriorityDependency = Http2Session::kFollowerGroupID; + } + // Cache this for later use in case of tab switch. + mDefaultPriorityDependency = mPriorityDependency; + SetPriorityDependency(aAssociatedStream->Priority() + 1, mPriorityDependency); + // Assume we are on the same tab as our associated stream, for priority + // purposes. It's possible this could change when we get paired with a sink, + // but it's unlikely and doesn't much matter anyway. + mTransactionTabId = aAssociatedStream->TransactionTabId(); +} + +bool Http2PushedStream::GetPushComplete() { return mPushCompleted; } + +nsresult Http2PushedStream::WriteSegments(nsAHttpSegmentWriter* writer, + uint32_t count, + uint32_t* countWritten) { + nsresult rv = Http2Stream::WriteSegments(writer, count, countWritten); + if (NS_SUCCEEDED(rv) && *countWritten) { + mLastRead = TimeStamp::Now(); + } + + if (rv == NS_BASE_STREAM_CLOSED) { + mPushCompleted = true; + rv = NS_OK; // this is what a normal HTTP transaction would do + } + if (rv != NS_BASE_STREAM_WOULD_BLOCK && NS_FAILED(rv)) mStatus = rv; + return rv; +} + +bool Http2PushedStream::DeferCleanup(nsresult status) { + LOG3(("Http2PushedStream::DeferCleanup Query %p %" PRIx32 "\n", this, + static_cast(status))); + + if (NS_SUCCEEDED(status) && mDeferCleanupOnSuccess) { + LOG3(("Http2PushedStream::DeferCleanup %p %" PRIx32 " defer on success\n", + this, static_cast(status))); + return true; + } + if (mDeferCleanupOnPush) { + LOG3(("Http2PushedStream::DeferCleanup %p %" PRIx32 " defer onPush ref\n", + this, static_cast(status))); + return true; + } + if (mConsumerStream) { + LOG3(("Http2PushedStream::DeferCleanup %p %" PRIx32 + " defer active consumer\n", + this, static_cast(status))); + return true; + } + LOG3(("Http2PushedStream::DeferCleanup Query %p %" PRIx32 " not deferred\n", + this, static_cast(status))); + return false; +} + +// return true if channel implements nsIHttpPushListener +bool Http2PushedStream::TryOnPush() { + nsHttpTransaction* trans = mAssociatedTransaction->QueryHttpTransaction(); + if (!trans) { + return false; + } + + if (!(trans->Caps() & NS_HTTP_ONPUSH_LISTENER)) { + return false; + } + + mDeferCleanupOnPush = true; + mResourceUrl = Origin() + Path(); + RefPtr stream = new Http2PushedStreamWrapper(this); + trans->OnPush(stream); + return true; +} + +// side effect free static method to determine if Http2Stream implements +// nsIHttpPushListener +bool Http2PushedStream::TestOnPush(Http2Stream* stream) { + if (!stream) { + return false; + } + nsAHttpTransaction* abstractTransaction = stream->Transaction(); + if (!abstractTransaction) { + return false; + } + nsHttpTransaction* trans = abstractTransaction->QueryHttpTransaction(); + if (!trans) { + return false; + } + return trans->Caps() & NS_HTTP_ONPUSH_LISTENER; +} + +nsresult Http2PushedStream::ReadSegments(nsAHttpSegmentReader* reader, uint32_t, + uint32_t* count) { + nsresult rv = NS_OK; + *count = 0; + + mozilla::OriginAttributes originAttributes; + switch (mUpstreamState) { + case GENERATING_HEADERS: + // The request headers for this has been processed, so we need to verify + // that :authority, :scheme, and :path MUST be present. :method MUST NOT + // be present + mSocketTransport->GetOriginAttributes(&originAttributes); + CreatePushHashKey(mHeaderScheme, mHeaderHost, originAttributes, + mSession->Serial(), mHeaderPath, mOrigin, mHashKey); + + LOG3(("Http2PushStream 0x%X hash key %s\n", mStreamID, mHashKey.get())); + + // the write side of a pushed transaction just involves manipulating a + // little state + SetSentFin(true); + Http2Stream::mRequestHeadersDone = 1; + Http2Stream::mOpenGenerated = 1; + Http2Stream::ChangeState(UPSTREAM_COMPLETE); + break; + + case UPSTREAM_COMPLETE: + // Let's just clear the stream's transmit buffer by pushing it into + // the session. This is probably a window adjustment. + LOG3(("Http2Push::ReadSegments 0x%X \n", mStreamID)); + mSegmentReader = reader; + rv = TransmitFrame(nullptr, nullptr, true); + mSegmentReader = nullptr; + break; + + case GENERATING_BODY: + case SENDING_BODY: + case SENDING_FIN_STREAM: + default: + break; + } + + return rv; +} + +void Http2PushedStream::AdjustInitialWindow() { + LOG3(("Http2PushStream %p 0x%X AdjustInitialWindow", this, mStreamID)); + if (mConsumerStream) { + LOG3( + ("Http2PushStream::AdjustInitialWindow %p 0x%X " + "calling super consumer %p 0x%X\n", + this, mStreamID, mConsumerStream, mConsumerStream->StreamID())); + Http2Stream::AdjustInitialWindow(); + // Http2PushedStream::ReadSegments is needed to call TransmitFrame() + // and actually get this information into the session bytestream + mSession->TransactionHasDataToWrite(this); + } + // Otherwise, when we get hooked up, the initial window will get bumped + // anyway, so we're good to go. +} + +void Http2PushedStream::SetConsumerStream(Http2Stream* consumer) { + LOG3(("Http2PushedStream::SetConsumerStream this=%p consumer=%p", this, + consumer)); + + mConsumerStream = consumer; + mDeferCleanupOnPush = false; +} + +bool Http2PushedStream::GetHashKey(nsCString& key) { + if (mHashKey.IsEmpty()) return false; + + key = mHashKey; + return true; +} + +void Http2PushedStream::ConnectPushedStream(Http2Stream* stream) { + mSession->ConnectPushedStream(stream); +} + +bool Http2PushedStream::IsOrphaned(TimeStamp now) { + MOZ_ASSERT(!now.IsNull()); + + // if session is not transmitting, and is also not connected to a consumer + // stream, and its been like that for too long then it is oprhaned + + if (mConsumerStream || mDeferCleanupOnPush) { + return false; + } + + if (mOnPushFailed) { + return true; + } + + bool rv = ((now - mLastRead).ToSeconds() > 30.0); + if (rv) { + LOG3(("Http2PushedStream:IsOrphaned 0x%X IsOrphaned %3.2f\n", mStreamID, + (now - mLastRead).ToSeconds())); + } + return rv; +} + +nsresult Http2PushedStream::GetBufferedData(char* buf, uint32_t count, + uint32_t* countWritten) { + if (NS_FAILED(mStatus)) return mStatus; + + nsresult rv = mBufferedPush->GetBufferedData(buf, count, countWritten); + if (NS_FAILED(rv)) return rv; + + if (!*countWritten) + rv = GetPushComplete() ? NS_BASE_STREAM_CLOSED : NS_BASE_STREAM_WOULD_BLOCK; + + return rv; +} + +void Http2PushedStream::TopLevelOuterContentWindowIdChanged(uint64_t windowId) { + if (mConsumerStream) { + // Pass through to our sink, who will handle things appropriately. + mConsumerStream->TopLevelOuterContentWindowIdChangedInternal(windowId); + return; + } + + MOZ_ASSERT(gHttpHandler->ActiveTabPriority()); + + mCurrentForegroundTabOuterContentWindowId = windowId; + + if (!mSession->UseH2Deps()) { + return; + } + + uint32_t oldDependency = mPriorityDependency; + if (mTransactionTabId != mCurrentForegroundTabOuterContentWindowId) { + mPriorityDependency = Http2Session::kBackgroundGroupID; + nsHttp::NotifyActiveTabLoadOptimization(); + } else { + mPriorityDependency = mDefaultPriorityDependency; + } + + if (mPriorityDependency != oldDependency) { + mSession->SendPriorityFrame(mStreamID, mPriorityDependency, + mPriorityWeight); + } +} + +////////////////////////////////////////// +// Http2PushTransactionBuffer +// This is the nsAHttpTransction owned by the stream when the pushed +// stream has not yet been matched with a pull request +////////////////////////////////////////// + +NS_IMPL_ISUPPORTS0(Http2PushTransactionBuffer) + +Http2PushTransactionBuffer::Http2PushTransactionBuffer() + : mStatus(NS_OK), + mRequestHead(nullptr), + mPushStream(nullptr), + mIsDone(false), + mBufferedHTTP1Size(kDefaultBufferSize), + mBufferedHTTP1Used(0), + mBufferedHTTP1Consumed(0) { + mBufferedHTTP1 = MakeUnique(mBufferedHTTP1Size); +} + +Http2PushTransactionBuffer::~Http2PushTransactionBuffer() { + delete mRequestHead; +} + +void Http2PushTransactionBuffer::SetConnection(nsAHttpConnection* conn) {} + +nsAHttpConnection* Http2PushTransactionBuffer::Connection() { return nullptr; } + +void Http2PushTransactionBuffer::GetSecurityCallbacks( + nsIInterfaceRequestor** outCB) { + *outCB = nullptr; +} + +void Http2PushTransactionBuffer::OnTransportStatus(nsITransport* transport, + nsresult status, + int64_t progress) {} + +nsHttpConnectionInfo* Http2PushTransactionBuffer::ConnectionInfo() { + if (!mPushStream) { + return nullptr; + } + if (!mPushStream->Transaction()) { + return nullptr; + } + MOZ_ASSERT(mPushStream->Transaction() != this); + return mPushStream->Transaction()->ConnectionInfo(); +} + +bool Http2PushTransactionBuffer::IsDone() { return mIsDone; } + +nsresult Http2PushTransactionBuffer::Status() { return mStatus; } + +uint32_t Http2PushTransactionBuffer::Caps() { return 0; } + +uint64_t Http2PushTransactionBuffer::Available() { + return mBufferedHTTP1Used - mBufferedHTTP1Consumed; +} + +nsresult Http2PushTransactionBuffer::ReadSegments(nsAHttpSegmentReader* reader, + uint32_t count, + uint32_t* countRead) { + *countRead = 0; + return NS_ERROR_NOT_IMPLEMENTED; +} + +nsresult Http2PushTransactionBuffer::WriteSegments(nsAHttpSegmentWriter* writer, + uint32_t count, + uint32_t* countWritten) { + if ((mBufferedHTTP1Size - mBufferedHTTP1Used) < 20480) { + EnsureBuffer(mBufferedHTTP1, mBufferedHTTP1Size + kDefaultBufferSize, + mBufferedHTTP1Used, mBufferedHTTP1Size); + } + + count = std::min(count, mBufferedHTTP1Size - mBufferedHTTP1Used); + nsresult rv = writer->OnWriteSegment(&mBufferedHTTP1[mBufferedHTTP1Used], + count, countWritten); + if (NS_SUCCEEDED(rv)) { + mBufferedHTTP1Used += *countWritten; + } else if (rv == NS_BASE_STREAM_CLOSED) { + mIsDone = true; + } + + if (Available() || mIsDone) { + Http2Stream* consumer = mPushStream->GetConsumerStream(); + + if (consumer) { + LOG3( + ("Http2PushTransactionBuffer::WriteSegments notifying connection " + "consumer data available 0x%X [%" PRIu64 "] done=%d\n", + mPushStream->StreamID(), Available(), mIsDone)); + mPushStream->ConnectPushedStream(consumer); + } + } + + return rv; +} + +uint32_t Http2PushTransactionBuffer::Http1xTransactionCount() { return 0; } + +nsHttpRequestHead* Http2PushTransactionBuffer::RequestHead() { + if (!mRequestHead) mRequestHead = new nsHttpRequestHead(); + return mRequestHead; +} + +nsresult Http2PushTransactionBuffer::TakeSubTransactions( + nsTArray >& outTransactions) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +void Http2PushTransactionBuffer::SetProxyConnectFailed() {} + +void Http2PushTransactionBuffer::Close(nsresult reason) { + mStatus = reason; + mIsDone = true; +} + +nsresult Http2PushTransactionBuffer::GetBufferedData(char* buf, uint32_t count, + uint32_t* countWritten) { + *countWritten = std::min(count, static_cast(Available())); + if (*countWritten) { + memcpy(buf, &mBufferedHTTP1[mBufferedHTTP1Consumed], *countWritten); + mBufferedHTTP1Consumed += *countWritten; + } + + // If all the data has been consumed then reset the buffer + if (mBufferedHTTP1Consumed == mBufferedHTTP1Used) { + mBufferedHTTP1Consumed = 0; + mBufferedHTTP1Used = 0; + } + + return NS_OK; +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/Http2Push.h b/netwerk/protocol/http/Http2Push.h new file mode 100644 index 0000000000..c1dfc433a4 --- /dev/null +++ b/netwerk/protocol/http/Http2Push.h @@ -0,0 +1,161 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef mozilla_net_Http2Push_Internal_h +#define mozilla_net_Http2Push_Internal_h + +// HTTP/2 - RFC 7540 +// https://www.rfc-editor.org/rfc/rfc7540.txt + +#include "Http2Session.h" +#include "Http2Stream.h" + +#include "mozilla/Attributes.h" +#include "mozilla/TimeStamp.h" +#include "mozilla/UniquePtr.h" +#include "nsHttpRequestHead.h" +#include "nsIRequestContext.h" +#include "nsString.h" +#include "PSpdyPush.h" + +namespace mozilla { +namespace net { + +class Http2PushTransactionBuffer; + +class Http2PushedStream final : public Http2Stream { + public: + Http2PushedStream(Http2PushTransactionBuffer* aTransaction, + Http2Session* aSession, Http2Stream* aAssociatedStream, + uint32_t aID, + uint64_t aCurrentForegroundTabOuterContentWindowId); + + bool GetPushComplete(); + + // The consumer stream is the synthetic pull stream hooked up to this push + virtual Http2Stream* GetConsumerStream() override { return mConsumerStream; }; + + void SetConsumerStream(Http2Stream* aStream); + [[nodiscard]] bool GetHashKey(nsCString& key); + + // override of Http2Stream + [[nodiscard]] nsresult ReadSegments(nsAHttpSegmentReader*, uint32_t, + uint32_t*) override; + [[nodiscard]] nsresult WriteSegments(nsAHttpSegmentWriter*, uint32_t, + uint32_t*) override; + void AdjustInitialWindow() override; + + nsIRequestContext* RequestContext() override { return mRequestContext; }; + void ConnectPushedStream(Http2Stream* consumer); + + [[nodiscard]] bool TryOnPush(); + [[nodiscard]] static bool TestOnPush(Http2Stream* consumer); + + virtual bool DeferCleanup(nsresult status) override; + void SetDeferCleanupOnSuccess(bool val) { mDeferCleanupOnSuccess = val; } + + bool IsOrphaned(TimeStamp now); + void OnPushFailed() { + mDeferCleanupOnPush = false; + mOnPushFailed = true; + } + + [[nodiscard]] nsresult GetBufferedData(char* buf, uint32_t count, + uint32_t* countWritten); + + // overload of Http2Stream + virtual bool HasSink() override { return !!mConsumerStream; } + virtual void SetPushComplete() override { mPushCompleted = true; } + virtual void TopLevelOuterContentWindowIdChanged(uint64_t) override; + + nsCString& GetRequestString() { return mRequestString; } + nsCString& GetResourceUrl() { return mResourceUrl; } + + private: + virtual ~Http2PushedStream() = default; + Http2Stream* + mConsumerStream; // paired request stream that consumes from + // real http/2 one.. null until a match is made. + + nsCOMPtr mRequestContext; + + nsAHttpTransaction* mAssociatedTransaction; + + Http2PushTransactionBuffer* mBufferedPush; + mozilla::TimeStamp mLastRead; + + nsCString mHashKey; + nsresult mStatus; + bool mPushCompleted; // server push FIN received + bool mDeferCleanupOnSuccess; + + // mDeferCleanupOnPush prevents Http2Session::CleanupStream() from + // destroying the push stream on an error code during the period between + // when we need to do OnPush() on another thread and the time it takes + // for that event to create a synthetic pull stream attached to this + // object. That synthetic pull will become mConsuemerStream. + // Ths is essentially a delete protecting reference. + bool mDeferCleanupOnPush; + bool mOnPushFailed; + nsCString mRequestString; + nsCString mResourceUrl; + + uint32_t mDefaultPriorityDependency; +}; + +class Http2PushTransactionBuffer final : public nsAHttpTransaction { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSAHTTPTRANSACTION + + Http2PushTransactionBuffer(); + + [[nodiscard]] nsresult GetBufferedData(char* buf, uint32_t count, + uint32_t* countWritten); + void SetPushStream(Http2PushedStream* stream) { mPushStream = stream; } + + private: + virtual ~Http2PushTransactionBuffer(); + uint64_t Available(); + + const static uint32_t kDefaultBufferSize = 4096; + + nsresult mStatus; + nsHttpRequestHead* mRequestHead; + Http2PushedStream* mPushStream; + bool mIsDone; + + UniquePtr mBufferedHTTP1; + uint32_t mBufferedHTTP1Size; + uint32_t mBufferedHTTP1Used; + uint32_t mBufferedHTTP1Consumed; +}; + +class Http2PushedStreamWrapper : public nsISupports { + public: + NS_DECL_THREADSAFE_ISUPPORTS + bool DispatchRelease(); + + explicit Http2PushedStreamWrapper(Http2PushedStream* aPushStream); + + nsCString& GetRequestString() { return mRequestString; } + nsCString& GetResourceUrl() { return mResourceUrl; } + Http2PushedStream* GetStream(); + void OnPushFailed(); + uint32_t StreamID() { return mStreamID; } + + private: + virtual ~Http2PushedStreamWrapper(); + + nsCString mRequestString; + nsCString mResourceUrl; + uint32_t mStreamID; + WeakPtr mStream; +}; + +} // namespace net +} // namespace mozilla + +#endif // mozilla_net_Http2Push_Internal_h diff --git a/netwerk/protocol/http/Http2Session.cpp b/netwerk/protocol/http/Http2Session.cpp new file mode 100644 index 0000000000..c7e6aa14c0 --- /dev/null +++ b/netwerk/protocol/http/Http2Session.cpp @@ -0,0 +1,4653 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et 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/. */ + +// HttpLog.h should generally be included first +#include "HttpLog.h" + +// Log on level :5, instead of default :4. +#undef LOG +#define LOG(args) LOG5(args) +#undef LOG_ENABLED +#define LOG_ENABLED() LOG5_ENABLED() + +#include + +#include "AltServiceChild.h" +#include "Http2Session.h" +#include "Http2Stream.h" +#include "Http2Push.h" + +#include "mozilla/EndianUtils.h" +#include "mozilla/Telemetry.h" +#include "mozilla/Preferences.h" +#include "nsHttp.h" +#include "nsHttpHandler.h" +#include "nsHttpConnection.h" +#include "nsIRequestContext.h" +#include "nsISSLSocketControl.h" +#include "nsISupportsPriority.h" +#include "nsStandardURL.h" +#include "nsURLHelper.h" +#include "prnetdb.h" +#include "sslt.h" +#include "mozilla/Sprintf.h" +#include "nsSocketTransportService2.h" +#include "nsNetUtil.h" +#include "CacheControlParser.h" +#include "CachePushChecker.h" +#include "LoadContextInfo.h" +#include "TCPFastOpenLayer.h" +#include "nsQueryObject.h" + +namespace mozilla { +namespace net { + +// Http2Session has multiple inheritance of things that implement nsISupports +NS_IMPL_ADDREF(Http2Session) +NS_IMPL_RELEASE(Http2Session) +NS_INTERFACE_MAP_BEGIN(Http2Session) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsAHttpConnection) +NS_INTERFACE_MAP_END + +// "magic" refers to the string that preceeds HTTP/2 on the wire +// to help find any intermediaries speaking an older version of HTTP +const uint8_t Http2Session::kMagicHello[] = { + 0x50, 0x52, 0x49, 0x20, 0x2a, 0x20, 0x48, 0x54, 0x54, 0x50, 0x2f, 0x32, + 0x2e, 0x30, 0x0d, 0x0a, 0x0d, 0x0a, 0x53, 0x4d, 0x0d, 0x0a, 0x0d, 0x0a}; + +Http2Session* Http2Session::CreateSession(nsISocketTransport* aSocketTransport, + enum SpdyVersion version, + bool attemptingEarlyData) { + if (!gHttpHandler) { + RefPtr handler = nsHttpHandler::GetInstance(); + Unused << handler.get(); + } + + Http2Session* session = + new Http2Session(aSocketTransport, version, attemptingEarlyData); + session->SendHello(); + return session; +} + +Http2Session::Http2Session(nsISocketTransport* aSocketTransport, + enum SpdyVersion version, bool attemptingEarlyData) + : mSocketTransport(aSocketTransport), + mSegmentReader(nullptr), + mSegmentWriter(nullptr), + mNextStreamID(3) // 1 is reserved for Updgrade handshakes + , + mLastPushedID(0), + mConcurrentHighWater(0), + mDownstreamState(BUFFERING_OPENING_SETTINGS), + mInputFrameBufferSize(kDefaultBufferSize), + mInputFrameBufferUsed(0), + mInputFrameDataSize(0), + mInputFrameDataRead(0), + mInputFrameFinal(false), + mInputFrameType(0), + mInputFrameFlags(0), + mInputFrameID(0), + mPaddingLength(0), + mInputFrameDataStream(nullptr), + mNeedsCleanup(nullptr), + mDownstreamRstReason(NO_HTTP_ERROR), + mExpectedHeaderID(0), + mExpectedPushPromiseID(0), + mContinuedPromiseStream(0), + mFlatHTTPResponseHeadersOut(0), + mShouldGoAway(false), + mClosed(false), + mCleanShutdown(false), + mReceivedSettings(false), + mTLSProfileConfirmed(false), + mGoAwayReason(NO_HTTP_ERROR), + mClientGoAwayReason(UNASSIGNED), + mPeerGoAwayReason(UNASSIGNED), + mGoAwayID(0), + mOutgoingGoAwayID(0), + mConcurrent(0), + mServerPushedResources(0), + mServerInitialStreamWindow(kDefaultRwin), + mLocalSessionWindow(kDefaultRwin), + mServerSessionWindow(kDefaultRwin), + mInitialRwin(ASpdySession::kInitialRwin), + mOutputQueueSize(kDefaultQueueSize), + mOutputQueueUsed(0), + mOutputQueueSent(0), + mLastReadEpoch(PR_IntervalNow()), + mPingSentEpoch(0), + mPreviousUsed(false), + mAggregatedHeaderSize(0), + mWaitingForSettingsAck(false), + mGoAwayOnPush(false), + mUseH2Deps(false), + mAttemptingEarlyData(attemptingEarlyData), + mOriginFrameActivated(false), + mCntActivated(0), + mTlsHandshakeFinished(false), + mCheckNetworkStallsWithTFO(false), + mLastRequestBytesSentTime(0), + mPeerFailedHandshake(false), + mTrrStreams(0), + mEnableWebsockets(false), + mPeerAllowsWebsockets(false), + mProcessedWaitingWebsockets(false) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + static uint64_t sSerial; + mSerial = ++sSerial; + + LOG3(("Http2Session::Http2Session %p serial=0x%" PRIX64 "\n", this, mSerial)); + + mInputFrameBuffer = MakeUnique(mInputFrameBufferSize); + mOutputQueueBuffer = MakeUnique(mOutputQueueSize); + mDecompressBuffer.SetCapacity(kDefaultBufferSize); + + mPushAllowance = gHttpHandler->SpdyPushAllowance(); + mInitialRwin = std::max(gHttpHandler->SpdyPullAllowance(), mPushAllowance); + mMaxConcurrent = gHttpHandler->DefaultSpdyConcurrent(); + mSendingChunkSize = gHttpHandler->SpdySendingChunkSize(); + + mLastDataReadEpoch = mLastReadEpoch; + + mPingThreshold = gHttpHandler->SpdyPingThreshold(); + mPreviousPingThreshold = mPingThreshold; + mCurrentForegroundTabOuterContentWindowId = + gHttpHandler->ConnMgr()->CurrentTopLevelOuterContentWindowId(); + + mEnableWebsockets = gHttpHandler->IsH2WebsocketsEnabled(); + + bool dumpHpackTables = gHttpHandler->DumpHpackTables(); + mCompressor.SetDumpTables(dumpHpackTables); + mDecompressor.SetDumpTables(dumpHpackTables); +} + +void Http2Session::Shutdown() { + for (auto iter = mStreamTransactionHash.Iter(); !iter.Done(); iter.Next()) { + auto stream = iter.UserData(); + + // On a clean server hangup the server sets the GoAwayID to be the ID of + // the last transaction it processed. If the ID of stream in the + // local stream is greater than that it can safely be restarted because the + // server guarantees it was not partially processed. Streams that have not + // registered an ID haven't actually been sent yet so they can always be + // restarted. + if (mCleanShutdown && + (stream->StreamID() > mGoAwayID || !stream->HasRegisteredID())) { + CloseStream(stream, NS_ERROR_NET_RESET); // can be restarted + } else if (stream->RecvdData()) { + CloseStream(stream, NS_ERROR_NET_PARTIAL_TRANSFER); + } else if (mGoAwayReason == INADEQUATE_SECURITY) { + CloseStream(stream, NS_ERROR_NET_INADEQUATE_SECURITY); + } else if (!mCleanShutdown && (mGoAwayReason != NO_HTTP_ERROR)) { + CloseStream(stream, NS_ERROR_NET_HTTP2_SENT_GOAWAY); + } else { + CloseStream(stream, NS_ERROR_ABORT); + } + } +} + +Http2Session::~Http2Session() { + LOG3(("Http2Session::~Http2Session %p mDownstreamState=%X", this, + mDownstreamState)); + + Shutdown(); + + if (mTrrStreams) { + Telemetry::Accumulate(Telemetry::DNS_TRR_REQUEST_PER_CONN, mTrrStreams); + } + Telemetry::Accumulate(Telemetry::SPDY_PARALLEL_STREAMS, mConcurrentHighWater); + Telemetry::Accumulate(Telemetry::SPDY_REQUEST_PER_CONN_3, mCntActivated); + Telemetry::Accumulate(Telemetry::SPDY_SERVER_INITIATED_STREAMS, + mServerPushedResources); + Telemetry::Accumulate(Telemetry::SPDY_GOAWAY_LOCAL, mClientGoAwayReason); + Telemetry::Accumulate(Telemetry::SPDY_GOAWAY_PEER, mPeerGoAwayReason); + Telemetry::Accumulate(Telemetry::HTTP2_FAIL_BEFORE_SETTINGS, + mPeerFailedHandshake); +} + +inline nsresult Http2Session::SessionError(enum errorType reason) { + LOG3(("Http2Session::SessionError %p reason=0x%x mPeerGoAwayReason=0x%x", + this, reason, mPeerGoAwayReason)); + mGoAwayReason = reason; + + if (reason == INADEQUATE_SECURITY) { + // This one is special, as we have an error page just for this + return NS_ERROR_NET_INADEQUATE_SECURITY; + } + + // We're the one sending a generic GOAWAY + return NS_ERROR_NET_HTTP2_SENT_GOAWAY; +} + +void Http2Session::LogIO(Http2Session* self, Http2Stream* stream, + const char* label, const char* data, + uint32_t datalen) { + if (!LOG5_ENABLED()) return; + + LOG5(("Http2Session::LogIO %p stream=%p id=0x%X [%s]", self, stream, + stream ? stream->StreamID() : 0, label)); + + // Max line is (16 * 3) + 10(prefix) + newline + null + char linebuf[128]; + uint32_t index; + char* line = linebuf; + + linebuf[127] = 0; + + for (index = 0; index < datalen; ++index) { + if (!(index % 16)) { + if (index) { + *line = 0; + LOG5(("%s", linebuf)); + } + line = linebuf; + snprintf(line, 128, "%08X: ", index); + line += 10; + } + snprintf(line, 128 - (line - linebuf), "%02X ", + (reinterpret_cast(data))[index]); + line += 3; + } + if (index) { + *line = 0; + LOG5(("%s", linebuf)); + } +} + +typedef nsresult (*Http2ControlFx)(Http2Session* self); +static Http2ControlFx sControlFunctions[] = { + nullptr, // type 0 data is not a control function + Http2Session::RecvHeaders, + Http2Session::RecvPriority, + Http2Session::RecvRstStream, + Http2Session::RecvSettings, + Http2Session::RecvPushPromise, + Http2Session::RecvPing, + Http2Session::RecvGoAway, + Http2Session::RecvWindowUpdate, + Http2Session::RecvContinuation, + Http2Session::RecvAltSvc, // extension for type 0x0A + Http2Session::RecvUnused, // 0x0B was BLOCKED still radioactive + Http2Session::RecvOrigin // extension for type 0x0C +}; + +bool Http2Session::RoomForMoreConcurrent() { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + return (mConcurrent < mMaxConcurrent); +} + +bool Http2Session::RoomForMoreStreams() { + if (mNextStreamID + mStreamTransactionHash.Count() * 2 > kMaxStreamID) + return false; + + return !mShouldGoAway; +} + +PRIntervalTime Http2Session::IdleTime() { + return PR_IntervalNow() - mLastDataReadEpoch; +} + +uint32_t Http2Session::ReadTimeoutTick(PRIntervalTime now) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + LOG3(("Http2Session::ReadTimeoutTick %p delta since last read %ds\n", this, + PR_IntervalToSeconds(now - mLastReadEpoch))); + + uint32_t nextTick = UINT32_MAX; + if (mCheckNetworkStallsWithTFO && mLastRequestBytesSentTime) { + PRIntervalTime initialResponseDelta = now - mLastRequestBytesSentTime; + if (initialResponseDelta >= gHttpHandler->FastOpenStallsTimeout()) { + gHttpHandler->IncrementFastOpenStallsCounter(); + mCheckNetworkStallsWithTFO = false; + } else { + nextTick = PR_IntervalToSeconds(gHttpHandler->FastOpenStallsTimeout()) - + PR_IntervalToSeconds(initialResponseDelta); + } + } + if (!mPingThreshold) return nextTick; + + if ((now - mLastReadEpoch) < mPingThreshold) { + // recent activity means ping is not an issue + if (mPingSentEpoch) { + mPingSentEpoch = 0; + if (mPreviousUsed) { + // restore the former value + mPingThreshold = mPreviousPingThreshold; + mPreviousUsed = false; + } + } + + return std::min(nextTick, PR_IntervalToSeconds(mPingThreshold) - + PR_IntervalToSeconds(now - mLastReadEpoch)); + } + + if (mPingSentEpoch) { + LOG3(("Http2Session::ReadTimeoutTick %p handle outstanding ping\n", this)); + if ((now - mPingSentEpoch) >= gHttpHandler->SpdyPingTimeout()) { + LOG3(("Http2Session::ReadTimeoutTick %p Ping Timer Exhaustion\n", this)); + mPingSentEpoch = 0; + Close(NS_ERROR_NET_TIMEOUT); + return UINT32_MAX; + } + return 1; // run the tick aggressively while ping is outstanding + } + + LOG3(("Http2Session::ReadTimeoutTick %p generating ping\n", this)); + + mPingSentEpoch = PR_IntervalNow(); + if (!mPingSentEpoch) { + mPingSentEpoch = 1; // avoid the 0 sentinel value + } + GeneratePing(false); + Unused << ResumeRecv(); // read the ping reply + + // Check for orphaned push streams. This looks expensive, but generally the + // list is empty. + Http2PushedStream* deleteMe; + TimeStamp timestampNow; + do { + deleteMe = nullptr; + + for (uint32_t index = mPushedStreams.Length(); index > 0; --index) { + Http2PushedStream* pushedStream = mPushedStreams[index - 1]; + + if (timestampNow.IsNull()) + timestampNow = TimeStamp::Now(); // lazy initializer + + // if stream finished, but is not connected, and its been like that for + // long then cleanup the stream. + if (pushedStream->IsOrphaned(timestampNow)) { + LOG3(("Http2Session Timeout Pushed Stream %p 0x%X\n", this, + pushedStream->StreamID())); + deleteMe = pushedStream; + break; // don't CleanupStream() while iterating this vector + } + } + if (deleteMe) CleanupStream(deleteMe, NS_ERROR_ABORT, CANCEL_ERROR); + + } while (deleteMe); + + return 1; // run the tick aggressively while ping is outstanding +} + +uint32_t Http2Session::RegisterStreamID(Http2Stream* stream, uint32_t aNewID) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + MOZ_ASSERT(mNextStreamID < 0xfffffff0, + "should have stopped admitting streams"); + MOZ_ASSERT(!(aNewID & 1), + "0 for autoassign pull, otherwise explicit even push assignment"); + + if (!aNewID) { + // auto generate a new pull stream ID + aNewID = mNextStreamID; + MOZ_ASSERT(aNewID & 1, "pull ID must be odd."); + mNextStreamID += 2; + } + + LOG1( + ("Http2Session::RegisterStreamID session=%p stream=%p id=0x%X " + "concurrent=%d", + this, stream, aNewID, mConcurrent)); + + // We've used up plenty of ID's on this session. Start + // moving to a new one before there is a crunch involving + // server push streams or concurrent non-registered submits + if (aNewID >= kMaxStreamID) mShouldGoAway = true; + + // integrity check + if (mStreamIDHash.Contains(aNewID)) { + LOG3((" New ID already present\n")); + MOZ_ASSERT(false, "New ID already present in mStreamIDHash"); + mShouldGoAway = true; + return kDeadStreamID; + } + + mStreamIDHash.Put(aNewID, stream); + + // If TCP fast Open has been used and conection was idle for some time + // we will be cautious and watch out for bug 1395494. + if (!mCheckNetworkStallsWithTFO && mConnection) { + RefPtr connBase = mConnection->HttpConnection(); + RefPtr conn = do_QueryObject(connBase); + if (conn && (conn->GetFastOpenStatus() == TFO_DATA_SENT) && + gHttpHandler + ->CheckIfConnectionIsStalledOnlyIfIdleForThisAmountOfSeconds() && + IdleTime() >= + gHttpHandler + ->CheckIfConnectionIsStalledOnlyIfIdleForThisAmountOfSeconds()) { + // If a connection was using the TCP FastOpen and it was idle for a + // long time we should check for stalls like bug 1395494. + mCheckNetworkStallsWithTFO = true; + mLastRequestBytesSentTime = PR_IntervalNow(); + } + } + + if (aNewID & 1) { + // don't count push streams here + MOZ_ASSERT(stream->Transaction(), "no transation for the stream!"); + RefPtr ci(stream->Transaction()->ConnectionInfo()); + if (ci && ci->GetIsTrrServiceChannel()) { + IncrementTrrCounter(); + } + } + return aNewID; +} + +bool Http2Session::AddStream(nsAHttpTransaction* aHttpTransaction, + int32_t aPriority, bool aUseTunnel, + bool aIsWebsocket, + nsIInterfaceRequestor* aCallbacks) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + // integrity check + if (mStreamTransactionHash.Contains(aHttpTransaction)) { + LOG3((" New transaction already present\n")); + MOZ_ASSERT(false, "AddStream duplicate transaction pointer"); + return false; + } + + if (!mConnection) { + mConnection = aHttpTransaction->Connection(); + } + + if (!mFirstHttpTransaction && !mTlsHandshakeFinished) { + mFirstHttpTransaction = aHttpTransaction->QueryHttpTransaction(); + LOG3(("Http2Session::AddStream first session=%p trans=%p ", this, + mFirstHttpTransaction.get())); + } + + if (mClosed || mShouldGoAway) { + nsHttpTransaction* trans = aHttpTransaction->QueryHttpTransaction(); + if (trans) { + RefPtr pushedStreamWrapper; + pushedStreamWrapper = trans->GetPushedStream(); + if (!pushedStreamWrapper || !pushedStreamWrapper->GetStream()) { + LOG3( + ("Http2Session::AddStream %p atrans=%p trans=%p session unusable - " + "resched.\n", + this, aHttpTransaction, trans)); + aHttpTransaction->SetConnection(nullptr); + nsresult rv = + gHttpHandler->InitiateTransaction(trans, trans->Priority()); + if (NS_FAILED(rv)) { + LOG3( + ("Http2Session::AddStream %p atrans=%p trans=%p failed to " + "initiate " + "transaction (%08x).\n", + this, aHttpTransaction, trans, static_cast(rv))); + } + return true; + } + } + } + + aHttpTransaction->SetConnection(this); + aHttpTransaction->OnActivated(); + + if (aIsWebsocket) { + MOZ_ASSERT(!aUseTunnel, "Websocket on tunnel?!"); + nsHttpTransaction* trans = aHttpTransaction->QueryHttpTransaction(); + MOZ_ASSERT(trans, "Websocket without transaction?!"); + if (!trans) { + LOG3(("Http2Session::AddStream %p websocket without transaction. WAT?!", + this)); + return true; + } + + if (!mEnableWebsockets) { + LOG3( + ("Http2Session::AddStream %p Re-queuing websocket as h1 due to " + "mEnableWebsockets=false", + this)); + aHttpTransaction->SetConnection(nullptr); + aHttpTransaction->DisableSpdy(); + nsresult rv = gHttpHandler->InitiateTransaction(trans, trans->Priority()); + if (NS_FAILED(rv)) { + LOG3( + ("Http2Session::AddStream %p failed to reinitiate websocket " + "transaction (0x%08x).", + this, static_cast(rv))); + } + + return true; + } + + if (!mPeerAllowsWebsockets) { + LOG3(("Http2Session::AddStream %p mPeerAllowsWebsockets=false", this)); + if (!mProcessedWaitingWebsockets) { + LOG3( + ("Http2Session::AddStream %p waiting for SETTINGS to determine " + "fate of websocket", + this)); + mWaitingWebsockets.AppendElement(aHttpTransaction); + mWaitingWebsocketCallbacks.AppendElement(aCallbacks); + } else { + LOG3( + ("Http2Session::AddStream %p Re-queuing websocket as h1 due to " + "mPeerAllowsWebsockets=false", + this)); + aHttpTransaction->SetConnection(nullptr); + aHttpTransaction->DisableSpdy(); + if (trans) { + nsresult rv = + gHttpHandler->InitiateTransaction(trans, trans->Priority()); + if (NS_FAILED(rv)) { + LOG3( + ("Http2Session::AddStream %p failed to reinitiate websocket " + "transaction (%08x).\n", + this, static_cast(rv))); + } + } + } + return true; + } + + LOG3(("Http2Session::AddStream session=%p trans=%p websocket", this, + aHttpTransaction)); + CreateWebsocketStream(aHttpTransaction, aCallbacks); + return true; + } + + if (aUseTunnel) { + LOG3(("Http2Session::AddStream session=%p trans=%p OnTunnel", this, + aHttpTransaction)); + DispatchOnTunnel(aHttpTransaction, aCallbacks); + return true; + } + + RefPtr refStream = + new Http2Stream(aHttpTransaction, this, aPriority, + mCurrentForegroundTabOuterContentWindowId); + + LOG3(("Http2Session::AddStream session=%p stream=%p serial=%" PRIu64 " " + "NextID=0x%X (tentative)", + this, refStream.get(), mSerial, mNextStreamID)); + + RefPtr stream = refStream; + mStreamTransactionHash.Put(aHttpTransaction, std::move(refStream)); + + mReadyForWrite.Push(stream); + SetWriteCallbacks(); + + // Kick off the SYN transmit without waiting for the poll loop + // This won't work for the first stream because there is no segment reader + // yet. + if (mSegmentReader) { + uint32_t countRead; + Unused << ReadSegments(nullptr, kDefaultBufferSize, &countRead); + } + + if (!(aHttpTransaction->Caps() & NS_HTTP_ALLOW_KEEPALIVE) && + !aHttpTransaction->IsNullTransaction()) { + LOG3(("Http2Session::AddStream %p transaction %p forces keep-alive off.\n", + this, aHttpTransaction)); + DontReuse(); + } + + return true; +} + +void Http2Session::QueueStream(Http2Stream* stream) { + // will be removed via processpending or a shutdown path + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + MOZ_ASSERT(!stream->CountAsActive()); + MOZ_ASSERT(!stream->Queued()); + + LOG3(("Http2Session::QueueStream %p stream %p queued.", this, stream)); + +#ifdef DEBUG + int32_t qsize = mQueuedStreams.GetSize(); + for (int32_t i = 0; i < qsize; i++) { + Http2Stream* qStream = mQueuedStreams.ObjectAt(i); + MOZ_ASSERT(qStream != stream); + MOZ_ASSERT(qStream->Queued()); + } +#endif + + stream->SetQueued(true); + mQueuedStreams.Push(stream); +} + +void Http2Session::ProcessPending() { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + Http2Stream* stream; + while (RoomForMoreConcurrent() && (stream = mQueuedStreams.PopFront())) { + LOG3(("Http2Session::ProcessPending %p stream %p woken from queue.", this, + stream)); + MOZ_ASSERT(!stream->CountAsActive()); + MOZ_ASSERT(stream->Queued()); + stream->SetQueued(false); + mReadyForWrite.Push(stream); + SetWriteCallbacks(); + } +} + +nsresult Http2Session::NetworkRead(nsAHttpSegmentWriter* writer, char* buf, + uint32_t count, uint32_t* countWritten) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + if (!count) { + *countWritten = 0; + return NS_OK; + } + + nsresult rv = writer->OnWriteSegment(buf, count, countWritten); + if (NS_SUCCEEDED(rv) && *countWritten > 0) { + mLastReadEpoch = PR_IntervalNow(); + mCheckNetworkStallsWithTFO = false; + } + return rv; +} + +void Http2Session::SetWriteCallbacks() { + if (mConnection && + (GetWriteQueueSize() || (mOutputQueueUsed > mOutputQueueSent))) { + Unused << mConnection->ResumeSend(); + } +} + +void Http2Session::RealignOutputQueue() { + if (mAttemptingEarlyData) { + // We can't realign right now, because we may need what's in there if early + // data fails. + return; + } + + mOutputQueueUsed -= mOutputQueueSent; + memmove(mOutputQueueBuffer.get(), mOutputQueueBuffer.get() + mOutputQueueSent, + mOutputQueueUsed); + mOutputQueueSent = 0; +} + +void Http2Session::FlushOutputQueue() { + if (!mSegmentReader || !mOutputQueueUsed) return; + + nsresult rv; + uint32_t countRead; + uint32_t avail = mOutputQueueUsed - mOutputQueueSent; + + if (!avail && mAttemptingEarlyData) { + // This is kind of a hack, but there are cases where we'll have already + // written the data we want whlie doing early data, but we get called again + // with a reader, and we need to avoid calling the reader when there's + // nothing for it to read. + return; + } + + rv = mSegmentReader->OnReadSegment( + mOutputQueueBuffer.get() + mOutputQueueSent, avail, &countRead); + LOG3(("Http2Session::FlushOutputQueue %p sz=%d rv=%" PRIx32 " actual=%d", + this, avail, static_cast(rv), countRead)); + + // Dont worry about errors on write, we will pick this up as a read error too + if (NS_FAILED(rv)) return; + + mOutputQueueSent += countRead; + + if (mAttemptingEarlyData) { + return; + } + + if (countRead == avail) { + mOutputQueueUsed = 0; + mOutputQueueSent = 0; + return; + } + + // If the output queue is close to filling up and we have sent out a good + // chunk of data from the beginning then realign it. + + if ((mOutputQueueSent >= kQueueMinimumCleanup) && + ((mOutputQueueSize - mOutputQueueUsed) < kQueueTailRoom)) { + RealignOutputQueue(); + } +} + +void Http2Session::DontReuse() { + LOG3(("Http2Session::DontReuse %p\n", this)); + if (!OnSocketThread()) { + LOG3(("Http2Session %p not on socket thread\n", this)); + nsCOMPtr event = NewRunnableMethod( + "Http2Session::DontReuse", this, &Http2Session::DontReuse); + gSocketTransportService->Dispatch(event, NS_DISPATCH_NORMAL); + return; + } + + mShouldGoAway = true; + if (!mClosed && !mStreamTransactionHash.Count()) { + Close(NS_OK); + } +} + +enum SpdyVersion Http2Session::SpdyVersion() { return SpdyVersion::HTTP_2; } + +uint32_t Http2Session::GetWriteQueueSize() { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + return mReadyForWrite.GetSize(); +} + +void Http2Session::ChangeDownstreamState(enum internalStateType newState) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + LOG3(("Http2Session::ChangeDownstreamState() %p from %X to %X", this, + mDownstreamState, newState)); + mDownstreamState = newState; +} + +void Http2Session::ResetDownstreamState() { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + LOG3(("Http2Session::ResetDownstreamState() %p", this)); + ChangeDownstreamState(BUFFERING_FRAME_HEADER); + + if (mInputFrameFinal && mInputFrameDataStream) { + mInputFrameFinal = false; + LOG3((" SetRecvdFin id=0x%x\n", mInputFrameDataStream->StreamID())); + mInputFrameDataStream->SetRecvdFin(true); + MaybeDecrementConcurrent(mInputFrameDataStream); + } + mInputFrameFinal = false; + mInputFrameBufferUsed = 0; + mInputFrameDataStream = nullptr; +} + +// return true if activated (and counted against max) +// otherwise return false and queue +bool Http2Session::TryToActivate(Http2Stream* aStream) { + if (aStream->Queued()) { + LOG3(("Http2Session::TryToActivate %p stream=%p already queued.\n", this, + aStream)); + return false; + } + + if (!RoomForMoreConcurrent()) { + LOG3( + ("Http2Session::TryToActivate %p stream=%p no room for more concurrent " + "streams\n", + this, aStream)); + QueueStream(aStream); + return false; + } + + LOG3(("Http2Session::TryToActivate %p stream=%p\n", this, aStream)); + IncrementConcurrent(aStream); + + mCntActivated++; + return true; +} + +void Http2Session::IncrementConcurrent(Http2Stream* stream) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + MOZ_ASSERT(!stream->StreamID() || (stream->StreamID() & 1), + "Do not activate pushed streams"); + + nsAHttpTransaction* trans = stream->Transaction(); + if (!trans || !trans->IsNullTransaction() || + trans->QuerySpdyConnectTransaction()) { + MOZ_ASSERT(!stream->CountAsActive()); + stream->SetCountAsActive(true); + ++mConcurrent; + + if (mConcurrent > mConcurrentHighWater) { + mConcurrentHighWater = mConcurrent; + } + LOG3( + ("Http2Session::IncrementCounter %p counting stream %p Currently %d " + "streams in session, high water mark is %d\n", + this, stream, mConcurrent, mConcurrentHighWater)); + } +} + +// call with data length (i.e. 0 for 0 data bytes - ignore 9 byte header) +// dest must have 9 bytes of allocated space +template +void Http2Session::CreateFrameHeader(charType dest, uint16_t frameLength, + uint8_t frameType, uint8_t frameFlags, + uint32_t streamID) { + MOZ_ASSERT(frameLength <= kMaxFrameData, "framelength too large"); + MOZ_ASSERT(!(streamID & 0x80000000)); + MOZ_ASSERT(!frameFlags || (frameType != FRAME_TYPE_PRIORITY && + frameType != FRAME_TYPE_RST_STREAM && + frameType != FRAME_TYPE_GOAWAY && + frameType != FRAME_TYPE_WINDOW_UPDATE)); + + dest[0] = 0x00; + NetworkEndian::writeUint16(dest + 1, frameLength); + dest[3] = frameType; + dest[4] = frameFlags; + NetworkEndian::writeUint32(dest + 5, streamID); +} + +char* Http2Session::EnsureOutputBuffer(uint32_t spaceNeeded) { + // this is an infallible allocation (if an allocation is + // needed, which is probably isn't) + EnsureBuffer(mOutputQueueBuffer, mOutputQueueUsed + spaceNeeded, + mOutputQueueUsed, mOutputQueueSize); + return mOutputQueueBuffer.get() + mOutputQueueUsed; +} + +template void Http2Session::CreateFrameHeader(char* dest, uint16_t frameLength, + uint8_t frameType, + uint8_t frameFlags, + uint32_t streamID); + +template void Http2Session::CreateFrameHeader(uint8_t* dest, + uint16_t frameLength, + uint8_t frameType, + uint8_t frameFlags, + uint32_t streamID); + +void Http2Session::MaybeDecrementConcurrent(Http2Stream* aStream) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG3(("MaybeDecrementConcurrent %p id=0x%X concurrent=%d active=%d\n", this, + aStream->StreamID(), mConcurrent, aStream->CountAsActive())); + + if (!aStream->CountAsActive()) return; + + MOZ_ASSERT(mConcurrent); + aStream->SetCountAsActive(false); + --mConcurrent; + ProcessPending(); +} + +// Need to decompress some data in order to keep the compression +// context correct, but we really don't care what the result is +nsresult Http2Session::UncompressAndDiscard(bool isPush) { + nsresult rv; + nsAutoCString trash; + + rv = mDecompressor.DecodeHeaderBlock( + reinterpret_cast(mDecompressBuffer.BeginReading()), + mDecompressBuffer.Length(), trash, isPush); + mDecompressBuffer.Truncate(); + if (NS_FAILED(rv)) { + LOG3(("Http2Session::UncompressAndDiscard %p Compression Error\n", this)); + mGoAwayReason = COMPRESSION_ERROR; + return rv; + } + return NS_OK; +} + +void Http2Session::GeneratePing(bool isAck) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG3(("Http2Session::GeneratePing %p isAck=%d\n", this, isAck)); + + char* packet = EnsureOutputBuffer(kFrameHeaderBytes + 8); + mOutputQueueUsed += kFrameHeaderBytes + 8; + + if (isAck) { + CreateFrameHeader(packet, 8, FRAME_TYPE_PING, kFlag_ACK, 0); + memcpy(packet + kFrameHeaderBytes, + mInputFrameBuffer.get() + kFrameHeaderBytes, 8); + } else { + CreateFrameHeader(packet, 8, FRAME_TYPE_PING, 0, 0); + memset(packet + kFrameHeaderBytes, 0, 8); + } + + LogIO(this, nullptr, "Generate Ping", packet, kFrameHeaderBytes + 8); + FlushOutputQueue(); +} + +void Http2Session::GenerateSettingsAck() { + // need to generate ack of this settings frame + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG3(("Http2Session::GenerateSettingsAck %p\n", this)); + + char* packet = EnsureOutputBuffer(kFrameHeaderBytes); + mOutputQueueUsed += kFrameHeaderBytes; + CreateFrameHeader(packet, 0, FRAME_TYPE_SETTINGS, kFlag_ACK, 0); + LogIO(this, nullptr, "Generate Settings ACK", packet, kFrameHeaderBytes); + FlushOutputQueue(); +} + +void Http2Session::GeneratePriority(uint32_t aID, uint8_t aPriorityWeight) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG3(("Http2Session::GeneratePriority %p %X %X\n", this, aID, + aPriorityWeight)); + + char* packet = CreatePriorityFrame(aID, 0, aPriorityWeight); + + LogIO(this, nullptr, "Generate Priority", packet, kFrameHeaderBytes + 5); + FlushOutputQueue(); +} + +void Http2Session::GenerateRstStream(uint32_t aStatusCode, uint32_t aID) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + // make sure we don't do this twice for the same stream (at least if we + // have a stream entry for it) + Http2Stream* stream = mStreamIDHash.Get(aID); + if (stream) { + if (stream->SentReset()) return; + stream->SetSentReset(true); + } + + LOG3(("Http2Session::GenerateRst %p 0x%X %d\n", this, aID, aStatusCode)); + + uint32_t frameSize = kFrameHeaderBytes + 4; + char* packet = EnsureOutputBuffer(frameSize); + mOutputQueueUsed += frameSize; + CreateFrameHeader(packet, 4, FRAME_TYPE_RST_STREAM, 0, aID); + + NetworkEndian::writeUint32(packet + kFrameHeaderBytes, aStatusCode); + + LogIO(this, nullptr, "Generate Reset", packet, frameSize); + FlushOutputQueue(); +} + +void Http2Session::GenerateGoAway(uint32_t aStatusCode) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG3(("Http2Session::GenerateGoAway %p code=%X\n", this, aStatusCode)); + + mClientGoAwayReason = aStatusCode; + uint32_t frameSize = kFrameHeaderBytes + 8; + char* packet = EnsureOutputBuffer(frameSize); + mOutputQueueUsed += frameSize; + + CreateFrameHeader(packet, 8, FRAME_TYPE_GOAWAY, 0, 0); + + // last-good-stream-id are bytes 9-12 reflecting pushes + NetworkEndian::writeUint32(packet + kFrameHeaderBytes, mOutgoingGoAwayID); + + // bytes 13-16 are the status code. + NetworkEndian::writeUint32(packet + frameSize - 4, aStatusCode); + + LogIO(this, nullptr, "Generate GoAway", packet, frameSize); + FlushOutputQueue(); +} + +// The Hello is comprised of +// 1] 24 octets of magic, which are designed to +// flush out silent but broken intermediaries +// 2] a settings frame which sets a small flow control window for pushes +// 3] a window update frame which creates a large session flow control window +// 4] 6 priority frames for streams which will never be opened with headers +// these streams (3, 5, 7, 9, b, d) build a dependency tree that all other +// streams will be direct leaves of. +void Http2Session::SendHello() { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG3(("Http2Session::SendHello %p\n", this)); + + // sized for magic + 5 settings and a session window update and 6 priority + // frames 24 magic, 33 for settings (9 header + 4 settings @6), 13 for window + // update, 6 priority frames at 14 (9 + 5) each + static const uint32_t maxSettings = 5; + static const uint32_t prioritySize = + kPriorityGroupCount * (kFrameHeaderBytes + 5); + static const uint32_t maxDataLen = + 24 + kFrameHeaderBytes + maxSettings * 6 + 13 + prioritySize; + char* packet = EnsureOutputBuffer(maxDataLen); + memcpy(packet, kMagicHello, 24); + mOutputQueueUsed += 24; + LogIO(this, nullptr, "Magic Connection Header", packet, 24); + + packet = mOutputQueueBuffer.get() + mOutputQueueUsed; + memset(packet, 0, maxDataLen - 24); + + // frame header will be filled in after we know how long the frame is + uint8_t numberOfEntries = 0; + + // entries need to be listed in order by ID + // 1st entry is bytes 9 to 14 + // 2nd entry is bytes 15 to 20 + // 3rd entry is bytes 21 to 26 + // 4th entry is bytes 27 to 32 + // 5th entry is bytes 33 to 38 + + // Let the other endpoint know about our default HPACK decompress table size + uint32_t maxHpackBufferSize = gHttpHandler->DefaultHpackBuffer(); + mDecompressor.SetInitialMaxBufferSize(maxHpackBufferSize); + NetworkEndian::writeUint16(packet + kFrameHeaderBytes + (6 * numberOfEntries), + SETTINGS_TYPE_HEADER_TABLE_SIZE); + NetworkEndian::writeUint32( + packet + kFrameHeaderBytes + (6 * numberOfEntries) + 2, + maxHpackBufferSize); + numberOfEntries++; + + if (!gHttpHandler->AllowPush()) { + // If we don't support push then set MAX_CONCURRENT to 0 and also + // set ENABLE_PUSH to 0 + NetworkEndian::writeUint16( + packet + kFrameHeaderBytes + (6 * numberOfEntries), + SETTINGS_TYPE_ENABLE_PUSH); + // The value portion of the setting pair is already initialized to 0 + numberOfEntries++; + + NetworkEndian::writeUint16( + packet + kFrameHeaderBytes + (6 * numberOfEntries), + SETTINGS_TYPE_MAX_CONCURRENT); + // The value portion of the setting pair is already initialized to 0 + numberOfEntries++; + + mWaitingForSettingsAck = true; + } + + // Advertise the Push RWIN for the session, and on each new pull stream + // send a window update + NetworkEndian::writeUint16(packet + kFrameHeaderBytes + (6 * numberOfEntries), + SETTINGS_TYPE_INITIAL_WINDOW); + NetworkEndian::writeUint32( + packet + kFrameHeaderBytes + (6 * numberOfEntries) + 2, mPushAllowance); + numberOfEntries++; + + // Make sure the other endpoint knows that we're sticking to the default max + // frame size + NetworkEndian::writeUint16(packet + kFrameHeaderBytes + (6 * numberOfEntries), + SETTINGS_TYPE_MAX_FRAME_SIZE); + NetworkEndian::writeUint32( + packet + kFrameHeaderBytes + (6 * numberOfEntries) + 2, kMaxFrameData); + numberOfEntries++; + + MOZ_ASSERT(numberOfEntries <= maxSettings); + uint32_t dataLen = 6 * numberOfEntries; + CreateFrameHeader(packet, dataLen, FRAME_TYPE_SETTINGS, 0, 0); + mOutputQueueUsed += kFrameHeaderBytes + dataLen; + + LogIO(this, nullptr, "Generate Settings", packet, + kFrameHeaderBytes + dataLen); + + // now bump the local session window from 64KB + uint32_t sessionWindowBump = mInitialRwin - kDefaultRwin; + if (kDefaultRwin < mInitialRwin) { + // send a window update for the session (Stream 0) for something large + mLocalSessionWindow = mInitialRwin; + + packet = mOutputQueueBuffer.get() + mOutputQueueUsed; + CreateFrameHeader(packet, 4, FRAME_TYPE_WINDOW_UPDATE, 0, 0); + mOutputQueueUsed += kFrameHeaderBytes + 4; + NetworkEndian::writeUint32(packet + kFrameHeaderBytes, sessionWindowBump); + + LOG3(("Session Window increase at start of session %p %u\n", this, + sessionWindowBump)); + LogIO(this, nullptr, "Session Window Bump ", packet, kFrameHeaderBytes + 4); + } + + if (gHttpHandler->UseH2Deps() && + gHttpHandler->CriticalRequestPrioritization()) { + mUseH2Deps = true; + MOZ_ASSERT(mNextStreamID == kLeaderGroupID); + CreatePriorityNode(kLeaderGroupID, 0, 200, "leader"); + mNextStreamID += 2; + MOZ_ASSERT(mNextStreamID == kOtherGroupID); + CreatePriorityNode(kOtherGroupID, 0, 100, "other"); + mNextStreamID += 2; + MOZ_ASSERT(mNextStreamID == kBackgroundGroupID); + CreatePriorityNode(kBackgroundGroupID, 0, 0, "background"); + mNextStreamID += 2; + MOZ_ASSERT(mNextStreamID == kSpeculativeGroupID); + CreatePriorityNode(kSpeculativeGroupID, kBackgroundGroupID, 0, + "speculative"); + mNextStreamID += 2; + MOZ_ASSERT(mNextStreamID == kFollowerGroupID); + CreatePriorityNode(kFollowerGroupID, kLeaderGroupID, 0, "follower"); + mNextStreamID += 2; + MOZ_ASSERT(mNextStreamID == kUrgentStartGroupID); + CreatePriorityNode(kUrgentStartGroupID, 0, 240, "urgentStart"); + mNextStreamID += 2; + // Hey, you! YES YOU! If you add/remove any groups here, you almost + // certainly need to change the lookup of the stream/ID hash in + // Http2Session::OnTransportStatus. Yeah, that's right. YOU! + } + + FlushOutputQueue(); +} + +void Http2Session::SendPriorityFrame(uint32_t streamID, uint32_t dependsOn, + uint8_t weight) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG3( + ("Http2Session::SendPriorityFrame %p Frame 0x%X depends on 0x%X " + "weight %d\n", + this, streamID, dependsOn, weight)); + + char* packet = CreatePriorityFrame(streamID, dependsOn, weight); + + LogIO(this, nullptr, "SendPriorityFrame", packet, kFrameHeaderBytes + 5); + FlushOutputQueue(); +} + +char* Http2Session::CreatePriorityFrame(uint32_t streamID, uint32_t dependsOn, + uint8_t weight) { + MOZ_ASSERT(streamID, "Priority on stream 0"); + char* packet = EnsureOutputBuffer(kFrameHeaderBytes + 5); + CreateFrameHeader(packet, 5, FRAME_TYPE_PRIORITY, 0, streamID); + mOutputQueueUsed += kFrameHeaderBytes + 5; + NetworkEndian::writeUint32(packet + kFrameHeaderBytes, + dependsOn); // depends on + packet[kFrameHeaderBytes + 4] = weight; // weight + return packet; +} + +void Http2Session::CreatePriorityNode(uint32_t streamID, uint32_t dependsOn, + uint8_t weight, const char* label) { + char* packet = CreatePriorityFrame(streamID, dependsOn, weight); + + LOG3( + ("Http2Session %p generate Priority Frame 0x%X depends on 0x%X " + "weight %d for %s class\n", + this, streamID, dependsOn, weight, label)); + LogIO(this, nullptr, "Priority dep node", packet, kFrameHeaderBytes + 5); +} + +// perform a bunch of integrity checks on the stream. +// returns true if passed, false (plus LOG and ABORT) if failed. +bool Http2Session::VerifyStream(Http2Stream* aStream, + uint32_t aOptionalID = 0) { + // This is annoying, but at least it is O(1) + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + +#ifndef DEBUG + // Only do the real verification in debug builds + return true; +#else // DEBUG + + if (!aStream) return true; + + uint32_t test = 0; + + do { + if (aStream->StreamID() == kDeadStreamID) break; + + nsAHttpTransaction* trans = aStream->Transaction(); + + test++; + if (!trans) break; + + test++; + if (mStreamTransactionHash.GetWeak(trans) != aStream) break; + + if (aStream->StreamID()) { + Http2Stream* idStream = mStreamIDHash.Get(aStream->StreamID()); + + test++; + if (idStream != aStream) break; + + if (aOptionalID) { + test++; + if (idStream->StreamID() != aOptionalID) break; + } + } + + // tests passed + return true; + } while (false); + + LOG3( + ("Http2Session %p VerifyStream Failure %p stream->id=0x%X " + "optionalID=0x%X trans=%p test=%d\n", + this, aStream, aStream->StreamID(), aOptionalID, aStream->Transaction(), + test)); + + MOZ_ASSERT(false, "VerifyStream"); + return false; +#endif // DEBUG +} + +void Http2Session::CleanupStream(Http2Stream* aStream, nsresult aResult, + errorType aResetCode) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG3(("Http2Session::CleanupStream %p %p 0x%X %" PRIX32 "\n", this, aStream, + aStream ? aStream->StreamID() : 0, static_cast(aResult))); + if (!aStream) { + return; + } + + Http2PushedStream* pushSource = aStream->PushSource(); + if (pushSource) { + // aStream is a synthetic attached to an even push + MOZ_ASSERT(pushSource->GetConsumerStream() == aStream); + MOZ_ASSERT(!aStream->StreamID()); + MOZ_ASSERT(!(pushSource->StreamID() & 0x1)); + aStream->ClearPushSource(); + } + + if (aStream->DeferCleanup(aResult)) { + LOG3(("Http2Session::CleanupStream 0x%X deferred\n", aStream->StreamID())); + return; + } + + if (!VerifyStream(aStream)) { + LOG3(("Http2Session::CleanupStream failed to verify stream\n")); + return; + } + + // don't reset a stream that has recevied a fin or rst + if (!aStream->RecvdFin() && !aStream->RecvdReset() && aStream->StreamID() && + !(mInputFrameFinal && + (aStream == mInputFrameDataStream))) { // !(recvdfin with mark pending) + LOG3(("Stream 0x%X had not processed recv FIN, sending RST code %X\n", + aStream->StreamID(), aResetCode)); + GenerateRstStream(aResetCode, aStream->StreamID()); + } + + CloseStream(aStream, aResult); + + // Remove the stream from the ID hash table and, if an even id, the pushed + // table too. + uint32_t id = aStream->StreamID(); + if (id > 0) { + mStreamIDHash.Remove(id); + if (!(id & 1)) { + mPushedStreams.RemoveElement(aStream); + Http2PushedStream* pushStream = static_cast(aStream); + nsAutoCString hashKey; + DebugOnly rv = pushStream->GetHashKey(hashKey); + MOZ_ASSERT(rv); + nsIRequestContext* requestContext = aStream->RequestContext(); + if (requestContext) { + SpdyPushCache* cache = requestContext->GetSpdyPushCache(); + if (cache) { + // Make sure the id of the stream in the push cache is the same + // as the id of the stream we're cleaning up! See bug 1368080. + Http2PushedStream* trash = + cache->RemovePushedStreamHttp2ByID(hashKey, aStream->StreamID()); + LOG3( + ("Http2Session::CleanupStream %p aStream=%p pushStream=%p " + "trash=%p", + this, aStream, pushStream, trash)); + } + } + } + } + + RemoveStreamFromQueues(aStream); + + // removing from the stream transaction hash will + // delete the Http2Stream and drop the reference to + // its transaction + mStreamTransactionHash.Remove(aStream->Transaction()); + + if (mShouldGoAway && !mStreamTransactionHash.Count()) Close(NS_OK); + + if (pushSource) { + pushSource->SetDeferCleanupOnSuccess(false); + CleanupStream(pushSource, aResult, aResetCode); + } +} + +void Http2Session::CleanupStream(uint32_t aID, nsresult aResult, + errorType aResetCode) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + Http2Stream* stream = mStreamIDHash.Get(aID); + LOG3(("Http2Session::CleanupStream %p by ID 0x%X to stream %p\n", this, aID, + stream)); + if (!stream) { + return; + } + CleanupStream(stream, aResult, aResetCode); +} + +static void RemoveStreamFromQueue(Http2Stream* aStream, + nsDeque& queue) { + size_t size = queue.GetSize(); + for (size_t count = 0; count < size; ++count) { + Http2Stream* stream = queue.PopFront(); + if (stream != aStream) queue.Push(stream); + } +} + +void Http2Session::RemoveStreamFromQueues(Http2Stream* aStream) { + RemoveStreamFromQueue(aStream, mReadyForWrite); + RemoveStreamFromQueue(aStream, mQueuedStreams); + RemoveStreamFromQueue(aStream, mPushesReadyForRead); + RemoveStreamFromQueue(aStream, mSlowConsumersReadyForRead); +} + +void Http2Session::CloseStream(Http2Stream* aStream, nsresult aResult) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG3(("Http2Session::CloseStream %p %p 0x%x %" PRIX32 "\n", this, aStream, + aStream->StreamID(), static_cast(aResult))); + + MaybeDecrementConcurrent(aStream); + + // Check if partial frame reader + if (aStream == mInputFrameDataStream) { + LOG3(("Stream had active partial read frame on close")); + ChangeDownstreamState(DISCARDING_DATA_FRAME); + mInputFrameDataStream = nullptr; + } + + RemoveStreamFromQueues(aStream); + + if (aStream->IsTunnel()) { + UnRegisterTunnel(aStream); + } + + // Send the stream the close() indication + aStream->Close(aResult); +} + +nsresult Http2Session::SetInputFrameDataStream(uint32_t streamID) { + mInputFrameDataStream = mStreamIDHash.Get(streamID); + if (VerifyStream(mInputFrameDataStream, streamID)) return NS_OK; + + LOG3(("Http2Session::SetInputFrameDataStream failed to verify 0x%X\n", + streamID)); + mInputFrameDataStream = nullptr; + return NS_ERROR_UNEXPECTED; +} + +nsresult Http2Session::ParsePadding(uint8_t& paddingControlBytes, + uint16_t& paddingLength) { + if (mInputFrameFlags & kFlag_PADDED) { + paddingLength = + *reinterpret_cast(&mInputFrameBuffer[kFrameHeaderBytes]); + paddingControlBytes = 1; + } else { + paddingLength = 0; + paddingControlBytes = 0; + } + + if (static_cast(paddingLength + paddingControlBytes) > + mInputFrameDataSize) { + // This is fatal to the session + LOG3( + ("Http2Session::ParsePadding %p stream 0x%x PROTOCOL_ERROR " + "paddingLength %d > frame size %d\n", + this, mInputFrameID, paddingLength, mInputFrameDataSize)); + return SessionError(PROTOCOL_ERROR); + } + + return NS_OK; +} + +nsresult Http2Session::RecvHeaders(Http2Session* self) { + MOZ_ASSERT(self->mInputFrameType == FRAME_TYPE_HEADERS || + self->mInputFrameType == FRAME_TYPE_CONTINUATION); + + bool isContinuation = self->mExpectedHeaderID != 0; + + // If this doesn't have END_HEADERS set on it then require the next + // frame to be HEADERS of the same ID + bool endHeadersFlag = self->mInputFrameFlags & kFlag_END_HEADERS; + + if (endHeadersFlag) + self->mExpectedHeaderID = 0; + else + self->mExpectedHeaderID = self->mInputFrameID; + + uint32_t priorityLen = 0; + if (self->mInputFrameFlags & kFlag_PRIORITY) { + priorityLen = 5; + } + nsresult rv = self->SetInputFrameDataStream(self->mInputFrameID); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + // Find out how much padding this frame has, so we can only extract the real + // header data from the frame. + uint16_t paddingLength = 0; + uint8_t paddingControlBytes = 0; + + if (!isContinuation) { + self->mDecompressBuffer.Truncate(); + rv = self->ParsePadding(paddingControlBytes, paddingLength); + if (NS_FAILED(rv)) { + return rv; + } + } + + LOG3( + ("Http2Session::RecvHeaders %p stream 0x%X priorityLen=%d stream=%p " + "end_stream=%d end_headers=%d priority_group=%d " + "paddingLength=%d padded=%d\n", + self, self->mInputFrameID, priorityLen, self->mInputFrameDataStream, + self->mInputFrameFlags & kFlag_END_STREAM, + self->mInputFrameFlags & kFlag_END_HEADERS, + self->mInputFrameFlags & kFlag_PRIORITY, paddingLength, + self->mInputFrameFlags & kFlag_PADDED)); + + if ((paddingControlBytes + priorityLen + paddingLength) > + self->mInputFrameDataSize) { + // This is fatal to the session + return self->SessionError(PROTOCOL_ERROR); + } + + if (!self->mInputFrameDataStream) { + // Cannot find stream. We can continue the session, but we need to + // uncompress the header block to maintain the correct compression context + + LOG3( + ("Http2Session::RecvHeaders %p lookup mInputFrameID stream " + "0x%X failed. NextStreamID = 0x%X\n", + self, self->mInputFrameID, self->mNextStreamID)); + + if (self->mInputFrameID >= self->mNextStreamID) + self->GenerateRstStream(PROTOCOL_ERROR, self->mInputFrameID); + + self->mDecompressBuffer.Append( + &self->mInputFrameBuffer[kFrameHeaderBytes + paddingControlBytes + + priorityLen], + self->mInputFrameDataSize - paddingControlBytes - priorityLen - + paddingLength); + + if (self->mInputFrameFlags & kFlag_END_HEADERS) { + rv = self->UncompressAndDiscard(false); + if (NS_FAILED(rv)) { + LOG3(("Http2Session::RecvHeaders uncompress failed\n")); + // this is fatal to the session + self->mGoAwayReason = COMPRESSION_ERROR; + return rv; + } + } + + self->ResetDownstreamState(); + return NS_OK; + } + + // make sure this is either the first headers or a trailer + if (self->mInputFrameDataStream->AllHeadersReceived() && + !(self->mInputFrameFlags & kFlag_END_STREAM)) { + // Any header block after the first that does *not* end the stream is + // illegal. + LOG3(("Http2Session::Illegal Extra HeaderBlock %p 0x%X\n", self, + self->mInputFrameID)); + return self->SessionError(PROTOCOL_ERROR); + } + + // queue up any compression bytes + self->mDecompressBuffer.Append( + &self->mInputFrameBuffer[kFrameHeaderBytes + paddingControlBytes + + priorityLen], + self->mInputFrameDataSize - paddingControlBytes - priorityLen - + paddingLength); + + self->mInputFrameDataStream->UpdateTransportReadEvents( + self->mInputFrameDataSize); + self->mLastDataReadEpoch = self->mLastReadEpoch; + + if (!isContinuation) { + self->mAggregatedHeaderSize = self->mInputFrameDataSize - + paddingControlBytes - priorityLen - + paddingLength; + } else { + self->mAggregatedHeaderSize += self->mInputFrameDataSize - + paddingControlBytes - priorityLen - + paddingLength; + } + + if (!endHeadersFlag) { // more are coming - don't process yet + self->ResetDownstreamState(); + return NS_OK; + } + + if (isContinuation) { + Telemetry::Accumulate(Telemetry::SPDY_CONTINUED_HEADERS, + self->mAggregatedHeaderSize); + } + + rv = self->ResponseHeadersComplete(); + if (rv == NS_ERROR_ILLEGAL_VALUE) { + LOG3(("Http2Session::RecvHeaders %p PROTOCOL_ERROR detected stream 0x%X\n", + self, self->mInputFrameID)); + self->CleanupStream(self->mInputFrameDataStream, rv, PROTOCOL_ERROR); + self->ResetDownstreamState(); + rv = NS_OK; + } else if (NS_FAILED(rv)) { + // This is fatal to the session. + self->mGoAwayReason = COMPRESSION_ERROR; + } + return rv; +} + +// ResponseHeadersComplete() returns NS_ERROR_ILLEGAL_VALUE when the stream +// should be reset with a PROTOCOL_ERROR, NS_OK when the response headers were +// fine, and any other error is fatal to the session. +nsresult Http2Session::ResponseHeadersComplete() { + LOG3(("Http2Session::ResponseHeadersComplete %p for 0x%X fin=%d", this, + mInputFrameDataStream->StreamID(), mInputFrameFinal)); + + // Anything prior to AllHeadersReceived() => true is actual headers. After + // that, we need to handle them as trailers instead (which are special-cased + // so we don't have to use the nasty chunked parser for all h2, just in case). + if (mInputFrameDataStream->AllHeadersReceived()) { + LOG3(("Http2Session::ResponseHeadersComplete processing trailers")); + MOZ_ASSERT(mInputFrameFlags & kFlag_END_STREAM); + nsresult rv = mInputFrameDataStream->ConvertResponseTrailers( + &mDecompressor, mDecompressBuffer); + if (NS_FAILED(rv)) { + LOG3(( + "Http2Session::ResponseHeadersComplete trailer conversion failed\n")); + return rv; + } + mFlatHTTPResponseHeadersOut = 0; + mFlatHTTPResponseHeaders.Truncate(); + if (mInputFrameFinal) { + // need to process the fin + ChangeDownstreamState(PROCESSING_COMPLETE_HEADERS); + } else { + ResetDownstreamState(); + } + + return NS_OK; + } + + // if this turns out to be a 1xx response code we have to + // undo the headers received bit that we are setting here. + bool didFirstSetAllRecvd = !mInputFrameDataStream->AllHeadersReceived(); + mInputFrameDataStream->SetAllHeadersReceived(); + + // The stream needs to see flattened http headers + // Uncompressed http/2 format headers currently live in + // Http2Stream::mDecompressBuffer - convert that to HTTP format in + // mFlatHTTPResponseHeaders via ConvertHeaders() + + nsresult rv; + int32_t httpResponseCode; // out param to ConvertResponseHeaders + mFlatHTTPResponseHeadersOut = 0; + rv = mInputFrameDataStream->ConvertResponseHeaders( + &mDecompressor, mDecompressBuffer, mFlatHTTPResponseHeaders, + httpResponseCode); + if (rv == NS_ERROR_NET_RESET) { + LOG( + ("Http2Session::ResponseHeadersComplete %p ConvertResponseHeaders " + "reset\n", + this)); + // This means the stream found connection-oriented auth. Treat this like we + // got a reset with HTTP_1_1_REQUIRED. + mInputFrameDataStream->Transaction()->DisableSpdy(); + CleanupStream(mInputFrameDataStream, NS_ERROR_NET_RESET, CANCEL_ERROR); + ResetDownstreamState(); + return NS_OK; + } else if (NS_FAILED(rv)) { + return rv; + } + + // allow more headers in the case of 1xx + if (((httpResponseCode / 100) == 1) && didFirstSetAllRecvd) { + mInputFrameDataStream->UnsetAllHeadersReceived(); + } + + ChangeDownstreamState(PROCESSING_COMPLETE_HEADERS); + return NS_OK; +} + +nsresult Http2Session::RecvPriority(Http2Session* self) { + MOZ_ASSERT(self->mInputFrameType == FRAME_TYPE_PRIORITY); + + if (self->mInputFrameDataSize != 5) { + LOG3(("Http2Session::RecvPriority %p wrong length data=%d\n", self, + self->mInputFrameDataSize)); + return self->SessionError(PROTOCOL_ERROR); + } + + if (!self->mInputFrameID) { + LOG3(("Http2Session::RecvPriority %p stream ID of 0.\n", self)); + return self->SessionError(PROTOCOL_ERROR); + } + + nsresult rv = self->SetInputFrameDataStream(self->mInputFrameID); + if (NS_FAILED(rv)) return rv; + + uint32_t newPriorityDependency = NetworkEndian::readUint32( + self->mInputFrameBuffer.get() + kFrameHeaderBytes); + bool exclusive = !!(newPriorityDependency & 0x80000000); + newPriorityDependency &= 0x7fffffff; + uint8_t newPriorityWeight = + *(self->mInputFrameBuffer.get() + kFrameHeaderBytes + 4); + + // undefined what it means when the server sends a priority frame. ignore it. + LOG3( + ("Http2Session::RecvPriority %p 0x%X received dependency=0x%X " + "weight=%u exclusive=%d", + self->mInputFrameDataStream, self->mInputFrameID, newPriorityDependency, + newPriorityWeight, exclusive)); + + self->ResetDownstreamState(); + return NS_OK; +} + +nsresult Http2Session::RecvRstStream(Http2Session* self) { + MOZ_ASSERT(self->mInputFrameType == FRAME_TYPE_RST_STREAM); + + if (self->mInputFrameDataSize != 4) { + LOG3(("Http2Session::RecvRstStream %p RST_STREAM wrong length data=%d", + self, self->mInputFrameDataSize)); + return self->SessionError(PROTOCOL_ERROR); + } + + if (!self->mInputFrameID) { + LOG3(("Http2Session::RecvRstStream %p stream ID of 0.\n", self)); + return self->SessionError(PROTOCOL_ERROR); + } + + self->mDownstreamRstReason = NetworkEndian::readUint32( + self->mInputFrameBuffer.get() + kFrameHeaderBytes); + + LOG3(("Http2Session::RecvRstStream %p RST_STREAM Reason Code %u ID %x\n", + self, self->mDownstreamRstReason, self->mInputFrameID)); + + DebugOnly rv = self->SetInputFrameDataStream(self->mInputFrameID); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + if (!self->mInputFrameDataStream) { + // if we can't find the stream just ignore it (4.2 closed) + self->ResetDownstreamState(); + return NS_OK; + } + + self->mInputFrameDataStream->SetRecvdReset(true); + self->MaybeDecrementConcurrent(self->mInputFrameDataStream); + self->ChangeDownstreamState(PROCESSING_CONTROL_RST_STREAM); + return NS_OK; +} + +nsresult Http2Session::RecvSettings(Http2Session* self) { + MOZ_ASSERT(self->mInputFrameType == FRAME_TYPE_SETTINGS); + + if (self->mInputFrameID) { + LOG3(("Http2Session::RecvSettings %p needs stream ID of 0. 0x%X\n", self, + self->mInputFrameID)); + return self->SessionError(PROTOCOL_ERROR); + } + + if (self->mInputFrameDataSize % 6) { + // Number of Settings is determined by dividing by each 6 byte setting + // entry. So the payload must be a multiple of 6. + LOG3(("Http2Session::RecvSettings %p SETTINGS wrong length data=%d", self, + self->mInputFrameDataSize)); + return self->SessionError(PROTOCOL_ERROR); + } + + self->mReceivedSettings = true; + + uint32_t numEntries = self->mInputFrameDataSize / 6; + LOG3( + ("Http2Session::RecvSettings %p SETTINGS Control Frame " + "with %d entries ack=%X", + self, numEntries, self->mInputFrameFlags & kFlag_ACK)); + + if ((self->mInputFrameFlags & kFlag_ACK) && self->mInputFrameDataSize) { + LOG3(("Http2Session::RecvSettings %p ACK with non zero payload is err\n", + self)); + return self->SessionError(PROTOCOL_ERROR); + } + + for (uint32_t index = 0; index < numEntries; ++index) { + uint8_t* setting = + reinterpret_cast(self->mInputFrameBuffer.get()) + + kFrameHeaderBytes + index * 6; + + uint16_t id = NetworkEndian::readUint16(setting); + uint32_t value = NetworkEndian::readUint32(setting + 2); + LOG3(("Settings ID %u, Value %u", id, value)); + + switch (id) { + case SETTINGS_TYPE_HEADER_TABLE_SIZE: + LOG3(("Compression header table setting received: %d\n", value)); + self->mCompressor.SetMaxBufferSize(value); + break; + + case SETTINGS_TYPE_ENABLE_PUSH: + LOG3(("Client received an ENABLE Push SETTING. Odd.\n")); + // nop + break; + + case SETTINGS_TYPE_MAX_CONCURRENT: + self->mMaxConcurrent = value; + Telemetry::Accumulate(Telemetry::SPDY_SETTINGS_MAX_STREAMS, value); + self->ProcessPending(); + break; + + case SETTINGS_TYPE_INITIAL_WINDOW: { + Telemetry::Accumulate(Telemetry::SPDY_SETTINGS_IW, value >> 10); + int32_t delta = value - self->mServerInitialStreamWindow; + self->mServerInitialStreamWindow = value; + + // SETTINGS only adjusts stream windows. Leave the session window alone. + // We need to add the delta to all open streams (delta can be negative) + for (auto iter = self->mStreamTransactionHash.Iter(); !iter.Done(); + iter.Next()) { + iter.Data()->UpdateServerReceiveWindow(delta); + } + } break; + + case SETTINGS_TYPE_MAX_FRAME_SIZE: { + if ((value < kMaxFrameData) || (value >= 0x01000000)) { + LOG3(("Received invalid max frame size 0x%X", value)); + return self->SessionError(PROTOCOL_ERROR); + } + // We stick to the default for simplicity's sake, so nothing to change + } break; + + case SETTINGS_TYPE_ENABLE_CONNECT_PROTOCOL: { + if (value == 1) { + LOG3(("Enabling extended CONNECT")); + self->mPeerAllowsWebsockets = true; + } else if (value > 1) { + LOG3(("Peer sent invalid value for ENABLE_CONNECT_PROTOCOL %d", + value)); + return self->SessionError(PROTOCOL_ERROR); + } else if (self->mPeerAllowsWebsockets) { + LOG3(("Peer tried to re-disable extended CONNECT")); + return self->SessionError(PROTOCOL_ERROR); + } + } break; + + default: + LOG3(("Received an unknown SETTING id %d. Ignoring.", id)); + break; + } + } + + self->ResetDownstreamState(); + + if (!(self->mInputFrameFlags & kFlag_ACK)) { + self->GenerateSettingsAck(); + } else if (self->mWaitingForSettingsAck) { + self->mGoAwayOnPush = true; + } + + if (!self->mProcessedWaitingWebsockets) { + self->ProcessWaitingWebsockets(); + } + + return NS_OK; +} + +nsresult Http2Session::RecvPushPromise(Http2Session* self) { + MOZ_ASSERT(self->mInputFrameType == FRAME_TYPE_PUSH_PROMISE || + self->mInputFrameType == FRAME_TYPE_CONTINUATION); + + // Find out how much padding this frame has, so we can only extract the real + // header data from the frame. + uint16_t paddingLength = 0; + uint8_t paddingControlBytes = 0; + + // If this doesn't have END_PUSH_PROMISE set on it then require the next + // frame to be PUSH_PROMISE of the same ID + uint32_t promiseLen; + uint32_t promisedID; + + if (self->mExpectedPushPromiseID) { + promiseLen = 0; // really a continuation frame + promisedID = self->mContinuedPromiseStream; + } else { + self->mDecompressBuffer.Truncate(); + nsresult rv = self->ParsePadding(paddingControlBytes, paddingLength); + if (NS_FAILED(rv)) { + return rv; + } + promiseLen = 4; + promisedID = + NetworkEndian::readUint32(self->mInputFrameBuffer.get() + + kFrameHeaderBytes + paddingControlBytes); + promisedID &= 0x7fffffff; + if (promisedID <= self->mLastPushedID) { + LOG3(("Http2Session::RecvPushPromise %p ID too low %u expected > %u.\n", + self, promisedID, self->mLastPushedID)); + return self->SessionError(PROTOCOL_ERROR); + } + self->mLastPushedID = promisedID; + } + + uint32_t associatedID = self->mInputFrameID; + + if (self->mInputFrameFlags & kFlag_END_PUSH_PROMISE) { + self->mExpectedPushPromiseID = 0; + self->mContinuedPromiseStream = 0; + } else { + self->mExpectedPushPromiseID = self->mInputFrameID; + self->mContinuedPromiseStream = promisedID; + } + + if ((paddingControlBytes + promiseLen + paddingLength) > + self->mInputFrameDataSize) { + // This is fatal to the session + LOG3( + ("Http2Session::RecvPushPromise %p ID 0x%X assoc ID 0x%X " + "PROTOCOL_ERROR extra %d > frame size %d\n", + self, promisedID, associatedID, + (paddingControlBytes + promiseLen + paddingLength), + self->mInputFrameDataSize)); + return self->SessionError(PROTOCOL_ERROR); + } + + LOG3( + ("Http2Session::RecvPushPromise %p ID 0x%X assoc ID 0x%X " + "paddingLength %d padded %d\n", + self, promisedID, associatedID, paddingLength, + self->mInputFrameFlags & kFlag_PADDED)); + + if (!associatedID || !promisedID || (promisedID & 1)) { + LOG3(("Http2Session::RecvPushPromise %p ID invalid.\n", self)); + return self->SessionError(PROTOCOL_ERROR); + } + + // confirm associated-to + nsresult rv = self->SetInputFrameDataStream(associatedID); + if (NS_FAILED(rv)) return rv; + + Http2Stream* associatedStream = self->mInputFrameDataStream; + ++(self->mServerPushedResources); + + // Anytime we start using the high bit of stream ID (either client or server) + // begin to migrate to a new session. + if (promisedID >= kMaxStreamID) self->mShouldGoAway = true; + + bool resetStream = true; + SpdyPushCache* cache = nullptr; + + if (self->mShouldGoAway && !Http2PushedStream::TestOnPush(associatedStream)) { + LOG3( + ("Http2Session::RecvPushPromise %p cache push while in GoAway " + "mode refused.\n", + self)); + self->GenerateRstStream(REFUSED_STREAM_ERROR, promisedID); + } else if (!gHttpHandler->AllowPush()) { + // ENABLE_PUSH and MAX_CONCURRENT_STREAMS of 0 in settings disabled push + LOG3(("Http2Session::RecvPushPromise Push Recevied when Disabled\n")); + if (self->mGoAwayOnPush) { + LOG3(("Http2Session::RecvPushPromise sending GOAWAY")); + return self->SessionError(PROTOCOL_ERROR); + } + self->GenerateRstStream(REFUSED_STREAM_ERROR, promisedID); + } else if (!(associatedID & 1)) { + LOG3( + ("Http2Session::RecvPushPromise %p assocated=0x%X on pushed (even) " + "stream not allowed\n", + self, associatedID)); + self->GenerateRstStream(PROTOCOL_ERROR, promisedID); + } else if (!associatedStream) { + LOG3(("Http2Session::RecvPushPromise %p lookup associated ID failed.\n", + self)); + self->GenerateRstStream(PROTOCOL_ERROR, promisedID); + } else if (Http2PushedStream::TestOnPush(associatedStream)) { + LOG3(("Http2Session::RecvPushPromise %p will be handled by push listener.", + self)); + resetStream = false; + } else { + nsIRequestContext* requestContext = associatedStream->RequestContext(); + if (requestContext) { + cache = requestContext->GetSpdyPushCache(); + if (!cache) { + cache = new SpdyPushCache(); + requestContext->SetSpdyPushCache(cache); + } + } + if (!cache) { + // this is unexpected, but we can handle it just by refusing the push + LOG3( + ("Http2Session::RecvPushPromise Push Recevied without push cache\n")); + self->GenerateRstStream(REFUSED_STREAM_ERROR, promisedID); + } else { + resetStream = false; + } + } + + if (resetStream) { + // Need to decompress the headers even though we aren't using them yet in + // order to keep the compression context consistent for other frames + self->mDecompressBuffer.Append( + &self->mInputFrameBuffer[kFrameHeaderBytes + paddingControlBytes + + promiseLen], + self->mInputFrameDataSize - paddingControlBytes - promiseLen - + paddingLength); + if (self->mInputFrameFlags & kFlag_END_PUSH_PROMISE) { + rv = self->UncompressAndDiscard(true); + if (NS_FAILED(rv)) { + LOG3(("Http2Session::RecvPushPromise uncompress failed\n")); + self->mGoAwayReason = COMPRESSION_ERROR; + return rv; + } + } + self->ResetDownstreamState(); + return NS_OK; + } + + self->mDecompressBuffer.Append( + &self->mInputFrameBuffer[kFrameHeaderBytes + paddingControlBytes + + promiseLen], + self->mInputFrameDataSize - paddingControlBytes - promiseLen - + paddingLength); + + if (self->mInputFrameType != FRAME_TYPE_CONTINUATION) { + self->mAggregatedHeaderSize = self->mInputFrameDataSize - + paddingControlBytes - promiseLen - + paddingLength; + } else { + self->mAggregatedHeaderSize += self->mInputFrameDataSize - + paddingControlBytes - promiseLen - + paddingLength; + } + + if (!(self->mInputFrameFlags & kFlag_END_PUSH_PROMISE)) { + LOG3( + ("Http2Session::RecvPushPromise not finishing processing for " + "multi-frame push\n")); + self->ResetDownstreamState(); + return NS_OK; + } + + if (self->mInputFrameType == FRAME_TYPE_CONTINUATION) { + Telemetry::Accumulate(Telemetry::SPDY_CONTINUED_HEADERS, + self->mAggregatedHeaderSize); + } + + // Create the buffering transaction and push stream + RefPtr transactionBuffer = + new Http2PushTransactionBuffer(); + transactionBuffer->SetConnection(self); + RefPtr pushedStream(new Http2PushedStream( + transactionBuffer, self, associatedStream, promisedID, + self->mCurrentForegroundTabOuterContentWindowId)); + + rv = pushedStream->ConvertPushHeaders(&self->mDecompressor, + self->mDecompressBuffer, + pushedStream->GetRequestString()); + + if (rv == NS_ERROR_NOT_IMPLEMENTED) { + LOG3(("Http2Session::PushPromise Semantics not Implemented\n")); + self->GenerateRstStream(REFUSED_STREAM_ERROR, promisedID); + self->ResetDownstreamState(); + return NS_OK; + } + + if (rv == NS_ERROR_ILLEGAL_VALUE) { + // This means the decompression completed ok, but there was a problem with + // the decoded headers. Reset the stream and go away. + self->GenerateRstStream(PROTOCOL_ERROR, promisedID); + self->ResetDownstreamState(); + return NS_OK; + } else if (NS_FAILED(rv)) { + // This is fatal to the session. + self->mGoAwayReason = COMPRESSION_ERROR; + return rv; + } + + // Ownership of the pushed stream is by the transaction hash, just as it + // is for a client initiated stream. Errors that aren't fatal to the + // whole session must call cleanupStream() after this point in order + // to remove the stream from that hash. + WeakPtr pushedWeak = pushedStream.get(); + self->mStreamTransactionHash.Put(transactionBuffer, std::move(pushedStream)); + self->mPushedStreams.AppendElement( + static_cast(pushedWeak.get())); + + if (self->RegisterStreamID(pushedWeak, promisedID) == kDeadStreamID) { + LOG3(("Http2Session::RecvPushPromise registerstreamid failed\n")); + self->mGoAwayReason = INTERNAL_ERROR; + return NS_ERROR_FAILURE; + } + + if (promisedID > self->mOutgoingGoAwayID) + self->mOutgoingGoAwayID = promisedID; + + // Fake the request side of the pushed HTTP transaction. Sets up hash + // key and origin + uint32_t notUsed; + Unused << pushedWeak->ReadSegments(nullptr, 1, ¬Used); + + nsAutoCString key; + if (!static_cast(pushedWeak.get())->GetHashKey(key)) { + LOG3( + ("Http2Session::RecvPushPromise one of :authority :scheme :path " + "missing from push\n")); + self->CleanupStream(pushedWeak, NS_ERROR_FAILURE, PROTOCOL_ERROR); + self->ResetDownstreamState(); + return NS_OK; + } + + // does the pushed origin belong on this connection? + LOG3(("Http2Session::RecvPushPromise %p origin check %s", self, + pushedWeak->Origin().get())); + nsCOMPtr pushedOrigin; + rv = Http2Stream::MakeOriginURL(pushedWeak->Origin(), pushedOrigin); + nsAutoCString pushedHostName; + int32_t pushedPort = -1; + if (NS_SUCCEEDED(rv)) { + rv = pushedOrigin->GetHost(pushedHostName); + } + if (NS_SUCCEEDED(rv)) { + rv = pushedOrigin->GetPort(&pushedPort); + if (NS_SUCCEEDED(rv) && pushedPort == -1) { + // Need to get the right default port, so TestJoinConnection below can + // check things correctly. See bug 1397621. + if (pushedOrigin->SchemeIs("http")) { + pushedPort = NS_HTTP_DEFAULT_PORT; + } else { + pushedPort = NS_HTTPS_DEFAULT_PORT; + } + } + } + if (NS_FAILED(rv) || !self->TestJoinConnection(pushedHostName, pushedPort)) { + LOG3(( + "Http2Session::RecvPushPromise %p pushed stream mismatched origin %s\n", + self, pushedWeak->Origin().get())); + self->CleanupStream(pushedWeak, NS_ERROR_FAILURE, REFUSED_STREAM_ERROR); + self->ResetDownstreamState(); + return NS_OK; + } + + if (static_cast(pushedWeak.get())->TryOnPush()) { + LOG3( + ("Http2Session::RecvPushPromise %p channel implements " + "nsIHttpPushListener " + "stream %p will not be placed into session cache.\n", + self, pushedWeak.get())); + } else { + LOG3(("Http2Session::RecvPushPromise %p place stream into session cache\n", + self)); + if (!cache->RegisterPushedStreamHttp2( + key, static_cast(pushedWeak.get()))) { + // This only happens if they've already pushed us this item. + LOG3(("Http2Session::RecvPushPromise registerPushedStream Failed\n")); + self->CleanupStream(pushedWeak, NS_ERROR_FAILURE, REFUSED_STREAM_ERROR); + self->ResetDownstreamState(); + return NS_OK; + } + + // Kick off a lookup into the HTTP cache so we can cancel the push if it's + // unneeded (we already have it in our local regular cache). See bug + // 1367551. + + // Build up our full URL for the cache lookup + nsAutoCString spec; + spec.Assign(pushedWeak->Origin()); + spec.Append(pushedWeak->Path()); + nsCOMPtr pushedURL; + // Nifty trick: this doesn't actually do anything origin-specific, it's just + // named that way. So by passing it the full spec here, we get a URL with + // the full path. + // Another nifty trick! Even though this is using nsIURIs (which are not + // generally ok off the main thread), since we're not using the protocol + // handler to create any URIs, this will work just fine here. Don't try this + // at home, though, kids. I'm a trained professional. + if (NS_SUCCEEDED(Http2Stream::MakeOriginURL(spec, pushedURL))) { + LOG3(("Http2Session::RecvPushPromise %p check disk cache for entry", + self)); + mozilla::OriginAttributes oa; + pushedWeak->GetOriginAttributes(&oa); + RefPtr session = self; + auto cachePushCheckCallback = [session, promisedID](bool aAccepted) { + MOZ_ASSERT(OnSocketThread()); + + if (!aAccepted) { + session->CleanupStream(promisedID, NS_ERROR_FAILURE, + Http2Session::REFUSED_STREAM_ERROR); + } + }; + RefPtr checker = new CachePushChecker( + pushedURL, oa, + static_cast(pushedWeak.get())->GetRequestString(), + std::move(cachePushCheckCallback)); + if (NS_FAILED(checker->DoCheck())) { + LOG3( + ("Http2Session::RecvPushPromise %p failed to open cache entry for " + "push check", + self)); + } + } + } + + if (!pushedWeak) { + // We have an up to date entry in our cache, so the stream was closed + // and released in the cachePushCheckCallback above. + self->ResetDownstreamState(); + return NS_OK; + } + + pushedWeak->SetHTTPState(Http2Stream::RESERVED_BY_REMOTE); + static_assert(Http2Stream::kWorstPriority >= 0, + "kWorstPriority out of range"); + uint32_t priorityDependency = pushedWeak->PriorityDependency(); + uint8_t priorityWeight = pushedWeak->PriorityWeight(); + self->SendPriorityFrame(promisedID, priorityDependency, priorityWeight); + self->ResetDownstreamState(); + return NS_OK; +} + +nsresult Http2Session::RecvPing(Http2Session* self) { + MOZ_ASSERT(self->mInputFrameType == FRAME_TYPE_PING); + + LOG3(("Http2Session::RecvPing %p PING Flags 0x%X.", self, + self->mInputFrameFlags)); + + if (self->mInputFrameDataSize != 8) { + LOG3(("Http2Session::RecvPing %p PING had wrong amount of data %d", self, + self->mInputFrameDataSize)); + return self->SessionError(FRAME_SIZE_ERROR); + } + + if (self->mInputFrameID) { + LOG3(("Http2Session::RecvPing %p PING needs stream ID of 0. 0x%X\n", self, + self->mInputFrameID)); + return self->SessionError(PROTOCOL_ERROR); + } + + if (self->mInputFrameFlags & kFlag_ACK) { + // presumably a reply to our timeout ping.. don't reply to it + self->mPingSentEpoch = 0; + } else { + // reply with a ack'd ping + self->GeneratePing(true); + } + + self->ResetDownstreamState(); + return NS_OK; +} + +nsresult Http2Session::RecvGoAway(Http2Session* self) { + MOZ_ASSERT(self->mInputFrameType == FRAME_TYPE_GOAWAY); + + if (self->mInputFrameDataSize < 8) { + // data > 8 is an opaque token that we can't interpret. NSPR Logs will + // have the hex of all packets so there is no point in separately logging. + LOG3(("Http2Session::RecvGoAway %p GOAWAY had wrong amount of data %d", + self, self->mInputFrameDataSize)); + return self->SessionError(PROTOCOL_ERROR); + } + + if (self->mInputFrameID) { + LOG3(("Http2Session::RecvGoAway %p GOAWAY had non zero stream ID 0x%X\n", + self, self->mInputFrameID)); + return self->SessionError(PROTOCOL_ERROR); + } + + self->mShouldGoAway = true; + self->mGoAwayID = NetworkEndian::readUint32(self->mInputFrameBuffer.get() + + kFrameHeaderBytes); + self->mGoAwayID &= 0x7fffffff; + self->mCleanShutdown = true; + self->mPeerGoAwayReason = NetworkEndian::readUint32( + self->mInputFrameBuffer.get() + kFrameHeaderBytes + 4); + + // Find streams greater than the last-good ID and mark them for deletion + // in the mGoAwayStreamsToRestart queue. The underlying transaction can be + // restarted. + for (auto iter = self->mStreamTransactionHash.Iter(); !iter.Done(); + iter.Next()) { + // these streams were not processed by the server and can be restarted. + // Do that after the enumerator completes to avoid the risk of + // a restart event re-entrantly modifying this hash. Be sure not to restart + // a pushed (even numbered) stream + auto stream = iter.UserData(); + if ((stream->StreamID() > self->mGoAwayID && (stream->StreamID() & 1)) || + !stream->HasRegisteredID()) { + self->mGoAwayStreamsToRestart.Push(stream); + } + } + + // Process the streams marked for deletion and restart. + size_t size = self->mGoAwayStreamsToRestart.GetSize(); + for (size_t count = 0; count < size; ++count) { + Http2Stream* stream = + static_cast(self->mGoAwayStreamsToRestart.PopFront()); + + if (self->mPeerGoAwayReason == HTTP_1_1_REQUIRED) { + stream->Transaction()->DisableSpdy(); + } + self->CloseStream(stream, NS_ERROR_NET_RESET); + if (stream->HasRegisteredID()) + self->mStreamIDHash.Remove(stream->StreamID()); + self->mStreamTransactionHash.Remove(stream->Transaction()); + } + + // Queued streams can also be deleted from this session and restarted + // in another one. (they were never sent on the network so they implicitly + // are not covered by the last-good id. + size = self->mQueuedStreams.GetSize(); + for (size_t count = 0; count < size; ++count) { + Http2Stream* stream = self->mQueuedStreams.PopFront(); + MOZ_ASSERT(stream->Queued()); + stream->SetQueued(false); + if (self->mPeerGoAwayReason == HTTP_1_1_REQUIRED) { + stream->Transaction()->DisableSpdy(); + } + self->CloseStream(stream, NS_ERROR_NET_RESET); + self->mStreamTransactionHash.Remove(stream->Transaction()); + } + + LOG3( + ("Http2Session::RecvGoAway %p GOAWAY Last-Good-ID 0x%X status 0x%X " + "live streams=%d\n", + self, self->mGoAwayID, self->mPeerGoAwayReason, + self->mStreamTransactionHash.Count())); + + self->ResetDownstreamState(); + return NS_OK; +} + +nsresult Http2Session::RecvWindowUpdate(Http2Session* self) { + MOZ_ASSERT(self->mInputFrameType == FRAME_TYPE_WINDOW_UPDATE); + + if (self->mInputFrameDataSize != 4) { + LOG3(("Http2Session::RecvWindowUpdate %p Window Update wrong length %d\n", + self, self->mInputFrameDataSize)); + return self->SessionError(PROTOCOL_ERROR); + } + + uint32_t delta = NetworkEndian::readUint32(self->mInputFrameBuffer.get() + + kFrameHeaderBytes); + delta &= 0x7fffffff; + + LOG3(("Http2Session::RecvWindowUpdate %p len=%d Stream 0x%X.\n", self, delta, + self->mInputFrameID)); + + if (self->mInputFrameID) { // stream window + nsresult rv = self->SetInputFrameDataStream(self->mInputFrameID); + if (NS_FAILED(rv)) return rv; + + if (!self->mInputFrameDataStream) { + LOG3(("Http2Session::RecvWindowUpdate %p lookup streamID 0x%X failed.\n", + self, self->mInputFrameID)); + // only resest the session if the ID is one we haven't ever opened + if (self->mInputFrameID >= self->mNextStreamID) + self->GenerateRstStream(PROTOCOL_ERROR, self->mInputFrameID); + self->ResetDownstreamState(); + return NS_OK; + } + + if (delta == 0) { + LOG3(("Http2Session::RecvWindowUpdate %p received 0 stream window update", + self)); + self->CleanupStream(self->mInputFrameDataStream, NS_ERROR_ILLEGAL_VALUE, + PROTOCOL_ERROR); + self->ResetDownstreamState(); + return NS_OK; + } + + int64_t oldRemoteWindow = + self->mInputFrameDataStream->ServerReceiveWindow(); + self->mInputFrameDataStream->UpdateServerReceiveWindow(delta); + if (self->mInputFrameDataStream->ServerReceiveWindow() >= 0x80000000) { + // a window cannot reach 2^31 and be in compliance. Our calculations + // are 64 bit safe though. + LOG3( + ("Http2Session::RecvWindowUpdate %p stream window " + "exceeds 2^31 - 1\n", + self)); + self->CleanupStream(self->mInputFrameDataStream, NS_ERROR_ILLEGAL_VALUE, + FLOW_CONTROL_ERROR); + self->ResetDownstreamState(); + return NS_OK; + } + + LOG3( + ("Http2Session::RecvWindowUpdate %p stream 0x%X window " + "%" PRId64 " increased by %" PRIu32 " now %" PRId64 ".\n", + self, self->mInputFrameID, oldRemoteWindow, delta, + oldRemoteWindow + delta)); + + } else { // session window update + if (delta == 0) { + LOG3( + ("Http2Session::RecvWindowUpdate %p received 0 session window update", + self)); + return self->SessionError(PROTOCOL_ERROR); + } + + int64_t oldRemoteWindow = self->mServerSessionWindow; + self->mServerSessionWindow += delta; + + if (self->mServerSessionWindow >= 0x80000000) { + // a window cannot reach 2^31 and be in compliance. Our calculations + // are 64 bit safe though. + LOG3( + ("Http2Session::RecvWindowUpdate %p session window " + "exceeds 2^31 - 1\n", + self)); + return self->SessionError(FLOW_CONTROL_ERROR); + } + + if ((oldRemoteWindow <= 0) && (self->mServerSessionWindow > 0)) { + LOG3( + ("Http2Session::RecvWindowUpdate %p restart session window\n", self)); + for (auto iter = self->mStreamTransactionHash.Iter(); !iter.Done(); + iter.Next()) { + MOZ_ASSERT(self->mServerSessionWindow > 0); + + auto stream = iter.UserData(); + if (!stream->BlockedOnRwin() || stream->ServerReceiveWindow() <= 0) { + continue; + } + + self->mReadyForWrite.Push(stream); + self->SetWriteCallbacks(); + } + } + LOG3( + ("Http2Session::RecvWindowUpdate %p session window " + "%" PRId64 " increased by %d now %" PRId64 ".\n", + self, oldRemoteWindow, delta, oldRemoteWindow + delta)); + } + + self->ResetDownstreamState(); + return NS_OK; +} + +nsresult Http2Session::RecvContinuation(Http2Session* self) { + MOZ_ASSERT(self->mInputFrameType == FRAME_TYPE_CONTINUATION); + MOZ_ASSERT(self->mInputFrameID); + MOZ_ASSERT(self->mExpectedPushPromiseID || self->mExpectedHeaderID); + MOZ_ASSERT(!(self->mExpectedPushPromiseID && self->mExpectedHeaderID)); + + LOG3( + ("Http2Session::RecvContinuation %p Flags 0x%X id 0x%X " + "promise id 0x%X header id 0x%X\n", + self, self->mInputFrameFlags, self->mInputFrameID, + self->mExpectedPushPromiseID, self->mExpectedHeaderID)); + + DebugOnly rv = self->SetInputFrameDataStream(self->mInputFrameID); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + if (!self->mInputFrameDataStream) { + LOG3(("Http2Session::RecvContination stream ID 0x%X not found.", + self->mInputFrameID)); + return self->SessionError(PROTOCOL_ERROR); + } + + // continued headers + if (self->mExpectedHeaderID) { + self->mInputFrameFlags &= ~kFlag_PRIORITY; + return RecvHeaders(self); + } + + // continued push promise + if (self->mInputFrameFlags & kFlag_END_HEADERS) { + self->mInputFrameFlags &= ~kFlag_END_HEADERS; + self->mInputFrameFlags |= kFlag_END_PUSH_PROMISE; + } + return RecvPushPromise(self); +} + +class UpdateAltSvcEvent : public Runnable { + public: + UpdateAltSvcEvent(const nsCString& header, const nsCString& aOrigin, + nsHttpConnectionInfo* aCI, nsIInterfaceRequestor* callbacks) + : Runnable("net::UpdateAltSvcEvent"), + mHeader(header), + mOrigin(aOrigin), + mCI(aCI), + mCallbacks(callbacks) {} + + NS_IMETHOD Run() override { + MOZ_ASSERT(NS_IsMainThread()); + + nsCString originScheme; + nsCString originHost; + int32_t originPort = -1; + + nsCOMPtr uri; + if (NS_FAILED(NS_NewURI(getter_AddRefs(uri), mOrigin))) { + LOG(("UpdateAltSvcEvent origin does not parse %s\n", mOrigin.get())); + return NS_OK; + } + uri->GetScheme(originScheme); + uri->GetHost(originHost); + uri->GetPort(&originPort); + + if (XRE_IsSocketProcess()) { + AltServiceChild::ProcessHeader( + mHeader, originScheme, originHost, originPort, mCI->GetUsername(), + mCI->GetTopWindowOrigin(), mCI->GetPrivate(), mCI->GetIsolated(), + mCallbacks, mCI->ProxyInfo(), 0, mCI->GetOriginAttributes()); + return NS_OK; + } + + AltSvcMapping::ProcessHeader( + mHeader, originScheme, originHost, originPort, mCI->GetUsername(), + mCI->GetTopWindowOrigin(), mCI->GetPrivate(), mCI->GetIsolated(), + nullptr, mCI->ProxyInfo(), 0, mCI->GetOriginAttributes()); + return NS_OK; + } + + private: + nsCString mHeader; + nsCString mOrigin; + RefPtr mCI; + nsCOMPtr mCallbacks; +}; + +// defined as an http2 extension - alt-svc +// defines receipt of frame type 0x0A.. See AlternateSevices.h at least draft +// -06 sec 4 as this is an extension, never generate protocol error - just +// ignore problems +nsresult Http2Session::RecvAltSvc(Http2Session* self) { + MOZ_ASSERT(self->mInputFrameType == FRAME_TYPE_ALTSVC); + LOG3(("Http2Session::RecvAltSvc %p Flags 0x%X id 0x%X\n", self, + self->mInputFrameFlags, self->mInputFrameID)); + + if (self->mInputFrameDataSize < 2) { + LOG3(("Http2Session::RecvAltSvc %p frame too small", self)); + self->ResetDownstreamState(); + return NS_OK; + } + + uint16_t originLen = NetworkEndian::readUint16(self->mInputFrameBuffer.get() + + kFrameHeaderBytes); + if (originLen + 2U > self->mInputFrameDataSize) { + LOG3(("Http2Session::RecvAltSvc %p origin len too big for frame", self)); + self->ResetDownstreamState(); + return NS_OK; + } + + if (!gHttpHandler->AllowAltSvc()) { + LOG3(("Http2Session::RecvAltSvc %p frame alt service pref'd off", self)); + self->ResetDownstreamState(); + return NS_OK; + } + + uint16_t altSvcFieldValueLen = + static_cast(self->mInputFrameDataSize) - 2U - originLen; + LOG3(( + "Http2Session::RecvAltSvc %p frame originLen=%u altSvcFieldValueLen=%u\n", + self, originLen, altSvcFieldValueLen)); + + if (self->mInputFrameDataSize > 2000) { + LOG3(("Http2Session::RecvAltSvc %p frame too large to parse sensibly", + self)); + self->ResetDownstreamState(); + return NS_OK; + } + + nsAutoCString origin; + bool impliedOrigin = true; + if (originLen) { + origin.Assign(self->mInputFrameBuffer.get() + kFrameHeaderBytes + 2, + originLen); + impliedOrigin = false; + } + + nsAutoCString altSvcFieldValue; + if (altSvcFieldValueLen) { + altSvcFieldValue.Assign( + self->mInputFrameBuffer.get() + kFrameHeaderBytes + 2 + originLen, + altSvcFieldValueLen); + } + + if (altSvcFieldValue.IsEmpty() || + !nsHttp::IsReasonableHeaderValue(altSvcFieldValue)) { + LOG( + ("Http2Session %p Alt-Svc Response Header seems unreasonable - " + "skipping\n", + self)); + self->ResetDownstreamState(); + return NS_OK; + } + + if (self->mInputFrameID & 1) { + // pulled streams apply to the origin of the pulled stream. + // If the origin field is filled in the frame, the frame should be ignored + if (!origin.IsEmpty()) { + LOG(("Http2Session %p Alt-Svc pulled stream has non empty origin\n", + self)); + self->ResetDownstreamState(); + return NS_OK; + } + + if (NS_FAILED(self->SetInputFrameDataStream(self->mInputFrameID)) || + !self->mInputFrameDataStream || + !self->mInputFrameDataStream->Transaction() || + !self->mInputFrameDataStream->Transaction()->RequestHead()) { + LOG3( + ("Http2Session::RecvAltSvc %p got frame w/o origin on invalid stream", + self)); + self->ResetDownstreamState(); + return NS_OK; + } + + self->mInputFrameDataStream->Transaction()->RequestHead()->Origin(origin); + } else if (!self->mInputFrameID) { + // ID 0 streams must supply their own origin + if (origin.IsEmpty()) { + LOG(("Http2Session %p Alt-Svc Stream 0 has empty origin\n", self)); + self->ResetDownstreamState(); + return NS_OK; + } + } else { + // handling of push streams is not defined. Let's ignore it + LOG(("Http2Session %p Alt-Svc received on pushed stream - ignoring\n", + self)); + self->ResetDownstreamState(); + return NS_OK; + } + + RefPtr ci(self->ConnectionInfo()); + if (!self->mConnection || !ci) { + LOG3(("Http2Session::RecvAltSvc %p no connection or conninfo for %d", self, + self->mInputFrameID)); + self->ResetDownstreamState(); + return NS_OK; + } + + if (!impliedOrigin) { + bool okToReroute = true; + nsCOMPtr securityInfo; + self->mConnection->GetSecurityInfo(getter_AddRefs(securityInfo)); + nsCOMPtr ssl = do_QueryInterface(securityInfo); + if (!ssl) { + okToReroute = false; + } + + // a little off main thread origin parser. This is a non critical function + // because any alternate route created has to be verified anyhow + nsAutoCString specifiedOriginHost; + if (origin.EqualsIgnoreCase("https://", 8)) { + specifiedOriginHost.Assign(origin.get() + 8, origin.Length() - 8); + } else if (origin.EqualsIgnoreCase("http://", 7)) { + specifiedOriginHost.Assign(origin.get() + 7, origin.Length() - 7); + } + + int32_t colonOffset = specifiedOriginHost.FindCharInSet(":", 0); + if (colonOffset != kNotFound) { + specifiedOriginHost.Truncate(colonOffset); + } + + if (okToReroute) { + ssl->IsAcceptableForHost(specifiedOriginHost, &okToReroute); + } + + if (!okToReroute) { + LOG3( + ("Http2Session::RecvAltSvc %p can't reroute non-authoritative origin " + "%s", + self, origin.BeginReading())); + self->ResetDownstreamState(); + return NS_OK; + } + } + + nsCOMPtr callbacks; + self->mConnection->GetSecurityInfo(getter_AddRefs(callbacks)); + nsCOMPtr irCallbacks = do_QueryInterface(callbacks); + + RefPtr event = + new UpdateAltSvcEvent(altSvcFieldValue, origin, ci, irCallbacks); + NS_DispatchToMainThread(event); + self->ResetDownstreamState(); + return NS_OK; +} + +void Http2Session::Received421(nsHttpConnectionInfo* ci) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG3(("Http2Session::Recevied421 %p %d\n", this, mOriginFrameActivated)); + if (!mOriginFrameActivated || !ci) { + return; + } + + nsAutoCString key(ci->GetOrigin()); + key.Append(':'); + key.AppendInt(ci->OriginPort()); + mOriginFrame.Remove(key); + LOG3(("Http2Session::Received421 %p key %s removed\n", this, key.get())); +} + +nsresult Http2Session::RecvUnused(Http2Session* self) { + LOG3(("Http2Session %p unknown frame type %x ignored\n", self, + self->mInputFrameType)); + self->ResetDownstreamState(); + return NS_OK; +} + +// defined as an http2 extension - origin +// defines receipt of frame type 0x0b.. +// http://httpwg.org/http-extensions/origin-frame.html as this is an extension, +// never generate protocol error - just ignore problems +nsresult Http2Session::RecvOrigin(Http2Session* self) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + MOZ_ASSERT(self->mInputFrameType == FRAME_TYPE_ORIGIN); + LOG3(("Http2Session::RecvOrigin %p Flags 0x%X id 0x%X\n", self, + self->mInputFrameFlags, self->mInputFrameID)); + + if (self->mInputFrameFlags & 0x0F) { + LOG3(("Http2Session::RecvOrigin %p leading flags must be 0", self)); + self->ResetDownstreamState(); + return NS_OK; + } + + if (self->mInputFrameID) { + LOG3(("Http2Session::RecvOrigin %p not stream 0", self)); + self->ResetDownstreamState(); + return NS_OK; + } + + if (self->ConnectionInfo()->UsingProxy()) { + LOG3(("Http2Session::RecvOrigin %p must not use proxy", self)); + self->ResetDownstreamState(); + return NS_OK; + } + + if (!gHttpHandler->AllowOriginExtension()) { + LOG3(("Http2Session::RecvOrigin %p origin extension pref'd off", self)); + self->ResetDownstreamState(); + return NS_OK; + } + + uint32_t offset = 0; + self->mOriginFrameActivated = true; + + while (self->mInputFrameDataSize >= (offset + 2U)) { + uint16_t originLen = NetworkEndian::readUint16( + self->mInputFrameBuffer.get() + kFrameHeaderBytes + offset); + LOG3(("Http2Session::RecvOrigin %p origin extension defined as %d bytes\n", + self, originLen)); + if (originLen + 2U + offset > self->mInputFrameDataSize) { + LOG3(("Http2Session::RecvOrigin %p origin len too big for frame", self)); + break; + } + + nsAutoCString originString; + nsCOMPtr originURL; + originString.Assign( + self->mInputFrameBuffer.get() + kFrameHeaderBytes + offset + 2, + originLen); + offset += originLen + 2; + if (NS_FAILED(Http2Stream::MakeOriginURL(originString, originURL))) { + LOG3( + ("Http2Session::RecvOrigin %p origin frame string %s failed to " + "parse\n", + self, originString.get())); + continue; + } + + LOG3(("Http2Session::RecvOrigin %p origin frame string %s parsed OK\n", + self, originString.get())); + if (!originURL->SchemeIs("https")) { + LOG3(("Http2Session::RecvOrigin %p origin frame not https\n", self)); + continue; + } + + int32_t port = -1; + originURL->GetPort(&port); + if (port == -1) { + port = 443; + } + // dont use ->GetHostPort because we want explicit 443 + nsAutoCString host; + originURL->GetHost(host); + nsAutoCString key(host); + key.Append(':'); + key.AppendInt(port); + if (!self->mOriginFrame.Get(key)) { + self->mOriginFrame.Put(key, true); + RefPtr conn(self->HttpConnection()); + MOZ_ASSERT(conn.get()); + gHttpHandler->ConnMgr()->RegisterOriginCoalescingKey(conn, host, port); + } else { + LOG3(("Http2Session::RecvOrigin %p origin frame already in set\n", self)); + } + } + + self->ResetDownstreamState(); + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsAHttpTransaction. It is expected that nsHttpConnection is the caller +// of these methods +//----------------------------------------------------------------------------- + +void Http2Session::OnTransportStatus(nsITransport* aTransport, nsresult aStatus, + int64_t aProgress) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + switch (aStatus) { + // These should appear only once, deliver to the first + // transaction on the session. + case NS_NET_STATUS_RESOLVING_HOST: + case NS_NET_STATUS_RESOLVED_HOST: + case NS_NET_STATUS_CONNECTING_TO: + case NS_NET_STATUS_CONNECTED_TO: + case NS_NET_STATUS_TLS_HANDSHAKE_STARTING: + case NS_NET_STATUS_TLS_HANDSHAKE_ENDED: { + if (!mFirstHttpTransaction) { + // if we still do not have a HttpTransaction store timings info in + // a HttpConnection. + // If some error occur it can happen that we do not have a connection. + if (mConnection) { + RefPtr conn = mConnection->HttpConnection(); + conn->SetEvent(aStatus); + } + } else { + mFirstHttpTransaction->OnTransportStatus(aTransport, aStatus, + aProgress); + } + + if (aStatus == NS_NET_STATUS_TLS_HANDSHAKE_ENDED) { + mFirstHttpTransaction = nullptr; + mTlsHandshakeFinished = true; + } + break; + } + + default: + // The other transport events are ignored here because there is no good + // way to map them to the right transaction in http/2. Instead, the events + // are generated again from the http/2 code and passed directly to the + // correct transaction. + + // NS_NET_STATUS_SENDING_TO: + // This is generated by the socket transport when (part) of + // a transaction is written out + // + // There is no good way to map it to the right transaction in http/2, + // so it is ignored here and generated separately when the request + // is sent from Http2Stream::TransmitFrame + + // NS_NET_STATUS_WAITING_FOR: + // Created by nsHttpConnection when the request has been totally sent. + // There is no good way to map it to the right transaction in http/2, + // so it is ignored here and generated separately when the same + // condition is complete in Http2Stream when there is no more + // request body left to be transmitted. + + // NS_NET_STATUS_RECEIVING_FROM + // Generated in session whenever we read a data frame or a HEADERS + // that can be attributed to a particular stream/transaction + + break; + } +} + +// ReadSegments() is used to write data to the network. Generally, HTTP +// request data is pulled from the approriate transaction and +// converted to http/2 data. Sometimes control data like window-update are +// generated instead. + +nsresult Http2Session::ReadSegmentsAgain(nsAHttpSegmentReader* reader, + uint32_t count, uint32_t* countRead, + bool* again) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + MOZ_ASSERT(!mSegmentReader || !reader || (mSegmentReader == reader), + "Inconsistent Write Function Callback"); + + nsresult rv = ConfirmTLSProfile(); + if (NS_FAILED(rv)) { + if (mGoAwayReason == INADEQUATE_SECURITY) { + LOG3( + ("Http2Session::ReadSegments %p returning INADEQUATE_SECURITY " + "%" PRIx32, + this, static_cast(NS_ERROR_NET_INADEQUATE_SECURITY))); + rv = NS_ERROR_NET_INADEQUATE_SECURITY; + } + return rv; + } + + if (reader) mSegmentReader = reader; + + *countRead = 0; + + LOG3(("Http2Session::ReadSegments %p", this)); + + Http2Stream* stream = mReadyForWrite.PopFront(); + if (!stream) { + LOG3(("Http2Session %p could not identify a stream to write; suspending.", + this)); + uint32_t availBeforeFlush = mOutputQueueUsed - mOutputQueueSent; + FlushOutputQueue(); + uint32_t availAfterFlush = mOutputQueueUsed - mOutputQueueSent; + if (availBeforeFlush != availAfterFlush) { + LOG3(("Http2Session %p ResumeRecv After early flush in ReadSegments", + this)); + Unused << ResumeRecv(); + } + SetWriteCallbacks(); + if (mAttemptingEarlyData) { + // We can still try to send our preamble as early-data + *countRead = mOutputQueueUsed - mOutputQueueSent; + } + return *countRead ? NS_OK : NS_BASE_STREAM_WOULD_BLOCK; + } + + uint32_t earlyDataUsed = 0; + if (mAttemptingEarlyData) { + if (!stream->Do0RTT()) { + LOG3(("Http2Session %p will not get early data from Http2Stream %p 0x%X", + this, stream, stream->StreamID())); + FlushOutputQueue(); + SetWriteCallbacks(); + if (!mCannotDo0RTTStreams.Contains(stream)) { + mCannotDo0RTTStreams.AppendElement(stream); + } + // We can still send our preamble + *countRead = mOutputQueueUsed - mOutputQueueSent; + return *countRead ? NS_OK : NS_BASE_STREAM_WOULD_BLOCK; + } + + // Need to adjust this to only take as much as we can fit in with the + // preamble/settings/priority stuff + count -= (mOutputQueueUsed - mOutputQueueSent); + + // Keep track of this to add it into countRead later, as + // stream->ReadSegments will likely change the value of mOutputQueueUsed. + earlyDataUsed = mOutputQueueUsed - mOutputQueueSent; + } + + LOG3( + ("Http2Session %p will write from Http2Stream %p 0x%X " + "block-input=%d block-output=%d\n", + this, stream, stream->StreamID(), stream->RequestBlockedOnRead(), + stream->BlockedOnRwin())); + + rv = stream->ReadSegments(this, count, countRead); + + if (earlyDataUsed) { + // Do this here because countRead could get reset somewhere down the rabbit + // hole of stream->ReadSegments, and we want to make sure we return the + // proper value to our caller. + *countRead += earlyDataUsed; + } + + if (mAttemptingEarlyData && !m0RTTStreams.Contains(stream)) { + LOG3(("Http2Session::ReadSegmentsAgain adding stream %d to m0RTTStreams\n", + stream->StreamID())); + m0RTTStreams.AppendElement(stream); + } + + // Not every permutation of stream->ReadSegents produces data (and therefore + // tries to flush the output queue) - SENDING_FIN_STREAM can be an example + // of that. But we might still have old data buffered that would be good + // to flush. + FlushOutputQueue(); + + // Allow new server reads - that might be data or control information + // (e.g. window updates or http replies) that are responses to these writes + Unused << ResumeRecv(); + + if (stream->RequestBlockedOnRead()) { + // We are blocked waiting for input - either more http headers or + // any request body data. When more data from the request stream + // becomes available the httptransaction will call conn->ResumeSend(). + + LOG3(("Http2Session::ReadSegments %p dealing with block on read", this)); + + // call readsegments again if there are other streams ready + // to run in this session + if (GetWriteQueueSize()) { + rv = NS_OK; + } else { + rv = NS_BASE_STREAM_WOULD_BLOCK; + } + SetWriteCallbacks(); + return rv; + } + + if (NS_FAILED(rv)) { + LOG3(("Http2Session::ReadSegments %p may return FAIL code %" PRIX32, this, + static_cast(rv))); + if (rv == NS_BASE_STREAM_WOULD_BLOCK) { + return rv; + } + + CleanupStream(stream, rv, CANCEL_ERROR); + if (SoftStreamError(rv)) { + LOG3(("Http2Session::ReadSegments %p soft error override\n", this)); + *again = false; + SetWriteCallbacks(); + rv = NS_OK; + } + return rv; + } + + if (*countRead > 0) { + LOG3(("Http2Session::ReadSegments %p stream=%p countread=%d", this, stream, + *countRead)); + mReadyForWrite.Push(stream); + SetWriteCallbacks(); + return rv; + } + + if (stream->BlockedOnRwin()) { + LOG3(("Http2Session %p will stream %p 0x%X suspended for flow control\n", + this, stream, stream->StreamID())); + return NS_BASE_STREAM_WOULD_BLOCK; + } + + LOG3(("Http2Session::ReadSegments %p stream=%p stream send complete", this, + stream)); + + // call readsegments again if there are other streams ready + // to go in this session + SetWriteCallbacks(); + + return rv; +} + +nsresult Http2Session::ReadSegments(nsAHttpSegmentReader* reader, + uint32_t count, uint32_t* countRead) { + bool again = false; + return ReadSegmentsAgain(reader, count, countRead, &again); +} + +nsresult Http2Session::ReadyToProcessDataFrame( + enum internalStateType newState) { + MOZ_ASSERT(newState == PROCESSING_DATA_FRAME || + newState == DISCARDING_DATA_FRAME_PADDING); + ChangeDownstreamState(newState); + + Telemetry::Accumulate(Telemetry::SPDY_CHUNK_RECVD, mInputFrameDataSize >> 10); + mLastDataReadEpoch = mLastReadEpoch; + + if (!mInputFrameID) { + LOG3(("Http2Session::ReadyToProcessDataFrame %p data frame stream 0\n", + this)); + return SessionError(PROTOCOL_ERROR); + } + + nsresult rv = SetInputFrameDataStream(mInputFrameID); + if (NS_FAILED(rv)) { + LOG3( + ("Http2Session::ReadyToProcessDataFrame %p lookup streamID 0x%X " + "failed. probably due to verification.\n", + this, mInputFrameID)); + return rv; + } + if (!mInputFrameDataStream) { + LOG3( + ("Http2Session::ReadyToProcessDataFrame %p lookup streamID 0x%X " + "failed. Next = 0x%X", + this, mInputFrameID, mNextStreamID)); + if (mInputFrameID >= mNextStreamID) + GenerateRstStream(PROTOCOL_ERROR, mInputFrameID); + ChangeDownstreamState(DISCARDING_DATA_FRAME); + } else if (mInputFrameDataStream->RecvdFin() || + mInputFrameDataStream->RecvdReset() || + mInputFrameDataStream->SentReset()) { + LOG3( + ("Http2Session::ReadyToProcessDataFrame %p streamID 0x%X " + "Data arrived for already server closed stream.\n", + this, mInputFrameID)); + if (mInputFrameDataStream->RecvdFin() || + mInputFrameDataStream->RecvdReset()) + GenerateRstStream(STREAM_CLOSED_ERROR, mInputFrameID); + ChangeDownstreamState(DISCARDING_DATA_FRAME); + } else if (mInputFrameDataSize == 0 && !mInputFrameFinal) { + // Only if non-final because the stream properly handles final frames of any + // size, and we want the stream to be able to notice its own end flag. + LOG3( + ("Http2Session::ReadyToProcessDataFrame %p streamID 0x%X " + "Ignoring 0-length non-terminal data frame.", + this, mInputFrameID)); + ChangeDownstreamState(DISCARDING_DATA_FRAME); + } + + LOG3( + ("Start Processing Data Frame. " + "Session=%p Stream ID 0x%X Stream Ptr %p Fin=%d Len=%d", + this, mInputFrameID, mInputFrameDataStream, mInputFrameFinal, + mInputFrameDataSize)); + UpdateLocalRwin(mInputFrameDataStream, mInputFrameDataSize); + + if (mInputFrameDataStream) { + mInputFrameDataStream->SetRecvdData(true); + } + + return NS_OK; +} + +// WriteSegments() is used to read data off the socket. Generally this is +// just the http2 frame header and from there the appropriate *Stream +// is identified from the Stream-ID. The http transaction associated with +// that read then pulls in the data directly, which it will feed to +// OnWriteSegment(). That function will gateway it into http and feed +// it to the appropriate transaction. + +// we call writer->OnWriteSegment via NetworkRead() to get a http2 header.. +// and decide if it is data or control.. if it is control, just deal with it. +// if it is data, identify the stream +// call stream->WriteSegments which can call this::OnWriteSegment to get the +// data. It always gets full frames if they are part of the stream + +nsresult Http2Session::WriteSegmentsAgain(nsAHttpSegmentWriter* writer, + uint32_t count, + uint32_t* countWritten, bool* again) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + LOG3(("Http2Session::WriteSegments %p InternalState %X\n", this, + mDownstreamState)); + + *countWritten = 0; + + if (mClosed) return NS_ERROR_FAILURE; + + nsresult rv = ConfirmTLSProfile(); + if (NS_FAILED(rv)) return rv; + + SetWriteCallbacks(); + + // If there are http transactions attached to a push stream with filled + // buffers trigger that data pump here. This only reads from buffers (not the + // network) so mDownstreamState doesn't matter. + Http2Stream* pushConnectedStream = mPushesReadyForRead.PopFront(); + if (pushConnectedStream) { + return ProcessConnectedPush(pushConnectedStream, writer, count, + countWritten); + } + + // feed gecko channels that previously stopped consuming data + // only take data from stored buffers + Http2Stream* slowConsumer = mSlowConsumersReadyForRead.PopFront(); + if (slowConsumer) { + internalStateType savedState = mDownstreamState; + mDownstreamState = NOT_USING_NETWORK; + rv = ProcessSlowConsumer(slowConsumer, writer, count, countWritten); + mDownstreamState = savedState; + return rv; + } + + // The BUFFERING_OPENING_SETTINGS state is just like any + // BUFFERING_FRAME_HEADER except the only frame type it will allow is SETTINGS + + // The session layer buffers the leading 8 byte header of every frame. + // Non-Data frames are then buffered for their full length, but data + // frames (type 0) are passed through to the http stack unprocessed + + if (mDownstreamState == BUFFERING_OPENING_SETTINGS || + mDownstreamState == BUFFERING_FRAME_HEADER) { + // The first 9 bytes of every frame is header information that + // we are going to want to strip before passing to http. That is + // true of both control and data packets. + + MOZ_ASSERT(mInputFrameBufferUsed < kFrameHeaderBytes, + "Frame Buffer Used Too Large for State"); + + rv = NetworkRead(writer, &mInputFrameBuffer[mInputFrameBufferUsed], + kFrameHeaderBytes - mInputFrameBufferUsed, countWritten); + + if (NS_FAILED(rv)) { + LOG3(("Http2Session %p buffering frame header read failure %" PRIx32 "\n", + this, static_cast(rv))); + // maybe just blocked reading from network + if (rv == NS_BASE_STREAM_WOULD_BLOCK) rv = NS_OK; + return rv; + } + + LogIO(this, nullptr, "Reading Frame Header", + &mInputFrameBuffer[mInputFrameBufferUsed], *countWritten); + + mInputFrameBufferUsed += *countWritten; + + if (mInputFrameBufferUsed < kFrameHeaderBytes) { + LOG3( + ("Http2Session::WriteSegments %p " + "BUFFERING FRAME HEADER incomplete size=%d", + this, mInputFrameBufferUsed)); + return rv; + } + + // 3 bytes of length, 1 type byte, 1 flag byte, 1 unused bit, 31 bits of ID + uint8_t totallyWastedByte = mInputFrameBuffer.get()[0]; + mInputFrameDataSize = + NetworkEndian::readUint16(mInputFrameBuffer.get() + 1); + if (totallyWastedByte || (mInputFrameDataSize > kMaxFrameData)) { + LOG3(("Got frame too large 0x%02X%04X", totallyWastedByte, + mInputFrameDataSize)); + return SessionError(PROTOCOL_ERROR); + } + mInputFrameType = *reinterpret_cast(mInputFrameBuffer.get() + + kFrameLengthBytes); + mInputFrameFlags = *reinterpret_cast( + mInputFrameBuffer.get() + kFrameLengthBytes + kFrameTypeBytes); + mInputFrameID = + NetworkEndian::readUint32(mInputFrameBuffer.get() + kFrameLengthBytes + + kFrameTypeBytes + kFrameFlagBytes); + mInputFrameID &= 0x7fffffff; + mInputFrameDataRead = 0; + + if (mInputFrameType == FRAME_TYPE_DATA || + mInputFrameType == FRAME_TYPE_HEADERS) { + mInputFrameFinal = mInputFrameFlags & kFlag_END_STREAM; + } else { + mInputFrameFinal = false; + } + + mPaddingLength = 0; + + LOG3(("Http2Session::WriteSegments[%p::%" PRIu64 "] Frame Header Read " + "type %X data len %u flags %x id 0x%X", + this, mSerial, mInputFrameType, mInputFrameDataSize, mInputFrameFlags, + mInputFrameID)); + + // if mExpectedHeaderID is non 0, it means this frame must be a CONTINUATION + // of a HEADERS frame with a matching ID (section 6.2) + if (mExpectedHeaderID && ((mInputFrameType != FRAME_TYPE_CONTINUATION) || + (mExpectedHeaderID != mInputFrameID))) { + LOG3( + ("Expected CONINUATION OF HEADERS for ID 0x%X\n", mExpectedHeaderID)); + return SessionError(PROTOCOL_ERROR); + } + + // if mExpectedPushPromiseID is non 0, it means this frame must be a + // CONTINUATION of a PUSH_PROMISE with a matching ID (section 6.2) + if (mExpectedPushPromiseID && + ((mInputFrameType != FRAME_TYPE_CONTINUATION) || + (mExpectedPushPromiseID != mInputFrameID))) { + LOG3(("Expected CONTINUATION of PUSH PROMISE for ID 0x%X\n", + mExpectedPushPromiseID)); + return SessionError(PROTOCOL_ERROR); + } + + if (mDownstreamState == BUFFERING_OPENING_SETTINGS && + mInputFrameType != FRAME_TYPE_SETTINGS) { + LOG3(("First Frame Type Must Be Settings\n")); + mPeerFailedHandshake = true; + + // Don't allow any more h2 connections to this host + RefPtr ci = ConnectionInfo(); + if (ci) { + gHttpHandler->ExcludeHttp2(ci); + } + + // Go through and re-start all of our transactions with h2 disabled. + for (auto iter = mStreamTransactionHash.Iter(); !iter.Done(); + iter.Next()) { + auto stream = iter.UserData(); + stream->Transaction()->DisableSpdy(); + CloseStream(stream, NS_ERROR_NET_RESET); + } + mStreamTransactionHash.Clear(); + return SessionError(PROTOCOL_ERROR); + } + + if (mInputFrameType != FRAME_TYPE_DATA) { // control frame + EnsureBuffer(mInputFrameBuffer, mInputFrameDataSize + kFrameHeaderBytes, + kFrameHeaderBytes, mInputFrameBufferSize); + ChangeDownstreamState(BUFFERING_CONTROL_FRAME); + } else if (mInputFrameFlags & kFlag_PADDED) { + ChangeDownstreamState(PROCESSING_DATA_FRAME_PADDING_CONTROL); + } else { + rv = ReadyToProcessDataFrame(PROCESSING_DATA_FRAME); + if (NS_FAILED(rv)) { + return rv; + } + } + } + + if (mDownstreamState == PROCESSING_DATA_FRAME_PADDING_CONTROL) { + MOZ_ASSERT(mInputFrameFlags & kFlag_PADDED, + "Processing padding control on unpadded frame"); + + MOZ_ASSERT(mInputFrameBufferUsed < (kFrameHeaderBytes + 1), + "Frame buffer used too large for state"); + + rv = NetworkRead(writer, &mInputFrameBuffer[mInputFrameBufferUsed], + (kFrameHeaderBytes + 1) - mInputFrameBufferUsed, + countWritten); + + if (NS_FAILED(rv)) { + LOG3( + ("Http2Session %p buffering data frame padding control read failure " + "%" PRIx32 "\n", + this, static_cast(rv))); + // maybe just blocked reading from network + if (rv == NS_BASE_STREAM_WOULD_BLOCK) rv = NS_OK; + return rv; + } + + LogIO(this, nullptr, "Reading Data Frame Padding Control", + &mInputFrameBuffer[mInputFrameBufferUsed], *countWritten); + + mInputFrameBufferUsed += *countWritten; + + if (mInputFrameBufferUsed - kFrameHeaderBytes < 1) { + LOG3( + ("Http2Session::WriteSegments %p " + "BUFFERING DATA FRAME CONTROL PADDING incomplete size=%d", + this, mInputFrameBufferUsed - 8)); + return rv; + } + + ++mInputFrameDataRead; + + char* control = &mInputFrameBuffer[kFrameHeaderBytes]; + mPaddingLength = static_cast(*control); + + LOG3(("Http2Session::WriteSegments %p stream 0x%X mPaddingLength=%d", this, + mInputFrameID, mPaddingLength)); + + if (1U + mPaddingLength > mInputFrameDataSize) { + LOG3( + ("Http2Session::WriteSegments %p stream 0x%X padding too large for " + "frame", + this, mInputFrameID)); + return SessionError(PROTOCOL_ERROR); + } else if (1U + mPaddingLength == mInputFrameDataSize) { + // This frame consists entirely of padding, we can just discard it + LOG3( + ("Http2Session::WriteSegments %p stream 0x%X frame with only padding", + this, mInputFrameID)); + rv = ReadyToProcessDataFrame(DISCARDING_DATA_FRAME_PADDING); + if (NS_FAILED(rv)) { + return rv; + } + } else { + LOG3( + ("Http2Session::WriteSegments %p stream 0x%X ready to read HTTP data", + this, mInputFrameID)); + rv = ReadyToProcessDataFrame(PROCESSING_DATA_FRAME); + if (NS_FAILED(rv)) { + return rv; + } + } + } + + if (mDownstreamState == PROCESSING_CONTROL_RST_STREAM) { + nsresult streamCleanupCode; + + // There is no bounds checking on the error code.. we provide special + // handling for a couple of cases and all others (including unknown) are + // equivalent to cancel. + if (mDownstreamRstReason == REFUSED_STREAM_ERROR) { + streamCleanupCode = NS_ERROR_NET_RESET; // can retry this 100% safely + mInputFrameDataStream->Transaction()->ReuseConnectionOnRestartOK(true); + } else if (mDownstreamRstReason == HTTP_1_1_REQUIRED) { + streamCleanupCode = NS_ERROR_NET_RESET; + mInputFrameDataStream->Transaction()->ReuseConnectionOnRestartOK(true); + mInputFrameDataStream->Transaction()->DisableSpdy(); + mInputFrameDataStream->Transaction() + ->MakeNonSticky(); // actully allow restart by unsticking + } else { + streamCleanupCode = mInputFrameDataStream->RecvdData() + ? NS_ERROR_NET_PARTIAL_TRANSFER + : NS_ERROR_NET_INTERRUPT; + } + + if (mDownstreamRstReason == COMPRESSION_ERROR) { + mShouldGoAway = true; + } + + // mInputFrameDataStream is reset by ChangeDownstreamState + Http2Stream* stream = mInputFrameDataStream; + ResetDownstreamState(); + LOG3( + ("Http2Session::WriteSegments cleanup stream on recv of rst " + "session=%p stream=%p 0x%X\n", + this, stream, stream ? stream->StreamID() : 0)); + CleanupStream(stream, streamCleanupCode, CANCEL_ERROR); + return NS_OK; + } + + if (mDownstreamState == PROCESSING_DATA_FRAME || + mDownstreamState == PROCESSING_COMPLETE_HEADERS) { + // The cleanup stream should only be set while stream->WriteSegments is + // on the stack and then cleaned up in this code block afterwards. + MOZ_ASSERT(!mNeedsCleanup, "cleanup stream set unexpectedly"); + mNeedsCleanup = nullptr; /* just in case */ + + uint32_t streamID = mInputFrameDataStream->StreamID(); + mSegmentWriter = writer; + rv = mInputFrameDataStream->WriteSegments(this, count, countWritten); + mSegmentWriter = nullptr; + + mLastDataReadEpoch = mLastReadEpoch; + + if (SoftStreamError(rv)) { + // This will happen when the transaction figures out it is EOF, generally + // due to a content-length match being made. Return OK from this function + // otherwise the whole session would be torn down. + + // if we were doing PROCESSING_COMPLETE_HEADERS need to pop the state + // back to PROCESSING_DATA_FRAME where we came from + mDownstreamState = PROCESSING_DATA_FRAME; + + if (mInputFrameDataRead == mInputFrameDataSize) ResetDownstreamState(); + LOG3( + ("Http2Session::WriteSegments session=%p id 0x%X " + "needscleanup=%p. cleanup stream based on " + "stream->writeSegments returning code %" PRIx32 "\n", + this, streamID, mNeedsCleanup, static_cast(rv))); + MOZ_ASSERT(!mNeedsCleanup || mNeedsCleanup->StreamID() == streamID); + CleanupStream( + streamID, + (rv == NS_BINDING_RETARGETED) ? NS_BINDING_RETARGETED : NS_OK, + CANCEL_ERROR); + mNeedsCleanup = nullptr; + *again = false; + rv = ResumeRecv(); + if (NS_FAILED(rv)) { + LOG3(("ResumeRecv returned code %x", static_cast(rv))); + } + return NS_OK; + } + + if (mNeedsCleanup) { + LOG3( + ("Http2Session::WriteSegments session=%p stream=%p 0x%X " + "cleanup stream based on mNeedsCleanup.\n", + this, mNeedsCleanup, mNeedsCleanup ? mNeedsCleanup->StreamID() : 0)); + CleanupStream(mNeedsCleanup, NS_OK, CANCEL_ERROR); + mNeedsCleanup = nullptr; + } + + if (NS_FAILED(rv)) { + LOG3(("Http2Session %p data frame read failure %" PRIx32 "\n", this, + static_cast(rv))); + // maybe just blocked reading from network + if (rv == NS_BASE_STREAM_WOULD_BLOCK) rv = NS_OK; + } + + return rv; + } + + if (mDownstreamState == DISCARDING_DATA_FRAME || + mDownstreamState == DISCARDING_DATA_FRAME_PADDING) { + char trash[4096]; + uint32_t discardCount = + std::min(mInputFrameDataSize - mInputFrameDataRead, 4096U); + LOG3(("Http2Session::WriteSegments %p trying to discard %d bytes of %s", + this, discardCount, + mDownstreamState == DISCARDING_DATA_FRAME ? "data" : "padding")); + + if (!discardCount && mDownstreamState == DISCARDING_DATA_FRAME) { + // Only do this short-cirtuit if we're not discarding a pure padding + // frame, as we need to potentially handle the stream FIN in those cases. + // See bug 1381016 comment 36 for more details. + ResetDownstreamState(); + Unused << ResumeRecv(); + return NS_BASE_STREAM_WOULD_BLOCK; + } + + rv = NetworkRead(writer, trash, discardCount, countWritten); + + if (NS_FAILED(rv)) { + LOG3(("Http2Session %p discard frame read failure %" PRIx32 "\n", this, + static_cast(rv))); + // maybe just blocked reading from network + if (rv == NS_BASE_STREAM_WOULD_BLOCK) rv = NS_OK; + return rv; + } + + LogIO(this, nullptr, "Discarding Frame", trash, *countWritten); + + mInputFrameDataRead += *countWritten; + + if (mInputFrameDataRead == mInputFrameDataSize) { + Http2Stream* streamToCleanup = nullptr; + if (mInputFrameFinal) { + streamToCleanup = mInputFrameDataStream; + } + + bool discardedPadding = + (mDownstreamState == DISCARDING_DATA_FRAME_PADDING); + ResetDownstreamState(); + + if (streamToCleanup) { + if (discardedPadding && !(streamToCleanup->StreamID() & 1)) { + // Pushed streams are special on padding-only final data frames. + // See bug 1409570 comments 6-8 for details. + streamToCleanup->SetPushComplete(); + Http2Stream* pushSink = streamToCleanup->GetConsumerStream(); + if (pushSink) { + bool enqueueSink = true; + for (auto s : mPushesReadyForRead) { + if (s == pushSink) { + enqueueSink = false; + break; + } + } + if (enqueueSink) { + mPushesReadyForRead.Push(pushSink); + // No use trying to clean up, it won't do anything, anyway + streamToCleanup = nullptr; + } + } + } + CleanupStream(streamToCleanup, NS_OK, CANCEL_ERROR); + } + } + return rv; + } + + if (mDownstreamState != BUFFERING_CONTROL_FRAME) { + MOZ_ASSERT(false); // this cannot happen + return NS_ERROR_UNEXPECTED; + } + + MOZ_ASSERT(mInputFrameBufferUsed == kFrameHeaderBytes, + "Frame Buffer Header Not Present"); + MOZ_ASSERT(mInputFrameDataSize + kFrameHeaderBytes <= mInputFrameBufferSize, + "allocation for control frame insufficient"); + + rv = NetworkRead(writer, + &mInputFrameBuffer[kFrameHeaderBytes + mInputFrameDataRead], + mInputFrameDataSize - mInputFrameDataRead, countWritten); + + if (NS_FAILED(rv)) { + LOG3(("Http2Session %p buffering control frame read failure %" PRIx32 "\n", + this, static_cast(rv))); + // maybe just blocked reading from network + if (rv == NS_BASE_STREAM_WOULD_BLOCK) rv = NS_OK; + return rv; + } + + LogIO(this, nullptr, "Reading Control Frame", + &mInputFrameBuffer[kFrameHeaderBytes + mInputFrameDataRead], + *countWritten); + + mInputFrameDataRead += *countWritten; + + if (mInputFrameDataRead != mInputFrameDataSize) return NS_OK; + + MOZ_ASSERT(mInputFrameType != FRAME_TYPE_DATA); + if (mInputFrameType < FRAME_TYPE_LAST) { + rv = sControlFunctions[mInputFrameType](this); + } else { + // Section 4.1 requires this to be ignored; though protocol_error would + // be better + LOG3(("Http2Session %p unknown frame type %x ignored\n", this, + mInputFrameType)); + ResetDownstreamState(); + rv = NS_OK; + } + + MOZ_ASSERT(NS_FAILED(rv) || mDownstreamState != BUFFERING_CONTROL_FRAME, + "Control Handler returned OK but did not change state"); + + if (mShouldGoAway && !mStreamTransactionHash.Count()) Close(NS_OK); + return rv; +} + +nsresult Http2Session::WriteSegments(nsAHttpSegmentWriter* writer, + uint32_t count, uint32_t* countWritten) { + bool again = false; + return WriteSegmentsAgain(writer, count, countWritten, &again); +} + +nsresult Http2Session::Finish0RTT(bool aRestart, bool aAlpnChanged) { + MOZ_ASSERT(mAttemptingEarlyData); + LOG3(("Http2Session::Finish0RTT %p aRestart=%d aAlpnChanged=%d", this, + aRestart, aAlpnChanged)); + + for (size_t i = 0; i < m0RTTStreams.Length(); ++i) { + if (m0RTTStreams[i]) { + m0RTTStreams[i]->Finish0RTT(aRestart, aAlpnChanged); + } + } + + if (aRestart) { + // 0RTT failed + if (aAlpnChanged) { + // This is a slightly more involved case - we need to get all our streams/ + // transactions back in the queue so they can restart as http/1 + + // These must be set this way to ensure we gracefully restart all streams + mGoAwayID = 0; + mCleanShutdown = true; + + // Close takes care of the rest of our work for us. The reason code here + // doesn't matter, as we aren't actually going to send a GOAWAY frame, but + // we use NS_ERROR_NET_RESET as it's closest to the truth. + Close(NS_ERROR_NET_RESET); + } else { + // This is the easy case - early data failed, but we're speaking h2, so + // we just need to rewind to the beginning of the preamble and try again. + mOutputQueueSent = 0; + + for (size_t i = 0; i < mCannotDo0RTTStreams.Length(); ++i) { + if (mCannotDo0RTTStreams[i] && VerifyStream(mCannotDo0RTTStreams[i])) { + TransactionHasDataToWrite(mCannotDo0RTTStreams[i]); + } + } + } + } else { + // 0RTT succeeded + for (size_t i = 0; i < mCannotDo0RTTStreams.Length(); ++i) { + if (mCannotDo0RTTStreams[i] && VerifyStream(mCannotDo0RTTStreams[i])) { + TransactionHasDataToWrite(mCannotDo0RTTStreams[i]); + } + } + // Make sure we look for any incoming data in repsonse to our early data. + Unused << ResumeRecv(); + } + + mAttemptingEarlyData = false; + m0RTTStreams.Clear(); + mCannotDo0RTTStreams.Clear(); + RealignOutputQueue(); + + return NS_OK; +} + +void Http2Session::SetFastOpenStatus(uint8_t aStatus) { + LOG3(("Http2Session::SetFastOpenStatus %d [this=%p]", aStatus, this)); + + for (size_t i = 0; i < m0RTTStreams.Length(); ++i) { + if (m0RTTStreams[i]) { + m0RTTStreams[i]->Transaction()->SetFastOpenStatus(aStatus); + } + } +} + +nsresult Http2Session::ProcessConnectedPush(Http2Stream* pushConnectedStream, + nsAHttpSegmentWriter* writer, + uint32_t count, + uint32_t* countWritten) { + LOG3(("Http2Session::ProcessConnectedPush %p 0x%X\n", this, + pushConnectedStream->StreamID())); + mSegmentWriter = writer; + nsresult rv = pushConnectedStream->WriteSegments(this, count, countWritten); + mSegmentWriter = nullptr; + + // The pipe in nsHttpTransaction rewrites CLOSED error codes into OK + // so we need this check to determine the truth. + if (NS_SUCCEEDED(rv) && !*countWritten && pushConnectedStream->PushSource() && + pushConnectedStream->PushSource()->GetPushComplete()) { + rv = NS_BASE_STREAM_CLOSED; + } + + if (rv == NS_BASE_STREAM_CLOSED) { + CleanupStream(pushConnectedStream, NS_OK, CANCEL_ERROR); + rv = NS_OK; + } + + // if we return OK to nsHttpConnection it will use mSocketInCondition + // to determine whether to schedule more reads, incorrectly + // assuming that nsHttpConnection::OnSocketWrite() was called. + if (NS_SUCCEEDED(rv) || rv == NS_BASE_STREAM_WOULD_BLOCK) { + rv = NS_BASE_STREAM_WOULD_BLOCK; + Unused << ResumeRecv(); + } + return rv; +} + +nsresult Http2Session::ProcessSlowConsumer(Http2Stream* slowConsumer, + nsAHttpSegmentWriter* writer, + uint32_t count, + uint32_t* countWritten) { + LOG3(("Http2Session::ProcessSlowConsumer %p 0x%X\n", this, + slowConsumer->StreamID())); + mSegmentWriter = writer; + nsresult rv = slowConsumer->WriteSegments(this, count, countWritten); + mSegmentWriter = nullptr; + LOG3(("Http2Session::ProcessSlowConsumer Writesegments %p 0x%X rv %" PRIX32 + " %d\n", + this, slowConsumer->StreamID(), static_cast(rv), + *countWritten)); + if (NS_SUCCEEDED(rv) && !*countWritten && slowConsumer->RecvdFin()) { + rv = NS_BASE_STREAM_CLOSED; + } + + if (NS_SUCCEEDED(rv) && (*countWritten > 0)) { + // There have been buffered bytes successfully fed into the + // formerly blocked consumer. Repeat until buffer empty or + // consumer is blocked again. + UpdateLocalRwin(slowConsumer, 0); + ConnectSlowConsumer(slowConsumer); + } + + if (rv == NS_BASE_STREAM_CLOSED) { + CleanupStream(slowConsumer, NS_OK, CANCEL_ERROR); + rv = NS_OK; + } + + return rv; +} + +void Http2Session::UpdateLocalStreamWindow(Http2Stream* stream, + uint32_t bytes) { + if (!stream) // this is ok - it means there was a data frame for a rst stream + return; + + // If this data packet was not for a valid or live stream then there + // is no reason to mess with the flow control + if (!stream || stream->RecvdFin() || stream->RecvdReset() || + mInputFrameFinal) { + return; + } + + stream->DecrementClientReceiveWindow(bytes); + + // Don't necessarily ack every data packet. Only do it + // after a significant amount of data. + uint64_t unacked = stream->LocalUnAcked(); + int64_t localWindow = stream->ClientReceiveWindow(); + + LOG3( + ("Http2Session::UpdateLocalStreamWindow this=%p id=0x%X newbytes=%u " + "unacked=%" PRIu64 " localWindow=%" PRId64 "\n", + this, stream->StreamID(), bytes, unacked, localWindow)); + + if (!unacked) return; + + if ((unacked < kMinimumToAck) && (localWindow > kEmergencyWindowThreshold)) + return; + + if (!stream->HasSink()) { + LOG3( + ("Http2Session::UpdateLocalStreamWindow %p 0x%X Pushed Stream Has No " + "Sink\n", + this, stream->StreamID())); + return; + } + + // Generate window updates directly out of session instead of the stream + // in order to avoid queue delays in getting the 'ACK' out. + uint32_t toack = (unacked <= 0x7fffffffU) ? unacked : 0x7fffffffU; + + LOG3( + ("Http2Session::UpdateLocalStreamWindow Ack this=%p id=0x%X acksize=%d\n", + this, stream->StreamID(), toack)); + stream->IncrementClientReceiveWindow(toack); + if (toack == 0) { + // Ensure we never send an illegal 0 window update + return; + } + + // room for this packet needs to be ensured before calling this function + char* packet = mOutputQueueBuffer.get() + mOutputQueueUsed; + mOutputQueueUsed += kFrameHeaderBytes + 4; + MOZ_ASSERT(mOutputQueueUsed <= mOutputQueueSize); + + CreateFrameHeader(packet, 4, FRAME_TYPE_WINDOW_UPDATE, 0, stream->StreamID()); + NetworkEndian::writeUint32(packet + kFrameHeaderBytes, toack); + + LogIO(this, stream, "Stream Window Update", packet, kFrameHeaderBytes + 4); + // dont flush here, this write can commonly be coalesced with a + // session window update to immediately follow. +} + +void Http2Session::UpdateLocalSessionWindow(uint32_t bytes) { + if (!bytes) return; + + mLocalSessionWindow -= bytes; + + LOG3( + ("Http2Session::UpdateLocalSessionWindow this=%p newbytes=%u " + "localWindow=%" PRId64 "\n", + this, bytes, mLocalSessionWindow)); + + // Don't necessarily ack every data packet. Only do it + // after a significant amount of data. + if ((mLocalSessionWindow > (mInitialRwin - kMinimumToAck)) && + (mLocalSessionWindow > kEmergencyWindowThreshold)) + return; + + // Only send max bits of window updates at a time. + uint64_t toack64 = mInitialRwin - mLocalSessionWindow; + uint32_t toack = (toack64 <= 0x7fffffffU) ? toack64 : 0x7fffffffU; + + LOG3(("Http2Session::UpdateLocalSessionWindow Ack this=%p acksize=%u\n", this, + toack)); + mLocalSessionWindow += toack; + + if (toack == 0) { + // Ensure we never send an illegal 0 window update + return; + } + + // room for this packet needs to be ensured before calling this function + char* packet = mOutputQueueBuffer.get() + mOutputQueueUsed; + mOutputQueueUsed += kFrameHeaderBytes + 4; + MOZ_ASSERT(mOutputQueueUsed <= mOutputQueueSize); + + CreateFrameHeader(packet, 4, FRAME_TYPE_WINDOW_UPDATE, 0, 0); + NetworkEndian::writeUint32(packet + kFrameHeaderBytes, toack); + + LogIO(this, nullptr, "Session Window Update", packet, kFrameHeaderBytes + 4); + // dont flush here, this write can commonly be coalesced with others +} + +void Http2Session::UpdateLocalRwin(Http2Stream* stream, uint32_t bytes) { + // make sure there is room for 2 window updates even though + // we may not generate any. + EnsureOutputBuffer(2 * (kFrameHeaderBytes + 4)); + + UpdateLocalStreamWindow(stream, bytes); + UpdateLocalSessionWindow(bytes); + FlushOutputQueue(); +} + +void Http2Session::Close(nsresult aReason) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + if (mClosed) return; + + LOG3(("Http2Session::Close %p %" PRIX32, this, + static_cast(aReason))); + + mClosed = true; + + Shutdown(); + + mStreamIDHash.Clear(); + mStreamTransactionHash.Clear(); + + // If we have any websocket transactions waiting for settings frame, + // reinitiate them. This can happend if we close a h2 connection before the + // settings frame is received. + if (mWaitingWebsockets.Length()) { + MOZ_ASSERT(!mProcessedWaitingWebsockets); + MOZ_ASSERT(mWaitingWebsockets.Length() == + mWaitingWebsocketCallbacks.Length()); + + mProcessedWaitingWebsockets = true; + for (size_t i = 0; i < mWaitingWebsockets.Length(); ++i) { + RefPtr httpTransaction = mWaitingWebsockets[i]; + LOG3(("Http2Session::Close %p Re-queuing websocket.", this)); + httpTransaction->SetConnection(nullptr); + nsHttpTransaction* trans = httpTransaction->QueryHttpTransaction(); + if (trans) { + nsresult rv = + gHttpHandler->InitiateTransaction(trans, trans->Priority()); + if (NS_FAILED(rv)) { + LOG3( + ("Http2Session::Close %p failed to reinitiate websocket " + "transaction (%08x).\n", + this, static_cast(rv))); + } + } else { + LOG3(("Http2Session::Close %p missing transaction?!", this)); + } + } + mWaitingWebsockets.Clear(); + mWaitingWebsocketCallbacks.Clear(); + } + + uint32_t goAwayReason; + if (mGoAwayReason != NO_HTTP_ERROR) { + goAwayReason = mGoAwayReason; + } else if (NS_SUCCEEDED(aReason)) { + goAwayReason = NO_HTTP_ERROR; + } else if (aReason == NS_ERROR_NET_HTTP2_SENT_GOAWAY) { + goAwayReason = PROTOCOL_ERROR; + } else if (mCleanShutdown) { + goAwayReason = NO_HTTP_ERROR; + } else { + goAwayReason = INTERNAL_ERROR; + } + if (!mAttemptingEarlyData) { + GenerateGoAway(goAwayReason); + } + mConnection = nullptr; + mSegmentReader = nullptr; + mSegmentWriter = nullptr; +} + +nsHttpConnectionInfo* Http2Session::ConnectionInfo() { + RefPtr ci; + GetConnectionInfo(getter_AddRefs(ci)); + return ci.get(); +} + +void Http2Session::CloseTransaction(nsAHttpTransaction* aTransaction, + nsresult aResult) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG3(("Http2Session::CloseTransaction %p %p %" PRIx32, this, aTransaction, + static_cast(aResult))); + + // Generally this arrives as a cancel event from the connection manager. + + // need to find the stream and call CleanupStream() on it. + RefPtr stream = mStreamTransactionHash.Get(aTransaction); + if (!stream) { + LOG3(("Http2Session::CloseTransaction %p %p %" PRIx32 " - not found.", this, + aTransaction, static_cast(aResult))); + return; + } + LOG3( + ("Http2Session::CloseTransaction probably a cancel. " + "this=%p, trans=%p, result=%" PRIx32 ", streamID=0x%X stream=%p", + this, aTransaction, static_cast(aResult), stream->StreamID(), + stream.get())); + CleanupStream(stream, aResult, CANCEL_ERROR); + nsresult rv = ResumeRecv(); + if (NS_FAILED(rv)) { + LOG3(("Http2Session::CloseTransaction %p %p %x ResumeRecv returned %x", + this, aTransaction, static_cast(aResult), + static_cast(rv))); + } +} + +//----------------------------------------------------------------------------- +// nsAHttpSegmentReader +//----------------------------------------------------------------------------- + +nsresult Http2Session::OnReadSegment(const char* buf, uint32_t count, + uint32_t* countRead) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + nsresult rv; + + // If we can release old queued data then we can try and write the new + // data directly to the network without using the output queue at all + if (mOutputQueueUsed) FlushOutputQueue(); + + if (!mOutputQueueUsed && mSegmentReader) { + // try and write directly without output queue + rv = mSegmentReader->OnReadSegment(buf, count, countRead); + + if (rv == NS_BASE_STREAM_WOULD_BLOCK) { + *countRead = 0; + } else if (NS_FAILED(rv)) { + return rv; + } + + if (*countRead < count) { + uint32_t required = count - *countRead; + // assuming a commitment() happened, this ensurebuffer is a nop + // but just in case the queuesize is too small for the required data + // call ensurebuffer(). + EnsureBuffer(mOutputQueueBuffer, required, 0, mOutputQueueSize); + memcpy(mOutputQueueBuffer.get(), buf + *countRead, required); + mOutputQueueUsed = required; + } + + *countRead = count; + return NS_OK; + } + + // At this point we are going to buffer the new data in the output + // queue if it fits. By coalescing multiple small submissions into one larger + // buffer we can get larger writes out to the network later on. + + // This routine should not be allowed to fill up the output queue + // all on its own - at least kQueueReserved bytes are always left + // for other routines to use - but this is an all-or-nothing function, + // so if it will not all fit just return WOULD_BLOCK + + if ((mOutputQueueUsed + count) > (mOutputQueueSize - kQueueReserved)) + return NS_BASE_STREAM_WOULD_BLOCK; + + memcpy(mOutputQueueBuffer.get() + mOutputQueueUsed, buf, count); + mOutputQueueUsed += count; + *countRead = count; + + FlushOutputQueue(); + + return NS_OK; +} + +nsresult Http2Session::CommitToSegmentSize(uint32_t count, + bool forceCommitment) { + if (mOutputQueueUsed && !mAttemptingEarlyData) FlushOutputQueue(); + + // would there be enough room to buffer this if needed? + if ((mOutputQueueUsed + count) <= (mOutputQueueSize - kQueueReserved)) + return NS_OK; + + // if we are using part of our buffers already, try again later unless + // forceCommitment is set. + if (mOutputQueueUsed && !forceCommitment) return NS_BASE_STREAM_WOULD_BLOCK; + + if (mOutputQueueUsed) { + // normally we avoid the memmove of RealignOutputQueue, but we'll try + // it if forceCommitment is set before growing the buffer. + RealignOutputQueue(); + + // is there enough room now? + if ((mOutputQueueUsed + count) <= (mOutputQueueSize - kQueueReserved)) + return NS_OK; + } + + // resize the buffers as needed + EnsureOutputBuffer(count + kQueueReserved); + + MOZ_ASSERT((mOutputQueueUsed + count) <= (mOutputQueueSize - kQueueReserved), + "buffer not as large as expected"); + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsAHttpSegmentWriter +//----------------------------------------------------------------------------- + +nsresult Http2Session::OnWriteSegment(char* buf, uint32_t count, + uint32_t* countWritten) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + nsresult rv; + + if (!mSegmentWriter) { + // the only way this could happen would be if Close() were called on the + // stack with WriteSegments() + return NS_ERROR_FAILURE; + } + + if (mDownstreamState == NOT_USING_NETWORK || + mDownstreamState == BUFFERING_FRAME_HEADER || + mDownstreamState == DISCARDING_DATA_FRAME_PADDING) { + return NS_BASE_STREAM_WOULD_BLOCK; + } + + if (mDownstreamState == PROCESSING_DATA_FRAME) { + if (mInputFrameFinal && mInputFrameDataRead == mInputFrameDataSize) { + *countWritten = 0; + SetNeedsCleanup(); + return NS_BASE_STREAM_CLOSED; + } + + count = std::min(count, mInputFrameDataSize - mInputFrameDataRead); + rv = NetworkRead(mSegmentWriter, buf, count, countWritten); + if (NS_FAILED(rv)) return rv; + + LogIO(this, mInputFrameDataStream, "Reading Data Frame", buf, + *countWritten); + + mInputFrameDataRead += *countWritten; + if (mPaddingLength && + (mInputFrameDataSize - mInputFrameDataRead <= mPaddingLength)) { + // We are crossing from real HTTP data into the realm of padding. If + // we've actually crossed the line, we need to munge countWritten for the + // sake of goodness and sanity. No matter what, any future calls to + // WriteSegments need to just discard data until we reach the end of this + // frame. + if (mInputFrameDataSize != mInputFrameDataRead) { + // Only change state if we still have padding to read. If we don't do + // this, we can end up hanging on frames that combine real data, + // padding, and END_STREAM (see bug 1019921) + ChangeDownstreamState(DISCARDING_DATA_FRAME_PADDING); + } + uint32_t paddingRead = + mPaddingLength - (mInputFrameDataSize - mInputFrameDataRead); + LOG3( + ("Http2Session::OnWriteSegment %p stream 0x%X len=%d read=%d " + "crossed from HTTP data into padding (%d of %d) countWritten=%d", + this, mInputFrameID, mInputFrameDataSize, mInputFrameDataRead, + paddingRead, mPaddingLength, *countWritten)); + *countWritten -= paddingRead; + LOG3(("Http2Session::OnWriteSegment %p stream 0x%X new countWritten=%d", + this, mInputFrameID, *countWritten)); + } + + mInputFrameDataStream->UpdateTransportReadEvents(*countWritten); + if ((mInputFrameDataRead == mInputFrameDataSize) && !mInputFrameFinal) + ResetDownstreamState(); + + return rv; + } + + if (mDownstreamState == PROCESSING_COMPLETE_HEADERS) { + if (mFlatHTTPResponseHeaders.Length() == mFlatHTTPResponseHeadersOut && + mInputFrameFinal) { + *countWritten = 0; + SetNeedsCleanup(); + return NS_BASE_STREAM_CLOSED; + } + + count = std::min( + count, mFlatHTTPResponseHeaders.Length() - mFlatHTTPResponseHeadersOut); + memcpy(buf, mFlatHTTPResponseHeaders.get() + mFlatHTTPResponseHeadersOut, + count); + mFlatHTTPResponseHeadersOut += count; + *countWritten = count; + + if (mFlatHTTPResponseHeaders.Length() == mFlatHTTPResponseHeadersOut) { + if (!mInputFrameFinal) { + // If more frames are expected in this stream, then reset the state so + // they can be handled. Otherwise (e.g. a 0 length response with the fin + // on the incoming headers) stay in PROCESSING_COMPLETE_HEADERS state so + // the SetNeedsCleanup() code above can cleanup the stream. + ResetDownstreamState(); + } + } + + return NS_OK; + } + + MOZ_ASSERT(false); + return NS_ERROR_UNEXPECTED; +} + +void Http2Session::SetNeedsCleanup() { + LOG3( + ("Http2Session::SetNeedsCleanup %p - recorded downstream fin of " + "stream %p 0x%X", + this, mInputFrameDataStream, mInputFrameDataStream->StreamID())); + + // This will result in Close() being called + MOZ_ASSERT(!mNeedsCleanup, "mNeedsCleanup unexpectedly set"); + mInputFrameDataStream->SetResponseIsComplete(); + mNeedsCleanup = mInputFrameDataStream; + ResetDownstreamState(); +} + +void Http2Session::ConnectPushedStream(Http2Stream* stream) { + mPushesReadyForRead.Push(stream); + Unused << ForceRecv(); +} + +void Http2Session::ConnectSlowConsumer(Http2Stream* stream) { + LOG3(("Http2Session::ConnectSlowConsumer %p 0x%X\n", this, + stream->StreamID())); + mSlowConsumersReadyForRead.Push(stream); + Unused << ForceRecv(); +} + +uint32_t Http2Session::FindTunnelCount(nsHttpConnectionInfo* aConnInfo) { + return FindTunnelCount(aConnInfo->HashKey()); +} +uint32_t Http2Session::FindTunnelCount(nsCString const& aHashKey) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + uint32_t rv = 0; + mTunnelHash.Get(aHashKey, &rv); + return rv; +} + +void Http2Session::RegisterTunnel(Http2Stream* aTunnel) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + nsCString const& regKey = aTunnel->RegistrationKey(); + uint32_t newcount = FindTunnelCount(regKey) + 1; + mTunnelHash.Remove(regKey); + mTunnelHash.Put(regKey, newcount); + LOG3(("Http2Stream::RegisterTunnel %p stream=%p tunnels=%d [%s]", this, + aTunnel, newcount, regKey.get())); +} + +void Http2Session::UnRegisterTunnel(Http2Stream* aTunnel) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + nsCString const& regKey = aTunnel->RegistrationKey(); + MOZ_ASSERT(FindTunnelCount(regKey)); + uint32_t newcount = FindTunnelCount(regKey) - 1; + mTunnelHash.Remove(regKey); + if (newcount) { + mTunnelHash.Put(regKey, newcount); + } + LOG3(("Http2Session::UnRegisterTunnel %p stream=%p tunnels=%d [%s]", this, + aTunnel, newcount, regKey.get())); +} + +void Http2Session::CreateTunnel(nsHttpTransaction* trans, + nsHttpConnectionInfo* ci, + nsIInterfaceRequestor* aCallbacks) { + LOG(("Http2Session::CreateTunnel %p %p make new tunnel\n", this, trans)); + // The connect transaction will hold onto the underlying http + // transaction so that an auth created by the connect can be mappped + // to the correct security callbacks + + RefPtr clone(ci->Clone()); + RefPtr connectTrans = new SpdyConnectTransaction( + clone, aCallbacks, trans->Caps(), trans, this, false); + DebugOnly rv = + AddStream(connectTrans, nsISupportsPriority::PRIORITY_NORMAL, false, + false, nullptr); + MOZ_ASSERT(rv); + RefPtr tunnel = mStreamTransactionHash.Get(connectTrans); + MOZ_ASSERT(tunnel); + RegisterTunnel(tunnel); +} + +void Http2Session::DispatchOnTunnel(nsAHttpTransaction* aHttpTransaction, + nsIInterfaceRequestor* aCallbacks) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + nsHttpTransaction* trans = aHttpTransaction->QueryHttpTransaction(); + nsHttpConnectionInfo* ci = aHttpTransaction->ConnectionInfo(); + MOZ_ASSERT(trans); + + LOG3(("Http2Session::DispatchOnTunnel %p trans=%p", this, trans)); + + aHttpTransaction->SetConnection(nullptr); + + // this transaction has done its work of setting up a tunnel, let + // the connection manager queue it if necessary + trans->SetTunnelProvider(this); + trans->EnableKeepAlive(); + + if (FindTunnelCount(ci) < gHttpHandler->MaxConnectionsPerOrigin()) { + LOG3(("Http2Session::DispatchOnTunnel %p create on new tunnel %s", this, + ci->HashKey().get())); + CreateTunnel(trans, ci, aCallbacks); + } else { + // requeue it. The connection manager is responsible for actually putting + // this on the tunnel connection with the specific ci. If that can't + // happen the cmgr checks with us via MaybeReTunnel() to see if it should + // make a new tunnel or just wait longer. + LOG3( + ("Http2Session::DispatchOnTunnel %p trans=%p queue in connection " + "manager", + this, trans)); + nsresult rv = gHttpHandler->InitiateTransaction(trans, trans->Priority()); + if (NS_FAILED(rv)) { + LOG3( + ("Http2Session::DispatchOnTunnel %p trans=%p failed to initiate " + "transaction (%08x)", + this, trans, static_cast(rv))); + } + } +} + +// From ASpdySession +bool Http2Session::MaybeReTunnel(nsAHttpTransaction* aHttpTransaction) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + nsHttpTransaction* trans = aHttpTransaction->QueryHttpTransaction(); + LOG(("Http2Session::MaybeReTunnel %p trans=%p\n", this, trans)); + if (!trans || trans->TunnelProvider() != this) { + // this isn't really one of our transactions. + return false; + } + + if (mClosed || mShouldGoAway) { + LOG(("Http2Session::MaybeReTunnel %p %p session closed - requeue\n", this, + trans)); + trans->SetTunnelProvider(nullptr); + nsresult rv = gHttpHandler->InitiateTransaction(trans, trans->Priority()); + if (NS_FAILED(rv)) { + LOG3( + ("Http2Session::MaybeReTunnel %p trans=%p failed to initiate " + "transaction (%08x)", + this, trans, static_cast(rv))); + } + return true; + } + + nsHttpConnectionInfo* ci = aHttpTransaction->ConnectionInfo(); + LOG(("Http2Session:MaybeReTunnel %p %p count=%d limit %d\n", this, trans, + FindTunnelCount(ci), gHttpHandler->MaxConnectionsPerOrigin())); + if (FindTunnelCount(ci) >= gHttpHandler->MaxConnectionsPerOrigin()) { + // patience - a tunnel will open up. + return false; + } + + LOG(("Http2Session::MaybeReTunnel %p %p make new tunnel\n", this, trans)); + CreateTunnel(trans, ci, trans->SecurityCallbacks()); + return true; +} + +nsresult Http2Session::BufferOutput(const char* buf, uint32_t count, + uint32_t* countRead) { + RefPtr old; + mSegmentReader.swap(old); // Make mSegmentReader null + nsresult rv = OnReadSegment(buf, count, countRead); + mSegmentReader.swap(old); // Restore the old mSegmentReader + return rv; +} + +bool // static +Http2Session::ALPNCallback(nsISupports* securityInfo) { + nsCOMPtr ssl = do_QueryInterface(securityInfo); + LOG3(("Http2Session::ALPNCallback sslsocketcontrol=%p\n", ssl.get())); + if (ssl) { + int16_t version = ssl->GetSSLVersionOffered(); + LOG3(("Http2Session::ALPNCallback version=%x\n", version)); + + if (version == nsISSLSocketControl::TLS_VERSION_1_2 && + !gHttpHandler->IsH2MandatorySuiteEnabled()) { + LOG3(("Http2Session::ALPNCallback Mandatory Cipher Suite Unavailable\n")); + return false; + } + + if (version >= nsISSLSocketControl::TLS_VERSION_1_2) { + return true; + } + } + return false; +} + +nsresult Http2Session::ConfirmTLSProfile() { + if (mTLSProfileConfirmed) { + return NS_OK; + } + + LOG3(("Http2Session::ConfirmTLSProfile %p mConnection=%p\n", this, + mConnection.get())); + + if (mAttemptingEarlyData) { + LOG3( + ("Http2Session::ConfirmTLSProfile %p temporarily passing due to early " + "data\n", + this)); + return NS_OK; + } + + if (!gHttpHandler->EnforceHttp2TlsProfile()) { + LOG3( + ("Http2Session::ConfirmTLSProfile %p passed due to configuration " + "bypass\n", + this)); + mTLSProfileConfirmed = true; + return NS_OK; + } + + if (!mConnection) return NS_ERROR_FAILURE; + + nsCOMPtr securityInfo; + mConnection->GetSecurityInfo(getter_AddRefs(securityInfo)); + nsCOMPtr ssl = do_QueryInterface(securityInfo); + LOG3(("Http2Session::ConfirmTLSProfile %p sslsocketcontrol=%p\n", this, + ssl.get())); + if (!ssl) return NS_ERROR_FAILURE; + + int16_t version = ssl->GetSSLVersionUsed(); + LOG3(("Http2Session::ConfirmTLSProfile %p version=%x\n", this, version)); + if (version < nsISSLSocketControl::TLS_VERSION_1_2) { + LOG3(("Http2Session::ConfirmTLSProfile %p FAILED due to lack of TLS1.2\n", + this)); + return SessionError(INADEQUATE_SECURITY); + } + + uint16_t kea = ssl->GetKEAUsed(); + if (kea != ssl_kea_dh && kea != ssl_kea_ecdh) { + LOG3(("Http2Session::ConfirmTLSProfile %p FAILED due to invalid KEA %d\n", + this, kea)); + return SessionError(INADEQUATE_SECURITY); + } + + uint32_t keybits = ssl->GetKEAKeyBits(); + if (kea == ssl_kea_dh && keybits < 2048) { + LOG3(("Http2Session::ConfirmTLSProfile %p FAILED due to DH %d < 2048\n", + this, keybits)); + return SessionError(INADEQUATE_SECURITY); + } else if (kea == ssl_kea_ecdh && keybits < 224) { // see rfc7540 9.2.1. + LOG3(("Http2Session::ConfirmTLSProfile %p FAILED due to ECDH %d < 224\n", + this, keybits)); + return SessionError(INADEQUATE_SECURITY); + } + + int16_t macAlgorithm = ssl->GetMACAlgorithmUsed(); + LOG3(("Http2Session::ConfirmTLSProfile %p MAC Algortihm (aead==6) %d\n", this, + macAlgorithm)); + if (macAlgorithm != nsISSLSocketControl::SSL_MAC_AEAD) { + LOG3(("Http2Session::ConfirmTLSProfile %p FAILED due to lack of AEAD\n", + this)); + return SessionError(INADEQUATE_SECURITY); + } + + /* We are required to send SNI. We do that already, so no check is done + * here to make sure we did. */ + + /* We really should check to ensure TLS compression isn't enabled on + * this connection. However, we never enable TLS compression on our end, + * anyway, so it'll never be on. All the same, see https://bugzil.la/965881 + * for the possibility for an interface to ensure it never gets turned on. */ + + mTLSProfileConfirmed = true; + return NS_OK; +} + +//----------------------------------------------------------------------------- +// Modified methods of nsAHttpConnection +//----------------------------------------------------------------------------- + +void Http2Session::TransactionHasDataToWrite(nsAHttpTransaction* caller) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG3(("Http2Session::TransactionHasDataToWrite %p trans=%p", this, caller)); + + // a trapped signal from the http transaction to the connection that + // it is no longer blocked on read. + + RefPtr stream = mStreamTransactionHash.Get(caller); + if (!stream || !VerifyStream(stream)) { + LOG3(("Http2Session::TransactionHasDataToWrite %p caller %p not found", + this, caller)); + return; + } + + LOG3(("Http2Session::TransactionHasDataToWrite %p ID is 0x%X\n", this, + stream->StreamID())); + + if (!mClosed) { + mReadyForWrite.Push(stream); + SetWriteCallbacks(); + } else { + LOG3( + ("Http2Session::TransactionHasDataToWrite %p closed so not setting " + "Ready4Write\n", + this)); + } + + // NSPR poll will not poll the network if there are non system PR_FileDesc's + // that are ready - so we can get into a deadlock waiting for the system IO + // to come back here if we don't force the send loop manually. + Unused << ForceSend(); +} + +void Http2Session::TransactionHasDataToRecv(nsAHttpTransaction* caller) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG3(("Http2Session::TransactionHasDataToRecv %p trans=%p", this, caller)); + + // a signal from the http transaction to the connection that it will consume + // more + RefPtr stream = mStreamTransactionHash.Get(caller); + if (!stream || !VerifyStream(stream)) { + LOG3(("Http2Session::TransactionHasDataToRecv %p caller %p not found", this, + caller)); + return; + } + + LOG3(("Http2Session::TransactionHasDataToRecv %p ID is 0x%X\n", this, + stream->StreamID())); + ConnectSlowConsumer(stream); +} + +void Http2Session::TransactionHasDataToWrite(Http2Stream* stream) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG3(("Http2Session::TransactionHasDataToWrite %p stream=%p ID=0x%x", this, + stream, stream->StreamID())); + + mReadyForWrite.Push(stream); + SetWriteCallbacks(); + Unused << ForceSend(); +} + +bool Http2Session::IsPersistent() { return true; } + +nsresult Http2Session::TakeTransport(nsISocketTransport**, + nsIAsyncInputStream**, + nsIAsyncOutputStream**) { + MOZ_ASSERT(false, "TakeTransport of Http2Session"); + return NS_ERROR_UNEXPECTED; +} + +already_AddRefed Http2Session::TakeHttpConnection() { + MOZ_ASSERT(false, "TakeHttpConnection of Http2Session"); + return nullptr; +} + +already_AddRefed Http2Session::HttpConnection() { + if (mConnection) { + return mConnection->HttpConnection(); + } + return nullptr; +} + +void Http2Session::GetSecurityCallbacks(nsIInterfaceRequestor** aOut) { + *aOut = nullptr; +} + +//----------------------------------------------------------------------------- +// unused methods of nsAHttpTransaction +// We can be sure of this because Http2Session is only constructed in +// nsHttpConnection and is never passed out of that object or a +// TLSFilterTransaction TLS tunnel +//----------------------------------------------------------------------------- + +void Http2Session::SetConnection(nsAHttpConnection*) { + // This is unexpected + MOZ_ASSERT(false, "Http2Session::SetConnection()"); +} + +void Http2Session::SetProxyConnectFailed() { + MOZ_ASSERT(false, "Http2Session::SetProxyConnectFailed()"); +} + +bool Http2Session::IsDone() { return !mStreamTransactionHash.Count(); } + +nsresult Http2Session::Status() { + MOZ_ASSERT(false, "Http2Session::Status()"); + return NS_ERROR_UNEXPECTED; +} + +uint32_t Http2Session::Caps() { + MOZ_ASSERT(false, "Http2Session::Caps()"); + return 0; +} + +nsHttpRequestHead* Http2Session::RequestHead() { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + MOZ_ASSERT(false, + "Http2Session::RequestHead() " + "should not be called after http/2 is setup"); + return nullptr; +} + +uint32_t Http2Session::Http1xTransactionCount() { return 0; } + +nsresult Http2Session::TakeSubTransactions( + nsTArray >& outTransactions) { + // Generally this cannot be done with http/2 as transactions are + // started right away. + + LOG3(("Http2Session::TakeSubTransactions %p\n", this)); + + if (mConcurrentHighWater > 0) return NS_ERROR_ALREADY_OPENED; + + LOG3((" taking %d\n", mStreamTransactionHash.Count())); + + for (auto iter = mStreamTransactionHash.Iter(); !iter.Done(); iter.Next()) { + outTransactions.AppendElement(iter.Key()); + + // Removing the stream from the hash will delete the stream and drop the + // transaction reference the hash held. + iter.Remove(); + } + return NS_OK; +} + +//----------------------------------------------------------------------------- +// Pass through methods of nsAHttpConnection +//----------------------------------------------------------------------------- + +nsAHttpConnection* Http2Session::Connection() { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + return mConnection; +} + +nsresult Http2Session::OnHeadersAvailable(nsAHttpTransaction* transaction, + nsHttpRequestHead* requestHead, + nsHttpResponseHead* responseHead, + bool* reset) { + return mConnection->OnHeadersAvailable(transaction, requestHead, responseHead, + reset); +} + +bool Http2Session::IsReused() { return mConnection->IsReused(); } + +nsresult Http2Session::PushBack(const char* buf, uint32_t len) { + return mConnection->PushBack(buf, len); +} + +void Http2Session::SendPing() { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + if (mPreviousUsed) { + // alredy in progress, get out + return; + } + + mPingSentEpoch = PR_IntervalNow(); + if (!mPingSentEpoch) { + mPingSentEpoch = 1; // avoid the 0 sentinel value + } + if (!mPingThreshold || + (mPingThreshold > gHttpHandler->NetworkChangedTimeout())) { + mPreviousPingThreshold = mPingThreshold; + mPreviousUsed = true; + mPingThreshold = gHttpHandler->NetworkChangedTimeout(); + } + GeneratePing(false); + Unused << ResumeRecv(); +} + +bool Http2Session::TestOriginFrame(const nsACString& hostname, int32_t port) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + MOZ_ASSERT(mOriginFrameActivated); + + nsAutoCString key(hostname); + key.Append(':'); + key.AppendInt(port); + bool rv = mOriginFrame.Get(key); + LOG3(("TestOriginFrame() hash.get %p %s %d\n", this, key.get(), rv)); + if (!rv && ConnectionInfo()) { + // the SNI is also implicitly in this list, so consult that too + nsHttpConnectionInfo* ci = ConnectionInfo(); + rv = nsCString(hostname).EqualsIgnoreCase(ci->Origin()) && + (port == ci->OriginPort()); + LOG3(("TestOriginFrame() %p sni test %d\n", this, rv)); + } + return rv; +} + +bool Http2Session::TestJoinConnection(const nsACString& hostname, + int32_t port) { + return RealJoinConnection(hostname, port, true); +} + +bool Http2Session::JoinConnection(const nsACString& hostname, int32_t port) { + return RealJoinConnection(hostname, port, false); +} + +bool Http2Session::RealJoinConnection(const nsACString& hostname, int32_t port, + bool justKidding) { + if (!mConnection || mClosed || mShouldGoAway) { + return false; + } + + nsHttpConnectionInfo* ci = ConnectionInfo(); + if (nsCString(hostname).EqualsIgnoreCase(ci->Origin()) && + (port == ci->OriginPort())) { + return true; + } + + if (!mReceivedSettings) { + return false; + } + + if (mOriginFrameActivated) { + bool originFrameResult = TestOriginFrame(hostname, port); + if (!originFrameResult) { + return false; + } + } else { + LOG3(("JoinConnection %p no origin frame check used.\n", this)); + } + + nsAutoCString key(hostname); + key.Append(':'); + key.Append(justKidding ? 'k' : '.'); + key.AppendInt(port); + bool cachedResult; + if (mJoinConnectionCache.Get(key, &cachedResult)) { + LOG(("joinconnection [%p %s] %s result=%d cache\n", this, + ConnectionInfo()->HashKey().get(), key.get(), cachedResult)); + return cachedResult; + } + + nsresult rv; + bool isJoined = false; + + nsCOMPtr securityInfo; + nsCOMPtr sslSocketControl; + + mConnection->GetSecurityInfo(getter_AddRefs(securityInfo)); + sslSocketControl = do_QueryInterface(securityInfo, &rv); + if (NS_FAILED(rv) || !sslSocketControl) { + return false; + } + + // try all the coalescable versions we support. + const SpdyInformation* info = gHttpHandler->SpdyInfo(); + static_assert(SpdyInformation::kCount == 1, "assume 1 alpn version"); + bool joinedReturn = false; + if (info->ProtocolEnabled(0)) { + if (justKidding) { + rv = sslSocketControl->TestJoinConnection(info->VersionString[0], + hostname, port, &isJoined); + } else { + rv = sslSocketControl->JoinConnection(info->VersionString[0], hostname, + port, &isJoined); + } + if (NS_SUCCEEDED(rv) && isJoined) { + joinedReturn = true; + } + } + + LOG(("joinconnection [%p %s] %s result=%d lookup\n", this, + ConnectionInfo()->HashKey().get(), key.get(), joinedReturn)); + mJoinConnectionCache.Put(key, joinedReturn); + if (!justKidding) { + // cache a kidding entry too as this one is good for both + nsAutoCString key2(hostname); + key2.Append(':'); + key2.Append('k'); + key2.AppendInt(port); + if (!mJoinConnectionCache.Get(key2)) { + mJoinConnectionCache.Put(key2, joinedReturn); + } + } + return joinedReturn; +} + +void Http2Session::TopLevelOuterContentWindowIdChanged(uint64_t windowId) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + mCurrentForegroundTabOuterContentWindowId = windowId; + + for (auto iter = mStreamTransactionHash.Iter(); !iter.Done(); iter.Next()) { + iter.Data()->TopLevelOuterContentWindowIdChanged(windowId); + } +} + +void Http2Session::SetCleanShutdown(bool aCleanShutdown) { + mCleanShutdown = aCleanShutdown; +} + +void Http2Session::CreateWebsocketStream( + nsAHttpTransaction* aOriginalTransaction, + nsIInterfaceRequestor* aCallbacks) { + LOG(("Http2Session::CreateWebsocketStream %p %p\n", this, + aOriginalTransaction)); + + nsHttpTransaction* trans = aOriginalTransaction->QueryHttpTransaction(); + MOZ_ASSERT(trans); + + nsHttpConnectionInfo* ci = aOriginalTransaction->ConnectionInfo(); + MOZ_ASSERT(ci); + + RefPtr clone(ci->Clone()); + RefPtr connectTrans = new SpdyConnectTransaction( + clone, aCallbacks, trans->Caps(), trans, this, true); + DebugOnly rv = + AddStream(connectTrans, nsISupportsPriority::PRIORITY_NORMAL, false, + false, nullptr); + MOZ_ASSERT(rv); +} + +void Http2Session::ProcessWaitingWebsockets() { + MOZ_ASSERT(!mProcessedWaitingWebsockets); + MOZ_ASSERT(mWaitingWebsockets.Length() == + mWaitingWebsocketCallbacks.Length()); + + mProcessedWaitingWebsockets = true; + + if (!mWaitingWebsockets.Length()) { + // Nothing to do here + LOG3(("Http2Session::ProcessWaitingWebsockets %p nothing to do", this)); + return; + } + + for (size_t i = 0; i < mWaitingWebsockets.Length(); ++i) { + RefPtr httpTransaction = mWaitingWebsockets[i]; + nsCOMPtr callbacks = mWaitingWebsocketCallbacks[i]; + + if (mPeerAllowsWebsockets) { + LOG3( + ("Http2Session::ProcessWaitingWebsockets session=%p trans=%p " + "websocket", + this, httpTransaction.get())); + CreateWebsocketStream(httpTransaction, callbacks); + } else { + LOG3( + ("Http2Session::ProcessWaitingWebsockets %p Re-queuing websocket as " + "h1 due to mPeerAllowsWebsockets=false", + this)); + httpTransaction->SetConnection(nullptr); + httpTransaction->DisableSpdy(); + nsHttpTransaction* trans = httpTransaction->QueryHttpTransaction(); + if (trans) { + nsresult rv = + gHttpHandler->InitiateTransaction(trans, trans->Priority()); + if (NS_FAILED(rv)) { + LOG3( + ("Http2Session::ProcessWaitingWebsockets %p failed to reinitiate " + "websocket transaction (%08x).\n", + this, static_cast(rv))); + } + } else { + LOG3(("Http2Session::ProcessWaitingWebsockets %p missing transaction?!", + this)); + } + } + } + + mWaitingWebsockets.Clear(); + mWaitingWebsocketCallbacks.Clear(); +} + +bool Http2Session::CanAcceptWebsocket() { + LOG3(("Http2Session::CanAcceptWebsocket %p enable=%d allow=%d processed=%d", + this, mEnableWebsockets, mPeerAllowsWebsockets, + mProcessedWaitingWebsockets)); + if (mEnableWebsockets && + (mPeerAllowsWebsockets || !mProcessedWaitingWebsockets)) { + return true; + } + + return false; +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/Http2Session.h b/netwerk/protocol/http/Http2Session.h new file mode 100644 index 0000000000..ddc52fc5b7 --- /dev/null +++ b/netwerk/protocol/http/Http2Session.h @@ -0,0 +1,609 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef mozilla_net_Http2Session_h +#define mozilla_net_Http2Session_h + +// HTTP/2 - RFC 7540 +// https://www.rfc-editor.org/rfc/rfc7540.txt + +#include "ASpdySession.h" +#include "mozilla/Attributes.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/WeakPtr.h" +#include "nsAHttpConnection.h" +#include "nsCOMArray.h" +#include "nsRefPtrHashtable.h" +#include "nsDataHashtable.h" +#include "nsDeque.h" +#include "nsHashKeys.h" +#include "nsHttpRequestHead.h" +#include "nsICacheEntryOpenCallback.h" + +#include "Http2Compression.h" + +class nsISocketTransport; + +namespace mozilla { +namespace net { + +class Http2PushedStream; +class Http2Stream; +class nsHttpTransaction; + +class Http2Session final : public ASpdySession, + public nsAHttpConnection, + public nsAHttpSegmentReader, + public nsAHttpSegmentWriter { + ~Http2Session(); + + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSAHTTPTRANSACTION + NS_DECL_NSAHTTPCONNECTION(mConnection) + NS_DECL_NSAHTTPSEGMENTREADER + NS_DECL_NSAHTTPSEGMENTWRITER + + static Http2Session* CreateSession(nsISocketTransport*, + enum SpdyVersion version, + bool attemptingEarlyData); + + [[nodiscard]] bool AddStream(nsAHttpTransaction*, int32_t, bool, bool, + nsIInterfaceRequestor*) override; + bool CanReuse() override { return !mShouldGoAway && !mClosed; } + bool RoomForMoreStreams() override; + enum SpdyVersion SpdyVersion() override; + bool TestJoinConnection(const nsACString& hostname, int32_t port) override; + bool JoinConnection(const nsACString& hostname, int32_t port) override; + + // When the connection is active this is called up to once every 1 second + // return the interval (in seconds) that the connection next wants to + // have this invoked. It might happen sooner depending on the needs of + // other connections. + uint32_t ReadTimeoutTick(PRIntervalTime now) override; + + // Idle time represents time since "goodput".. e.g. a data or header frame + PRIntervalTime IdleTime() override; + + // Registering with a newID of 0 means pick the next available odd ID + uint32_t RegisterStreamID(Http2Stream*, uint32_t aNewID = 0); + + /* + HTTP/2 framing + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Length (16) | Type (8) | Flags (8) | + +-+-------------+---------------+-------------------------------+ + |R| Stream Identifier (31) | + +-+-------------------------------------------------------------+ + | Frame Data (0...) ... + +---------------------------------------------------------------+ + */ + + enum FrameType { + FRAME_TYPE_DATA = 0x0, + FRAME_TYPE_HEADERS = 0x1, + FRAME_TYPE_PRIORITY = 0x2, + FRAME_TYPE_RST_STREAM = 0x3, + FRAME_TYPE_SETTINGS = 0x4, + FRAME_TYPE_PUSH_PROMISE = 0x5, + FRAME_TYPE_PING = 0x6, + FRAME_TYPE_GOAWAY = 0x7, + FRAME_TYPE_WINDOW_UPDATE = 0x8, + FRAME_TYPE_CONTINUATION = 0x9, + FRAME_TYPE_ALTSVC = 0xA, + FRAME_TYPE_UNUSED = 0xB, + FRAME_TYPE_ORIGIN = 0xC, + FRAME_TYPE_LAST = 0xD + }; + + // NO_ERROR is a macro defined on windows, so we'll name the HTTP2 goaway + // code NO_ERROR to be NO_HTTP_ERROR + enum errorType { + NO_HTTP_ERROR = 0, + PROTOCOL_ERROR = 1, + INTERNAL_ERROR = 2, + FLOW_CONTROL_ERROR = 3, + SETTINGS_TIMEOUT_ERROR = 4, + STREAM_CLOSED_ERROR = 5, + FRAME_SIZE_ERROR = 6, + REFUSED_STREAM_ERROR = 7, + CANCEL_ERROR = 8, + COMPRESSION_ERROR = 9, + CONNECT_ERROR = 10, + ENHANCE_YOUR_CALM = 11, + INADEQUATE_SECURITY = 12, + HTTP_1_1_REQUIRED = 13, + UNASSIGNED = 31 + }; + + // These are frame flags. If they, or other undefined flags, are + // used on frames other than the comments indicate they MUST be ignored. + const static uint8_t kFlag_END_STREAM = 0x01; // data, headers + const static uint8_t kFlag_END_HEADERS = 0x04; // headers, continuation + const static uint8_t kFlag_END_PUSH_PROMISE = 0x04; // push promise + const static uint8_t kFlag_ACK = 0x01; // ping and settings + const static uint8_t kFlag_PADDED = + 0x08; // data, headers, push promise, continuation + const static uint8_t kFlag_PRIORITY = 0x20; // headers + + enum { + SETTINGS_TYPE_HEADER_TABLE_SIZE = 1, // compression table size + SETTINGS_TYPE_ENABLE_PUSH = 2, // can be used to disable push + SETTINGS_TYPE_MAX_CONCURRENT = 3, // streams recvr allowed to initiate + SETTINGS_TYPE_INITIAL_WINDOW = 4, // bytes for flow control default + SETTINGS_TYPE_MAX_FRAME_SIZE = + 5, // max frame size settings sender allows receipt of + // 6 is SETTINGS_TYPE_MAX_HEADER_LIST - advisory, we ignore it + // 7 is unassigned + SETTINGS_TYPE_ENABLE_CONNECT_PROTOCOL = + 8 // if sender implements extended CONNECT for websockets + }; + + // This should be big enough to hold all of your control packets, + // but if it needs to grow for huge headers it can do so dynamically. + const static uint32_t kDefaultBufferSize = 2048; + + // kDefaultQueueSize must be >= other queue size constants + const static uint32_t kDefaultQueueSize = 32768; + const static uint32_t kQueueMinimumCleanup = 24576; + const static uint32_t kQueueTailRoom = 4096; + const static uint32_t kQueueReserved = 1024; + + const static uint32_t kMaxStreamID = 0x7800000; + + // This is a sentinel for a deleted stream. It is not a valid + // 31 bit stream ID. + const static uint32_t kDeadStreamID = 0xffffdead; + + // below the emergency threshold of local window we ack every received + // byte. Above that we coalesce bytes into the MinimumToAck size. + const static int32_t kEmergencyWindowThreshold = 96 * 1024; + const static uint32_t kMinimumToAck = 4 * 1024 * 1024; + + // The default rwin is 64KB - 1 unless updated by a settings frame + const static uint32_t kDefaultRwin = 65535; + + // We limit frames to 2^14 bytes of length in order to preserve responsiveness + // This is the smallest allowed value for SETTINGS_MAX_FRAME_SIZE + const static uint32_t kMaxFrameData = 0x4000; + + const static uint8_t kFrameLengthBytes = 3; + const static uint8_t kFrameStreamIDBytes = 4; + const static uint8_t kFrameFlagBytes = 1; + const static uint8_t kFrameTypeBytes = 1; + const static uint8_t kFrameHeaderBytes = kFrameLengthBytes + kFrameFlagBytes + + kFrameTypeBytes + + kFrameStreamIDBytes; + + enum { + kLeaderGroupID = 0x3, + kOtherGroupID = 0x5, + kBackgroundGroupID = 0x7, + kSpeculativeGroupID = 0x9, + kFollowerGroupID = 0xB, + kUrgentStartGroupID = 0xD + // Hey, you! YES YOU! If you add/remove any groups here, you almost + // certainly need to change the lookup of the stream/ID hash in + // Http2Session::OnTransportStatus and |kPriorityGroupCount| below. + // Yeah, that's right. YOU! + }; + const static uint8_t kPriorityGroupCount = 6; + + static nsresult RecvHeaders(Http2Session*); + static nsresult RecvPriority(Http2Session*); + static nsresult RecvRstStream(Http2Session*); + static nsresult RecvSettings(Http2Session*); + static nsresult RecvPushPromise(Http2Session*); + static nsresult RecvPing(Http2Session*); + static nsresult RecvGoAway(Http2Session*); + static nsresult RecvWindowUpdate(Http2Session*); + static nsresult RecvContinuation(Http2Session*); + static nsresult RecvAltSvc(Http2Session*); + static nsresult RecvUnused(Http2Session*); + static nsresult RecvOrigin(Http2Session*); + + char* EnsureOutputBuffer(uint32_t needed); + + template + void CreateFrameHeader(charType dest, uint16_t frameLength, uint8_t frameType, + uint8_t frameFlags, uint32_t streamID); + + // For writing the data stream to LOG4 + static void LogIO(Http2Session*, Http2Stream*, const char*, const char*, + uint32_t); + + // overload of nsAHttpConnection + void TransactionHasDataToWrite(nsAHttpTransaction*) override; + void TransactionHasDataToRecv(nsAHttpTransaction*) override; + + // a similar version for Http2Stream + void TransactionHasDataToWrite(Http2Stream*); + + // an overload of nsAHttpSegementReader + [[nodiscard]] virtual nsresult CommitToSegmentSize( + uint32_t size, bool forceCommitment) override; + [[nodiscard]] nsresult BufferOutput(const char*, uint32_t, uint32_t*); + void FlushOutputQueue(); + uint32_t AmountOfOutputBuffered() { + return mOutputQueueUsed - mOutputQueueSent; + } + + uint32_t GetServerInitialStreamWindow() { return mServerInitialStreamWindow; } + + [[nodiscard]] bool TryToActivate(Http2Stream* stream); + void ConnectPushedStream(Http2Stream* stream); + void ConnectSlowConsumer(Http2Stream* stream); + + [[nodiscard]] nsresult ConfirmTLSProfile(); + [[nodiscard]] static bool ALPNCallback(nsISupports* securityInfo); + + uint64_t Serial() { return mSerial; } + + void PrintDiagnostics(nsCString& log) override; + + // Streams need access to these + uint32_t SendingChunkSize() { return mSendingChunkSize; } + uint32_t PushAllowance() { return mPushAllowance; } + Http2Compressor* Compressor() { return &mCompressor; } + nsISocketTransport* SocketTransport() { return mSocketTransport; } + int64_t ServerSessionWindow() { return mServerSessionWindow; } + void DecrementServerSessionWindow(uint32_t bytes) { + mServerSessionWindow -= bytes; + } + uint32_t InitialRwin() { return mInitialRwin; } + + void SendPing() override; + [[nodiscard]] bool MaybeReTunnel(nsAHttpTransaction*) override; + bool UseH2Deps() { return mUseH2Deps; } + void SetCleanShutdown(bool) override; + + // overload of nsAHttpTransaction + [[nodiscard]] nsresult ReadSegmentsAgain(nsAHttpSegmentReader*, uint32_t, + uint32_t*, bool*) final; + [[nodiscard]] nsresult WriteSegmentsAgain(nsAHttpSegmentWriter*, uint32_t, + uint32_t*, bool*) final; + [[nodiscard]] bool Do0RTT() final { return true; } + [[nodiscard]] nsresult Finish0RTT(bool aRestart, bool aAlpnChanged) final; + void SetFastOpenStatus(uint8_t aStatus) final; + + // For use by an HTTP2Stream + void Received421(nsHttpConnectionInfo* ci); + + void SendPriorityFrame(uint32_t streamID, uint32_t dependsOn, uint8_t weight); + void IncrementTrrCounter() { mTrrStreams++; } + + bool CanAcceptWebsocket() override; + + private: + Http2Session(nsISocketTransport*, enum SpdyVersion version, + bool attemptingEarlyData); + + // These internal states do not correspond to the states of the HTTP/2 + // specification + enum internalStateType { + BUFFERING_OPENING_SETTINGS, + BUFFERING_FRAME_HEADER, + BUFFERING_CONTROL_FRAME, + PROCESSING_DATA_FRAME_PADDING_CONTROL, + PROCESSING_DATA_FRAME, + DISCARDING_DATA_FRAME_PADDING, + DISCARDING_DATA_FRAME, + PROCESSING_COMPLETE_HEADERS, + PROCESSING_CONTROL_RST_STREAM, + NOT_USING_NETWORK + }; + + static const uint8_t kMagicHello[24]; + + [[nodiscard]] nsresult ResponseHeadersComplete(); + uint32_t GetWriteQueueSize(); + void ChangeDownstreamState(enum internalStateType); + void ResetDownstreamState(); + [[nodiscard]] nsresult ReadyToProcessDataFrame(enum internalStateType); + [[nodiscard]] nsresult UncompressAndDiscard(bool); + void GeneratePing(bool); + void GenerateSettingsAck(); + void GeneratePriority(uint32_t, uint8_t); + void GenerateRstStream(uint32_t, uint32_t); + void GenerateGoAway(uint32_t); + void CleanupStream(Http2Stream*, nsresult, errorType); + void CleanupStream(uint32_t, nsresult, errorType); + void CloseStream(Http2Stream*, nsresult); + void SendHello(); + void RemoveStreamFromQueues(Http2Stream*); + [[nodiscard]] nsresult ParsePadding(uint8_t&, uint16_t&); + + void SetWriteCallbacks(); + void RealignOutputQueue(); + + void ProcessPending(); + [[nodiscard]] nsresult ProcessConnectedPush(Http2Stream*, + nsAHttpSegmentWriter*, uint32_t, + uint32_t*); + [[nodiscard]] nsresult ProcessSlowConsumer(Http2Stream*, + nsAHttpSegmentWriter*, uint32_t, + uint32_t*); + + [[nodiscard]] nsresult SetInputFrameDataStream(uint32_t); + void CreatePriorityNode(uint32_t, uint32_t, uint8_t, const char*); + char* CreatePriorityFrame(uint32_t, uint32_t, uint8_t); + bool VerifyStream(Http2Stream*, uint32_t); + void SetNeedsCleanup(); + + void UpdateLocalRwin(Http2Stream* stream, uint32_t bytes); + void UpdateLocalStreamWindow(Http2Stream* stream, uint32_t bytes); + void UpdateLocalSessionWindow(uint32_t bytes); + + void MaybeDecrementConcurrent(Http2Stream* stream); + bool RoomForMoreConcurrent(); + void IncrementConcurrent(Http2Stream* stream); + void QueueStream(Http2Stream* stream); + + // a wrapper for all calls to the nshttpconnection level segment writer. Used + // to track network I/O for timeout purposes + [[nodiscard]] nsresult NetworkRead(nsAHttpSegmentWriter*, char*, uint32_t, + uint32_t*); + + void Shutdown(); + + nsresult SessionError(enum errorType); + + // This is intended to be nsHttpConnectionMgr:nsConnectionHandle taken + // from the first transaction on this session. That object contains the + // pointer to the real network-level nsHttpConnection object. + RefPtr mConnection; + + // The underlying socket transport object is needed to propogate some events + nsISocketTransport* mSocketTransport; + + // These are temporary state variables to hold the argument to + // Read/WriteSegments so it can be accessed by On(read/write)segment + // further up the stack. + RefPtr mSegmentReader; + nsAHttpSegmentWriter* mSegmentWriter; + + uint32_t mSendingChunkSize; /* the transmission chunk size */ + uint32_t mNextStreamID; /* 24 bits */ + uint32_t mLastPushedID; + uint32_t mConcurrentHighWater; /* max parallelism on session */ + uint32_t mPushAllowance; /* rwin for unmatched pushes */ + + internalStateType mDownstreamState; /* in frame, between frames, etc.. */ + + // Maintain 2 indexes - one by stream ID, one by transaction pointer. + // There are also several lists of streams: ready to write, queued due to + // max parallelism, streams that need to force a read for push, and the full + // set of pushed streams. + nsDataHashtable mStreamIDHash; + nsRefPtrHashtable, Http2Stream> + mStreamTransactionHash; + + nsDeque mReadyForWrite; + nsDeque mQueuedStreams; + nsDeque mPushesReadyForRead; + nsDeque mSlowConsumersReadyForRead; + nsTArray mPushedStreams; + + // Compression contexts for header transport. + // HTTP/2 compresses only HTTP headers and does not reset the context in + // between frames. Even data that is not associated with a stream (e.g invalid + // stream ID) is passed through these contexts to keep the compression + // context correct. + Http2Compressor mCompressor; + Http2Decompressor mDecompressor; + nsCString mDecompressBuffer; + + // mInputFrameBuffer is used to store received control packets and the 8 bytes + // of header on data packets + uint32_t mInputFrameBufferSize; // buffer allocation + uint32_t mInputFrameBufferUsed; // amt of allocation used + UniquePtr mInputFrameBuffer; + + // mInputFrameDataSize/Read are used for tracking the amount of data consumed + // in a frame after the 8 byte header. Control frames are always fully + // buffered and the fixed 8 byte leading header is at mInputFrameBuffer + 0, + // the first data byte (i.e. the first settings/goaway/etc.. specific byte) is + // at mInputFrameBuffer + 8 The frame size is mInputFrameDataSize + the + // constant 8 byte header + uint32_t mInputFrameDataSize; + uint32_t mInputFrameDataRead; + bool mInputFrameFinal; // This frame was marked FIN + uint8_t mInputFrameType; + uint8_t mInputFrameFlags; + uint32_t mInputFrameID; + uint16_t mPaddingLength; + + // When a frame has been received that is addressed to a particular stream + // (e.g. a data frame after the stream-id has been decoded), this points + // to the stream. + Http2Stream* mInputFrameDataStream; + + // mNeedsCleanup is a state variable to defer cleanup of a closed stream + // If needed, It is set in session::OnWriteSegments() and acted on and + // cleared when the stack returns to session::WriteSegments(). The stream + // cannot be destroyed directly out of OnWriteSegments because + // stream::writeSegments() is on the stack at that time. + Http2Stream* mNeedsCleanup; + + // This reason code in the last processed RESET frame + uint32_t mDownstreamRstReason; + + // When HEADERS/PROMISE are chained together, this is the expected ID of the + // next recvd frame which must be the same type + uint32_t mExpectedHeaderID; + uint32_t mExpectedPushPromiseID; + uint32_t mContinuedPromiseStream; + + // for the conversion of downstream http headers into http/2 formatted headers + // The data here does not persist between frames + nsCString mFlatHTTPResponseHeaders; + uint32_t mFlatHTTPResponseHeadersOut; + + // when set, the session will go away when it reaches 0 streams. This flag + // is set when: the stream IDs are running out (at either the client or the + // server), when DontReuse() is called, a RST that is not specific to a + // particular stream is received, a GOAWAY frame has been received from + // the server. + bool mShouldGoAway; + + // the session has received a nsAHttpTransaction::Close() call + bool mClosed; + + // the session received a GoAway frame with a valid GoAwayID + bool mCleanShutdown; + + // the session received the opening SETTINGS frame from the server + bool mReceivedSettings; + + // The TLS comlpiance checks are not done in the ctor beacuse of bad + // exception handling - so we do them at IO time and cache the result + bool mTLSProfileConfirmed; + + // A specifc reason code for the eventual GoAway frame. If set to + // NO_HTTP_ERROR only NO_HTTP_ERROR, PROTOCOL_ERROR, or INTERNAL_ERROR will be + // sent. + errorType mGoAwayReason; + + // The error code sent/received on the session goaway frame. UNASSIGNED/31 + // if not transmitted. + int32_t mClientGoAwayReason; + int32_t mPeerGoAwayReason; + + // If a GoAway message was received this is the ID of the last valid + // stream. 0 otherwise. (0 is never a valid stream id.) + uint32_t mGoAwayID; + + // The last stream processed ID we will send in our GoAway frame. + uint32_t mOutgoingGoAwayID; + + // The limit on number of concurrent streams for this session. Normally it + // is basically unlimited, but the SETTINGS control message from the + // server might bring it down. + uint32_t mMaxConcurrent; + + // The actual number of concurrent streams at this moment. Generally below + // mMaxConcurrent, but the max can be lowered in real time to a value + // below the current value + uint32_t mConcurrent; + + // The number of server initiated promises, tracked for telemetry + uint32_t mServerPushedResources; + + // The server rwin for new streams as determined from a SETTINGS frame + uint32_t mServerInitialStreamWindow; + + // The Local Session window is how much data the server is allowed to send + // (across all streams) without getting a window update to stream 0. It is + // signed because asynchronous changes via SETTINGS can drive it negative. + int64_t mLocalSessionWindow; + + // The Remote Session Window is how much data the client is allowed to send + // (across all streams) without receiving a window update to stream 0. It is + // signed because asynchronous changes via SETTINGS can drive it negative. + int64_t mServerSessionWindow; + + // The initial value of the local stream and session window + uint32_t mInitialRwin; + + // This is a output queue of bytes ready to be written to the SSL stream. + // When that streams returns WOULD_BLOCK on direct write the bytes get + // coalesced together here. This results in larger writes to the SSL layer. + // The buffer is not dynamically grown to accomodate stream writes, but + // does expand to accept infallible session wide frames like GoAway and RST. + uint32_t mOutputQueueSize; + uint32_t mOutputQueueUsed; + uint32_t mOutputQueueSent; + UniquePtr mOutputQueueBuffer; + + PRIntervalTime mPingThreshold; + PRIntervalTime mLastReadEpoch; // used for ping timeouts + PRIntervalTime mLastDataReadEpoch; // used for IdleTime() + PRIntervalTime mPingSentEpoch; + + PRIntervalTime mPreviousPingThreshold; // backup for the former value + bool mPreviousUsed; // true when backup is used + + // used as a temporary buffer while enumerating the stream hash during GoAway + nsDeque mGoAwayStreamsToRestart; + + // Each session gets a unique serial number because the push cache is + // correlated by the load group and the serial number can be used as part of + // the cache key to make sure streams aren't shared across sessions. + uint64_t mSerial; + + // Telemetry for continued headers (pushed and pulled) for quic design + uint32_t mAggregatedHeaderSize; + + // If push is disabled, we want to be able to send PROTOCOL_ERRORs if we + // receive a PUSH_PROMISE, but we have to wait for the SETTINGS ACK before + // we can actually tell the other end to go away. These help us keep track + // of that state so we can behave appropriately. + bool mWaitingForSettingsAck; + bool mGoAwayOnPush; + + bool mUseH2Deps; + + bool mAttemptingEarlyData; + // The ID(s) of the stream(s) that we are getting 0RTT data from. + nsTArray> m0RTTStreams; + // The ID(s) of the stream(s) that are not able to send 0RTT data. We need to + // remember them put them into mReadyForWrite queue when 0RTT finishes. + nsTArray> mCannotDo0RTTStreams; + + bool RealJoinConnection(const nsACString& hostname, int32_t port, bool jk); + bool TestOriginFrame(const nsACString& name, int32_t port); + bool mOriginFrameActivated; + nsDataHashtable mOriginFrame; + + nsDataHashtable mJoinConnectionCache; + + uint64_t mCurrentForegroundTabOuterContentWindowId; + + uint32_t mCntActivated; + + // A h2 session will be created before all socket events are trigered, + // e.g. NS_NET_STATUS_TLS_HANDSHAKE_ENDED and for TFO many others. + // We should propagate this events to the first nsHttpTransaction. + RefPtr mFirstHttpTransaction; + bool mTlsHandshakeFinished; + + bool mCheckNetworkStallsWithTFO; + PRIntervalTime mLastRequestBytesSentTime; + + bool mPeerFailedHandshake; + + private: + /// connect tunnels + void DispatchOnTunnel(nsAHttpTransaction*, nsIInterfaceRequestor*); + void CreateTunnel(nsHttpTransaction*, nsHttpConnectionInfo*, + nsIInterfaceRequestor*); + void RegisterTunnel(Http2Stream*); + void UnRegisterTunnel(Http2Stream*); + uint32_t FindTunnelCount(nsHttpConnectionInfo*); + uint32_t FindTunnelCount(nsCString const&); + nsDataHashtable mTunnelHash; + uint32_t mTrrStreams; + + // websockets + void CreateWebsocketStream(nsAHttpTransaction*, nsIInterfaceRequestor*); + void ProcessWaitingWebsockets(); + bool mEnableWebsockets; // Whether we allow websockets, based on a pref + bool mPeerAllowsWebsockets; // Whether our peer allows websockets, based on + // SETTINGS + bool mProcessedWaitingWebsockets; // True once we've received at least one + // SETTINGS + nsTArray> + mWaitingWebsockets; // Websocket transactions that may be waiting for the + // opening SETTINGS + nsCOMArray mWaitingWebsocketCallbacks; +}; + +} // namespace net +} // namespace mozilla + +#endif // mozilla_net_Http2Session_h diff --git a/netwerk/protocol/http/Http2Stream.cpp b/netwerk/protocol/http/Http2Stream.cpp new file mode 100644 index 0000000000..eed2accec5 --- /dev/null +++ b/netwerk/protocol/http/Http2Stream.cpp @@ -0,0 +1,1678 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et 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/. */ + +// HttpLog.h should generally be included first +#include "HttpLog.h" + +// Log on level :5, instead of default :4. +#undef LOG +#define LOG(args) LOG5(args) +#undef LOG_ENABLED +#define LOG_ENABLED() LOG5_ENABLED() + +#include + +#include "Http2Compression.h" +#include "Http2Session.h" +#include "Http2Stream.h" +#include "Http2Push.h" +#include "TunnelUtils.h" + +#include "mozilla/BasePrincipal.h" +#include "mozilla/Telemetry.h" +#include "nsAlgorithm.h" +#include "nsHttp.h" +#include "nsHttpHandler.h" +#include "nsHttpRequestHead.h" +#include "nsIClassOfService.h" +#include "nsStandardURL.h" +#include "prnetdb.h" + +namespace mozilla { +namespace net { + +Http2Stream::Http2Stream(nsAHttpTransaction* httpTransaction, + Http2Session* session, int32_t priority, + uint64_t windowId) + : mStreamID(0), + mSession(session), + mSegmentReader(nullptr), + mSegmentWriter(nullptr), + mUpstreamState(GENERATING_HEADERS), + mState(IDLE), + mRequestHeadersDone(0), + mOpenGenerated(0), + mAllHeadersReceived(0), + mQueued(0), + mSocketTransport(session->SocketTransport()), + mCurrentForegroundTabOuterContentWindowId(windowId), + mTransactionTabId(0), + mTransaction(httpTransaction), + mChunkSize(session->SendingChunkSize()), + mRequestBlockedOnRead(0), + mRecvdFin(0), + mReceivedData(0), + mRecvdReset(0), + mSentReset(0), + mCountAsActive(0), + mSentFin(0), + mSentWaitingFor(0), + mSetTCPSocketBuffer(0), + mBypassInputBuffer(0), + mTxInlineFrameSize(Http2Session::kDefaultBufferSize), + mTxInlineFrameUsed(0), + mTxStreamFrameSize(0), + mRequestBodyLenRemaining(0), + mLocalUnacked(0), + mBlockedOnRwin(false), + mTotalSent(0), + mTotalRead(0), + mPushSource(nullptr), + mAttempting0RTT(false), + mIsTunnel(false), + mPlainTextTunnel(false), + mIsWebsocket(false) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + nsHttpTransaction* trans = mTransaction->QueryHttpTransaction(); + LOG1(("Http2Stream::Http2Stream %p trans=%p atrans=%p", this, trans, + httpTransaction)); + + mServerReceiveWindow = session->GetServerInitialStreamWindow(); + mClientReceiveWindow = session->PushAllowance(); + + mTxInlineFrame = MakeUnique(mTxInlineFrameSize); + + static_assert(nsISupportsPriority::PRIORITY_LOWEST <= kNormalPriority, + "Lowest Priority should be less than kNormalPriority"); + + // values of priority closer to 0 are higher priority for the priority + // argument. This value is used as a group, which maps to a + // weight that is related to the nsISupportsPriority that we are given. + int32_t httpPriority; + if (priority >= nsISupportsPriority::PRIORITY_LOWEST) { + httpPriority = kWorstPriority; + } else if (priority <= nsISupportsPriority::PRIORITY_HIGHEST) { + httpPriority = kBestPriority; + } else { + httpPriority = kNormalPriority + priority; + } + MOZ_ASSERT(httpPriority >= 0); + SetPriority(static_cast(httpPriority)); + + if (trans) { + mTransactionTabId = trans->TopLevelOuterContentWindowId(); + } +} + +Http2Stream::~Http2Stream() { + ClearPushSource(); + ClearTransactionsBlockedOnTunnel(); + mStreamID = Http2Session::kDeadStreamID; + + LOG3(("Http2Stream::~Http2Stream %p", this)); +} + +void Http2Stream::ClearPushSource() { + if (mPushSource) { + mPushSource->SetConsumerStream(nullptr); + mPushSource = nullptr; + } +} + +// ReadSegments() is used to write data down the socket. Generally, HTTP +// request data is pulled from the approriate transaction and +// converted to HTTP/2 data. Sometimes control data like a window-update is +// generated instead. + +nsresult Http2Stream::ReadSegments(nsAHttpSegmentReader* reader, uint32_t count, + uint32_t* countRead) { + LOG3(("Http2Stream %p ReadSegments reader=%p count=%d state=%x", this, reader, + count, mUpstreamState)); + + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + nsresult rv = NS_ERROR_UNEXPECTED; + mRequestBlockedOnRead = 0; + + if (mRecvdFin || mRecvdReset) { + // Don't transmit any request frames if the peer cannot respond + LOG3( + ("Http2Stream %p ReadSegments request stream aborted due to" + " response side closure\n", + this)); + return NS_ERROR_ABORT; + } + + // avoid runt chunks if possible by anticipating + // full data frames + if (count > (mChunkSize + 8)) { + uint32_t numchunks = count / (mChunkSize + 8); + count = numchunks * (mChunkSize + 8); + } + + switch (mUpstreamState) { + case GENERATING_HEADERS: + case GENERATING_BODY: + case SENDING_BODY: + // Call into the HTTP Transaction to generate the HTTP request + // stream. That stream will show up in OnReadSegment(). + mSegmentReader = reader; + rv = mTransaction->ReadSegments(this, count, countRead); + mSegmentReader = nullptr; + + LOG3(("Http2Stream::ReadSegments %p trans readsegments rv %" PRIx32 + " read=%d\n", + this, static_cast(rv), *countRead)); + + // Check to see if the transaction's request could be written out now. + // If not, mark the stream for callback when writing can proceed. + if (NS_SUCCEEDED(rv) && mUpstreamState == GENERATING_HEADERS && + !mRequestHeadersDone) + mSession->TransactionHasDataToWrite(this); + + // mTxinlineFrameUsed represents any queued un-sent frame. It might + // be 0 if there is no such frame, which is not a gurantee that we + // don't have more request body to send - just that any data that was + // sent comprised a complete HTTP/2 frame. Likewise, a non 0 value is + // a queued, but complete, http/2 frame length. + + // Mark that we are blocked on read if the http transaction needs to + // provide more of the request message body and there is nothing queued + // for writing + if (rv == NS_BASE_STREAM_WOULD_BLOCK && !mTxInlineFrameUsed) { + LOG(("Http2Stream %p mRequestBlockedOnRead = 1", this)); + mRequestBlockedOnRead = 1; + } + + // A transaction that had already generated its headers before it was + // queued at the session level (due to concurrency concerns) may not call + // onReadSegment off the ReadSegments() stack above. + + // When mTransaction->ReadSegments returns NS_BASE_STREAM_WOULD_BLOCK it + // means it may have already finished providing all the request data + // necessary to generate open, calling OnReadSegment will drive sending + // the request; this may happen after dequeue of the stream. + + if (mUpstreamState == GENERATING_HEADERS && + (NS_SUCCEEDED(rv) || rv == NS_BASE_STREAM_WOULD_BLOCK)) { + LOG3( + ("Http2Stream %p ReadSegments forcing OnReadSegment call\n", this)); + uint32_t wasted = 0; + mSegmentReader = reader; + nsresult rv2 = OnReadSegment("", 0, &wasted); + mSegmentReader = nullptr; + + LOG3((" OnReadSegment returned 0x%08" PRIx32, + static_cast(rv2))); + if (NS_SUCCEEDED(rv2)) { + mRequestBlockedOnRead = 0; + } + } + + // If the sending flow control window is open (!mBlockedOnRwin) then + // continue sending the request + if (!mBlockedOnRwin && mOpenGenerated && !mTxInlineFrameUsed && + NS_SUCCEEDED(rv) && (!*countRead)) { + MOZ_ASSERT(!mQueued); + MOZ_ASSERT(mRequestHeadersDone); + LOG3(( + "Http2Stream::ReadSegments %p 0x%X: Sending request data complete, " + "mUpstreamState=%x\n", + this, mStreamID, mUpstreamState)); + if (mSentFin) { + ChangeState(UPSTREAM_COMPLETE); + } else { + GenerateDataFrameHeader(0, true); + ChangeState(SENDING_FIN_STREAM); + mSession->TransactionHasDataToWrite(this); + rv = NS_BASE_STREAM_WOULD_BLOCK; + } + } + break; + + case SENDING_FIN_STREAM: + // We were trying to send the FIN-STREAM but were blocked from + // sending it out - try again. + if (!mSentFin) { + mSegmentReader = reader; + rv = TransmitFrame(nullptr, nullptr, false); + mSegmentReader = nullptr; + MOZ_ASSERT(NS_FAILED(rv) || !mTxInlineFrameUsed, + "Transmit Frame should be all or nothing"); + if (NS_SUCCEEDED(rv)) ChangeState(UPSTREAM_COMPLETE); + } else { + rv = NS_OK; + mTxInlineFrameUsed = 0; // cancel fin data packet + ChangeState(UPSTREAM_COMPLETE); + } + + *countRead = 0; + + // don't change OK to WOULD BLOCK. we are really done sending if OK + break; + + case UPSTREAM_COMPLETE: + *countRead = 0; + rv = NS_OK; + break; + + default: + MOZ_ASSERT(false, "Http2Stream::ReadSegments unknown state"); + break; + } + + return rv; +} + +uint64_t Http2Stream::LocalUnAcked() { + // reduce unacked by the amount of undelivered data + // to help assert flow control + uint64_t undelivered = mSimpleBuffer.Available(); + + if (undelivered > mLocalUnacked) { + return 0; + } + return mLocalUnacked - undelivered; +} + +nsresult Http2Stream::BufferInput(uint32_t count, uint32_t* countWritten) { + char buf[SimpleBufferPage::kSimpleBufferPageSize]; + if (SimpleBufferPage::kSimpleBufferPageSize < count) { + count = SimpleBufferPage::kSimpleBufferPageSize; + } + + mBypassInputBuffer = 1; + nsresult rv = mSegmentWriter->OnWriteSegment(buf, count, countWritten); + mBypassInputBuffer = 0; + + if (NS_SUCCEEDED(rv)) { + rv = mSimpleBuffer.Write(buf, *countWritten); + if (NS_FAILED(rv)) { + MOZ_ASSERT(rv == NS_ERROR_OUT_OF_MEMORY); + return NS_ERROR_OUT_OF_MEMORY; + } + } + return rv; +} + +bool Http2Stream::DeferCleanup(nsresult status) { + // do not cleanup a stream that has data buffered for the transaction + return (NS_SUCCEEDED(status) && mSimpleBuffer.Available()); +} + +// WriteSegments() is used to read data off the socket. Generally this is +// just a call through to the associated nsHttpTransaction for this stream +// for the remaining data bytes indicated by the current DATA frame. + +nsresult Http2Stream::WriteSegments(nsAHttpSegmentWriter* writer, + uint32_t count, uint32_t* countWritten) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + MOZ_ASSERT(!mSegmentWriter, "segment writer in progress"); + + LOG3(("Http2Stream::WriteSegments %p count=%d state=%x", this, count, + mUpstreamState)); + + mSegmentWriter = writer; + nsresult rv = mTransaction->WriteSegments(this, count, countWritten); + + if (rv == NS_BASE_STREAM_WOULD_BLOCK) { + // consuming transaction won't take data. but we need to read it into a + // buffer so that it won't block other streams. but we should not advance + // the flow control window so that we'll eventually push back on the sender. + + // with tunnels you need to make sure that this is an underlying connction + // established that can be meaningfully giving this signal + bool doBuffer = true; + if (mIsTunnel) { + RefPtr qiTrans( + mTransaction->QuerySpdyConnectTransaction()); + if (qiTrans) { + doBuffer = qiTrans->ConnectedReadyForInput(); + } + } + // stash this data + if (doBuffer) { + rv = BufferInput(count, countWritten); + LOG3(("Http2Stream::WriteSegments %p Buffered %" PRIX32 " %d\n", this, + static_cast(rv), *countWritten)); + } + } + mSegmentWriter = nullptr; + return rv; +} + +nsresult Http2Stream::MakeOriginURL(const nsACString& origin, + nsCOMPtr& url) { + nsAutoCString scheme; + nsresult rv = net_ExtractURLScheme(origin, scheme); + NS_ENSURE_SUCCESS(rv, rv); + return MakeOriginURL(scheme, origin, url); +} + +nsresult Http2Stream::MakeOriginURL(const nsACString& scheme, + const nsACString& origin, + nsCOMPtr& url) { + return NS_MutateURI(new nsStandardURL::Mutator()) + .Apply(NS_MutatorMethod( + &nsIStandardURLMutator::Init, nsIStandardURL::URLTYPE_AUTHORITY, + scheme.EqualsLiteral("http") ? NS_HTTP_DEFAULT_PORT + : NS_HTTPS_DEFAULT_PORT, + nsCString(origin), nullptr, nullptr, nullptr)) + .Finalize(url); +} + +void Http2Stream::CreatePushHashKey( + const nsCString& scheme, const nsCString& hostHeader, + const mozilla::OriginAttributes& originAttributes, uint64_t serial, + const nsACString& pathInfo, nsCString& outOrigin, nsCString& outKey) { + nsCString fullOrigin = scheme; + fullOrigin.AppendLiteral("://"); + fullOrigin.Append(hostHeader); + + nsCOMPtr origin; + nsresult rv = Http2Stream::MakeOriginURL(scheme, fullOrigin, origin); + + if (NS_SUCCEEDED(rv)) { + rv = origin->GetAsciiSpec(outOrigin); + outOrigin.Trim("/", false, true, false); + } + + if (NS_FAILED(rv)) { + // Fallback to plain text copy - this may end up behaving poorly + outOrigin = fullOrigin; + } + + outKey = outOrigin; + outKey.AppendLiteral("/["); + nsAutoCString suffix; + originAttributes.CreateSuffix(suffix); + outKey.Append(suffix); + outKey.Append(']'); + outKey.AppendLiteral("/[http2."); + outKey.AppendInt(serial); + outKey.Append(']'); + outKey.Append(pathInfo); +} + +nsresult Http2Stream::ParseHttpRequestHeaders(const char* buf, uint32_t avail, + uint32_t* countUsed) { + // Returns NS_OK even if the headers are incomplete + // set mRequestHeadersDone flag if they are complete + + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + MOZ_ASSERT(mUpstreamState == GENERATING_HEADERS); + MOZ_ASSERT(!mRequestHeadersDone); + + LOG3(("Http2Stream::ParseHttpRequestHeaders %p avail=%d state=%x", this, + avail, mUpstreamState)); + + mFlatHttpRequestHeaders.Append(buf, avail); + nsHttpRequestHead* head = mTransaction->RequestHead(); + + // We can use the simple double crlf because firefox is the + // only client we are parsing + int32_t endHeader = mFlatHttpRequestHeaders.Find("\r\n\r\n"); + + if (endHeader == kNotFound) { + // We don't have all the headers yet + LOG3( + ("Http2Stream::ParseHttpRequestHeaders %p " + "Need more header bytes. Len = %d", + this, mFlatHttpRequestHeaders.Length())); + *countUsed = avail; + return NS_OK; + } + + // We have recvd all the headers, trim the local + // buffer of the final empty line, and set countUsed to reflect + // the whole header has been consumed. + uint32_t oldLen = mFlatHttpRequestHeaders.Length(); + mFlatHttpRequestHeaders.SetLength(endHeader + 2); + *countUsed = avail - (oldLen - endHeader) + 4; + mRequestHeadersDone = 1; + + nsAutoCString authorityHeader; + nsAutoCString hashkey; + nsresult rv = head->GetHeader(nsHttp::Host, authorityHeader); + if (NS_FAILED(rv)) { + MOZ_ASSERT(false); + return rv; + } + + nsAutoCString requestURI; + head->RequestURI(requestURI); + + mozilla::OriginAttributes originAttributes; + mSocketTransport->GetOriginAttributes(&originAttributes); + + CreatePushHashKey(nsDependentCString(head->IsHTTPS() ? "https" : "http"), + authorityHeader, originAttributes, mSession->Serial(), + requestURI, mOrigin, hashkey); + + // check the push cache for GET + if (head->IsGet()) { + // from :scheme, :authority, :path + nsIRequestContext* requestContext = mTransaction->RequestContext(); + SpdyPushCache* cache = nullptr; + if (requestContext) { + cache = requestContext->GetSpdyPushCache(); + } + + RefPtr pushedStreamWrapper; + Http2PushedStream* pushedStream = nullptr; + + // If a push stream is attached to the transaction via onPush, match only + // with that one. This occurs when a push was made with in conjunction with + // a nsIHttpPushListener + nsHttpTransaction* trans = mTransaction->QueryHttpTransaction(); + if (trans && (pushedStreamWrapper = trans->TakePushedStream()) && + (pushedStream = pushedStreamWrapper->GetStream())) { + if (pushedStream->mSession == mSession) { + LOG3(("Pushed Stream match based on OnPush correlation %p", + pushedStream)); + } else { + LOG3(("Pushed Stream match failed due to stream mismatch %p %" PRId64 + " %" PRId64 "\n", + pushedStream, pushedStream->mSession->Serial(), + mSession->Serial())); + pushedStream->OnPushFailed(); + pushedStream = nullptr; + } + } + + // we remove the pushedstream from the push cache so that + // it will not be used for another GET. This does not destroy the + // stream itself - that is done when the transactionhash is done with it. + if (cache && !pushedStream) { + pushedStream = cache->RemovePushedStreamHttp2(hashkey); + } + + LOG3( + ("Pushed Stream Lookup " + "session=%p key=%s requestcontext=%p cache=%p hit=%p\n", + mSession, hashkey.get(), requestContext, cache, pushedStream)); + + if (pushedStream) { + LOG3(("Pushed Stream Match located %p id=0x%X key=%s\n", pushedStream, + pushedStream->StreamID(), hashkey.get())); + pushedStream->SetConsumerStream(this); + mPushSource = pushedStream; + SetSentFin(true); + AdjustPushedPriority(); + + // There is probably pushed data buffered so trigger a read manually + // as we can't rely on future network events to do it + mSession->ConnectPushedStream(this); + mOpenGenerated = 1; + + // if the "mother stream" had TRR, this one is a TRR stream too! + RefPtr ci(Transaction()->ConnectionInfo()); + if (ci && ci->GetIsTrrServiceChannel()) { + mSession->IncrementTrrCounter(); + } + + return NS_OK; + } + } + return NS_OK; +} + +// This is really a headers frame, but open is pretty clear from a workflow pov +nsresult Http2Stream::GenerateOpen() { + // It is now OK to assign a streamID that we are assured will + // be monotonically increasing amongst new streams on this + // session + mStreamID = mSession->RegisterStreamID(this); + MOZ_ASSERT(mStreamID & 1, "Http2 Stream Channel ID must be odd"); + MOZ_ASSERT(!mOpenGenerated); + + mOpenGenerated = 1; + + nsHttpRequestHead* head = mTransaction->RequestHead(); + nsAutoCString requestURI; + head->RequestURI(requestURI); + LOG3(("Http2Stream %p Stream ID 0x%X [session=%p] for URI %s\n", this, + mStreamID, mSession, requestURI.get())); + + if (mStreamID >= 0x80000000) { + // streamID must fit in 31 bits. Evading This is theoretically possible + // because stream ID assignment is asynchronous to stream creation + // because of the protocol requirement that the new stream ID + // be monotonically increasing. In reality this is really not possible + // because new streams stop being added to a session with millions of + // IDs still available and no race condition is going to bridge that gap; + // so we can be comfortable on just erroring out for correctness in that + // case. + LOG3(("Stream assigned out of range ID: 0x%X", mStreamID)); + return NS_ERROR_UNEXPECTED; + } + + // Now we need to convert the flat http headers into a set + // of HTTP/2 headers by writing to mTxInlineFrame{sz} + + nsAutoCStringN<1025> compressedData; + nsAutoCString authorityHeader; + nsresult rv = head->GetHeader(nsHttp::Host, authorityHeader); + if (NS_FAILED(rv)) { + MOZ_ASSERT(false); + return rv; + } + + nsDependentCString scheme(head->IsHTTPS() ? "https" : "http"); + if (head->IsConnect()) { + SpdyConnectTransaction* scTrans = + mTransaction->QuerySpdyConnectTransaction(); + MOZ_ASSERT(scTrans); + if (scTrans->IsWebsocket()) { + mIsWebsocket = true; + } else { + mIsTunnel = true; + } + mRequestBodyLenRemaining = 0x0fffffffffffffffULL; + + if (mIsTunnel) { + // Our normal authority has an implicit port, best to use an + // explicit one with a tunnel + nsHttpConnectionInfo* ci = mTransaction->ConnectionInfo(); + if (!ci) { + return NS_ERROR_UNEXPECTED; + } + + authorityHeader = ci->GetOrigin(); + authorityHeader.Append(':'); + authorityHeader.AppendInt(ci->OriginPort()); + } + } + + nsAutoCString method; + nsAutoCString path; + head->Method(method); + head->Path(path); + bool useSimpleConnect = head->IsConnect(); + nsAutoCString protocol; + if (mIsWebsocket) { + useSimpleConnect = false; + protocol.AppendLiteral("websocket"); + } + rv = mSession->Compressor()->EncodeHeaderBlock( + mFlatHttpRequestHeaders, method, path, authorityHeader, scheme, protocol, + useSimpleConnect, compressedData); + NS_ENSURE_SUCCESS(rv, rv); + + int64_t clVal = mSession->Compressor()->GetParsedContentLength(); + if (clVal != -1) { + mRequestBodyLenRemaining = clVal; + } + + // Determine whether to put the fin bit on the header frame or whether + // to wait for a data packet to put it on. + uint8_t firstFrameFlags = Http2Session::kFlag_PRIORITY; + + if (head->IsGet() || head->IsHead()) { + // for GET and HEAD place the fin bit right on the + // header packet + + SetSentFin(true); + firstFrameFlags |= Http2Session::kFlag_END_STREAM; + } else if (head->IsPost() || head->IsPut() || head->IsConnect()) { + // place fin in a data frame even for 0 length messages for iterop + } else if (!mRequestBodyLenRemaining) { + // for other HTTP extension methods, rely on the content-length + // to determine whether or not to put fin on headers + SetSentFin(true); + firstFrameFlags |= Http2Session::kFlag_END_STREAM; + } + + // split this one HEADERS frame up into N HEADERS + CONTINUATION frames if it + // exceeds the 2^14-1 limit for 1 frame. Do it by inserting header size gaps + // in the existing frame for the new headers and for the first one a priority + // field. There is no question this is ugly, but a 16KB HEADERS frame should + // be a long tail event, so this is really just for correctness and a nop in + // the base case. + // + + MOZ_ASSERT(!mTxInlineFrameUsed); + + uint32_t dataLength = compressedData.Length(); + uint32_t maxFrameData = + Http2Session::kMaxFrameData - 5; // 5 bytes for priority + uint32_t numFrames = 1; + + if (dataLength > maxFrameData) { + numFrames += + ((dataLength - maxFrameData) + Http2Session::kMaxFrameData - 1) / + Http2Session::kMaxFrameData; + MOZ_ASSERT(numFrames > 1); + } + + // note that we could still have 1 frame for 0 bytes of data. that's ok. + + uint32_t messageSize = dataLength; + messageSize += Http2Session::kFrameHeaderBytes + + 5; // frame header + priority overhead in HEADERS frame + messageSize += (numFrames - 1) * + Http2Session::kFrameHeaderBytes; // frame header overhead in + // CONTINUATION frames + + EnsureBuffer(mTxInlineFrame, messageSize, mTxInlineFrameUsed, + mTxInlineFrameSize); + + mTxInlineFrameUsed += messageSize; + UpdatePriorityDependency(); + LOG1( + ("Http2Stream %p Generating %d bytes of HEADERS for stream 0x%X with " + "priority weight %u dep 0x%X frames %u uri=%s\n", + this, mTxInlineFrameUsed, mStreamID, mPriorityWeight, + mPriorityDependency, numFrames, requestURI.get())); + + uint32_t outputOffset = 0; + uint32_t compressedDataOffset = 0; + for (uint32_t idx = 0; idx < numFrames; ++idx) { + uint32_t flags, frameLen; + bool lastFrame = (idx == numFrames - 1); + + flags = 0; + frameLen = maxFrameData; + if (!idx) { + flags |= firstFrameFlags; + // Only the first frame needs the 4-byte offset + maxFrameData = Http2Session::kMaxFrameData; + } + if (lastFrame) { + frameLen = dataLength; + flags |= Http2Session::kFlag_END_HEADERS; + } + dataLength -= frameLen; + + mSession->CreateFrameHeader(mTxInlineFrame.get() + outputOffset, + frameLen + (idx ? 0 : 5), + (idx) ? Http2Session::FRAME_TYPE_CONTINUATION + : Http2Session::FRAME_TYPE_HEADERS, + flags, mStreamID); + outputOffset += Http2Session::kFrameHeaderBytes; + + if (!idx) { + uint32_t wireDep = PR_htonl(mPriorityDependency); + memcpy(mTxInlineFrame.get() + outputOffset, &wireDep, 4); + memcpy(mTxInlineFrame.get() + outputOffset + 4, &mPriorityWeight, 1); + outputOffset += 5; + } + + memcpy(mTxInlineFrame.get() + outputOffset, + compressedData.BeginReading() + compressedDataOffset, frameLen); + compressedDataOffset += frameLen; + outputOffset += frameLen; + } + + Telemetry::Accumulate(Telemetry::SPDY_SYN_SIZE, compressedData.Length()); + + // The size of the input headers is approximate + uint32_t ratio = + compressedData.Length() * 100 / + (11 + requestURI.Length() + mFlatHttpRequestHeaders.Length()); + + mFlatHttpRequestHeaders.Truncate(); + Telemetry::Accumulate(Telemetry::SPDY_SYN_RATIO, ratio); + return NS_OK; +} + +void Http2Stream::AdjustInitialWindow() { + // The default initial_window is sized for pushed streams. When we + // generate a client pulled stream we want to disable flow control for + // the stream with a window update. Do the same for pushed streams + // when they connect to a pull. + + // >0 even numbered IDs are pushed streams. + // odd numbered IDs are pulled streams. + // 0 is the sink for a pushed stream. + Http2Stream* stream = this; + if (!mStreamID) { + MOZ_ASSERT(mPushSource); + if (!mPushSource) return; + stream = mPushSource; + MOZ_ASSERT(stream->mStreamID); + MOZ_ASSERT(!(stream->mStreamID & 1)); // is a push stream + + // If the pushed stream has recvd a FIN, there is no reason to update + // the window + if (stream->RecvdFin() || stream->RecvdReset()) return; + } + + if (stream->mState == RESERVED_BY_REMOTE) { + // h2-14 prevents sending a window update in this state + return; + } + + // right now mClientReceiveWindow is the lower push limit + // bump it up to the pull limit set by the channel or session + // don't allow windows less than push + uint32_t bump = 0; + nsHttpTransaction* trans = mTransaction->QueryHttpTransaction(); + if (trans && trans->InitialRwin()) { + bump = (trans->InitialRwin() > mClientReceiveWindow) + ? (trans->InitialRwin() - mClientReceiveWindow) + : 0; + } else { + MOZ_ASSERT(mSession->InitialRwin() >= mClientReceiveWindow); + bump = mSession->InitialRwin() - mClientReceiveWindow; + } + + LOG3(("AdjustInitialwindow increased flow control window %p 0x%X %u\n", this, + stream->mStreamID, bump)); + if (!bump) { // nothing to do + return; + } + + EnsureBuffer(mTxInlineFrame, + mTxInlineFrameUsed + Http2Session::kFrameHeaderBytes + 4, + mTxInlineFrameUsed, mTxInlineFrameSize); + uint8_t* packet = mTxInlineFrame.get() + mTxInlineFrameUsed; + mTxInlineFrameUsed += Http2Session::kFrameHeaderBytes + 4; + + mSession->CreateFrameHeader(packet, 4, Http2Session::FRAME_TYPE_WINDOW_UPDATE, + 0, stream->mStreamID); + + mClientReceiveWindow += bump; + bump = PR_htonl(bump); + memcpy(packet + Http2Session::kFrameHeaderBytes, &bump, 4); +} + +void Http2Stream::AdjustPushedPriority() { + // >0 even numbered IDs are pushed streams. odd numbered IDs are pulled + // streams. 0 is the sink for a pushed stream. + + if (mStreamID || !mPushSource) return; + + MOZ_ASSERT(mPushSource->mStreamID && !(mPushSource->mStreamID & 1)); + + // If the pushed stream has recvd a FIN, there is no reason to update + // the window + if (mPushSource->RecvdFin() || mPushSource->RecvdReset()) return; + + // Ensure we pick up the right dependency to place the pushed stream under. + UpdatePriorityDependency(); + + EnsureBuffer(mTxInlineFrame, + mTxInlineFrameUsed + Http2Session::kFrameHeaderBytes + 5, + mTxInlineFrameUsed, mTxInlineFrameSize); + uint8_t* packet = mTxInlineFrame.get() + mTxInlineFrameUsed; + mTxInlineFrameUsed += Http2Session::kFrameHeaderBytes + 5; + + mSession->CreateFrameHeader(packet, 5, Http2Session::FRAME_TYPE_PRIORITY, 0, + mPushSource->mStreamID); + + mPushSource->SetPriorityDependency(mPriority, mPriorityDependency); + uint32_t wireDep = PR_htonl(mPriorityDependency); + memcpy(packet + Http2Session::kFrameHeaderBytes, &wireDep, 4); + memcpy(packet + Http2Session::kFrameHeaderBytes + 4, &mPriorityWeight, 1); + + LOG3(("AdjustPushedPriority %p id 0x%X to dep %X weight %X\n", this, + mPushSource->mStreamID, mPriorityDependency, mPriorityWeight)); +} + +void Http2Stream::UpdateTransportReadEvents(uint32_t count) { + mTotalRead += count; + if (!mSocketTransport) { + return; + } + + mTransaction->OnTransportStatus(mSocketTransport, + NS_NET_STATUS_RECEIVING_FROM, mTotalRead); +} + +void Http2Stream::UpdateTransportSendEvents(uint32_t count) { + mTotalSent += count; + + // normally on non-windows platform we use TCP autotuning for + // the socket buffers, and this works well (managing enough + // buffers for BDP while conserving memory) for HTTP even when + // it creates really deep queues. However this 'buffer bloat' is + // a problem for http/2 because it ruins the low latency properties + // necessary for PING and cancel to work meaningfully. + // + // If this stream represents a large upload, disable autotuning for + // the session and cap the send buffers by default at 128KB. + // (10Mbit/sec @ 100ms) + // + uint32_t bufferSize = gHttpHandler->SpdySendBufferSize(); + if ((mTotalSent > bufferSize) && !mSetTCPSocketBuffer) { + mSetTCPSocketBuffer = 1; + mSocketTransport->SetSendBufferSize(bufferSize); + } + + if (mUpstreamState != SENDING_FIN_STREAM) + mTransaction->OnTransportStatus(mSocketTransport, NS_NET_STATUS_SENDING_TO, + mTotalSent); + + if (!mSentWaitingFor && !mRequestBodyLenRemaining) { + mSentWaitingFor = 1; + mTransaction->OnTransportStatus(mSocketTransport, NS_NET_STATUS_WAITING_FOR, + 0); + } +} + +nsresult Http2Stream::TransmitFrame(const char* buf, uint32_t* countUsed, + bool forceCommitment) { + // If TransmitFrame returns SUCCESS than all the data is sent (or at least + // buffered at the session level), if it returns WOULD_BLOCK then none of + // the data is sent. + + // You can call this function with no data and no out parameter in order to + // flush internal buffers that were previously blocked on writing. You can + // of course feed new data to it as well. + + LOG3(("Http2Stream::TransmitFrame %p inline=%d stream=%d", this, + mTxInlineFrameUsed, mTxStreamFrameSize)); + if (countUsed) *countUsed = 0; + + if (!mTxInlineFrameUsed) { + MOZ_ASSERT(!buf); + return NS_OK; + } + + MOZ_ASSERT(mTxInlineFrameUsed, "empty stream frame in transmit"); + MOZ_ASSERT(mSegmentReader, "TransmitFrame with null mSegmentReader"); + MOZ_ASSERT((buf && countUsed) || (!buf && !countUsed), + "TransmitFrame arguments inconsistent"); + + uint32_t transmittedCount; + nsresult rv; + + // In the (relatively common) event that we have a small amount of data + // split between the inlineframe and the streamframe, then move the stream + // data into the inlineframe via copy in order to coalesce into one write. + // Given the interaction with ssl this is worth the small copy cost. + if (mTxStreamFrameSize && mTxInlineFrameUsed && + mTxStreamFrameSize < Http2Session::kDefaultBufferSize && + mTxInlineFrameUsed + mTxStreamFrameSize < mTxInlineFrameSize) { + LOG3(("Coalesce Transmit")); + memcpy(&mTxInlineFrame[mTxInlineFrameUsed], buf, mTxStreamFrameSize); + if (countUsed) *countUsed += mTxStreamFrameSize; + mTxInlineFrameUsed += mTxStreamFrameSize; + mTxStreamFrameSize = 0; + } + + rv = mSegmentReader->CommitToSegmentSize( + mTxStreamFrameSize + mTxInlineFrameUsed, forceCommitment); + + if (rv == NS_BASE_STREAM_WOULD_BLOCK) { + MOZ_ASSERT(!forceCommitment, "forceCommitment with WOULD_BLOCK"); + mSession->TransactionHasDataToWrite(this); + } + if (NS_FAILED(rv)) // this will include WOULD_BLOCK + return rv; + + // This function calls mSegmentReader->OnReadSegment to report the actual + // http/2 bytes through to the session object and then the HttpConnection + // which calls the socket write function. It will accept all of the inline and + // stream data because of the above 'commitment' even if it has to buffer + + rv = mSession->BufferOutput(reinterpret_cast(mTxInlineFrame.get()), + mTxInlineFrameUsed, &transmittedCount); + LOG3( + ("Http2Stream::TransmitFrame for inline BufferOutput session=%p " + "stream=%p result %" PRIx32 " len=%d", + mSession, this, static_cast(rv), transmittedCount)); + + MOZ_ASSERT(rv != NS_BASE_STREAM_WOULD_BLOCK, + "inconsistent inline commitment result"); + + if (NS_FAILED(rv)) return rv; + + MOZ_ASSERT(transmittedCount == mTxInlineFrameUsed, + "inconsistent inline commitment count"); + + Http2Session::LogIO(mSession, this, "Writing from Inline Buffer", + reinterpret_cast(mTxInlineFrame.get()), + transmittedCount); + + if (mTxStreamFrameSize) { + if (!buf) { + // this cannot happen + MOZ_ASSERT(false, + "Stream transmit with null buf argument to " + "TransmitFrame()"); + LOG3(("Stream transmit with null buf argument to TransmitFrame()\n")); + return NS_ERROR_UNEXPECTED; + } + + // If there is already data buffered, just add to that to form + // a single TLS Application Data Record - otherwise skip the memcpy + if (mSession->AmountOfOutputBuffered()) { + rv = mSession->BufferOutput(buf, mTxStreamFrameSize, &transmittedCount); + } else { + rv = mSession->OnReadSegment(buf, mTxStreamFrameSize, &transmittedCount); + } + + LOG3( + ("Http2Stream::TransmitFrame for regular session=%p " + "stream=%p result %" PRIx32 " len=%d", + mSession, this, static_cast(rv), transmittedCount)); + + MOZ_ASSERT(rv != NS_BASE_STREAM_WOULD_BLOCK, + "inconsistent stream commitment result"); + + if (NS_FAILED(rv)) return rv; + + MOZ_ASSERT(transmittedCount == mTxStreamFrameSize, + "inconsistent stream commitment count"); + + Http2Session::LogIO(mSession, this, "Writing from Transaction Buffer", buf, + transmittedCount); + + *countUsed += mTxStreamFrameSize; + } + + if (!mAttempting0RTT) { + mSession->FlushOutputQueue(); + } + + // calling this will trigger waiting_for if mRequestBodyLenRemaining is 0 + UpdateTransportSendEvents(mTxInlineFrameUsed + mTxStreamFrameSize); + + mTxInlineFrameUsed = 0; + mTxStreamFrameSize = 0; + + return NS_OK; +} + +void Http2Stream::ChangeState(enum upstreamStateType newState) { + LOG3(("Http2Stream::ChangeState() %p from %X to %X", this, mUpstreamState, + newState)); + mUpstreamState = newState; +} + +void Http2Stream::GenerateDataFrameHeader(uint32_t dataLength, bool lastFrame) { + LOG3(("Http2Stream::GenerateDataFrameHeader %p len=%d last=%d", this, + dataLength, lastFrame)); + + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + MOZ_ASSERT(!mTxInlineFrameUsed, "inline frame not empty"); + MOZ_ASSERT(!mTxStreamFrameSize, "stream frame not empty"); + + uint8_t frameFlags = 0; + if (lastFrame) { + frameFlags |= Http2Session::kFlag_END_STREAM; + if (dataLength) SetSentFin(true); + } + + mSession->CreateFrameHeader(mTxInlineFrame.get(), dataLength, + Http2Session::FRAME_TYPE_DATA, frameFlags, + mStreamID); + + mTxInlineFrameUsed = Http2Session::kFrameHeaderBytes; + mTxStreamFrameSize = dataLength; +} + +// ConvertResponseHeaders is used to convert the response headers +// into HTTP/1 format and report some telemetry +nsresult Http2Stream::ConvertResponseHeaders(Http2Decompressor* decompressor, + nsACString& aHeadersIn, + nsACString& aHeadersOut, + int32_t& httpResponseCode) { + nsresult rv = decompressor->DecodeHeaderBlock( + reinterpret_cast(aHeadersIn.BeginReading()), + aHeadersIn.Length(), aHeadersOut, false); + if (NS_FAILED(rv)) { + LOG3(("Http2Stream::ConvertResponseHeaders %p decode Error\n", this)); + return rv; + } + + nsAutoCString statusString; + decompressor->GetStatus(statusString); + if (statusString.IsEmpty()) { + LOG3(("Http2Stream::ConvertResponseHeaders %p Error - no status\n", this)); + return NS_ERROR_ILLEGAL_VALUE; + } + + nsresult errcode; + httpResponseCode = statusString.ToInteger(&errcode); + + // Ensure the :status is just an HTTP status code + // https://tools.ietf.org/html/rfc7540#section-8.1.2.4 + // https://bugzilla.mozilla.org/show_bug.cgi?id=1352146 + nsAutoCString parsedStatusString; + parsedStatusString.AppendInt(httpResponseCode); + if (!parsedStatusString.Equals(statusString)) { + LOG3(("Http2Stream::ConvertResposeHeaders %p status %s is not just a code", + this, statusString.BeginReading())); + // Results in stream reset with PROTOCOL_ERROR + return NS_ERROR_ILLEGAL_VALUE; + } + + LOG3(("Http2Stream::ConvertResponseHeaders %p response code %d\n", this, + httpResponseCode)); + if (mIsTunnel) { + LOG3(("Http2Stream %p Tunnel Response code %d", this, httpResponseCode)); + // 1xx response is simply skipeed and a final response is expected. + // 2xx response needs to be encrypted. + if ((httpResponseCode / 100) > 2) { + MapStreamToPlainText(); + } + if (MapStreamToHttpConnection(aHeadersOut, httpResponseCode)) { + // Process transactions only if we have a final response, i.e., response + // code >= 200. + ClearTransactionsBlockedOnTunnel(); + } + } else if (mIsWebsocket) { + LOG3(("Http2Stream %p websocket response code %d", this, httpResponseCode)); + if (httpResponseCode == 200) { + MapStreamToHttpConnection(aHeadersOut); + } + } + + if (httpResponseCode == 421) { + // Origin Frame requires 421 to remove this origin from the origin set + mSession->Received421(mTransaction->ConnectionInfo()); + } + + if (aHeadersIn.Length() && aHeadersOut.Length()) { + Telemetry::Accumulate(Telemetry::SPDY_SYN_REPLY_SIZE, aHeadersIn.Length()); + uint32_t ratio = aHeadersIn.Length() * 100 / aHeadersOut.Length(); + Telemetry::Accumulate(Telemetry::SPDY_SYN_REPLY_RATIO, ratio); + } + + // The decoding went ok. Now we can customize and clean up. + + aHeadersIn.Truncate(); + aHeadersOut.AppendLiteral("X-Firefox-Spdy: h2"); + aHeadersOut.AppendLiteral("\r\n\r\n"); + LOG(("decoded response headers are:\n%s", aHeadersOut.BeginReading())); + if (mIsTunnel && !mPlainTextTunnel) { + aHeadersOut.Truncate(); + LOG(("Http2Stream::ConvertHeaders %p 0x%X headers removed for tunnel\n", + this, mStreamID)); + } + return NS_OK; +} + +// ConvertPushHeaders is used to convert the pushed request headers +// into HTTP/1 format and report some telemetry +nsresult Http2Stream::ConvertPushHeaders(Http2Decompressor* decompressor, + nsACString& aHeadersIn, + nsACString& aHeadersOut) { + nsresult rv = decompressor->DecodeHeaderBlock( + reinterpret_cast(aHeadersIn.BeginReading()), + aHeadersIn.Length(), aHeadersOut, true); + if (NS_FAILED(rv)) { + LOG3(("Http2Stream::ConvertPushHeaders %p Error\n", this)); + return rv; + } + + nsCString method; + decompressor->GetHost(mHeaderHost); + decompressor->GetScheme(mHeaderScheme); + decompressor->GetPath(mHeaderPath); + + if (mHeaderHost.IsEmpty() || mHeaderScheme.IsEmpty() || + mHeaderPath.IsEmpty()) { + LOG3( + ("Http2Stream::ConvertPushHeaders %p Error - missing required " + "host=%s scheme=%s path=%s\n", + this, mHeaderHost.get(), mHeaderScheme.get(), mHeaderPath.get())); + return NS_ERROR_ILLEGAL_VALUE; + } + + decompressor->GetMethod(method); + if (!method.EqualsLiteral("GET")) { + LOG3(( + "Http2Stream::ConvertPushHeaders %p Error - method not supported: %s\n", + this, method.get())); + return NS_ERROR_NOT_IMPLEMENTED; + } + + aHeadersIn.Truncate(); + LOG(("id 0x%X decoded push headers %s %s %s are:\n%s", mStreamID, + mHeaderScheme.get(), mHeaderHost.get(), mHeaderPath.get(), + aHeadersOut.BeginReading())); + return NS_OK; +} + +nsresult Http2Stream::ConvertResponseTrailers(Http2Decompressor* decompressor, + nsACString& aTrailersIn) { + LOG3(("Http2Stream::ConvertResponseTrailers %p", this)); + nsAutoCString flatTrailers; + + nsresult rv = decompressor->DecodeHeaderBlock( + reinterpret_cast(aTrailersIn.BeginReading()), + aTrailersIn.Length(), flatTrailers, false); + if (NS_FAILED(rv)) { + LOG3(("Http2Stream::ConvertResponseTrailers %p decode Error", this)); + return rv; + } + + nsHttpTransaction* trans = mTransaction->QueryHttpTransaction(); + if (trans) { + trans->SetHttpTrailers(flatTrailers); + } else { + LOG3(("Http2Stream::ConvertResponseTrailers %p no trans", this)); + } + + return NS_OK; +} + +void Http2Stream::Close(nsresult reason) { + // In case we are connected to a push, make sure the push knows we are closed, + // so it doesn't try to give us any more DATA that comes on it after our + // close. + ClearPushSource(); + + mTransaction->Close(reason); +} + +void Http2Stream::SetResponseIsComplete() { + nsHttpTransaction* trans = mTransaction->QueryHttpTransaction(); + if (trans) { + trans->SetResponseIsComplete(); + } +} + +void Http2Stream::SetAllHeadersReceived() { + if (mAllHeadersReceived) { + return; + } + + if (mState == RESERVED_BY_REMOTE) { + // pushed streams needs to wait until headers have + // arrived to open up their window + LOG3(("Http2Stream::SetAllHeadersReceived %p state OPEN from reserved\n", + this)); + mState = OPEN; + AdjustInitialWindow(); + } + + mAllHeadersReceived = 1; +} + +bool Http2Stream::AllowFlowControlledWrite() { + return (mSession->ServerSessionWindow() > 0) && (mServerReceiveWindow > 0); +} + +void Http2Stream::UpdateServerReceiveWindow(int32_t delta) { + mServerReceiveWindow += delta; + + if (mBlockedOnRwin && AllowFlowControlledWrite()) { + LOG3( + ("Http2Stream::UpdateServerReceived UnPause %p 0x%X " + "Open stream window\n", + this, mStreamID)); + mSession->TransactionHasDataToWrite(this); + } +} + +void Http2Stream::SetPriority(uint32_t newPriority) { + int32_t httpPriority = static_cast(newPriority); + if (httpPriority > kWorstPriority) { + httpPriority = kWorstPriority; + } else if (httpPriority < kBestPriority) { + httpPriority = kBestPriority; + } + mPriority = static_cast(httpPriority); + mPriorityWeight = (nsISupportsPriority::PRIORITY_LOWEST + 1) - + (httpPriority - kNormalPriority); + + mPriorityDependency = 0; // maybe adjusted later +} + +void Http2Stream::SetPriorityDependency(uint32_t newPriority, + uint32_t newDependency) { + SetPriority(newPriority); + mPriorityDependency = newDependency; +} + +static uint32_t GetPriorityDependencyFromTransaction(nsHttpTransaction* trans) { + MOZ_ASSERT(trans); + + uint32_t classFlags = trans->ClassOfService(); + + if (classFlags & nsIClassOfService::UrgentStart) { + return Http2Session::kUrgentStartGroupID; + } + + if (classFlags & nsIClassOfService::Leader) { + return Http2Session::kLeaderGroupID; + } + + if (classFlags & nsIClassOfService::Follower) { + return Http2Session::kFollowerGroupID; + } + + if (classFlags & nsIClassOfService::Speculative) { + return Http2Session::kSpeculativeGroupID; + } + + if (classFlags & nsIClassOfService::Background) { + return Http2Session::kBackgroundGroupID; + } + + if (classFlags & nsIClassOfService::Unblocked) { + return Http2Session::kOtherGroupID; + } + + return Http2Session::kFollowerGroupID; // unmarked followers +} + +void Http2Stream::UpdatePriorityDependency() { + if (!mSession->UseH2Deps()) { + return; + } + + nsHttpTransaction* trans = mTransaction->QueryHttpTransaction(); + if (!trans) { + return; + } + + // we create 6 fake dependency streams per session, + // these streams are never opened with HEADERS. our first opened stream is 0xd + // 3 depends 0, weight 200, leader class (kLeaderGroupID) + // 5 depends 0, weight 100, other (kOtherGroupID) + // 7 depends 0, weight 0, background (kBackgroundGroupID) + // 9 depends 7, weight 0, speculative (kSpeculativeGroupID) + // b depends 3, weight 0, follower class (kFollowerGroupID) + // d depends 0, weight 240, urgent-start class (kUrgentStartGroupID) + // + // streams for leaders (html, js, css) depend on 3 + // streams for folowers (images) depend on b + // default streams (xhr, async js) depend on 5 + // explicit bg streams (beacon, etc..) depend on 7 + // spculative bg streams depend on 9 + // urgent-start streams depend on d + + mPriorityDependency = GetPriorityDependencyFromTransaction(trans); + + if (gHttpHandler->ActiveTabPriority() && + mTransactionTabId != mCurrentForegroundTabOuterContentWindowId && + mPriorityDependency != Http2Session::kUrgentStartGroupID) { + LOG3( + ("Http2Stream::UpdatePriorityDependency %p " + " depends on background group for trans %p\n", + this, trans)); + mPriorityDependency = Http2Session::kBackgroundGroupID; + + nsHttp::NotifyActiveTabLoadOptimization(); + } + + LOG1( + ("Http2Stream::UpdatePriorityDependency %p " + "depends on stream 0x%X\n", + this, mPriorityDependency)); +} + +void Http2Stream::TopLevelOuterContentWindowIdChanged(uint64_t windowId) { + if (!mStreamID) { + // For pushed streams, we ignore the direct call from the session and + // instead let it come to the internal function from the pushed stream, so + // we don't accidentally send two PRIORITY frames for the same stream. + return; + } + + TopLevelOuterContentWindowIdChangedInternal(windowId); +} + +void Http2Stream::TopLevelOuterContentWindowIdChangedInternal( + uint64_t windowId) { + MOZ_ASSERT(gHttpHandler->ActiveTabPriority()); + + LOG3( + ("Http2Stream::TopLevelOuterContentWindowIdChanged " + "%p windowId=%" PRIx64 "\n", + this, windowId)); + + mCurrentForegroundTabOuterContentWindowId = windowId; + + if (!mSession->UseH2Deps()) { + return; + } + + // Urgent start takes an absolute precedence, so don't + // change mPriorityDependency here. + if (mPriorityDependency == Http2Session::kUrgentStartGroupID) { + return; + } + + if (mTransactionTabId != mCurrentForegroundTabOuterContentWindowId) { + mPriorityDependency = Http2Session::kBackgroundGroupID; + LOG3( + ("Http2Stream::TopLevelOuterContentWindowIdChanged %p " + "move into background group.\n", + this)); + + nsHttp::NotifyActiveTabLoadOptimization(); + } else { + nsHttpTransaction* trans = mTransaction->QueryHttpTransaction(); + if (!trans) { + return; + } + + mPriorityDependency = GetPriorityDependencyFromTransaction(trans); + LOG3( + ("Http2Stream::TopLevelOuterContentWindowIdChanged %p " + "depends on stream 0x%X\n", + this, mPriorityDependency)); + } + + uint32_t modifyStreamID = mStreamID; + if (!modifyStreamID && mPushSource) { + modifyStreamID = mPushSource->StreamID(); + } + if (modifyStreamID) { + mSession->SendPriorityFrame(modifyStreamID, mPriorityDependency, + mPriorityWeight); + } +} + +void Http2Stream::SetRecvdFin(bool aStatus) { + mRecvdFin = aStatus ? 1 : 0; + if (!aStatus) return; + + if (mState == OPEN || mState == RESERVED_BY_REMOTE) { + mState = CLOSED_BY_REMOTE; + } else if (mState == CLOSED_BY_LOCAL) { + mState = CLOSED; + } +} + +void Http2Stream::SetSentFin(bool aStatus) { + mSentFin = aStatus ? 1 : 0; + if (!aStatus) return; + + if (mState == OPEN || mState == RESERVED_BY_REMOTE) { + mState = CLOSED_BY_LOCAL; + } else if (mState == CLOSED_BY_REMOTE) { + mState = CLOSED; + } +} + +void Http2Stream::SetRecvdReset(bool aStatus) { + mRecvdReset = aStatus ? 1 : 0; + if (!aStatus) return; + mState = CLOSED; +} + +void Http2Stream::SetSentReset(bool aStatus) { + mSentReset = aStatus ? 1 : 0; + if (!aStatus) return; + mState = CLOSED; +} + +//----------------------------------------------------------------------------- +// nsAHttpSegmentReader +//----------------------------------------------------------------------------- + +nsresult Http2Stream::OnReadSegment(const char* buf, uint32_t count, + uint32_t* countRead) { + LOG3(("Http2Stream::OnReadSegment %p count=%d state=%x", this, count, + mUpstreamState)); + + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + MOZ_ASSERT(mSegmentReader, "OnReadSegment with null mSegmentReader"); + + nsresult rv = NS_ERROR_UNEXPECTED; + uint32_t dataLength; + + switch (mUpstreamState) { + case GENERATING_HEADERS: + // The buffer is the HTTP request stream, including at least part of the + // HTTP request header. This state's job is to build a HEADERS frame + // from the header information. count is the number of http bytes + // available (which may include more than the header), and in countRead we + // return the number of those bytes that we consume (i.e. the portion that + // are header bytes) + + if (!mRequestHeadersDone) { + if (NS_FAILED(rv = ParseHttpRequestHeaders(buf, count, countRead))) { + return rv; + } + } + + if (mRequestHeadersDone && !mOpenGenerated) { + if (!mSession->TryToActivate(this)) { + LOG3(("Http2Stream::OnReadSegment %p cannot activate now. queued.\n", + this)); + return *countRead ? NS_OK : NS_BASE_STREAM_WOULD_BLOCK; + } + if (NS_FAILED(rv = GenerateOpen())) { + return rv; + } + } + + LOG3( + ("ParseHttpRequestHeaders %p used %d of %d. " + "requestheadersdone = %d mOpenGenerated = %d\n", + this, *countRead, count, mRequestHeadersDone, mOpenGenerated)); + if (mOpenGenerated) { + SetHTTPState(OPEN); + AdjustInitialWindow(); + // This version of TransmitFrame cannot block + rv = TransmitFrame(nullptr, nullptr, true); + ChangeState(GENERATING_BODY); + break; + } + MOZ_ASSERT(*countRead == count, + "Header parsing not complete but unused data"); + break; + + case GENERATING_BODY: + // if there is session flow control and either the stream window is active + // and exhaused or the session window is exhausted then suspend + if (!AllowFlowControlledWrite()) { + *countRead = 0; + LOG3( + ("Http2Stream this=%p, id 0x%X request body suspended because " + "remote window is stream=%" PRId64 " session=%" PRId64 ".\n", + this, mStreamID, mServerReceiveWindow, + mSession->ServerSessionWindow())); + mBlockedOnRwin = true; + return NS_BASE_STREAM_WOULD_BLOCK; + } + mBlockedOnRwin = false; + + // The chunk is the smallest of: availableData, configured chunkSize, + // stream window, session window, or 14 bit framing limit. + // Its amazing we send anything at all. + dataLength = std::min(count, mChunkSize); + + if (dataLength > Http2Session::kMaxFrameData) + dataLength = Http2Session::kMaxFrameData; + + if (dataLength > mSession->ServerSessionWindow()) + dataLength = static_cast(mSession->ServerSessionWindow()); + + if (dataLength > mServerReceiveWindow) + dataLength = static_cast(mServerReceiveWindow); + + LOG3( + ("Http2Stream this=%p id 0x%X send calculation " + "avail=%d chunksize=%d stream window=%" PRId64 + " session window=%" PRId64 " " + "max frame=%d USING=%u\n", + this, mStreamID, count, mChunkSize, mServerReceiveWindow, + mSession->ServerSessionWindow(), Http2Session::kMaxFrameData, + dataLength)); + + mSession->DecrementServerSessionWindow(dataLength); + mServerReceiveWindow -= dataLength; + + LOG3(("Http2Stream %p id 0x%x request len remaining %" PRId64 ", " + "count avail %u, chunk used %u", + this, mStreamID, mRequestBodyLenRemaining, count, dataLength)); + if (!dataLength && mRequestBodyLenRemaining) { + return NS_BASE_STREAM_WOULD_BLOCK; + } + if (dataLength > mRequestBodyLenRemaining) { + return NS_ERROR_UNEXPECTED; + } + mRequestBodyLenRemaining -= dataLength; + GenerateDataFrameHeader(dataLength, !mRequestBodyLenRemaining); + ChangeState(SENDING_BODY); + [[fallthrough]]; + + case SENDING_BODY: + MOZ_ASSERT(mTxInlineFrameUsed, "OnReadSegment Send Data Header 0b"); + rv = TransmitFrame(buf, countRead, false); + MOZ_ASSERT(NS_FAILED(rv) || !mTxInlineFrameUsed, + "Transmit Frame should be all or nothing"); + + LOG3(("TransmitFrame() rv=%" PRIx32 " returning %d data bytes. " + "Header is %d Body is %d.", + static_cast(rv), *countRead, mTxInlineFrameUsed, + mTxStreamFrameSize)); + + // normalize a partial write with a WOULD_BLOCK into just a partial write + // as some code will take WOULD_BLOCK to mean an error with nothing + // written (e.g. nsHttpTransaction::ReadRequestSegment() + if (rv == NS_BASE_STREAM_WOULD_BLOCK && *countRead) rv = NS_OK; + + // If that frame was all sent, look for another one + if (!mTxInlineFrameUsed) ChangeState(GENERATING_BODY); + break; + + case SENDING_FIN_STREAM: + MOZ_ASSERT(false, "resuming partial fin stream out of OnReadSegment"); + break; + + case UPSTREAM_COMPLETE: + MOZ_ASSERT(mPushSource); + rv = TransmitFrame(nullptr, nullptr, true); + break; + + default: + MOZ_ASSERT(false, "Http2Stream::OnReadSegment non-write state"); + break; + } + + return rv; +} + +//----------------------------------------------------------------------------- +// nsAHttpSegmentWriter +//----------------------------------------------------------------------------- + +nsresult Http2Stream::OnWriteSegment(char* buf, uint32_t count, + uint32_t* countWritten) { + LOG3(("Http2Stream::OnWriteSegment %p count=%d state=%x 0x%X\n", this, count, + mUpstreamState, mStreamID)); + + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + MOZ_ASSERT(mSegmentWriter); + + if (mPushSource) { + nsresult rv; + rv = mPushSource->GetBufferedData(buf, count, countWritten); + if (NS_FAILED(rv)) return rv; + + mSession->ConnectPushedStream(this); + return NS_OK; + } + + // sometimes we have read data from the network and stored it in a pipe + // so that other streams can proceed when the gecko caller is not processing + // data events fast enough and flow control hasn't caught up yet. This + // gets the stored data out of that pipe + if (!mBypassInputBuffer && mSimpleBuffer.Available()) { + *countWritten = mSimpleBuffer.Read(buf, count); + MOZ_ASSERT(*countWritten); + LOG3( + ("Http2Stream::OnWriteSegment read from flow control buffer %p %x %d\n", + this, mStreamID, *countWritten)); + return NS_OK; + } + + // read from the network + return mSegmentWriter->OnWriteSegment(buf, count, countWritten); +} + +/// connect tunnels + +nsCString& Http2Stream::RegistrationKey() { + if (mRegistrationKey.IsEmpty()) { + MOZ_ASSERT(Transaction()); + MOZ_ASSERT(Transaction()->ConnectionInfo()); + + mRegistrationKey = Transaction()->ConnectionInfo()->HashKey(); + } + + return mRegistrationKey; +} + +void Http2Stream::ClearTransactionsBlockedOnTunnel() { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + if (!mIsTunnel) { + return; + } + nsresult rv = + gHttpHandler->ConnMgr()->ProcessPendingQ(mTransaction->ConnectionInfo()); + if (NS_FAILED(rv)) { + LOG3( + ("Http2Stream::ClearTransactionsBlockedOnTunnel %p\n" + " ProcessPendingQ failed: %08x\n", + this, static_cast(rv))); + } +} + +void Http2Stream::MapStreamToPlainText() { + RefPtr qiTrans( + mTransaction->QuerySpdyConnectTransaction()); + MOZ_ASSERT(qiTrans); + mPlainTextTunnel = true; + qiTrans->ForcePlainText(); +} + +bool Http2Stream::MapStreamToHttpConnection(const nsACString& aFlat407Headers, + int32_t aHttpResponseCode) { + RefPtr qiTrans( + mTransaction->QuerySpdyConnectTransaction()); + MOZ_ASSERT(qiTrans); + + return qiTrans->MapStreamToHttpConnection( + mSocketTransport, mTransaction->ConnectionInfo(), aFlat407Headers, + mIsTunnel ? aHttpResponseCode : -1); +} + +// ----------------------------------------------------------------------------- +// mirror nsAHttpTransaction +// ----------------------------------------------------------------------------- + +bool Http2Stream::Do0RTT() { + MOZ_ASSERT(mTransaction); + mAttempting0RTT = mTransaction->Do0RTT(); + return mAttempting0RTT; +} + +nsresult Http2Stream::Finish0RTT(bool aRestart, bool aAlpnChanged) { + MOZ_ASSERT(mTransaction); + mAttempting0RTT = false; + // Instead of passing (aRestart, aAlpnChanged) here, we use aAlpnChanged for + // both arguments because as long as the alpn token stayed the same, we can + // just reuse what we have in our buffer to send instead of having to have + // the transaction rewind and read it all over again. We only need to rewind + // the transaction if we're switching to a new protocol, because our buffer + // won't get used in that case. + // .. + // however, we send in the aRestart value to indicate that early data failed + // for devtools purposes + nsresult rv = mTransaction->Finish0RTT(aAlpnChanged, aAlpnChanged); + if (aRestart) { + nsHttpTransaction* trans = mTransaction->QueryHttpTransaction(); + if (trans) { + trans->Refused0RTT(); + } + } + return rv; +} + +nsresult Http2Stream::GetOriginAttributes(mozilla::OriginAttributes* oa) { + if (!mSocketTransport) { + return NS_ERROR_UNEXPECTED; + } + + return mSocketTransport->GetOriginAttributes(oa); +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/Http2Stream.h b/netwerk/protocol/http/Http2Stream.h new file mode 100644 index 0000000000..197ccfce6a --- /dev/null +++ b/netwerk/protocol/http/Http2Stream.h @@ -0,0 +1,395 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef mozilla_net_Http2Stream_h +#define mozilla_net_Http2Stream_h + +// HTTP/2 - RFC7540 +// https://www.rfc-editor.org/rfc/rfc7540.txt + +#include "mozilla/Attributes.h" +#include "mozilla/UniquePtr.h" +#include "nsAHttpTransaction.h" +#include "nsISupportsPriority.h" +#include "SimpleBuffer.h" +#include "nsISupportsImpl.h" + +class nsIInputStream; +class nsIOutputStream; + +namespace mozilla { +class OriginAttributes; +} + +namespace mozilla { +namespace net { + +class nsStandardURL; +class Http2Session; +class Http2Decompressor; + +class Http2Stream : public nsAHttpSegmentReader, + public nsAHttpSegmentWriter, + public SupportsWeakPtr { + public: + NS_DECL_NSAHTTPSEGMENTREADER + NS_DECL_NSAHTTPSEGMENTWRITER + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(Http2Stream, override) + + enum stateType { + IDLE, + RESERVED_BY_REMOTE, + OPEN, + CLOSED_BY_LOCAL, + CLOSED_BY_REMOTE, + CLOSED + }; + + const static int32_t kNormalPriority = 0x1000; + const static int32_t kWorstPriority = + kNormalPriority + nsISupportsPriority::PRIORITY_LOWEST; + const static int32_t kBestPriority = + kNormalPriority + nsISupportsPriority::PRIORITY_HIGHEST; + + Http2Stream(nsAHttpTransaction*, Http2Session*, int32_t, uint64_t); + + uint32_t StreamID() { return mStreamID; } + Http2PushedStream* PushSource() { return mPushSource; } + void ClearPushSource(); + + stateType HTTPState() { return mState; } + void SetHTTPState(stateType val) { mState = val; } + + [[nodiscard]] virtual nsresult ReadSegments(nsAHttpSegmentReader*, uint32_t, + uint32_t*); + [[nodiscard]] virtual nsresult WriteSegments(nsAHttpSegmentWriter*, uint32_t, + uint32_t*); + virtual bool DeferCleanup(nsresult status); + + // The consumer stream is the synthetic pull stream hooked up to this stream + // http2PushedStream overrides it + virtual Http2Stream* GetConsumerStream() { return nullptr; }; + + const nsCString& Origin() const { return mOrigin; } + const nsCString& Host() const { return mHeaderHost; } + const nsCString& Path() const { return mHeaderPath; } + + bool RequestBlockedOnRead() { + return static_cast(mRequestBlockedOnRead); + } + + bool HasRegisteredID() { return mStreamID != 0; } + + nsAHttpTransaction* Transaction() { return mTransaction; } + virtual nsIRequestContext* RequestContext() { + return mTransaction ? mTransaction->RequestContext() : nullptr; + } + + void Close(nsresult reason); + void SetResponseIsComplete(); + + void SetRecvdFin(bool aStatus); + bool RecvdFin() { return mRecvdFin; } + + void SetRecvdData(bool aStatus) { mReceivedData = aStatus ? 1 : 0; } + bool RecvdData() { return mReceivedData; } + + void SetSentFin(bool aStatus); + bool SentFin() { return mSentFin; } + + void SetRecvdReset(bool aStatus); + bool RecvdReset() { return mRecvdReset; } + + void SetSentReset(bool aStatus); + bool SentReset() { return mSentReset; } + + void SetQueued(bool aStatus) { mQueued = aStatus ? 1 : 0; } + bool Queued() { return mQueued; } + + void SetCountAsActive(bool aStatus) { mCountAsActive = aStatus ? 1 : 0; } + bool CountAsActive() { return mCountAsActive; } + + void SetAllHeadersReceived(); + void UnsetAllHeadersReceived() { mAllHeadersReceived = 0; } + bool AllHeadersReceived() { return mAllHeadersReceived; } + + void UpdateTransportSendEvents(uint32_t count); + void UpdateTransportReadEvents(uint32_t count); + + // NS_ERROR_ABORT terminates stream, other failure terminates session + [[nodiscard]] nsresult ConvertResponseHeaders(Http2Decompressor*, nsACString&, + nsACString&, int32_t&); + [[nodiscard]] nsresult ConvertPushHeaders(Http2Decompressor*, nsACString&, + nsACString&); + [[nodiscard]] nsresult ConvertResponseTrailers(Http2Decompressor*, + nsACString&); + + bool AllowFlowControlledWrite(); + void UpdateServerReceiveWindow(int32_t delta); + int64_t ServerReceiveWindow() { return mServerReceiveWindow; } + + void DecrementClientReceiveWindow(uint32_t delta) { + mClientReceiveWindow -= delta; + mLocalUnacked += delta; + } + + void IncrementClientReceiveWindow(uint32_t delta) { + mClientReceiveWindow += delta; + mLocalUnacked -= delta; + } + + uint64_t LocalUnAcked(); + int64_t ClientReceiveWindow() { return mClientReceiveWindow; } + + bool BlockedOnRwin() { return mBlockedOnRwin; } + + uint32_t Priority() { return mPriority; } + uint32_t PriorityDependency() { return mPriorityDependency; } + uint8_t PriorityWeight() { return mPriorityWeight; } + void SetPriority(uint32_t); + void SetPriorityDependency(uint32_t, uint32_t); + void UpdatePriorityDependency(); + + uint64_t TransactionTabId() { return mTransactionTabId; } + + // A pull stream has an implicit sink, a pushed stream has a sink + // once it is matched to a pull stream. + virtual bool HasSink() { return true; } + + // This is a no-op on pull streams. Pushed streams override this. + virtual void SetPushComplete(){}; + + Http2Session* Session() { return mSession; } + + [[nodiscard]] static nsresult MakeOriginURL(const nsACString& origin, + nsCOMPtr& url); + + [[nodiscard]] static nsresult MakeOriginURL(const nsACString& scheme, + const nsACString& origin, + nsCOMPtr& url); + + // Mirrors nsAHttpTransaction + bool Do0RTT(); + nsresult Finish0RTT(bool aRestart, bool aAlpnIgnored); + + nsresult GetOriginAttributes(mozilla::OriginAttributes* oa); + + virtual void TopLevelOuterContentWindowIdChanged(uint64_t windowId); + void TopLevelOuterContentWindowIdChangedInternal( + uint64_t windowId); // For use by pushed streams only + + protected: + virtual ~Http2Stream(); + static void CreatePushHashKey( + const nsCString& scheme, const nsCString& hostHeader, + const mozilla::OriginAttributes& originAttributes, uint64_t serial, + const nsACString& pathInfo, nsCString& outOrigin, nsCString& outKey); + + // These internal states track request generation + enum upstreamStateType { + GENERATING_HEADERS, + GENERATING_BODY, + SENDING_BODY, + SENDING_FIN_STREAM, + UPSTREAM_COMPLETE + }; + + uint32_t mStreamID; + + // The session that this stream is a subset of + Http2Session* mSession; + + // These are temporary state variables to hold the argument to + // Read/WriteSegments so it can be accessed by On(read/write)segment + // further up the stack. + nsAHttpSegmentReader* mSegmentReader; + nsAHttpSegmentWriter* mSegmentWriter; + + nsCString mOrigin; + nsCString mHeaderHost; + nsCString mHeaderScheme; + nsCString mHeaderPath; + + // Each stream goes from generating_headers to upstream_complete, perhaps + // looping on multiple instances of generating_body and + // sending_body for each frame in the upload. + enum upstreamStateType mUpstreamState; + + // The HTTP/2 state for the stream from section 5.1 + enum stateType mState; + + // Flag is set when all http request headers have been read ID is not stable + uint32_t mRequestHeadersDone : 1; + + // Flag is set when ID is stable and concurrency limits are met + uint32_t mOpenGenerated : 1; + + // Flag is set when all http response headers have been read + uint32_t mAllHeadersReceived : 1; + + // Flag is set when stream is queued inside the session due to + // concurrency limits being exceeded + uint32_t mQueued : 1; + + void ChangeState(enum upstreamStateType); + + virtual void AdjustInitialWindow(); + [[nodiscard]] nsresult TransmitFrame(const char*, uint32_t*, + bool forceCommitment); + + // The underlying socket transport object is needed to propogate some events + nsISocketTransport* mSocketTransport; + + uint8_t mPriorityWeight; // h2 weight + uint32_t mPriorityDependency; // h2 stream id this one depends on + uint64_t mCurrentForegroundTabOuterContentWindowId; + uint64_t mTransactionTabId; + + private: + friend class mozilla::DefaultDelete; + + [[nodiscard]] nsresult ParseHttpRequestHeaders(const char*, uint32_t, + uint32_t*); + [[nodiscard]] nsresult GenerateOpen(); + + void AdjustPushedPriority(); + void GenerateDataFrameHeader(uint32_t, bool); + + [[nodiscard]] nsresult BufferInput(uint32_t, uint32_t*); + + // The underlying HTTP transaction. This pointer is used as the key + // in the Http2Session mStreamTransactionHash so it is important to + // keep a reference to it as long as this stream is a member of that hash. + // (i.e. don't change it or release it after it is set in the ctor). + RefPtr mTransaction; + + // The quanta upstream data frames are chopped into + uint32_t mChunkSize; + + // Flag is set when the HTTP processor has more data to send + // but has blocked in doing so. + uint32_t mRequestBlockedOnRead : 1; + + // Flag is set after the response frame bearing the fin bit has + // been processed. (i.e. after the server has closed). + uint32_t mRecvdFin : 1; + + // Flag is set after 1st DATA frame has been passed to stream + uint32_t mReceivedData : 1; + + // Flag is set after RST_STREAM has been received for this stream + uint32_t mRecvdReset : 1; + + // Flag is set after RST_STREAM has been generated for this stream + uint32_t mSentReset : 1; + + // Flag is set when stream is counted towards MAX_CONCURRENT streams in + // session + uint32_t mCountAsActive : 1; + + // Flag is set when a FIN has been placed on a data or header frame + // (i.e after the client has closed) + uint32_t mSentFin : 1; + + // Flag is set after the WAITING_FOR Transport event has been generated + uint32_t mSentWaitingFor : 1; + + // Flag is set after TCP send autotuning has been disabled + uint32_t mSetTCPSocketBuffer : 1; + + // Flag is set when OnWriteSegment is being called directly from stream + // instead of transaction + uint32_t mBypassInputBuffer : 1; + + // The InlineFrame and associated data is used for composing control + // frames and data frame headers. + UniquePtr mTxInlineFrame; + uint32_t mTxInlineFrameSize; + uint32_t mTxInlineFrameUsed; + + // mTxStreamFrameSize tracks the progress of + // transmitting a request body data frame. The data frame itself + // is never copied into the spdy layer. + uint32_t mTxStreamFrameSize; + + // Buffer for request header compression. + nsCString mFlatHttpRequestHeaders; + + // Track the content-length of a request body so that we can + // place the fin flag on the last data packet instead of waiting + // for a stream closed indication. Relying on stream close results + // in an extra 0-length runt packet and seems to have some interop + // problems with the google servers. Connect does rely on stream + // close by setting this to the max value. + int64_t mRequestBodyLenRemaining; + + uint32_t mPriority; // geckoish weight + + // mClientReceiveWindow, mServerReceiveWindow, and mLocalUnacked are for flow + // control. *window are signed because the race conditions in asynchronous + // SETTINGS messages can force them temporarily negative. + + // mClientReceiveWindow is how much data the server will send without getting + // a + // window update + int64_t mClientReceiveWindow; + + // mServerReceiveWindow is how much data the client is allowed to send without + // getting a window update + int64_t mServerReceiveWindow; + + // LocalUnacked is the number of bytes received by the client but not + // yet reflected in a window update. Sending that update will increment + // ClientReceiveWindow + uint64_t mLocalUnacked; + + // True when sending is suspended becuase the server receive window is + // <= 0 + bool mBlockedOnRwin; + + // For Progress Events + uint64_t mTotalSent; + uint64_t mTotalRead; + + // For Http2Push + Http2PushedStream* mPushSource; + + // Used to store stream data when the transaction channel cannot keep up + // and flow control has not yet kicked in. + SimpleBuffer mSimpleBuffer; + + bool mAttempting0RTT; + + /// connect tunnels + public: + bool IsTunnel() { return mIsTunnel; } + // TODO - remove as part of bug 1564120 fix? + // This method saves the key the tunnel was registered under, so that when the + // associated transaction connection info hash key changes, we still find it + // and decrement the correct item in the session's tunnel hash table. + nsCString& RegistrationKey(); + + private: + void ClearTransactionsBlockedOnTunnel(); + void MapStreamToPlainText(); + bool MapStreamToHttpConnection(const nsACString& aFlat407Headers, + int32_t aHttpResponseCode = -1); + + bool mIsTunnel; + bool mPlainTextTunnel; + nsCString mRegistrationKey; + + /// websockets + public: + bool IsWebsocket() { return mIsWebsocket; } + + private: + bool mIsWebsocket; +}; + +} // namespace net +} // namespace mozilla + +#endif // mozilla_net_Http2Stream_h diff --git a/netwerk/protocol/http/Http3Session.cpp b/netwerk/protocol/http/Http3Session.cpp new file mode 100644 index 0000000000..9674356808 --- /dev/null +++ b/netwerk/protocol/http/Http3Session.cpp @@ -0,0 +1,1715 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=4 sw=2 et cindent: */ +/* 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 "HttpLog.h" +#include "Http3Session.h" +#include "Http3Stream.h" +#include "mozilla/net/DNS.h" +#include "nsHttpHandler.h" +#include "mozilla/RefPtr.h" +#include "mozilla/Telemetry.h" +#include "ASpdySession.h" // because of SoftStreamError() +#include "nsIOService.h" +#include "nsISSLSocketControl.h" +#include "ScopedNSSTypes.h" +#include "nsQueryObject.h" +#include "nsSocketTransportService2.h" +#include "nsThreadUtils.h" +#include "QuicSocketControl.h" +#include "SSLServerCertVerification.h" +#include "SSLTokensCache.h" +#include "HttpConnectionUDP.h" +#include "sslerr.h" + +namespace mozilla { +namespace net { + +const uint64_t HTTP3_APP_ERROR_NO_ERROR = 0x100; +const uint64_t HTTP3_APP_ERROR_GENERAL_PROTOCOL_ERROR = 0x101; +const uint64_t HTTP3_APP_ERROR_INTERNAL_ERROR = 0x102; +const uint64_t HTTP3_APP_ERROR_STREAM_CREATION_ERROR = 0x103; +const uint64_t HTTP3_APP_ERROR_CLOSED_CRITICAL_STREAM = 0x104; +const uint64_t HTTP3_APP_ERROR_FRAME_UNEXPECTED = 0x105; +const uint64_t HTTP3_APP_ERROR_FRAME_ERROR = 0x106; +const uint64_t HTTP3_APP_ERROR_EXCESSIVE_LOAD = 0x107; +const uint64_t HTTP3_APP_ERROR_ID_ERROR = 0x108; +const uint64_t HTTP3_APP_ERROR_SETTINGS_ERROR = 0x109; +const uint64_t HTTP3_APP_ERROR_MISSING_SETTINGS = 0x10a; +const uint64_t HTTP3_APP_ERROR_REQUEST_REJECTED = 0x10b; +const uint64_t HTTP3_APP_ERROR_REQUEST_CANCELLED = 0x10c; +const uint64_t HTTP3_APP_ERROR_REQUEST_INCOMPLETE = 0x10d; +const uint64_t HTTP3_APP_ERROR_EARLY_RESPONSE = 0x10e; +const uint64_t HTTP3_APP_ERROR_CONNECT_ERROR = 0x10f; +const uint64_t HTTP3_APP_ERROR_VERSION_FALLBACK = 0x110; + +const uint32_t UDP_MAX_PACKET_SIZE = 4096; +const uint32_t MAX_PTO_COUNTS = 16; + +NS_IMPL_ADDREF(Http3Session) +NS_IMPL_RELEASE(Http3Session) +NS_INTERFACE_MAP_BEGIN(Http3Session) + NS_INTERFACE_MAP_ENTRY(nsAHttpConnection) + NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference) + NS_INTERFACE_MAP_ENTRY_CONCRETE(Http3Session) +NS_INTERFACE_MAP_END + +Http3Session::Http3Session() + : mState(INITIALIZING), + mAuthenticationStarted(false), + mCleanShutdown(false), + mGoawayReceived(false), + mShouldClose(false), + mIsClosedByNeqo(false), + mError(NS_OK), + mSocketError(NS_OK), + mBeforeConnectedError(false), + mTimerActive(false) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG(("Http3Session::Http3Session [this=%p]", this)); + + mCurrentForegroundTabOuterContentWindowId = + gHttpHandler->ConnMgr()->CurrentTopLevelOuterContentWindowId(); +} + +nsresult Http3Session::Init(const nsHttpConnectionInfo* aConnInfo, + nsISocketTransport* aSocketTransport, + HttpConnectionUDP* readerWriter) { + LOG3(("Http3Session::Init %p", this)); + + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + MOZ_ASSERT(aSocketTransport); + MOZ_ASSERT(readerWriter); + + mConnInfo = aConnInfo->Clone(); + mSocketTransport = aSocketTransport; + + nsCOMPtr info; + Unused << mSocketTransport->GetSecurityInfo(getter_AddRefs(info)); + mSocketControl = do_QueryObject(info); + + // Get the local and remote address neqo needs it. + NetAddr selfAddr; + if (NS_FAILED(mSocketTransport->GetSelfAddr(&selfAddr))) { + LOG3(("Http3Session::Init GetSelfAddr failed [this=%p]", this)); + return NS_ERROR_FAILURE; + } + char buf[kIPv6CStrBufSize]; + selfAddr.ToStringBuffer(buf, kIPv6CStrBufSize); + + nsAutoCString selfAddrStr; + if (selfAddr.raw.family == AF_INET6) { + selfAddrStr.Append("["); + } + // Append terminating ']' and port. + selfAddrStr.Append(buf, strlen(buf)); + if (selfAddr.raw.family == AF_INET6) { + selfAddrStr.Append("]:"); + selfAddrStr.AppendInt(ntohs(selfAddr.inet6.port)); + } else { + selfAddrStr.Append(":"); + selfAddrStr.AppendInt(ntohs(selfAddr.inet.port)); + } + + NetAddr peerAddr; + if (NS_FAILED(mSocketTransport->GetPeerAddr(&peerAddr))) { + LOG3(("Http3Session::Init GetPeerAddr failed [this=%p]", this)); + return NS_ERROR_FAILURE; + } + peerAddr.ToStringBuffer(buf, kIPv6CStrBufSize); + + nsAutoCString peerAddrStr; + if (peerAddr.raw.family == AF_INET6) { + peerAddrStr.Append("["); + } + peerAddrStr.Append(buf, strlen(buf)); + // Append terminating ']' and port. + if (peerAddr.raw.family == AF_INET6) { + peerAddrStr.Append("]:"); + peerAddrStr.AppendInt(ntohs(peerAddr.inet6.port)); + } else { + peerAddrStr.Append(':'); + peerAddrStr.AppendInt(ntohs(peerAddr.inet.port)); + } + + LOG3( + ("Http3Session::Init origin=%s, alpn=%s, selfAddr=%s, peerAddr=%s," + " qpack table size=%u, max blocked streams=%u [this=%p]", + PromiseFlatCString(mConnInfo->GetOrigin()).get(), + PromiseFlatCString(mConnInfo->GetNPNToken()).get(), selfAddrStr.get(), + peerAddrStr.get(), gHttpHandler->DefaultQpackTableSize(), + gHttpHandler->DefaultHttp3MaxBlockedStreams(), this)); + + nsresult rv = NeqoHttp3Conn::Init( + mConnInfo->GetOrigin(), mConnInfo->GetNPNToken(), selfAddrStr, + peerAddrStr, gHttpHandler->DefaultQpackTableSize(), + gHttpHandler->DefaultHttp3MaxBlockedStreams(), + gHttpHandler->Http3QlogDir(), getter_AddRefs(mHttp3Connection)); + if (NS_FAILED(rv)) { + return rv; + } + + nsAutoCString peerId; + mSocketControl->GetPeerId(peerId); + nsTArray token; + if (NS_SUCCEEDED(SSLTokensCache::Get(peerId, token))) { + LOG(("Found a resumption token in the cache.")); + mHttp3Connection->SetResumptionToken(token); + if (mHttp3Connection->IsZeroRtt()) { + LOG(("Can send ZeroRtt data")); + RefPtr self(this); + mState = ZERORTT; + // Let the nsHttpConnectionMgr know that the connection can accept + // transactions. + // We need to dispatch the following function to this thread so that + // it is executed after the current function. At this point a + // Http3Session is still being initialized and ReportHttp3Connection + // will try to dispatch transaction on this session therefore it + // needs to be executed after the initializationg is done. + DebugOnly rv = NS_DispatchToCurrentThread( + NS_NewRunnableFunction("Http3Session::ReportHttp3Connection", + [self]() { self->ReportHttp3Connection(); })); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "NS_DispatchToCurrentThread failed"); + } + } + + // After this line, Http3Session and HttpConnectionUDP become a cycle. We put + // this line in the end of Http3Session::Init to make sure Http3Session can be + // released when Http3Session::Init early returned. + mSegmentReaderWriter = readerWriter; + return NS_OK; +} + +// Shutdown the http3session and close all transactions. +void Http3Session::Shutdown() { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + if ((mBeforeConnectedError || + (mError == NS_ERROR_NET_HTTP3_PROTOCOL_ERROR)) && + (mError != + mozilla::psm::GetXPCOMFromNSSError(SSL_ERROR_BAD_CERT_DOMAIN))) { + gHttpHandler->ExcludeHttp3(mConnInfo); + } + + for (auto iter = mStreamTransactionHash.Iter(); !iter.Done(); iter.Next()) { + RefPtr stream = iter.Data(); + + if (mBeforeConnectedError) { + // We have an error before we were connected, just restart transactions. + // The transaction restart code path will remove AltSvc mapping and the + // direct path will be used. + MOZ_ASSERT(NS_FAILED(mError)); + stream->Close(NS_ERROR_NET_RESET); + } else if (!stream->HasStreamId()) { + if (NS_SUCCEEDED(mError)) { + // Connection has not been started yet. We can restart it. + stream->Transaction()->DoNotRemoveAltSvc(); + } + stream->Close(NS_ERROR_NET_RESET); + } else if (stream->RecvdData()) { + stream->Close(NS_ERROR_NET_PARTIAL_TRANSFER); + } else if (mError == NS_ERROR_NET_HTTP3_PROTOCOL_ERROR) { + stream->Close(NS_ERROR_NET_HTTP3_PROTOCOL_ERROR); + } else { + stream->Close(NS_ERROR_ABORT); + } + RemoveStreamFromQueues(stream); + if (stream->HasStreamId()) { + mStreamIdHash.Remove(stream->StreamId()); + } + } + + mStreamTransactionHash.Clear(); +} + +Http3Session::~Http3Session() { + LOG3(("Http3Session::~Http3Session %p", this)); + + Telemetry::Accumulate(Telemetry::HTTP3_REQUEST_PER_CONN, mTransactionCount); + Telemetry::Accumulate(Telemetry::HTTP3_BLOCKED_BY_STREAM_LIMIT_PER_CONN, + mBlockedByStreamLimitCount); + Telemetry::Accumulate(Telemetry::HTTP3_TRANS_BLOCKED_BY_STREAM_LIMIT_PER_CONN, + mTransactionsBlockedByStreamLimitCount); + + Telemetry::Accumulate( + Telemetry::HTTP3_TRANS_SENDING_BLOCKED_BY_FLOW_CONTROL_PER_CONN, + mTransactionsSenderBlockedByFlowControlCount); + + Shutdown(); +} + +// This function may return a socket error. +// It will not return an error if socket error is +// NS_BASE_STREAM_WOULD_BLOCK. +// A caller of this function will close the Http3 connection +// in case of a error. +// The only callers is: +// HttpConnectionUDP::OnInputStreamReady -> +// HttpConnectionUDP::OnSocketReadable -> +// Http3Session::WriteSegmentsAgain +nsresult Http3Session::ProcessInput(uint32_t* aCountRead) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + MOZ_ASSERT(mSegmentReaderWriter); + + LOG(("Http3Session::ProcessInput writer=%p [this=%p state=%d]", + mSegmentReaderWriter.get(), this, mState)); + + uint8_t packet[UDP_MAX_PACKET_SIZE]; + nsresult rv = NS_OK; + // Read from socket until NS_BASE_STREAM_WOULD_BLOCK or another error. + do { + uint32_t read = 0; + rv = mSegmentReaderWriter->OnWriteSegment((char*)packet, + UDP_MAX_PACKET_SIZE, &read); + if (NS_SUCCEEDED(rv)) { + mHttp3Connection->ProcessInput(packet, read); + *aCountRead += read; + } + } while (NS_SUCCEEDED(rv)); + // NS_BASE_STREAM_WOULD_BLOCK means that there is no more date to read. + if (rv == NS_BASE_STREAM_WOULD_BLOCK) { + return NS_OK; + } + + LOG(("Http3Session::ProcessInput error=%" PRIx32 " [this=%p]", + static_cast(rv), this)); + if (NS_SUCCEEDED(mSocketError)) { + mSocketError = rv; + } + return rv; +} + +nsresult Http3Session::ProcessTransactionRead(uint64_t stream_id, + uint32_t count, + uint32_t* countWritten) { + RefPtr stream = mStreamIdHash.Get(stream_id); + if (!stream) { + LOG( + ("Http3Session::ProcessTransactionRead - stream not found " + "stream_id=0x%" PRIx64 " [this=%p].", + stream_id, this)); + return NS_OK; + } + + return ProcessTransactionRead(stream, count, countWritten); +} + +nsresult Http3Session::ProcessTransactionRead(Http3Stream* stream, + uint32_t count, + uint32_t* countWritten) { + nsresult rv = stream->WriteSegments(stream, count, countWritten); + + if (ASpdySession::SoftStreamError(rv) || stream->Done()) { + LOG3( + ("Http3Session::ProcessSingleTransactionRead session=%p stream=%p " + "0x%" PRIx64 " cleanup stream rv=0x%" PRIx32 " done=%d.\n", + this, stream, stream->StreamId(), static_cast(rv), + stream->Done())); + CloseStream(stream, + (rv == NS_BINDING_RETARGETED) ? NS_BINDING_RETARGETED : NS_OK); + return NS_OK; + } + + if (NS_FAILED(rv) && rv != NS_BASE_STREAM_WOULD_BLOCK) { + return rv; + } + return NS_OK; +} + +nsresult Http3Session::ProcessEvents(uint32_t count) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + LOG(("Http3Session::ProcessEvents [this=%p]", this)); + + // We need an array to pick up header data or a resumption token. + nsTArray data; + Http3Event event; + event.tag = Http3Event::Tag::NoEvent; + + nsresult rv = mHttp3Connection->GetEvent(&event, data); + if (NS_FAILED(rv)) { + LOG(("Http3Session::ProcessEvents [this=%p] rv=%" PRIx32, this, + static_cast(rv))); + return rv; + } + + while (event.tag != Http3Event::Tag::NoEvent) { + switch (event.tag) { + case Http3Event::Tag::HeaderReady: { + MOZ_ASSERT(mState == CONNECTED); + LOG(("Http3Session::ProcessEvents - HeaderReady")); + uint64_t id = event.header_ready.stream_id; + + RefPtr stream = mStreamIdHash.Get(id); + if (!stream) { + LOG( + ("Http3Session::ProcessEvents - HeaderReady - stream not found " + "stream_id=0x%" PRIx64 " [this=%p].", + id, this)); + continue; + } + + stream->SetResponseHeaders(data, event.header_ready.fin); + + uint32_t read = 0; + rv = ProcessTransactionRead(stream, count, &read); + + if (NS_FAILED(rv)) { + LOG(("Http3Session::ProcessEvents [this=%p] rv=%" PRIx32, this, + static_cast(rv))); + return rv; + } + break; + } + case Http3Event::Tag::DataReadable: { + MOZ_ASSERT(mState == CONNECTED); + LOG(("Http3Session::ProcessEvents - DataReadable")); + uint64_t id = event.data_readable.stream_id; + + uint32_t read = 0; + nsresult rv = ProcessTransactionRead(id, count, &read); + + if (NS_FAILED(rv)) { + LOG(("Http3Session::ProcessEvents [this=%p] rv=%" PRIx32, this, + static_cast(rv))); + return rv; + } + break; + } + case Http3Event::Tag::DataWritable: { + MOZ_ASSERT(CanSandData()); + LOG(("Http3Session::ProcessEvents - DataWritable")); + + RefPtr stream = + mStreamIdHash.Get(event.data_writable.stream_id); + if (stream) { + StreamReadyToWrite(stream); + } + } break; + case Http3Event::Tag::Reset: + LOG(("Http3Session::ProcessEvents - Reset")); + ResetRecvd(event.reset.stream_id, event.reset.error); + break; + case Http3Event::Tag::StopSending: + LOG(("Http3Session::ProcessEvents - StopSeniding with error 0x%" PRIx64, + event.stop_sending.error)); + if (event.stop_sending.error == HTTP3_APP_ERROR_NO_ERROR) { + RefPtr stream = + mStreamIdHash.Get(event.data_writable.stream_id); + if (stream) { + stream->StopSending(); + } + } else { + ResetRecvd(event.reset.stream_id, event.reset.error); + } + break; + case Http3Event::Tag::PushPromise: + LOG(("Http3Session::ProcessEvents - PushPromise")); + break; + case Http3Event::Tag::PushHeaderReady: + LOG(("Http3Session::ProcessEvents - PushHeaderReady")); + break; + case Http3Event::Tag::PushDataReadable: + LOG(("Http3Session::ProcessEvents - PushDataReadable")); + break; + case Http3Event::Tag::PushCanceled: + LOG(("Http3Session::ProcessEvents - PushCanceled")); + break; + case Http3Event::Tag::RequestsCreatable: + LOG(("Http3Session::ProcessEvents - StreamCreatable")); + ProcessPending(); + break; + case Http3Event::Tag::AuthenticationNeeded: + LOG(("Http3Session::ProcessEvents - AuthenticationNeeded %d", + mAuthenticationStarted)); + if (!mAuthenticationStarted) { + mAuthenticationStarted = true; + LOG(("Http3Session::ProcessEvents - AuthenticationNeeded called")); + CallCertVerification(); + } + break; + case Http3Event::Tag::ZeroRttRejected: + LOG(("Http3Session::ProcessEvents - ZeroRttRejected")); + if (mState == ZERORTT) { + mState = INITIALIZING; + Finish0Rtt(true); + } + break; + case Http3Event::Tag::ResumptionToken: { + LOG(("Http3Session::ProcessEvents - ResumptionToken")); + if (!data.IsEmpty()) { + LOG(("Got a resumption token")); + nsAutoCString peerId; + mSocketControl->GetPeerId(peerId); + if (NS_FAILED(SSLTokensCache::Put( + peerId, data.Elements(), data.Length(), mSocketControl, + PR_Now() + event.resumption_token.expire_in))) { + LOG(("Adding resumption token failed")); + } + } + } break; + case Http3Event::Tag::ConnectionConnected: { + LOG(("Http3Session::ProcessEvents - ConnectionConnected")); + bool was0RTT = mState == ZERORTT; + mState = CONNECTED; + SetSecInfo(); + mSocketControl->HandshakeCompleted(); + if (was0RTT) { + Finish0Rtt(false); + } + + OnTransportStatus(mSocketTransport, NS_NET_STATUS_CONNECTED_TO, 0); + // Also send the NS_NET_STATUS_TLS_HANDSHAKE_ENDED event. + OnTransportStatus(mSocketTransport, NS_NET_STATUS_TLS_HANDSHAKE_ENDED, + 0); + + ReportHttp3Connection(); + } break; + case Http3Event::Tag::GoawayReceived: + LOG(("Http3Session::ProcessEvents - GoawayReceived")); + mGoawayReceived = true; + break; + case Http3Event::Tag::ConnectionClosing: + LOG(("Http3Session::ProcessEvents - ConnectionClosing")); + if (NS_SUCCEEDED(mError) && !IsClosing()) { + mError = NS_ERROR_NET_HTTP3_PROTOCOL_ERROR; + CloseConnectionTelemetry(event.connection_closing.error, true); + } + return mError; + break; + case Http3Event::Tag::ConnectionClosed: + LOG(("Http3Session::ProcessEvents - ConnectionClosed")); + if (NS_SUCCEEDED(mError)) { + mError = NS_ERROR_NET_TIMEOUT; + CloseConnectionTelemetry(event.connection_closed.error, false); + } + mIsClosedByNeqo = true; + LOG(("Http3Session::ProcessEvents - ConnectionClosed error=%" PRIx32, + static_cast(mError))); + // We need to return here and let HttpConnectionUDP close the session. + return mError; + break; + default: + break; + } + // Delete previous content of data + data.TruncateLength(0); + rv = mHttp3Connection->GetEvent(&event, data); + if (NS_FAILED(rv)) { + LOG(("Http3Session::ProcessEvents [this=%p] rv=%" PRIx32, this, + static_cast(rv))); + return rv; + } + } + + return NS_OK; +} // namespace net + +// This function may return a socket error. +// It will not return an error if socket error is +// NS_BASE_STREAM_WOULD_BLOCK. +// A Caller of this function will close the Http3 connection +// if this function returns an error. +// Callers are: +// 1) HttpConnectionUDP::OnQuicTimeoutExpired +// 2) HttpConnectionUDP::OnOutputStreamReady -> +// HttpConnectionUDP::OnSocketWritable -> +// Http3Session::ReadSegmentsAgain +nsresult Http3Session::ProcessOutput() { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + MOZ_ASSERT(mSegmentReaderWriter); + + LOG(("Http3Session::ProcessOutput reader=%p, [this=%p]", + mSegmentReaderWriter.get(), this)); + + // Process neqo. + uint64_t timeout = mHttp3Connection->ProcessOutput(); + + // Check if we have a packet that could not have been sent in a previous + // iteration or maybe get new packets to send. + while (mPacketToSend.Length() || + NS_SUCCEEDED(mHttp3Connection->GetDataToSend(mPacketToSend))) { + MOZ_ASSERT(mPacketToSend.Length()); + LOG(("Http3Session::ProcessOutput sending packet with %u bytes [this=%p].", + (uint32_t)mPacketToSend.Length(), this)); + uint32_t written = 0; + nsresult rv = mSegmentReaderWriter->OnReadSegment( + (const char*)mPacketToSend.Elements(), mPacketToSend.Length(), + &written); + if (rv == NS_BASE_STREAM_WOULD_BLOCK) { + // The socket is blocked, keep the packet and we will send it when the + // socket is ready to send data again. + if (mConnection) { + Unused << mConnection->ResumeSend(); + } + SetupTimer(timeout); + return NS_OK; + } + if (NS_FAILED(rv)) { + mSocketError = rv; + // Ok the socket is blocked or there is an error, return from here, + // we do not need to set a timer if error is not + // NS_BASE_STREAM_WOULD_BLOCK, i.e. we are closing the connection. + return rv; + } + MOZ_ASSERT(written == mPacketToSend.Length()); + mPacketToSend.TruncateLength(0); + } + + SetupTimer(timeout); + return NS_OK; +} + +// This is only called when timer expires. +// It is called by HttpConnectionUDP::OnQuicTimeout. +// If tihs function returns an error OnQuicTimeout will handle the error +// properly and close the connection. +nsresult Http3Session::ProcessOutputAndEvents() { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + // ProcessOutput could fire another timer. Need to unset the flag before that. + mTimerActive = false; + + MOZ_ASSERT(mTimerShouldTrigger); + + Telemetry::AccumulateTimeDelta(Telemetry::HTTP3_TIMER_DELAYED, + mTimerShouldTrigger, TimeStamp::Now()); + + mTimerShouldTrigger = TimeStamp(); + + nsresult rv = ProcessOutput(); + if (NS_FAILED(rv)) { + return rv; + } + return ProcessEvents(nsIOService::gDefaultSegmentSize); +} + +void Http3Session::SetupTimer(uint64_t aTimeout) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + // UINT64_MAX indicated a no-op from neqo, which only happens when a + // connection is in or going to be Closed state. + if (aTimeout == UINT64_MAX) { + return; + } + + LOG(("Http3Session::SetupTimer to %" PRIu64 "ms [this=%p].", aTimeout, this)); + + // Remember the time when the timer should trigger. + mTimerShouldTrigger = + TimeStamp::Now() + TimeDuration::FromMilliseconds(aTimeout); + + if (mTimerActive && mTimer) { + LOG( + (" -- Previous timer has not fired. Update the delay instead of " + "re-initializing the timer")); + mTimer->SetDelay(aTimeout); + return; + } + + if (!mTimer) { + mTimer = NS_NewTimer(); + } + + mTimerActive = true; + + if (!mTimer || + NS_FAILED(mTimer->InitWithNamedFuncCallback( + &HttpConnectionUDP::OnQuicTimeout, mSegmentReaderWriter, aTimeout, + nsITimer::TYPE_ONE_SHOT, "net::HttpConnectionUDP::OnQuicTimeout"))) { + NS_DispatchToCurrentThread(NewRunnableMethod( + "net::HttpConnectionUDP::OnQuicTimeoutExpired", mSegmentReaderWriter, + &HttpConnectionUDP::OnQuicTimeoutExpired)); + } +} + +bool Http3Session::AddStream(nsAHttpTransaction* aHttpTransaction, + int32_t aPriority, + nsIInterfaceRequestor* aCallbacks) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + nsHttpTransaction* trans = aHttpTransaction->QueryHttpTransaction(); + + if (!mConnection) { + // Get the connection from the first transaction. + mConnection = aHttpTransaction->Connection(); + } + + if (IsClosing()) { + LOG3( + ("Http3Session::AddStream %p atrans=%p trans=%p session unusable - " + "resched.\n", + this, aHttpTransaction, trans)); + aHttpTransaction->SetConnection(nullptr); + nsresult rv = gHttpHandler->InitiateTransaction(trans, trans->Priority()); + if (NS_FAILED(rv)) { + LOG3( + ("Http3Session::AddStream %p atrans=%p trans=%p failed to initiate " + "transaction (0x%" PRIx32 ").\n", + this, aHttpTransaction, trans, static_cast(rv))); + } + return true; + } + + aHttpTransaction->SetConnection(this); + aHttpTransaction->OnActivated(); + + LOG3(("Http3Session::AddStream %p atrans=%p.\n", this, aHttpTransaction)); + Http3Stream* stream = new Http3Stream(aHttpTransaction, this); + mStreamTransactionHash.Put(aHttpTransaction, RefPtr{stream}); + + if (mState == ZERORTT) { + if (!stream->Do0RTT()) { + LOG(("Http3Session %p will not get early data from Http3Stream %p", this, + stream)); + if (!mCannotDo0RTTStreams.Contains(stream)) { + mCannotDo0RTTStreams.AppendElement(stream); + } + return true; + } else { + m0RTTStreams.AppendElement(stream); + } + } + + if (!mFirstHttpTransaction && !IsConnected()) { + mFirstHttpTransaction = aHttpTransaction->QueryHttpTransaction(); + LOG3(("Http3Session::AddStream first session=%p trans=%p ", this, + mFirstHttpTransaction.get())); + } + + StreamReadyToWrite(stream); + + return true; +} + +bool Http3Session::CanReuse() { + return CanSandData() && !(mGoawayReceived || mShouldClose); +} + +void Http3Session::QueueStream(Http3Stream* stream) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + MOZ_ASSERT(!stream->Queued()); + + LOG3(("Http3Session::QueueStream %p stream %p queued.", this, stream)); + + stream->SetQueued(true); + mQueuedStreams.Push(stream); +} + +void Http3Session::ProcessPending() { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + Http3Stream* stream; + while ((stream = mQueuedStreams.PopFront())) { + LOG3(("Http3Session::ProcessPending %p stream %p woken from queue.", this, + stream)); + MOZ_ASSERT(stream->Queued()); + stream->SetQueued(false); + mReadyForWrite.Push(stream); + } + MaybeResumeSend(); +} + +static void RemoveStreamFromQueue(Http3Stream* aStream, + nsDeque& queue) { + size_t size = queue.GetSize(); + for (size_t count = 0; count < size; ++count) { + Http3Stream* stream = queue.PopFront(); + if (stream != aStream) { + queue.Push(stream); + } + } +} + +void Http3Session::RemoveStreamFromQueues(Http3Stream* aStream) { + RemoveStreamFromQueue(aStream, mReadyForWrite); + RemoveStreamFromQueue(aStream, mQueuedStreams); + mSlowConsumersReadyForRead.RemoveElement(aStream); +} + +// This is called by Http3Stream::OnReadSegment. +// ProcessOutput will be called in Http3Session::ReadSegment that +// calls Http3Stream::OnReadSegment. +nsresult Http3Session::TryActivating( + const nsACString& aMethod, const nsACString& aScheme, + const nsACString& aAuthorityHeader, const nsACString& aPath, + const nsACString& aHeaders, uint64_t* aStreamId, Http3Stream* aStream) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + MOZ_ASSERT(*aStreamId == UINT64_MAX); + + LOG(("Http3Session::TryActivating [stream=%p, this=%p state=%d]", aStream, + this, mState)); + + if (IsClosing()) { + if (NS_FAILED(mError)) { + return mError; + } + return NS_ERROR_FAILURE; + } + + if (aStream->Queued()) { + LOG3(("Http3Session::TryActivating %p stream=%p already queued.\n", this, + aStream)); + return NS_BASE_STREAM_WOULD_BLOCK; + } + + if (mState == ZERORTT) { + if (!aStream->Do0RTT()) { + MOZ_ASSERT(!mCannotDo0RTTStreams.Contains(aStream)); + return NS_BASE_STREAM_WOULD_BLOCK; + } + } + + nsresult rv = mHttp3Connection->Fetch(aMethod, aScheme, aAuthorityHeader, + aPath, aHeaders, aStreamId); + if (NS_FAILED(rv)) { + LOG(("Http3Session::TryActivating returns error=0x%" PRIx32 "[stream=%p, " + "this=%p]", + static_cast(rv), aStream, this)); + if (rv == NS_BASE_STREAM_WOULD_BLOCK) { + LOG3( + ("Http3Session::TryActivating %p stream=%p no room for more " + "concurrent streams\n", + this, aStream)); + mTransactionsBlockedByStreamLimitCount++; + if (mQueuedStreams.GetSize() == 0) { + mBlockedByStreamLimitCount++; + } + QueueStream(aStream); + return rv; + } + // Ignore this error. This may happen if some events are not handled yet. + // TODO we may try to add an assertion here. + return NS_OK; + } + + LOG(("Http3Session::TryActivating streamId=0x%" PRIx64 + " for stream=%p [this=%p].", + *aStreamId, aStream, this)); + + MOZ_ASSERT(*aStreamId != UINT64_MAX); + + if (mTransactionCount > 0 && mStreamIdHash.IsEmpty()) { + // TODO: investigate why this is failing MOZ_ASSERT(mConnectionIdleStart); + MOZ_ASSERT(mFirstStreamIdReuseIdleConnection.isNothing()); + + mConnectionIdleEnd = TimeStamp::Now(); + mFirstStreamIdReuseIdleConnection = Some(*aStreamId); + } + mStreamIdHash.Put(*aStreamId, RefPtr{aStream}); + mTransactionCount++; + + return NS_OK; +} + +// This is only called by Http3Stream::OnReadSegment. +// ProcessOutput will be called in Http3Session::ReadSegment that +// calls Http3Stream::OnReadSegment. +void Http3Session::CloseSendingSide(uint64_t aStreamId) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + mHttp3Connection->CloseStream(aStreamId); +} + +// This is only called by Http3Stream::OnReadSegment. +// ProcessOutput will be called in Http3Session::ReadSegment that +// calls Http3Stream::OnReadSegment. +nsresult Http3Session::SendRequestBody(uint64_t aStreamId, const char* buf, + uint32_t count, uint32_t* countRead) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + nsresult rv = mHttp3Connection->SendRequestBody( + aStreamId, (const uint8_t*)buf, count, countRead); + if (rv == NS_BASE_STREAM_WOULD_BLOCK) { + mTransactionsSenderBlockedByFlowControlCount++; + } else if (NS_FAILED(rv)) { + // Ignore this error. This may happen if some events are not handled yet. + // TODO we may try to add an assertion here. + // We will pretend that sender is blocked, that will cause the caller to + // stop trying. + *countRead = 0; + rv = NS_BASE_STREAM_WOULD_BLOCK; + } + + MOZ_ASSERT((*countRead != 0) || NS_FAILED(rv)); + return rv; +} + +void Http3Session::ResetRecvd(uint64_t aStreamId, uint64_t aError) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + RefPtr stream = mStreamIdHash.Get(aStreamId); + if (!stream) { + return; + } + + stream->SetRecvdReset(); + + // We only handle some of Http3 error as epecial, the rest are just equivalent + // to cancel. + if (aError == HTTP3_APP_ERROR_VERSION_FALLBACK) { + // We will restart the request and the alt-svc will be removed + // automatically. + // Also disable http3 we want http1.1. + stream->Transaction()->DisableHttp3(); + stream->Transaction()->DisableSpdy(); + CloseStream(stream, NS_ERROR_NET_RESET); + } else if (aError == HTTP3_APP_ERROR_REQUEST_REJECTED) { + // This request was rejected because server is probably busy or going away. + // We can restart the request using alt-svc. Without calling + // DoNotRemoveAltSvc the alt-svc route will be removed. + stream->Transaction()->DoNotRemoveAltSvc(); + CloseStream(stream, NS_ERROR_NET_RESET); + } else { + if (stream->RecvdData()) { + CloseStream(stream, NS_ERROR_NET_PARTIAL_TRANSFER); + } else { + CloseStream(stream, NS_ERROR_NET_INTERRUPT); + } + } +} + +void Http3Session::SetConnection(nsAHttpConnection* aConn) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + mConnection = aConn; +} + +void Http3Session::GetSecurityCallbacks(nsIInterfaceRequestor** aOut) { + *aOut = nullptr; +} + +// TODO +void Http3Session::OnTransportStatus(nsITransport* aTransport, nsresult aStatus, + int64_t aProgress) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + if ((aStatus == NS_NET_STATUS_CONNECTED_TO) && !IsConnected()) { + // We should ignore the event. This is sent by the nsSocketTranpsort + // and it does not mean that HTTP3 session is connected. + // We will use this event to mark start of TLS handshake + aStatus = NS_NET_STATUS_TLS_HANDSHAKE_STARTING; + } + + switch (aStatus) { + // These should appear only once, deliver to the first + // transaction on the session. + case NS_NET_STATUS_RESOLVING_HOST: + case NS_NET_STATUS_RESOLVED_HOST: + case NS_NET_STATUS_CONNECTING_TO: + case NS_NET_STATUS_CONNECTED_TO: + case NS_NET_STATUS_TLS_HANDSHAKE_STARTING: + case NS_NET_STATUS_TLS_HANDSHAKE_ENDED: { + if (!mFirstHttpTransaction) { + // if we still do not have a HttpTransaction store timings info in + // a HttpConnection. + // If some error occur it can happen that we do not have a connection. + if (mConnection) { + RefPtr conn = mConnection->HttpConnection(); + conn->SetEvent(aStatus); + } + } else { + mFirstHttpTransaction->OnTransportStatus(aTransport, aStatus, + aProgress); + } + + if (aStatus == NS_NET_STATUS_CONNECTED_TO) { + mFirstHttpTransaction = nullptr; + } + break; + } + + default: + // The other transport events are ignored here because there is no good + // way to map them to the right transaction in HTTP3. Instead, the events + // are generated again from the HTTP3 code and passed directly to the + // correct transaction. + + // NS_NET_STATUS_SENDING_TO: + // This is generated by the socket transport when (part) of + // a transaction is written out + // + // There is no good way to map it to the right transaction in HTTP3, + // so it is ignored here and generated separately when the request + // is sent from Http3Stream. + + // NS_NET_STATUS_WAITING_FOR: + // Created by nsHttpConnection when the request has been totally sent. + // There is no good way to map it to the right transaction in HTTP3, + // so it is ignored here and generated separately when the same + // condition is complete in Http3Stream when there is no more + // request body left to be transmitted. + + // NS_NET_STATUS_RECEIVING_FROM + // Generated in Http3Stream whenever the stream reads data. + + break; + } +} + +bool Http3Session::IsDone() { return mState == CLOSED; } + +nsresult Http3Session::Status() { + MOZ_ASSERT(false, "Http3Session::Status()"); + return NS_ERROR_UNEXPECTED; +} + +uint32_t Http3Session::Caps() { + MOZ_ASSERT(false, "Http3Session::Caps()"); + return 0; +} + +nsresult Http3Session::ReadSegments(nsAHttpSegmentReader* reader, + uint32_t count, uint32_t* countRead) { + bool again = false; + return ReadSegmentsAgain(reader, count, countRead, &again); +} + +nsresult Http3Session::ReadSegmentsAgain(nsAHttpSegmentReader* reader, + uint32_t count, uint32_t* countRead, + bool* again) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + LOG(("Http3Session::ReadSegmentsAgain [this=%p]", this)); + *again = false; + + *countRead = 0; + + // 1) go through all streams/transactions that are ready to write and + // write their data into quic streams (no network write yet). + // 2) call ProcessOutput that will loop until all available packets are + // written to a socket or the socket returns an error code. + // 3) if we still have streams ready to write call ResumeSend()(we may + // still have such streams because on an stream error we return earlier + // to let the error be handled). + + nsresult rv = NS_OK; + Http3Stream* stream = nullptr; + + // Step 1) + while (CanSandData() && (stream = mReadyForWrite.PopFront())) { + LOG( + ("Http3Session::ReadSegmentsAgain call ReadSegments from stream=%p " + "[this=%p]", + stream, this)); + + rv = stream->ReadSegments(this); + + // on stream error we return earlier to let the error be handled. + if (NS_FAILED(rv)) { + LOG3(("Http3Session::ReadSegmentsAgain %p returns error code 0x%" PRIx32, + this, static_cast(rv))); + MOZ_ASSERT(rv != NS_BASE_STREAM_WOULD_BLOCK); + if (rv == NS_BASE_STREAM_WOULD_BLOCK) { // Just in case! + rv = NS_OK; + } else if (ASpdySession::SoftStreamError(rv)) { + CloseStream(stream, rv); + LOG3(("Http3Session::ReadSegments %p soft error override\n", this)); + rv = NS_OK; + } else { + break; + } + } + } + + if (NS_SUCCEEDED(rv)) { + // Step 2: + // Call actuall network write. + rv = ProcessOutput(); + } + + if (NS_SUCCEEDED(rv)) { + if (mConnection) { + Unused << mConnection->ResumeRecv(); + } + + // Step 3: + MaybeResumeSend(); + } + + if (rv == NS_BASE_STREAM_WOULD_BLOCK) { + rv = NS_OK; + } + + return rv; +} + +void Http3Session::StreamReadyToWrite(Http3Stream* aStream) { + MOZ_ASSERT(aStream); + mReadyForWrite.Push(aStream); + if (CanSandData() && mConnection) { + Unused << mConnection->ResumeSend(); + } +} + +void Http3Session::MaybeResumeSend() { + if ((mReadyForWrite.GetSize() > 0) && CanSandData() && mConnection) { + Unused << mConnection->ResumeSend(); + } +} + +nsresult Http3Session::ProcessSlowConsumers() { + if (mSlowConsumersReadyForRead.IsEmpty()) { + return NS_OK; + } + + RefPtr slowConsumer = mSlowConsumersReadyForRead.ElementAt(0); + mSlowConsumersReadyForRead.RemoveElementAt(0); + + uint32_t countRead = 0; + nsresult rv = ProcessTransactionRead( + slowConsumer, nsIOService::gDefaultSegmentSize, &countRead); + + return rv; +} + +nsresult Http3Session::WriteSegments(nsAHttpSegmentWriter* writer, + uint32_t count, uint32_t* countWritten) { + bool again = false; + return WriteSegmentsAgain(writer, count, countWritten, &again); +} + +nsresult Http3Session::WriteSegmentsAgain(nsAHttpSegmentWriter* writer, + uint32_t count, + uint32_t* countWritten, bool* again) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + *again = false; + + // Process slow consumers. + nsresult rv = ProcessSlowConsumers(); + if (NS_FAILED(rv)) { + LOG3(("Http3Session %p ProcessSlowConsumers returns 0x%" PRIx32 "\n", this, + static_cast(rv))); + return rv; + } + + rv = ProcessInput(countWritten); + if (NS_FAILED(rv)) { + LOG3(("Http3Session %p processInput returns 0x%" PRIx32 "\n", this, + static_cast(rv))); + return rv; + } + rv = ProcessEvents(count); + if (NS_FAILED(rv)) { + return rv; + } + if (mConnection) { + Unused << mConnection->ResumeRecv(); + } + + // Update timeout and check for new packets to be written. + // We call ResumeSend to trigger writes if needed. + uint64_t timeout = mHttp3Connection->ProcessOutput(); + + if (mHttp3Connection->HasDataToSend() && mConnection) { + Unused << mConnection->ResumeSend(); + } + SetupTimer(timeout); + + return NS_OK; +} + +void Http3Session::Close(nsresult aReason) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + LOG(("Http3Session::Close [this=%p]", this)); + + if (NS_FAILED(mError)) { + CloseInternal(false); + } else { + mError = aReason; + // If necko closes connection, this will map to "closing" key and 37 in the + // graph. + Telemetry::Accumulate(Telemetry::HTTP3_CONNECTTION_CLOSE_CODE, "closing"_ns, + 37); + CloseInternal(true); + } + + if (mCleanShutdown || mIsClosedByNeqo || NS_FAILED(mSocketError)) { + // It is network-tear-down, a socker error or neqo is state CLOSED + // (it does not need to send any more packets or wait for new packets). + // We need to remove all references, so that + // Http3Session will be destroyed. + if (mTimer) { + mTimer->Cancel(); + } + mConnection = nullptr; + mSocketTransport = nullptr; + mSegmentReaderWriter = nullptr; + mState = CLOSED; + } + if (mConnection) { + // resume sending to send CLOSE_CONNECTION frame. + Unused << mConnection->ResumeSend(); + } +} + +void Http3Session::CloseInternal(bool aCallNeqoClose) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + if (IsClosing()) { + return; + } + + LOG(("Http3Session::Closing [this=%p]", this)); + + if (mState != CONNECTED) { + mBeforeConnectedError = true; + } + mState = CLOSING; + Shutdown(); + + if (aCallNeqoClose) { + mHttp3Connection->Close(HTTP3_APP_ERROR_NO_ERROR); + } + + mStreamIdHash.Clear(); + mStreamTransactionHash.Clear(); +} + +nsHttpConnectionInfo* Http3Session::ConnectionInfo() { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + RefPtr ci; + GetConnectionInfo(getter_AddRefs(ci)); + return ci.get(); +} + +void Http3Session::SetProxyConnectFailed() { + MOZ_ASSERT(false, "Http3Session::SetProxyConnectFailed()"); +} + +nsHttpRequestHead* Http3Session::RequestHead() { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + MOZ_ASSERT(false, + "Http3Session::RequestHead() " + "should not be called after http/3 is setup"); + return nullptr; +} + +uint32_t Http3Session::Http1xTransactionCount() { return 0; } + +nsresult Http3Session::TakeSubTransactions( + nsTArray>& outTransactions) { + return NS_OK; +} + +//----------------------------------------------------------------------------- +// Pass through methods of nsAHttpConnection +//----------------------------------------------------------------------------- + +nsAHttpConnection* Http3Session::Connection() { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + return mConnection; +} + +nsresult Http3Session::OnHeadersAvailable(nsAHttpTransaction* transaction, + nsHttpRequestHead* requestHead, + nsHttpResponseHead* responseHead, + bool* reset) { + MOZ_ASSERT(mConnection); + if (mConnection) { + return mConnection->OnHeadersAvailable(transaction, requestHead, + responseHead, reset); + } + return NS_OK; +} + +bool Http3Session::IsReused() { + if (mConnection) { + return mConnection->IsReused(); + } + return true; +} + +nsresult Http3Session::PushBack(const char* buf, uint32_t len) { + return NS_ERROR_UNEXPECTED; +} + +already_AddRefed Http3Session::TakeHttpConnection() { + MOZ_ASSERT(false, "TakeHttpConnection of Http3Session"); + return nullptr; +} + +already_AddRefed Http3Session::HttpConnection() { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + if (mConnection) { + return mConnection->HttpConnection(); + } + return nullptr; +} + +void Http3Session::CloseTransaction(nsAHttpTransaction* aTransaction, + nsresult aResult) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG3(("Http3Session::CloseTransaction %p %p 0x%" PRIx32, this, aTransaction, + static_cast(aResult))); + + // Generally this arrives as a cancel event from the connection manager. + + // need to find the stream and call CloseStream() on it. + RefPtr stream = mStreamTransactionHash.Get(aTransaction); + if (!stream) { + LOG3(("Http3Session::CloseTransaction %p %p 0x%" PRIx32 " - not found.", + this, aTransaction, static_cast(aResult))); + return; + } + LOG3( + ("Http3Session::CloseTransaction probably a cancel. this=%p, " + "trans=%p, result=0x%" PRIx32 ", streamId=0x%" PRIx64 " stream=%p", + this, aTransaction, static_cast(aResult), stream->StreamId(), + stream.get())); + CloseStream(stream, aResult); + if (mConnection) { + Unused << mConnection->ResumeSend(); + } +} + +void Http3Session::CloseStream(Http3Stream* aStream, nsresult aResult) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + if (!aStream->RecvdFin() && !aStream->RecvdReset() && + (aStream->HasStreamId())) { + mHttp3Connection->ResetStream(aStream->StreamId(), + HTTP3_APP_ERROR_REQUEST_CANCELLED); + } + aStream->Close(aResult); + if (aStream->HasStreamId()) { + // We know the transaction reusing an idle connection has succeeded or + // failed. + if (mFirstStreamIdReuseIdleConnection.isSome() && + aStream->StreamId() == *mFirstStreamIdReuseIdleConnection) { + // TODO: investigate why this is failing MOZ_ASSERT(mConnectionIdleStart); + MOZ_ASSERT(mConnectionIdleEnd); + + if (mConnectionIdleStart) { + Telemetry::AccumulateTimeDelta( + Telemetry::HTTP3_TIME_TO_REUSE_IDLE_CONNECTTION_MS, + NS_SUCCEEDED(aResult) ? "succeeded"_ns : "failed"_ns, + mConnectionIdleStart, mConnectionIdleEnd); + } + + mConnectionIdleStart = TimeStamp(); + mConnectionIdleEnd = TimeStamp(); + mFirstStreamIdReuseIdleConnection.reset(); + } + + mStreamIdHash.Remove(aStream->StreamId()); + + // Start to idle when we remove the last stream. + if (mStreamIdHash.IsEmpty()) { + mConnectionIdleStart = TimeStamp::Now(); + } + } + RemoveStreamFromQueues(aStream); + mStreamTransactionHash.Remove(aStream->Transaction()); + if ((mShouldClose || mGoawayReceived) && !mStreamTransactionHash.Count()) { + MOZ_ASSERT(!IsClosing()); + Close(NS_OK); + } +} + +nsresult Http3Session::TakeTransport(nsISocketTransport**, + nsIAsyncInputStream**, + nsIAsyncOutputStream**) { + MOZ_ASSERT(false, "TakeTransport of Http3Session"); + return NS_ERROR_UNEXPECTED; +} + +bool Http3Session::IsPersistent() { return true; } + +void Http3Session::DontReuse() { + LOG3(("Http3Session::DontReuse %p\n", this)); + if (!OnSocketThread()) { + LOG3(("Http3Session %p not on socket thread\n", this)); + nsCOMPtr event = NewRunnableMethod( + "Http3Session::DontReuse", this, &Http3Session::DontReuse); + gSocketTransportService->Dispatch(event, NS_DISPATCH_NORMAL); + return; + } + + if (mGoawayReceived || IsClosing()) { + return; + } + + mShouldClose = true; + if (!mStreamTransactionHash.Count()) { + Close(NS_OK); + } +} + +void Http3Session::TopLevelOuterContentWindowIdChanged(uint64_t windowId) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + mCurrentForegroundTabOuterContentWindowId = windowId; + + for (auto iter = mStreamTransactionHash.Iter(); !iter.Done(); iter.Next()) { + iter.Data()->TopLevelOuterContentWindowIdChanged(windowId); + } +} + +nsresult Http3Session::OnReadSegment(const char* buf, uint32_t count, + uint32_t* countRead) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG3(("Http3Session::OnReadSegment")); + *countRead = 0; + return NS_OK; +} + +nsresult Http3Session::OnWriteSegment(char* buf, uint32_t count, + uint32_t* countWritten) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG3(("Http3Session::OnWriteSegment")); + *countWritten = 0; + return NS_OK; +} + +// This is called by Http3Stream::OnWriteSegment. +nsresult Http3Session::ReadResponseData(uint64_t aStreamId, char* aBuf, + uint32_t aCount, + uint32_t* aCountWritten, bool* aFin) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + nsresult rv = mHttp3Connection->ReadResponseData(aStreamId, (uint8_t*)aBuf, + aCount, aCountWritten, aFin); + + // This should not happen, i.e. stream must be present in neqo and in necko at + // the same time. + MOZ_ASSERT(rv != NS_ERROR_INVALID_ARG); + if (NS_FAILED(rv)) { + LOG3(("Http3Session::ReadResponseData return an error %" PRIx32 + " [this=%p]", + static_cast(rv), this)); + // This error will be handled by neqo and the whole connection will be + // closed. We will return NS_BASE_STREAM_WOULD_BLOCK here. + *aCountWritten = 0; + *aFin = false; + rv = NS_BASE_STREAM_WOULD_BLOCK; + } + + MOZ_ASSERT((*aCountWritten != 0) || aFin || NS_FAILED(rv)); + return rv; +} + +void Http3Session::TransactionHasDataToWrite(nsAHttpTransaction* caller) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG3(("Http3Session::TransactionHasDataToWrite %p trans=%p", this, caller)); + + // a trapped signal from the http transaction to the connection that + // it is no longer blocked on read. + + RefPtr stream = mStreamTransactionHash.Get(caller); + if (!stream) { + LOG3(("Http3Session::TransactionHasDataToWrite %p caller %p not found", + this, caller)); + return; + } + + LOG3(("Http3Session::TransactionHasDataToWrite %p ID is 0x%" PRIx64, this, + stream->StreamId())); + + if (!IsClosing()) { + StreamReadyToWrite(stream); + } else { + LOG3( + ("Http3Session::TransactionHasDataToWrite %p closed so not setting " + "Ready4Write\n", + this)); + } + + // NSPR poll will not poll the network if there are non system PR_FileDesc's + // that are ready - so we can get into a deadlock waiting for the system IO + // to come back here if we don't force the send loop manually. + Unused << ForceSend(); +} + +void Http3Session::TransactionHasDataToRecv(nsAHttpTransaction* caller) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG3(("Http3Session::TransactionHasDataToRecv %p trans=%p", this, caller)); + + // a signal from the http transaction to the connection that it will consume + // more + RefPtr stream = mStreamTransactionHash.Get(caller); + if (!stream) { + LOG3(("Http3Session::TransactionHasDataToRecv %p caller %p not found", this, + caller)); + return; + } + + LOG3(("Http3Session::TransactionHasDataToRecv %p ID is 0x%" PRIx64 "\n", this, + stream->StreamId())); + ConnectSlowConsumer(stream); +} + +void Http3Session::ConnectSlowConsumer(Http3Stream* stream) { + LOG3(("Http3Session::ConnectSlowConsumer %p 0x%" PRIx64 "\n", this, + stream->StreamId())); + mSlowConsumersReadyForRead.AppendElement(stream); + Unused << ForceRecv(); +} + +bool Http3Session::TestJoinConnection(const nsACString& hostname, + int32_t port) { + return RealJoinConnection(hostname, port, true); +} + +bool Http3Session::JoinConnection(const nsACString& hostname, int32_t port) { + return RealJoinConnection(hostname, port, false); +} + +// TODO test +bool Http3Session::RealJoinConnection(const nsACString& hostname, int32_t port, + bool justKidding) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + if (!mConnection || !CanSandData() || mShouldClose || mGoawayReceived) { + return false; + } + + nsHttpConnectionInfo* ci = ConnectionInfo(); + if (nsCString(hostname).EqualsIgnoreCase(ci->Origin()) && + (port == ci->OriginPort())) { + return true; + } + + nsAutoCString key(hostname); + key.Append(':'); + key.Append(justKidding ? 'k' : '.'); + key.AppendInt(port); + bool cachedResult; + if (mJoinConnectionCache.Get(key, &cachedResult)) { + LOG(("joinconnection [%p %s] %s result=%d cache\n", this, + ConnectionInfo()->HashKey().get(), key.get(), cachedResult)); + return cachedResult; + } + + nsresult rv; + bool isJoined = false; + + nsCOMPtr securityInfo; + nsCOMPtr sslSocketControl; + + mConnection->GetSecurityInfo(getter_AddRefs(securityInfo)); + sslSocketControl = do_QueryInterface(securityInfo, &rv); + if (NS_FAILED(rv) || !sslSocketControl) { + return false; + } + + bool joinedReturn = false; + if (justKidding) { + rv = sslSocketControl->TestJoinConnection(mConnInfo->GetNPNToken(), + hostname, port, &isJoined); + } else { + rv = sslSocketControl->JoinConnection(mConnInfo->GetNPNToken(), hostname, + port, &isJoined); + } + if (NS_SUCCEEDED(rv) && isJoined) { + joinedReturn = true; + } + + LOG(("joinconnection [%p %s] %s result=%d lookup\n", this, + ConnectionInfo()->HashKey().get(), key.get(), joinedReturn)); + mJoinConnectionCache.Put(key, joinedReturn); + if (!justKidding) { + // cache a kidding entry too as this one is good for both + nsAutoCString key2(hostname); + key2.Append(':'); + key2.Append('k'); + key2.AppendInt(port); + if (!mJoinConnectionCache.Get(key2)) { + mJoinConnectionCache.Put(key2, joinedReturn); + } + } + return joinedReturn; +} + +void Http3Session::CallCertVerification() { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG(("Http3Session::CallCertVerification [this=%p]", this)); + + NeqoCertificateInfo certInfo; + if (NS_FAILED(mHttp3Connection->PeerCertificateInfo(&certInfo))) { + LOG(("Http3Session::CallCertVerification [this=%p] - no cert", this)); + mHttp3Connection->PeerAuthenticated(SSL_ERROR_BAD_CERTIFICATE); + mError = psm::GetXPCOMFromNSSError(SSL_ERROR_BAD_CERTIFICATE); + return; + } + + Maybe>> stapledOCSPResponse; + if (certInfo.stapled_ocsp_responses_present) { + stapledOCSPResponse.emplace(std::move(certInfo.stapled_ocsp_responses)); + } + + Maybe> sctsFromTLSExtension; + if (certInfo.signed_cert_timestamp_present) { + sctsFromTLSExtension.emplace(std::move(certInfo.signed_cert_timestamp)); + } + + mSocketControl->SetAuthenticationCallback(this); + uint32_t providerFlags; + // the return value is always NS_OK, just ignore it. + Unused << mSocketControl->GetProviderFlags(&providerFlags); + + SECStatus rv = AuthCertificateHookWithInfo( + mSocketControl, static_cast(this), std::move(certInfo.certs), + stapledOCSPResponse, sctsFromTLSExtension, providerFlags); + if ((rv != SECSuccess) && (rv != SECWouldBlock)) { + LOG(("Http3Session::CallCertVerification [this=%p] AuthCertificate failed", + this)); + mHttp3Connection->PeerAuthenticated(SSL_ERROR_BAD_CERTIFICATE); + mError = psm::GetXPCOMFromNSSError(SSL_ERROR_BAD_CERTIFICATE); + } +} + +void Http3Session::Authenticated(int32_t aError) { + LOG(("Http3Session::Authenticated error=0x%" PRIx32 " [this=%p].", aError, + this)); + if ((mState == INITIALIZING) || (mState == ZERORTT)) { + if (psm::IsNSSErrorCode(aError)) { + mError = psm::GetXPCOMFromNSSError(aError); + LOG(("Http3Session::Authenticated psm-error=0x%" PRIx32 " [this=%p].", + static_cast(mError), this)); + } + mHttp3Connection->PeerAuthenticated(aError); + + // Call OnQuicTimeoutExpired to properly process neqo events and outputs. + // We call OnQuicTimeoutExpired instead of ProcessOutputAndEvents, because + // HttpConnectionUDP must close this session in case of an error. + NS_DispatchToCurrentThread(NewRunnableMethod( + "net::HttpConnectionUDP::OnQuicTimeoutExpired", mSegmentReaderWriter, + &HttpConnectionUDP::OnQuicTimeoutExpired)); + } +} + +void Http3Session::SetSecInfo() { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + NeqoSecretInfo secInfo; + if (NS_SUCCEEDED(mHttp3Connection->GetSecInfo(&secInfo))) { + mSocketControl->SetSSLVersionUsed(secInfo.version); + mSocketControl->SetResumed(secInfo.resumed); + + mSocketControl->SetInfo(secInfo.cipher, secInfo.version, secInfo.group, + secInfo.signature_scheme); + } + + if (!mSocketControl->HasServerCert() && + StaticPrefs::network_ssl_tokens_cache_enabled()) { + mSocketControl->RebuildCertificateInfoFromSSLTokenCache(); + } +} + +void Http3Session::CloseConnectionTelemetry(CloseError& aError, bool aClosing) { + uint64_t value = 0; + + switch (aError.tag) { + case CloseError::Tag::QuicTransportError: + // Transport error have values from 0 to 12. + // (https://tools.ietf.org/html/draft-ietf-quic-transport-24#section-20) + // We will map this error to 0-12. + // 13 wil capture error codes between and including 13 and 0x0ff. This + // error codes are not define by the spec but who know peer may sent them. + // CryptoAlerts have value 0x100 + alert code. For now we will map them + // to 14. (https://tools.ietf.org/html/draft-ietf-quic-tls-24#section-4.9) + // (telemetry does not allow more than 100 bucket and to easily map alerts + // we need 256. If we find problem with too many alerts we could map + // them.) + if (aError.quic_transport_error._0 <= 12) { + value = aError.quic_transport_error._0; + } else if (aError.quic_transport_error._0 < 0x100) { + value = 13; + } else { + value = 14; + } + break; + case CloseError::Tag::Http3AppError: + if (aError.http3_app_error._0 <= 0x110) { + // Http3 error codes are 0x100-0x110. + // (https://tools.ietf.org/html/draft-ietf-quic-http-24#section-8.1) + // The will be mapped to 15-31. + value = (aError.http3_app_error._0 - 0x100) + 15; + } else if (aError.http3_app_error._0 < 0x200) { + // Error codes between 0x111 and 0x1ff are not defined and will be + // mapped int 32 + value = 32; // 0x11 + 15 + } else if (aError.http3_app_error._0 < 0x203) { + // Error codes between 0x200 and 0x202 are related to qpack. + // (https://tools.ietf.org/html/draft-ietf-quic-qpack-11#section-6) + // They will be mapped to 33-35 + value = aError.http3_app_error._0 - 0x200 + 33; + } else { + value = 36; + } + } + + // A connection may be closed in two ways: client side closes connection or + // server side. In former case http3 state will first change to closing state + // in the later case itt will change to closed. In tthis way we can + // distinguish which side is closing connecttion first. If necko closes + // connection, this will map to "closing" key and 37 in the graph. + Telemetry::Accumulate(Telemetry::HTTP3_CONNECTTION_CLOSE_CODE, + aClosing ? "closing"_ns : "closed"_ns, value); + + Http3Stats stats; + mHttp3Connection->GetStats(&stats); + + if (stats.packets_tx > 0) { + unsigned long loss = (stats.lost * 10000) / stats.packets_tx; + Telemetry::Accumulate(Telemetry::HTTP3_LOSS_RATIO, loss); + + Telemetry::Accumulate(Telemetry::HTTP3_LATE_ACK, "ack"_ns, stats.late_ack); + Telemetry::Accumulate(Telemetry::HTTP3_LATE_ACK, "pto"_ns, stats.pto_ack); + + unsigned long late_ack_ratio = (stats.late_ack * 10000) / stats.packets_tx; + unsigned long pto_ack_ratio = (stats.pto_ack * 10000) / stats.packets_tx; + Telemetry::Accumulate(Telemetry::HTTP3_LATE_ACK_RATIO, "ack"_ns, + late_ack_ratio); + Telemetry::Accumulate(Telemetry::HTTP3_LATE_ACK_RATIO, "pto"_ns, + pto_ack_ratio); + + for (uint32_t i = 0; i < MAX_PTO_COUNTS; i++) { + nsAutoCString key; + key.AppendInt(i); + Telemetry::Accumulate(Telemetry::HTTP3_COUNTS_PTO, key, + stats.pto_counts[i]); + } + + Telemetry::Accumulate(Telemetry::HTTP3_DROP_DGRAMS, stats.dropped_rx); + Telemetry::Accumulate(Telemetry::HTTP3_SAVED_DGRAMS, stats.saved_datagrams); + } + + Telemetry::Accumulate(Telemetry::HTTP3_RECEIVED_SENT_DGRAMS, "received"_ns, + stats.packets_rx); + Telemetry::Accumulate(Telemetry::HTTP3_RECEIVED_SENT_DGRAMS, "sent"_ns, + stats.packets_tx); +} + +void Http3Session::Finish0Rtt(bool aRestart) { + for (size_t i = 0; i < m0RTTStreams.Length(); ++i) { + if (m0RTTStreams[i]) { + if (aRestart) { + // When we need to restart transactions remove them from all lists. + if (m0RTTStreams[i]->HasStreamId()) { + mStreamIdHash.Remove(m0RTTStreams[i]->StreamId()); + } + RemoveStreamFromQueues(m0RTTStreams[i]); + // The stream is ready to write again. + mReadyForWrite.Push(m0RTTStreams[i]); + } + m0RTTStreams[i]->Finish0RTT(aRestart); + } + } + + for (size_t i = 0; i < mCannotDo0RTTStreams.Length(); ++i) { + if (mCannotDo0RTTStreams[i]) { + mReadyForWrite.Push(mCannotDo0RTTStreams[i]); + } + } + m0RTTStreams.Clear(); + mCannotDo0RTTStreams.Clear(); + MaybeResumeSend(); +} + +void Http3Session::ReportHttp3Connection() { + if (CanSandData() && !mHttp3ConnectionReported) { + mHttp3ConnectionReported = true; + gHttpHandler->ConnMgr()->ReportHttp3Connection(mSegmentReaderWriter); + MaybeResumeSend(); + } +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/Http3Session.h b/netwerk/protocol/http/Http3Session.h new file mode 100644 index 0000000000..6fe980cea0 --- /dev/null +++ b/netwerk/protocol/http/Http3Session.h @@ -0,0 +1,218 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=4 sw=2 et cindent: */ +/* 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/. */ + +#ifndef Http3Session_H__ +#define Http3Session_H__ + +#include "nsISupportsImpl.h" +#include "nsITimer.h" +#include "mozilla/net/NeqoHttp3Conn.h" +#include "nsAHttpConnection.h" +#include "nsRefPtrHashtable.h" +#include "nsWeakReference.h" +#include "HttpTrafficAnalyzer.h" +#include "mozilla/UniquePtr.h" +#include "nsDeque.h" + +namespace mozilla { +namespace net { + +class HttpConnectionUDP; +class Http3Stream; +class QuicSocketControl; + +// IID for the Http3Session interface +#define NS_HTTP3SESSION_IID \ + { \ + 0x8fc82aaf, 0xc4ef, 0x46ed, { \ + 0x89, 0x41, 0x93, 0x95, 0x8f, 0xac, 0x4f, 0x21 \ + } \ + } + +class Http3Session final : public nsAHttpTransaction, + public nsAHttpConnection, + public nsAHttpSegmentReader, + public nsAHttpSegmentWriter { + public: + NS_DECLARE_STATIC_IID_ACCESSOR(NS_HTTP3SESSION_IID) + + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSAHTTPTRANSACTION + NS_DECL_NSAHTTPCONNECTION(mConnection) + NS_DECL_NSAHTTPSEGMENTREADER + NS_DECL_NSAHTTPSEGMENTWRITER + + Http3Session(); + nsresult Init(const nsHttpConnectionInfo* aConnInfo, + nsISocketTransport* aSocketTransport, + HttpConnectionUDP* readerWriter); + + bool IsConnected() const { return mState == CONNECTED; } + bool CanSandData() const { + return (mState == CONNECTED) || (mState == ZERORTT); + } + bool IsClosing() const { return (mState == CLOSING || mState == CLOSED); } + bool IsClosed() const { return mState == CLOSED; } + + bool AddStream(nsAHttpTransaction* aHttpTransaction, int32_t aPriority, + nsIInterfaceRequestor* aCallbacks); + + bool CanReuse(); + + // overload of nsAHttpTransaction + [[nodiscard]] nsresult ReadSegmentsAgain(nsAHttpSegmentReader*, uint32_t, + uint32_t*, bool*) final; + [[nodiscard]] nsresult WriteSegmentsAgain(nsAHttpSegmentWriter*, uint32_t, + uint32_t*, bool*) final; + + // The folowing functions are used by Http3Stream: + nsresult TryActivating(const nsACString& aMethod, const nsACString& aScheme, + const nsACString& aHost, const nsACString& aPath, + const nsACString& aHeaders, uint64_t* aStreamId, + Http3Stream* aStream); + void CloseSendingSide(uint64_t aStreamId); + nsresult SendRequestBody(uint64_t aStreamId, const char* buf, uint32_t count, + uint32_t* countRead); + nsresult ReadResponseHeaders(uint64_t aStreamId, + nsTArray& aResponseHeaders, bool* aFin); + nsresult ReadResponseData(uint64_t aStreamId, char* aBuf, uint32_t aCount, + uint32_t* aCountWritten, bool* aFin); + + void CloseStream(Http3Stream* aStream, nsresult aResult); + + void SetCleanShutdown(bool aCleanShutdown) { + mCleanShutdown = aCleanShutdown; + } + + bool TestJoinConnection(const nsACString& hostname, int32_t port); + bool JoinConnection(const nsACString& hostname, int32_t port); + + void TransactionHasDataToWrite(nsAHttpTransaction* caller) override; + void TransactionHasDataToRecv(nsAHttpTransaction* caller) override; + + nsISocketTransport* SocketTransport() { return mSocketTransport; } + + // This function will be called by QuicSocketControl when the certificate + // verification is done. + void Authenticated(int32_t aError); + + nsresult ProcessOutputAndEvents(); + + const nsCString& GetAlpnToken() { return mAlpnToken; } + + void ReportHttp3Connection(); + + private: + ~Http3Session(); + + void CloseInternal(bool aCallNeqoClose); + void Shutdown(); + + bool RealJoinConnection(const nsACString& hostname, int32_t port, + bool justKidding); + + nsresult ProcessOutput(); + nsresult ProcessInput(uint32_t* aCountRead); + nsresult ProcessEvents(uint32_t count); + + nsresult ProcessTransactionRead(uint64_t stream_id, uint32_t count, + uint32_t* countWritten); + nsresult ProcessTransactionRead(Http3Stream* stream, uint32_t count, + uint32_t* countWritten); + nsresult ProcessSlowConsumers(); + void ConnectSlowConsumer(Http3Stream* stream); + + void SetupTimer(uint64_t aTimeout); + + void ResetRecvd(uint64_t aStreamId, uint64_t aError); + + void QueueStream(Http3Stream* stream); + void RemoveStreamFromQueues(Http3Stream*); + void ProcessPending(); + + void CallCertVerification(); + void SetSecInfo(); + + void StreamReadyToWrite(Http3Stream* aStream); + void MaybeResumeSend(); + + void CloseConnectionTelemetry(CloseError& aError, bool aClosing); + void Finish0Rtt(bool aRestart); + + RefPtr mHttp3Connection; + RefPtr mConnection; + nsRefPtrHashtable mStreamIdHash; + nsRefPtrHashtable, Http3Stream> + mStreamTransactionHash; + + nsDeque mReadyForWrite; + nsTArray> mSlowConsumersReadyForRead; + nsDeque mQueuedStreams; + + enum State { INITIALIZING, ZERORTT, CONNECTED, CLOSING, CLOSED } mState; + + bool mAuthenticationStarted; + bool mCleanShutdown; + bool mGoawayReceived; + bool mShouldClose; + bool mIsClosedByNeqo; + bool mHttp3ConnectionReported = false; + // mError is neqo error (a protocol error) and that may mean that we will + // send some packets after that. + nsresult mError; + // This is a socket error, there is no poioint in sending anything on that + // socket. + nsresult mSocketError; + bool mBeforeConnectedError; + uint64_t mCurrentForegroundTabOuterContentWindowId; + + // True if the mTimer is inited and waiting for firing. + bool mTimerActive; + + nsTArray mPacketToSend; + + RefPtr mSegmentReaderWriter; + + // The underlying socket transport object is needed to propogate some events + RefPtr mSocketTransport; + + nsCOMPtr mTimer; + + nsDataHashtable mJoinConnectionCache; + + RefPtr mSocketControl; + nsCString mAlpnToken; + + uint64_t mTransactionCount = 0; + + // The stream(s) that we are getting 0RTT data from. + nsTArray> m0RTTStreams; + // The stream(s) that are not able to send 0RTT data. We need to + // remember them put them into mReadyForWrite queue when 0RTT finishes. + nsTArray> mCannotDo0RTTStreams; + + // The following variables are needed for telemetry. + TimeStamp mConnectionIdleStart; + TimeStamp mConnectionIdleEnd; + Maybe mFirstStreamIdReuseIdleConnection; + TimeStamp mTimerShouldTrigger; + uint64_t mBlockedByStreamLimitCount = 0; + uint64_t mTransactionsBlockedByStreamLimitCount = 0; + uint64_t mTransactionsSenderBlockedByFlowControlCount = 0; + + // NS_NET_STATUS_CONNECTED_TO event will be created by the Http3Session. + // We want to propagate it to the first transaction. + RefPtr mFirstHttpTransaction; + + RefPtr mConnInfo; +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(Http3Session, NS_HTTP3SESSION_IID); + +} // namespace net +} // namespace mozilla + +#endif // Http3Session_H__ diff --git a/netwerk/protocol/http/Http3Stream.cpp b/netwerk/protocol/http/Http3Stream.cpp new file mode 100644 index 0000000000..5451eb7c3b --- /dev/null +++ b/netwerk/protocol/http/Http3Stream.cpp @@ -0,0 +1,489 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et 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/. */ + +// HttpLog.h should generally be included first +#include "HttpLog.h" +#include "Http3Session.h" +#include "Http3Stream.h" +#include "nsHttpRequestHead.h" +#include "nsISocketTransport.h" +#include "nsSocketTransportService2.h" + +#include + +namespace mozilla { +namespace net { + +Http3Stream::Http3Stream(nsAHttpTransaction* httpTransaction, + Http3Session* session) + : mSendState(PREPARING_HEADERS), + mRecvState(BEFORE_HEADERS), + mStreamId(UINT64_MAX), + mSession(session), + mTransaction(httpTransaction), + mQueued(false), + mDataReceived(false), + mResetRecv(false), + mRequestBodyLenRemaining(0), + mSocketTransport(session->SocketTransport()), + mTotalSent(0), + mTotalRead(0), + mFin(false), + mSendingBlockedByFlowControlCount(0) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG3(("Http3Stream::Http3Stream [this=%p]", this)); +} + +void Http3Stream::Close(nsresult aResult) { + mRecvState = RECV_DONE; + mTransaction->Close(aResult); +} + +bool Http3Stream::GetHeadersString(const char* buf, uint32_t avail, + uint32_t* countUsed) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG3(("Http3Stream::GetHeadersString %p avail=%u.", this, avail)); + + mFlatHttpRequestHeaders.Append(buf, avail); + // We can use the simple double crlf because firefox is the + // only client we are parsing + int32_t endHeader = mFlatHttpRequestHeaders.Find("\r\n\r\n"); + + if (endHeader == kNotFound) { + // We don't have all the headers yet + LOG3( + ("Http3Stream::GetHeadersString %p " + "Need more header bytes. Len = %u", + this, mFlatHttpRequestHeaders.Length())); + *countUsed = avail; + return false; + } + + uint32_t oldLen = mFlatHttpRequestHeaders.Length(); + mFlatHttpRequestHeaders.SetLength(endHeader + 2); + *countUsed = avail - (oldLen - endHeader) + 4; + + FindRequestContentLength(); + return true; +} + +void Http3Stream::FindRequestContentLength() { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + // Look for Content-Length header to find out if we have request body and + // how long it is. + int32_t contentLengthStart = mFlatHttpRequestHeaders.Find("Content-Length:"); + if (contentLengthStart == -1) { + // There is no content-Length. + return; + } + + // We have Content-Length header, find the end of it. + int32_t crlfIndex = + mFlatHttpRequestHeaders.Find("\r\n", false, contentLengthStart); + if (crlfIndex == -1) { + MOZ_ASSERT(false, "We must have \\r\\n at the end of the headers string."); + return; + } + + // Find the beginning. + int32_t valueIndex = + mFlatHttpRequestHeaders.Find(":", false, contentLengthStart) + 1; + if (valueIndex > crlfIndex) { + // Content-Length headers is empty. + MOZ_ASSERT(false, "Content-Length must have a value."); + return; + } + + const char* beginBuffer = mFlatHttpRequestHeaders.BeginReading(); + while (valueIndex < crlfIndex && beginBuffer[valueIndex] == ' ') { + ++valueIndex; + } + + nsDependentCSubstring value = + Substring(beginBuffer + valueIndex, beginBuffer + crlfIndex); + + int64_t len; + nsCString tmp(value); + if (nsHttp::ParseInt64(tmp.get(), nullptr, &len)) { + mRequestBodyLenRemaining = len; + } +} + +nsresult Http3Stream::TryActivating() { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG(("Http3Stream::TryActivating [this=%p]", this)); + nsHttpRequestHead* head = mTransaction->RequestHead(); + + nsAutoCString authorityHeader; + nsresult rv = head->GetHeader(nsHttp::Host, authorityHeader); + if (NS_FAILED(rv)) { + MOZ_ASSERT(false); + return rv; + } + + nsDependentCString scheme(head->IsHTTPS() ? "https" : "http"); + + nsAutoCString method; + nsAutoCString path; + head->Method(method); + head->Path(path); + + return mSession->TryActivating(method, scheme, authorityHeader, path, + mFlatHttpRequestHeaders, &mStreamId, this); +} + +nsresult Http3Stream::OnReadSegment(const char* buf, uint32_t count, + uint32_t* countRead) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + LOG(("Http3Stream::OnReadSegment count=%u state=%d [this=%p]", count, + mSendState, this)); + + nsresult rv = NS_OK; + + switch (mSendState) { + case PREPARING_HEADERS: { + bool done = GetHeadersString(buf, count, countRead); + + if (*countRead) { + mTotalSent += *countRead; + } + + if (!done) { + break; + } + mSendState = WAITING_TO_ACTIVATE; + } + [[fallthrough]]; + case WAITING_TO_ACTIVATE: + rv = TryActivating(); + if (rv == NS_BASE_STREAM_WOULD_BLOCK) { + LOG3(("Http3Stream::OnReadSegment %p cannot activate now. queued.\n", + this)); + rv = *countRead ? NS_OK : NS_BASE_STREAM_WOULD_BLOCK; + break; + } + if (NS_FAILED(rv)) { + LOG3(("Http3Stream::OnReadSegment %p cannot activate error=0x%" PRIx32 + ".", + this, static_cast(rv))); + break; + } + + // Successfully activated. + mTransaction->OnTransportStatus(mSocketTransport, + NS_NET_STATUS_SENDING_TO, mTotalSent); + + if (mRequestBodyLenRemaining) { + mSendState = SENDING_BODY; + } else { + mTransaction->OnTransportStatus(mSocketTransport, + NS_NET_STATUS_WAITING_FOR, 0); + mSession->CloseSendingSide(mStreamId); + mSendState = SEND_DONE; + } + break; + case SENDING_BODY: { + rv = mSession->SendRequestBody(mStreamId, buf, count, countRead); + if (rv == NS_BASE_STREAM_WOULD_BLOCK) { + mSendingBlockedByFlowControlCount++; + } + MOZ_ASSERT(mRequestBodyLenRemaining >= *countRead, + "We cannot send more that than we promised."); + if (mRequestBodyLenRemaining < *countRead) { + rv = NS_ERROR_UNEXPECTED; + } + if (NS_FAILED(rv)) { + LOG3( + ("Http3Stream::OnReadSegment %p sending body returns " + "error=0x%" PRIx32 ".", + this, static_cast(rv))); + break; + } + + mRequestBodyLenRemaining -= *countRead; + if (!mRequestBodyLenRemaining) { + mTransaction->OnTransportStatus(mSocketTransport, + NS_NET_STATUS_WAITING_FOR, 0); + mSession->CloseSendingSide(mStreamId); + mSendState = SEND_DONE; + Telemetry::Accumulate( + Telemetry::HTTP3_SENDING_BLOCKED_BY_FLOW_CONTROL_PER_TRANS, + mSendingBlockedByFlowControlCount); + } + } break; + case EARLY_RESPONSE: + // We do not need to send the rest of the request, so just ignore it. + *countRead = count; + mRequestBodyLenRemaining -= count; + if (!mRequestBodyLenRemaining) { + mTransaction->OnTransportStatus(mSocketTransport, + NS_NET_STATUS_WAITING_FOR, 0); + mSendState = SEND_DONE; + } + break; + default: + MOZ_ASSERT(false, "We are done sending this request!"); + break; + } + + mSocketOutCondition = rv; + + return mSocketOutCondition; +} + +void Http3Stream::SetResponseHeaders(nsTArray& aResponseHeaders, + bool aFin) { + MOZ_ASSERT(mRecvState == BEFORE_HEADERS); + MOZ_ASSERT(mFlatResponseHeaders.IsEmpty(), + "Cannot set response headers more than once"); + mFlatResponseHeaders = std::move(aResponseHeaders); + mRecvState = READING_HEADERS; + mDataReceived = true; + mFin = aFin; +} + +void Http3Stream::StopSending() { + MOZ_ASSERT((mSendState == SENDING_BODY) || (mSendState == SEND_DONE)); + if (mSendState == SENDING_BODY) { + mSendState = EARLY_RESPONSE; + } +} + +nsresult Http3Stream::OnWriteSegment(char* buf, uint32_t count, + uint32_t* countWritten) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + LOG(("Http3Stream::OnWriteSegment [this=%p, state=%d", this, mRecvState)); + nsresult rv = NS_OK; + switch (mRecvState) { + case BEFORE_HEADERS: { + *countWritten = 0; + rv = NS_BASE_STREAM_WOULD_BLOCK; + } break; + case READING_HEADERS: { + // SetResponseHeaders should have been previously called. + MOZ_ASSERT(!mFlatResponseHeaders.IsEmpty(), "Headers empty!"); + *countWritten = (mFlatResponseHeaders.Length() > count) + ? count + : mFlatResponseHeaders.Length(); + memcpy(buf, mFlatResponseHeaders.Elements(), *countWritten); + + mFlatResponseHeaders.RemoveElementsAt(0, *countWritten); + if (mFlatResponseHeaders.Length() == 0) { + mRecvState = mFin ? RECEIVED_FIN : READING_DATA; + } + + if (*countWritten == 0) { + rv = NS_BASE_STREAM_WOULD_BLOCK; + } else { + mTotalRead += *countWritten; + mTransaction->OnTransportStatus( + mSocketTransport, NS_NET_STATUS_RECEIVING_FROM, mTotalRead); + } + } break; + case READING_DATA: { + rv = mSession->ReadResponseData(mStreamId, buf, count, countWritten, + &mFin); + if (NS_FAILED(rv)) { + break; + } + if (*countWritten == 0) { + if (mFin) { + mRecvState = RECV_DONE; + rv = NS_BASE_STREAM_CLOSED; + } else { + rv = NS_BASE_STREAM_WOULD_BLOCK; + } + } else { + mTotalRead += *countWritten; + mTransaction->OnTransportStatus( + mSocketTransport, NS_NET_STATUS_RECEIVING_FROM, mTotalRead); + + if (mFin) { + mRecvState = RECEIVED_FIN; + } + } + } break; + case RECEIVED_FIN: + rv = NS_BASE_STREAM_CLOSED; + mRecvState = RECV_DONE; + break; + case RECV_DONE: + rv = NS_ERROR_UNEXPECTED; + } + + // Remember the error received from lower layers. A stream pipe may overwrite + // it. + // If rv == NS_OK this will reset mSocketInCondition. + mSocketInCondition = rv; + + return rv; +} + +nsresult Http3Stream::ReadSegments(nsAHttpSegmentReader* reader) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + if (mRecvState == RECV_DONE) { + // Don't transmit any request frames if the peer cannot respond or respone + // is already done. + LOG3( + ("Http3Stream %p ReadSegments request stream aborted due to" + " response side closure\n", + this)); + return NS_ERROR_ABORT; + } + + nsresult rv = NS_OK; + uint32_t transactionBytes; + bool again = true; + do { + transactionBytes = 0; + rv = mSocketOutCondition = NS_OK; + LOG(("Http3Stream::ReadSegments state=%d [this=%p]", mSendState, this)); + switch (mSendState) { + case WAITING_TO_ACTIVATE: { + // A transaction that had already generated its headers before it was + // queued at the session level (due to concurrency concerns) may not + // call onReadSegment off the ReadSegments() stack above. + LOG3( + ("Http3Stream %p ReadSegments forcing OnReadSegment call\n", this)); + uint32_t wasted = 0; + nsresult rv2 = OnReadSegment("", 0, &wasted); + LOG3((" OnReadSegment returned 0x%08" PRIx32, + static_cast(rv2))); + if (mSendState != SENDING_BODY) { + break; + } + } + // If we are in state SENDING_BODY we can continue sending data. + [[fallthrough]]; + case PREPARING_HEADERS: + case SENDING_BODY: { + rv = mTransaction->ReadSegmentsAgain( + this, nsIOService::gDefaultSegmentSize, &transactionBytes, &again); + } break; + default: + transactionBytes = 0; + rv = NS_OK; + break; + } + + LOG(("Http3Stream::ReadSegments rv=0x%" PRIx32 " read=%u sock-cond=%" PRIx32 + " again=%d [this=%p]", + static_cast(rv), transactionBytes, + static_cast(mSocketOutCondition), again, this)); + + // XXX some streams return NS_BASE_STREAM_CLOSED to indicate EOF. + if (rv == NS_BASE_STREAM_CLOSED && !mTransaction->IsDone()) { + rv = NS_OK; + transactionBytes = 0; + } + + if (NS_FAILED(rv)) { + // if the transaction didn't want to write any more data, then + // wait for the transaction to call ResumeSend. + if (rv == NS_BASE_STREAM_WOULD_BLOCK) { + rv = NS_OK; + } + again = false; + } else if (NS_FAILED(mSocketOutCondition)) { + if (mSocketOutCondition != NS_BASE_STREAM_WOULD_BLOCK) { + rv = mSocketOutCondition; + } + again = false; + } else if (!transactionBytes) { + rv = NS_OK; + again = false; + } + // write more to the socket until error or end-of-request... + } while (again && gHttpHandler->Active()); + return rv; +} + +nsresult Http3Stream::WriteSegments(nsAHttpSegmentWriter* writer, + uint32_t count, uint32_t* countWritten) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG(("Http3Stream::WriteSegments [this=%p]", this)); + nsresult rv = NS_OK; + uint32_t countWrittenSingle = 0; + bool again = true; + + do { + mSocketInCondition = NS_OK; + rv = mTransaction->WriteSegmentsAgain(this, count, &countWrittenSingle, + &again); + *countWritten += countWrittenSingle; + LOG(("Http3Stream::WriteSegments rv=0x%" PRIx32 + " countWrittenSingle=%" PRIu32 " socketin=%" PRIx32 " [this=%p]", + static_cast(rv), countWrittenSingle, + static_cast(mSocketInCondition), this)); + if (mTransaction->IsDone()) { + // If a transaction has read the amount of data specified in + // Content-Length it is marked as done.The Http3Stream should be + // marked as done as well to start the process of cleanup and + // closure. + mRecvState = RECV_DONE; + } + + if (NS_FAILED(rv)) { + // if the transaction didn't want to take any more data, then + // wait for the transaction to call ResumeRecv. + if (rv == NS_BASE_STREAM_WOULD_BLOCK) { + rv = NS_OK; + } + again = false; + } else if (NS_FAILED(mSocketInCondition)) { + if (mSocketInCondition != NS_BASE_STREAM_WOULD_BLOCK) { + rv = mSocketInCondition; + } + again = false; + } + // read more from the socket until error... + } while (again && gHttpHandler->Active()); + + return rv; +} + +bool Http3Stream::Do0RTT() { + MOZ_ASSERT(mTransaction); + mAttempting0RTT = mTransaction->Do0RTT(); + return mAttempting0RTT; +} + +nsresult Http3Stream::Finish0RTT(bool aRestart) { + MOZ_ASSERT(mTransaction); + mAttempting0RTT = false; + nsresult rv = mTransaction->Finish0RTT(aRestart, false); + if (aRestart) { + nsHttpTransaction* trans = mTransaction->QueryHttpTransaction(); + if (trans) { + trans->Refused0RTT(); + } + + // Reset Http3Sream states as well. + mSendState = PREPARING_HEADERS; + mRecvState = BEFORE_HEADERS; + mStreamId = UINT64_MAX; + mFlatHttpRequestHeaders = ""; + mQueued = false; + mDataReceived = false; + mResetRecv = false; + mFlatResponseHeaders.TruncateLength(0); + mRequestBodyLenRemaining = 0; + mTotalSent = 0; + mTotalRead = 0; + mFin = false; + mSendingBlockedByFlowControlCount = 0; + mSocketInCondition = NS_ERROR_NOT_INITIALIZED; + mSocketOutCondition = NS_ERROR_NOT_INITIALIZED; + } + + return rv; +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/Http3Stream.h b/netwerk/protocol/http/Http3Stream.h new file mode 100644 index 0000000000..8463a3f860 --- /dev/null +++ b/netwerk/protocol/http/Http3Stream.h @@ -0,0 +1,158 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef mozilla_net_Http3Stream_h +#define mozilla_net_Http3Stream_h + +#include "nsAHttpTransaction.h" +#include "ARefBase.h" +#include "mozilla/WeakPtr.h" + +namespace mozilla { +namespace net { + +class Http3Session; + +class Http3Stream final : public nsAHttpSegmentReader, + public nsAHttpSegmentWriter, + public SupportsWeakPtr, + public ARefBase { + public: + NS_DECL_NSAHTTPSEGMENTREADER + NS_DECL_NSAHTTPSEGMENTWRITER + // for RefPtr + NS_INLINE_DECL_REFCOUNTING(Http3Stream, override) + + Http3Stream(nsAHttpTransaction* httpTransaction, Http3Session* session); + + bool HasStreamId() const { return mStreamId != UINT64_MAX; } + uint64_t StreamId() const { return mStreamId; } + + nsresult TryActivating(); + + // TODO priorities + void TopLevelOuterContentWindowIdChanged(uint64_t windowId){}; + + [[nodiscard]] nsresult ReadSegments(nsAHttpSegmentReader*); + [[nodiscard]] nsresult WriteSegments(nsAHttpSegmentWriter*, uint32_t, + uint32_t*); + + void SetQueued(bool aStatus) { mQueued = aStatus; } + bool Queued() const { return mQueued; } + + bool Done() const { return mRecvState == RECV_DONE; } + + void Close(nsresult aResult); + bool RecvdData() const { return mDataReceived; } + + nsAHttpTransaction* Transaction() { return mTransaction; } + bool RecvdFin() const { return mFin; } + bool RecvdReset() const { return mResetRecv; } + void SetRecvdReset() { mResetRecv = true; } + + void StopSending(); + + void SetResponseHeaders(nsTArray& aResponseHeaders, bool fin); + + // Mirrors nsAHttpTransaction + bool Do0RTT(); + nsresult Finish0RTT(bool aRestart); + + private: + ~Http3Stream() = default; + + bool GetHeadersString(const char* buf, uint32_t avail, uint32_t* countUsed); + nsresult StartRequest(); + void FindRequestContentLength(); + + /** + * SendStreamState: + * While sending request: + * - PREPARING_HEADERS: + * In this state we are collecting the headers and in some cases also + * waiting to be able to create a new stream. + * We need to read all headers into a buffer before calling + * Http3Session::TryActivating. Neqo may not have place for a new + * stream if it hits MAX_STREAMS limit. In that case the steam will be + * queued and dequeue when neqo can again create new stream + * (RequestsCreatable will be called). + * If transaction has data to send state changes to SENDING_BODY, + * otherwise the state transfers to READING_HEADERS. + * - SENDING_BODY: + * The stream will be in this state while the transaction is sending + * request body. Http3Session::SendRequestBody will be call to give + * the data to neqo. + * After SENDING_BODY, the state transfers to READING_HEADERS. + * - EARLY_RESPONSE: + * The server may send STOP_SENDING frame with error HTTP_NO_ERROR. + * That error means that the server is not interested in the request + * body. In this state the server will just ignore the request body. + **/ + enum SendStreamState { + PREPARING_HEADERS, + WAITING_TO_ACTIVATE, + SENDING_BODY, + EARLY_RESPONSE, + SEND_DONE + } mSendState; + + /** + * RecvStreamState: + * - BEFORE_HEADERS: + * The stream has not received headers yet. + * - READING_HEADERS: + * In this state Http3Session::ReadResponseHeaders will be called to read + * the response headers. All headers will be read at once into + * mFlatResponseHeaders. The stream will be in this state until all + * headers are given to the transaction. + * If the stream has been closed by the server after sending headers the + * stream will transit into RECEIVED_FIN state, otherwise it transits to + * READING_DATA state. + * - READING_DATA: + * In this state Http3Session::ReadResponseData will be called and the + * response body will be given to the transaction. + * This state may transfer to RECEIVED_FIN or DONE state. + * - DONE: + * The transaction is done. + **/ + enum RecvStreamState { + BEFORE_HEADERS, + READING_HEADERS, + READING_DATA, + RECEIVED_FIN, + RECV_DONE + } mRecvState; + + uint64_t mStreamId; + Http3Session* mSession; + RefPtr mTransaction; + nsCString mFlatHttpRequestHeaders; + bool mQueued; + bool mDataReceived; + bool mResetRecv; + nsTArray mFlatResponseHeaders; + uint32_t mRequestBodyLenRemaining; + + // The underlying socket transport object is needed to propogate some events + RefPtr mSocketTransport; + + // For Progress Events + uint64_t mTotalSent; + uint64_t mTotalRead; + + bool mFin; + + bool mAttempting0RTT = false; + + uint32_t mSendingBlockedByFlowControlCount = 0; + + nsresult mSocketInCondition = NS_ERROR_NOT_INITIALIZED; + nsresult mSocketOutCondition = NS_ERROR_NOT_INITIALIZED; +}; + +} // namespace net +} // namespace mozilla + +#endif // mozilla_net_Http3Stream_h diff --git a/netwerk/protocol/http/HttpAuthUtils.cpp b/netwerk/protocol/http/HttpAuthUtils.cpp new file mode 100644 index 0000000000..d48d4aa0fe --- /dev/null +++ b/netwerk/protocol/http/HttpAuthUtils.cpp @@ -0,0 +1,169 @@ +/* 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/net/HttpAuthUtils.h" +#include "mozilla/Tokenizer.h" +#include "nsIURI.h" +#include "nsNetUtil.h" +#include "nsUnicharUtils.h" + +namespace mozilla { +namespace net { +namespace auth { + +namespace detail { + +bool MatchesBaseURI(const nsACString& matchScheme, const nsACString& matchHost, + int32_t matchPort, nsDependentCSubstring const& url) { + // check if scheme://host:port matches baseURI + + // parse the base URI + mozilla::Tokenizer t(url); + mozilla::Tokenizer::Token token; + + t.SkipWhites(); + + // We don't know if the url to check against starts with scheme + // or a host name. Start recording here. + t.Record(); + + mozilla::Unused << t.Next(token); + + // The ipv6 literals MUST be enclosed with [] in the preference. + bool ipv6 = false; + if (token.Equals(mozilla::Tokenizer::Token::Char('['))) { + nsDependentCSubstring ipv6BareLiteral; + if (!t.ReadUntil(mozilla::Tokenizer::Token::Char(']'), ipv6BareLiteral)) { + // Broken ipv6 literal + return false; + } + + nsDependentCSubstring ipv6Literal; + t.Claim(ipv6Literal, mozilla::Tokenizer::INCLUDE_LAST); + if (!matchHost.Equals(ipv6Literal, nsCaseInsensitiveUTF8StringComparator) && + !matchHost.Equals(ipv6BareLiteral, + nsCaseInsensitiveUTF8StringComparator)) { + return false; + } + + ipv6 = true; + } else if (t.CheckChar(':') && t.CheckChar('/') && t.CheckChar('/')) { + if (!matchScheme.Equals(token.Fragment())) { + return false; + } + // Re-start recording the hostname from the point after scheme://. + t.Record(); + } + + while (t.Next(token)) { + bool eof = token.Equals(mozilla::Tokenizer::Token::EndOfFile()); + bool port = token.Equals(mozilla::Tokenizer::Token::Char(':')); + + if (eof || port) { + if (!ipv6) { // Match already performed above. + nsDependentCSubstring hostName; + t.Claim(hostName); + + // An empty hostname means to accept everything for the schema + if (!hostName.IsEmpty()) { + /* + host: bar.com foo.bar.com foobar.com foo.bar.com bar.com + pref: bar.com bar.com bar.com .bar.com .bar.com + result: accept accept reject accept reject + */ + if (!StringEndsWith(matchHost, hostName, + nsCaseInsensitiveUTF8StringComparator)) { + return false; + } + if (matchHost.Length() > hostName.Length() && + matchHost[matchHost.Length() - hostName.Length() - 1] != '.' && + hostName[0] != '.') { + return false; + } + } + } + + if (port) { + uint16_t portNumber; + if (!t.ReadInteger(&portNumber)) { + // Missing port number + return false; + } + if (matchPort != portNumber) { + return false; + } + if (!t.CheckEOF()) { + return false; + } + } + } else if (ipv6) { + // After an ipv6 literal there can only be EOF or :port. Everything else + // must be treated as non-match/broken input. + return false; + } + } + + // All negative checks has passed positively. + return true; +} + +} // namespace detail + +bool URIMatchesPrefPattern(nsIURI* uri, const char* pref) { + nsCOMPtr prefs = do_GetService(NS_PREFSERVICE_CONTRACTID); + if (!prefs) { + return false; + } + + nsAutoCString scheme, host; + int32_t port; + + if (NS_FAILED(uri->GetScheme(scheme))) { + return false; + } + if (NS_FAILED(uri->GetAsciiHost(host))) { + return false; + } + + port = NS_GetRealPort(uri); + if (port == -1) { + return false; + } + + nsAutoCString hostList; + if (NS_FAILED(prefs->GetCharPref(pref, hostList))) { + return false; + } + + // pseudo-BNF + // ---------- + // + // url-list base-url ( base-url "," LWS )* + // base-url ( scheme-part | host-part | scheme-part host-part ) + // scheme-part scheme "://" + // host-part host [":" port] + // + // for example: + // "https://, http://office.foo.com" + // + + mozilla::Tokenizer t(hostList); + while (!t.CheckEOF()) { + t.SkipWhites(); + nsDependentCSubstring url; + mozilla::Unused << t.ReadUntil(mozilla::Tokenizer::Token::Char(','), url); + if (url.IsEmpty()) { + continue; + } + if (detail::MatchesBaseURI(scheme, host, port, url)) { + return true; + } + } + + return false; +} + +} // namespace auth +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/HttpAuthUtils.h b/netwerk/protocol/http/HttpAuthUtils.h new file mode 100644 index 0000000000..c045f81701 --- /dev/null +++ b/netwerk/protocol/http/HttpAuthUtils.h @@ -0,0 +1,33 @@ +/* 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/. */ + +#ifndef HttpAuthUtils_h__ +#define HttpAuthUtils_h__ + +class nsIURI; + +namespace mozilla { +namespace net { +namespace auth { + +/* Tries to match the given URI against the value of a given pref + * + * The pref should be in pseudo-BNF format. + * url-list base-url ( base-url "," LWS )* + * base-url ( scheme-part | host-part | scheme-part host-part ) + * scheme-part scheme "://" + * host-part host [":" port] + * + * for example: + * "https://, http://office.foo.com" + * + * Will return true if the URI matches any of the patterns, or false otherwise. + */ +bool URIMatchesPrefPattern(nsIURI* uri, const char* pref); + +} // namespace auth +} // namespace net +} // namespace mozilla + +#endif // HttpAuthUtils_h__ diff --git a/netwerk/protocol/http/HttpBackgroundChannelChild.cpp b/netwerk/protocol/http/HttpBackgroundChannelChild.cpp new file mode 100644 index 0000000000..4c7b6cddd8 --- /dev/null +++ b/netwerk/protocol/http/HttpBackgroundChannelChild.cpp @@ -0,0 +1,504 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et 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/. */ + +// HttpLog.h should generally be included first +#include "HttpLog.h" + +#include "HttpBackgroundChannelChild.h" + +#include "HttpChannelChild.h" +#include "mozilla/ipc/BackgroundChild.h" +#include "mozilla/ipc/PBackgroundChild.h" +#include "mozilla/IntegerPrintfMacros.h" +#include "mozilla/net/BackgroundDataBridgeChild.h" +#include "mozilla/Unused.h" +#include "nsSocketTransportService2.h" + +using mozilla::ipc::BackgroundChild; +using mozilla::ipc::IPCResult; + +namespace mozilla { +namespace net { + +// HttpBackgroundChannelChild +HttpBackgroundChannelChild::HttpBackgroundChannelChild() = default; + +HttpBackgroundChannelChild::~HttpBackgroundChannelChild() = default; + +nsresult HttpBackgroundChannelChild::Init(HttpChannelChild* aChannelChild) { + LOG( + ("HttpBackgroundChannelChild::Init [this=%p httpChannel=%p " + "channelId=%" PRIu64 "]\n", + this, aChannelChild, aChannelChild->ChannelId())); + MOZ_ASSERT(OnSocketThread()); + NS_ENSURE_ARG(aChannelChild); + + mChannelChild = aChannelChild; + + if (NS_WARN_IF(!CreateBackgroundChannel())) { +#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED + mChannelChild->mCreateBackgroundChannelFailed = true; +#endif + mChannelChild = nullptr; + return NS_ERROR_FAILURE; + } + + mFirstODASource = ODA_PENDING; + mOnStopRequestCalled = false; + return NS_OK; +} + +void HttpBackgroundChannelChild::CreateDataBridge() { + MOZ_ASSERT(OnSocketThread()); + + if (!mChannelChild) { + return; + } + + PBackgroundChild* actorChild = + BackgroundChild::GetOrCreateSocketActorForCurrentThread(); + if (NS_WARN_IF(!actorChild)) { + return; + } + + RefPtr dataBridgeChild = + new BackgroundDataBridgeChild(this); + Unused << actorChild->SendPBackgroundDataBridgeConstructor( + dataBridgeChild, mChannelChild->ChannelId()); +} + +void HttpBackgroundChannelChild::OnChannelClosed() { + LOG(("HttpBackgroundChannelChild::OnChannelClosed [this=%p]\n", this)); + MOZ_ASSERT(OnSocketThread()); + +#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED + if (mChannelChild) { + mChannelChild->mBackgroundChildQueueFinalState = + mQueuedRunnables.IsEmpty() ? HttpChannelChild::BCKCHILD_EMPTY + : HttpChannelChild::BCKCHILD_NON_EMPTY; + } +#endif + + // HttpChannelChild is not going to handle any incoming message. + mChannelChild = nullptr; + + // Remove pending IPC messages as well. + mQueuedRunnables.Clear(); + mConsoleReportTask = nullptr; +} + +bool HttpBackgroundChannelChild::ChannelClosed() { + MOZ_ASSERT(OnSocketThread()); + + return !mChannelChild; +} + +void HttpBackgroundChannelChild::OnStartRequestReceived( + Maybe aMultiPartID) { + LOG(("HttpBackgroundChannelChild::OnStartRequestReceived [this=%p]\n", this)); + MOZ_ASSERT(OnSocketThread()); + MOZ_ASSERT(mChannelChild); + MOZ_ASSERT(!mStartReceived || *aMultiPartID > 0); + + mStartReceived = true; + + nsTArray> runnables = std::move(mQueuedRunnables); + + for (const auto& event : runnables) { + // Note: these runnables call Recv* methods on HttpBackgroundChannelChild + // but not the Process* methods on HttpChannelChild. + event->Run(); + } + + // Ensure no new message is enqueued. + MOZ_ASSERT(mQueuedRunnables.IsEmpty()); +} + +bool HttpBackgroundChannelChild::CreateBackgroundChannel() { + LOG(("HttpBackgroundChannelChild::CreateBackgroundChannel [this=%p]\n", + this)); + MOZ_ASSERT(OnSocketThread()); + MOZ_ASSERT(mChannelChild); + + PBackgroundChild* actorChild = BackgroundChild::GetOrCreateForCurrentThread(); + if (NS_WARN_IF(!actorChild)) { + return false; + } + + const uint64_t channelId = mChannelChild->ChannelId(); + if (!actorChild->SendPHttpBackgroundChannelConstructor(this, channelId)) { + return false; + } + + mChannelChild->OnBackgroundChildReady(this); + return true; +} + +IPCResult HttpBackgroundChannelChild::RecvOnAfterLastPart( + const nsresult& aStatus) { + LOG(("HttpBackgroundChannelChild::RecvOnAfterLastPart [this=%p]\n", this)); + MOZ_ASSERT(OnSocketThread()); + + if (NS_WARN_IF(!mChannelChild)) { + return IPC_OK(); + } + + mChannelChild->ProcessOnAfterLastPart(aStatus); + return IPC_OK(); +} + +IPCResult HttpBackgroundChannelChild::RecvOnProgress( + const int64_t& aProgress, const int64_t& aProgressMax) { + LOG(("HttpBackgroundChannelChild::RecvOnProgress [this=%p]\n", this)); + MOZ_ASSERT(OnSocketThread()); + + if (NS_WARN_IF(!mChannelChild)) { + return IPC_OK(); + } + + mChannelChild->ProcessOnProgress(aProgress, aProgressMax); + return IPC_OK(); +} + +IPCResult HttpBackgroundChannelChild::RecvOnStatus(const nsresult& aStatus) { + LOG(("HttpBackgroundChannelChild::RecvOnStatus [this=%p]\n", this)); + MOZ_ASSERT(OnSocketThread()); + + if (NS_WARN_IF(!mChannelChild)) { + return IPC_OK(); + } + + mChannelChild->ProcessOnStatus(aStatus); + return IPC_OK(); +} + +bool HttpBackgroundChannelChild::IsWaitingOnStartRequest() { + MOZ_ASSERT(OnSocketThread()); + + // Need to wait for OnStartRequest if it is sent by + // parent process but not received by content process. + return !mStartReceived; +} + +// PHttpBackgroundChannelChild +IPCResult HttpBackgroundChannelChild::RecvOnStartRequest( + const nsHttpResponseHead& aResponseHead, const bool& aUseResponseHead, + const nsHttpHeaderArray& aRequestHeaders, + const HttpChannelOnStartRequestArgs& aArgs) { + LOG(( + "HttpBackgroundChannelChild::RecvOnStartRequest [this=%p, status=%" PRIx32 + "]\n", + this, static_cast(aArgs.channelStatus()))); + MOZ_ASSERT(OnSocketThread()); + + if (NS_WARN_IF(!mChannelChild)) { + return IPC_OK(); + } + + mFirstODASource = + aArgs.dataFromSocketProcess() ? ODA_FROM_SOCKET : ODA_FROM_PARENT; + + mChannelChild->ProcessOnStartRequest(aResponseHead, aUseResponseHead, + aRequestHeaders, aArgs); + // Allow to queue other runnable since OnStartRequest Event already hits the + // child's mEventQ. + OnStartRequestReceived(aArgs.multiPartID()); + + return IPC_OK(); +} + +IPCResult HttpBackgroundChannelChild::RecvOnTransportAndData( + const nsresult& aChannelStatus, const nsresult& aTransportStatus, + const uint64_t& aOffset, const uint32_t& aCount, + const nsDependentCSubstring& aData, const bool& aDataFromSocketProcess) { + RefPtr self = this; + nsCString data(aData); + std::function callProcessOnTransportAndData = + [self, aChannelStatus, aTransportStatus, aOffset, aCount, data, + aDataFromSocketProcess]() { + LOG( + ("HttpBackgroundChannelChild::RecvOnTransportAndData [this=%p, " + "aDataFromSocketProcess=%d, mFirstODASource=%d]\n", + self.get(), aDataFromSocketProcess, self->mFirstODASource)); + MOZ_ASSERT(OnSocketThread()); + + if (NS_WARN_IF(!self->mChannelChild)) { + return; + } + + if (((self->mFirstODASource == ODA_FROM_SOCKET) && + !aDataFromSocketProcess) || + ((self->mFirstODASource == ODA_FROM_PARENT) && + aDataFromSocketProcess)) { + return; + } + + // The HttpTransactionChild in socket process may not know that this + // request is cancelled or failed due to the IPC delay. In this case, we + // should not forward ODA to HttpChannelChild. + nsresult channelStatus; + self->mChannelChild->GetStatus(&channelStatus); + if (NS_FAILED(channelStatus)) { + return; + } + + self->mChannelChild->ProcessOnTransportAndData( + aChannelStatus, aTransportStatus, aOffset, aCount, data); + }; + + // Bug 1641336: Race only happens if the data is from socket process. + if (IsWaitingOnStartRequest()) { + LOG((" > pending until OnStartRequest [offset=%" PRIu64 " count=%" PRIu32 + "]\n", + aOffset, aCount)); + + mQueuedRunnables.AppendElement(NS_NewRunnableFunction( + "HttpBackgroundChannelChild::RecvOnTransportAndData", + std::move(callProcessOnTransportAndData))); + return IPC_OK(); + } + + callProcessOnTransportAndData(); + return IPC_OK(); +} + +IPCResult HttpBackgroundChannelChild::RecvOnStopRequest( + const nsresult& aChannelStatus, const ResourceTimingStructArgs& aTiming, + const TimeStamp& aLastActiveTabOptHit, + const nsHttpHeaderArray& aResponseTrailers, + nsTArray&& aConsoleReports, + const bool& aFromSocketProcess) { + LOG( + ("HttpBackgroundChannelChild::RecvOnStopRequest [this=%p, " + "aFromSocketProcess=%d, mFirstODASource=%d]\n", + this, aFromSocketProcess, mFirstODASource)); + MOZ_ASSERT(gSocketTransportService); + MOZ_ASSERT(gSocketTransportService->IsOnCurrentThreadInfallible()); + + // It's enough to set this from (just before) OnStopRequest notification, + // since we don't need this value sooner than a channel was done loading - + // everything this timestamp affects takes place only after a channel's + // OnStopRequest. + nsHttp::SetLastActiveTabLoadOptimizationHit(aLastActiveTabOptHit); + + if (NS_WARN_IF(!mChannelChild)) { + return IPC_OK(); + } + + if (IsWaitingOnStartRequest()) { + LOG((" > pending until OnStartRequest [status=%" PRIx32 "]\n", + static_cast(aChannelStatus))); + + RefPtr self = this; + + nsCOMPtr task = NS_NewRunnableFunction( + "HttpBackgroundChannelChild::RecvOnStopRequest", + [self, aChannelStatus, aTiming, aLastActiveTabOptHit, aResponseTrailers, + consoleReports = CopyableTArray{std::move(aConsoleReports)}, + aFromSocketProcess]() mutable { + self->RecvOnStopRequest(aChannelStatus, aTiming, aLastActiveTabOptHit, + aResponseTrailers, std::move(consoleReports), + aFromSocketProcess); + }); + + mQueuedRunnables.AppendElement(task.forget()); + return IPC_OK(); + } + + if (mFirstODASource != ODA_FROM_SOCKET) { + if (!aFromSocketProcess) { + mOnStopRequestCalled = true; + mChannelChild->ProcessOnStopRequest(aChannelStatus, aTiming, + aResponseTrailers, + std::move(aConsoleReports), false); + } + return IPC_OK(); + } + + MOZ_ASSERT(mFirstODASource == ODA_FROM_SOCKET); + + if (aFromSocketProcess) { + MOZ_ASSERT(!mOnStopRequestCalled); + mOnStopRequestCalled = true; + mChannelChild->ProcessOnStopRequest(aChannelStatus, aTiming, + aResponseTrailers, + std::move(aConsoleReports), true); + if (mConsoleReportTask) { + mConsoleReportTask(); + mConsoleReportTask = nullptr; + } + return IPC_OK(); + } + + return IPC_OK(); +} + +IPCResult HttpBackgroundChannelChild::RecvOnConsoleReport( + nsTArray&& aConsoleReports) { + LOG(("HttpBackgroundChannelChild::RecvOnConsoleReport [this=%p]\n", this)); + MOZ_ASSERT(mFirstODASource == ODA_FROM_SOCKET); + MOZ_ASSERT(gSocketTransportService); + MOZ_ASSERT(gSocketTransportService->IsOnCurrentThreadInfallible()); + + if (IsWaitingOnStartRequest()) { + LOG((" > pending until OnStartRequest\n")); + + RefPtr self = this; + + nsCOMPtr task = NS_NewRunnableFunction( + "HttpBackgroundChannelChild::RecvOnConsoleReport", + [self, consoleReports = + CopyableTArray{std::move(aConsoleReports)}]() mutable { + self->RecvOnConsoleReport(std::move(consoleReports)); + }); + + mQueuedRunnables.AppendElement(task.forget()); + return IPC_OK(); + } + + if (mOnStopRequestCalled) { + mChannelChild->ProcessOnConsoleReport(std::move(aConsoleReports)); + } else { + RefPtr self = this; + mConsoleReportTask = [self, consoleReports = CopyableTArray{ + std::move(aConsoleReports)}]() mutable { + self->mChannelChild->ProcessOnConsoleReport(std::move(consoleReports)); + }; + } + + return IPC_OK(); +} + +IPCResult HttpBackgroundChannelChild::RecvNotifyClassificationFlags( + const uint32_t& aClassificationFlags, const bool& aIsThirdParty) { + LOG( + ("HttpBackgroundChannelChild::RecvNotifyClassificationFlags " + "classificationFlags=%" PRIu32 ", thirdparty=%d [this=%p]\n", + aClassificationFlags, static_cast(aIsThirdParty), this)); + MOZ_ASSERT(OnSocketThread()); + + if (NS_WARN_IF(!mChannelChild)) { + return IPC_OK(); + } + + // NotifyClassificationFlags has no order dependency to OnStartRequest. + // It this be handled as soon as possible + mChannelChild->ProcessNotifyClassificationFlags(aClassificationFlags, + aIsThirdParty); + + return IPC_OK(); +} + +IPCResult HttpBackgroundChannelChild::RecvNotifyFlashPluginStateChanged( + const nsIHttpChannel::FlashPluginState& aState) { + LOG( + ("HttpBackgroundChannelChild::RecvNotifyFlashPluginStateChanged " + "[this=%p]\n", + this)); + MOZ_ASSERT(OnSocketThread()); + + if (NS_WARN_IF(!mChannelChild)) { + return IPC_OK(); + } + + // NotifyFlashPluginStateChanged has no order dependency to OnStartRequest. + // It this be handled as soon as possible + mChannelChild->ProcessNotifyFlashPluginStateChanged(aState); + + return IPC_OK(); +} + +IPCResult HttpBackgroundChannelChild::RecvSetClassifierMatchedInfo( + const ClassifierInfo& info) { + LOG(("HttpBackgroundChannelChild::RecvSetClassifierMatchedInfo [this=%p]\n", + this)); + MOZ_ASSERT(OnSocketThread()); + + if (NS_WARN_IF(!mChannelChild)) { + return IPC_OK(); + } + + // SetClassifierMatchedInfo has no order dependency to OnStartRequest. + // It this be handled as soon as possible + mChannelChild->ProcessSetClassifierMatchedInfo(info.list(), info.provider(), + info.fullhash()); + + return IPC_OK(); +} + +IPCResult HttpBackgroundChannelChild::RecvSetClassifierMatchedTrackingInfo( + const ClassifierInfo& info) { + LOG( + ("HttpBackgroundChannelChild::RecvSetClassifierMatchedTrackingInfo " + "[this=%p]\n", + this)); + MOZ_ASSERT(OnSocketThread()); + + if (NS_WARN_IF(!mChannelChild)) { + return IPC_OK(); + } + + // SetClassifierMatchedTrackingInfo has no order dependency to + // OnStartRequest. It this be handled as soon as possible + mChannelChild->ProcessSetClassifierMatchedTrackingInfo(info.list(), + info.fullhash()); + + return IPC_OK(); +} + +IPCResult HttpBackgroundChannelChild::RecvAttachStreamFilter( + Endpoint&& aEndpoint) { + LOG(("HttpBackgroundChannelChild::RecvAttachStreamFilter [this=%p]\n", this)); + MOZ_ASSERT(OnSocketThread()); + + if (NS_WARN_IF(!mChannelChild)) { + return IPC_OK(); + } + + mChannelChild->ProcessAttachStreamFilter(std::move(aEndpoint)); + return IPC_OK(); +} + +void HttpBackgroundChannelChild::ActorDestroy(ActorDestroyReason aWhy) { + LOG(("HttpBackgroundChannelChild::ActorDestroy[this=%p]\n", this)); + // This function might be called during shutdown phase, so OnSocketThread() + // might return false even on STS thread. Use IsOnCurrentThreadInfallible() + // to get correct information. + MOZ_ASSERT(gSocketTransportService); + MOZ_ASSERT(gSocketTransportService->IsOnCurrentThreadInfallible()); + + // Ensure all IPC messages received before ActorDestroy can be + // handled correctly. If there is any pending IPC message, destroyed + // mChannelChild until those messages are flushed. + // If background channel is not closed by normal IPDL actor deletion, + // remove the HttpChannelChild reference and notify background channel + // destroyed immediately. + if (aWhy == Deletion && !mQueuedRunnables.IsEmpty()) { + LOG((" > pending until queued messages are flushed\n")); + RefPtr self = this; + mQueuedRunnables.AppendElement(NS_NewRunnableFunction( + "HttpBackgroundChannelChild::ActorDestroy", [self]() { + MOZ_ASSERT(OnSocketThread()); + RefPtr channelChild = + std::move(self->mChannelChild); + + if (channelChild) { + channelChild->OnBackgroundChildDestroyed(self); + } + })); + return; + } + + if (mChannelChild) { + RefPtr channelChild = std::move(mChannelChild); + + channelChild->OnBackgroundChildDestroyed(this); + } +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/HttpBackgroundChannelChild.h b/netwerk/protocol/http/HttpBackgroundChannelChild.h new file mode 100644 index 0000000000..af9fba57f8 --- /dev/null +++ b/netwerk/protocol/http/HttpBackgroundChannelChild.h @@ -0,0 +1,152 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et 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/. */ + +#ifndef mozilla_net_HttpBackgroundChannelChild_h +#define mozilla_net_HttpBackgroundChannelChild_h + +#include "mozilla/net/PHttpBackgroundChannelChild.h" +#include "nsIRunnable.h" +#include "nsTArray.h" + +using mozilla::ipc::IPCResult; + +namespace mozilla { +namespace net { + +class BackgroundDataBridgeChild; +class HttpChannelChild; + +class HttpBackgroundChannelChild final : public PHttpBackgroundChannelChild { + friend class BackgroundChannelCreateCallback; + friend class PHttpBackgroundChannelChild; + friend class HttpChannelChild; + friend class BackgroundDataBridgeChild; + + public: + explicit HttpBackgroundChannelChild(); + + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(HttpBackgroundChannelChild, final) + + // Associate this background channel with a HttpChannelChild and + // initiate the createion of the PBackground IPC channel. + nsresult Init(HttpChannelChild* aChannelChild); + + // Callback while the associated HttpChannelChild is not going to + // handle any incoming messages over background channel. + void OnChannelClosed(); + + // Return true if OnChannelClosed has been called. + bool ChannelClosed(); + + // Callback when OnStartRequest is received and handled by HttpChannelChild. + // Enqueued messages in background channel will be flushed. + void OnStartRequestReceived(Maybe aMultiPartID); + +#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED + bool IsQueueEmpty() const { return mQueuedRunnables.IsEmpty(); } +#endif + + protected: + IPCResult RecvOnStartRequest(const nsHttpResponseHead& aResponseHead, + const bool& aUseResponseHead, + const nsHttpHeaderArray& aRequestHeaders, + const HttpChannelOnStartRequestArgs& aArgs); + + IPCResult RecvOnTransportAndData(const nsresult& aChannelStatus, + const nsresult& aTransportStatus, + const uint64_t& aOffset, + const uint32_t& aCount, + const nsDependentCSubstring& aData, + const bool& aDataFromSocketProcess); + + IPCResult RecvOnStopRequest( + const nsresult& aChannelStatus, const ResourceTimingStructArgs& aTiming, + const TimeStamp& aLastActiveTabOptHit, + const nsHttpHeaderArray& aResponseTrailers, + nsTArray&& aConsoleReports, + const bool& aFromSocketProcess); + + IPCResult RecvOnConsoleReport( + nsTArray&& aConsoleReports); + + IPCResult RecvOnAfterLastPart(const nsresult& aStatus); + + IPCResult RecvOnProgress(const int64_t& aProgress, + const int64_t& aProgressMax); + + IPCResult RecvOnStatus(const nsresult& aStatus); + + IPCResult RecvNotifyClassificationFlags(const uint32_t& aClassificationFlags, + const bool& aIsThirdParty); + + IPCResult RecvNotifyFlashPluginStateChanged( + const nsIHttpChannel::FlashPluginState& aState); + + IPCResult RecvSetClassifierMatchedInfo(const ClassifierInfo& info); + + IPCResult RecvSetClassifierMatchedTrackingInfo(const ClassifierInfo& info); + + IPCResult RecvAttachStreamFilter( + Endpoint&& aEndpoint); + + void ActorDestroy(ActorDestroyReason aWhy) override; + + void CreateDataBridge(); + + private: + virtual ~HttpBackgroundChannelChild(); + + // Initiate the creation of the PBckground IPC channel. + // Return false if failed. + bool CreateBackgroundChannel(); + + // Check OnStartRequest is sent by parent process over main thread IPC + // but not yet received on child process. + // return true before RecvOnStartRequestSent is invoked. + // return false if RecvOnStartRequestSent is invoked but not + // OnStartRequestReceived. + // return true after both RecvOnStartRequestSend and OnStartRequestReceived + // are invoked. + bool IsWaitingOnStartRequest(); + + // Associated HttpChannelChild for handling the channel events. + // Will be removed while failed to create background channel, + // destruction of the background channel, or explicitly dissociation + // via OnChannelClosed callback. + RefPtr mChannelChild; + + // True if OnStartRequest is received by HttpChannelChild. + // Should only access on STS thread. + bool mStartReceived = false; + + // Store pending messages that require to be handled after OnStartRequest. + // Should be flushed after OnStartRequest is received and handled. + // Should only access on STS thread. + nsTArray> mQueuedRunnables; + + enum ODASource { + ODA_PENDING = 0, // ODA is pending + ODA_FROM_PARENT = 1, // ODA from parent process. + ODA_FROM_SOCKET = 2 // response coming from the network + }; + // We need to know the first ODA will be from socket process or parent + // process. This information is from OnStartRequest message from parent + // process. + ODASource mFirstODASource; + + // Indicate whether HttpChannelChild::ProcessOnStopRequest is called. + bool mOnStopRequestCalled; + + // This is used when we receive the console report from parent process, but + // still not get the OnStopRequest from socket process. + std::function mConsoleReportTask; +}; + +} // namespace net +} // namespace mozilla + +#endif // mozilla_net_HttpBackgroundChannelChild_h diff --git a/netwerk/protocol/http/HttpBackgroundChannelParent.cpp b/netwerk/protocol/http/HttpBackgroundChannelParent.cpp new file mode 100644 index 0000000000..3c25e8454e --- /dev/null +++ b/netwerk/protocol/http/HttpBackgroundChannelParent.cpp @@ -0,0 +1,519 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et 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/. */ + +// HttpLog.h should generally be included first +#include "HttpLog.h" + +#include "HttpBackgroundChannelParent.h" + +#include "HttpChannelParent.h" +#include "mozilla/ipc/BackgroundParent.h" +#include "mozilla/IntegerPrintfMacros.h" +#include "mozilla/Unused.h" +#include "mozilla/net/BackgroundChannelRegistrar.h" +#include "nsNetCID.h" +#include "nsQueryObject.h" +#include "nsThreadUtils.h" + +using mozilla::dom::ContentParent; +using mozilla::ipc::AssertIsInMainProcess; +using mozilla::ipc::AssertIsOnBackgroundThread; +using mozilla::ipc::BackgroundParent; +using mozilla::ipc::IPCResult; +using mozilla::ipc::IsOnBackgroundThread; + +namespace mozilla { +namespace net { + +/* + * Helper class for continuing the AsyncOpen procedure on main thread. + */ +class ContinueAsyncOpenRunnable final : public Runnable { + public: + ContinueAsyncOpenRunnable(HttpBackgroundChannelParent* aActor, + const uint64_t& aChannelId) + : Runnable("net::ContinueAsyncOpenRunnable"), + mActor(aActor), + mChannelId(aChannelId) { + AssertIsInMainProcess(); + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mActor); + } + + NS_IMETHOD Run() override { + LOG( + ("HttpBackgroundChannelParent::ContinueAsyncOpen [this=%p " + "channelId=%" PRIu64 "]\n", + mActor.get(), mChannelId)); + AssertIsInMainProcess(); + MOZ_ASSERT(NS_IsMainThread()); + + nsCOMPtr registrar = + BackgroundChannelRegistrar::GetOrCreate(); + MOZ_ASSERT(registrar); + + registrar->LinkBackgroundChannel(mChannelId, mActor); + return NS_OK; + } + + private: + RefPtr mActor; + const uint64_t mChannelId; +}; + +HttpBackgroundChannelParent::HttpBackgroundChannelParent() + : mIPCOpened(true), + mBgThreadMutex("HttpBackgroundChannelParent::BgThreadMutex") { + AssertIsInMainProcess(); + AssertIsOnBackgroundThread(); + + { + MutexAutoLock lock(mBgThreadMutex); + mBackgroundThread = NS_GetCurrentThread(); + } +} + +HttpBackgroundChannelParent::~HttpBackgroundChannelParent() { + MOZ_ASSERT(NS_IsMainThread() || IsOnBackgroundThread()); + MOZ_ASSERT(!mIPCOpened); +} + +nsresult HttpBackgroundChannelParent::Init(const uint64_t& aChannelId) { + LOG(("HttpBackgroundChannelParent::Init [this=%p channelId=%" PRIu64 "]\n", + this, aChannelId)); + AssertIsInMainProcess(); + AssertIsOnBackgroundThread(); + + RefPtr runnable = + new ContinueAsyncOpenRunnable(this, aChannelId); + + return NS_DispatchToMainThread(runnable); +} + +void HttpBackgroundChannelParent::LinkToChannel( + HttpChannelParent* aChannelParent) { + LOG(("HttpBackgroundChannelParent::LinkToChannel [this=%p channel=%p]\n", + this, aChannelParent)); + AssertIsInMainProcess(); + MOZ_ASSERT(NS_IsMainThread()); + + if (!mIPCOpened) { + return; + } + + mChannelParent = aChannelParent; +} + +void HttpBackgroundChannelParent::OnChannelClosed() { + LOG(("HttpBackgroundChannelParent::OnChannelClosed [this=%p]\n", this)); + AssertIsInMainProcess(); + MOZ_ASSERT(NS_IsMainThread()); + + if (!mIPCOpened) { + return; + } + + nsresult rv; + + { + MutexAutoLock lock(mBgThreadMutex); + RefPtr self = this; + rv = mBackgroundThread->Dispatch( + NS_NewRunnableFunction( + "net::HttpBackgroundChannelParent::OnChannelClosed", + [self]() { + LOG(("HttpBackgroundChannelParent::DeleteRunnable [this=%p]\n", + self.get())); + AssertIsOnBackgroundThread(); + + if (!self->mIPCOpened.compareExchange(true, false)) { + return; + } + + Unused << self->Send__delete__(self); + }), + NS_DISPATCH_NORMAL); + } + + Unused << NS_WARN_IF(NS_FAILED(rv)); +} + +bool HttpBackgroundChannelParent::OnStartRequest( + const nsHttpResponseHead& aResponseHead, const bool& aUseResponseHead, + const nsHttpHeaderArray& aRequestHeaders, + const HttpChannelOnStartRequestArgs& aArgs) { + LOG(("HttpBackgroundChannelParent::OnStartRequest [this=%p]\n", this)); + AssertIsInMainProcess(); + + if (NS_WARN_IF(!mIPCOpened)) { + return false; + } + + if (!IsOnBackgroundThread()) { + MutexAutoLock lock(mBgThreadMutex); + nsresult rv = mBackgroundThread->Dispatch( + NewRunnableMethod( + "net::HttpBackgroundChannelParent::OnStartRequest", this, + &HttpBackgroundChannelParent::OnStartRequest, aResponseHead, + aUseResponseHead, aRequestHeaders, aArgs), + NS_DISPATCH_NORMAL); + + MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv)); + + return NS_SUCCEEDED(rv); + } + + return SendOnStartRequest(aResponseHead, aUseResponseHead, aRequestHeaders, + aArgs); +} + +bool HttpBackgroundChannelParent::OnTransportAndData( + const nsresult& aChannelStatus, const nsresult& aTransportStatus, + const uint64_t& aOffset, const uint32_t& aCount, const nsCString& aData) { + LOG(("HttpBackgroundChannelParent::OnTransportAndData [this=%p]\n", this)); + AssertIsInMainProcess(); + + if (NS_WARN_IF(!mIPCOpened)) { + return false; + } + + if (!IsOnBackgroundThread()) { + MutexAutoLock lock(mBgThreadMutex); + nsresult rv = mBackgroundThread->Dispatch( + NewRunnableMethod( + "net::HttpBackgroundChannelParent::OnTransportAndData", this, + &HttpBackgroundChannelParent::OnTransportAndData, aChannelStatus, + aTransportStatus, aOffset, aCount, aData), + NS_DISPATCH_NORMAL); + + MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv)); + + return NS_SUCCEEDED(rv); + } + + nsHttp::SendFunc sendFunc = + [self = UnsafePtr(this), aChannelStatus, + aTransportStatus](const nsDependentCSubstring& aData, uint64_t aOffset, + uint32_t aCount) { + return self->SendOnTransportAndData(aChannelStatus, aTransportStatus, + aOffset, aCount, aData, false); + }; + + return nsHttp::SendDataInChunks(aData, aOffset, aCount, sendFunc); +} + +bool HttpBackgroundChannelParent::OnStopRequest( + const nsresult& aChannelStatus, const ResourceTimingStructArgs& aTiming, + const nsHttpHeaderArray& aResponseTrailers, + const nsTArray& aConsoleReports) { + LOG( + ("HttpBackgroundChannelParent::OnStopRequest [this=%p " + "status=%" PRIx32 "]\n", + this, static_cast(aChannelStatus))); + AssertIsInMainProcess(); + + if (NS_WARN_IF(!mIPCOpened)) { + return false; + } + + if (!IsOnBackgroundThread()) { + MutexAutoLock lock(mBgThreadMutex); + nsresult rv = mBackgroundThread->Dispatch( + NewRunnableMethod>( + "net::HttpBackgroundChannelParent::OnStopRequest", this, + &HttpBackgroundChannelParent::OnStopRequest, aChannelStatus, + aTiming, aResponseTrailers, aConsoleReports), + NS_DISPATCH_NORMAL); + + MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv)); + + return NS_SUCCEEDED(rv); + } + + // See the child code for why we do this. + TimeStamp lastActTabOpt = nsHttp::GetLastActiveTabLoadOptimizationHit(); + + return SendOnStopRequest(aChannelStatus, aTiming, lastActTabOpt, + aResponseTrailers, aConsoleReports, false); +} + +bool HttpBackgroundChannelParent::OnConsoleReport( + const nsTArray& aConsoleReports) { + LOG(("HttpBackgroundChannelParent::OnConsoleReport [this=%p]", this)); + AssertIsInMainProcess(); + + if (NS_WARN_IF(!mIPCOpened)) { + return false; + } + + if (!IsOnBackgroundThread()) { + MutexAutoLock lock(mBgThreadMutex); + nsresult rv = mBackgroundThread->Dispatch( + NewRunnableMethod>( + "net::HttpBackgroundChannelParent::OnConsoleReport", this, + &HttpBackgroundChannelParent::OnConsoleReport, aConsoleReports), + NS_DISPATCH_NORMAL); + + MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv)); + + return NS_SUCCEEDED(rv); + } + + return SendOnConsoleReport(aConsoleReports); +} + +bool HttpBackgroundChannelParent::OnAfterLastPart(const nsresult aStatus) { + LOG(("HttpBackgroundChannelParent::OnAfterLastPart [this=%p]\n", this)); + AssertIsInMainProcess(); + + if (NS_WARN_IF(!mIPCOpened)) { + return false; + } + + if (!IsOnBackgroundThread()) { + MutexAutoLock lock(mBgThreadMutex); + nsresult rv = mBackgroundThread->Dispatch( + NewRunnableMethod( + "net::HttpBackgroundChannelParent::OnAfterLastPart", this, + &HttpBackgroundChannelParent::OnAfterLastPart, aStatus), + NS_DISPATCH_NORMAL); + + MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv)); + + return NS_SUCCEEDED(rv); + } + + return SendOnAfterLastPart(aStatus); +} + +bool HttpBackgroundChannelParent::OnProgress(const int64_t aProgress, + const int64_t aProgressMax) { + LOG(("HttpBackgroundChannelParent::OnProgress [this=%p]\n", this)); + AssertIsInMainProcess(); + + if (NS_WARN_IF(!mIPCOpened)) { + return false; + } + + if (!IsOnBackgroundThread()) { + MutexAutoLock lock(mBgThreadMutex); + nsresult rv = mBackgroundThread->Dispatch( + NewRunnableMethod( + "net::HttpBackgroundChannelParent::OnProgress", this, + &HttpBackgroundChannelParent::OnProgress, aProgress, aProgressMax), + NS_DISPATCH_NORMAL); + + MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv)); + + return NS_SUCCEEDED(rv); + } + + return SendOnProgress(aProgress, aProgressMax); +} + +bool HttpBackgroundChannelParent::OnStatus(const nsresult aStatus) { + LOG(("HttpBackgroundChannelParent::OnStatus [this=%p]\n", this)); + AssertIsInMainProcess(); + + if (NS_WARN_IF(!mIPCOpened)) { + return false; + } + + if (!IsOnBackgroundThread()) { + MutexAutoLock lock(mBgThreadMutex); + nsresult rv = mBackgroundThread->Dispatch( + NewRunnableMethod( + "net::HttpBackgroundChannelParent::OnStatus", this, + &HttpBackgroundChannelParent::OnStatus, aStatus), + NS_DISPATCH_NORMAL); + + MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv)); + + return NS_SUCCEEDED(rv); + } + + return SendOnStatus(aStatus); +} + +bool HttpBackgroundChannelParent::OnNotifyClassificationFlags( + uint32_t aClassificationFlags, bool aIsThirdParty) { + LOG( + ("HttpBackgroundChannelParent::OnNotifyClassificationFlags " + "classificationFlags=%" PRIu32 ", thirdparty=%d [this=%p]\n", + aClassificationFlags, static_cast(aIsThirdParty), this)); + AssertIsInMainProcess(); + + if (NS_WARN_IF(!mIPCOpened)) { + return false; + } + + if (!IsOnBackgroundThread()) { + MutexAutoLock lock(mBgThreadMutex); + nsresult rv = mBackgroundThread->Dispatch( + NewRunnableMethod( + "net::HttpBackgroundChannelParent::OnNotifyClassificationFlags", + this, &HttpBackgroundChannelParent::OnNotifyClassificationFlags, + aClassificationFlags, aIsThirdParty), + NS_DISPATCH_NORMAL); + + MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv)); + + return NS_SUCCEEDED(rv); + } + + return SendNotifyClassificationFlags(aClassificationFlags, aIsThirdParty); +} + +bool HttpBackgroundChannelParent::OnNotifyFlashPluginStateChanged( + nsIHttpChannel::FlashPluginState aState) { + LOG( + ("HttpBackgroundChannelParent::OnNotifyFlashPluginStateChanged " + "[this=%p]\n", + this)); + AssertIsInMainProcess(); + + if (NS_WARN_IF(!mIPCOpened)) { + return false; + } + + if (!IsOnBackgroundThread()) { + MutexAutoLock lock(mBgThreadMutex); + RefPtr self = this; + nsresult rv = mBackgroundThread->Dispatch( + NS_NewRunnableFunction( + "net::HttpBackgroundChannelParent::OnNotifyFlashPluginStateChanged", + [self, aState]() { + self->OnNotifyFlashPluginStateChanged(aState); + }), + NS_DISPATCH_NORMAL); + + MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv)); + + return NS_SUCCEEDED(rv); + } + + return SendNotifyFlashPluginStateChanged(aState); +} + +bool HttpBackgroundChannelParent::OnSetClassifierMatchedInfo( + const nsACString& aList, const nsACString& aProvider, + const nsACString& aFullHash) { + LOG(("HttpBackgroundChannelParent::OnSetClassifierMatchedInfo [this=%p]\n", + this)); + AssertIsInMainProcess(); + + if (NS_WARN_IF(!mIPCOpened)) { + return false; + } + + if (!IsOnBackgroundThread()) { + MutexAutoLock lock(mBgThreadMutex); + nsresult rv = mBackgroundThread->Dispatch( + NewRunnableMethod( + "net::HttpBackgroundChannelParent::OnSetClassifierMatchedInfo", + this, &HttpBackgroundChannelParent::OnSetClassifierMatchedInfo, + aList, aProvider, aFullHash), + NS_DISPATCH_NORMAL); + + MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv)); + + return NS_SUCCEEDED(rv); + } + + ClassifierInfo info; + info.list() = aList; + info.fullhash() = aFullHash; + info.provider() = aProvider; + + return SendSetClassifierMatchedInfo(info); +} + +bool HttpBackgroundChannelParent::OnSetClassifierMatchedTrackingInfo( + const nsACString& aLists, const nsACString& aFullHashes) { + LOG( + ("HttpBackgroundChannelParent::OnSetClassifierMatchedTrackingInfo " + "[this=%p]\n", + this)); + AssertIsInMainProcess(); + + if (NS_WARN_IF(!mIPCOpened)) { + return false; + } + + if (!IsOnBackgroundThread()) { + MutexAutoLock lock(mBgThreadMutex); + nsresult rv = mBackgroundThread->Dispatch( + NewRunnableMethod( + "net::HttpBackgroundChannelParent::" + "OnSetClassifierMatchedTrackingInfo", + this, + &HttpBackgroundChannelParent::OnSetClassifierMatchedTrackingInfo, + aLists, aFullHashes), + NS_DISPATCH_NORMAL); + + MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv)); + + return NS_SUCCEEDED(rv); + } + + ClassifierInfo info; + info.list() = aLists; + info.fullhash() = aFullHashes; + + return SendSetClassifierMatchedTrackingInfo(info); +} + +nsISerialEventTarget* HttpBackgroundChannelParent::GetBackgroundTarget() { + MOZ_ASSERT(mBackgroundThread); + return mBackgroundThread.get(); +} + +auto HttpBackgroundChannelParent::AttachStreamFilter( + Endpoint&& aParentEndpoint, + Endpoint&& aChildEndpoint) + -> RefPtr { + LOG(("HttpBackgroundChannelParent::AttachStreamFilter [this=%p]\n", this)); + MOZ_ASSERT(IsOnBackgroundThread()); + + if (NS_WARN_IF(!mIPCOpened) || + !SendAttachStreamFilter(std::move(aParentEndpoint))) { + return ChildEndpointPromise::CreateAndReject(false, __func__); + } + + return ChildEndpointPromise::CreateAndResolve(std::move(aChildEndpoint), + __func__); +} + +void HttpBackgroundChannelParent::ActorDestroy(ActorDestroyReason aWhy) { + LOG(("HttpBackgroundChannelParent::ActorDestroy [this=%p]\n", this)); + AssertIsInMainProcess(); + AssertIsOnBackgroundThread(); + + mIPCOpened = false; + + RefPtr self = this; + DebugOnly rv = NS_DispatchToMainThread(NS_NewRunnableFunction( + "net::HttpBackgroundChannelParent::ActorDestroy", [self]() { + MOZ_ASSERT(NS_IsMainThread()); + + RefPtr channelParent = + std::move(self->mChannelParent); + + if (channelParent) { + channelParent->OnBackgroundParentDestroyed(); + } + })); + MOZ_ASSERT(NS_SUCCEEDED(rv)); +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/HttpBackgroundChannelParent.h b/netwerk/protocol/http/HttpBackgroundChannelParent.h new file mode 100644 index 0000000000..c2d25f1df6 --- /dev/null +++ b/netwerk/protocol/http/HttpBackgroundChannelParent.h @@ -0,0 +1,121 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et 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/. */ + +#ifndef mozilla_net_HttpBackgroundChannelParent_h +#define mozilla_net_HttpBackgroundChannelParent_h + +#include "mozilla/net/PHttpBackgroundChannelParent.h" +#include "mozilla/Atomics.h" +#include "mozilla/Mutex.h" +#include "nsID.h" +#include "nsISupportsImpl.h" + +class nsISerialEventTarget; + +namespace mozilla { +namespace net { + +class HttpChannelParent; + +class HttpBackgroundChannelParent final : public PHttpBackgroundChannelParent { + public: + explicit HttpBackgroundChannelParent(); + + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(HttpBackgroundChannelParent, final) + + // Try to find associated HttpChannelParent with the same + // channel Id. + nsresult Init(const uint64_t& aChannelId); + + // Callbacks for BackgroundChannelRegistrar to notify + // the associated HttpChannelParent is found. + void LinkToChannel(HttpChannelParent* aChannelParent); + + // Callbacks for HttpChannelParent to close the background + // IPC channel. + void OnChannelClosed(); + + // To send OnStartRequest message over background channel. + bool OnStartRequest(const nsHttpResponseHead& aResponseHead, + const bool& aUseResponseHead, + const nsHttpHeaderArray& aRequestHeaders, + const HttpChannelOnStartRequestArgs& aArgs); + + // To send OnTransportAndData message over background channel. + bool OnTransportAndData(const nsresult& aChannelStatus, + const nsresult& aTransportStatus, + const uint64_t& aOffset, const uint32_t& aCount, + const nsCString& aData); + + // To send OnStopRequest message over background channel. + bool OnStopRequest(const nsresult& aChannelStatus, + const ResourceTimingStructArgs& aTiming, + const nsHttpHeaderArray& aResponseTrailers, + const nsTArray& aConsoleReports); + + // When ODA and OnStopRequest are sending from socket process to child + // process, this is the last IPC message sent from parent process. + bool OnConsoleReport(const nsTArray& aConsoleReports); + + // To send OnAfterLastPart message over background channel. + bool OnAfterLastPart(const nsresult aStatus); + + // To send OnProgress message over background channel. + bool OnProgress(const int64_t aProgress, const int64_t aProgressMax); + + // To send OnStatus message over background channel. + bool OnStatus(const nsresult aStatus); + + // To send FlushedForDiversion and DivertMessages messages + // over background channel. + bool OnDiversion(); + + // To send NotifyClassificationFlags message over background channel. + bool OnNotifyClassificationFlags(uint32_t aClassificationFlags, + bool aIsThirdParty); + + // To send NotifyFlashPluginStateChanged message over background channel. + bool OnNotifyFlashPluginStateChanged(nsIHttpChannel::FlashPluginState aState); + + // To send SetClassifierMatchedInfo message over background channel. + bool OnSetClassifierMatchedInfo(const nsACString& aList, + const nsACString& aProvider, + const nsACString& aFullHash); + + // To send SetClassifierMatchedTrackingInfo message over background channel. + bool OnSetClassifierMatchedTrackingInfo(const nsACString& aLists, + const nsACString& aFullHashes); + + nsISerialEventTarget* GetBackgroundTarget(); + + using ChildEndpointPromise = + MozPromise, bool, true>; + [[nodiscard]] RefPtr AttachStreamFilter( + Endpoint&& aParentEndpoint, + Endpoint&& aChildEndpoint); + + protected: + void ActorDestroy(ActorDestroyReason aWhy) override; + + private: + virtual ~HttpBackgroundChannelParent(); + + Atomic mIPCOpened; + + // Used to ensure atomicity of mBackgroundThread + Mutex mBgThreadMutex; + + nsCOMPtr mBackgroundThread; + + // associated HttpChannelParent for generating the channel events + RefPtr mChannelParent; +}; + +} // namespace net +} // namespace mozilla + +#endif // mozilla_net_HttpBackgroundChannelParent_h diff --git a/netwerk/protocol/http/HttpBaseChannel.cpp b/netwerk/protocol/http/HttpBaseChannel.cpp new file mode 100644 index 0000000000..7193953ad1 --- /dev/null +++ b/netwerk/protocol/http/HttpBaseChannel.cpp @@ -0,0 +1,5330 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et 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/. */ + +// HttpLog.h should generally be included first +#include "mozilla/net/HttpBaseChannel.h" + +#include +#include + +#include "HttpBaseChannel.h" +#include "HttpLog.h" +#include "LoadInfo.h" +#include "ReferrerInfo.h" +#include "mozIThirdPartyUtil.h" +#include "mozilla/AntiTrackingUtils.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/BinarySearch.h" +#include "mozilla/ConsoleReportCollector.h" +#include "mozilla/DebugOnly.h" +#include "mozilla/InputStreamLengthHelper.h" +#include "mozilla/NullPrincipal.h" +#include "mozilla/Services.h" +#include "mozilla/StaticPrefs_browser.h" +#include "mozilla/StaticPrefs_security.h" +#include "mozilla/Telemetry.h" +#include "mozilla/Tokenizer.h" +#include "mozilla/dom/BrowsingContext.h" +#include "mozilla/dom/CanonicalBrowsingContext.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/Performance.h" +#include "mozilla/dom/PerformanceStorage.h" +#include "mozilla/dom/WindowGlobalParent.h" +#include "mozilla/net/PartiallySeekableInputStream.h" +#include "mozilla/net/UrlClassifierCommon.h" +#include "mozilla/net/UrlClassifierFeatureFactory.h" +#include "nsCRT.h" +#include "nsContentSecurityManager.h" +#include "nsContentSecurityUtils.h" +#include "nsContentUtils.h" +#include "nsEscape.h" +#include "nsGlobalWindowOuter.h" +#include "nsHttpChannel.h" +#include "nsHttpHandler.h" +#include "nsIApplicationCacheChannel.h" +#include "nsICacheInfoChannel.h" +#include "nsICachingChannel.h" +#include "nsIChannelEventSink.h" +#include "nsIConsoleService.h" +#include "nsIContentPolicy.h" +#include "nsICookieService.h" +#include "nsIDOMWindowUtils.h" +#include "nsIDocShell.h" +#include "nsIDNSService.h" +#include "nsIEncodedChannel.h" +#include "nsIHttpHeaderVisitor.h" +#include "nsILoadGroupChild.h" +#include "nsIMIMEInputStream.h" +#include "nsIMutableArray.h" +#include "nsINetworkInterceptController.h" +#include "nsIObserverService.h" +#include "nsIPrincipal.h" +#include "nsIProtocolProxyService.h" +#include "nsIScriptError.h" +#include "nsIScriptSecurityManager.h" +#include "nsISecurityConsoleMessage.h" +#include "nsISeekableStream.h" +#include "nsIStorageStream.h" +#include "nsIStreamConverterService.h" +#include "nsITimedChannel.h" +#include "nsITransportSecurityInfo.h" +#include "nsIURIMutator.h" +#include "nsMimeTypes.h" +#include "nsNetCID.h" +#include "nsNetUtil.h" +#include "nsPIDOMWindow.h" +#include "nsProxyRelease.h" +#include "nsReadableUtils.h" +#include "nsRedirectHistoryEntry.h" +#include "nsServerTiming.h" +#include "nsStreamListenerWrapper.h" +#include "nsStreamUtils.h" +#include "nsThreadUtils.h" +#include "nsURLHelper.h" +#include "mozilla/RemoteLazyInputStreamChild.h" +#include "mozilla/RemoteLazyInputStreamUtils.h" +#include "mozilla/net/SFVService.h" +#include "mozilla/dom/ContentChild.h" + +namespace mozilla { +namespace net { + +static bool IsHeaderBlacklistedForRedirectCopy(nsHttpAtom const& aHeader) { + // IMPORTANT: keep this list ASCII-code sorted + static nsHttpAtom const* blackList[] = {&nsHttp::Accept, + &nsHttp::Accept_Encoding, + &nsHttp::Accept_Language, + &nsHttp::Authentication, + &nsHttp::Authorization, + &nsHttp::Connection, + &nsHttp::Content_Length, + &nsHttp::Cookie, + &nsHttp::Host, + &nsHttp::If, + &nsHttp::If_Match, + &nsHttp::If_Modified_Since, + &nsHttp::If_None_Match, + &nsHttp::If_None_Match_Any, + &nsHttp::If_Range, + &nsHttp::If_Unmodified_Since, + &nsHttp::Proxy_Authenticate, + &nsHttp::Proxy_Authorization, + &nsHttp::Range, + &nsHttp::TE, + &nsHttp::Transfer_Encoding, + &nsHttp::Upgrade, + &nsHttp::User_Agent, + &nsHttp::WWW_Authenticate}; + + class HttpAtomComparator { + nsHttpAtom const& mTarget; + + public: + explicit HttpAtomComparator(nsHttpAtom const& aTarget) : mTarget(aTarget) {} + int operator()(nsHttpAtom const* aVal) const { + if (mTarget == *aVal) { + return 0; + } + return strcmp(mTarget._val, aVal->_val); + } + }; + + size_t unused; + return BinarySearchIf(blackList, 0, ArrayLength(blackList), + HttpAtomComparator(aHeader), &unused); +} + +class AddHeadersToChannelVisitor final : public nsIHttpHeaderVisitor { + public: + NS_DECL_ISUPPORTS + + explicit AddHeadersToChannelVisitor(nsIHttpChannel* aChannel) + : mChannel(aChannel) {} + + NS_IMETHOD VisitHeader(const nsACString& aHeader, + const nsACString& aValue) override { + nsHttpAtom atom = nsHttp::ResolveAtom(aHeader); + if (!IsHeaderBlacklistedForRedirectCopy(atom)) { + DebugOnly rv = + mChannel->SetRequestHeader(aHeader, aValue, false); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + return NS_OK; + } + + private: + ~AddHeadersToChannelVisitor() = default; + + nsCOMPtr mChannel; +}; + +NS_IMPL_ISUPPORTS(AddHeadersToChannelVisitor, nsIHttpHeaderVisitor) + +HttpBaseChannel::HttpBaseChannel() + : mReportCollector(new ConsoleReportCollector()), + mHttpHandler(gHttpHandler), + mChannelCreationTime(0), + mComputedCrossOriginOpenerPolicy(nsILoadInfo::OPENER_POLICY_UNSAFE_NONE), + mStartPos(UINT64_MAX), + mTransferSize(0), + mRequestSize(0), + mDecodedBodySize(0), + mSupportsHTTP3(false), + mEncodedBodySize(0), + mRequestContextID(0), + mContentWindowId(0), + mTopLevelOuterContentWindowId(0), + mAltDataLength(-1), + mChannelId(0), + mReqContentLength(0U), + mStatus(NS_OK), + mCanceled(false), + mFirstPartyClassificationFlags(0), + mThirdPartyClassificationFlags(0), + mFlashPluginState(nsIHttpChannel::FlashPluginUnknown), + mLoadFlags(LOAD_NORMAL), + mCaps(0), + mClassOfService(0), + mTlsFlags(0), + mSuspendCount(0), + mInitialRwin(0), + mProxyResolveFlags(0), + mContentDispositionHint(UINT32_MAX), + mCorsMode(nsIHttpChannelInternal::CORS_MODE_NO_CORS), + mRedirectMode(nsIHttpChannelInternal::REDIRECT_MODE_FOLLOW), + mLastRedirectFlags(0), + mPriority(PRIORITY_NORMAL), + mRedirectionLimit(gHttpHandler->RedirectionLimit()), + mRedirectCount(0), + mInternalRedirectCount(0) { + StoreApplyConversion(true); + StoreAllowSTS(true); + StoreInheritApplicationCache(true); + StoreTracingEnabled(true); + StoreReportTiming(true); + StoreAllowSpdy(true); + StoreAllowHttp3(true); + StoreAllowAltSvc(true); + StoreResponseTimeoutEnabled(true); + StoreAllRedirectsSameOrigin(true); + StoreAllRedirectsPassTimingAllowCheck(true); + StoreUpgradableToSecure(true); + + this->mSelfAddr.inet = {}; + this->mPeerAddr.inet = {}; + LOG(("Creating HttpBaseChannel @%p\n", this)); + + // Subfields of unions cannot be targeted in an initializer list. +#ifdef MOZ_VALGRIND + // Zero the entire unions so that Valgrind doesn't complain when we send them + // to another process. + memset(&mSelfAddr, 0, sizeof(NetAddr)); + memset(&mPeerAddr, 0, sizeof(NetAddr)); +#endif + mSelfAddr.raw.family = PR_AF_UNSPEC; + mPeerAddr.raw.family = PR_AF_UNSPEC; +} + +HttpBaseChannel::~HttpBaseChannel() { + LOG(("Destroying HttpBaseChannel @%p\n", this)); + + // Make sure we don't leak + CleanRedirectCacheChainIfNecessary(); + + ReleaseMainThreadOnlyReferences(); +} + +namespace { // anon + +class NonTailRemover : public nsISupports { + NS_DECL_THREADSAFE_ISUPPORTS + + explicit NonTailRemover(nsIRequestContext* rc) : mRequestContext(rc) {} + + private: + virtual ~NonTailRemover() { + MOZ_ASSERT(NS_IsMainThread()); + mRequestContext->RemoveNonTailRequest(); + } + + nsCOMPtr mRequestContext; +}; + +NS_IMPL_ISUPPORTS0(NonTailRemover) + +} // namespace + +void HttpBaseChannel::ReleaseMainThreadOnlyReferences() { + if (NS_IsMainThread()) { + // Already on main thread, let dtor to + // take care of releasing references + RemoveAsNonTailRequest(); + return; + } + + nsTArray> arrayToRelease; + arrayToRelease.AppendElement(mLoadGroup.forget()); + arrayToRelease.AppendElement(mLoadInfo.forget()); + arrayToRelease.AppendElement(mCallbacks.forget()); + arrayToRelease.AppendElement(mProgressSink.forget()); + arrayToRelease.AppendElement(mApplicationCache.forget()); + arrayToRelease.AppendElement(mPrincipal.forget()); + arrayToRelease.AppendElement(mListener.forget()); + arrayToRelease.AppendElement(mCompressListener.forget()); + + if (LoadAddedAsNonTailRequest()) { + // RemoveNonTailRequest() on our request context must be called on the main + // thread + MOZ_RELEASE_ASSERT(mRequestContext, + "Someone released rc or set flags w/o having it?"); + + nsCOMPtr nonTailRemover(new NonTailRemover(mRequestContext)); + arrayToRelease.AppendElement(nonTailRemover.forget()); + } + + NS_DispatchToMainThread(new ProxyReleaseRunnable(std::move(arrayToRelease))); +} + +void HttpBaseChannel::AddClassificationFlags(uint32_t aClassificationFlags, + bool aIsThirdParty) { + LOG( + ("HttpBaseChannel::AddClassificationFlags classificationFlags=%d " + "thirdparty=%d %p", + aClassificationFlags, static_cast(aIsThirdParty), this)); + + if (aIsThirdParty) { + mThirdPartyClassificationFlags |= aClassificationFlags; + } else { + mFirstPartyClassificationFlags |= aClassificationFlags; + } +} + +void HttpBaseChannel::SetFlashPluginState( + nsIHttpChannel::FlashPluginState aState) { + LOG(("HttpBaseChannel::SetFlashPluginState %p", this)); + mFlashPluginState = aState; +} + +nsresult HttpBaseChannel::Init(nsIURI* aURI, uint32_t aCaps, + nsProxyInfo* aProxyInfo, + uint32_t aProxyResolveFlags, nsIURI* aProxyURI, + uint64_t aChannelId, + ExtContentPolicyType aContentPolicyType) { + LOG1(("HttpBaseChannel::Init [this=%p]\n", this)); + + MOZ_ASSERT(aURI, "null uri"); + + mURI = aURI; + mOriginalURI = aURI; + mDocumentURI = nullptr; + mCaps = aCaps; + mProxyResolveFlags = aProxyResolveFlags; + mProxyURI = aProxyURI; + mChannelId = aChannelId; + + // Construct connection info object + nsAutoCString host; + int32_t port = -1; + bool isHTTPS = mURI->SchemeIs("https"); + + nsresult rv = mURI->GetAsciiHost(host); + if (NS_FAILED(rv)) return rv; + + // Reject the URL if it doesn't specify a host + if (host.IsEmpty()) return NS_ERROR_MALFORMED_URI; + + rv = mURI->GetPort(&port); + if (NS_FAILED(rv)) return rv; + + LOG1(("host=%s port=%d\n", host.get(), port)); + + rv = mURI->GetAsciiSpec(mSpec); + if (NS_FAILED(rv)) return rv; + LOG1(("uri=%s\n", mSpec.get())); + + // Assert default request method + MOZ_ASSERT(mRequestHead.EqualsMethod(nsHttpRequestHead::kMethod_Get)); + + // Set request headers + nsAutoCString hostLine; + rv = nsHttpHandler::GenerateHostPort(host, port, hostLine); + if (NS_FAILED(rv)) return rv; + + rv = mRequestHead.SetHeader(nsHttp::Host, hostLine); + if (NS_FAILED(rv)) return rv; + + rv = gHttpHandler->AddStandardRequestHeaders(&mRequestHead, isHTTPS, + aContentPolicyType); + if (NS_FAILED(rv)) return rv; + + nsAutoCString type; + if (aProxyInfo && NS_SUCCEEDED(aProxyInfo->GetType(type)) && + !type.EqualsLiteral("unknown")) + mProxyInfo = aProxyInfo; + + mCurrentThread = GetCurrentEventTarget(); + return rv; +} + +//----------------------------------------------------------------------------- +// HttpBaseChannel::nsISupports +//----------------------------------------------------------------------------- + +NS_IMPL_ADDREF(HttpBaseChannel) +NS_IMPL_RELEASE(HttpBaseChannel) + +NS_INTERFACE_MAP_BEGIN(HttpBaseChannel) + NS_INTERFACE_MAP_ENTRY(nsIRequest) + NS_INTERFACE_MAP_ENTRY(nsIChannel) + NS_INTERFACE_MAP_ENTRY(nsIIdentChannel) + NS_INTERFACE_MAP_ENTRY(nsIEncodedChannel) + NS_INTERFACE_MAP_ENTRY(nsIHttpChannel) + NS_INTERFACE_MAP_ENTRY(nsIHttpChannelInternal) + NS_INTERFACE_MAP_ENTRY(nsIForcePendingChannel) + NS_INTERFACE_MAP_ENTRY(nsIUploadChannel) + NS_INTERFACE_MAP_ENTRY(nsIFormPOSTActionChannel) + NS_INTERFACE_MAP_ENTRY(nsIUploadChannel2) + NS_INTERFACE_MAP_ENTRY(nsISupportsPriority) + NS_INTERFACE_MAP_ENTRY(nsITraceableChannel) + NS_INTERFACE_MAP_ENTRY(nsIPrivateBrowsingChannel) + NS_INTERFACE_MAP_ENTRY(nsITimedChannel) + NS_INTERFACE_MAP_ENTRY(nsIConsoleReportCollector) + NS_INTERFACE_MAP_ENTRY(nsIThrottledInputChannel) + NS_INTERFACE_MAP_ENTRY(nsIClassifiedChannel) + NS_INTERFACE_MAP_ENTRY_CONCRETE(HttpBaseChannel) +NS_INTERFACE_MAP_END_INHERITING(nsHashPropertyBag) + +//----------------------------------------------------------------------------- +// HttpBaseChannel::nsIRequest +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +HttpBaseChannel::GetName(nsACString& aName) { + aName = mSpec; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::IsPending(bool* aIsPending) { + NS_ENSURE_ARG_POINTER(aIsPending); + *aIsPending = LoadIsPending() || LoadForcePending(); + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetStatus(nsresult* aStatus) { + NS_ENSURE_ARG_POINTER(aStatus); + *aStatus = mStatus; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetLoadGroup(nsILoadGroup** aLoadGroup) { + NS_ENSURE_ARG_POINTER(aLoadGroup); + *aLoadGroup = mLoadGroup; + NS_IF_ADDREF(*aLoadGroup); + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::SetLoadGroup(nsILoadGroup* aLoadGroup) { + MOZ_ASSERT(NS_IsMainThread(), "Should only be called on the main thread."); + + if (!CanSetLoadGroup(aLoadGroup)) { + return NS_ERROR_FAILURE; + } + + mLoadGroup = aLoadGroup; + mProgressSink = nullptr; + UpdatePrivateBrowsing(); + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetLoadFlags(nsLoadFlags* aLoadFlags) { + NS_ENSURE_ARG_POINTER(aLoadFlags); + *aLoadFlags = mLoadFlags; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::SetLoadFlags(nsLoadFlags aLoadFlags) { + mLoadFlags = aLoadFlags; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetTRRMode(nsIRequest::TRRMode* aTRRMode) { + return GetTRRModeImpl(aTRRMode); +} + +NS_IMETHODIMP +HttpBaseChannel::SetTRRMode(nsIRequest::TRRMode aTRRMode) { + return SetTRRModeImpl(aTRRMode); +} + +NS_IMETHODIMP +HttpBaseChannel::SetDocshellUserAgentOverride() { + RefPtr bc; + MOZ_ALWAYS_SUCCEEDS(mLoadInfo->GetBrowsingContext(getter_AddRefs(bc))); + if (!bc) { + return NS_OK; + } + + const nsString& customUserAgent = bc->GetUserAgentOverride(); + if (customUserAgent.IsEmpty() || customUserAgent.IsVoid()) { + return NS_OK; + } + + NS_ConvertUTF16toUTF8 utf8CustomUserAgent(customUserAgent); + nsresult rv = SetRequestHeader("User-Agent"_ns, utf8CustomUserAgent, false); + if (NS_FAILED(rv)) return rv; + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// HttpBaseChannel::nsIChannel +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +HttpBaseChannel::GetOriginalURI(nsIURI** aOriginalURI) { + NS_ENSURE_ARG_POINTER(aOriginalURI); + *aOriginalURI = do_AddRef(mOriginalURI).take(); + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::SetOriginalURI(nsIURI* aOriginalURI) { + ENSURE_CALLED_BEFORE_CONNECT(); + + NS_ENSURE_ARG_POINTER(aOriginalURI); + mOriginalURI = aOriginalURI; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetURI(nsIURI** aURI) { + NS_ENSURE_ARG_POINTER(aURI); + *aURI = do_AddRef(mURI).take(); + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetOwner(nsISupports** aOwner) { + NS_ENSURE_ARG_POINTER(aOwner); + *aOwner = mOwner; + NS_IF_ADDREF(*aOwner); + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::SetOwner(nsISupports* aOwner) { + mOwner = aOwner; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::SetLoadInfo(nsILoadInfo* aLoadInfo) { + MOZ_RELEASE_ASSERT(aLoadInfo, "loadinfo can't be null"); + mLoadInfo = aLoadInfo; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetLoadInfo(nsILoadInfo** aLoadInfo) { + NS_IF_ADDREF(*aLoadInfo = mLoadInfo); + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetIsDocument(bool* aIsDocument) { + return NS_GetIsDocumentChannel(this, aIsDocument); +} + +NS_IMETHODIMP +HttpBaseChannel::GetNotificationCallbacks(nsIInterfaceRequestor** aCallbacks) { + *aCallbacks = mCallbacks; + NS_IF_ADDREF(*aCallbacks); + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::SetNotificationCallbacks(nsIInterfaceRequestor* aCallbacks) { + MOZ_ASSERT(NS_IsMainThread(), "Should only be called on the main thread."); + + if (!CanSetCallbacks(aCallbacks)) { + return NS_ERROR_FAILURE; + } + + mCallbacks = aCallbacks; + mProgressSink = nullptr; + + UpdatePrivateBrowsing(); + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetContentType(nsACString& aContentType) { + if (!mResponseHead) { + aContentType.Truncate(); + return NS_ERROR_NOT_AVAILABLE; + } + + mResponseHead->ContentType(aContentType); + if (!aContentType.IsEmpty()) { + return NS_OK; + } + + aContentType.AssignLiteral(UNKNOWN_CONTENT_TYPE); + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::SetContentType(const nsACString& aContentType) { + if (mListener || LoadWasOpened()) { + if (!mResponseHead) return NS_ERROR_NOT_AVAILABLE; + + nsAutoCString contentTypeBuf, charsetBuf; + bool hadCharset; + net_ParseContentType(aContentType, contentTypeBuf, charsetBuf, &hadCharset); + + mResponseHead->SetContentType(contentTypeBuf); + + // take care not to stomp on an existing charset + if (hadCharset) mResponseHead->SetContentCharset(charsetBuf); + + } else { + // We are being given a content-type hint. + bool dummy; + net_ParseContentType(aContentType, mContentTypeHint, mContentCharsetHint, + &dummy); + } + + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetContentCharset(nsACString& aContentCharset) { + if (!mResponseHead) return NS_ERROR_NOT_AVAILABLE; + + mResponseHead->ContentCharset(aContentCharset); + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::SetContentCharset(const nsACString& aContentCharset) { + if (mListener) { + if (!mResponseHead) return NS_ERROR_NOT_AVAILABLE; + + mResponseHead->SetContentCharset(aContentCharset); + } else { + // Charset hint + mContentCharsetHint = aContentCharset; + } + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetContentDisposition(uint32_t* aContentDisposition) { + // See bug 1658877. If mContentDispositionHint is already + // DISPOSITION_ATTACHMENT, it means this channel is created from a + // download attribute. In this case, we should prefer the value from the + // download attribute rather than the value in content disposition header. + if (mContentDispositionHint == nsIChannel::DISPOSITION_ATTACHMENT) { + *aContentDisposition = mContentDispositionHint; + return NS_OK; + } + + nsresult rv; + nsCString header; + + rv = GetContentDispositionHeader(header); + if (NS_FAILED(rv)) { + if (mContentDispositionHint == UINT32_MAX) return rv; + + *aContentDisposition = mContentDispositionHint; + return NS_OK; + } + + *aContentDisposition = NS_GetContentDispositionFromHeader(header, this); + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::SetContentDisposition(uint32_t aContentDisposition) { + mContentDispositionHint = aContentDisposition; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetContentDispositionFilename( + nsAString& aContentDispositionFilename) { + aContentDispositionFilename.Truncate(); + nsresult rv; + nsCString header; + + rv = GetContentDispositionHeader(header); + if (NS_SUCCEEDED(rv)) { + rv = NS_GetFilenameFromDisposition(aContentDispositionFilename, header); + } + + // If we failed to get the filename from header, we should use + // mContentDispositionFilename, since mContentDispositionFilename is set from + // the download attribute. + if (NS_FAILED(rv)) { + if (!mContentDispositionFilename) { + return rv; + } + + aContentDispositionFilename = *mContentDispositionFilename; + return NS_OK; + } + + return rv; +} + +NS_IMETHODIMP +HttpBaseChannel::SetContentDispositionFilename( + const nsAString& aContentDispositionFilename) { + mContentDispositionFilename = + MakeUnique(aContentDispositionFilename); + + // For safety reasons ensure the filename doesn't contain null characters and + // replace them with underscores. We may later pass the extension to system + // MIME APIs that expect null terminated strings. + mContentDispositionFilename->ReplaceChar(char16_t(0), '_'); + + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetContentDispositionHeader( + nsACString& aContentDispositionHeader) { + if (!mResponseHead) return NS_ERROR_NOT_AVAILABLE; + + nsresult rv = mResponseHead->GetHeader(nsHttp::Content_Disposition, + aContentDispositionHeader); + if (NS_FAILED(rv) || aContentDispositionHeader.IsEmpty()) + return NS_ERROR_NOT_AVAILABLE; + + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetContentLength(int64_t* aContentLength) { + NS_ENSURE_ARG_POINTER(aContentLength); + + if (!mResponseHead) return NS_ERROR_NOT_AVAILABLE; + + if (LoadDeliveringAltData()) { + MOZ_ASSERT(!mAvailableCachedAltDataType.IsEmpty()); + *aContentLength = mAltDataLength; + return NS_OK; + } + + *aContentLength = mResponseHead->ContentLength(); + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::SetContentLength(int64_t value) { + MOZ_ASSERT_UNREACHABLE("HttpBaseChannel::SetContentLength"); + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +HttpBaseChannel::Open(nsIInputStream** aStream) { + if (!gHttpHandler->Active()) { + LOG(("HttpBaseChannel::Open after HTTP shutdown...")); + return NS_ERROR_NOT_AVAILABLE; + } + + nsCOMPtr listener; + nsresult rv = + nsContentSecurityManager::doContentSecurityCheck(this, listener); + NS_ENSURE_SUCCESS(rv, rv); + + NS_ENSURE_TRUE(!LoadWasOpened(), NS_ERROR_IN_PROGRESS); + + if (!gHttpHandler->Active()) { + LOG(("HttpBaseChannel::Open after HTTP shutdown...")); + return NS_ERROR_NOT_AVAILABLE; + } + + return NS_ImplementChannelOpen(this, aStream); +} + +//----------------------------------------------------------------------------- +// HttpBaseChannel::nsIUploadChannel +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +HttpBaseChannel::GetUploadStream(nsIInputStream** stream) { + NS_ENSURE_ARG_POINTER(stream); + *stream = mUploadStream; + NS_IF_ADDREF(*stream); + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::SetUploadStream(nsIInputStream* stream, + const nsACString& contentTypeArg, + int64_t contentLength) { + // NOTE: for backwards compatibility and for compatibility with old style + // plugins, |stream| may include headers, specifically Content-Type and + // Content-Length headers. in this case, |contentType| and |contentLength| + // would be unspecified. this is traditionally the case of a POST request, + // and so we select POST as the request method if contentType and + // contentLength are unspecified. + + if (stream) { + nsAutoCString method; + bool hasHeaders = false; + + // This method and ExplicitSetUploadStream mean different things by "empty + // content type string". This method means "no header", but + // ExplicitSetUploadStream means "header with empty value". So we have to + // massage the contentType argument into the form ExplicitSetUploadStream + // expects. + nsCOMPtr mimeStream; + nsCString contentType(contentTypeArg); + if (contentType.IsEmpty()) { + contentType.SetIsVoid(true); + method = "POST"_ns; + + // MIME streams are a special case, and include headers which need to be + // copied to the channel. + mimeStream = do_QueryInterface(stream); + if (mimeStream) { + // Copy non-origin related headers to the channel. + nsCOMPtr visitor = + new AddHeadersToChannelVisitor(this); + mimeStream->VisitHeaders(visitor); + + return ExplicitSetUploadStream(stream, contentType, contentLength, + method, hasHeaders); + } + + hasHeaders = true; + } else { + method = "PUT"_ns; + + MOZ_ASSERT( + NS_FAILED(CallQueryInterface(stream, getter_AddRefs(mimeStream))), + "nsIMIMEInputStream should not be set with an explicit content type"); + } + return ExplicitSetUploadStream(stream, contentType, contentLength, method, + hasHeaders); + } + + // if stream is null, ExplicitSetUploadStream returns error. + // So we need special case for GET method. + StoreUploadStreamHasHeaders(false); + mRequestHead.SetMethod("GET"_ns); // revert to GET request + mUploadStream = stream; + return NS_OK; +} + +namespace { + +void CopyComplete(void* aClosure, nsresult aStatus) { +#ifdef DEBUG + // Called on the STS thread by NS_AsyncCopy + nsCOMPtr sts = + do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID); + bool result = false; + sts->IsOnCurrentThread(&result); + MOZ_ASSERT(result, "Should only be called on the STS thread."); +#endif + + auto channel = static_cast(aClosure); + channel->OnCopyComplete(aStatus); +} + +} // anonymous namespace + +NS_IMETHODIMP +HttpBaseChannel::EnsureUploadStreamIsCloneable(nsIRunnable* aCallback) { + MOZ_ASSERT(NS_IsMainThread(), "Should only be called on the main thread."); + NS_ENSURE_ARG_POINTER(aCallback); + + // We could in theory allow multiple callers to use this method, + // but the complexity does not seem worth it yet. Just fail if + // this is called more than once simultaneously. + NS_ENSURE_FALSE(mUploadCloneableCallback, NS_ERROR_UNEXPECTED); + + // We can immediately exec the callback if we don't have an upload stream. + if (!mUploadStream) { + aCallback->Run(); + return NS_OK; + } + + // Upload nsIInputStream must be cloneable and seekable in order to be + // processed by devtools network inspector. + nsCOMPtr seekable = do_QueryInterface(mUploadStream); + if (seekable && NS_InputStreamIsCloneable(mUploadStream)) { + aCallback->Run(); + return NS_OK; + } + + nsCOMPtr storageStream; + nsresult rv = + NS_NewStorageStream(4096, UINT32_MAX, getter_AddRefs(storageStream)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr newUploadStream; + rv = storageStream->NewInputStream(0, getter_AddRefs(newUploadStream)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr sink; + rv = storageStream->GetOutputStream(0, getter_AddRefs(sink)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr source; + if (NS_InputStreamIsBuffered(mUploadStream)) { + source = mUploadStream; + } else { + rv = NS_NewBufferedInputStream(getter_AddRefs(source), + mUploadStream.forget(), 4096); + NS_ENSURE_SUCCESS(rv, rv); + } + + nsCOMPtr target = + do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID); + + mUploadCloneableCallback = aCallback; + + rv = NS_AsyncCopy(source, sink, target, NS_ASYNCCOPY_VIA_READSEGMENTS, + 4096, // copy segment size + CopyComplete, this); + if (NS_WARN_IF(NS_FAILED(rv))) { + mUploadCloneableCallback = nullptr; + return rv; + } + + // Since we're consuming the old stream, replace it with the new + // stream immediately. + mUploadStream = newUploadStream; + + // Explicity hold the stream alive until copying is complete. This will + // be released in EnsureUploadStreamIsCloneableComplete(). + AddRef(); + + return NS_OK; +} + +void HttpBaseChannel::OnCopyComplete(nsresult aStatus) { + // Assert in parent process because we don't have to label the runnable + // in parent process. + MOZ_ASSERT(XRE_IsParentProcess()); + + nsCOMPtr runnable = NewRunnableMethod( + "net::HttpBaseChannel::EnsureUploadStreamIsCloneableComplete", this, + &HttpBaseChannel::EnsureUploadStreamIsCloneableComplete, aStatus); + NS_DispatchToMainThread(runnable.forget()); +} + +void HttpBaseChannel::EnsureUploadStreamIsCloneableComplete(nsresult aStatus) { + MOZ_ASSERT(NS_IsMainThread(), "Should only be called on the main thread."); + MOZ_ASSERT(mUploadCloneableCallback); + + if (NS_SUCCEEDED(mStatus)) { + mStatus = aStatus; + } + + mUploadCloneableCallback->Run(); + mUploadCloneableCallback = nullptr; + + // Release the reference we grabbed in EnsureUploadStreamIsCloneable() now + // that the copying is complete. + Release(); +} + +NS_IMETHODIMP +HttpBaseChannel::CloneUploadStream(int64_t* aContentLength, + nsIInputStream** aClonedStream) { + NS_ENSURE_ARG_POINTER(aContentLength); + NS_ENSURE_ARG_POINTER(aClonedStream); + *aClonedStream = nullptr; + + if (!mUploadStream) { + return NS_OK; + } + + nsCOMPtr clonedStream; + nsresult rv = + NS_CloneInputStream(mUploadStream, getter_AddRefs(clonedStream)); + NS_ENSURE_SUCCESS(rv, rv); + + clonedStream.forget(aClonedStream); + + *aContentLength = mReqContentLength; + return NS_OK; +} + +//----------------------------------------------------------------------------- +// HttpBaseChannel::nsIUploadChannel2 +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +HttpBaseChannel::ExplicitSetUploadStream(nsIInputStream* aStream, + const nsACString& aContentType, + int64_t aContentLength, + const nsACString& aMethod, + bool aStreamHasHeaders) { + // Ensure stream is set and method is valid + NS_ENSURE_TRUE(aStream, NS_ERROR_FAILURE); + + { + DebugOnly> mimeStream; + MOZ_ASSERT( + !aStreamHasHeaders || NS_FAILED(CallQueryInterface( + aStream, getter_AddRefs(mimeStream.value))), + "nsIMIMEInputStream should not include headers"); + } + + nsresult rv = SetRequestMethod(aMethod); + NS_ENSURE_SUCCESS(rv, rv); + + if (!aStreamHasHeaders && !aContentType.IsVoid()) { + if (aContentType.IsEmpty()) { + SetEmptyRequestHeader("Content-Type"_ns); + } else { + SetRequestHeader("Content-Type"_ns, aContentType, false); + } + } + + StoreUploadStreamHasHeaders(aStreamHasHeaders); + + nsCOMPtr seekable = do_QueryInterface(aStream); + if (!seekable) { + nsCOMPtr stream = aStream; + seekable = new PartiallySeekableInputStream(stream.forget()); + } + + mUploadStream = do_QueryInterface(seekable); + + if (aContentLength >= 0) { + ExplicitSetUploadStreamLength(aContentLength, aStreamHasHeaders); + return NS_OK; + } + + // Sync access to the stream length. + int64_t length; + if (InputStreamLengthHelper::GetSyncLength(aStream, &length)) { + ExplicitSetUploadStreamLength(length >= 0 ? length : 0, aStreamHasHeaders); + return NS_OK; + } + + // Let's resolve the size of the stream. + RefPtr self = this; + InputStreamLengthHelper::GetAsyncLength( + aStream, [self, aStreamHasHeaders](int64_t aLength) { + self->StorePendingInputStreamLengthOperation(false); + self->ExplicitSetUploadStreamLength(aLength >= 0 ? aLength : 0, + aStreamHasHeaders); + self->MaybeResumeAsyncOpen(); + }); + StorePendingInputStreamLengthOperation(true); + return NS_OK; +} + +void HttpBaseChannel::ExplicitSetUploadStreamLength(uint64_t aContentLength, + bool aStreamHasHeaders) { + // We already have the content length. We don't need to determinate it. + mReqContentLength = aContentLength; + + if (aStreamHasHeaders) { + return; + } + + nsAutoCString header; + header.AssignLiteral("Content-Length"); + + // Maybe the content-length header has been already set. + nsAutoCString value; + nsresult rv = GetRequestHeader(header, value); + if (NS_SUCCEEDED(rv) && !value.IsEmpty()) { + return; + } + + // SetRequestHeader propagates headers to chrome if HttpChannelChild + MOZ_ASSERT(!LoadWasOpened()); + nsAutoCString contentLengthStr; + contentLengthStr.AppendInt(aContentLength); + SetRequestHeader(header, contentLengthStr, false); +} + +NS_IMETHODIMP +HttpBaseChannel::GetUploadStreamHasHeaders(bool* hasHeaders) { + NS_ENSURE_ARG(hasHeaders); + + *hasHeaders = LoadUploadStreamHasHeaders(); + return NS_OK; +} + +bool HttpBaseChannel::MaybeWaitForUploadStreamLength( + nsIStreamListener* aListener, nsISupports* aContext) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!LoadAsyncOpenWaitingForStreamLength(), + "AsyncOpen() called twice?"); + + if (!LoadPendingInputStreamLengthOperation()) { + return false; + } + + mListener = aListener; + StoreAsyncOpenWaitingForStreamLength(true); + return true; +} + +void HttpBaseChannel::MaybeResumeAsyncOpen() { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!LoadPendingInputStreamLengthOperation()); + + if (!LoadAsyncOpenWaitingForStreamLength()) { + return; + } + + nsCOMPtr listener; + listener.swap(mListener); + + StoreAsyncOpenWaitingForStreamLength(false); + + nsresult rv = AsyncOpen(listener); + if (NS_WARN_IF(NS_FAILED(rv))) { + DoAsyncAbort(rv); + } +} + +//----------------------------------------------------------------------------- +// HttpBaseChannel::nsIEncodedChannel +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +HttpBaseChannel::GetApplyConversion(bool* value) { + *value = LoadApplyConversion(); + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::SetApplyConversion(bool value) { + LOG(("HttpBaseChannel::SetApplyConversion [this=%p value=%d]\n", this, + value)); + StoreApplyConversion(value); + return NS_OK; +} + +nsresult HttpBaseChannel::DoApplyContentConversions( + nsIStreamListener* aNextListener, nsIStreamListener** aNewNextListener) { + return DoApplyContentConversions(aNextListener, aNewNextListener, nullptr); +} + +// create a listener chain that looks like this +// http-channel -> decompressor (n times) -> InterceptFailedOnSTop -> +// channel-creator-listener +// +// we need to do this because not every decompressor has fully streamed output +// so may need a call to OnStopRequest to identify its completion state.. and if +// it creates an error there the channel status code needs to be updated before +// calling the terminal listener. Having the decompress do it via cancel() means +// channels cannot effectively be used in two contexts (specifically this one +// and a peek context for sniffing) +// +class InterceptFailedOnStop : public nsIStreamListener { + virtual ~InterceptFailedOnStop() = default; + nsCOMPtr mNext; + HttpBaseChannel* mChannel; + + public: + InterceptFailedOnStop(nsIStreamListener* arg, HttpBaseChannel* chan) + : mNext(arg), mChannel(chan) {} + NS_DECL_THREADSAFE_ISUPPORTS + + NS_IMETHOD OnStartRequest(nsIRequest* aRequest) override { + return mNext->OnStartRequest(aRequest); + } + + NS_IMETHOD OnStopRequest(nsIRequest* aRequest, + nsresult aStatusCode) override { + if (NS_FAILED(aStatusCode) && NS_SUCCEEDED(mChannel->mStatus)) { + LOG(("HttpBaseChannel::InterceptFailedOnStop %p seting status %" PRIx32, + mChannel, static_cast(aStatusCode))); + mChannel->mStatus = aStatusCode; + } + return mNext->OnStopRequest(aRequest, aStatusCode); + } + + NS_IMETHOD OnDataAvailable(nsIRequest* aRequest, nsIInputStream* aInputStream, + uint64_t aOffset, uint32_t aCount) override { + return mNext->OnDataAvailable(aRequest, aInputStream, aOffset, aCount); + } +}; + +NS_IMPL_ISUPPORTS(InterceptFailedOnStop, nsIStreamListener, nsIRequestObserver) + +NS_IMETHODIMP +HttpBaseChannel::DoApplyContentConversions(nsIStreamListener* aNextListener, + nsIStreamListener** aNewNextListener, + nsISupports* aCtxt) { + *aNewNextListener = nullptr; + if (!mResponseHead || !aNextListener) { + return NS_OK; + } + + LOG(("HttpBaseChannel::DoApplyContentConversions [this=%p]\n", this)); + + if (!LoadApplyConversion()) { + LOG(("not applying conversion per ApplyConversion\n")); + return NS_OK; + } + + if (LoadHasAppliedConversion()) { + LOG(("not applying conversion because HasAppliedConversion is true\n")); + return NS_OK; + } + + if (LoadDeliveringAltData()) { + MOZ_ASSERT(!mAvailableCachedAltDataType.IsEmpty()); + LOG(("not applying conversion because delivering alt-data\n")); + return NS_OK; + } + + nsAutoCString contentEncoding; + nsresult rv = + mResponseHead->GetHeader(nsHttp::Content_Encoding, contentEncoding); + if (NS_FAILED(rv) || contentEncoding.IsEmpty()) return NS_OK; + + nsCOMPtr nextListener = + new InterceptFailedOnStop(aNextListener, this); + + // The encodings are listed in the order they were applied + // (see rfc 2616 section 14.11), so they need to removed in reverse + // order. This is accomplished because the converter chain ends up + // being a stack with the last converter created being the first one + // to accept the raw network data. + + char* cePtr = contentEncoding.BeginWriting(); + uint32_t count = 0; + while (char* val = nsCRT::strtok(cePtr, HTTP_LWS ",", &cePtr)) { + if (++count > 16) { + // That's ridiculous. We only understand 2 different ones :) + // but for compatibility with old code, we will just carry on without + // removing the encodings + LOG(("Too many Content-Encodings. Ignoring remainder.\n")); + break; + } + + if (gHttpHandler->IsAcceptableEncoding(val, mURI->SchemeIs("https"))) { + nsCOMPtr serv; + rv = gHttpHandler->GetStreamConverterService(getter_AddRefs(serv)); + + // we won't fail to load the page just because we couldn't load the + // stream converter service.. carry on.. + if (NS_FAILED(rv)) { + if (val) LOG(("Unknown content encoding '%s', ignoring\n", val)); + continue; + } + + nsCOMPtr converter; + nsAutoCString from(val); + ToLowerCase(from); + rv = serv->AsyncConvertData(from.get(), "uncompressed", nextListener, + aCtxt, getter_AddRefs(converter)); + if (NS_FAILED(rv)) { + LOG(("Unexpected failure of AsyncConvertData %s\n", val)); + return rv; + } + + LOG(("converter removed '%s' content-encoding\n", val)); + if (Telemetry::CanRecordPrereleaseData()) { + int mode = 0; + if (from.EqualsLiteral("gzip") || from.EqualsLiteral("x-gzip")) { + mode = 1; + } else if (from.EqualsLiteral("deflate") || + from.EqualsLiteral("x-deflate")) { + mode = 2; + } else if (from.EqualsLiteral("br")) { + mode = 3; + } + Telemetry::Accumulate(Telemetry::HTTP_CONTENT_ENCODING, mode); + } + nextListener = converter; + } else { + if (val) LOG(("Unknown content encoding '%s', ignoring\n", val)); + } + } + *aNewNextListener = nextListener; + NS_IF_ADDREF(*aNewNextListener); + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetContentEncodings(nsIUTF8StringEnumerator** aEncodings) { + if (!mResponseHead) { + *aEncodings = nullptr; + return NS_OK; + } + + nsAutoCString encoding; + Unused << mResponseHead->GetHeader(nsHttp::Content_Encoding, encoding); + if (encoding.IsEmpty()) { + *aEncodings = nullptr; + return NS_OK; + } + RefPtr enumerator = + new nsContentEncodings(this, encoding.get()); + enumerator.forget(aEncodings); + return NS_OK; +} + +//----------------------------------------------------------------------------- +// HttpBaseChannel::nsContentEncodings +//----------------------------------------------------------------------------- + +HttpBaseChannel::nsContentEncodings::nsContentEncodings( + nsIHttpChannel* aChannel, const char* aEncodingHeader) + : mEncodingHeader(aEncodingHeader), mChannel(aChannel), mReady(false) { + mCurEnd = aEncodingHeader + strlen(aEncodingHeader); + mCurStart = mCurEnd; +} + +//----------------------------------------------------------------------------- +// HttpBaseChannel::nsContentEncodings::nsISimpleEnumerator +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +HttpBaseChannel::nsContentEncodings::HasMore(bool* aMoreEncodings) { + if (mReady) { + *aMoreEncodings = true; + return NS_OK; + } + + nsresult rv = PrepareForNext(); + *aMoreEncodings = NS_SUCCEEDED(rv); + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::nsContentEncodings::GetNext(nsACString& aNextEncoding) { + aNextEncoding.Truncate(); + if (!mReady) { + nsresult rv = PrepareForNext(); + if (NS_FAILED(rv)) { + return NS_ERROR_FAILURE; + } + } + + const nsACString& encoding = Substring(mCurStart, mCurEnd); + + nsACString::const_iterator start, end; + encoding.BeginReading(start); + encoding.EndReading(end); + + bool haveType = false; + if (CaseInsensitiveFindInReadable("gzip"_ns, start, end)) { + aNextEncoding.AssignLiteral(APPLICATION_GZIP); + haveType = true; + } + + if (!haveType) { + encoding.BeginReading(start); + if (CaseInsensitiveFindInReadable("compress"_ns, start, end)) { + aNextEncoding.AssignLiteral(APPLICATION_COMPRESS); + haveType = true; + } + } + + if (!haveType) { + encoding.BeginReading(start); + if (CaseInsensitiveFindInReadable("deflate"_ns, start, end)) { + aNextEncoding.AssignLiteral(APPLICATION_ZIP); + haveType = true; + } + } + + if (!haveType) { + encoding.BeginReading(start); + if (CaseInsensitiveFindInReadable("br"_ns, start, end)) { + aNextEncoding.AssignLiteral(APPLICATION_BROTLI); + haveType = true; + } + } + + // Prepare to fetch the next encoding + mCurEnd = mCurStart; + mReady = false; + + if (haveType) return NS_OK; + + NS_WARNING("Unknown encoding type"); + return NS_ERROR_FAILURE; +} + +//----------------------------------------------------------------------------- +// HttpBaseChannel::nsContentEncodings::nsISupports +//----------------------------------------------------------------------------- + +NS_IMPL_ISUPPORTS(HttpBaseChannel::nsContentEncodings, nsIUTF8StringEnumerator, + nsIStringEnumerator) + +//----------------------------------------------------------------------------- +// HttpBaseChannel::nsContentEncodings +//----------------------------------------------------------------------------- + +nsresult HttpBaseChannel::nsContentEncodings::PrepareForNext(void) { + MOZ_ASSERT(mCurStart == mCurEnd, "Indeterminate state"); + + // At this point both mCurStart and mCurEnd point to somewhere + // past the end of the next thing we want to return + + while (mCurEnd != mEncodingHeader) { + --mCurEnd; + if (*mCurEnd != ',' && !nsCRT::IsAsciiSpace(*mCurEnd)) break; + } + if (mCurEnd == mEncodingHeader) + return NS_ERROR_NOT_AVAILABLE; // no more encodings + ++mCurEnd; + + // At this point mCurEnd points to the first char _after_ the + // header we want. Furthermore, mCurEnd - 1 != mEncodingHeader + + mCurStart = mCurEnd - 1; + while (mCurStart != mEncodingHeader && *mCurStart != ',' && + !nsCRT::IsAsciiSpace(*mCurStart)) + --mCurStart; + if (*mCurStart == ',' || nsCRT::IsAsciiSpace(*mCurStart)) + ++mCurStart; // we stopped because of a weird char, so move up one + + // At this point mCurStart and mCurEnd bracket the encoding string + // we want. Check that it's not "identity" + if (Substring(mCurStart, mCurEnd) + .Equals("identity", nsCaseInsensitiveCStringComparator)) { + mCurEnd = mCurStart; + return PrepareForNext(); + } + + mReady = true; + return NS_OK; +} + +//----------------------------------------------------------------------------- +// HttpBaseChannel::nsIHttpChannel +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +HttpBaseChannel::GetChannelId(uint64_t* aChannelId) { + NS_ENSURE_ARG_POINTER(aChannelId); + *aChannelId = mChannelId; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::SetChannelId(uint64_t aChannelId) { + mChannelId = aChannelId; + return NS_OK; +} + +NS_IMETHODIMP HttpBaseChannel::GetTopLevelContentWindowId(uint64_t* aWindowId) { + if (!mContentWindowId) { + nsCOMPtr loadContext; + GetCallback(loadContext); + if (loadContext) { + nsCOMPtr topWindow; + loadContext->GetTopWindow(getter_AddRefs(topWindow)); + if (topWindow) { + if (nsPIDOMWindowInner* inner = + nsPIDOMWindowOuter::From(topWindow)->GetCurrentInnerWindow()) { + mContentWindowId = inner->WindowID(); + } + } + } + } + *aWindowId = mContentWindowId; + return NS_OK; +} + +NS_IMETHODIMP HttpBaseChannel::SetTopLevelOuterContentWindowId( + uint64_t aWindowId) { + mTopLevelOuterContentWindowId = aWindowId; + return NS_OK; +} + +NS_IMETHODIMP HttpBaseChannel::GetTopLevelOuterContentWindowId( + uint64_t* aWindowId) { + EnsureTopLevelOuterContentWindowId(); + *aWindowId = mTopLevelOuterContentWindowId; + return NS_OK; +} + +NS_IMETHODIMP HttpBaseChannel::SetTopLevelContentWindowId(uint64_t aWindowId) { + mContentWindowId = aWindowId; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::IsThirdPartyTrackingResource(bool* aIsTrackingResource) { + MOZ_ASSERT( + !(mFirstPartyClassificationFlags && mThirdPartyClassificationFlags)); + *aIsTrackingResource = UrlClassifierCommon::IsTrackingClassificationFlag( + mThirdPartyClassificationFlags); + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::IsThirdPartySocialTrackingResource( + bool* aIsThirdPartySocialTrackingResource) { + MOZ_ASSERT(!mFirstPartyClassificationFlags || + !mThirdPartyClassificationFlags); + *aIsThirdPartySocialTrackingResource = + UrlClassifierCommon::IsSocialTrackingClassificationFlag( + mThirdPartyClassificationFlags); + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetClassificationFlags(uint32_t* aFlags) { + if (mThirdPartyClassificationFlags) { + *aFlags = mThirdPartyClassificationFlags; + } else { + *aFlags = mFirstPartyClassificationFlags; + } + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetFirstPartyClassificationFlags(uint32_t* aFlags) { + *aFlags = mFirstPartyClassificationFlags; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetThirdPartyClassificationFlags(uint32_t* aFlags) { + *aFlags = mThirdPartyClassificationFlags; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetFlashPluginState(nsIHttpChannel::FlashPluginState* aState) { + uint32_t flashPluginState = mFlashPluginState; + *aState = (nsIHttpChannel::FlashPluginState)flashPluginState; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetTransferSize(uint64_t* aTransferSize) { + *aTransferSize = mTransferSize; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetRequestSize(uint64_t* aRequestSize) { + *aRequestSize = mRequestSize; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetDecodedBodySize(uint64_t* aDecodedBodySize) { + *aDecodedBodySize = mDecodedBodySize; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetEncodedBodySize(uint64_t* aEncodedBodySize) { + *aEncodedBodySize = mEncodedBodySize; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetSupportsHTTP3(bool* aSupportsHTTP3) { + *aSupportsHTTP3 = mSupportsHTTP3; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetRequestMethod(nsACString& aMethod) { + mRequestHead.Method(aMethod); + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::SetRequestMethod(const nsACString& aMethod) { + ENSURE_CALLED_BEFORE_CONNECT(); + + const nsCString& flatMethod = PromiseFlatCString(aMethod); + + // Method names are restricted to valid HTTP tokens. + if (!nsHttp::IsValidToken(flatMethod)) return NS_ERROR_INVALID_ARG; + + mRequestHead.SetMethod(flatMethod); + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetReferrerInfo(nsIReferrerInfo** aReferrerInfo) { + NS_ENSURE_ARG_POINTER(aReferrerInfo); + *aReferrerInfo = do_AddRef(mReferrerInfo).take(); + return NS_OK; +} + +nsresult HttpBaseChannel::SetReferrerInfoInternal( + nsIReferrerInfo* aReferrerInfo, bool aClone, bool aCompute, + bool aRespectBeforeConnect) { + LOG( + ("HttpBaseChannel::SetReferrerInfoInternal [this=%p aClone(%d) " + "aCompute(%d)]\n", + this, aClone, aCompute)); + if (aRespectBeforeConnect) { + ENSURE_CALLED_BEFORE_CONNECT(); + } + + mReferrerInfo = aReferrerInfo; + + // clear existing referrer, if any + nsresult rv = ClearReferrerHeader(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (!mReferrerInfo) { + return NS_OK; + } + + if (aClone) { + mReferrerInfo = static_cast(aReferrerInfo)->Clone(); + } + + dom::ReferrerInfo* referrerInfo = + static_cast(mReferrerInfo.get()); + + // Don't set referrerInfo if it has not been initialized. + if (!referrerInfo->IsInitialized()) { + mReferrerInfo = nullptr; + return NS_ERROR_NOT_INITIALIZED; + } + + if (aCompute) { + rv = referrerInfo->ComputeReferrer(this); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + nsCOMPtr computedReferrer = mReferrerInfo->GetComputedReferrer(); + if (!computedReferrer) { + return NS_OK; + } + + nsAutoCString spec; + rv = computedReferrer->GetSpec(spec); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return SetReferrerHeader(spec, aRespectBeforeConnect); +} + +NS_IMETHODIMP +HttpBaseChannel::SetReferrerInfo(nsIReferrerInfo* aReferrerInfo) { + return SetReferrerInfoInternal(aReferrerInfo, true, true, true); +} + +NS_IMETHODIMP +HttpBaseChannel::SetReferrerInfoWithoutClone(nsIReferrerInfo* aReferrerInfo) { + return SetReferrerInfoInternal(aReferrerInfo, false, true, true); +} + +// Return the channel's proxy URI, or if it doesn't exist, the +// channel's main URI. +NS_IMETHODIMP +HttpBaseChannel::GetProxyURI(nsIURI** aOut) { + NS_ENSURE_ARG_POINTER(aOut); + nsCOMPtr result(mProxyURI); + result.forget(aOut); + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetRequestHeader(const nsACString& aHeader, + nsACString& aValue) { + aValue.Truncate(); + + // XXX might be better to search the header list directly instead of + // hitting the http atom hash table. + nsHttpAtom atom = nsHttp::ResolveAtom(aHeader); + if (!atom) return NS_ERROR_NOT_AVAILABLE; + + return mRequestHead.GetHeader(atom, aValue); +} + +NS_IMETHODIMP +HttpBaseChannel::SetRequestHeader(const nsACString& aHeader, + const nsACString& aValue, bool aMerge) { + const nsCString& flatHeader = PromiseFlatCString(aHeader); + const nsCString& flatValue = PromiseFlatCString(aValue); + + LOG( + ("HttpBaseChannel::SetRequestHeader [this=%p header=\"%s\" value=\"%s\" " + "merge=%u]\n", + this, flatHeader.get(), flatValue.get(), aMerge)); + + // Verify header names are valid HTTP tokens and header values are reasonably + // close to whats allowed in RFC 2616. + if (!nsHttp::IsValidToken(flatHeader) || + !nsHttp::IsReasonableHeaderValue(flatValue)) { + return NS_ERROR_INVALID_ARG; + } + + return mRequestHead.SetHeader(aHeader, flatValue, aMerge); +} + +NS_IMETHODIMP +HttpBaseChannel::SetNewReferrerInfo(const nsACString& aUrl, + nsIReferrerInfo::ReferrerPolicyIDL aPolicy, + bool aSendReferrer) { + nsresult rv; + // Create URI from string + nsCOMPtr aURI; + rv = NS_NewURI(getter_AddRefs(aURI), aUrl); + NS_ENSURE_SUCCESS(rv, rv); + // Create new ReferrerInfo and initialize it. + nsCOMPtr referrerInfo = new mozilla::dom::ReferrerInfo(); + rv = referrerInfo->Init(aPolicy, aSendReferrer, aURI); + NS_ENSURE_SUCCESS(rv, rv); + // Set ReferrerInfo + return SetReferrerInfo(referrerInfo); +} + +NS_IMETHODIMP +HttpBaseChannel::SetEmptyRequestHeader(const nsACString& aHeader) { + const nsCString& flatHeader = PromiseFlatCString(aHeader); + + LOG(("HttpBaseChannel::SetEmptyRequestHeader [this=%p header=\"%s\"]\n", this, + flatHeader.get())); + + // Verify header names are valid HTTP tokens and header values are reasonably + // close to whats allowed in RFC 2616. + if (!nsHttp::IsValidToken(flatHeader)) { + return NS_ERROR_INVALID_ARG; + } + + return mRequestHead.SetEmptyHeader(aHeader); +} + +NS_IMETHODIMP +HttpBaseChannel::VisitRequestHeaders(nsIHttpHeaderVisitor* visitor) { + return mRequestHead.VisitHeaders(visitor); +} + +NS_IMETHODIMP +HttpBaseChannel::VisitNonDefaultRequestHeaders(nsIHttpHeaderVisitor* visitor) { + return mRequestHead.VisitHeaders(visitor, + nsHttpHeaderArray::eFilterSkipDefault); +} + +NS_IMETHODIMP +HttpBaseChannel::GetResponseHeader(const nsACString& header, + nsACString& value) { + value.Truncate(); + + if (!mResponseHead) return NS_ERROR_NOT_AVAILABLE; + + nsHttpAtom atom = nsHttp::ResolveAtom(header); + if (!atom) return NS_ERROR_NOT_AVAILABLE; + + return mResponseHead->GetHeader(atom, value); +} + +NS_IMETHODIMP +HttpBaseChannel::SetResponseHeader(const nsACString& header, + const nsACString& value, bool merge) { + LOG( + ("HttpBaseChannel::SetResponseHeader [this=%p header=\"%s\" value=\"%s\" " + "merge=%u]\n", + this, PromiseFlatCString(header).get(), PromiseFlatCString(value).get(), + merge)); + + if (!mResponseHead) return NS_ERROR_NOT_AVAILABLE; + + nsHttpAtom atom = nsHttp::ResolveAtom(header); + if (!atom) return NS_ERROR_NOT_AVAILABLE; + + // these response headers must not be changed + if (atom == nsHttp::Content_Type || atom == nsHttp::Content_Length || + atom == nsHttp::Content_Encoding || atom == nsHttp::Trailer || + atom == nsHttp::Transfer_Encoding) + return NS_ERROR_ILLEGAL_VALUE; + + StoreResponseHeadersModified(true); + + return mResponseHead->SetHeader(header, value, merge); +} + +NS_IMETHODIMP +HttpBaseChannel::VisitResponseHeaders(nsIHttpHeaderVisitor* visitor) { + if (!mResponseHead) { + return NS_ERROR_NOT_AVAILABLE; + } + return mResponseHead->VisitHeaders(visitor, + nsHttpHeaderArray::eFilterResponse); +} + +NS_IMETHODIMP +HttpBaseChannel::GetOriginalResponseHeader(const nsACString& aHeader, + nsIHttpHeaderVisitor* aVisitor) { + if (!mResponseHead) { + return NS_ERROR_NOT_AVAILABLE; + } + + nsHttpAtom atom = nsHttp::ResolveAtom(aHeader); + if (!atom) { + return NS_ERROR_NOT_AVAILABLE; + } + + return mResponseHead->GetOriginalHeader(atom, aVisitor); +} + +NS_IMETHODIMP +HttpBaseChannel::VisitOriginalResponseHeaders(nsIHttpHeaderVisitor* aVisitor) { + if (!mResponseHead) { + return NS_ERROR_NOT_AVAILABLE; + } + + return mResponseHead->VisitHeaders( + aVisitor, nsHttpHeaderArray::eFilterResponseOriginal); +} + +NS_IMETHODIMP +HttpBaseChannel::GetAllowPipelining(bool* value) { + NS_ENSURE_ARG_POINTER(value); + *value = false; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::SetAllowPipelining(bool value) { + ENSURE_CALLED_BEFORE_CONNECT(); + // nop + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetAllowSTS(bool* value) { + NS_ENSURE_ARG_POINTER(value); + *value = LoadAllowSTS(); + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::SetAllowSTS(bool value) { + ENSURE_CALLED_BEFORE_CONNECT(); + + if (!value) { + // The only channels that are allowSTS == false are OCSPRequest + // If this is an OCSP channel, and the global TRR mode is TRR_ONLY (3) + // then we set the mode for this channel as TRR_FIRST. + // We do this to prevent a TRR service channel's OCSP validation from + // blocking DNS resolution completely. + nsCOMPtr dns = do_GetService(NS_DNSSERVICE_CONTRACTID); + uint32_t trrMode = 0; + if (dns && NS_SUCCEEDED(dns->GetCurrentTrrMode(&trrMode)) && trrMode == 3) { + SetTRRMode(nsIRequest::TRR_FIRST_MODE); + } + } + + StoreAllowSTS(value); + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetRedirectionLimit(uint32_t* value) { + NS_ENSURE_ARG_POINTER(value); + *value = mRedirectionLimit; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::SetRedirectionLimit(uint32_t value) { + ENSURE_CALLED_BEFORE_CONNECT(); + + mRedirectionLimit = std::min(value, 0xff); + return NS_OK; +} + +nsresult HttpBaseChannel::OverrideSecurityInfo(nsISupports* aSecurityInfo) { + MOZ_ASSERT(!mSecurityInfo, + "This can only be called when we don't have a security info " + "object already"); + MOZ_RELEASE_ASSERT( + aSecurityInfo, + "This can only be called with a valid security info object"); + MOZ_ASSERT(!BypassServiceWorker(), + "This can only be called on channels that are not bypassing " + "interception"); + MOZ_ASSERT(LoadResponseCouldBeSynthesized(), + "This can only be called on channels that can be intercepted"); + if (mSecurityInfo) { + LOG( + ("HttpBaseChannel::OverrideSecurityInfo mSecurityInfo is null! " + "[this=%p]\n", + this)); + return NS_ERROR_UNEXPECTED; + } + if (!LoadResponseCouldBeSynthesized()) { + LOG( + ("HttpBaseChannel::OverrideSecurityInfo channel cannot be intercepted! " + "[this=%p]\n", + this)); + return NS_ERROR_UNEXPECTED; + } + + mSecurityInfo = aSecurityInfo; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::IsNoStoreResponse(bool* value) { + if (!mResponseHead) return NS_ERROR_NOT_AVAILABLE; + *value = mResponseHead->NoStore(); + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::IsNoCacheResponse(bool* value) { + if (!mResponseHead) return NS_ERROR_NOT_AVAILABLE; + *value = mResponseHead->NoCache(); + if (!*value) *value = mResponseHead->ExpiresInPast(); + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::IsPrivateResponse(bool* value) { + if (!mResponseHead) return NS_ERROR_NOT_AVAILABLE; + *value = mResponseHead->Private(); + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetResponseStatus(uint32_t* aValue) { + if (!mResponseHead) return NS_ERROR_NOT_AVAILABLE; + *aValue = mResponseHead->Status(); + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetResponseStatusText(nsACString& aValue) { + if (!mResponseHead) return NS_ERROR_NOT_AVAILABLE; + mResponseHead->StatusText(aValue); + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetRequestSucceeded(bool* aValue) { + if (!mResponseHead) return NS_ERROR_NOT_AVAILABLE; + uint32_t status = mResponseHead->Status(); + *aValue = (status / 100 == 2); + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::RedirectTo(nsIURI* targetURI) { + NS_ENSURE_ARG(targetURI); + + nsAutoCString spec; + targetURI->GetAsciiSpec(spec); + LOG(("HttpBaseChannel::RedirectTo [this=%p, uri=%s]", this, spec.get())); + LogCallingScriptLocation(this); + + // We cannot redirect after OnStartRequest of the listener + // has been called, since to redirect we have to switch channels + // and the dance with OnStartRequest et al has to start over. + // This would break the nsIStreamListener contract. + NS_ENSURE_FALSE(LoadOnStartRequestCalled(), NS_ERROR_NOT_AVAILABLE); + + mAPIRedirectToURI = targetURI; + // Only Web Extensions are allowed to redirect a channel to a data: + // URI. To avoid any bypasses after the channel was flagged by + // the WebRequst API, we are dropping the flag here. + mLoadInfo->SetAllowInsecureRedirectToDataURI(false); + + // We may want to rewrite origin allowance, hence we need an + // artificial response head. + if (!mResponseHead) { + mResponseHead.reset(new nsHttpResponseHead()); + } + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::UpgradeToSecure() { + // Upgrades are handled internally between http-on-modify-request and + // http-on-before-connect, which means upgrades are only possible during + // on-modify, or WebRequest.onBeforeRequest in Web Extensions. Once we are + // past the code path where upgrades are handled, attempting an upgrade + // will throw an error. + NS_ENSURE_TRUE(LoadUpgradableToSecure(), NS_ERROR_NOT_AVAILABLE); + + StoreUpgradeToSecure(true); + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetRequestContextID(uint64_t* aRCID) { + NS_ENSURE_ARG_POINTER(aRCID); + *aRCID = mRequestContextID; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::SetRequestContextID(uint64_t aRCID) { + mRequestContextID = aRCID; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetIsMainDocumentChannel(bool* aValue) { + NS_ENSURE_ARG_POINTER(aValue); + *aValue = IsNavigation(); + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::SetIsMainDocumentChannel(bool aValue) { + StoreForceMainDocumentChannel(aValue); + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetProtocolVersion(nsACString& aProtocolVersion) { + nsresult rv; + nsCOMPtr info = + do_QueryInterface(mSecurityInfo, &rv); + nsAutoCString protocol; + if (NS_SUCCEEDED(rv) && info && + NS_SUCCEEDED(info->GetNegotiatedNPN(protocol)) && !protocol.IsEmpty()) { + // The negotiated protocol was not empty so we can use it. + aProtocolVersion = protocol; + return NS_OK; + } + + if (mResponseHead) { + HttpVersion version = mResponseHead->Version(); + aProtocolVersion.Assign(nsHttp::GetProtocolVersion(version)); + return NS_OK; + } + + return NS_ERROR_NOT_AVAILABLE; +} + +//----------------------------------------------------------------------------- +// HttpBaseChannel::nsIHttpChannelInternal +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +HttpBaseChannel::SetTopWindowURIIfUnknown(nsIURI* aTopWindowURI) { + if (!aTopWindowURI) { + return NS_ERROR_INVALID_ARG; + } + + if (mTopWindowURI) { + LOG( + ("HttpChannelBase::SetTopWindowURIIfUnknown [this=%p] " + "mTopWindowURI is already set.\n", + this)); + return NS_ERROR_FAILURE; + } + + nsCOMPtr topWindowURI; + Unused << GetTopWindowURI(getter_AddRefs(topWindowURI)); + + // Don't modify |mTopWindowURI| if we can get one from GetTopWindowURI(). + if (topWindowURI) { + LOG( + ("HttpChannelBase::SetTopWindowURIIfUnknown [this=%p] " + "Return an error since we got a top window uri.\n", + this)); + return NS_ERROR_FAILURE; + } + + mTopWindowURI = aTopWindowURI; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetTopWindowURI(nsIURI** aTopWindowURI) { + nsCOMPtr uriBeingLoaded = + AntiTrackingUtils::MaybeGetDocumentURIBeingLoaded(this); + return GetTopWindowURI(uriBeingLoaded, aTopWindowURI); +} + +nsresult HttpBaseChannel::GetTopWindowURI(nsIURI* aURIBeingLoaded, + nsIURI** aTopWindowURI) { + nsresult rv = NS_OK; + nsCOMPtr util; + // Only compute the top window URI once. In e10s, this must be computed in the + // child. The parent gets the top window URI through HttpChannelOpenArgs. + if (!mTopWindowURI) { + util = services::GetThirdPartyUtil(); + if (!util) { + return NS_ERROR_NOT_AVAILABLE; + } + nsCOMPtr win; + rv = util->GetTopWindowForChannel(this, aURIBeingLoaded, + getter_AddRefs(win)); + if (NS_SUCCEEDED(rv)) { + rv = util->GetURIFromWindow(win, getter_AddRefs(mTopWindowURI)); +#if DEBUG + if (mTopWindowURI) { + nsCString spec; + if (NS_SUCCEEDED(mTopWindowURI->GetSpec(spec))) { + LOG(("HttpChannelBase::Setting topwindow URI spec %s [this=%p]\n", + spec.get(), this)); + } + } +#endif + } + } + NS_IF_ADDREF(*aTopWindowURI = mTopWindowURI); + return rv; +} + +NS_IMETHODIMP +HttpBaseChannel::GetDocumentURI(nsIURI** aDocumentURI) { + NS_ENSURE_ARG_POINTER(aDocumentURI); + *aDocumentURI = mDocumentURI; + NS_IF_ADDREF(*aDocumentURI); + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::SetDocumentURI(nsIURI* aDocumentURI) { + ENSURE_CALLED_BEFORE_CONNECT(); + + mDocumentURI = aDocumentURI; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetRequestVersion(uint32_t* major, uint32_t* minor) { + HttpVersion version = mRequestHead.Version(); + + if (major) { + *major = static_cast(version) / 10; + } + if (minor) { + *minor = static_cast(version) % 10; + } + + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetResponseVersion(uint32_t* major, uint32_t* minor) { + if (!mResponseHead) { + *major = *minor = 0; // we should at least be kind about it + return NS_ERROR_NOT_AVAILABLE; + } + + HttpVersion version = mResponseHead->Version(); + + if (major) { + *major = static_cast(version) / 10; + } + if (minor) { + *minor = static_cast(version) % 10; + } + + return NS_OK; +} + +void HttpBaseChannel::NotifySetCookie(const nsACString& aCookie) { + nsCOMPtr obs = services::GetObserverService(); + if (obs) { + obs->NotifyObservers(static_cast(this), + "http-on-response-set-cookie", + NS_ConvertASCIItoUTF16(aCookie).get()); + } +} + +bool HttpBaseChannel::IsBrowsingContextDiscarded() const { + if (mLoadGroup && mLoadGroup->GetIsBrowsingContextDiscarded()) { + return true; + } + + return false; +} + +// https://mikewest.github.io/corpp/#process-navigation-response +nsresult HttpBaseChannel::ProcessCrossOriginEmbedderPolicyHeader() { + nsresult rv; + if (!StaticPrefs::browser_tabs_remote_useCrossOriginEmbedderPolicy()) { + return NS_OK; + } + + // Only consider Cross-Origin-Embedder-Policy for document loads. + if (mLoadInfo->GetExternalContentPolicyType() != + ExtContentPolicy::TYPE_DOCUMENT && + mLoadInfo->GetExternalContentPolicyType() != + ExtContentPolicy::TYPE_SUBDOCUMENT) { + return NS_OK; + } + + nsILoadInfo::CrossOriginEmbedderPolicy resultPolicy = + nsILoadInfo::EMBEDDER_POLICY_NULL; + rv = GetResponseEmbedderPolicy(&resultPolicy); + if (NS_FAILED(rv)) { + return NS_OK; + } + + // https://mikewest.github.io/corpp/#abstract-opdef-process-navigation-response + if (mLoadInfo->GetExternalContentPolicyType() == + ExtContentPolicy::TYPE_SUBDOCUMENT && + mLoadInfo->GetLoadingEmbedderPolicy() != + nsILoadInfo::EMBEDDER_POLICY_NULL && + resultPolicy != nsILoadInfo::EMBEDDER_POLICY_REQUIRE_CORP) { + return NS_ERROR_BLOCKED_BY_POLICY; + } + + return NS_OK; +} + +// https://mikewest.github.io/corpp/#corp-check +nsresult HttpBaseChannel::ProcessCrossOriginResourcePolicyHeader() { + // Fetch 4.5.9 + uint32_t corsMode; + MOZ_ALWAYS_SUCCEEDS(GetCorsMode(&corsMode)); + if (corsMode != nsIHttpChannelInternal::CORS_MODE_NO_CORS) { + return NS_OK; + } + + // We only apply this for resources. + if (mLoadInfo->GetExternalContentPolicyType() == + ExtContentPolicy::TYPE_DOCUMENT || + mLoadInfo->GetExternalContentPolicyType() == + ExtContentPolicy::TYPE_WEBSOCKET) { + return NS_OK; + } + + if (mLoadInfo->GetExternalContentPolicyType() == + ExtContentPolicy::TYPE_SUBDOCUMENT) { + // COEP pref off, skip CORP checking for subdocument. + if (!StaticPrefs::browser_tabs_remote_useCrossOriginEmbedderPolicy()) { + return NS_OK; + } + // COEP 3.2.1.2 when request targets a nested browsing context then embedder + // policy value is "unsafe-none", then return allowed. + if (mLoadInfo->GetLoadingEmbedderPolicy() == + nsILoadInfo::EMBEDDER_POLICY_NULL) { + return NS_OK; + } + } + + MOZ_ASSERT(mLoadInfo->GetLoadingPrincipal(), + "Resources should always have a LoadingPrincipal"); + if (!mResponseHead) { + return NS_OK; + } + + nsAutoCString content; + Unused << mResponseHead->GetHeader(nsHttp::Cross_Origin_Resource_Policy, + content); + + if (StaticPrefs::browser_tabs_remote_useCrossOriginEmbedderPolicy()) { + // COEP 3.2.1.6 If policy is null, and embedder policy is "require-corp", + // set policy to "same-origin". + // Note that we treat invalid value as "cross-origin", which spec + // indicates. We might want to make that stricter. + if (content.IsEmpty() && mLoadInfo->GetLoadingEmbedderPolicy() == + nsILoadInfo::EMBEDDER_POLICY_REQUIRE_CORP) { + content = "same-origin"_ns; + } + } + + if (content.IsEmpty()) { + return NS_OK; + } + + nsCOMPtr channelOrigin; + nsContentUtils::GetSecurityManager()->GetChannelResultPrincipal( + this, getter_AddRefs(channelOrigin)); + + // Cross-Origin-Resource-Policy = %s"same-origin" / %s"same-site" / + // %s"cross-origin" + if (content.EqualsLiteral("same-origin")) { + if (!channelOrigin->Equals(mLoadInfo->GetLoadingPrincipal())) { + return NS_ERROR_DOM_CORP_FAILED; + } + return NS_OK; + } + if (content.EqualsLiteral("same-site")) { + nsAutoCString documentBaseDomain; + nsAutoCString resourceBaseDomain; + mLoadInfo->GetLoadingPrincipal()->GetBaseDomain(documentBaseDomain); + channelOrigin->GetBaseDomain(resourceBaseDomain); + if (documentBaseDomain != resourceBaseDomain) { + return NS_ERROR_DOM_CORP_FAILED; + } + + nsCOMPtr resourceURI = channelOrigin->GetURI(); + if (!mLoadInfo->GetLoadingPrincipal()->SchemeIs("https") && + resourceURI->SchemeIs("https")) { + return NS_ERROR_DOM_CORP_FAILED; + } + + return NS_OK; + } + + return NS_OK; +} + +// See https://gist.github.com/annevk/6f2dd8c79c77123f39797f6bdac43f3e +// This method runs steps 1-4 of the algorithm to compare +// cross-origin-opener policies +static bool CompareCrossOriginOpenerPolicies( + nsILoadInfo::CrossOriginOpenerPolicy documentPolicy, + nsIPrincipal* documentOrigin, + nsILoadInfo::CrossOriginOpenerPolicy resultPolicy, + nsIPrincipal* resultOrigin) { + if (documentPolicy == nsILoadInfo::OPENER_POLICY_UNSAFE_NONE && + resultPolicy == nsILoadInfo::OPENER_POLICY_UNSAFE_NONE) { + return true; + } + + if (documentPolicy == nsILoadInfo::OPENER_POLICY_UNSAFE_NONE || + resultPolicy == nsILoadInfo::OPENER_POLICY_UNSAFE_NONE) { + return false; + } + + if (documentPolicy == resultPolicy && documentOrigin->Equals(resultOrigin)) { + return true; + } + + return false; +} + +// This runs steps 1-5 of the algorithm when navigating a top level document. +// See https://gist.github.com/annevk/6f2dd8c79c77123f39797f6bdac43f3e +nsresult HttpBaseChannel::ComputeCrossOriginOpenerPolicyMismatch() { + StoreHasCrossOriginOpenerPolicyMismatch(false); + if (!StaticPrefs::browser_tabs_remote_useCrossOriginOpenerPolicy()) { + return NS_OK; + } + + // Only consider Cross-Origin-Opener-Policy for toplevel document loads. + if (mLoadInfo->GetExternalContentPolicyType() != + ExtContentPolicy::TYPE_DOCUMENT) { + return NS_OK; + } + + // Maybe the channel failed and we have no response head? + if (!mResponseHead) { + // Not having a response head is not a hard failure at the point where + // this method is called. + return NS_OK; + } + + RefPtr ctx; + mLoadInfo->GetBrowsingContext(getter_AddRefs(ctx)); + + // In xpcshell-tests we don't always have a browsingContext + if (!ctx) { + return NS_OK; + } + + // Get the policy of the active document, and the policy for the result. + nsILoadInfo::CrossOriginOpenerPolicy documentPolicy = ctx->GetOpenerPolicy(); + nsILoadInfo::CrossOriginOpenerPolicy resultPolicy = + nsILoadInfo::OPENER_POLICY_UNSAFE_NONE; + Unused << ComputeCrossOriginOpenerPolicy(documentPolicy, &resultPolicy); + mComputedCrossOriginOpenerPolicy = resultPolicy; + + // If bc's popup sandboxing flag set is not empty and potentialCOOP is + // non-null, then navigate bc to a network error and abort these steps. + if (resultPolicy != nsILoadInfo::OPENER_POLICY_UNSAFE_NONE && + GetHasNonEmptySandboxingFlag()) { + LOG(( + "HttpBaseChannel::ComputeCrossOriginOpenerPolicyMismatch network error " + "for non empty sandboxing and non null COOP")); + return NS_ERROR_BLOCKED_BY_POLICY; + } + + // In xpcshell-tests we don't always have a current window global + if (!ctx->Canonical()->GetCurrentWindowGlobal()) { + return NS_OK; + } + + // We use the top window principal as the documentOrigin + nsCOMPtr documentOrigin = + ctx->Canonical()->GetCurrentWindowGlobal()->DocumentPrincipal(); + nsCOMPtr resultOrigin; + + nsContentUtils::GetSecurityManager()->GetChannelResultPrincipal( + this, getter_AddRefs(resultOrigin)); + + bool compareResult = CompareCrossOriginOpenerPolicies( + documentPolicy, documentOrigin, resultPolicy, resultOrigin); + + if (LOG_ENABLED()) { + LOG( + ("HttpBaseChannel::HasCrossOriginOpenerPolicyMismatch - " + "doc:%d result:%d - compare:%d\n", + documentPolicy, resultPolicy, compareResult)); + nsAutoCString docOrigin("(null)"); + nsCOMPtr uri = documentOrigin->GetURI(); + if (uri) { + uri->GetSpec(docOrigin); + } + nsAutoCString resOrigin("(null)"); + uri = resultOrigin->GetURI(); + if (uri) { + uri->GetSpec(resOrigin); + } + LOG(("doc origin:%s - res origin: %s\n", docOrigin.get(), resOrigin.get())); + } + + if (compareResult) { + return NS_OK; + } + + // If one of the following is false: + // - document's policy is same-origin-allow-popups + // - resultPolicy is null + // - doc is the initial about:blank document + // then we have a mismatch. + + if (documentPolicy != nsILoadInfo::OPENER_POLICY_SAME_ORIGIN_ALLOW_POPUPS) { + StoreHasCrossOriginOpenerPolicyMismatch(true); + return NS_OK; + } + + if (resultPolicy != nsILoadInfo::OPENER_POLICY_UNSAFE_NONE) { + StoreHasCrossOriginOpenerPolicyMismatch(true); + return NS_OK; + } + + if (!ctx->Canonical()->GetCurrentWindowGlobal()->IsInitialDocument()) { + StoreHasCrossOriginOpenerPolicyMismatch(true); + return NS_OK; + } + + return NS_OK; +} + +enum class Report { Error, Warning }; + +// Helper Function to report messages to the console when the loaded +// script had a wrong MIME type. +void ReportMimeTypeMismatch(HttpBaseChannel* aChannel, const char* aMessageName, + nsIURI* aURI, const nsACString& aContentType, + Report report) { + NS_ConvertUTF8toUTF16 spec(aURI->GetSpecOrDefault()); + NS_ConvertUTF8toUTF16 contentType(aContentType); + + aChannel->LogMimeTypeMismatch(nsCString(aMessageName), + report == Report::Warning, spec, contentType); +} + +// Check and potentially enforce X-Content-Type-Options: nosniff +nsresult ProcessXCTO(HttpBaseChannel* aChannel, nsIURI* aURI, + nsHttpResponseHead* aResponseHead, + nsILoadInfo* aLoadInfo) { + if (!aURI || !aResponseHead || !aLoadInfo) { + // if there is no uri, no response head or no loadInfo, then there is + // nothing to do + return NS_OK; + } + + // 1) Query the XCTO header and check if 'nosniff' is the first value. + nsAutoCString contentTypeOptionsHeader; + if (!aResponseHead->GetContentTypeOptionsHeader(contentTypeOptionsHeader)) { + // if failed to get XCTO header, then there is nothing to do. + return NS_OK; + } + + // let's compare the header (ignoring case) + // e.g. "NoSniFF" -> "nosniff" + // if it's not 'nosniff' then there is nothing to do here + if (!contentTypeOptionsHeader.EqualsIgnoreCase("nosniff")) { + // since we are getting here, the XCTO header was sent; + // a non matching value most likely means a mistake happenend; + // e.g. sending 'nosnif' instead of 'nosniff', let's log a warning. + AutoTArray params; + CopyUTF8toUTF16(contentTypeOptionsHeader, *params.AppendElement()); + RefPtr doc; + aLoadInfo->GetLoadingDocument(getter_AddRefs(doc)); + nsContentUtils::ReportToConsole(nsIScriptError::warningFlag, "XCTO"_ns, doc, + nsContentUtils::eSECURITY_PROPERTIES, + "XCTOHeaderValueMissing", params); + return NS_OK; + } + + // 2) Query the content type from the channel + nsAutoCString contentType; + aResponseHead->ContentType(contentType); + + // 3) Compare the expected MIME type with the actual type + if (aLoadInfo->GetExternalContentPolicyType() == + ExtContentPolicy::TYPE_STYLESHEET) { + if (contentType.EqualsLiteral(TEXT_CSS)) { + return NS_OK; + } + ReportMimeTypeMismatch(aChannel, "MimeTypeMismatch2", aURI, contentType, + Report::Error); + return NS_ERROR_CORRUPTED_CONTENT; + } + + if (aLoadInfo->GetExternalContentPolicyType() == + ExtContentPolicy::TYPE_SCRIPT) { + if (nsContentUtils::IsJavascriptMIMEType( + NS_ConvertUTF8toUTF16(contentType))) { + return NS_OK; + } + ReportMimeTypeMismatch(aChannel, "MimeTypeMismatch2", aURI, contentType, + Report::Error); + return NS_ERROR_CORRUPTED_CONTENT; + } + + auto policyType = aLoadInfo->GetExternalContentPolicyType(); + if (policyType == ExtContentPolicy::TYPE_DOCUMENT || + policyType == ExtContentPolicy::TYPE_SUBDOCUMENT) { + // If the header XCTO nosniff is set for any browsing context, then + // we set the skipContentSniffing flag on the Loadinfo. Within + // GetMIMETypeFromContent we then bail early and do not do any sniffing. + aLoadInfo->SetSkipContentSniffing(true); + return NS_OK; + } + + return NS_OK; +} + +// Ensure that a load of type script has correct MIME type +nsresult EnsureMIMEOfScript(HttpBaseChannel* aChannel, nsIURI* aURI, + nsHttpResponseHead* aResponseHead, + nsILoadInfo* aLoadInfo) { + if (!aURI || !aResponseHead || !aLoadInfo) { + // if there is no uri, no response head or no loadInfo, then there is + // nothing to do + return NS_OK; + } + + if (aLoadInfo->GetExternalContentPolicyType() != + ExtContentPolicy::TYPE_SCRIPT) { + // if this is not a script load, then there is nothing to do + return NS_OK; + } + + nsAutoCString contentType; + aResponseHead->ContentType(contentType); + NS_ConvertUTF8toUTF16 typeString(contentType); + + if (nsContentUtils::IsJavascriptMIMEType(typeString)) { + // script load has type script + AccumulateCategorical( + Telemetry::LABELS_SCRIPT_BLOCK_INCORRECT_MIME_3::javaScript); + return NS_OK; + } + + switch (aLoadInfo->InternalContentPolicyType()) { + case nsIContentPolicy::TYPE_SCRIPT: + case nsIContentPolicy::TYPE_INTERNAL_SCRIPT: + case nsIContentPolicy::TYPE_INTERNAL_SCRIPT_PRELOAD: + case nsIContentPolicy::TYPE_INTERNAL_MODULE: + case nsIContentPolicy::TYPE_INTERNAL_MODULE_PRELOAD: + case nsIContentPolicy::TYPE_INTERNAL_CHROMEUTILS_COMPILED_SCRIPT: + case nsIContentPolicy::TYPE_INTERNAL_FRAME_MESSAGEMANAGER_SCRIPT: + AccumulateCategorical( + Telemetry::LABELS_SCRIPT_BLOCK_INCORRECT_MIME_3::script_load); + break; + case nsIContentPolicy::TYPE_INTERNAL_WORKER: + case nsIContentPolicy::TYPE_INTERNAL_SHARED_WORKER: + AccumulateCategorical( + Telemetry::LABELS_SCRIPT_BLOCK_INCORRECT_MIME_3::worker_load); + break; + case nsIContentPolicy::TYPE_INTERNAL_SERVICE_WORKER: + AccumulateCategorical( + Telemetry::LABELS_SCRIPT_BLOCK_INCORRECT_MIME_3::serviceworker_load); + break; + case nsIContentPolicy::TYPE_INTERNAL_WORKER_IMPORT_SCRIPTS: + AccumulateCategorical( + Telemetry::LABELS_SCRIPT_BLOCK_INCORRECT_MIME_3::importScript_load); + break; + case nsIContentPolicy::TYPE_INTERNAL_AUDIOWORKLET: + case nsIContentPolicy::TYPE_INTERNAL_PAINTWORKLET: + AccumulateCategorical( + Telemetry::LABELS_SCRIPT_BLOCK_INCORRECT_MIME_3::worklet_load); + break; + default: + MOZ_ASSERT_UNREACHABLE("unexpected script type"); + break; + } + + bool isPrivateWin = aLoadInfo->GetOriginAttributes().mPrivateBrowsingId > 0; + bool isSameOrigin = false; + aLoadInfo->GetLoadingPrincipal()->IsSameOrigin(aURI, isPrivateWin, + &isSameOrigin); + if (isSameOrigin) { + // same origin + AccumulateCategorical( + Telemetry::LABELS_SCRIPT_BLOCK_INCORRECT_MIME_3::same_origin); + } else { + bool cors = false; + nsAutoCString corsOrigin; + nsresult rv = aResponseHead->GetHeader( + nsHttp::ResolveAtom("Access-Control-Allow-Origin"), corsOrigin); + if (NS_SUCCEEDED(rv)) { + if (corsOrigin.Equals("*")) { + cors = true; + } else { + nsCOMPtr corsOriginURI; + rv = NS_NewURI(getter_AddRefs(corsOriginURI), corsOrigin); + if (NS_SUCCEEDED(rv)) { + bool isPrivateWin = + aLoadInfo->GetOriginAttributes().mPrivateBrowsingId > 0; + bool isSameOrigin = false; + aLoadInfo->GetLoadingPrincipal()->IsSameOrigin( + corsOriginURI, isPrivateWin, &isSameOrigin); + if (isSameOrigin) { + cors = true; + } + } + } + } + if (cors) { + // cors origin + AccumulateCategorical( + Telemetry::LABELS_SCRIPT_BLOCK_INCORRECT_MIME_3::CORS_origin); + } else { + // cross origin + AccumulateCategorical( + Telemetry::LABELS_SCRIPT_BLOCK_INCORRECT_MIME_3::cross_origin); + } + } + + bool block = false; + if (StringBeginsWith(contentType, "image/"_ns)) { + // script load has type image + AccumulateCategorical( + Telemetry::LABELS_SCRIPT_BLOCK_INCORRECT_MIME_3::image); + block = true; + } else if (StringBeginsWith(contentType, "audio/"_ns)) { + // script load has type audio + AccumulateCategorical( + Telemetry::LABELS_SCRIPT_BLOCK_INCORRECT_MIME_3::audio); + block = true; + } else if (StringBeginsWith(contentType, "video/"_ns)) { + // script load has type video + AccumulateCategorical( + Telemetry::LABELS_SCRIPT_BLOCK_INCORRECT_MIME_3::video); + block = true; + } else if (StringBeginsWith(contentType, "text/csv"_ns)) { + // script load has type text/csv + AccumulateCategorical( + Telemetry::LABELS_SCRIPT_BLOCK_INCORRECT_MIME_3::text_csv); + block = true; + } + + if (block) { + ReportMimeTypeMismatch(aChannel, "BlockScriptWithWrongMimeType2", aURI, + contentType, Report::Error); + return NS_ERROR_CORRUPTED_CONTENT; + } + + if (StringBeginsWith(contentType, "text/plain"_ns)) { + // script load has type text/plain + AccumulateCategorical( + Telemetry::LABELS_SCRIPT_BLOCK_INCORRECT_MIME_3::text_plain); + } else if (StringBeginsWith(contentType, "text/xml"_ns)) { + // script load has type text/xml + AccumulateCategorical( + Telemetry::LABELS_SCRIPT_BLOCK_INCORRECT_MIME_3::text_xml); + } else if (StringBeginsWith(contentType, "application/octet-stream"_ns)) { + // script load has type application/octet-stream + AccumulateCategorical( + Telemetry::LABELS_SCRIPT_BLOCK_INCORRECT_MIME_3::app_octet_stream); + } else if (StringBeginsWith(contentType, "application/xml"_ns)) { + // script load has type application/xml + AccumulateCategorical( + Telemetry::LABELS_SCRIPT_BLOCK_INCORRECT_MIME_3::app_xml); + } else if (StringBeginsWith(contentType, "application/json"_ns)) { + // script load has type application/json + AccumulateCategorical( + Telemetry::LABELS_SCRIPT_BLOCK_INCORRECT_MIME_3::app_json); + } else if (StringBeginsWith(contentType, "text/json"_ns)) { + // script load has type text/json + AccumulateCategorical( + Telemetry::LABELS_SCRIPT_BLOCK_INCORRECT_MIME_3::text_json); + } else if (StringBeginsWith(contentType, "text/html"_ns)) { + // script load has type text/html + AccumulateCategorical( + Telemetry::LABELS_SCRIPT_BLOCK_INCORRECT_MIME_3::text_html); + } else if (contentType.IsEmpty()) { + // script load has no type + AccumulateCategorical( + Telemetry::LABELS_SCRIPT_BLOCK_INCORRECT_MIME_3::empty); + } else { + // script load has unknown type + AccumulateCategorical( + Telemetry::LABELS_SCRIPT_BLOCK_INCORRECT_MIME_3::unknown); + } + + // We restrict importScripts() in worker code to JavaScript MIME types. + nsContentPolicyType internalType = aLoadInfo->InternalContentPolicyType(); + if (internalType == nsIContentPolicy::TYPE_INTERNAL_WORKER_IMPORT_SCRIPTS) { + ReportMimeTypeMismatch(aChannel, "BlockImportScriptsWithWrongMimeType", + aURI, contentType, Report::Error); + return NS_ERROR_CORRUPTED_CONTENT; + } + + if (internalType == nsIContentPolicy::TYPE_INTERNAL_WORKER || + internalType == nsIContentPolicy::TYPE_INTERNAL_SHARED_WORKER) { + // Do not block the load if the feature is not enabled. + if (!StaticPrefs::security_block_Worker_with_wrong_mime()) { + return NS_OK; + } + + ReportMimeTypeMismatch(aChannel, "BlockWorkerWithWrongMimeType", aURI, + contentType, Report::Error); + return NS_ERROR_CORRUPTED_CONTENT; + } + + // ES6 modules require a strict MIME type check. + if (internalType == nsIContentPolicy::TYPE_INTERNAL_MODULE || + internalType == nsIContentPolicy::TYPE_INTERNAL_MODULE_PRELOAD) { + ReportMimeTypeMismatch(aChannel, "BlockModuleWithWrongMimeType", aURI, + contentType, Report::Error); + return NS_ERROR_CORRUPTED_CONTENT; + } + + return NS_OK; +} + +// Warn when a load of type script uses a wrong MIME type and +// wasn't blocked by EnsureMIMEOfScript or ProcessXCTO. +void WarnWrongMIMEOfScript(HttpBaseChannel* aChannel, nsIURI* aURI, + nsHttpResponseHead* aResponseHead, + nsILoadInfo* aLoadInfo) { + if (!aURI || !aResponseHead || !aLoadInfo) { + // If there is no uri, no response head or no loadInfo, then there is + // nothing to do. + return; + } + + if (aLoadInfo->GetExternalContentPolicyType() != + ExtContentPolicy::TYPE_SCRIPT) { + // If this is not a script load, then there is nothing to do. + return; + } + + bool succeeded; + MOZ_ALWAYS_SUCCEEDS(aChannel->GetRequestSucceeded(&succeeded)); + if (!succeeded) { + // Do not warn for failed loads: HTTP error pages are usually in HTML. + return; + } + + nsAutoCString contentType; + aResponseHead->ContentType(contentType); + NS_ConvertUTF8toUTF16 typeString(contentType); + if (!nsContentUtils::IsJavascriptMIMEType(typeString)) { + ReportMimeTypeMismatch(aChannel, "WarnScriptWithWrongMimeType", aURI, + contentType, Report::Warning); + } +} + +nsresult HttpBaseChannel::ValidateMIMEType() { + nsresult rv = EnsureMIMEOfScript(this, mURI, mResponseHead.get(), mLoadInfo); + if (NS_FAILED(rv)) { + return rv; + } + + rv = ProcessXCTO(this, mURI, mResponseHead.get(), mLoadInfo); + if (NS_FAILED(rv)) { + return rv; + } + + WarnWrongMIMEOfScript(this, mURI, mResponseHead.get(), mLoadInfo); + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::SetCookie(const nsACString& aCookieHeader) { + if (mLoadFlags & LOAD_ANONYMOUS) return NS_OK; + + if (IsBrowsingContextDiscarded()) { + return NS_OK; + } + + // empty header isn't an error + if (aCookieHeader.IsEmpty()) { + return NS_OK; + } + + nsICookieService* cs = gHttpHandler->GetCookieService(); + NS_ENSURE_TRUE(cs, NS_ERROR_FAILURE); + + nsresult rv = cs->SetCookieStringFromHttp(mURI, aCookieHeader, this); + if (NS_SUCCEEDED(rv)) { + NotifySetCookie(aCookieHeader); + } + return rv; +} + +NS_IMETHODIMP +HttpBaseChannel::GetThirdPartyFlags(uint32_t* aFlags) { + *aFlags = LoadThirdPartyFlags(); + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::SetThirdPartyFlags(uint32_t aFlags) { + ENSURE_CALLED_BEFORE_ASYNC_OPEN(); + + StoreThirdPartyFlags(aFlags); + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetForceAllowThirdPartyCookie(bool* aForce) { + *aForce = !!(LoadThirdPartyFlags() & + nsIHttpChannelInternal::THIRD_PARTY_FORCE_ALLOW); + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::SetForceAllowThirdPartyCookie(bool aForce) { + ENSURE_CALLED_BEFORE_ASYNC_OPEN(); + + if (aForce) + StoreThirdPartyFlags(LoadThirdPartyFlags() | + nsIHttpChannelInternal::THIRD_PARTY_FORCE_ALLOW); + else + StoreThirdPartyFlags(LoadThirdPartyFlags() & + ~nsIHttpChannelInternal::THIRD_PARTY_FORCE_ALLOW); + + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetCanceled(bool* aCanceled) { + *aCanceled = mCanceled; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetChannelIsForDownload(bool* aChannelIsForDownload) { + *aChannelIsForDownload = LoadChannelIsForDownload(); + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::SetChannelIsForDownload(bool aChannelIsForDownload) { + StoreChannelIsForDownload(aChannelIsForDownload); + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::SetCacheKeysRedirectChain(nsTArray* cacheKeys) { + mRedirectedCachekeys = WrapUnique(cacheKeys); + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetLocalAddress(nsACString& addr) { + if (mSelfAddr.raw.family == PR_AF_UNSPEC) return NS_ERROR_NOT_AVAILABLE; + + addr.SetLength(kIPv6CStrBufSize); + mSelfAddr.ToStringBuffer(addr.BeginWriting(), kIPv6CStrBufSize); + addr.SetLength(strlen(addr.BeginReading())); + + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::TakeAllSecurityMessages( + nsCOMArray& aMessages) { + MOZ_ASSERT(NS_IsMainThread()); + + aMessages.Clear(); + for (auto pair : mSecurityConsoleMessages) { + nsresult rv; + nsCOMPtr message = + do_CreateInstance(NS_SECURITY_CONSOLE_MESSAGE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + message->SetTag(pair.first); + message->SetCategory(pair.second); + aMessages.AppendElement(message); + } + + MOZ_ASSERT(mSecurityConsoleMessages.Length() == aMessages.Length()); + mSecurityConsoleMessages.Clear(); + + return NS_OK; +} + +/* Please use this method with care. This can cause the message + * queue to grow large and cause the channel to take up a lot + * of memory. Use only static string messages and do not add + * server side data to the queue, as that can be large. + * Add only a limited number of messages to the queue to keep + * the channel size down and do so only in rare erroneous situations. + * More information can be found here: + * https://bugzilla.mozilla.org/show_bug.cgi?id=846918 + */ +nsresult HttpBaseChannel::AddSecurityMessage( + const nsAString& aMessageTag, const nsAString& aMessageCategory) { + MOZ_ASSERT(NS_IsMainThread()); + + nsresult rv; + + // nsSecurityConsoleMessage is not thread-safe refcounted. + // Delay the object construction until requested. + // See TakeAllSecurityMessages() + std::pair pair(aMessageTag, aMessageCategory); + mSecurityConsoleMessages.AppendElement(std::move(pair)); + + nsCOMPtr console( + do_GetService(NS_CONSOLESERVICE_CONTRACTID)); + if (!console) { + return NS_ERROR_FAILURE; + } + + nsCOMPtr loadInfo = LoadInfo(); + + auto innerWindowID = loadInfo->GetInnerWindowID(); + + nsAutoString errorText; + rv = nsContentUtils::GetLocalizedString( + nsContentUtils::eSECURITY_PROPERTIES, + NS_ConvertUTF16toUTF8(aMessageTag).get(), errorText); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr error(do_CreateInstance(NS_SCRIPTERROR_CONTRACTID)); + error->InitWithSourceURI( + errorText, mURI, u""_ns, 0, 0, nsIScriptError::warningFlag, + NS_ConvertUTF16toUTF8(aMessageCategory), innerWindowID); + + console->LogMessage(error); + + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetLocalPort(int32_t* port) { + NS_ENSURE_ARG_POINTER(port); + + if (mSelfAddr.raw.family == PR_AF_INET) { + *port = (int32_t)ntohs(mSelfAddr.inet.port); + } else if (mSelfAddr.raw.family == PR_AF_INET6) { + *port = (int32_t)ntohs(mSelfAddr.inet6.port); + } else + return NS_ERROR_NOT_AVAILABLE; + + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetRemoteAddress(nsACString& addr) { + if (mPeerAddr.raw.family == PR_AF_UNSPEC) return NS_ERROR_NOT_AVAILABLE; + + addr.SetLength(kIPv6CStrBufSize); + mPeerAddr.ToStringBuffer(addr.BeginWriting(), kIPv6CStrBufSize); + addr.SetLength(strlen(addr.BeginReading())); + + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetRemotePort(int32_t* port) { + NS_ENSURE_ARG_POINTER(port); + + if (mPeerAddr.raw.family == PR_AF_INET) { + *port = (int32_t)ntohs(mPeerAddr.inet.port); + } else if (mPeerAddr.raw.family == PR_AF_INET6) { + *port = (int32_t)ntohs(mPeerAddr.inet6.port); + } else + return NS_ERROR_NOT_AVAILABLE; + + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::HTTPUpgrade(const nsACString& aProtocolName, + nsIHttpUpgradeListener* aListener) { + NS_ENSURE_ARG(!aProtocolName.IsEmpty()); + NS_ENSURE_ARG_POINTER(aListener); + + mUpgradeProtocol = aProtocolName; + mUpgradeProtocolCallback = aListener; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetOnlyConnect(bool* aOnlyConnect) { + NS_ENSURE_ARG_POINTER(aOnlyConnect); + + *aOnlyConnect = mCaps & NS_HTTP_CONNECT_ONLY; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::SetConnectOnly() { + ENSURE_CALLED_BEFORE_CONNECT(); + + if (!mUpgradeProtocolCallback) { + return NS_ERROR_FAILURE; + } + + mCaps |= NS_HTTP_CONNECT_ONLY; + mProxyResolveFlags = nsIProtocolProxyService::RESOLVE_PREFER_HTTPS_PROXY | + nsIProtocolProxyService::RESOLVE_ALWAYS_TUNNEL; + return SetLoadFlags(nsIRequest::INHIBIT_CACHING | nsIChannel::LOAD_ANONYMOUS | + nsIRequest::LOAD_BYPASS_CACHE | + nsIChannel::LOAD_BYPASS_SERVICE_WORKER); +} + +NS_IMETHODIMP +HttpBaseChannel::GetAllowSpdy(bool* aAllowSpdy) { + NS_ENSURE_ARG_POINTER(aAllowSpdy); + + *aAllowSpdy = LoadAllowSpdy(); + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::SetAllowSpdy(bool aAllowSpdy) { + StoreAllowSpdy(aAllowSpdy); + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetAllowHttp3(bool* aAllowHttp3) { + NS_ENSURE_ARG_POINTER(aAllowHttp3); + + *aAllowHttp3 = LoadAllowHttp3(); + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::SetAllowHttp3(bool aAllowHttp3) { + StoreAllowHttp3(aAllowHttp3); + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetAllowAltSvc(bool* aAllowAltSvc) { + NS_ENSURE_ARG_POINTER(aAllowAltSvc); + + *aAllowAltSvc = LoadAllowAltSvc(); + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::SetAllowAltSvc(bool aAllowAltSvc) { + StoreAllowAltSvc(aAllowAltSvc); + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetBeConservative(bool* aBeConservative) { + NS_ENSURE_ARG_POINTER(aBeConservative); + + *aBeConservative = LoadBeConservative(); + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::SetBeConservative(bool aBeConservative) { + StoreBeConservative(aBeConservative); + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetIsTRRServiceChannel(bool* aIsTRRServiceChannel) { + NS_ENSURE_ARG_POINTER(aIsTRRServiceChannel); + + *aIsTRRServiceChannel = LoadIsTRRServiceChannel(); + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::SetIsTRRServiceChannel(bool aIsTRRServiceChannel) { + StoreIsTRRServiceChannel(aIsTRRServiceChannel); + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetIsResolvedByTRR(bool* aResolvedByTRR) { + NS_ENSURE_ARG_POINTER(aResolvedByTRR); + *aResolvedByTRR = LoadResolvedByTRR(); + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetTlsFlags(uint32_t* aTlsFlags) { + NS_ENSURE_ARG_POINTER(aTlsFlags); + + *aTlsFlags = mTlsFlags; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::SetTlsFlags(uint32_t aTlsFlags) { + mTlsFlags = aTlsFlags; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetApiRedirectToURI(nsIURI** aResult) { + NS_ENSURE_ARG_POINTER(aResult); + NS_IF_ADDREF(*aResult = mAPIRedirectToURI); + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetResponseTimeoutEnabled(bool* aEnable) { + if (NS_WARN_IF(!aEnable)) { + return NS_ERROR_NULL_POINTER; + } + *aEnable = LoadResponseTimeoutEnabled(); + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::SetResponseTimeoutEnabled(bool aEnable) { + StoreResponseTimeoutEnabled(aEnable); + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetInitialRwin(uint32_t* aRwin) { + if (NS_WARN_IF(!aRwin)) { + return NS_ERROR_NULL_POINTER; + } + *aRwin = mInitialRwin; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::SetInitialRwin(uint32_t aRwin) { + ENSURE_CALLED_BEFORE_CONNECT(); + mInitialRwin = aRwin; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::ForcePending(bool aForcePending) { + StoreForcePending(aForcePending); + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetLastModifiedTime(PRTime* lastModifiedTime) { + if (!mResponseHead) return NS_ERROR_NOT_AVAILABLE; + uint32_t lastMod; + nsresult rv = mResponseHead->GetLastModifiedValue(&lastMod); + NS_ENSURE_SUCCESS(rv, rv); + *lastModifiedTime = lastMod; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetCorsIncludeCredentials(bool* aInclude) { + *aInclude = LoadCorsIncludeCredentials(); + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::SetCorsIncludeCredentials(bool aInclude) { + StoreCorsIncludeCredentials(aInclude); + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetCorsMode(uint32_t* aMode) { + *aMode = mCorsMode; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::SetCorsMode(uint32_t aMode) { + mCorsMode = aMode; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetRedirectMode(uint32_t* aMode) { + *aMode = mRedirectMode; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::SetRedirectMode(uint32_t aMode) { + mRedirectMode = aMode; + return NS_OK; +} + +namespace { + +bool ContainsAllFlags(uint32_t aLoadFlags, uint32_t aMask) { + return (aLoadFlags & aMask) == aMask; +} + +} // anonymous namespace + +NS_IMETHODIMP +HttpBaseChannel::GetFetchCacheMode(uint32_t* aFetchCacheMode) { + NS_ENSURE_ARG_POINTER(aFetchCacheMode); + + // Otherwise try to guess an appropriate cache mode from the load flags. + if (ContainsAllFlags(mLoadFlags, INHIBIT_CACHING | LOAD_BYPASS_CACHE)) { + *aFetchCacheMode = nsIHttpChannelInternal::FETCH_CACHE_MODE_NO_STORE; + } else if (ContainsAllFlags(mLoadFlags, LOAD_BYPASS_CACHE)) { + *aFetchCacheMode = nsIHttpChannelInternal::FETCH_CACHE_MODE_RELOAD; + } else if (ContainsAllFlags(mLoadFlags, VALIDATE_ALWAYS)) { + *aFetchCacheMode = nsIHttpChannelInternal::FETCH_CACHE_MODE_NO_CACHE; + } else if (ContainsAllFlags( + mLoadFlags, + VALIDATE_NEVER | nsICachingChannel::LOAD_ONLY_FROM_CACHE)) { + *aFetchCacheMode = nsIHttpChannelInternal::FETCH_CACHE_MODE_ONLY_IF_CACHED; + } else if (ContainsAllFlags(mLoadFlags, VALIDATE_NEVER)) { + *aFetchCacheMode = nsIHttpChannelInternal::FETCH_CACHE_MODE_FORCE_CACHE; + } else { + *aFetchCacheMode = nsIHttpChannelInternal::FETCH_CACHE_MODE_DEFAULT; + } + + return NS_OK; +} + +namespace { + +void SetCacheFlags(uint32_t& aLoadFlags, uint32_t aFlags) { + // First, clear any possible cache related flags. + uint32_t allPossibleFlags = + nsIRequest::INHIBIT_CACHING | nsIRequest::LOAD_BYPASS_CACHE | + nsIRequest::VALIDATE_ALWAYS | nsIRequest::LOAD_FROM_CACHE | + nsICachingChannel::LOAD_ONLY_FROM_CACHE; + aLoadFlags &= ~allPossibleFlags; + + // Then set the new flags. + aLoadFlags |= aFlags; +} + +} // anonymous namespace + +NS_IMETHODIMP +HttpBaseChannel::SetFetchCacheMode(uint32_t aFetchCacheMode) { + ENSURE_CALLED_BEFORE_CONNECT(); + + // Now, set the load flags that implement each cache mode. + switch (aFetchCacheMode) { + case nsIHttpChannelInternal::FETCH_CACHE_MODE_DEFAULT: + // The "default" mode means to use the http cache normally and + // respect any http cache-control headers. We effectively want + // to clear our cache related load flags. + SetCacheFlags(mLoadFlags, 0); + break; + case nsIHttpChannelInternal::FETCH_CACHE_MODE_NO_STORE: + // no-store means don't consult the cache on the way to the network, and + // don't store the response in the cache even if it's cacheable. + SetCacheFlags(mLoadFlags, INHIBIT_CACHING | LOAD_BYPASS_CACHE); + break; + case nsIHttpChannelInternal::FETCH_CACHE_MODE_RELOAD: + // reload means don't consult the cache on the way to the network, but + // do store the response in the cache if possible. + SetCacheFlags(mLoadFlags, LOAD_BYPASS_CACHE); + break; + case nsIHttpChannelInternal::FETCH_CACHE_MODE_NO_CACHE: + // no-cache means always validate what's in the cache. + SetCacheFlags(mLoadFlags, VALIDATE_ALWAYS); + break; + case nsIHttpChannelInternal::FETCH_CACHE_MODE_FORCE_CACHE: + // force-cache means don't validate unless if the response would vary. + SetCacheFlags(mLoadFlags, VALIDATE_NEVER); + break; + case nsIHttpChannelInternal::FETCH_CACHE_MODE_ONLY_IF_CACHED: + // only-if-cached means only from cache, no network, no validation, + // generate a network error if the document was't in the cache. The + // privacy implications of these flags (making it fast/easy to check if + // the user has things in their cache without any network traffic side + // effects) are addressed in the Request constructor which + // enforces/requires same-origin request mode. + SetCacheFlags(mLoadFlags, + VALIDATE_NEVER | nsICachingChannel::LOAD_ONLY_FROM_CACHE); + break; + } + +#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED + uint32_t finalMode = 0; + MOZ_ALWAYS_SUCCEEDS(GetFetchCacheMode(&finalMode)); + MOZ_DIAGNOSTIC_ASSERT(finalMode == aFetchCacheMode); +#endif // MOZ_DIAGNOSTIC_ASSERT_ENABLED + + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::SetIntegrityMetadata(const nsAString& aIntegrityMetadata) { + mIntegrityMetadata = aIntegrityMetadata; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetIntegrityMetadata(nsAString& aIntegrityMetadata) { + aIntegrityMetadata = mIntegrityMetadata; + return NS_OK; +} + +//----------------------------------------------------------------------------- +// HttpBaseChannel::nsISupportsPriority +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +HttpBaseChannel::GetPriority(int32_t* value) { + *value = mPriority; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::AdjustPriority(int32_t delta) { + return SetPriority(mPriority + delta); +} + +//----------------------------------------------------------------------------- +// HttpBaseChannel::nsIResumableChannel +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +HttpBaseChannel::GetEntityID(nsACString& aEntityID) { + // Don't return an entity ID for Non-GET requests which require + // additional data + if (!mRequestHead.IsGet()) { + return NS_ERROR_NOT_RESUMABLE; + } + + uint64_t size = UINT64_MAX; + nsAutoCString etag, lastmod; + if (mResponseHead) { + // Don't return an entity if the server sent the following header: + // Accept-Ranges: none + // Not sending the Accept-Ranges header means we can still try + // sending range requests. + nsAutoCString acceptRanges; + Unused << mResponseHead->GetHeader(nsHttp::Accept_Ranges, acceptRanges); + if (!acceptRanges.IsEmpty() && + !nsHttp::FindToken(acceptRanges.get(), "bytes", + HTTP_HEADER_VALUE_SEPS)) { + return NS_ERROR_NOT_RESUMABLE; + } + + size = mResponseHead->TotalEntitySize(); + Unused << mResponseHead->GetHeader(nsHttp::Last_Modified, lastmod); + Unused << mResponseHead->GetHeader(nsHttp::ETag, etag); + } + nsCString entityID; + NS_EscapeURL(etag.BeginReading(), etag.Length(), + esc_AlwaysCopy | esc_FileBaseName | esc_Forced, entityID); + entityID.Append('/'); + entityID.AppendInt(int64_t(size)); + entityID.Append('/'); + entityID.Append(lastmod); + // NOTE: Appending lastmod as the last part avoids having to escape it + + aEntityID = entityID; + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// HttpBaseChannel::nsIConsoleReportCollector +//----------------------------------------------------------------------------- + +void HttpBaseChannel::AddConsoleReport( + uint32_t aErrorFlags, const nsACString& aCategory, + nsContentUtils::PropertiesFile aPropertiesFile, + const nsACString& aSourceFileURI, uint32_t aLineNumber, + uint32_t aColumnNumber, const nsACString& aMessageName, + const nsTArray& aStringParams) { + mReportCollector->AddConsoleReport(aErrorFlags, aCategory, aPropertiesFile, + aSourceFileURI, aLineNumber, aColumnNumber, + aMessageName, aStringParams); + + // If this channel is already part of a loadGroup, we can flush this console + // report immediately. + HttpBaseChannel::MaybeFlushConsoleReports(); +} + +void HttpBaseChannel::FlushReportsToConsole(uint64_t aInnerWindowID, + ReportAction aAction) { + mReportCollector->FlushReportsToConsole(aInnerWindowID, aAction); +} + +void HttpBaseChannel::FlushReportsToConsoleForServiceWorkerScope( + const nsACString& aScope, ReportAction aAction) { + mReportCollector->FlushReportsToConsoleForServiceWorkerScope(aScope, aAction); +} + +void HttpBaseChannel::FlushConsoleReports(dom::Document* aDocument, + ReportAction aAction) { + mReportCollector->FlushConsoleReports(aDocument, aAction); +} + +void HttpBaseChannel::FlushConsoleReports(nsILoadGroup* aLoadGroup, + ReportAction aAction) { + mReportCollector->FlushConsoleReports(aLoadGroup, aAction); +} + +void HttpBaseChannel::FlushConsoleReports( + nsIConsoleReportCollector* aCollector) { + mReportCollector->FlushConsoleReports(aCollector); +} + +void HttpBaseChannel::StealConsoleReports( + nsTArray& aReports) { + mReportCollector->StealConsoleReports(aReports); +} + +void HttpBaseChannel::ClearConsoleReports() { + mReportCollector->ClearConsoleReports(); +} + +nsIPrincipal* HttpBaseChannel::GetURIPrincipal() { + if (mPrincipal) { + return mPrincipal; + } + + nsIScriptSecurityManager* securityManager = + nsContentUtils::GetSecurityManager(); + + if (!securityManager) { + LOG(("HttpBaseChannel::GetURIPrincipal: No security manager [this=%p]", + this)); + return nullptr; + } + + securityManager->GetChannelURIPrincipal(this, getter_AddRefs(mPrincipal)); + if (!mPrincipal) { + LOG(("HttpBaseChannel::GetURIPrincipal: No channel principal [this=%p]", + this)); + return nullptr; + } + + return mPrincipal; +} + +bool HttpBaseChannel::IsNavigation() { + return LoadForceMainDocumentChannel() || (mLoadFlags & LOAD_DOCUMENT_URI); +} + +bool HttpBaseChannel::BypassServiceWorker() const { + return mLoadFlags & LOAD_BYPASS_SERVICE_WORKER; +} + +bool HttpBaseChannel::ShouldIntercept(nsIURI* aURI) { + nsCOMPtr controller; + GetCallback(controller); + bool shouldIntercept = false; + + // We should never intercept internal redirects. The ServiceWorker code + // can trigger interntal redirects as the result of a FetchEvent. If + // we re-intercept then an infinite loop can occur. + // + // Its also important that we do not set the LOAD_BYPASS_SERVICE_WORKER + // flag because an internal redirect occurs. Its possible that another + // interception should occur after the internal redirect. For example, + // if the ServiceWorker chooses not to call respondWith() the channel + // will be reset with an internal redirect. If the request is a navigation + // and the network then triggers a redirect its possible the new URL + // should be intercepted again. + // + // Note, HSTS upgrade redirects are often treated the same as internal + // redirects. In this case, however, we intentionally allow interception + // of HSTS upgrade redirects. This matches the expected spec behavior and + // does not run the risk of infinite loops as described above. + bool internalRedirect = + mLastRedirectFlags & nsIChannelEventSink::REDIRECT_INTERNAL; + + if (controller && mLoadInfo && !BypassServiceWorker() && !internalRedirect) { + nsresult rv = controller->ShouldPrepareForIntercept( + aURI ? aURI : mURI.get(), this, &shouldIntercept); + if (NS_FAILED(rv)) { + return false; + } + } + return shouldIntercept; +} + +void HttpBaseChannel::AddAsNonTailRequest() { + MOZ_ASSERT(NS_IsMainThread()); + + if (EnsureRequestContext()) { + LOG(( + "HttpBaseChannel::AddAsNonTailRequest this=%p, rc=%p, already added=%d", + this, mRequestContext.get(), (bool)LoadAddedAsNonTailRequest())); + + if (!LoadAddedAsNonTailRequest()) { + mRequestContext->AddNonTailRequest(); + StoreAddedAsNonTailRequest(true); + } + } +} + +void HttpBaseChannel::RemoveAsNonTailRequest() { + MOZ_ASSERT(NS_IsMainThread()); + + if (mRequestContext) { + LOG( + ("HttpBaseChannel::RemoveAsNonTailRequest this=%p, rc=%p, already " + "added=%d", + this, mRequestContext.get(), (bool)LoadAddedAsNonTailRequest())); + + if (LoadAddedAsNonTailRequest()) { + mRequestContext->RemoveNonTailRequest(); + StoreAddedAsNonTailRequest(false); + } + } +} + +#ifdef DEBUG +void HttpBaseChannel::AssertPrivateBrowsingId() { + nsCOMPtr loadContext; + NS_QueryNotificationCallbacks(this, loadContext); + + if (!loadContext) { + return; + } + + // We skip testing of favicon loading here since it could be triggered by XUL + // image which uses SystemPrincipal. The SystemPrincpal doesn't have + // mPrivateBrowsingId. + if (mLoadInfo->GetLoadingPrincipal() && + mLoadInfo->GetLoadingPrincipal()->IsSystemPrincipal() && + mLoadInfo->InternalContentPolicyType() == + nsIContentPolicy::TYPE_INTERNAL_IMAGE_FAVICON) { + return; + } + + OriginAttributes docShellAttrs; + loadContext->GetOriginAttributes(docShellAttrs); + MOZ_ASSERT(mLoadInfo->GetOriginAttributes().mPrivateBrowsingId == + docShellAttrs.mPrivateBrowsingId, + "PrivateBrowsingId values are not the same between LoadInfo and " + "LoadContext."); +} +#endif + +already_AddRefed HttpBaseChannel::CloneLoadInfoForRedirect( + nsIURI* aNewURI, uint32_t aRedirectFlags) { + // make a copy of the loadinfo, append to the redirectchain + // this will be set on the newly created channel for the redirect target. + nsCOMPtr newLoadInfo = + static_cast(mLoadInfo.get())->Clone(); + + ExtContentPolicyType contentPolicyType = + mLoadInfo->GetExternalContentPolicyType(); + if (contentPolicyType == ExtContentPolicy::TYPE_DOCUMENT || + contentPolicyType == ExtContentPolicy::TYPE_SUBDOCUMENT) { + nsCOMPtr nullPrincipalToInherit = + NullPrincipal::CreateWithoutOriginAttributes(); + newLoadInfo->SetPrincipalToInherit(nullPrincipalToInherit); + } + + bool isTopLevelDoc = newLoadInfo->GetExternalContentPolicyType() == + ExtContentPolicy::TYPE_DOCUMENT; + + if (isTopLevelDoc) { + // re-compute the origin attributes of the loadInfo if it's top-level load. + nsCOMPtr loadContext; + NS_QueryNotificationCallbacks(this, loadContext); + OriginAttributes docShellAttrs; + if (loadContext) { + loadContext->GetOriginAttributes(docShellAttrs); + } + + OriginAttributes attrs = newLoadInfo->GetOriginAttributes(); + + MOZ_ASSERT( + docShellAttrs.mUserContextId == attrs.mUserContextId, + "docshell and necko should have the same userContextId attribute."); + MOZ_ASSERT( + docShellAttrs.mPrivateBrowsingId == attrs.mPrivateBrowsingId, + "docshell and necko should have the same privateBrowsingId attribute."); + MOZ_ASSERT(docShellAttrs.mGeckoViewSessionContextId == + attrs.mGeckoViewSessionContextId, + "docshell and necko should have the same " + "geckoViewSessionContextId attribute"); + + attrs = docShellAttrs; + attrs.SetFirstPartyDomain(true, aNewURI); + newLoadInfo->SetOriginAttributes(attrs); + + // re-compute the upgrade insecure requests bit for document navigations + // since it should only apply to same-origin navigations (redirects). + // we only do this if the CSP of the triggering element (the cspToInherit) + // uses 'upgrade-insecure-requests', otherwise UIR does not apply. + nsCOMPtr csp = newLoadInfo->GetCspToInherit(); + if (csp) { + bool upgradeInsecureRequests = false; + csp->GetUpgradeInsecureRequests(&upgradeInsecureRequests); + if (upgradeInsecureRequests) { + nsCOMPtr resultPrincipal = + BasePrincipal::CreateContentPrincipal( + aNewURI, newLoadInfo->GetOriginAttributes()); + bool isConsideredSameOriginforUIR = + nsContentSecurityUtils::IsConsideredSameOriginForUIR( + newLoadInfo->TriggeringPrincipal(), resultPrincipal); + static_cast(newLoadInfo.get()) + ->SetUpgradeInsecureRequests(isConsideredSameOriginforUIR); + } + } + } + + // Leave empty, we want a 'clean ground' when creating the new channel. + // This will be ensured to be either set by the protocol handler or set + // to the redirect target URI properly after the channel creation. + newLoadInfo->SetResultPrincipalURI(nullptr); + + bool isInternalRedirect = + (aRedirectFlags & (nsIChannelEventSink::REDIRECT_INTERNAL | + nsIChannelEventSink::REDIRECT_STS_UPGRADE)); + + nsCString remoteAddress; + Unused << GetRemoteAddress(remoteAddress); + nsCOMPtr referrer; + if (mReferrerInfo) { + referrer = mReferrerInfo->GetComputedReferrer(); + } + + nsCOMPtr entry = + new nsRedirectHistoryEntry(GetURIPrincipal(), referrer, remoteAddress); + + newLoadInfo->AppendRedirectHistoryEntry(entry, isInternalRedirect); + + return newLoadInfo.forget(); +} + +//----------------------------------------------------------------------------- +// nsHttpChannel::nsITraceableChannel +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +HttpBaseChannel::SetNewListener(nsIStreamListener* aListener, + bool aMustApplyContentConversion, + nsIStreamListener** _retval) { + LOG(( + "HttpBaseChannel::SetNewListener [this=%p, mListener=%p, newListener=%p]", + this, mListener.get(), aListener)); + + if (!LoadTracingEnabled()) return NS_ERROR_FAILURE; + + NS_ENSURE_STATE(mListener); + NS_ENSURE_ARG_POINTER(aListener); + + nsCOMPtr wrapper = new nsStreamListenerWrapper(mListener); + + wrapper.forget(_retval); + mListener = aListener; + if (aMustApplyContentConversion) { + StoreListenerRequiresContentConversion(true); + } + return NS_OK; +} + +//----------------------------------------------------------------------------- +// HttpBaseChannel helpers +//----------------------------------------------------------------------------- + +void HttpBaseChannel::ReleaseListeners() { + MOZ_ASSERT(mCurrentThread->IsOnCurrentThread(), + "Should only be called on the current thread"); + + mListener = nullptr; + mCallbacks = nullptr; + mProgressSink = nullptr; + mCompressListener = nullptr; +} + +void HttpBaseChannel::DoNotifyListener() { + LOG(("HttpBaseChannel::DoNotifyListener this=%p", this)); + + // In case nsHttpChannel::OnStartRequest wasn't called (e.g. due to flag + // LOAD_ONLY_IF_MODIFIED) we want to set AfterOnStartRequestBegun to true + // before notifying listener. + if (!LoadAfterOnStartRequestBegun()) { + StoreAfterOnStartRequestBegun(true); + } + + if (mListener && !LoadOnStartRequestCalled()) { + nsCOMPtr listener = mListener; + StoreOnStartRequestCalled(true); + listener->OnStartRequest(this); + } + StoreOnStartRequestCalled(true); + + // Make sure IsPending is set to false. At this moment we are done from + // the point of view of our consumer and we have to report our self + // as not-pending. + StoreIsPending(false); + + if (mListener && !LoadOnStopRequestCalled()) { + nsCOMPtr listener = mListener; + StoreOnStopRequestCalled(true); + listener->OnStopRequest(this, mStatus); + } + StoreOnStopRequestCalled(true); + + // notify "http-on-stop-connect" observers + gHttpHandler->OnStopRequest(this); + + // This channel has finished its job, potentially release any tail-blocked + // requests with this. + RemoveAsNonTailRequest(); + + // We have to make sure to drop the references to listeners and callbacks + // no longer needed. + ReleaseListeners(); + + DoNotifyListenerCleanup(); + + // If this is a navigation, then we must let the docshell flush the reports + // to the console later. The LoadDocument() is pointing at the detached + // document that started the navigation. We want to show the reports on the + // new document. Otherwise the console is wiped and the user never sees + // the information. + if (!IsNavigation()) { + if (mLoadGroup) { + FlushConsoleReports(mLoadGroup); + } else { + RefPtr doc; + mLoadInfo->GetLoadingDocument(getter_AddRefs(doc)); + FlushConsoleReports(doc); + } + } +} + +void HttpBaseChannel::AddCookiesToRequest() { + if (mLoadFlags & LOAD_ANONYMOUS) { + return; + } + + bool useCookieService = (XRE_IsParentProcess()); + nsAutoCString cookie; + if (useCookieService) { + nsICookieService* cs = gHttpHandler->GetCookieService(); + if (cs) { + cs->GetCookieStringFromHttp(mURI, this, cookie); + } + + if (cookie.IsEmpty()) { + cookie = mUserSetCookieHeader; + } else if (!mUserSetCookieHeader.IsEmpty()) { + cookie.AppendLiteral("; "); + cookie.Append(mUserSetCookieHeader); + } + } else { + cookie = mUserSetCookieHeader; + } + + // If we are in the child process, we want the parent seeing any + // cookie headers that might have been set by SetRequestHeader() + SetRequestHeader(nsDependentCString(nsHttp::Cookie), cookie, false); +} + +/* static */ +void HttpBaseChannel::PropagateReferenceIfNeeded( + nsIURI* aURI, nsCOMPtr& aRedirectURI) { + bool hasRef = false; + nsresult rv = aRedirectURI->GetHasRef(&hasRef); + if (NS_SUCCEEDED(rv) && !hasRef) { + nsAutoCString ref; + aURI->GetRef(ref); + if (!ref.IsEmpty()) { + // NOTE: SetRef will fail if mRedirectURI is immutable + // (e.g. an about: URI)... Oh well. + Unused << NS_MutateURI(aRedirectURI).SetRef(ref).Finalize(aRedirectURI); + } + } +} + +bool HttpBaseChannel::ShouldRewriteRedirectToGET( + uint32_t httpStatus, nsHttpRequestHead::ParsedMethodType method) { + // for 301 and 302, only rewrite POST + if (httpStatus == 301 || httpStatus == 302) + return method == nsHttpRequestHead::kMethod_Post; + + // rewrite for 303 unless it was HEAD + if (httpStatus == 303) return method != nsHttpRequestHead::kMethod_Head; + + // otherwise, such as for 307, do not rewrite + return false; +} + +NS_IMETHODIMP +HttpBaseChannel::ShouldStripRequestBodyHeader(const nsACString& aMethod, + bool* aResult) { + *aResult = false; + uint32_t httpStatus = 0; + if (NS_FAILED(GetResponseStatus(&httpStatus))) { + return NS_OK; + } + + nsAutoCString method(aMethod); + nsHttpRequestHead::ParsedMethodType parsedMethod; + nsHttpRequestHead::ParseMethod(method, parsedMethod); + // Fetch 4.4.11, which is slightly different than the perserved method + // algrorithm: strip request-body-header for GET->GET redirection for 303. + *aResult = + ShouldRewriteRedirectToGET(httpStatus, parsedMethod) && + !(httpStatus == 303 && parsedMethod == nsHttpRequestHead::kMethod_Get); + + return NS_OK; +} + +HttpBaseChannel::ReplacementChannelConfig +HttpBaseChannel::CloneReplacementChannelConfig(bool aPreserveMethod, + uint32_t aRedirectFlags, + ReplacementReason aReason) { + ReplacementChannelConfig config; + config.redirectFlags = aRedirectFlags; + config.classOfService = mClassOfService; + + if (mPrivateBrowsingOverriden) { + config.privateBrowsing = Some(mPrivateBrowsing); + } + + if (mReferrerInfo) { + // When cloning for a document channel replacement (parent process + // copying values for a new content process channel), this happens after + // OnStartRequest so we have the headers for the response available. + // We don't want to apply them to the referrer for the channel though, + // since that is the referrer for the current document, and the header + // should only apply to navigations from the current document. + if (aReason == ReplacementReason::DocumentChannel) { + config.referrerInfo = mReferrerInfo; + } else { + dom::ReferrerPolicy referrerPolicy = dom::ReferrerPolicy::_empty; + nsAutoCString tRPHeaderCValue; + Unused << GetResponseHeader("referrer-policy"_ns, tRPHeaderCValue); + NS_ConvertUTF8toUTF16 tRPHeaderValue(tRPHeaderCValue); + + if (!tRPHeaderValue.IsEmpty()) { + referrerPolicy = + dom::ReferrerInfo::ReferrerPolicyFromHeaderString(tRPHeaderValue); + } + + if (referrerPolicy != dom::ReferrerPolicy::_empty) { + // We may reuse computed referrer in redirect, so if referrerPolicy + // changes, we must not use the old computed value, and have to compute + // again. + nsCOMPtr referrerInfo = + dom::ReferrerInfo::CreateFromOtherAndPolicyOverride(mReferrerInfo, + referrerPolicy); + config.referrerInfo = referrerInfo; + } else { + config.referrerInfo = mReferrerInfo; + } + } + } + + nsCOMPtr oldTimedChannel( + do_QueryInterface(static_cast(this))); + if (oldTimedChannel) { + config.timedChannel = Some(dom::TimedChannelInfo()); + config.timedChannel->timingEnabled() = LoadTimingEnabled(); + config.timedChannel->redirectCount() = mRedirectCount; + config.timedChannel->internalRedirectCount() = mInternalRedirectCount; + config.timedChannel->asyncOpen() = mAsyncOpenTime; + config.timedChannel->channelCreation() = mChannelCreationTimestamp; + config.timedChannel->redirectStart() = mRedirectStartTimeStamp; + config.timedChannel->redirectEnd() = mRedirectEndTimeStamp; + config.timedChannel->initiatorType() = mInitiatorType; + config.timedChannel->allRedirectsSameOrigin() = + LoadAllRedirectsSameOrigin(); + config.timedChannel->allRedirectsPassTimingAllowCheck() = + LoadAllRedirectsPassTimingAllowCheck(); + // Execute the timing allow check to determine whether + // to report the redirect timing info + nsCOMPtr loadInfo = LoadInfo(); + // TYPE_DOCUMENT loads don't have a loadingPrincipal, so we can't set + // AllRedirectsPassTimingAllowCheck on them. + if (loadInfo->GetExternalContentPolicyType() != + ExtContentPolicy::TYPE_DOCUMENT) { + nsCOMPtr principal = loadInfo->GetLoadingPrincipal(); + config.timedChannel->timingAllowCheckForPrincipal() = + Some(oldTimedChannel->TimingAllowCheck(principal)); + } + + config.timedChannel->allRedirectsPassTimingAllowCheck() = + LoadAllRedirectsPassTimingAllowCheck(); + config.timedChannel->launchServiceWorkerStart() = mLaunchServiceWorkerStart; + config.timedChannel->launchServiceWorkerEnd() = mLaunchServiceWorkerEnd; + config.timedChannel->dispatchFetchEventStart() = mDispatchFetchEventStart; + config.timedChannel->dispatchFetchEventEnd() = mDispatchFetchEventEnd; + config.timedChannel->handleFetchEventStart() = mHandleFetchEventStart; + config.timedChannel->handleFetchEventEnd() = mHandleFetchEventEnd; + config.timedChannel->responseStart() = mTransactionTimings.responseStart; + config.timedChannel->responseEnd() = mTransactionTimings.responseEnd; + } + + if (aPreserveMethod) { + // since preserveMethod is true, we need to ensure that the appropriate + // request method gets set on the channel, regardless of whether or not + // we set the upload stream above. This means SetRequestMethod() will + // be called twice if ExplicitSetUploadStream() gets called above. + + nsAutoCString method; + mRequestHead.Method(method); + config.method = Some(method); + + if (mUploadStream) { + // rewind upload stream + nsCOMPtr seekable = do_QueryInterface(mUploadStream); + if (seekable) { + seekable->Seek(nsISeekableStream::NS_SEEK_SET, 0); + } + config.uploadStream = mUploadStream; + } + config.uploadStreamLength = mReqContentLength; + config.uploadStreamHasHeaders = LoadUploadStreamHasHeaders(); + + nsAutoCString contentType; + nsresult rv = mRequestHead.GetHeader(nsHttp::Content_Type, contentType); + if (NS_SUCCEEDED(rv)) { + config.contentType = Some(contentType); + } + + nsAutoCString contentLength; + rv = mRequestHead.GetHeader(nsHttp::Content_Length, contentLength); + if (NS_SUCCEEDED(rv)) { + config.contentLength = Some(contentLength); + } + } + + return config; +} + +/* static */ void HttpBaseChannel::ConfigureReplacementChannel( + nsIChannel* newChannel, const ReplacementChannelConfig& config, + ReplacementReason aReason) { + nsCOMPtr cos(do_QueryInterface(newChannel)); + if (cos) { + cos->SetClassFlags(config.classOfService); + } + + // Try to preserve the privacy bit if it has been overridden + if (config.privateBrowsing) { + nsCOMPtr newPBChannel = + do_QueryInterface(newChannel); + if (newPBChannel) { + newPBChannel->SetPrivate(*config.privateBrowsing); + } + } + + // Transfer the timing data (if we are dealing with an nsITimedChannel). + nsCOMPtr newTimedChannel(do_QueryInterface(newChannel)); + if (config.timedChannel && newTimedChannel) { + newTimedChannel->SetTimingEnabled(config.timedChannel->timingEnabled()); + + // If we're an internal redirect, or a document channel replacement, + // then we shouldn't record any new timing for this and just copy + // over the existing values. + bool shouldHideTiming = aReason != ReplacementReason::Redirect; + if (shouldHideTiming) { + newTimedChannel->SetRedirectCount(config.timedChannel->redirectCount()); + int8_t newCount = config.timedChannel->internalRedirectCount() + 1; + newTimedChannel->SetInternalRedirectCount( + std::max(newCount, config.timedChannel->internalRedirectCount())); + } else { + int8_t newCount = config.timedChannel->redirectCount() + 1; + newTimedChannel->SetRedirectCount( + std::max(newCount, config.timedChannel->redirectCount())); + newTimedChannel->SetInternalRedirectCount( + config.timedChannel->internalRedirectCount()); + } + + if (shouldHideTiming) { + if (!config.timedChannel->channelCreation().IsNull()) { + newTimedChannel->SetChannelCreation( + config.timedChannel->channelCreation()); + } + + if (!config.timedChannel->asyncOpen().IsNull()) { + newTimedChannel->SetAsyncOpen(config.timedChannel->asyncOpen()); + } + } + + // If the RedirectStart is null, we will use the AsyncOpen value of the + // previous channel (this is the first redirect in the redirects chain). + if (config.timedChannel->redirectStart().IsNull()) { + // Only do this for real redirects. Internal redirects should be hidden. + if (!shouldHideTiming) { + newTimedChannel->SetRedirectStart(config.timedChannel->asyncOpen()); + } + } else { + newTimedChannel->SetRedirectStart(config.timedChannel->redirectStart()); + } + + // For internal redirects just propagate the last redirect end time + // forward. Otherwise the new redirect end time is the last response + // end time. + TimeStamp newRedirectEnd; + if (shouldHideTiming) { + newRedirectEnd = config.timedChannel->redirectEnd(); + } else { + newRedirectEnd = config.timedChannel->responseEnd(); + } + newTimedChannel->SetRedirectEnd(newRedirectEnd); + + newTimedChannel->SetInitiatorType(config.timedChannel->initiatorType()); + + nsCOMPtr loadInfo = newChannel->LoadInfo(); + MOZ_ASSERT(loadInfo); + + newTimedChannel->SetAllRedirectsSameOrigin( + config.timedChannel->allRedirectsSameOrigin()); + + if (config.timedChannel->timingAllowCheckForPrincipal()) { + newTimedChannel->SetAllRedirectsPassTimingAllowCheck( + config.timedChannel->allRedirectsPassTimingAllowCheck() && + *config.timedChannel->timingAllowCheckForPrincipal()); + } + + // Propagate service worker measurements across redirects. The + // PeformanceResourceTiming.workerStart API expects to see the + // worker start time after a redirect. + newTimedChannel->SetLaunchServiceWorkerStart( + config.timedChannel->launchServiceWorkerStart()); + newTimedChannel->SetLaunchServiceWorkerEnd( + config.timedChannel->launchServiceWorkerEnd()); + newTimedChannel->SetDispatchFetchEventStart( + config.timedChannel->dispatchFetchEventStart()); + newTimedChannel->SetDispatchFetchEventEnd( + config.timedChannel->dispatchFetchEventEnd()); + newTimedChannel->SetHandleFetchEventStart( + config.timedChannel->handleFetchEventStart()); + newTimedChannel->SetHandleFetchEventEnd( + config.timedChannel->handleFetchEventEnd()); + } + + nsCOMPtr httpChannel = do_QueryInterface(newChannel); + if (!httpChannel) { + return; // no other options to set + } + + if (config.uploadStream) { + nsCOMPtr uploadChannel = do_QueryInterface(httpChannel); + nsCOMPtr uploadChannel2 = do_QueryInterface(httpChannel); + if (uploadChannel2 || uploadChannel) { + // replicate original call to SetUploadStream... + if (uploadChannel2) { + const nsACString& ctype = + config.contentType ? *config.contentType : VoidCString(); + // If header is not present mRequestHead.HasHeaderValue will truncated + // it. But we want to end up with a void string, not an empty string, + // because ExplicitSetUploadStream treats the former as "no header" and + // the latter as "header with empty string value". + + const nsACString& method = + config.method ? *config.method : VoidCString(); + + uploadChannel2->ExplicitSetUploadStream( + config.uploadStream, ctype, config.uploadStreamLength, method, + config.uploadStreamHasHeaders); + } else { + if (config.uploadStreamHasHeaders) { + uploadChannel->SetUploadStream(config.uploadStream, ""_ns, + config.uploadStreamLength); + } else { + nsAutoCString ctype; + if (config.contentType) { + ctype = *config.contentType; + } else { + ctype = "application/octet-stream"_ns; + } + if (config.contentLength && !config.contentLength->IsEmpty()) { + uploadChannel->SetUploadStream( + config.uploadStream, ctype, + nsCRT::atoll(config.contentLength->get())); + } + } + } + } + } + + if (config.referrerInfo) { + DebugOnly success; + success = httpChannel->SetReferrerInfo(config.referrerInfo); + MOZ_ASSERT(NS_SUCCEEDED(success)); + } + + if (config.method) { + DebugOnly rv = httpChannel->SetRequestMethod(*config.method); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } +} + +HttpBaseChannel::ReplacementChannelConfig::ReplacementChannelConfig( + const dom::ReplacementChannelConfigInit& aInit) { + redirectFlags = aInit.redirectFlags(); + classOfService = aInit.classOfService(); + privateBrowsing = aInit.privateBrowsing(); + method = aInit.method(); + referrerInfo = aInit.referrerInfo(); + timedChannel = aInit.timedChannel(); + if (RemoteLazyInputStreamChild* actor = + static_cast(aInit.uploadStreamChild())) { + uploadStreamLength = actor->Size(); + uploadStream = actor->CreateStream(); + // actor can be deleted by CreateStream, so don't touch it + // after this. + } else { + uploadStreamLength = 0; + } + uploadStreamHasHeaders = aInit.uploadStreamHasHeaders(); + contentType = aInit.contentType(); + contentLength = aInit.contentLength(); +} + +dom::ReplacementChannelConfigInit +HttpBaseChannel::ReplacementChannelConfig::Serialize( + dom::ContentParent* aParent) { + dom::ReplacementChannelConfigInit config; + config.redirectFlags() = redirectFlags; + config.classOfService() = classOfService; + config.privateBrowsing() = privateBrowsing; + config.method() = method; + config.referrerInfo() = referrerInfo; + config.timedChannel() = timedChannel; + if (uploadStream) { + RemoteLazyStream ipdlStream; + RemoteLazyInputStreamUtils::SerializeInputStream( + uploadStream, uploadStreamLength, ipdlStream, aParent); + config.uploadStreamParent() = ipdlStream; + } + config.uploadStreamHasHeaders() = uploadStreamHasHeaders; + config.contentType() = contentType; + config.contentLength() = contentLength; + + return config; +} + +nsresult HttpBaseChannel::SetupReplacementChannel(nsIURI* newURI, + nsIChannel* newChannel, + bool preserveMethod, + uint32_t redirectFlags) { + nsresult rv; + + LOG( + ("HttpBaseChannel::SetupReplacementChannel " + "[this=%p newChannel=%p preserveMethod=%d]", + this, newChannel, preserveMethod)); + + // Ensure the channel's loadInfo's result principal URI so that it's + // either non-null or updated to the redirect target URI. + // We must do this because in case the loadInfo's result principal URI + // is null, it would be taken from OriginalURI of the channel. But we + // overwrite it with the whole redirect chain first URI before opening + // the target channel, hence the information would be lost. + // If the protocol handler that created the channel wants to use + // the originalURI of the channel as the principal URI, this fulfills + // that request - newURI is the original URI of the channel. + nsCOMPtr newLoadInfo = newChannel->LoadInfo(); + nsCOMPtr resultPrincipalURI; + rv = newLoadInfo->GetResultPrincipalURI(getter_AddRefs(resultPrincipalURI)); + NS_ENSURE_SUCCESS(rv, rv); + if (!resultPrincipalURI) { + rv = newLoadInfo->SetResultPrincipalURI(newURI); + NS_ENSURE_SUCCESS(rv, rv); + } + + nsLoadFlags loadFlags = mLoadFlags; + loadFlags |= LOAD_REPLACE; + + // if the original channel was using SSL and this channel is not using + // SSL, then no need to inhibit persistent caching. however, if the + // original channel was not using SSL and has INHIBIT_PERSISTENT_CACHING + // set, then allow the flag to apply to the redirected channel as well. + // since we force set INHIBIT_PERSISTENT_CACHING on all HTTPS channels, + // we only need to check if the original channel was using SSL. + if (mURI->SchemeIs("https")) { + loadFlags &= ~INHIBIT_PERSISTENT_CACHING; + } + + // Do not pass along LOAD_CHECK_OFFLINE_CACHE + loadFlags &= ~nsICachingChannel::LOAD_CHECK_OFFLINE_CACHE; + newChannel->SetLoadFlags(loadFlags); + + nsCOMPtr httpChannel = do_QueryInterface(newChannel); + + ReplacementReason redirectType = + (redirectFlags & nsIChannelEventSink::REDIRECT_INTERNAL) + ? ReplacementReason::InternalRedirect + : ReplacementReason::Redirect; + ReplacementChannelConfig config = CloneReplacementChannelConfig( + preserveMethod, redirectFlags, redirectType); + ConfigureReplacementChannel(newChannel, config, redirectType); + + // Check whether or not this was a cross-domain redirect. + nsCOMPtr newTimedChannel(do_QueryInterface(newChannel)); + if (config.timedChannel && newTimedChannel) { + newTimedChannel->SetAllRedirectsSameOrigin( + config.timedChannel->allRedirectsSameOrigin() && + SameOriginWithOriginalUri(newURI)); + } + + newChannel->SetLoadGroup(mLoadGroup); + newChannel->SetNotificationCallbacks(mCallbacks); + + if (!httpChannel) return NS_OK; // no other options to set + + // Preserve the CORS preflight information. + nsCOMPtr httpInternal = do_QueryInterface(newChannel); + if (httpInternal) { + httpInternal->SetLastRedirectFlags(redirectFlags); + + if (LoadRequireCORSPreflight()) { + httpInternal->SetCorsPreflightParameters(mUnsafeHeaders, false); + } + } + + // convey the LoadAllowSTS() flags + rv = httpChannel->SetAllowSTS(LoadAllowSTS()); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + // convey the Accept header value + { + nsAutoCString oldAcceptValue; + nsresult hasHeader = mRequestHead.GetHeader(nsHttp::Accept, oldAcceptValue); + if (NS_SUCCEEDED(hasHeader)) { + rv = httpChannel->SetRequestHeader("Accept"_ns, oldAcceptValue, false); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + } + + // convey the User-Agent header value + // since we might be setting custom user agent from DevTools. + if (httpInternal && mCorsMode == CORS_MODE_NO_CORS && + redirectType == ReplacementReason::Redirect) { + nsAutoCString oldUserAgent; + nsresult hasHeader = + mRequestHead.GetHeader(nsHttp::User_Agent, oldUserAgent); + if (NS_SUCCEEDED(hasHeader)) { + rv = httpChannel->SetRequestHeader("User-Agent"_ns, oldUserAgent, false); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + } + + // share the request context - see bug 1236650 + rv = httpChannel->SetRequestContextID(mRequestContextID); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + // When on the parent process, the channel can't attempt to get it itself. + // When on the child process, it would be waste to query it again. + rv = httpChannel->SetTopLevelOuterContentWindowId( + mTopLevelOuterContentWindowId); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + // Not setting this flag would break carrying permissions down to the child + // process when the channel is artificially forced to be a main document load. + rv = httpChannel->SetIsMainDocumentChannel(LoadForceMainDocumentChannel()); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + // Preserve the loading order + nsCOMPtr p = do_QueryInterface(newChannel); + if (p) { + p->SetPriority(mPriority); + } + + if (httpInternal) { + // Convey third party cookie, conservative, and spdy flags. + rv = httpInternal->SetThirdPartyFlags(LoadThirdPartyFlags()); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + rv = httpInternal->SetAllowSpdy(LoadAllowSpdy()); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + rv = httpInternal->SetAllowHttp3(LoadAllowHttp3()); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + rv = httpInternal->SetAllowAltSvc(LoadAllowAltSvc()); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + rv = httpInternal->SetBeConservative(LoadBeConservative()); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + rv = httpInternal->SetIsTRRServiceChannel(LoadIsTRRServiceChannel()); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + rv = httpInternal->SetTlsFlags(mTlsFlags); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + // Ensure the type of realChannel involves all types it may redirect to. + // Such as nsHttpChannel and InterceptedChannel. + // Even thought InterceptedChannel itself doesn't require these information, + // it may still be necessary for the following redirections. + // E.g. nsHttpChannel -> InterceptedChannel -> nsHttpChannel + RefPtr realChannel; + CallQueryInterface(newChannel, realChannel.StartAssignment()); + if (realChannel) { + realChannel->SetTopWindowURI(mTopWindowURI); + + realChannel->StoreTaintedOriginFlag( + ShouldTaintReplacementChannelOrigin(newURI)); + } + + // update the DocumentURI indicator since we are being redirected. + // if this was a top-level document channel, then the new channel + // should have its mDocumentURI point to newURI; otherwise, we + // just need to pass along our mDocumentURI to the new channel. + if (newURI && (mURI == mDocumentURI)) + rv = httpInternal->SetDocumentURI(newURI); + else + rv = httpInternal->SetDocumentURI(mDocumentURI); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + // if there is a chain of keys for redirect-responses we transfer it to + // the new channel (see bug #561276) + if (mRedirectedCachekeys) { + LOG( + ("HttpBaseChannel::SetupReplacementChannel " + "[this=%p] transferring chain of redirect cache-keys", + this)); + rv = httpInternal->SetCacheKeysRedirectChain( + mRedirectedCachekeys.release()); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + + // Preserve CORS mode flag. + rv = httpInternal->SetCorsMode(mCorsMode); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + // Preserve Redirect mode flag. + rv = httpInternal->SetRedirectMode(mRedirectMode); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + // Preserve Integrity metadata. + rv = httpInternal->SetIntegrityMetadata(mIntegrityMetadata); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + httpInternal->SetAltDataForChild(LoadAltDataForChild()); + if (LoadDisableAltDataCache()) { + httpInternal->DisableAltDataCache(); + } + } + + // transfer application cache information + nsCOMPtr appCacheChannel = + do_QueryInterface(newChannel); + if (appCacheChannel) { + appCacheChannel->SetApplicationCache(mApplicationCache); + appCacheChannel->SetInheritApplicationCache(LoadInheritApplicationCache()); + // We purposely avoid transfering ChooseApplicationCache. + } + + // transfer any properties + nsCOMPtr bag(do_QueryInterface(newChannel)); + if (bag) { + for (auto iter = mPropertyHash.Iter(); !iter.Done(); iter.Next()) { + bag->SetProperty(iter.Key(), iter.UserData()); + } + } + + // Pass the preferred alt-data type on to the new channel. + nsCOMPtr cacheInfoChan(do_QueryInterface(newChannel)); + if (cacheInfoChan) { + for (auto& data : mPreferredCachedAltDataTypes) { + cacheInfoChan->PreferAlternativeDataType(data.type(), data.contentType(), + data.deliverAltData()); + } + } + + if (redirectFlags & (nsIChannelEventSink::REDIRECT_INTERNAL | + nsIChannelEventSink::REDIRECT_STS_UPGRADE)) { + // Copy non-origin related headers to the new channel. + nsCOMPtr visitor = + new AddHeadersToChannelVisitor(httpChannel); + rv = mRequestHead.VisitHeaders(visitor); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + + // This channel has been redirected. Don't report timing info. + StoreTimingEnabled(false); + return NS_OK; +} + +bool HttpBaseChannel::ShouldTaintReplacementChannelOrigin(nsIURI* aNewURI) { + if (LoadTaintedOriginFlag()) { + return true; + } + + nsIScriptSecurityManager* ssm = nsContentUtils::GetSecurityManager(); + if (!ssm) { + return true; + } + bool isPrivateWin = mLoadInfo->GetOriginAttributes().mPrivateBrowsingId > 0; + nsresult rv = ssm->CheckSameOriginURI(aNewURI, mURI, false, isPrivateWin); + if (NS_SUCCEEDED(rv)) { + return false; + } + // If aNewURI <-> mURI are not same-origin we need to taint unless + // mURI <-> mOriginalURI/LoadingPrincipal are same origin. + + if (mLoadInfo->GetLoadingPrincipal()) { + bool sameOrigin = false; + rv = mLoadInfo->GetLoadingPrincipal()->IsSameOrigin(mURI, isPrivateWin, + &sameOrigin); + if (NS_FAILED(rv)) { + return true; + } + return !sameOrigin; + } + if (!mOriginalURI) { + return true; + } + + rv = ssm->CheckSameOriginURI(mOriginalURI, mURI, false, isPrivateWin); + return NS_FAILED(rv); +} + +// Redirect Tracking +bool HttpBaseChannel::SameOriginWithOriginalUri(nsIURI* aURI) { + nsIScriptSecurityManager* ssm = nsContentUtils::GetSecurityManager(); + bool isPrivateWin = mLoadInfo->GetOriginAttributes().mPrivateBrowsingId > 0; + nsresult rv = + ssm->CheckSameOriginURI(aURI, mOriginalURI, false, isPrivateWin); + return (NS_SUCCEEDED(rv)); +} + +//----------------------------------------------------------------------------- +// HttpBaseChannel::nsIClassifiedChannel + +NS_IMETHODIMP +HttpBaseChannel::GetMatchedList(nsACString& aList) { + aList = mMatchedList; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetMatchedProvider(nsACString& aProvider) { + aProvider = mMatchedProvider; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetMatchedFullHash(nsACString& aFullHash) { + aFullHash = mMatchedFullHash; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::SetMatchedInfo(const nsACString& aList, + const nsACString& aProvider, + const nsACString& aFullHash) { + NS_ENSURE_ARG(!aList.IsEmpty()); + + mMatchedList = aList; + mMatchedProvider = aProvider; + mMatchedFullHash = aFullHash; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetMatchedTrackingLists(nsTArray& aLists) { + aLists = mMatchedTrackingLists.Clone(); + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetMatchedTrackingFullHashes( + nsTArray& aFullHashes) { + aFullHashes = mMatchedTrackingFullHashes.Clone(); + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::SetMatchedTrackingInfo( + const nsTArray& aLists, const nsTArray& aFullHashes) { + NS_ENSURE_ARG(!aLists.IsEmpty()); + // aFullHashes can be empty for non hash-matching algorithm, for example, + // host based test entries in preference. + + mMatchedTrackingLists = aLists.Clone(); + mMatchedTrackingFullHashes = aFullHashes.Clone(); + return NS_OK; +} +//----------------------------------------------------------------------------- +// HttpBaseChannel::nsITimedChannel +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +HttpBaseChannel::SetTimingEnabled(bool enabled) { + StoreTimingEnabled(enabled); + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetTimingEnabled(bool* _retval) { + *_retval = LoadTimingEnabled(); + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetChannelCreation(TimeStamp* _retval) { + *_retval = mChannelCreationTimestamp; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::SetChannelCreation(TimeStamp aValue) { + MOZ_DIAGNOSTIC_ASSERT(!aValue.IsNull()); + TimeDuration adjust = aValue - mChannelCreationTimestamp; + mChannelCreationTimestamp = aValue; + mChannelCreationTime += (PRTime)adjust.ToMicroseconds(); + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetAsyncOpen(TimeStamp* _retval) { + *_retval = mAsyncOpenTime; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::SetAsyncOpen(TimeStamp aValue) { + MOZ_DIAGNOSTIC_ASSERT(!aValue.IsNull()); + mAsyncOpenTime = aValue; + StoreAsyncOpenTimeOverriden(true); + return NS_OK; +} + +/** + * @return the number of redirects. There is no check for cross-domain + * redirects. This check must be done by the consumers. + */ +NS_IMETHODIMP +HttpBaseChannel::GetRedirectCount(uint8_t* aRedirectCount) { + *aRedirectCount = mRedirectCount; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::SetRedirectCount(uint8_t aRedirectCount) { + mRedirectCount = aRedirectCount; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetInternalRedirectCount(uint8_t* aRedirectCount) { + *aRedirectCount = mInternalRedirectCount; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::SetInternalRedirectCount(uint8_t aRedirectCount) { + mInternalRedirectCount = aRedirectCount; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetRedirectStart(TimeStamp* _retval) { + *_retval = mRedirectStartTimeStamp; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::SetRedirectStart(TimeStamp aRedirectStart) { + mRedirectStartTimeStamp = aRedirectStart; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetRedirectEnd(TimeStamp* _retval) { + *_retval = mRedirectEndTimeStamp; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::SetRedirectEnd(TimeStamp aRedirectEnd) { + mRedirectEndTimeStamp = aRedirectEnd; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetAllRedirectsSameOrigin(bool* aAllRedirectsSameOrigin) { + *aAllRedirectsSameOrigin = LoadAllRedirectsSameOrigin(); + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::SetAllRedirectsSameOrigin(bool aAllRedirectsSameOrigin) { + StoreAllRedirectsSameOrigin(aAllRedirectsSameOrigin); + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetAllRedirectsPassTimingAllowCheck(bool* aPassesCheck) { + *aPassesCheck = LoadAllRedirectsPassTimingAllowCheck(); + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::SetAllRedirectsPassTimingAllowCheck(bool aPassesCheck) { + StoreAllRedirectsPassTimingAllowCheck(aPassesCheck); + return NS_OK; +} + +// http://www.w3.org/TR/resource-timing/#timing-allow-check +NS_IMETHODIMP +HttpBaseChannel::TimingAllowCheck(nsIPrincipal* aOrigin, bool* _retval) { + nsIScriptSecurityManager* ssm = nsContentUtils::GetSecurityManager(); + nsCOMPtr resourcePrincipal; + nsresult rv = + ssm->GetChannelURIPrincipal(this, getter_AddRefs(resourcePrincipal)); + if (NS_FAILED(rv) || !resourcePrincipal || !aOrigin) { + *_retval = false; + return NS_OK; + } + + bool sameOrigin = false; + rv = resourcePrincipal->Equals(aOrigin, &sameOrigin); + if (NS_SUCCEEDED(rv) && sameOrigin) { + *_retval = true; + return NS_OK; + } + + nsAutoCString headerValue; + rv = GetResponseHeader("Timing-Allow-Origin"_ns, headerValue); + if (NS_FAILED(rv)) { + *_retval = false; + return NS_OK; + } + + nsAutoCString origin; + aOrigin->GetAsciiOrigin(origin); + + Tokenizer p(headerValue); + Tokenizer::Token t; + + p.Record(); + nsAutoCString headerItem; + while (p.Next(t)) { + if (t.Type() == Tokenizer::TOKEN_EOF || + t.Equals(Tokenizer::Token::Char(','))) { + p.Claim(headerItem); + nsHttp::TrimHTTPWhitespace(headerItem, headerItem); + // If the list item contains a case-sensitive match for the value of the + // origin, or a wildcard, return pass + if (headerItem == origin || headerItem == "*") { + *_retval = true; + return NS_OK; + } + // We start recording again for the following items in the list + p.Record(); + } + } + + *_retval = false; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetLaunchServiceWorkerStart(TimeStamp* _retval) { + MOZ_ASSERT(_retval); + *_retval = mLaunchServiceWorkerStart; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::SetLaunchServiceWorkerStart(TimeStamp aTimeStamp) { + mLaunchServiceWorkerStart = aTimeStamp; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetLaunchServiceWorkerEnd(TimeStamp* _retval) { + MOZ_ASSERT(_retval); + *_retval = mLaunchServiceWorkerEnd; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::SetLaunchServiceWorkerEnd(TimeStamp aTimeStamp) { + mLaunchServiceWorkerEnd = aTimeStamp; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetDispatchFetchEventStart(TimeStamp* _retval) { + MOZ_ASSERT(_retval); + *_retval = mDispatchFetchEventStart; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::SetDispatchFetchEventStart(TimeStamp aTimeStamp) { + mDispatchFetchEventStart = aTimeStamp; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetDispatchFetchEventEnd(TimeStamp* _retval) { + MOZ_ASSERT(_retval); + *_retval = mDispatchFetchEventEnd; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::SetDispatchFetchEventEnd(TimeStamp aTimeStamp) { + mDispatchFetchEventEnd = aTimeStamp; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetHandleFetchEventStart(TimeStamp* _retval) { + MOZ_ASSERT(_retval); + *_retval = mHandleFetchEventStart; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::SetHandleFetchEventStart(TimeStamp aTimeStamp) { + mHandleFetchEventStart = aTimeStamp; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetHandleFetchEventEnd(TimeStamp* _retval) { + MOZ_ASSERT(_retval); + *_retval = mHandleFetchEventEnd; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::SetHandleFetchEventEnd(TimeStamp aTimeStamp) { + mHandleFetchEventEnd = aTimeStamp; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetDomainLookupStart(TimeStamp* _retval) { + *_retval = mTransactionTimings.domainLookupStart; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetDomainLookupEnd(TimeStamp* _retval) { + *_retval = mTransactionTimings.domainLookupEnd; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetConnectStart(TimeStamp* _retval) { + *_retval = mTransactionTimings.connectStart; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetTcpConnectEnd(TimeStamp* _retval) { + *_retval = mTransactionTimings.tcpConnectEnd; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetSecureConnectionStart(TimeStamp* _retval) { + *_retval = mTransactionTimings.secureConnectionStart; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetConnectEnd(TimeStamp* _retval) { + *_retval = mTransactionTimings.connectEnd; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetRequestStart(TimeStamp* _retval) { + *_retval = mTransactionTimings.requestStart; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetResponseStart(TimeStamp* _retval) { + *_retval = mTransactionTimings.responseStart; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetResponseEnd(TimeStamp* _retval) { + *_retval = mTransactionTimings.responseEnd; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetCacheReadStart(TimeStamp* _retval) { + *_retval = mCacheReadStart; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetCacheReadEnd(TimeStamp* _retval) { + *_retval = mCacheReadEnd; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetInitiatorType(nsAString& aInitiatorType) { + aInitiatorType = mInitiatorType; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::SetInitiatorType(const nsAString& aInitiatorType) { + mInitiatorType = aInitiatorType; + return NS_OK; +} + +#define IMPL_TIMING_ATTR(name) \ + NS_IMETHODIMP \ + HttpBaseChannel::Get##name##Time(PRTime* _retval) { \ + TimeStamp stamp; \ + Get##name(&stamp); \ + if (stamp.IsNull()) { \ + *_retval = 0; \ + return NS_OK; \ + } \ + *_retval = \ + mChannelCreationTime + \ + (PRTime)((stamp - mChannelCreationTimestamp).ToSeconds() * 1e6); \ + return NS_OK; \ + } + +IMPL_TIMING_ATTR(ChannelCreation) +IMPL_TIMING_ATTR(AsyncOpen) +IMPL_TIMING_ATTR(LaunchServiceWorkerStart) +IMPL_TIMING_ATTR(LaunchServiceWorkerEnd) +IMPL_TIMING_ATTR(DispatchFetchEventStart) +IMPL_TIMING_ATTR(DispatchFetchEventEnd) +IMPL_TIMING_ATTR(HandleFetchEventStart) +IMPL_TIMING_ATTR(HandleFetchEventEnd) +IMPL_TIMING_ATTR(DomainLookupStart) +IMPL_TIMING_ATTR(DomainLookupEnd) +IMPL_TIMING_ATTR(ConnectStart) +IMPL_TIMING_ATTR(TcpConnectEnd) +IMPL_TIMING_ATTR(SecureConnectionStart) +IMPL_TIMING_ATTR(ConnectEnd) +IMPL_TIMING_ATTR(RequestStart) +IMPL_TIMING_ATTR(ResponseStart) +IMPL_TIMING_ATTR(ResponseEnd) +IMPL_TIMING_ATTR(CacheReadStart) +IMPL_TIMING_ATTR(CacheReadEnd) +IMPL_TIMING_ATTR(RedirectStart) +IMPL_TIMING_ATTR(RedirectEnd) + +#undef IMPL_TIMING_ATTR + +mozilla::dom::PerformanceStorage* HttpBaseChannel::GetPerformanceStorage() { + // If performance timing is disabled, there is no need for the Performance + // object anymore. + if (!LoadTimingEnabled()) { + return nullptr; + } + + // There is no point in continuing, since the performance object in the parent + // isn't the same as the one in the child which will be reporting resource + // performance. + if (XRE_IsE10sParentProcess()) { + return nullptr; + } + return mLoadInfo->GetPerformanceStorage(); +} + +void HttpBaseChannel::MaybeReportTimingData() { + if (XRE_IsE10sParentProcess()) { + return; + } + + mozilla::dom::PerformanceStorage* documentPerformance = + GetPerformanceStorage(); + if (documentPerformance) { + documentPerformance->AddEntry(this, this); + return; + } + + if (!nsGlobalWindowInner::GetInnerWindowWithId( + mLoadInfo->GetInnerWindowID())) { + // The inner window is in a different process. + dom::ContentChild* child = dom::ContentChild::GetSingleton(); + + if (!child) { + return; + } + nsAutoString initiatorType; + nsAutoString entryName; + + UniquePtr performanceTimingData( + dom::PerformanceTimingData::Create(this, this, 0, initiatorType, + entryName)); + if (!performanceTimingData) { + return; + } + child->SendReportFrameTimingData(mLoadInfo->GetInnerWindowID(), entryName, + initiatorType, + std::move(performanceTimingData)); + } +} + +NS_IMETHODIMP +HttpBaseChannel::SetReportResourceTiming(bool enabled) { + StoreReportTiming(enabled); + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetReportResourceTiming(bool* _retval) { + *_retval = LoadReportTiming(); + return NS_OK; +} + +nsIURI* HttpBaseChannel::GetReferringPage() { + nsCOMPtr pDomWindow = GetInnerDOMWindow(); + if (!pDomWindow) { + return nullptr; + } + return pDomWindow->GetDocumentURI(); +} + +nsPIDOMWindowInner* HttpBaseChannel::GetInnerDOMWindow() { + nsCOMPtr loadContext; + NS_QueryNotificationCallbacks(this, loadContext); + if (!loadContext) { + return nullptr; + } + nsCOMPtr domWindow; + loadContext->GetAssociatedWindow(getter_AddRefs(domWindow)); + if (!domWindow) { + return nullptr; + } + auto* pDomWindow = nsPIDOMWindowOuter::From(domWindow); + if (!pDomWindow) { + return nullptr; + } + nsCOMPtr innerWindow = + pDomWindow->GetCurrentInnerWindow(); + if (!innerWindow) { + return nullptr; + } + + return innerWindow; +} + +//----------------------------------------------------------------------------- +// HttpBaseChannel::nsIThrottledInputChannel +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +HttpBaseChannel::SetThrottleQueue(nsIInputChannelThrottleQueue* aQueue) { + if (!XRE_IsParentProcess()) { + return NS_ERROR_FAILURE; + } + + mThrottleQueue = aQueue; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetThrottleQueue(nsIInputChannelThrottleQueue** aQueue) { + NS_ENSURE_ARG_POINTER(aQueue); + nsCOMPtr queue = mThrottleQueue; + queue.forget(aQueue); + return NS_OK; +} + +//------------------------------------------------------------------------------ + +bool HttpBaseChannel::EnsureRequestContextID() { + if (mRequestContextID) { + // Already have a request context ID, no need to do the rest of this work + LOG(("HttpBaseChannel::EnsureRequestContextID this=%p id=%" PRIx64, this, + mRequestContextID)); + return true; + } + + // Find the loadgroup at the end of the chain in order + // to make sure all channels derived from the load group + // use the same connection scope. + nsCOMPtr childLoadGroup = do_QueryInterface(mLoadGroup); + if (!childLoadGroup) { + return false; + } + + nsCOMPtr rootLoadGroup; + childLoadGroup->GetRootLoadGroup(getter_AddRefs(rootLoadGroup)); + if (!rootLoadGroup) { + return false; + } + + // Set the load group connection scope on this channel and its transaction + rootLoadGroup->GetRequestContextID(&mRequestContextID); + + LOG(("HttpBaseChannel::EnsureRequestContextID this=%p id=%" PRIx64, this, + mRequestContextID)); + + return true; +} + +bool HttpBaseChannel::EnsureRequestContext() { + if (mRequestContext) { + // Already have a request context, no need to do the rest of this work + return true; + } + + if (!EnsureRequestContextID()) { + return false; + } + + nsIRequestContextService* rcsvc = gHttpHandler->GetRequestContextService(); + if (!rcsvc) { + return false; + } + + rcsvc->GetRequestContext(mRequestContextID, getter_AddRefs(mRequestContext)); + if (!mRequestContext) { + return false; + } + + return true; +} + +void HttpBaseChannel::EnsureTopLevelOuterContentWindowId() { + if (mTopLevelOuterContentWindowId) { + return; + } + + nsCOMPtr loadContext; + GetCallback(loadContext); + if (!loadContext) { + return; + } + + nsCOMPtr topWindow; + loadContext->GetTopWindow(getter_AddRefs(topWindow)); + if (!topWindow) { + return; + } + + mTopLevelOuterContentWindowId = + nsPIDOMWindowOuter::From(topWindow)->WindowID(); +} + +void HttpBaseChannel::SetCorsPreflightParameters( + const nsTArray& aUnsafeHeaders, + bool aShouldStripRequestBodyHeader) { + MOZ_RELEASE_ASSERT(!LoadRequestObserversCalled()); + + StoreRequireCORSPreflight(true); + mUnsafeHeaders = aUnsafeHeaders.Clone(); + if (aShouldStripRequestBodyHeader) { + mUnsafeHeaders.RemoveElementsBy([&](const nsCString& aHeader) { + return aHeader.LowerCaseEqualsASCII("content-type") || + aHeader.LowerCaseEqualsASCII("content-encoding") || + aHeader.LowerCaseEqualsASCII("content-language") || + aHeader.LowerCaseEqualsASCII("content-location"); + }); + } +} + +void HttpBaseChannel::SetAltDataForChild(bool aIsForChild) { + StoreAltDataForChild(aIsForChild); +} + +NS_IMETHODIMP +HttpBaseChannel::GetBlockAuthPrompt(bool* aValue) { + if (!aValue) { + return NS_ERROR_FAILURE; + } + + *aValue = LoadBlockAuthPrompt(); + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::SetBlockAuthPrompt(bool aValue) { + ENSURE_CALLED_BEFORE_CONNECT(); + + StoreBlockAuthPrompt(aValue); + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetConnectionInfoHashKey(nsACString& aConnectionInfoHashKey) { + if (!mConnectionInfo) { + return NS_ERROR_FAILURE; + } + aConnectionInfoHashKey.Assign(mConnectionInfo->HashKey()); + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetLastRedirectFlags(uint32_t* aValue) { + NS_ENSURE_ARG(aValue); + *aValue = mLastRedirectFlags; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::SetLastRedirectFlags(uint32_t aValue) { + mLastRedirectFlags = aValue; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetNavigationStartTimeStamp(TimeStamp* aTimeStamp) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +HttpBaseChannel::SetNavigationStartTimeStamp(TimeStamp aTimeStamp) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +nsresult HttpBaseChannel::CheckRedirectLimit(uint32_t aRedirectFlags) const { + if (aRedirectFlags & nsIChannelEventSink::REDIRECT_INTERNAL) { + // Some platform features, like Service Workers, depend on internal + // redirects. We should allow some number of internal redirects above + // and beyond the normal redirect limit so these features continue + // to work. + static const int8_t kMinInternalRedirects = 5; + + if (mInternalRedirectCount >= (mRedirectionLimit + kMinInternalRedirects)) { + LOG(("internal redirection limit reached!\n")); + return NS_ERROR_REDIRECT_LOOP; + } + return NS_OK; + } + + MOZ_ASSERT(aRedirectFlags & (nsIChannelEventSink::REDIRECT_TEMPORARY | + nsIChannelEventSink::REDIRECT_PERMANENT | + nsIChannelEventSink::REDIRECT_STS_UPGRADE)); + + if (mRedirectCount >= mRedirectionLimit) { + LOG(("redirection limit reached!\n")); + return NS_ERROR_REDIRECT_LOOP; + } + + return NS_OK; +} + +// NOTE: This function duplicates code from nsBaseChannel. This will go away +// once HTTP uses nsBaseChannel (part of bug 312760) +/* static */ +void HttpBaseChannel::CallTypeSniffers(void* aClosure, const uint8_t* aData, + uint32_t aCount) { + nsIChannel* chan = static_cast(aClosure); + + nsAutoCString newType; + NS_SniffContent(NS_CONTENT_SNIFFER_CATEGORY, chan, aData, aCount, newType); + if (!newType.IsEmpty()) { + chan->SetContentType(newType); + } +} + +template +static void ParseServerTimingHeader( + const UniquePtr& aHeader, nsTArray>& aOutput) { + if (!aHeader) { + return; + } + + nsAutoCString serverTimingHeader; + Unused << aHeader->GetHeader(nsHttp::Server_Timing, serverTimingHeader); + if (serverTimingHeader.IsEmpty()) { + return; + } + + ServerTimingParser parser(serverTimingHeader); + parser.Parse(); + + nsTArray> array = parser.TakeServerTimingHeaders(); + aOutput.AppendElements(array); +} + +NS_IMETHODIMP +HttpBaseChannel::GetServerTiming(nsIArray** aServerTiming) { + nsresult rv; + NS_ENSURE_ARG_POINTER(aServerTiming); + + nsCOMPtr array = do_CreateInstance(NS_ARRAY_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsTArray> data; + rv = GetNativeServerTiming(data); + NS_ENSURE_SUCCESS(rv, rv); + + for (const auto& entry : data) { + array->AppendElement(entry); + } + + array.forget(aServerTiming); + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetNativeServerTiming( + nsTArray>& aServerTiming) { + aServerTiming.Clear(); + + if (mURI->SchemeIs("https")) { + ParseServerTimingHeader(mResponseHead, aServerTiming); + ParseServerTimingHeader(mResponseTrailers, aServerTiming); + } + + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::CancelByURLClassifier(nsresult aErrorCode) { + MOZ_ASSERT( + UrlClassifierFeatureFactory::IsClassifierBlockingErrorCode(aErrorCode)); + return Cancel(aErrorCode); +} + +void HttpBaseChannel::SetIPv4Disabled() { mCaps |= NS_HTTP_DISABLE_IPV4; } + +void HttpBaseChannel::SetIPv6Disabled() { mCaps |= NS_HTTP_DISABLE_IPV6; } + +NS_IMETHODIMP HttpBaseChannel::GetResponseEmbedderPolicy( + nsILoadInfo::CrossOriginEmbedderPolicy* aOutPolicy) { + *aOutPolicy = nsILoadInfo::EMBEDDER_POLICY_NULL; + if (!mResponseHead) { + return NS_ERROR_NOT_AVAILABLE; + } + + if (!nsContentUtils::ComputeIsSecureContext(this)) { + // Feature is only available for secure contexts. + return NS_OK; + } + + nsAutoCString content; + Unused << mResponseHead->GetHeader(nsHttp::Cross_Origin_Embedder_Policy, + content); + + *aOutPolicy = NS_GetCrossOriginEmbedderPolicyFromHeader(content); + return NS_OK; +} + +// Obtain a cross-origin opener-policy from a response response and a +// cross-origin opener policy initiator. +// https://gist.github.com/annevk/6f2dd8c79c77123f39797f6bdac43f3e +NS_IMETHODIMP HttpBaseChannel::ComputeCrossOriginOpenerPolicy( + nsILoadInfo::CrossOriginOpenerPolicy aInitiatorPolicy, + nsILoadInfo::CrossOriginOpenerPolicy* aOutPolicy) { + MOZ_ASSERT(aOutPolicy); + *aOutPolicy = nsILoadInfo::OPENER_POLICY_UNSAFE_NONE; + + if (!mResponseHead) { + return NS_ERROR_NOT_AVAILABLE; + } + + // COOP headers are ignored for insecure-context loads. + if (!nsContentUtils::ComputeIsSecureContext(this)) { + return NS_OK; + } + + nsAutoCString openerPolicy; + Unused << mResponseHead->GetHeader(nsHttp::Cross_Origin_Opener_Policy, + openerPolicy); + + // Cross-Origin-Opener-Policy = %s"same-origin" / + // %s"same-origin-allow-popups" / + // %s"unsafe-none"; case-sensitive + + nsCOMPtr sfv = GetSFVService(); + + nsCOMPtr item; + nsresult rv = sfv->ParseItem(openerPolicy, getter_AddRefs(item)); + if (NS_FAILED(rv)) { + return rv; + } + + nsCOMPtr value; + rv = item->GetValue(getter_AddRefs(value)); + if (NS_FAILED(rv)) { + return rv; + } + + nsCOMPtr token = do_QueryInterface(value); + if (!token) { + return NS_ERROR_UNEXPECTED; + } + + rv = token->GetValue(openerPolicy); + if (NS_FAILED(rv)) { + return rv; + } + + nsILoadInfo::CrossOriginOpenerPolicy policy = + nsILoadInfo::OPENER_POLICY_UNSAFE_NONE; + + if (openerPolicy.EqualsLiteral("same-origin")) { + policy = nsILoadInfo::OPENER_POLICY_SAME_ORIGIN; + } else if (openerPolicy.EqualsLiteral("same-origin-allow-popups")) { + policy = nsILoadInfo::OPENER_POLICY_SAME_ORIGIN_ALLOW_POPUPS; + } + + if (policy == nsILoadInfo::OPENER_POLICY_SAME_ORIGIN) { + nsILoadInfo::CrossOriginEmbedderPolicy coep = + nsILoadInfo::EMBEDDER_POLICY_NULL; + if (NS_SUCCEEDED(GetResponseEmbedderPolicy(&coep)) && + coep == nsILoadInfo::EMBEDDER_POLICY_REQUIRE_CORP) { + policy = + nsILoadInfo::OPENER_POLICY_SAME_ORIGIN_EMBEDDER_POLICY_REQUIRE_CORP; + } + } + + *aOutPolicy = policy; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetCrossOriginOpenerPolicy( + nsILoadInfo::CrossOriginOpenerPolicy* aPolicy) { + MOZ_ASSERT(aPolicy); + if (!aPolicy) { + return NS_ERROR_INVALID_ARG; + } + // If this method is called before OnStartRequest (ie. before we call + // ComputeCrossOriginOpenerPolicy) or if we were unable to compute the + // policy we'll throw an error. + if (!LoadOnStartRequestCalled()) { + return NS_ERROR_NOT_AVAILABLE; + } + *aPolicy = mComputedCrossOriginOpenerPolicy; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::HasCrossOriginOpenerPolicyMismatch(bool* aIsMismatch) { + // This should only be called in parent process. + MOZ_ASSERT(XRE_IsParentProcess()); + *aIsMismatch = LoadHasCrossOriginOpenerPolicyMismatch(); + return NS_OK; +} + +void HttpBaseChannel::MaybeFlushConsoleReports() { + // If this channel is part of a loadGroup, we can flush the console reports + // immediately. + nsCOMPtr loadGroup; + nsresult rv = GetLoadGroup(getter_AddRefs(loadGroup)); + if (NS_SUCCEEDED(rv) && loadGroup) { + FlushConsoleReports(loadGroup); + } +} + +void HttpBaseChannel::DoDiagnosticAssertWhenOnStopNotCalledOnDestroy() {} + +NS_IMETHODIMP HttpBaseChannel::SetWaitForHTTPSSVCRecord() { + mCaps |= NS_HTTP_WAIT_HTTPSSVC_RESULT; + return NS_OK; +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/HttpBaseChannel.h b/netwerk/protocol/http/HttpBaseChannel.h new file mode 100644 index 0000000000..f4e0896056 --- /dev/null +++ b/netwerk/protocol/http/HttpBaseChannel.h @@ -0,0 +1,1045 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et 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/. */ + +#ifndef mozilla_net_HttpBaseChannel_h +#define mozilla_net_HttpBaseChannel_h + +#include + +#include "mozilla/AtomicBitfields.h" +#include "mozilla/Atomics.h" +#include "mozilla/dom/DOMTypes.h" +#include "mozilla/net/DNS.h" +#include "mozilla/net/NeckoCommon.h" +#include "mozilla/net/PrivateBrowsingChannel.h" +#include "nsCOMPtr.h" +#include "nsHashPropertyBag.h" +#include "nsHttp.h" +#include "nsHttpHandler.h" +#include "nsHttpRequestHead.h" +#include "nsIClassOfService.h" +#include "nsIClassifiedChannel.h" +#include "nsIConsoleReportCollector.h" +#include "nsIEncodedChannel.h" +#include "nsIForcePendingChannel.h" +#include "nsIFormPOSTActionChannel.h" +#include "nsIHttpChannel.h" +#include "nsIHttpChannelInternal.h" +#include "nsILoadInfo.h" +#include "nsIResumableChannel.h" +#include "nsIStringEnumerator.h" +#include "nsISupportsPriority.h" +#include "nsIThrottledInputChannel.h" +#include "nsITimedChannel.h" +#include "nsITraceableChannel.h" +#include "nsIURI.h" +#include "nsIUploadChannel2.h" +#include "nsStringEnumerator.h" +#include "nsTArray.h" +#include "nsThreadUtils.h" + +#define HTTP_BASE_CHANNEL_IID \ + { \ + 0x9d5cde03, 0xe6e9, 0x4612, { \ + 0xbf, 0xef, 0xbb, 0x66, 0xf3, 0xbb, 0x74, 0x46 \ + } \ + } + +class nsIProgressEventSink; +class nsISecurityConsoleMessage; +class nsIPrincipal; + +namespace mozilla { + +namespace dom { +class PerformanceStorage; +class ContentParent; +} // namespace dom + +class LogCollector; + +namespace net { +extern mozilla::LazyLogModule gHttpLog; + +class PreferredAlternativeDataTypeParams; + +enum CacheDisposition : uint8_t { + kCacheUnresolved = 0, + kCacheHit = 1, + kCacheHitViaReval = 2, + kCacheMissedViaReval = 3, + kCacheMissed = 4, + kCacheUnknown = 5 +}; + +/* + * This class is a partial implementation of nsIHttpChannel. It contains code + * shared by nsHttpChannel and HttpChannelChild. + * - Note that this class has nothing to do with nsBaseChannel, which is an + * earlier effort at a base class for channels that somehow never made it all + * the way to the HTTP channel. + */ +class HttpBaseChannel : public nsHashPropertyBag, + public nsIEncodedChannel, + public nsIHttpChannel, + public nsIHttpChannelInternal, + public nsIFormPOSTActionChannel, + public nsIUploadChannel2, + public nsISupportsPriority, + public nsIClassOfService, + public nsIResumableChannel, + public nsITraceableChannel, + public PrivateBrowsingChannel, + public nsITimedChannel, + public nsIForcePendingChannel, + public nsIConsoleReportCollector, + public nsIThrottledInputChannel, + public nsIClassifiedChannel { + protected: + virtual ~HttpBaseChannel(); + + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_NSIUPLOADCHANNEL + NS_DECL_NSIFORMPOSTACTIONCHANNEL + NS_DECL_NSIUPLOADCHANNEL2 + NS_DECL_NSITRACEABLECHANNEL + NS_DECL_NSITIMEDCHANNEL + NS_DECL_NSITHROTTLEDINPUTCHANNEL + NS_DECL_NSICLASSIFIEDCHANNEL + + NS_DECLARE_STATIC_IID_ACCESSOR(HTTP_BASE_CHANNEL_IID) + + HttpBaseChannel(); + + [[nodiscard]] virtual nsresult Init(nsIURI* aURI, uint32_t aCaps, + nsProxyInfo* aProxyInfo, + uint32_t aProxyResolveFlags, + nsIURI* aProxyURI, uint64_t aChannelId, + ExtContentPolicyType aContentPolicyType); + + // nsIRequest + NS_IMETHOD GetName(nsACString& aName) override; + NS_IMETHOD IsPending(bool* aIsPending) override; + NS_IMETHOD GetStatus(nsresult* aStatus) override; + NS_IMETHOD GetLoadGroup(nsILoadGroup** aLoadGroup) override; + NS_IMETHOD SetLoadGroup(nsILoadGroup* aLoadGroup) override; + NS_IMETHOD GetLoadFlags(nsLoadFlags* aLoadFlags) override; + NS_IMETHOD SetLoadFlags(nsLoadFlags aLoadFlags) override; + NS_IMETHOD GetTRRMode(nsIRequest::TRRMode* aTRRMode) override; + NS_IMETHOD SetTRRMode(nsIRequest::TRRMode aTRRMode) override; + NS_IMETHOD SetDocshellUserAgentOverride(); + + // nsIChannel + NS_IMETHOD GetOriginalURI(nsIURI** aOriginalURI) override; + NS_IMETHOD SetOriginalURI(nsIURI* aOriginalURI) override; + NS_IMETHOD GetURI(nsIURI** aURI) override; + NS_IMETHOD GetOwner(nsISupports** aOwner) override; + NS_IMETHOD SetOwner(nsISupports* aOwner) override; + NS_IMETHOD GetLoadInfo(nsILoadInfo** aLoadInfo) override; + NS_IMETHOD SetLoadInfo(nsILoadInfo* aLoadInfo) override; + NS_IMETHOD GetIsDocument(bool* aIsDocument) override; + NS_IMETHOD GetNotificationCallbacks( + nsIInterfaceRequestor** aCallbacks) override; + NS_IMETHOD SetNotificationCallbacks( + nsIInterfaceRequestor* aCallbacks) override; + NS_IMETHOD GetContentType(nsACString& aContentType) override; + NS_IMETHOD SetContentType(const nsACString& aContentType) override; + NS_IMETHOD GetContentCharset(nsACString& aContentCharset) override; + NS_IMETHOD SetContentCharset(const nsACString& aContentCharset) override; + NS_IMETHOD GetContentDisposition(uint32_t* aContentDisposition) override; + NS_IMETHOD SetContentDisposition(uint32_t aContentDisposition) override; + NS_IMETHOD GetContentDispositionFilename( + nsAString& aContentDispositionFilename) override; + NS_IMETHOD SetContentDispositionFilename( + const nsAString& aContentDispositionFilename) override; + NS_IMETHOD GetContentDispositionHeader( + nsACString& aContentDispositionHeader) override; + NS_IMETHOD GetContentLength(int64_t* aContentLength) override; + NS_IMETHOD SetContentLength(int64_t aContentLength) override; + NS_IMETHOD Open(nsIInputStream** aResult) override; + NS_IMETHOD GetBlockAuthPrompt(bool* aValue) override; + NS_IMETHOD SetBlockAuthPrompt(bool aValue) override; + NS_IMETHOD GetCanceled(bool* aCanceled) override; + + // nsIEncodedChannel + NS_IMETHOD GetApplyConversion(bool* value) override; + NS_IMETHOD SetApplyConversion(bool value) override; + NS_IMETHOD GetContentEncodings(nsIUTF8StringEnumerator** aEncodings) override; + NS_IMETHOD DoApplyContentConversions(nsIStreamListener* aNextListener, + nsIStreamListener** aNewNextListener, + nsISupports* aCtxt) override; + + // HttpBaseChannel::nsIHttpChannel + NS_IMETHOD GetRequestMethod(nsACString& aMethod) override; + NS_IMETHOD SetRequestMethod(const nsACString& aMethod) override; + NS_IMETHOD GetReferrerInfo(nsIReferrerInfo** aReferrerInfo) override; + NS_IMETHOD SetReferrerInfo(nsIReferrerInfo* aReferrerInfo) override; + NS_IMETHOD SetReferrerInfoWithoutClone( + nsIReferrerInfo* aReferrerInfo) override; + NS_IMETHOD GetRequestHeader(const nsACString& aHeader, + nsACString& aValue) override; + NS_IMETHOD SetRequestHeader(const nsACString& aHeader, + const nsACString& aValue, bool aMerge) override; + NS_IMETHOD SetNewReferrerInfo(const nsACString& aUrl, + nsIReferrerInfo::ReferrerPolicyIDL aPolicy, + bool aSendReferrer) override; + NS_IMETHOD SetEmptyRequestHeader(const nsACString& aHeader) override; + NS_IMETHOD VisitRequestHeaders(nsIHttpHeaderVisitor* visitor) override; + NS_IMETHOD VisitNonDefaultRequestHeaders( + nsIHttpHeaderVisitor* visitor) override; + NS_IMETHOD ShouldStripRequestBodyHeader(const nsACString& aMethod, + bool* aResult) override; + NS_IMETHOD GetResponseHeader(const nsACString& header, + nsACString& value) override; + NS_IMETHOD SetResponseHeader(const nsACString& header, + const nsACString& value, bool merge) override; + NS_IMETHOD VisitResponseHeaders(nsIHttpHeaderVisitor* visitor) override; + NS_IMETHOD GetOriginalResponseHeader(const nsACString& aHeader, + nsIHttpHeaderVisitor* aVisitor) override; + NS_IMETHOD VisitOriginalResponseHeaders( + nsIHttpHeaderVisitor* aVisitor) override; + NS_IMETHOD GetAllowPipelining(bool* value) override; // deprecated + NS_IMETHOD SetAllowPipelining(bool value) override; // deprecated + NS_IMETHOD GetAllowSTS(bool* value) override; + NS_IMETHOD SetAllowSTS(bool value) override; + NS_IMETHOD GetRedirectionLimit(uint32_t* value) override; + NS_IMETHOD SetRedirectionLimit(uint32_t value) override; + NS_IMETHOD IsNoStoreResponse(bool* value) override; + NS_IMETHOD IsNoCacheResponse(bool* value) override; + NS_IMETHOD IsPrivateResponse(bool* value) override; + NS_IMETHOD GetResponseStatus(uint32_t* aValue) override; + NS_IMETHOD GetResponseStatusText(nsACString& aValue) override; + NS_IMETHOD GetRequestSucceeded(bool* aValue) override; + NS_IMETHOD RedirectTo(nsIURI* newURI) override; + NS_IMETHOD UpgradeToSecure() override; + NS_IMETHOD GetRequestContextID(uint64_t* aRCID) override; + NS_IMETHOD GetTransferSize(uint64_t* aTransferSize) override; + NS_IMETHOD GetRequestSize(uint64_t* aRequestSize) override; + NS_IMETHOD GetDecodedBodySize(uint64_t* aDecodedBodySize) override; + NS_IMETHOD GetEncodedBodySize(uint64_t* aEncodedBodySize) override; + NS_IMETHOD GetSupportsHTTP3(bool* aSupportsHTTP3) override; + NS_IMETHOD SetRequestContextID(uint64_t aRCID) override; + NS_IMETHOD GetIsMainDocumentChannel(bool* aValue) override; + NS_IMETHOD SetIsMainDocumentChannel(bool aValue) override; + NS_IMETHOD GetProtocolVersion(nsACString& aProtocolVersion) override; + NS_IMETHOD GetChannelId(uint64_t* aChannelId) override; + NS_IMETHOD SetChannelId(uint64_t aChannelId) override; + NS_IMETHOD GetTopLevelContentWindowId(uint64_t* aContentWindowId) override; + NS_IMETHOD SetTopLevelContentWindowId(uint64_t aContentWindowId) override; + NS_IMETHOD GetTopLevelOuterContentWindowId(uint64_t* aWindowId) override; + NS_IMETHOD SetTopLevelOuterContentWindowId(uint64_t aWindowId) override; + + NS_IMETHOD GetFlashPluginState( + nsIHttpChannel::FlashPluginState* aState) override; + + using nsIClassifiedChannel::IsThirdPartyTrackingResource; + + virtual void SetSource(UniquePtr aSource) override { + mSource = std::move(aSource); + } + + // nsIHttpChannelInternal + NS_IMETHOD GetDocumentURI(nsIURI** aDocumentURI) override; + NS_IMETHOD SetDocumentURI(nsIURI* aDocumentURI) override; + NS_IMETHOD GetRequestVersion(uint32_t* major, uint32_t* minor) override; + NS_IMETHOD GetResponseVersion(uint32_t* major, uint32_t* minor) override; + NS_IMETHOD SetCookie(const nsACString& aCookieHeader) override; + NS_IMETHOD GetThirdPartyFlags(uint32_t* aForce) override; + NS_IMETHOD SetThirdPartyFlags(uint32_t aForce) override; + NS_IMETHOD GetForceAllowThirdPartyCookie(bool* aForce) override; + NS_IMETHOD SetForceAllowThirdPartyCookie(bool aForce) override; + NS_IMETHOD GetChannelIsForDownload(bool* aChannelIsForDownload) override; + NS_IMETHOD SetChannelIsForDownload(bool aChannelIsForDownload) override; + NS_IMETHOD SetCacheKeysRedirectChain(nsTArray* cacheKeys) override; + NS_IMETHOD GetLocalAddress(nsACString& addr) override; + NS_IMETHOD GetLocalPort(int32_t* port) override; + NS_IMETHOD GetRemoteAddress(nsACString& addr) override; + NS_IMETHOD GetRemotePort(int32_t* port) override; + NS_IMETHOD GetOnlyConnect(bool* aOnlyConnect) override; + NS_IMETHOD SetConnectOnly() override; + NS_IMETHOD GetAllowSpdy(bool* aAllowSpdy) override; + NS_IMETHOD SetAllowSpdy(bool aAllowSpdy) override; + NS_IMETHOD GetAllowHttp3(bool* aAllowHttp3) override; + NS_IMETHOD SetAllowHttp3(bool aAllowHttp3) override; + NS_IMETHOD GetAllowAltSvc(bool* aAllowAltSvc) override; + NS_IMETHOD SetAllowAltSvc(bool aAllowAltSvc) override; + NS_IMETHOD GetBeConservative(bool* aBeConservative) override; + NS_IMETHOD SetBeConservative(bool aBeConservative) override; + NS_IMETHOD GetIsTRRServiceChannel(bool* aTRR) override; + NS_IMETHOD SetIsTRRServiceChannel(bool aTRR) override; + NS_IMETHOD GetIsResolvedByTRR(bool* aResolvedByTRR) override; + NS_IMETHOD GetTlsFlags(uint32_t* aTlsFlags) override; + NS_IMETHOD SetTlsFlags(uint32_t aTlsFlags) override; + NS_IMETHOD GetApiRedirectToURI(nsIURI** aApiRedirectToURI) override; + [[nodiscard]] virtual nsresult AddSecurityMessage( + const nsAString& aMessageTag, const nsAString& aMessageCategory); + NS_IMETHOD TakeAllSecurityMessages( + nsCOMArray& aMessages) override; + NS_IMETHOD GetResponseTimeoutEnabled(bool* aEnable) override; + NS_IMETHOD SetResponseTimeoutEnabled(bool aEnable) override; + NS_IMETHOD GetInitialRwin(uint32_t* aRwin) override; + NS_IMETHOD SetInitialRwin(uint32_t aRwin) override; + NS_IMETHOD ForcePending(bool aForcePending) override; + NS_IMETHOD GetLastModifiedTime(PRTime* lastModifiedTime) override; + NS_IMETHOD GetCorsIncludeCredentials(bool* aInclude) override; + NS_IMETHOD SetCorsIncludeCredentials(bool aInclude) override; + NS_IMETHOD GetCorsMode(uint32_t* aCorsMode) override; + NS_IMETHOD SetCorsMode(uint32_t aCorsMode) override; + NS_IMETHOD GetRedirectMode(uint32_t* aRedirectMode) override; + NS_IMETHOD SetRedirectMode(uint32_t aRedirectMode) override; + NS_IMETHOD GetFetchCacheMode(uint32_t* aFetchCacheMode) override; + NS_IMETHOD SetFetchCacheMode(uint32_t aFetchCacheMode) override; + NS_IMETHOD GetTopWindowURI(nsIURI** aTopWindowURI) override; + NS_IMETHOD SetTopWindowURIIfUnknown(nsIURI* aTopWindowURI) override; + NS_IMETHOD GetProxyURI(nsIURI** proxyURI) override; + virtual void SetCorsPreflightParameters( + const nsTArray& unsafeHeaders, + bool aShouldStripRequestBodyHeader) override; + virtual void SetAltDataForChild(bool aIsForChild) override; + virtual void DisableAltDataCache() override { + StoreDisableAltDataCache(true); + }; + + NS_IMETHOD GetConnectionInfoHashKey( + nsACString& aConnectionInfoHashKey) override; + NS_IMETHOD GetIntegrityMetadata(nsAString& aIntegrityMetadata) override; + NS_IMETHOD SetIntegrityMetadata(const nsAString& aIntegrityMetadata) override; + NS_IMETHOD GetLastRedirectFlags(uint32_t* aValue) override; + NS_IMETHOD SetLastRedirectFlags(uint32_t aValue) override; + NS_IMETHOD GetNavigationStartTimeStamp(TimeStamp* aTimeStamp) override; + NS_IMETHOD SetNavigationStartTimeStamp(TimeStamp aTimeStamp) override; + NS_IMETHOD CancelByURLClassifier(nsresult aErrorCode) override; + virtual void SetIPv4Disabled(void) override; + virtual void SetIPv6Disabled(void) override; + NS_IMETHOD GetCrossOriginOpenerPolicy( + nsILoadInfo::CrossOriginOpenerPolicy* aCrossOriginOpenerPolicy) override; + NS_IMETHOD ComputeCrossOriginOpenerPolicy( + nsILoadInfo::CrossOriginOpenerPolicy aInitiatorPolicy, + nsILoadInfo::CrossOriginOpenerPolicy* aOutPolicy) override; + NS_IMETHOD HasCrossOriginOpenerPolicyMismatch(bool* aIsMismatch) override; + NS_IMETHOD GetResponseEmbedderPolicy( + nsILoadInfo::CrossOriginEmbedderPolicy* aOutPolicy) override; + virtual bool GetHasNonEmptySandboxingFlag() override { + return LoadHasNonEmptySandboxingFlag(); + } + virtual void SetHasNonEmptySandboxingFlag( + bool aHasNonEmptySandboxingFlag) override { + StoreHasNonEmptySandboxingFlag(aHasNonEmptySandboxingFlag); + } + + inline void CleanRedirectCacheChainIfNecessary() { + mRedirectedCachekeys = nullptr; + } + NS_IMETHOD HTTPUpgrade(const nsACString& aProtocolName, + nsIHttpUpgradeListener* aListener) override; + void DoDiagnosticAssertWhenOnStopNotCalledOnDestroy() override; + + NS_IMETHOD SetWaitForHTTPSSVCRecord() override; + + // nsISupportsPriority + NS_IMETHOD GetPriority(int32_t* value) override; + NS_IMETHOD AdjustPriority(int32_t delta) override; + + // nsIClassOfService + NS_IMETHOD GetClassFlags(uint32_t* outFlags) override { + *outFlags = mClassOfService; + return NS_OK; + } + + // nsIResumableChannel + NS_IMETHOD GetEntityID(nsACString& aEntityID) override; + + // nsIConsoleReportCollector + void AddConsoleReport(uint32_t aErrorFlags, const nsACString& aCategory, + nsContentUtils::PropertiesFile aPropertiesFile, + const nsACString& aSourceFileURI, uint32_t aLineNumber, + uint32_t aColumnNumber, const nsACString& aMessageName, + const nsTArray& aStringParams) override; + + void FlushReportsToConsole( + uint64_t aInnerWindowID, + ReportAction aAction = ReportAction::Forget) override; + + void FlushReportsToConsoleForServiceWorkerScope( + const nsACString& aScope, + ReportAction aAction = ReportAction::Forget) override; + + void FlushConsoleReports( + dom::Document* aDocument, + ReportAction aAction = ReportAction::Forget) override; + + void FlushConsoleReports( + nsILoadGroup* aLoadGroup, + ReportAction aAction = ReportAction::Forget) override; + + void FlushConsoleReports(nsIConsoleReportCollector* aCollector) override; + + void StealConsoleReports( + nsTArray& aReports) override; + + void ClearConsoleReports() override; + + class nsContentEncodings : public nsStringEnumeratorBase { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIUTF8STRINGENUMERATOR + + using nsStringEnumeratorBase::GetNext; + + nsContentEncodings(nsIHttpChannel* aChannel, const char* aEncodingHeader); + + private: + virtual ~nsContentEncodings() = default; + + [[nodiscard]] nsresult PrepareForNext(void); + + // We do not own the buffer. The channel owns it. + const char* mEncodingHeader; + const char* mCurStart; // points to start of current header + const char* mCurEnd; // points to end of current header + + // Hold a ref to our channel so that it can't go away and take the + // header with it. + nsCOMPtr mChannel; + + bool mReady; + }; + + nsHttpResponseHead* GetResponseHead() const { return mResponseHead.get(); } + nsHttpRequestHead* GetRequestHead() { return &mRequestHead; } + nsHttpHeaderArray* GetResponseTrailers() const { + return mResponseTrailers.get(); + } + + const NetAddr& GetSelfAddr() { return mSelfAddr; } + const NetAddr& GetPeerAddr() { return mPeerAddr; } + + [[nodiscard]] nsresult OverrideSecurityInfo(nsISupports* aSecurityInfo); + + public: /* Necko internal use only... */ + int64_t GetAltDataLength() { return mAltDataLength; } + bool IsNavigation(); + + bool IsDeliveringAltData() const { return LoadDeliveringAltData(); } + + static void PropagateReferenceIfNeeded(nsIURI* aURI, + nsCOMPtr& aRedirectURI); + + // Return whether upon a redirect code of httpStatus for method, the + // request method should be rewritten to GET. + static bool ShouldRewriteRedirectToGET( + uint32_t httpStatus, nsHttpRequestHead::ParsedMethodType method); + + // Like nsIEncodedChannel::DoApplyConversions except context is set to + // mListenerContext. + [[nodiscard]] nsresult DoApplyContentConversions( + nsIStreamListener* aNextListener, nsIStreamListener** aNewNextListener); + + // Callback on STS thread called by CopyComplete when NS_AsyncCopy() + // is finished. This function works as a proxy function to dispatch + // |EnsureUploadStreamIsCloneableComplete| to main thread. + virtual void OnCopyComplete(nsresult aStatus); + + void AddClassificationFlags(uint32_t aFlags, bool aIsThirdParty); + + void SetFlashPluginState(nsIHttpChannel::FlashPluginState aState); + + const uint64_t& ChannelId() const { return mChannelId; } + + void InternalSetUploadStream(nsIInputStream* uploadStream) { + mUploadStream = uploadStream; + } + + void InternalSetUploadStreamLength(uint64_t aLength) { + mReqContentLength = aLength; + } + + void SetUploadStreamHasHeaders(bool hasHeaders) { + StoreUploadStreamHasHeaders(hasHeaders); + } + + virtual nsresult SetReferrerHeader(const nsACString& aReferrer, + bool aRespectBeforeConnect = true) { + if (aRespectBeforeConnect) { + ENSURE_CALLED_BEFORE_CONNECT(); + } + return mRequestHead.SetHeader(nsHttp::Referer, aReferrer); + } + + nsresult ClearReferrerHeader() { + ENSURE_CALLED_BEFORE_CONNECT(); + return mRequestHead.ClearHeader(nsHttp::Referer); + } + + void SetTopWindowURI(nsIURI* aTopWindowURI) { mTopWindowURI = aTopWindowURI; } + + // Set referrerInfo and compute the referrer header if neccessary. + // Pass true for aSetOriginal if this is a new referrer and should + // overwrite the 'original' value, false if this is a mutation (like + // stripping the path). + nsresult SetReferrerInfoInternal(nsIReferrerInfo* aReferrerInfo, bool aClone, + bool aCompute, bool aRespectBeforeConnect); + + struct ReplacementChannelConfig { + ReplacementChannelConfig() = default; + explicit ReplacementChannelConfig( + const dom::ReplacementChannelConfigInit& aInit); + + uint32_t redirectFlags = 0; + uint32_t classOfService = 0; + Maybe privateBrowsing = Nothing(); + Maybe method; + nsCOMPtr referrerInfo; + Maybe timedChannel; + nsCOMPtr uploadStream; + uint64_t uploadStreamLength; + bool uploadStreamHasHeaders; + Maybe contentType; + Maybe contentLength; + + dom::ReplacementChannelConfigInit Serialize(dom::ContentParent* aParent); + }; + + enum class ReplacementReason { + Redirect, + InternalRedirect, + DocumentChannel, + }; + + // Create a ReplacementChannelConfig object that can be used to duplicate the + // current channel. + ReplacementChannelConfig CloneReplacementChannelConfig( + bool aPreserveMethod, uint32_t aRedirectFlags, ReplacementReason aReason); + + static void ConfigureReplacementChannel(nsIChannel*, + const ReplacementChannelConfig&, + ReplacementReason); + + // Called before we create the redirect target channel. + already_AddRefed CloneLoadInfoForRedirect( + nsIURI* aNewURI, uint32_t aRedirectFlags); + + // True if we've already applied content conversion to the data + // passed to mListener. + bool HasAppliedConversion() { return LoadHasAppliedConversion(); } + + protected: + nsresult GetTopWindowURI(nsIURI* aURIBeingLoaded, nsIURI** aTopWindowURI); + + // Handle notifying listener, removing from loadgroup if request failed. + void DoNotifyListener(); + virtual void DoNotifyListenerCleanup() = 0; + + // drop reference to listener, its callbacks, and the progress sink + virtual void ReleaseListeners(); + + // Call AsyncAbort(). + virtual void DoAsyncAbort(nsresult aStatus) = 0; + + // This is fired only when a cookie is created due to the presence of + // Set-Cookie header in the response header of any network request. + // This notification will come only after the "http-on-examine-response" + // was fired. + void NotifySetCookie(const nsACString& aCookie); + + mozilla::dom::PerformanceStorage* GetPerformanceStorage(); + void MaybeReportTimingData(); + nsIURI* GetReferringPage(); + nsPIDOMWindowInner* GetInnerDOMWindow(); + + void AddCookiesToRequest(); + [[nodiscard]] virtual nsresult SetupReplacementChannel( + nsIURI*, nsIChannel*, bool preserveMethod, uint32_t redirectFlags); + + // bundle calling OMR observers and marking flag into one function + inline void CallOnModifyRequestObservers() { + gHttpHandler->OnModifyRequest(this); + MOZ_ASSERT(!LoadRequestObserversCalled()); + StoreRequestObserversCalled(true); + } + + // Helper function to simplify getting notification callbacks. + template + void GetCallback(nsCOMPtr& aResult) { + NS_QueryNotificationCallbacks(mCallbacks, mLoadGroup, + NS_GET_TEMPLATE_IID(T), + getter_AddRefs(aResult)); + } + + // Redirect tracking + // Checks whether or not aURI and mOriginalURI share the same domain. + virtual bool SameOriginWithOriginalUri(nsIURI* aURI); + + // GetPrincipal Returns the channel's URI principal. + nsIPrincipal* GetURIPrincipal(); + + [[nodiscard]] bool BypassServiceWorker() const; + + // Returns true if this channel should intercept the network request and + // prepare for a possible synthesized response instead. + bool ShouldIntercept(nsIURI* aURI = nullptr); + + // Callback on main thread when NS_AsyncCopy() is finished populating + // the new mUploadStream. + void EnsureUploadStreamIsCloneableComplete(nsresult aStatus); + +#ifdef DEBUG + // Check if mPrivateBrowsingId matches between LoadInfo and LoadContext. + void AssertPrivateBrowsingId(); +#endif + + static void CallTypeSniffers(void* aClosure, const uint8_t* aData, + uint32_t aCount); + + nsresult CheckRedirectLimit(uint32_t aRedirectFlags) const; + + bool MaybeWaitForUploadStreamLength(nsIStreamListener* aListener, + nsISupports* aContext); + + void MaybeFlushConsoleReports(); + + bool IsBrowsingContextDiscarded() const; + + nsresult ProcessCrossOriginEmbedderPolicyHeader(); + + nsresult ProcessCrossOriginResourcePolicyHeader(); + + nsresult ComputeCrossOriginOpenerPolicyMismatch(); + + nsresult ValidateMIMEType(); + + friend class PrivateBrowsingChannel; + friend class InterceptFailedOnStop; + + protected: + // this section is for main-thread-only object + // all the references need to be proxy released on main thread. + nsCOMPtr mURI; + nsCOMPtr mOriginalURI; + nsCOMPtr mDocumentURI; + nsCOMPtr mLoadGroup; + nsCOMPtr mLoadInfo; + nsCOMPtr mCallbacks; + nsCOMPtr mProgressSink; + nsCOMPtr mReferrerInfo; + nsCOMPtr mApplicationCache; + nsCOMPtr mAPIRedirectToURI; + nsCOMPtr mProxyURI; + nsCOMPtr mPrincipal; + nsCOMPtr mTopWindowURI; + nsCOMPtr mListener; + // An instance of nsHTTPCompressConv + nsCOMPtr mCompressListener; + nsCOMPtr mCurrentThread; + + private: + // WHATWG Fetch Standard 4.4. HTTP-redirect fetch, step 10 + bool ShouldTaintReplacementChannelOrigin(nsIURI* aNewURI); + + // Proxy release all members above on main thread. + void ReleaseMainThreadOnlyReferences(); + + void ExplicitSetUploadStreamLength(uint64_t aContentLength, + bool aStreamHasHeaders); + + void MaybeResumeAsyncOpen(); + + protected: + nsCString mSpec; // ASCII encoded URL spec + nsCString mContentTypeHint; + nsCString mContentCharsetHint; + nsCString mUserSetCookieHeader; + // HTTP Upgrade Data + nsCString mUpgradeProtocol; + // Resumable channel specific data + nsCString mEntityID; + // The initiator type (for this resource) - how was the resource referenced in + // the HTML file. + nsString mInitiatorType; + // Holds the name of the preferred alt-data type for each contentType. + nsTArray mPreferredCachedAltDataTypes; + // Holds the name of the alternative data type the channel returned. + nsCString mAvailableCachedAltDataType; + nsString mIntegrityMetadata; + + // Classified channel's matched information + nsCString mMatchedList; + nsCString mMatchedProvider; + nsCString mMatchedFullHash; + + nsTArray mMatchedTrackingLists; + nsTArray mMatchedTrackingFullHashes; + + nsCOMPtr mOwner; + + nsHttpRequestHead mRequestHead; + // Upload throttling. + nsCOMPtr mThrottleQueue; + nsCOMPtr mUploadStream; + nsCOMPtr mUploadCloneableCallback; + UniquePtr mResponseHead; + UniquePtr mResponseTrailers; + RefPtr mConnectionInfo; + nsCOMPtr mProxyInfo; + nsCOMPtr mSecurityInfo; + nsCOMPtr mUpgradeProtocolCallback; + UniquePtr mContentDispositionFilename; + nsCOMPtr mReportCollector; + + RefPtr mHttpHandler; // keep gHttpHandler alive + UniquePtr> mRedirectedCachekeys; + nsCOMPtr mRequestContext; + + NetAddr mSelfAddr; + NetAddr mPeerAddr; + + nsTArray> mSecurityConsoleMessages; + nsTArray mUnsafeHeaders; + + // A time value equal to the starting time of the fetch that initiates the + // redirect. + mozilla::TimeStamp mRedirectStartTimeStamp; + // A time value equal to the time immediately after receiving the last byte of + // the response of the last redirect. + mozilla::TimeStamp mRedirectEndTimeStamp; + + PRTime mChannelCreationTime; + TimeStamp mChannelCreationTimestamp; + TimeStamp mAsyncOpenTime; + TimeStamp mCacheReadStart; + TimeStamp mCacheReadEnd; + TimeStamp mLaunchServiceWorkerStart; + TimeStamp mLaunchServiceWorkerEnd; + TimeStamp mDispatchFetchEventStart; + TimeStamp mDispatchFetchEventEnd; + TimeStamp mHandleFetchEventStart; + TimeStamp mHandleFetchEventEnd; + // copied from the transaction before we null out mTransaction + // so that the timing can still be queried from OnStopRequest + TimingStruct mTransactionTimings; + + // Gets computed during ComputeCrossOriginOpenerPolicyMismatch so we have + // the channel's policy even if we don't know policy initiator. + nsILoadInfo::CrossOriginOpenerPolicy mComputedCrossOriginOpenerPolicy; + + uint64_t mStartPos; + uint64_t mTransferSize; + uint64_t mRequestSize; + uint64_t mDecodedBodySize; + // True only when the channel supports any of the versions of HTTP3 + bool mSupportsHTTP3; + uint64_t mEncodedBodySize; + uint64_t mRequestContextID; + // ID of the top-level document's inner window this channel is being + // originated from. + uint64_t mContentWindowId; + uint64_t mTopLevelOuterContentWindowId; + int64_t mAltDataLength; + uint64_t mChannelId; + uint64_t mReqContentLength; + + Atomic mStatus; + + // Use Release-Acquire ordering to ensure the OMT ODA is ignored while channel + // is canceled on main thread. + Atomic mCanceled; + Atomic mFirstPartyClassificationFlags; + Atomic mThirdPartyClassificationFlags; + Atomic mFlashPluginState; + + UniquePtr mSource; + + uint32_t mLoadFlags; + uint32_t mCaps; + uint32_t mClassOfService; + + // clang-format off + MOZ_ATOMIC_BITFIELDS(mAtomicBitfields1, 32, ( + (uint32_t, UpgradeToSecure, 1), + (uint32_t, ApplyConversion, 1), + // Set to true if DoApplyContentConversions has been applied to + // our default mListener. + (uint32_t, HasAppliedConversion, 1), + (uint32_t, IsPending, 1), + (uint32_t, WasOpened, 1), + // if 1 all "http-on-{opening|modify|etc}-request" observers have been + // called. + (uint32_t, RequestObserversCalled, 1), + (uint32_t, ResponseHeadersModified, 1), + (uint32_t, AllowSTS, 1), + (uint32_t, ThirdPartyFlags, 3), + (uint32_t, UploadStreamHasHeaders, 1), + (uint32_t, InheritApplicationCache, 1), + (uint32_t, ChooseApplicationCache, 1), + (uint32_t, LoadedFromApplicationCache, 1), + (uint32_t, ChannelIsForDownload, 1), + (uint32_t, TracingEnabled, 1), + // True if timing collection is enabled + (uint32_t, TimingEnabled, 1), + (uint32_t, ReportTiming, 1), + (uint32_t, AllowSpdy, 1), + (uint32_t, AllowHttp3, 1), + (uint32_t, AllowAltSvc, 1), + // !!! This is also used by the URL classifier to exempt channels from + // classification. If this is changed or removed, make sure we also update + // NS_ShouldClassifyChannel accordingly !!! + (uint32_t, BeConservative, 1), + // If the current channel is used to as a TRR connection. + (uint32_t, IsTRRServiceChannel, 1), + // If the request was performed to a TRR resolved IP address. + // Will be false if loading the resource does not create a connection + // (for example when it's loaded from the cache). + (uint32_t, ResolvedByTRR, 1), + (uint32_t, ResponseTimeoutEnabled, 1), + // A flag that should be false only if a cross-domain redirect occurred + (uint32_t, AllRedirectsSameOrigin, 1), + + // Is 1 if no redirects have occured or if all redirects + // pass the Resource Timing timing-allow-check + (uint32_t, AllRedirectsPassTimingAllowCheck, 1), + + // True if this channel was intercepted and could receive a synthesized + // response. + (uint32_t, ResponseCouldBeSynthesized, 1), + + (uint32_t, BlockAuthPrompt, 1), + + // If true, we behave as if the LOAD_FROM_CACHE flag has been set. + // Used to enforce that flag's behavior but not expose it externally. + (uint32_t, AllowStaleCacheContent, 1), + + // If true, we prefer the LOAD_FROM_CACHE flag over LOAD_BYPASS_CACHE or + // LOAD_BYPASS_LOCAL_CACHE. + (uint32_t, PreferCacheLoadOverBypass, 1) + )) + + // Broken up into two bitfields to avoid alignment requirements of uint64_t. + // (Too many bits used for one uint32_t.) + MOZ_ATOMIC_BITFIELDS(mAtomicBitfields2, 32, ( + // True iff this request has been calculated in its request context as + // a non tail request. We must remove it again when this channel is done. + (uint32_t, AddedAsNonTailRequest, 1), + + // True if AsyncOpen() is called when the stream length is still unknown. + // AsyncOpen() will be retriggered when InputStreamLengthHelper execs the + // callback, passing the stream length value. + (uint32_t, AsyncOpenWaitingForStreamLength, 1), + + // Defaults to true. This is set to false when it is no longer possible + // to upgrade the request to a secure channel. + (uint32_t, UpgradableToSecure, 1), + + // True if the docshell's sandboxing flag set is not empty. + (uint32_t, HasNonEmptySandboxingFlag, 1), + + // Tainted origin flag of a request, specified by + // WHATWG Fetch Standard 2.2.5. + (uint32_t, TaintedOriginFlag, 1) + )) + // clang-format on + + // An opaque flags for non-standard behavior of the TLS system. + // It is unlikely this will need to be set outside of telemetry studies + // relating to the TLS implementation. + uint32_t mTlsFlags; + + // Current suspension depth for this channel object + uint32_t mSuspendCount; + + // Per channel transport window override (0 means no override) + uint32_t mInitialRwin; + + uint32_t mProxyResolveFlags; + + uint32_t mContentDispositionHint; + + uint32_t mCorsMode; + uint32_t mRedirectMode; + + // If this channel was created as the result of a redirect, then this value + // will reflect the redirect flags passed to the SetupReplacementChannel() + // method. + uint32_t mLastRedirectFlags; + + int16_t mPriority; + uint8_t mRedirectionLimit; + + // Performance tracking + // Number of redirects that has occurred. + int8_t mRedirectCount; + // Number of internal redirects that has occurred. + int8_t mInternalRedirectCount; + + // clang-format off + MOZ_ATOMIC_BITFIELDS(mAtomicBitfields3, 8, ( + (bool, AsyncOpenTimeOverriden, 1), + (bool, ForcePending, 1), + + // true if the channel is deliving alt-data. + (bool, DeliveringAltData, 1), + + (bool, CorsIncludeCredentials, 1), + + // These parameters are used to ensure that we do not call OnStartRequest + // and OnStopRequest more than once. + (bool, OnStartRequestCalled, 1), + (bool, OnStopRequestCalled, 1), + + // Defaults to false. Is set to true at the begining of OnStartRequest. + // Used to ensure methods can't be called before OnStartRequest. + (bool, AfterOnStartRequestBegun, 1), + + (bool, RequireCORSPreflight, 1) + )) + + // Broken up into two bitfields to avoid alignment requirements of uint16_t. + // (Too many bits used for one uint8_t.) + MOZ_ATOMIC_BITFIELDS(mAtomicBitfields4, 8, ( + // This flag will be true if the consumer is requesting alt-data AND the + // consumer is in the child process. + (bool, AltDataForChild, 1), + // This flag will be true if the consumer cannot process alt-data. This + // is used in the webextension StreamFilter handler. If true, we bypass + // using alt-data for the request. + (bool, DisableAltDataCache, 1), + + (bool, ForceMainDocumentChannel, 1), + // This is set true if the channel is waiting for the + // InputStreamLengthHelper::GetAsyncLength callback. + (bool, PendingInputStreamLengthOperation, 1), + + // Set to true if our listener has indicated that it requires + // content conversion to be done by us. + (bool, ListenerRequiresContentConversion, 1), + + // True if this is a navigation to a page with a different cross origin + // opener policy ( see ComputeCrossOriginOpenerPolicyMismatch ) + (uint32_t, HasCrossOriginOpenerPolicyMismatch, 1) + )) + // clang-format on + + bool EnsureRequestContextID(); + bool EnsureRequestContext(); + + // Adds/removes this channel as a non-tailed request in its request context + // these helpers ensure we add it only once and remove it only when added + // via AddedAsNonTailRequest member tracking. + void AddAsNonTailRequest(); + void RemoveAsNonTailRequest(); + + void EnsureTopLevelOuterContentWindowId(); +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(HttpBaseChannel, HTTP_BASE_CHANNEL_IID) + +// Share some code while working around C++'s absurd inability to handle casting +// of member functions between base/derived types. +// - We want to store member function pointer to call at resume time, but one +// such function--HandleAsyncAbort--we want to share between the +// nsHttpChannel/HttpChannelChild. Can't define it in base class, because +// then we'd have to cast member function ptr between base/derived class +// types. Sigh... +template +class HttpAsyncAborter { + public: + explicit HttpAsyncAborter(T* derived) + : mThis(derived), mCallOnResume(nullptr) {} + + // Aborts channel: calls OnStart/Stop with provided status, removes channel + // from loadGroup. + [[nodiscard]] nsresult AsyncAbort(nsresult status); + + // Does most the actual work. + void HandleAsyncAbort(); + + // AsyncCall calls a member function asynchronously (via an event). + // retval isn't refcounted and is set only when event was successfully + // posted, the event is returned for the purpose of cancelling when needed + [[nodiscard]] virtual nsresult AsyncCall( + void (T::*funcPtr)(), nsRunnableMethod** retval = nullptr); + + private: + T* mThis; + + protected: + // Function to be called at resume time + std::function mCallOnResume; +}; + +template +[[nodiscard]] nsresult HttpAsyncAborter::AsyncAbort(nsresult status) { + MOZ_LOG(gHttpLog, LogLevel::Debug, + ("HttpAsyncAborter::AsyncAbort [this=%p status=%" PRIx32 "]\n", mThis, + static_cast(status))); + + mThis->mStatus = status; + + // if this fails? Callers ignore our return value anyway.... + return AsyncCall(&T::HandleAsyncAbort); +} + +// Each subclass needs to define its own version of this (which just calls this +// base version), else we wind up casting base/derived member function ptrs +template +inline void HttpAsyncAborter::HandleAsyncAbort() { + MOZ_ASSERT(!mCallOnResume, "How did that happen?"); + + if (mThis->mSuspendCount) { + MOZ_LOG( + gHttpLog, LogLevel::Debug, + ("Waiting until resume to do async notification [this=%p]\n", mThis)); + mCallOnResume = [](T* self) { + self->HandleAsyncAbort(); + return NS_OK; + }; + return; + } + + mThis->DoNotifyListener(); + + // finally remove ourselves from the load group. + if (mThis->mLoadGroup) + mThis->mLoadGroup->RemoveRequest(mThis, nullptr, mThis->mStatus); +} + +template +nsresult HttpAsyncAborter::AsyncCall(void (T::*funcPtr)(), + nsRunnableMethod** retval) { + nsresult rv; + + RefPtr> event = + NewRunnableMethod("net::HttpAsyncAborter::AsyncCall", mThis, funcPtr); + rv = NS_DispatchToCurrentThread(event); + if (NS_SUCCEEDED(rv) && retval) { + *retval = event; + } + + return rv; +} + +class ProxyReleaseRunnable final : public mozilla::Runnable { + public: + explicit ProxyReleaseRunnable(nsTArray>&& aDoomed) + : Runnable("ProxyReleaseRunnable"), mDoomed(std::move(aDoomed)) {} + + NS_IMETHOD + Run() override { + mDoomed.Clear(); + return NS_OK; + } + + private: + virtual ~ProxyReleaseRunnable() = default; + + nsTArray> mDoomed; +}; + +} // namespace net +} // namespace mozilla + +#endif // mozilla_net_HttpBaseChannel_h diff --git a/netwerk/protocol/http/HttpChannelChild.cpp b/netwerk/protocol/http/HttpChannelChild.cpp new file mode 100644 index 0000000000..fe757d6072 --- /dev/null +++ b/netwerk/protocol/http/HttpChannelChild.cpp @@ -0,0 +1,3156 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et 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/. */ + +// HttpLog.h should generally be included first +#include "HttpLog.h" + +#include "nsHttp.h" +#include "nsICacheEntry.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/Unused.h" +#include "mozilla/dom/ContentChild.h" +#include "mozilla/dom/DocGroup.h" +#include "mozilla/dom/ServiceWorkerUtils.h" +#include "mozilla/dom/BrowserChild.h" +#include "mozilla/extensions/StreamFilterParent.h" +#include "mozilla/ipc/FileDescriptorSetChild.h" +#include "mozilla/ipc/IPCStreamUtils.h" +#include "mozilla/net/NeckoChild.h" +#include "mozilla/net/HttpChannelChild.h" +#include "mozilla/net/UrlClassifierCommon.h" +#include "mozilla/net/UrlClassifierFeatureFactory.h" + +#include "AltDataOutputStreamChild.h" +#include "CookieServiceChild.h" +#include "HttpBackgroundChannelChild.h" +#include "nsCOMPtr.h" +#include "nsContentPolicyUtils.h" +#include "nsDOMNavigationTiming.h" +#include "nsGlobalWindow.h" +#include "nsStringStream.h" +#include "nsHttpChannel.h" +#include "nsHttpHandler.h" +#include "nsNetUtil.h" +#include "nsSerializationHelper.h" +#include "mozilla/Attributes.h" +#include "mozilla/dom/PerformanceStorage.h" +#include "mozilla/ipc/InputStreamUtils.h" +#include "mozilla/ipc/URIUtils.h" +#include "mozilla/ipc/BackgroundUtils.h" +#include "mozilla/net/DNS.h" +#include "mozilla/net/SocketProcessBridgeChild.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/StaticPrefs_network.h" +#include "mozilla/StoragePrincipalHelper.h" +#include "SerializedLoadContext.h" +#include "nsInputStreamPump.h" +#include "InterceptedChannel.h" +#include "nsContentSecurityManager.h" +#include "nsICompressConvStats.h" +#include "nsIDeprecationWarner.h" +#include "mozilla/dom/Document.h" +#include "nsIScriptError.h" +#include "nsISerialEventTarget.h" +#include "nsRedirectHistoryEntry.h" +#include "nsSocketTransportService2.h" +#include "nsStreamUtils.h" +#include "nsThreadUtils.h" +#include "nsCORSListenerProxy.h" +#include "nsApplicationCache.h" +#include "ClassifierDummyChannel.h" +#include "nsIOService.h" + +#ifdef MOZ_TASK_TRACER +# include "GeckoTaskTracer.h" +#endif + +#include + +using namespace mozilla::dom; +using namespace mozilla::ipc; + +namespace mozilla { +namespace net { + +//----------------------------------------------------------------------------- +// HttpChannelChild +//----------------------------------------------------------------------------- + +HttpChannelChild::HttpChannelChild() + : HttpAsyncAborter(this), + NeckoTargetHolder(nullptr), + mBgChildMutex("HttpChannelChild::BgChildMutex"), + mEventTargetMutex("HttpChannelChild::EventTargetMutex"), + mCacheEntryId(0), + mCacheKey(0), + mCacheFetchCount(0), + mCacheExpirationTime(nsICacheEntry::NO_EXPIRATION_TIME), + mDeletingChannelSent(false), + mIsFromCache(false), + mIsRacing(false), + mCacheNeedToReportBytesReadInitialized(false), + mNeedToReportBytesRead(true), +#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED + mBackgroundChildQueueFinalState(BCKCHILD_UNKNOWN), +#endif + mCacheEntryAvailable(false), + mAltDataCacheEntryAvailable(false), + mSendResumeAt(false), + mKeptAlive(false), + mIPCActorDeleted(false), + mSuspendSent(false), + mIsLastPartOfMultiPart(false), + mSuspendForWaitCompleteRedirectSetup(false), + mRecvOnStartRequestSentCalled(false), + mSuspendedByWaitingForPermissionCookie(false) { + LOG(("Creating HttpChannelChild @%p\n", this)); + + mChannelCreationTime = PR_Now(); + mChannelCreationTimestamp = TimeStamp::Now(); + mLastStatusReported = + mChannelCreationTimestamp; // in case we enable the profiler after Init() + mAsyncOpenTime = TimeStamp::Now(); + mEventQ = new ChannelEventQueue(static_cast(this)); + + // Ensure that the cookie service is initialized before the first + // IPC HTTP channel is created. + // We require that the parent cookie service actor exists while + // processing HTTP responses. + RefPtr cookieService = CookieServiceChild::GetSingleton(); +} + +HttpChannelChild::~HttpChannelChild() { + LOG(("Destroying HttpChannelChild @%p\n", this)); + +#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED + if (mDoDiagnosticAssertWhenOnStopNotCalledOnDestroy && mAsyncOpenSucceeded && + !mSuccesfullyRedirected && !LoadOnStopRequestCalled()) { + bool emptyBgChildQueue, nullBgChild; + { + MutexAutoLock lock(mBgChildMutex); + nullBgChild = !mBgChild; + emptyBgChildQueue = !nullBgChild && mBgChild->IsQueueEmpty(); + } + + uint32_t flags = + (mRedirectChannelChild ? 1 << 0 : 0) | + (mEventQ->IsEmpty() ? 1 << 1 : 0) | (nullBgChild ? 1 << 2 : 0) | + (emptyBgChildQueue ? 1 << 3 : 0) | + (LoadOnStartRequestCalled() ? 1 << 4 : 0) | + (mBackgroundChildQueueFinalState == BCKCHILD_EMPTY ? 1 << 5 : 0) | + (mBackgroundChildQueueFinalState == BCKCHILD_NON_EMPTY ? 1 << 6 : 0) | + (mRemoteChannelExistedAtCancel ? 1 << 7 : 0) | + (mEverHadBgChildAtAsyncOpen ? 1 << 8 : 0) | + (mEverHadBgChildAtConnectParent ? 1 << 9 : 0) | + (mCreateBackgroundChannelFailed ? 1 << 10 : 0) | + (mBgInitFailCallbackTriggered ? 1 << 11 : 0) | + (mCanSendAtCancel ? 1 << 12 : 0) | (!!mSuspendCount ? 1 << 13 : 0) | + (!!mCallOnResume ? 1 << 14 : 0); + MOZ_CRASH_UNSAFE_PRINTF( + "~HttpChannelChild, LoadOnStopRequestCalled()=false, mStatus=0x%08x, " + "mActorDestroyReason=%d, 20200717 flags=%u", + static_cast(nsresult(mStatus)), + static_cast(mActorDestroyReason ? *mActorDestroyReason : -1), + flags); + } +#endif + + ReleaseMainThreadOnlyReferences(); +} + +void HttpChannelChild::ReleaseMainThreadOnlyReferences() { + if (NS_IsMainThread()) { + // Already on main thread, let dtor to + // take care of releasing references + return; + } + + NS_ReleaseOnMainThread("HttpChannelChild::mRedirectChannelChild", + mRedirectChannelChild.forget()); +} +//----------------------------------------------------------------------------- +// HttpChannelChild::nsISupports +//----------------------------------------------------------------------------- + +NS_IMPL_ADDREF(HttpChannelChild) + +NS_IMETHODIMP_(MozExternalRefCountType) HttpChannelChild::Release() { + if (!NS_IsMainThread()) { + nsrefcnt count = mRefCnt; + nsresult rv = NS_DispatchToMainThread(NewNonOwningRunnableMethod( + "HttpChannelChild::Release", this, &HttpChannelChild::Release)); + + // Continue Release procedure if failed to dispatch to main thread. + if (!NS_WARN_IF(NS_FAILED(rv))) { + return count - 1; + } + } + + nsrefcnt count = --mRefCnt; + MOZ_ASSERT(int32_t(count) >= 0, "dup release"); + + // Normally we Send_delete in OnStopRequest, but when we need to retain the + // remote channel for security info IPDL itself holds 1 reference, so we + // Send_delete when refCnt==1. But if !CanSend(), then there's nobody to send + // to, so we fall through. + if (mKeptAlive && count == 1 && CanSend()) { + NS_LOG_RELEASE(this, 1, "HttpChannelChild"); + mKeptAlive = false; + // We send a message to the parent, which calls SendDelete, and then the + // child calling Send__delete__() to finally drop the refcount to 0. + TrySendDeletingChannel(); + return 1; + } + + if (count == 0) { + mRefCnt = 1; /* stabilize */ + + // We don't have a listener when AsyncOpen has failed or when this channel + // has been sucessfully redirected. + if (MOZ_LIKELY(LoadOnStartRequestCalled() && LoadOnStopRequestCalled()) || + !mListener) { + NS_LOG_RELEASE(this, 0, "HttpChannelChild"); + delete this; + return 0; + } + + // This makes sure we fulfill the stream listener contract all the time. + if (NS_SUCCEEDED(mStatus)) { + mStatus = NS_ERROR_ABORT; + } + + // Turn the stabilization refcount into a regular strong reference. + + // 1) We tell refcount logging about the "stabilization" AddRef, which + // will become the reference for |channel|. We do this first so that we + // don't tell refcount logging that the refcount has dropped to zero, which + // it will interpret as destroying the object. + NS_LOG_ADDREF(this, 2, "HttpChannelChild", sizeof(*this)); + + // 2) We tell refcount logging about the original call to Release(). + NS_LOG_RELEASE(this, 1, "HttpChannelChild"); + + // 3) Finally, we turn the reference into a regular smart pointer. + RefPtr channel = dont_AddRef(this); + + // This runnable will create a strong reference to |this|. + NS_DispatchToMainThread( + NewRunnableMethod("~HttpChannelChild>DoNotifyListener", channel, + &HttpChannelChild::DoNotifyListener)); + + // If NS_DispatchToMainThread failed then we're going to leak the runnable, + // and thus the channel, so there's no need to do anything else. + + // We should have already done any special handling for the refcount = 1 + // case when the refcount first went from 2 to 1. We don't want it to happen + // when |channel| is destroyed. + MOZ_ASSERT(!mKeptAlive || !CanSend()); + + // XXX If std::move(channel) is allowed, then we don't have to have extra + // checks for the refcount going from 2 to 1. See bug 1680217. + + // This will release the stabilization refcount, which is necessary to avoid + // a leak. + channel = nullptr; + + return mRefCnt; + } + + NS_LOG_RELEASE(this, count, "HttpChannelChild"); + return count; +} + +NS_INTERFACE_MAP_BEGIN(HttpChannelChild) + NS_INTERFACE_MAP_ENTRY(nsIRequest) + NS_INTERFACE_MAP_ENTRY(nsIChannel) + NS_INTERFACE_MAP_ENTRY(nsIHttpChannel) + NS_INTERFACE_MAP_ENTRY(nsIHttpChannelInternal) + NS_INTERFACE_MAP_ENTRY_CONDITIONAL(nsICacheInfoChannel, + !mMultiPartID.isSome()) + NS_INTERFACE_MAP_ENTRY(nsIResumableChannel) + NS_INTERFACE_MAP_ENTRY(nsISupportsPriority) + NS_INTERFACE_MAP_ENTRY(nsIClassOfService) + NS_INTERFACE_MAP_ENTRY(nsIProxiedChannel) + NS_INTERFACE_MAP_ENTRY(nsITraceableChannel) + NS_INTERFACE_MAP_ENTRY_CONDITIONAL(nsIApplicationCacheContainer, + !mMultiPartID.isSome()) + NS_INTERFACE_MAP_ENTRY(nsIApplicationCacheChannel) + NS_INTERFACE_MAP_ENTRY(nsIAsyncVerifyRedirectCallback) + NS_INTERFACE_MAP_ENTRY(nsIChildChannel) + NS_INTERFACE_MAP_ENTRY(nsIHttpChannelChild) + NS_INTERFACE_MAP_ENTRY_CONDITIONAL(nsIMultiPartChannel, mMultiPartID.isSome()) + NS_INTERFACE_MAP_ENTRY_CONDITIONAL(nsIThreadRetargetableRequest, + !mMultiPartID.isSome()) + NS_INTERFACE_MAP_ENTRY_CONCRETE(HttpChannelChild) +NS_INTERFACE_MAP_END_INHERITING(HttpBaseChannel) + +//----------------------------------------------------------------------------- +// HttpChannelChild::PHttpChannelChild +//----------------------------------------------------------------------------- + +void HttpChannelChild::OnBackgroundChildReady( + HttpBackgroundChannelChild* aBgChild) { + LOG(("HttpChannelChild::OnBackgroundChildReady [this=%p, bgChild=%p]\n", this, + aBgChild)); + MOZ_ASSERT(OnSocketThread()); + + { + MutexAutoLock lock(mBgChildMutex); + + // mBgChild might be removed or replaced while the original background + // channel is inited on STS thread. + if (mBgChild != aBgChild) { + return; + } + + MOZ_ASSERT(mBgInitFailCallback); + mBgInitFailCallback = nullptr; + } +} + +void HttpChannelChild::OnBackgroundChildDestroyed( + HttpBackgroundChannelChild* aBgChild) { + LOG(("HttpChannelChild::OnBackgroundChildDestroyed [this=%p]\n", this)); + // This function might be called during shutdown phase, so OnSocketThread() + // might return false even on STS thread. Use IsOnCurrentThreadInfallible() + // to get correct information. + MOZ_ASSERT(gSocketTransportService); + MOZ_ASSERT(gSocketTransportService->IsOnCurrentThreadInfallible()); + + nsCOMPtr callback; + { + MutexAutoLock lock(mBgChildMutex); + + // mBgChild might be removed or replaced while the original background + // channel is destroyed on STS thread. + if (aBgChild != mBgChild) { + return; + } + + mBgChild = nullptr; + callback = std::move(mBgInitFailCallback); + } + + if (callback) { +#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED + mBgInitFailCallbackTriggered = true; +#endif + nsCOMPtr neckoTarget = GetNeckoTarget(); + neckoTarget->Dispatch(callback, NS_DISPATCH_NORMAL); + } +} + +void HttpChannelChild::AssociateApplicationCache(const nsCString& aGroupID, + const nsCString& aClientID) { + LOG(("HttpChannelChild::AssociateApplicationCache [this=%p]\n", this)); + mApplicationCache = new nsApplicationCache(); + + StoreLoadedFromApplicationCache(true); + mApplicationCache->InitAsHandle(aGroupID, aClientID); +} + +mozilla::ipc::IPCResult HttpChannelChild::RecvOnStartRequestSent() { + LOG(("HttpChannelChild::RecvOnStartRequestSent [this=%p]\n", this)); + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!mRecvOnStartRequestSentCalled); + + mRecvOnStartRequestSentCalled = true; + + if (mSuspendedByWaitingForPermissionCookie) { + mSuspendedByWaitingForPermissionCookie = false; + mEventQ->Resume(); + } + return IPC_OK(); +} + +void HttpChannelChild::ProcessOnStartRequest( + const nsHttpResponseHead& aResponseHead, const bool& aUseResponseHead, + const nsHttpHeaderArray& aRequestHeaders, + const HttpChannelOnStartRequestArgs& aArgs) { + LOG(("HttpChannelChild::ProcessOnStartRequest [this=%p]\n", this)); + MOZ_ASSERT(OnSocketThread()); + + mEventQ->RunOrEnqueue(new NeckoTargetChannelFunctionEvent( + this, [self = UnsafePtr(this), aResponseHead, + aUseResponseHead, aRequestHeaders, aArgs]() { + self->OnStartRequest(aResponseHead, aUseResponseHead, aRequestHeaders, + aArgs); + })); +} + +static void ResourceTimingStructArgsToTimingsStruct( + const ResourceTimingStructArgs& aArgs, TimingStruct& aTimings) { + aTimings.domainLookupStart = aArgs.domainLookupStart(); + aTimings.domainLookupEnd = aArgs.domainLookupEnd(); + aTimings.connectStart = aArgs.connectStart(); + aTimings.tcpConnectEnd = aArgs.tcpConnectEnd(); + aTimings.secureConnectionStart = aArgs.secureConnectionStart(); + aTimings.connectEnd = aArgs.connectEnd(); + aTimings.requestStart = aArgs.requestStart(); + aTimings.responseStart = aArgs.responseStart(); + aTimings.responseEnd = aArgs.responseEnd(); +} + +void HttpChannelChild::OnStartRequest( + const nsHttpResponseHead& aResponseHead, const bool& aUseResponseHead, + const nsHttpHeaderArray& aRequestHeaders, + const HttpChannelOnStartRequestArgs& aArgs) { + LOG(("HttpChannelChild::OnStartRequest [this=%p]\n", this)); + + // If this channel was aborted by ActorDestroy, then there may be other + // OnStartRequest/OnStopRequest/OnDataAvailable IPC messages that need to + // be handled. In that case we just ignore them to avoid calling the listener + // twice. + if (LoadOnStartRequestCalled() && mIPCActorDeleted) { + return; + } + + // Copy arguments only. It's possible to handle other IPC between + // OnStartRequest and DoOnStartRequest. + mComputedCrossOriginOpenerPolicy = aArgs.openerPolicy(); + + if (!mCanceled && NS_SUCCEEDED(mStatus)) { + mStatus = aArgs.channelStatus(); + } + + // Cookies headers should not be visible to the child process + MOZ_ASSERT(!aRequestHeaders.HasHeader(nsHttp::Cookie)); + MOZ_ASSERT(!nsHttpResponseHead(aResponseHead).HasHeader(nsHttp::Set_Cookie)); + + if (aUseResponseHead && !mCanceled) + mResponseHead = MakeUnique(aResponseHead); + + if (!aArgs.securityInfoSerialization().IsEmpty()) { + [[maybe_unused]] nsresult rv = NS_DeserializeObject( + aArgs.securityInfoSerialization(), getter_AddRefs(mSecurityInfo)); + MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv), + "Deserializing security info should not fail"); + } + + ipc::MergeParentLoadInfoForwarder(aArgs.loadInfoForwarder(), mLoadInfo); + + mIsFromCache = aArgs.isFromCache(); + mIsRacing = aArgs.isRacing(); + mCacheEntryAvailable = aArgs.cacheEntryAvailable(); + mCacheEntryId = aArgs.cacheEntryId(); + mCacheFetchCount = aArgs.cacheFetchCount(); + mCacheExpirationTime = aArgs.cacheExpirationTime(); + mSelfAddr = aArgs.selfAddr(); + mPeerAddr = aArgs.peerAddr(); + + mRedirectCount = aArgs.redirectCount(); + mAvailableCachedAltDataType = aArgs.altDataType(); + StoreDeliveringAltData(aArgs.deliveringAltData()); + mAltDataLength = aArgs.altDataLength(); + StoreResolvedByTRR(aArgs.isResolvedByTRR()); + + SetApplyConversion(aArgs.applyConversion()); + + StoreAfterOnStartRequestBegun(true); + + AutoEventEnqueuer ensureSerialDispatch(mEventQ); + + mCacheKey = aArgs.cacheKey(); + + // replace our request headers with what actually got sent in the parent + mRequestHead.SetHeaders(aRequestHeaders); + + // Note: this is where we would notify "http-on-examine-response" observers. + // We have deliberately disabled this for child processes (see bug 806753) + // + // gHttpHandler->OnExamineResponse(this); + + ResourceTimingStructArgsToTimingsStruct(aArgs.timing(), mTransactionTimings); + + StoreAllRedirectsSameOrigin(aArgs.allRedirectsSameOrigin()); + + mMultiPartID = aArgs.multiPartID(); + mIsLastPartOfMultiPart = aArgs.isLastPartOfMultiPart(); + + if (!aArgs.appCacheGroupId().IsEmpty() && + !aArgs.appCacheClientId().IsEmpty()) { + AssociateApplicationCache(aArgs.appCacheGroupId(), + aArgs.appCacheClientId()); + } + + if (aArgs.overrideReferrerInfo()) { + // The arguments passed to SetReferrerInfoInternal here should mirror the + // arguments passed in + // nsHttpChannel::ReEvaluateReferrerAfterTrackingStatusIsKnown(), except for + // aRespectBeforeConnect which we pass false here since we're intentionally + // overriding the referrer after BeginConnect(). + Unused << SetReferrerInfoInternal(aArgs.overrideReferrerInfo(), false, true, + false); + } + + if (!aArgs.cookie().IsEmpty()) { + SetCookie(aArgs.cookie()); + } + + if (aArgs.shouldWaitForOnStartRequestSent() && + !mRecvOnStartRequestSentCalled) { + LOG((" > pending DoOnStartRequest until RecvOnStartRequestSent\n")); + MOZ_ASSERT(NS_IsMainThread()); + + mEventQ->Suspend(); + mSuspendedByWaitingForPermissionCookie = true; + mEventQ->PrependEvent(MakeUnique( + this, [self = UnsafePtr(this)]() { + self->DoOnStartRequest(self, nullptr); + })); + return; + } + + // Remember whether HTTP3 is supported + if (mResponseHead && (mResponseHead->Version() == HttpVersion::v2_0) && + (mResponseHead->Status() < 500) && (mResponseHead->Status() != 421)) { + nsAutoCString altSvc; + Unused << mResponseHead->GetHeader(nsHttp::Alternate_Service, altSvc); + if (!altSvc.IsEmpty() || nsHttp::IsReasonableHeaderValue(altSvc)) { + for (uint32_t i = 0; i < kHttp3VersionCount; i++) { + if (PL_strstr(altSvc.get(), kHttp3Versions[i].get())) { + mSupportsHTTP3 = true; + break; + } + } + } + } + + DoOnStartRequest(this, nullptr); +} + +void HttpChannelChild::ProcessOnAfterLastPart(const nsresult& aStatus) { + LOG(("HttpChannelChild::ProcessOnAfterLastPart [this=%p]\n", this)); + MOZ_ASSERT(OnSocketThread()); + mEventQ->RunOrEnqueue(new NeckoTargetChannelFunctionEvent( + this, [self = UnsafePtr(this), aStatus]() { + self->OnAfterLastPart(aStatus); + })); +} + +void HttpChannelChild::OnAfterLastPart(const nsresult& aStatus) { + if (LoadOnStopRequestCalled()) { + return; + } + StoreOnStopRequestCalled(true); + + // notify "http-on-stop-connect" observers + gHttpHandler->OnStopRequest(this); + + ReleaseListeners(); + + // If a preferred alt-data type was set, the parent would hold a reference to + // the cache entry in case the child calls openAlternativeOutputStream(). + // (see nsHttpChannel::OnStopRequest) + if (!mPreferredCachedAltDataTypes.IsEmpty()) { + mAltDataCacheEntryAvailable = mCacheEntryAvailable; + } + mCacheEntryAvailable = false; + + if (mLoadGroup) mLoadGroup->RemoveRequest(this, nullptr, mStatus); + CleanupBackgroundChannel(); + + if (mLoadFlags & LOAD_DOCUMENT_URI) { + // Keep IPDL channel open, but only for updating security info. + // If IPDL is already closed, then do nothing. + if (CanSend()) { + mKeptAlive = true; + SendDocumentChannelCleanup(true); + } + } else { + // The parent process will respond by sending a DeleteSelf message and + // making sure not to send any more messages after that. + TrySendDeletingChannel(); + } +} + +void HttpChannelChild::DoOnStartRequest(nsIRequest* aRequest, + nsISupports* aContext) { + nsresult rv; + + LOG(("HttpChannelChild::DoOnStartRequest [this=%p]\n", this)); + + // We handle all the listener chaining before OnStartRequest at this moment. + // Prevent additional listeners being added to the chain after the request + // as started. + StoreTracingEnabled(false); + + // mListener could be null if the redirect setup is not completed. + MOZ_ASSERT(mListener || LoadOnStartRequestCalled()); + if (!mListener) { + Cancel(NS_ERROR_FAILURE); + return; + } + + if (mListener) { + nsCOMPtr listener(mListener); + StoreOnStartRequestCalled(true); + rv = listener->OnStartRequest(aRequest); + } else { + rv = NS_ERROR_UNEXPECTED; + } + StoreOnStartRequestCalled(true); + + if (NS_FAILED(rv)) { + Cancel(rv); + return; + } + + nsCOMPtr listener; + rv = DoApplyContentConversions(mListener, getter_AddRefs(listener), nullptr); + if (NS_FAILED(rv)) { + Cancel(rv); + } else if (listener) { + mListener = listener; + mCompressListener = listener; + } +} + +void HttpChannelChild::ProcessOnTransportAndData( + const nsresult& aChannelStatus, const nsresult& aTransportStatus, + const uint64_t& aOffset, const uint32_t& aCount, const nsCString& aData) { + LOG(("HttpChannelChild::ProcessOnTransportAndData [this=%p]\n", this)); + MOZ_ASSERT(OnSocketThread()); + mEventQ->RunOrEnqueue(new ChannelFunctionEvent( + [self = UnsafePtr(this)]() { + return self->GetODATarget(); + }, + [self = UnsafePtr(this), aChannelStatus, + aTransportStatus, aOffset, aCount, aData]() { + self->OnTransportAndData(aChannelStatus, aTransportStatus, aOffset, + aCount, aData); + })); +} + +void HttpChannelChild::OnTransportAndData(const nsresult& aChannelStatus, + const nsresult& aTransportStatus, + const uint64_t& aOffset, + const uint32_t& aCount, + const nsCString& aData) { + LOG(("HttpChannelChild::OnTransportAndData [this=%p]\n", this)); + + if (!mCanceled && NS_SUCCEEDED(mStatus)) { + mStatus = aChannelStatus; + } + + if (mCanceled || NS_FAILED(mStatus)) { + return; + } + + // Hold queue lock throughout all three calls, else we might process a later + // necko msg in between them. + AutoEventEnqueuer ensureSerialDispatch(mEventQ); + + int64_t progressMax; + if (NS_FAILED(GetContentLength(&progressMax))) { + progressMax = -1; + } + + const int64_t progress = aOffset + aCount; + + // OnTransportAndData will be run on retargeted thread if applicable, however + // OnStatus/OnProgress event can only be fired on main thread. We need to + // dispatch the status/progress event handling back to main thread with the + // appropriate event target for networking. + if (NS_IsMainThread()) { + DoOnStatus(this, aTransportStatus); + DoOnProgress(this, progress, progressMax); + } else { + RefPtr self = this; + nsCOMPtr neckoTarget = GetNeckoTarget(); + MOZ_ASSERT(neckoTarget); + + DebugOnly rv = neckoTarget->Dispatch( + NS_NewRunnableFunction( + "net::HttpChannelChild::OnTransportAndData", + [self, aTransportStatus, progress, progressMax]() { + self->DoOnStatus(self, aTransportStatus); + self->DoOnProgress(self, progress, progressMax); + }), + NS_DISPATCH_NORMAL); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + + // OnDataAvailable + // + // NOTE: the OnDataAvailable contract requires the client to read all the data + // in the inputstream. This code relies on that ('data' will go away after + // this function). Apparently the previous, non-e10s behavior was to actually + // support only reading part of the data, allowing later calls to read the + // rest. + nsCOMPtr stringStream; + nsresult rv = + NS_NewByteInputStream(getter_AddRefs(stringStream), + Span(aData).To(aCount), NS_ASSIGNMENT_DEPEND); + if (NS_FAILED(rv)) { + Cancel(rv); + return; + } + + DoOnDataAvailable(this, nullptr, stringStream, aOffset, aCount); + stringStream->Close(); + + // TODO: Bug 1523916 backpressure needs to take into account if the data is + // coming from the main process or from the socket process via PBackground. + if (NeedToReportBytesRead()) { + mUnreportBytesRead += aCount; + if (mUnreportBytesRead >= gHttpHandler->SendWindowSize() >> 2) { + if (NS_IsMainThread()) { + Unused << SendBytesRead(mUnreportBytesRead); + } else { + // PHttpChannel connects to the main thread + RefPtr self = this; + int32_t bytesRead = mUnreportBytesRead; + nsCOMPtr neckoTarget = GetNeckoTarget(); + MOZ_ASSERT(neckoTarget); + + DebugOnly rv = neckoTarget->Dispatch( + NS_NewRunnableFunction("net::HttpChannelChild::SendBytesRead", + [self, bytesRead]() { + Unused << self->SendBytesRead(bytesRead); + }), + NS_DISPATCH_NORMAL); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + mUnreportBytesRead = 0; + } + } +} + +bool HttpChannelChild::NeedToReportBytesRead() { + if (mCacheNeedToReportBytesReadInitialized) { + return mNeedToReportBytesRead; + } + + // Might notify parent for partial cache, and the IPC message is ignored by + // parent. + int64_t contentLength = -1; + if (gHttpHandler->SendWindowSize() == 0 || mIsFromCache || + NS_FAILED(GetContentLength(&contentLength)) || + contentLength < gHttpHandler->SendWindowSize()) { + mNeedToReportBytesRead = false; + } + + mCacheNeedToReportBytesReadInitialized = true; + return mNeedToReportBytesRead; +} + +void HttpChannelChild::DoOnStatus(nsIRequest* aRequest, nsresult status) { + LOG(("HttpChannelChild::DoOnStatus [this=%p]\n", this)); + MOZ_ASSERT(NS_IsMainThread()); + + if (mCanceled) return; + + // cache the progress sink so we don't have to query for it each time. + if (!mProgressSink) GetCallback(mProgressSink); + + // block status/progress after Cancel or OnStopRequest has been called, + // or if channel has LOAD_BACKGROUND set. + if (mProgressSink && NS_SUCCEEDED(mStatus) && LoadIsPending() && + !(mLoadFlags & LOAD_BACKGROUND)) { + nsAutoCString host; + mURI->GetHost(host); + mProgressSink->OnStatus(aRequest, status, + NS_ConvertUTF8toUTF16(host).get()); + } +} + +void HttpChannelChild::DoOnProgress(nsIRequest* aRequest, int64_t progress, + int64_t progressMax) { + LOG(("HttpChannelChild::DoOnProgress [this=%p]\n", this)); + MOZ_ASSERT(NS_IsMainThread()); + + if (mCanceled) return; + + // cache the progress sink so we don't have to query for it each time. + if (!mProgressSink) GetCallback(mProgressSink); + + // block status/progress after Cancel or OnStopRequest has been called, + // or if channel has LOAD_BACKGROUND set. + if (mProgressSink && NS_SUCCEEDED(mStatus) && LoadIsPending()) { + // OnProgress + // + if (progress > 0) { + mProgressSink->OnProgress(aRequest, progress, progressMax); + } + } +} + +void HttpChannelChild::DoOnDataAvailable(nsIRequest* aRequest, + nsISupports* aContext, + nsIInputStream* aStream, + uint64_t aOffset, uint32_t aCount) { + AUTO_PROFILER_LABEL("HttpChannelChild::DoOnDataAvailable", NETWORK); + LOG(("HttpChannelChild::DoOnDataAvailable [this=%p]\n", this)); + if (mCanceled) return; + + if (mListener) { + nsCOMPtr listener(mListener); + nsresult rv = listener->OnDataAvailable(aRequest, aStream, aOffset, aCount); + if (NS_FAILED(rv)) { + CancelOnMainThread(rv); + } + } +} + +void HttpChannelChild::ProcessOnStopRequest( + const nsresult& aChannelStatus, const ResourceTimingStructArgs& aTiming, + const nsHttpHeaderArray& aResponseTrailers, + nsTArray&& aConsoleReports, + bool aFromSocketProcess) { + LOG( + ("HttpChannelChild::ProcessOnStopRequest [this=%p, " + "aFromSocketProcess=%d]\n", + this, aFromSocketProcess)); + MOZ_ASSERT(OnSocketThread()); + + mEventQ->RunOrEnqueue(new NeckoTargetChannelFunctionEvent( + this, [self = UnsafePtr(this), aChannelStatus, aTiming, + aResponseTrailers, + consoleReports = CopyableTArray{aConsoleReports.Clone()}, + aFromSocketProcess]() mutable { + self->OnStopRequest(aChannelStatus, aTiming, aResponseTrailers); + if (!aFromSocketProcess) { + self->DoOnConsoleReport(std::move(consoleReports)); + self->ContinueOnStopRequest(); + } + })); +} + +void HttpChannelChild::ProcessOnConsoleReport( + nsTArray&& aConsoleReports) { + LOG(("HttpChannelChild::ProcessOnConsoleReport [this=%p]\n", this)); + MOZ_ASSERT(OnSocketThread()); + + mEventQ->RunOrEnqueue(new NeckoTargetChannelFunctionEvent( + this, + [self = UnsafePtr(this), + consoleReports = CopyableTArray{aConsoleReports.Clone()}]() mutable { + self->DoOnConsoleReport(std::move(consoleReports)); + self->ContinueOnStopRequest(); + })); +} + +void HttpChannelChild::DoOnConsoleReport( + nsTArray&& aConsoleReports) { + if (aConsoleReports.IsEmpty()) { + return; + } + + for (ConsoleReportCollected& report : aConsoleReports) { + if (report.propertiesFile() < + nsContentUtils::PropertiesFile::PropertiesFile_COUNT) { + AddConsoleReport(report.errorFlags(), report.category(), + nsContentUtils::PropertiesFile(report.propertiesFile()), + report.sourceFileURI(), report.lineNumber(), + report.columnNumber(), report.messageName(), + report.stringParams()); + } + } + MaybeFlushConsoleReports(); +} + +void HttpChannelChild::OnStopRequest( + const nsresult& aChannelStatus, const ResourceTimingStructArgs& aTiming, + const nsHttpHeaderArray& aResponseTrailers) { + LOG(("HttpChannelChild::OnStopRequest [this=%p status=%" PRIx32 "]\n", this, + static_cast(aChannelStatus))); + MOZ_ASSERT(NS_IsMainThread()); + + // If this channel was aborted by ActorDestroy, then there may be other + // OnStartRequest/OnStopRequest/OnDataAvailable IPC messages that need to + // be handled. In that case we just ignore them to avoid calling the listener + // twice. + if (LoadOnStopRequestCalled() && mIPCActorDeleted) { + return; + } + + nsCOMPtr conv = do_QueryInterface(mCompressListener); + if (conv) { + conv->GetDecodedDataLength(&mDecodedBodySize); + } + + ResourceTimingStructArgsToTimingsStruct(aTiming, mTransactionTimings); + + // Do not overwrite or adjust the original mAsyncOpenTime by timing.fetchStart + // We must use the original child process time in order to account for child + // side work and IPC transit overhead. + // XXX: This depends on TimeStamp being equivalent across processes. + // This is true for modern hardware but for older platforms it is not always + // true. + + mRedirectStartTimeStamp = aTiming.redirectStart(); + mRedirectEndTimeStamp = aTiming.redirectEnd(); + mTransferSize = aTiming.transferSize(); + mEncodedBodySize = aTiming.encodedBodySize(); + mProtocolVersion = aTiming.protocolVersion(); + + mCacheReadStart = aTiming.cacheReadStart(); + mCacheReadEnd = aTiming.cacheReadEnd(); + +#ifdef MOZ_GECKO_PROFILER + if (profiler_can_accept_markers()) { + nsAutoCString requestMethod; + GetRequestMethod(requestMethod); + nsAutoCString contentType; + if (mResponseHead) { + mResponseHead->ContentType(contentType); + } + int32_t priority = PRIORITY_NORMAL; + GetPriority(&priority); + profiler_add_network_marker( + mURI, requestMethod, priority, mChannelId, NetworkLoadType::LOAD_STOP, + mLastStatusReported, TimeStamp::Now(), mTransferSize, kCacheUnknown, + mLoadInfo->GetInnerWindowID(), &mTransactionTimings, nullptr, + std::move(mSource), Some(nsDependentCString(contentType.get()))); + } +#endif + + mResponseTrailers = MakeUnique(aResponseTrailers); + + DoPreOnStopRequest(aChannelStatus); + + { // We must flush the queue before we Send__delete__ + // (although we really shouldn't receive any msgs after OnStop), + // so make sure this goes out of scope before then. + AutoEventEnqueuer ensureSerialDispatch(mEventQ); + + DoOnStopRequest(this, aChannelStatus, nullptr); + // DoOnStopRequest() calls ReleaseListeners() + } +} + +void HttpChannelChild::ContinueOnStopRequest() { + // If we're a multi-part stream, then don't cleanup yet, and we'll do so + // in OnAfterLastPart. + if (mMultiPartID) { + LOG( + ("HttpChannelChild::OnStopRequest - Expecting future parts on a " + "multipart channel postpone cleaning up.")); + return; + } + + CleanupBackgroundChannel(); + + // If there is a possibility we might want to write alt data to the cache + // entry, we keep the channel alive. We still send the DocumentChannelCleanup + // message but request the cache entry to be kept by the parent. + // If the channel has failed, the cache entry is in a non-writtable state and + // we want to release it to not block following consumers. + if (NS_SUCCEEDED(mStatus) && !mPreferredCachedAltDataTypes.IsEmpty()) { + mKeptAlive = true; + SendDocumentChannelCleanup(false); // don't clear cache entry + return; + } + + if (mLoadFlags & LOAD_DOCUMENT_URI) { + // Keep IPDL channel open, but only for updating security info. + // If IPDL is already closed, then do nothing. + if (CanSend()) { + mKeptAlive = true; + SendDocumentChannelCleanup(true); + } + } else { + // The parent process will respond by sending a DeleteSelf message and + // making sure not to send any more messages after that. + TrySendDeletingChannel(); + } +} + +void HttpChannelChild::DoPreOnStopRequest(nsresult aStatus) { + AUTO_PROFILER_LABEL("HttpChannelChild::DoPreOnStopRequest", NETWORK); + LOG(("HttpChannelChild::DoPreOnStopRequest [this=%p status=%" PRIx32 "]\n", + this, static_cast(aStatus))); + StoreIsPending(false); + + MaybeReportTimingData(); + + if (!mCanceled && NS_SUCCEEDED(mStatus)) { + mStatus = aStatus; + } + + CollectOMTTelemetry(); +} + +void HttpChannelChild::CollectOMTTelemetry() { + MOZ_ASSERT(NS_IsMainThread()); + + // Only collect telemetry for HTTP channel that is loaded successfully and + // completely. + if (mCanceled || NS_FAILED(mStatus)) { + return; + } + + // Use content policy type to accumulate data by usage. + nsAutoCString key( + NS_CP_ContentTypeName(mLoadInfo->InternalContentPolicyType())); + + Telemetry::AccumulateCategoricalKeyed(key, mOMTResult); +} + +void HttpChannelChild::DoOnStopRequest(nsIRequest* aRequest, + nsresult aChannelStatus, + nsISupports* aContext) { + AUTO_PROFILER_LABEL("HttpChannelChild::DoOnStopRequest", NETWORK); + LOG(("HttpChannelChild::DoOnStopRequest [this=%p]\n", this)); + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!LoadIsPending()); + + auto checkForBlockedContent = [&]() { + // NB: We use aChannelStatus here instead of mStatus because if there was an + // nsCORSListenerProxy on this request, it will override the tracking + // protection's return value. + if (UrlClassifierFeatureFactory::IsClassifierBlockingErrorCode( + aChannelStatus) || + aChannelStatus == NS_ERROR_MALWARE_URI || + aChannelStatus == NS_ERROR_UNWANTED_URI || + aChannelStatus == NS_ERROR_BLOCKED_URI || + aChannelStatus == NS_ERROR_HARMFUL_URI || + aChannelStatus == NS_ERROR_PHISHING_URI) { + nsCString list, provider, fullhash; + + nsresult rv = GetMatchedList(list); + NS_ENSURE_SUCCESS_VOID(rv); + + rv = GetMatchedProvider(provider); + NS_ENSURE_SUCCESS_VOID(rv); + + rv = GetMatchedFullHash(fullhash); + NS_ENSURE_SUCCESS_VOID(rv); + + UrlClassifierCommon::SetBlockedContent(this, aChannelStatus, list, + provider, fullhash); + } + }; + checkForBlockedContent(); + + // See bug 1587686. If the redirect setup is not completed, the post-redirect + // channel will be not opened and mListener will be null. + MOZ_ASSERT(mListener || !LoadWasOpened()); + if (!mListener) { + return; + } + + MOZ_ASSERT(!LoadOnStopRequestCalled(), + "We should not call OnStopRequest twice"); + + if (mListener) { + nsCOMPtr listener(mListener); + StoreOnStopRequestCalled(true); + listener->OnStopRequest(aRequest, mStatus); + } + StoreOnStopRequestCalled(true); + + // If we're a multi-part stream, then don't cleanup yet, and we'll do so + // in OnAfterLastPart. + if (mMultiPartID) { + LOG( + ("HttpChannelChild::DoOnStopRequest - Expecting future parts on a " + "multipart channel not releasing listeners.")); + StoreOnStopRequestCalled(false); + StoreOnStartRequestCalled(false); + return; + } + + // notify "http-on-stop-connect" observers + gHttpHandler->OnStopRequest(this); + + ReleaseListeners(); + + // If a preferred alt-data type was set, the parent would hold a reference to + // the cache entry in case the child calls openAlternativeOutputStream(). + // (see nsHttpChannel::OnStopRequest) + if (!mPreferredCachedAltDataTypes.IsEmpty()) { + mAltDataCacheEntryAvailable = mCacheEntryAvailable; + } + mCacheEntryAvailable = false; + + if (mLoadGroup) mLoadGroup->RemoveRequest(this, nullptr, mStatus); +} + +void HttpChannelChild::ProcessOnProgress(const int64_t& aProgress, + const int64_t& aProgressMax) { + MOZ_ASSERT(OnSocketThread()); + LOG(("HttpChannelChild::ProcessOnProgress [this=%p]\n", this)); + mEventQ->RunOrEnqueue(new NeckoTargetChannelFunctionEvent( + this, + [self = UnsafePtr(this), aProgress, aProgressMax]() { + AutoEventEnqueuer ensureSerialDispatch(self->mEventQ); + self->DoOnProgress(self, aProgress, aProgressMax); + })); +} + +void HttpChannelChild::ProcessOnStatus(const nsresult& aStatus) { + MOZ_ASSERT(OnSocketThread()); + LOG(("HttpChannelChild::ProcessOnStatus [this=%p]\n", this)); + mEventQ->RunOrEnqueue(new NeckoTargetChannelFunctionEvent( + this, [self = UnsafePtr(this), aStatus]() { + AutoEventEnqueuer ensureSerialDispatch(self->mEventQ); + self->DoOnStatus(self, aStatus); + })); +} + +mozilla::ipc::IPCResult HttpChannelChild::RecvFailedAsyncOpen( + const nsresult& aStatus) { + LOG(("HttpChannelChild::RecvFailedAsyncOpen [this=%p]\n", this)); + mEventQ->RunOrEnqueue(new NeckoTargetChannelFunctionEvent( + this, [self = UnsafePtr(this), aStatus]() { + self->FailedAsyncOpen(aStatus); + })); + return IPC_OK(); +} + +// We need to have an implementation of this function just so that we can keep +// all references to mCallOnResume of type HttpChannelChild: it's not OK in C++ +// to set a member function ptr to a base class function. +void HttpChannelChild::HandleAsyncAbort() { + HttpAsyncAborter::HandleAsyncAbort(); + + // Ignore all the messages from background channel after channel aborted. + CleanupBackgroundChannel(); +} + +void HttpChannelChild::FailedAsyncOpen(const nsresult& status) { + LOG(("HttpChannelChild::FailedAsyncOpen [this=%p status=%" PRIx32 "]\n", this, + static_cast(status))); + MOZ_ASSERT(NS_IsMainThread()); + + // Might be called twice in race condition in theory. + // (one by RecvFailedAsyncOpen, another by + // HttpBackgroundChannelChild::ActorFailed) + if (LoadOnStartRequestCalled()) { + return; + } + + if (NS_SUCCEEDED(mStatus)) { + mStatus = status; + } + + // We're already being called from IPDL, therefore already "async" + HandleAsyncAbort(); + + if (CanSend()) { + TrySendDeletingChannel(); + } +} + +void HttpChannelChild::CleanupBackgroundChannel() { + MutexAutoLock lock(mBgChildMutex); + + AUTO_PROFILER_LABEL("HttpChannelChild::CleanupBackgroundChannel", NETWORK); + LOG(("HttpChannelChild::CleanupBackgroundChannel [this=%p bgChild=%p]\n", + this, mBgChild.get())); + + mBgInitFailCallback = nullptr; + + if (!mBgChild) { + return; + } + + RefPtr bgChild = std::move(mBgChild); + + MOZ_RELEASE_ASSERT(gSocketTransportService); + if (!OnSocketThread()) { + gSocketTransportService->Dispatch( + NewRunnableMethod("HttpBackgroundChannelChild::OnChannelClosed", + bgChild, + &HttpBackgroundChannelChild::OnChannelClosed), + NS_DISPATCH_NORMAL); + } else { + bgChild->OnChannelClosed(); + } +} + +void HttpChannelChild::DoNotifyListenerCleanup() { + LOG(("HttpChannelChild::DoNotifyListenerCleanup [this=%p]\n", this)); +} + +void HttpChannelChild::DoAsyncAbort(nsresult aStatus) { + Unused << AsyncAbort(aStatus); +} + +mozilla::ipc::IPCResult HttpChannelChild::RecvDeleteSelf() { + LOG(("HttpChannelChild::RecvDeleteSelf [this=%p]\n", this)); + MOZ_ASSERT(NS_IsMainThread()); + + // The redirection is vetoed. No need to suspend the event queue. + if (mSuspendForWaitCompleteRedirectSetup) { + mSuspendForWaitCompleteRedirectSetup = false; + mEventQ->Resume(); + } + + mEventQ->RunOrEnqueue(new NeckoTargetChannelFunctionEvent( + this, + [self = UnsafePtr(this)]() { self->DeleteSelf(); })); + return IPC_OK(); +} + +void HttpChannelChild::DeleteSelf() { Send__delete__(this); } + +void HttpChannelChild::NotifyOrReleaseListeners(nsresult rv) { + MOZ_ASSERT(NS_IsMainThread()); + + if (NS_SUCCEEDED(rv) || + (LoadOnStartRequestCalled() && LoadOnStopRequestCalled())) { + ReleaseListeners(); + return; + } + + if (NS_SUCCEEDED(mStatus)) { + mStatus = rv; + } + + // This is enough what we need. Undelivered notifications will be pushed. + // DoNotifyListener ensures the call to ReleaseListeners when done. + DoNotifyListener(); +} + +void HttpChannelChild::DoNotifyListener() { + LOG(("HttpChannelChild::DoNotifyListener this=%p", this)); + MOZ_ASSERT(NS_IsMainThread()); + + // In case nsHttpChannel::OnStartRequest wasn't called (e.g. due to flag + // LOAD_ONLY_IF_MODIFIED) we want to set LoadAfterOnStartRequestBegun() to + // true before notifying listener. + if (!LoadAfterOnStartRequestBegun()) { + StoreAfterOnStartRequestBegun(true); + } + + if (mListener && !LoadOnStartRequestCalled()) { + nsCOMPtr listener = mListener; + StoreOnStartRequestCalled( + true); // avoid reentrancy bugs by setting this now + listener->OnStartRequest(this); + } + StoreOnStartRequestCalled(true); + + mEventQ->RunOrEnqueue(new NeckoTargetChannelFunctionEvent( + this, [self = UnsafePtr(this)] { + self->ContinueDoNotifyListener(); + })); +} + +void HttpChannelChild::ContinueDoNotifyListener() { + LOG(("HttpChannelChild::ContinueDoNotifyListener this=%p", this)); + MOZ_ASSERT(NS_IsMainThread()); + + // Make sure IsPending is set to false. At this moment we are done from + // the point of view of our consumer and we have to report our self + // as not-pending. + StoreIsPending(false); + + if (mListener && !LoadOnStopRequestCalled()) { + nsCOMPtr listener = mListener; + StoreOnStopRequestCalled(true); + listener->OnStopRequest(this, mStatus); + } + StoreOnStopRequestCalled(true); + + // notify "http-on-stop-request" observers + gHttpHandler->OnStopRequest(this); + + // This channel has finished its job, potentially release any tail-blocked + // requests with this. + RemoveAsNonTailRequest(); + + // We have to make sure to drop the references to listeners and callbacks + // no longer needed. + ReleaseListeners(); + + DoNotifyListenerCleanup(); + + // If this is a navigation, then we must let the docshell flush the reports + // to the console later. The LoadDocument() is pointing at the detached + // document that started the navigation. We want to show the reports on the + // new document. Otherwise the console is wiped and the user never sees + // the information. + if (!IsNavigation()) { + if (mLoadGroup) { + FlushConsoleReports(mLoadGroup); + } else { + RefPtr doc; + mLoadInfo->GetLoadingDocument(getter_AddRefs(doc)); + FlushConsoleReports(doc); + } + } +} + +mozilla::ipc::IPCResult HttpChannelChild::RecvReportSecurityMessage( + const nsString& messageTag, const nsString& messageCategory) { + DebugOnly rv = AddSecurityMessage(messageTag, messageCategory); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + return IPC_OK(); +} + +mozilla::ipc::IPCResult HttpChannelChild::RecvRedirect1Begin( + const uint32_t& aRegistrarId, const URIParams& aNewUri, + const uint32_t& aNewLoadFlags, const uint32_t& aRedirectFlags, + const ParentLoadInfoForwarderArgs& aLoadInfoForwarder, + const nsHttpResponseHead& aResponseHead, + const nsCString& aSecurityInfoSerialization, const uint64_t& aChannelId, + const NetAddr& aOldPeerAddr, const ResourceTimingStructArgs& aTiming) { + // TODO: handle security info + LOG(("HttpChannelChild::RecvRedirect1Begin [this=%p]\n", this)); + // We set peer address of child to the old peer, + // Then it will be updated to new peer in OnStartRequest + mPeerAddr = aOldPeerAddr; + + // Cookies headers should not be visible to the child process + MOZ_ASSERT(!nsHttpResponseHead(aResponseHead).HasHeader(nsHttp::Set_Cookie)); + + mEventQ->RunOrEnqueue(new NeckoTargetChannelFunctionEvent( + this, [self = UnsafePtr(this), aRegistrarId, aNewUri, + aNewLoadFlags, aRedirectFlags, aLoadInfoForwarder, aResponseHead, + aSecurityInfoSerialization, aChannelId, aTiming]() { + self->Redirect1Begin(aRegistrarId, aNewUri, aNewLoadFlags, + aRedirectFlags, aLoadInfoForwarder, aResponseHead, + aSecurityInfoSerialization, aChannelId, aTiming); + })); + return IPC_OK(); +} + +nsresult HttpChannelChild::SetupRedirect(nsIURI* uri, + const nsHttpResponseHead* responseHead, + const uint32_t& redirectFlags, + nsIChannel** outChannel) { + LOG(("HttpChannelChild::SetupRedirect [this=%p]\n", this)); + + if (mCanceled) { + return NS_ERROR_ABORT; + } + + nsresult rv; + nsCOMPtr ioService; + rv = gHttpHandler->GetIOService(getter_AddRefs(ioService)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr newChannel; + nsCOMPtr redirectLoadInfo = + CloneLoadInfoForRedirect(uri, redirectFlags); + rv = NS_NewChannelInternal(getter_AddRefs(newChannel), uri, redirectLoadInfo, + nullptr, // PerformanceStorage + nullptr, // aLoadGroup + nullptr, // aCallbacks + nsIRequest::LOAD_NORMAL, ioService); + NS_ENSURE_SUCCESS(rv, rv); + + // We won't get OnStartRequest, set cookies here. + mResponseHead = MakeUnique(*responseHead); + + bool rewriteToGET = HttpBaseChannel::ShouldRewriteRedirectToGET( + mResponseHead->Status(), mRequestHead.ParsedMethod()); + + rv = SetupReplacementChannel(uri, newChannel, !rewriteToGET, redirectFlags); + NS_ENSURE_SUCCESS(rv, rv); + + mRedirectChannelChild = do_QueryInterface(newChannel); + newChannel.forget(outChannel); + + return NS_OK; +} + +void HttpChannelChild::Redirect1Begin( + const uint32_t& registrarId, const URIParams& newOriginalURI, + const uint32_t& newLoadFlags, const uint32_t& redirectFlags, + const ParentLoadInfoForwarderArgs& loadInfoForwarder, + const nsHttpResponseHead& responseHead, + const nsACString& securityInfoSerialization, const uint64_t& channelId, + const ResourceTimingStructArgs& timing) { + nsresult rv; + + LOG(("HttpChannelChild::Redirect1Begin [this=%p]\n", this)); + + ipc::MergeParentLoadInfoForwarder(loadInfoForwarder, mLoadInfo); + + nsCOMPtr uri = DeserializeURI(newOriginalURI); + + ResourceTimingStructArgsToTimingsStruct(timing, mTransactionTimings); + +#ifdef MOZ_GECKO_PROFILER + if (profiler_can_accept_markers()) { + nsAutoCString requestMethod; + GetRequestMethod(requestMethod); + nsAutoCString contentType; + responseHead.ContentType(contentType); + + profiler_add_network_marker( + mURI, requestMethod, mPriority, mChannelId, + NetworkLoadType::LOAD_REDIRECT, mLastStatusReported, TimeStamp::Now(), + 0, kCacheUnknown, mLoadInfo->GetInnerWindowID(), &mTransactionTimings, + uri, std::move(mSource), Some(nsDependentCString(contentType.get()))); + } +#endif + + if (!securityInfoSerialization.IsEmpty()) { + rv = NS_DeserializeObject(securityInfoSerialization, + getter_AddRefs(mSecurityInfo)); + MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv), + "Deserializing security info should not fail"); + } + + nsCOMPtr newChannel; + rv = SetupRedirect(uri, &responseHead, redirectFlags, + getter_AddRefs(newChannel)); + + if (NS_SUCCEEDED(rv)) { + MOZ_ALWAYS_SUCCEEDS(newChannel->SetLoadFlags(newLoadFlags)); + + if (mRedirectChannelChild) { + // Set the channelId allocated in parent to the child instance + nsCOMPtr httpChannel = + do_QueryInterface(mRedirectChannelChild); + if (httpChannel) { + rv = httpChannel->SetChannelId(channelId); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + mRedirectChannelChild->ConnectParent(registrarId); + } + + nsCOMPtr target = GetNeckoTarget(); + MOZ_ASSERT(target); + + rv = gHttpHandler->AsyncOnChannelRedirect(this, newChannel, redirectFlags, + target); + } + + if (NS_FAILED(rv)) OnRedirectVerifyCallback(rv); +} + +mozilla::ipc::IPCResult HttpChannelChild::RecvRedirect3Complete() { + LOG(("HttpChannelChild::RecvRedirect3Complete [this=%p]\n", this)); + nsCOMPtr redirectChannel = + do_QueryInterface(mRedirectChannelChild); + MOZ_ASSERT(redirectChannel); + mEventQ->RunOrEnqueue(new NeckoTargetChannelFunctionEvent( + this, [self = UnsafePtr(this), redirectChannel]() { + nsresult rv = NS_OK; + Unused << self->GetStatus(&rv); + if (NS_FAILED(rv)) { + // Pre-redirect channel was canceled. Call |HandleAsyncAbort|, so + // mListener's OnStart/StopRequest can be called. Nothing else will + // trigger these notification after this point. + // We do this before |CompleteRedirectSetup|, so post-redirect channel + // stays unopened and we also make sure that OnStart/StopRequest won't + // be called twice. + self->HandleAsyncAbort(); + + nsCOMPtr chan = + do_QueryInterface(redirectChannel); + RefPtr httpChannelChild = + static_cast(chan.get()); + if (httpChannelChild) { + // For sending an IPC message to parent channel so that the loading + // can be cancelled. + Unused << httpChannelChild->Cancel(rv); + + // The post-redirect channel could still get OnStart/StopRequest IPC + // messages from parent, but the mListener is still null. So, we + // call |DoNotifyListener| to pretend that OnStart/StopRequest are + // already called. + httpChannelChild->DoNotifyListener(); + } + return; + } + + self->Redirect3Complete(); + })); + return IPC_OK(); +} + +void HttpChannelChild::ProcessNotifyClassificationFlags( + uint32_t aClassificationFlags, bool aIsThirdParty) { + LOG( + ("HttpChannelChild::ProcessNotifyClassificationFlags thirdparty=%d " + "flags=%" PRIu32 " [this=%p]\n", + static_cast(aIsThirdParty), aClassificationFlags, this)); + MOZ_ASSERT(OnSocketThread()); + + mEventQ->RunOrEnqueue(new NeckoTargetChannelFunctionEvent( + this, [self = UnsafePtr(this), aClassificationFlags, + aIsThirdParty]() { + self->AddClassificationFlags(aClassificationFlags, aIsThirdParty); + })); +} + +void HttpChannelChild::ProcessNotifyFlashPluginStateChanged( + nsIHttpChannel::FlashPluginState aState) { + LOG(("HttpChannelChild::ProcessNotifyFlashPluginStateChanged [this=%p]\n", + this)); + MOZ_ASSERT(OnSocketThread()); + + mEventQ->RunOrEnqueue(new NeckoTargetChannelFunctionEvent( + this, [self = UnsafePtr(this), aState]() { + self->SetFlashPluginState(aState); + })); +} + +void HttpChannelChild::ProcessSetClassifierMatchedInfo( + const nsCString& aList, const nsCString& aProvider, + const nsCString& aFullHash) { + LOG(("HttpChannelChild::ProcessSetClassifierMatchedInfo [this=%p]\n", this)); + MOZ_ASSERT(OnSocketThread()); + + mEventQ->RunOrEnqueue(new NeckoTargetChannelFunctionEvent( + this, + [self = UnsafePtr(this), aList, aProvider, + aFullHash]() { self->SetMatchedInfo(aList, aProvider, aFullHash); })); +} + +void HttpChannelChild::ProcessSetClassifierMatchedTrackingInfo( + const nsCString& aLists, const nsCString& aFullHashes) { + LOG(("HttpChannelChild::ProcessSetClassifierMatchedTrackingInfo [this=%p]\n", + this)); + MOZ_ASSERT(OnSocketThread()); + + nsTArray lists, fullhashes; + for (const nsACString& token : aLists.Split(',')) { + lists.AppendElement(token); + } + for (const nsACString& token : aFullHashes.Split(',')) { + fullhashes.AppendElement(token); + } + + mEventQ->RunOrEnqueue(new NeckoTargetChannelFunctionEvent( + this, [self = UnsafePtr(this), + lists = CopyableTArray{std::move(lists)}, + fullhashes = CopyableTArray{std::move(fullhashes)}]() { + self->SetMatchedTrackingInfo(lists, fullhashes); + })); +} + +// Completes the redirect and cleans up the old channel. +void HttpChannelChild::Redirect3Complete() { + LOG(("HttpChannelChild::Redirect3Complete [this=%p]\n", this)); + MOZ_ASSERT(NS_IsMainThread()); + + // Using an error as the default so that when we fail to forward this redirect + // to the target channel, we make sure to notify the current listener from + // CleanupRedirectingChannel. + nsresult rv = NS_BINDING_ABORTED; + + nsCOMPtr vetoHook; + GetCallback(vetoHook); + if (vetoHook) { + vetoHook->OnRedirectResult(true); + } + + // Chrome channel has been AsyncOpen'd. Reflect this in child. + if (mRedirectChannelChild) { + rv = mRedirectChannelChild->CompleteRedirectSetup(mListener); +#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED + mSuccesfullyRedirected = NS_SUCCEEDED(rv); +#endif + } + + CleanupRedirectingChannel(rv); +} + +void HttpChannelChild::CleanupRedirectingChannel(nsresult rv) { + // Redirecting to new channel: shut this down and init new channel + if (mLoadGroup) mLoadGroup->RemoveRequest(this, nullptr, NS_BINDING_ABORTED); + + if (NS_SUCCEEDED(rv)) { + nsCString remoteAddress; + Unused << GetRemoteAddress(remoteAddress); + nsCOMPtr referrer; + if (mReferrerInfo) { + referrer = mReferrerInfo->GetComputedReferrer(); + } + + nsCOMPtr entry = + new nsRedirectHistoryEntry(GetURIPrincipal(), referrer, remoteAddress); + + mLoadInfo->AppendRedirectHistoryEntry(entry, false); + } else { + NS_WARNING("CompleteRedirectSetup failed, HttpChannelChild already open?"); + } + + // Release ref to new channel. + mRedirectChannelChild = nullptr; + + NotifyOrReleaseListeners(rv); + CleanupBackgroundChannel(); +} + +//----------------------------------------------------------------------------- +// HttpChannelChild::nsIChildChannel +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +HttpChannelChild::ConnectParent(uint32_t registrarId) { + LOG(("HttpChannelChild::ConnectParent [this=%p, id=%" PRIu32 "]\n", this, + registrarId)); + MOZ_ASSERT(NS_IsMainThread()); + mozilla::dom::BrowserChild* browserChild = nullptr; + nsCOMPtr iBrowserChild; + GetCallback(iBrowserChild); + if (iBrowserChild) { + browserChild = + static_cast(iBrowserChild.get()); + } + + if (browserChild && !browserChild->IPCOpen()) { + return NS_ERROR_FAILURE; + } + + ContentChild* cc = static_cast(gNeckoChild->Manager()); + if (cc->IsShuttingDown()) { + return NS_ERROR_FAILURE; + } + + HttpBaseChannel::SetDocshellUserAgentOverride(); + + // This must happen before the constructor message is sent. Otherwise messages + // from the parent could arrive quickly and be delivered to the wrong event + // target. + SetEventTarget(); + + HttpChannelConnectArgs connectArgs(registrarId); + if (!gNeckoChild->SendPHttpChannelConstructor( + this, browserChild, IPC::SerializedLoadContext(this), connectArgs)) { + return NS_ERROR_FAILURE; + } + + { + MutexAutoLock lock(mBgChildMutex); + + MOZ_ASSERT(!mBgChild); + MOZ_ASSERT(!mBgInitFailCallback); + + mBgInitFailCallback = NewRunnableMethod( + "HttpChannelChild::OnRedirectVerifyCallback", this, + &HttpChannelChild::OnRedirectVerifyCallback, NS_ERROR_FAILURE); + + RefPtr bgChild = + new HttpBackgroundChannelChild(); + + MOZ_RELEASE_ASSERT(gSocketTransportService); + + RefPtr self = this; + nsresult rv = gSocketTransportService->Dispatch( + NewRunnableMethod>( + "HttpBackgroundChannelChild::Init", bgChild, + &HttpBackgroundChannelChild::Init, std::move(self)), + NS_DISPATCH_NORMAL); + + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + mBgChild = std::move(bgChild); +#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED + mEverHadBgChildAtConnectParent = true; +#endif + } + + // Should wait for CompleteRedirectSetup to set the listener. + mEventQ->Suspend(); + MOZ_ASSERT(!mSuspendForWaitCompleteRedirectSetup); + mSuspendForWaitCompleteRedirectSetup = true; + + // Connect to socket process after mEventQ is suspended. + MaybeConnectToSocketProcess(); + + return NS_OK; +} + +NS_IMETHODIMP +HttpChannelChild::CompleteRedirectSetup(nsIStreamListener* aListener) { + LOG(("HttpChannelChild::CompleteRedirectSetup [this=%p]\n", this)); + MOZ_ASSERT(NS_IsMainThread()); + + NS_ENSURE_TRUE(!LoadIsPending(), NS_ERROR_IN_PROGRESS); + NS_ENSURE_TRUE(!LoadWasOpened(), NS_ERROR_ALREADY_OPENED); + + // Resume the suspension in ConnectParent. + auto eventQueueResumeGuard = MakeScopeExit([&] { + MOZ_ASSERT(mSuspendForWaitCompleteRedirectSetup); + mEventQ->Resume(); + mSuspendForWaitCompleteRedirectSetup = false; + }); + + /* + * No need to check for cancel: we don't get here if nsHttpChannel canceled + * before AsyncOpen(); if it's canceled after that, OnStart/Stop will just + * get called with error code as usual. So just setup mListener and make the + * channel reflect AsyncOpen'ed state. + */ + + mLastStatusReported = TimeStamp::Now(); +#ifdef MOZ_GECKO_PROFILER + if (profiler_can_accept_markers()) { + nsAutoCString requestMethod; + GetRequestMethod(requestMethod); + + profiler_add_network_marker( + mURI, requestMethod, mPriority, mChannelId, NetworkLoadType::LOAD_START, + mChannelCreationTimestamp, mLastStatusReported, 0, kCacheUnknown, + mLoadInfo->GetInnerWindowID(), nullptr, nullptr); + } +#endif + StoreIsPending(true); + StoreWasOpened(true); + mListener = aListener; + + // add ourselves to the load group. + if (mLoadGroup) mLoadGroup->AddRequest(this, nullptr); + + // We already have an open IPDL connection to the parent. If on-modify-request + // listeners or load group observers canceled us, let the parent handle it + // and send it back to us naturally. + return NS_OK; +} + +//----------------------------------------------------------------------------- +// HttpChannelChild::nsIAsyncVerifyRedirectCallback +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +HttpChannelChild::OnRedirectVerifyCallback(nsresult aResult) { + LOG(("HttpChannelChild::OnRedirectVerifyCallback [this=%p]\n", this)); + MOZ_ASSERT(NS_IsMainThread()); + Maybe redirectURI; + nsresult rv; + + nsCOMPtr newHttpChannel = + do_QueryInterface(mRedirectChannelChild); + + if (NS_SUCCEEDED(aResult) && !mRedirectChannelChild) { + // mRedirectChannelChild doesn't exist means we're redirecting to a protocol + // that doesn't implement nsIChildChannel. The redirect result should be set + // as failed by veto listeners and shouldn't enter this condition. As the + // last resort, we synthesize the error result as NS_ERROR_DOM_BAD_URI here + // to let nsHttpChannel::ContinueProcessResponse2 know it's redirecting to + // another protocol and throw an error. + LOG((" redirecting to a protocol that doesn't implement nsIChildChannel")); + aResult = NS_ERROR_DOM_BAD_URI; + } + + nsCOMPtr referrerInfo; + if (newHttpChannel) { + // Must not be called until after redirect observers called. + newHttpChannel->SetOriginalURI(mOriginalURI); + referrerInfo = newHttpChannel->GetReferrerInfo(); + } + + RequestHeaderTuples emptyHeaders; + RequestHeaderTuples* headerTuples = &emptyHeaders; + nsLoadFlags loadFlags = 0; + Maybe corsPreflightArgs; + + nsCOMPtr newHttpChannelChild = + do_QueryInterface(mRedirectChannelChild); + if (newHttpChannelChild && NS_SUCCEEDED(aResult)) { + rv = newHttpChannelChild->AddCookiesToRequest(); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + rv = newHttpChannelChild->GetClientSetRequestHeaders(&headerTuples); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + newHttpChannelChild->GetClientSetCorsPreflightParameters(corsPreflightArgs); + } + + /* If the redirect was canceled, bypass OMR and send an empty API + * redirect URI */ + SerializeURI(nullptr, redirectURI); + + if (NS_SUCCEEDED(aResult)) { + // Note: this is where we would notify "http-on-modify-response" observers. + // We have deliberately disabled this for child processes (see bug 806753) + // + // After we verify redirect, nsHttpChannel may hit the network: must give + // "http-on-modify-request" observers the chance to cancel before that. + // base->CallOnModifyRequestObservers(); + + nsCOMPtr newHttpChannelInternal = + do_QueryInterface(mRedirectChannelChild); + if (newHttpChannelInternal) { + nsCOMPtr apiRedirectURI; + rv = newHttpChannelInternal->GetApiRedirectToURI( + getter_AddRefs(apiRedirectURI)); + if (NS_SUCCEEDED(rv) && apiRedirectURI) { + /* If there was an API redirect of this channel, we need to send it + * up here, since it can't be sent via SendAsyncOpen. */ + SerializeURI(apiRedirectURI, redirectURI); + } + } + + nsCOMPtr request = do_QueryInterface(mRedirectChannelChild); + if (request) { + request->GetLoadFlags(&loadFlags); + } + } + + bool chooseAppcache = false; + nsCOMPtr appCacheChannel = + do_QueryInterface(newHttpChannel); + if (appCacheChannel) { + appCacheChannel->GetChooseApplicationCache(&chooseAppcache); + } + + uint32_t sourceRequestBlockingReason = 0; + mLoadInfo->GetRequestBlockingReason(&sourceRequestBlockingReason); + + Maybe targetLoadInfoForwarder; + nsCOMPtr newChannel = do_QueryInterface(mRedirectChannelChild); + if (newChannel) { + ChildLoadInfoForwarderArgs args; + nsCOMPtr loadInfo = newChannel->LoadInfo(); + LoadInfoToChildLoadInfoForwarder(loadInfo, &args); + targetLoadInfoForwarder.emplace(args); + } + + if (CanSend()) + SendRedirect2Verify(aResult, *headerTuples, sourceRequestBlockingReason, + targetLoadInfoForwarder, loadFlags, referrerInfo, + redirectURI, corsPreflightArgs, chooseAppcache); + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// HttpChannelChild::nsIRequest +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +HttpChannelChild::Cancel(nsresult aStatus) { + LOG(("HttpChannelChild::Cancel [this=%p, status=%" PRIx32 "]\n", this, + static_cast(aStatus))); + LogCallingScriptLocation(this); + + MOZ_ASSERT(NS_IsMainThread()); + + if (!mCanceled) { + // If this cancel occurs before nsHttpChannel has been set up, AsyncOpen + // is responsible for cleaning up. + mCanceled = true; + mStatus = aStatus; + + bool remoteChannelExists = RemoteChannelExists(); +#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED + mCanSendAtCancel = CanSend(); + mRemoteChannelExistedAtCancel = remoteChannelExists; +#endif + + if (remoteChannelExists) { + SendCancel(aStatus, mLoadInfo->GetRequestBlockingReason()); + } + } + return NS_OK; +} + +NS_IMETHODIMP +HttpChannelChild::Suspend() { + LOG(("HttpChannelChild::Suspend [this=%p, mSuspendCount=%" PRIu32 "\n", this, + mSuspendCount + 1)); + MOZ_ASSERT(NS_IsMainThread()); + NS_ENSURE_TRUE(RemoteChannelExists(), NS_ERROR_NOT_AVAILABLE); + + LogCallingScriptLocation(this); + + // SendSuspend only once, when suspend goes from 0 to 1. + // Don't SendSuspend at all if we're diverting callbacks to the parent; + // suspend will be called at the correct time in the parent itself. + if (!mSuspendCount++) { + if (RemoteChannelExists()) { + SendSuspend(); + mSuspendSent = true; + } + } + mEventQ->Suspend(); + + return NS_OK; +} + +NS_IMETHODIMP +HttpChannelChild::Resume() { + LOG(("HttpChannelChild::Resume [this=%p, mSuspendCount=%" PRIu32 "\n", this, + mSuspendCount - 1)); + MOZ_ASSERT(NS_IsMainThread()); + NS_ENSURE_TRUE(RemoteChannelExists(), NS_ERROR_NOT_AVAILABLE); + NS_ENSURE_TRUE(mSuspendCount > 0, NS_ERROR_UNEXPECTED); + + LogCallingScriptLocation(this); + + nsresult rv = NS_OK; + + // SendResume only once, when suspend count drops to 0. + // Don't SendResume at all if we're diverting callbacks to the parent (unless + // suspend was sent earlier); otherwise, resume will be called at the correct + // time in the parent itself. + if (!--mSuspendCount && mSuspendSent) { + if (RemoteChannelExists()) { + SendResume(); + } + if (mCallOnResume) { + nsCOMPtr neckoTarget = GetNeckoTarget(); + MOZ_ASSERT(neckoTarget); + + RefPtr self = this; + std::function callOnResume = nullptr; + std::swap(callOnResume, mCallOnResume); + rv = neckoTarget->Dispatch( + NS_NewRunnableFunction( + "net::HttpChannelChild::mCallOnResume", + [callOnResume, self{std::move(self)}]() { callOnResume(self); }), + NS_DISPATCH_NORMAL); + } + } + mEventQ->Resume(); + + return rv; +} + +//----------------------------------------------------------------------------- +// HttpChannelChild::nsIChannel +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +HttpChannelChild::GetSecurityInfo(nsISupports** aSecurityInfo) { + NS_ENSURE_ARG_POINTER(aSecurityInfo); + NS_IF_ADDREF(*aSecurityInfo = mSecurityInfo); + return NS_OK; +} + +NS_IMETHODIMP +HttpChannelChild::AsyncOpen(nsIStreamListener* aListener) { + LOG(("HttpChannelChild::AsyncOpen [this=%p uri=%s]\n", this, mSpec.get())); + + nsresult rv = AsyncOpenInternal(aListener); + if (NS_FAILED(rv)) { + uint32_t blockingReason = 0; + mLoadInfo->GetRequestBlockingReason(&blockingReason); + LOG( + ("HttpChannelChild::AsyncOpen failed [this=%p rv=0x%08x " + "blocking-reason=%u]\n", + this, static_cast(rv), blockingReason)); + + gHttpHandler->OnFailedOpeningRequest(this); + } + +#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED + mAsyncOpenSucceeded = NS_SUCCEEDED(rv); +#endif + return rv; +} + +nsresult HttpChannelChild::AsyncOpenInternal(nsIStreamListener* aListener) { + nsresult rv; + + nsCOMPtr listener = aListener; + rv = nsContentSecurityManager::doContentSecurityCheck(this, listener); + if (NS_WARN_IF(NS_FAILED(rv))) { + ReleaseListeners(); + return rv; + } + + MOZ_ASSERT( + mLoadInfo->GetSecurityMode() == 0 || + mLoadInfo->GetInitialSecurityCheckDone() || + (mLoadInfo->GetSecurityMode() == + nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL && + mLoadInfo->GetLoadingPrincipal() && + mLoadInfo->GetLoadingPrincipal()->IsSystemPrincipal()), + "security flags in loadInfo but doContentSecurityCheck() not called"); + + LogCallingScriptLocation(this); + + if (!mLoadGroup && !mCallbacks) { + // If no one called SetLoadGroup or SetNotificationCallbacks, the private + // state has not been updated on PrivateBrowsingChannel (which we derive + // from) Hence, we have to call UpdatePrivateBrowsing() here + UpdatePrivateBrowsing(); + } + +#ifdef DEBUG + AssertPrivateBrowsingId(); +#endif + + if (mCanceled) { + ReleaseListeners(); + return mStatus; + } + + NS_ENSURE_TRUE(gNeckoChild != nullptr, NS_ERROR_FAILURE); + NS_ENSURE_ARG_POINTER(listener); + NS_ENSURE_TRUE(!LoadIsPending(), NS_ERROR_IN_PROGRESS); + NS_ENSURE_TRUE(!LoadWasOpened(), NS_ERROR_ALREADY_OPENED); + + if (MaybeWaitForUploadStreamLength(listener, nullptr)) { + return NS_OK; + } + + if (!LoadAsyncOpenTimeOverriden()) { + mAsyncOpenTime = TimeStamp::Now(); + } + +#ifdef MOZ_TASK_TRACER + if (tasktracer::IsStartLogging()) { + nsCOMPtr uri; + GetURI(getter_AddRefs(uri)); + nsAutoCString urispec; + uri->GetSpec(urispec); + tasktracer::AddLabel("HttpChannelChild::AsyncOpen %s", urispec.get()); + } +#endif + + // Port checked in parent, but duplicate here so we can return with error + // immediately + rv = NS_CheckPortSafety(mURI); + if (NS_FAILED(rv)) { + ReleaseListeners(); + return rv; + } + + nsAutoCString cookie; + if (NS_SUCCEEDED(mRequestHead.GetHeader(nsHttp::Cookie, cookie))) { + mUserSetCookieHeader = cookie; + } + + DebugOnly check = AddCookiesToRequest(); + MOZ_ASSERT(NS_SUCCEEDED(check)); + + // + // NOTE: From now on we must return NS_OK; all errors must be handled via + // OnStart/OnStopRequest + // + + // We notify "http-on-opening-request" observers in the child + // process so that devtools can capture a stack trace at the + // appropriate spot. See bug 806753 for some information about why + // other http-* notifications are disabled in child processes. + gHttpHandler->OnOpeningRequest(this); + + mLastStatusReported = TimeStamp::Now(); +#ifdef MOZ_GECKO_PROFILER + if (profiler_can_accept_markers()) { + nsAutoCString requestMethod; + GetRequestMethod(requestMethod); + + profiler_add_network_marker( + mURI, requestMethod, mPriority, mChannelId, NetworkLoadType::LOAD_START, + mChannelCreationTimestamp, mLastStatusReported, 0, kCacheUnknown, + mLoadInfo->GetInnerWindowID(), nullptr, nullptr); + } +#endif + StoreIsPending(true); + StoreWasOpened(true); + mListener = listener; + + // add ourselves to the load group. + if (mLoadGroup) mLoadGroup->AddRequest(this, nullptr); + + if (mCanceled) { + // We may have been canceled already, either by on-modify-request + // listeners or by load group observers; in that case, don't create IPDL + // connection. See nsHttpChannel::AsyncOpen(). + ReleaseListeners(); + return mStatus; + } + + // Set user agent override from docshell + HttpBaseChannel::SetDocshellUserAgentOverride(); + + rv = ContinueAsyncOpen(); + if (NS_FAILED(rv)) { + ReleaseListeners(); + } + return rv; +} + +// Assigns an nsISerialEventTarget to our IPDL actor so that IPC messages are +// sent to the correct DocGroup/TabGroup. +void HttpChannelChild::SetEventTarget() { + nsCOMPtr loadInfo = LoadInfo(); + + nsCOMPtr target = + nsContentUtils::GetEventTargetByLoadInfo(loadInfo, TaskCategory::Network); + + if (!target) { + return; + } + + gNeckoChild->SetEventTargetForActor(this, target); + + { + MutexAutoLock lock(mEventTargetMutex); + mNeckoTarget = target; + } +} + +already_AddRefed HttpChannelChild::GetNeckoTarget() { + nsCOMPtr target; + { + MutexAutoLock lock(mEventTargetMutex); + target = mNeckoTarget; + } + + if (!target) { + target = GetMainThreadSerialEventTarget(); + } + return target.forget(); +} + +already_AddRefed HttpChannelChild::GetODATarget() { + nsCOMPtr target; + { + MutexAutoLock lock(mEventTargetMutex); + if (mODATarget) { + target = mODATarget; + } else { + target = mNeckoTarget; + } + } + + if (!target) { + target = GetMainThreadEventTarget(); + } + return target.forget(); +} + +nsresult HttpChannelChild::ContinueAsyncOpen() { + nsresult rv; + nsCString appCacheClientId; + if (LoadInheritApplicationCache()) { + // Pick up an application cache from the notification + // callbacks if available + nsCOMPtr appCacheContainer; + GetCallback(appCacheContainer); + + if (appCacheContainer) { + nsCOMPtr appCache; + nsresult rv = + appCacheContainer->GetApplicationCache(getter_AddRefs(appCache)); + if (NS_SUCCEEDED(rv) && appCache) { + appCache->GetClientID(appCacheClientId); + } + } + } + + // + // Send request to the chrome process... + // + + mozilla::dom::BrowserChild* browserChild = nullptr; + nsCOMPtr iBrowserChild; + GetCallback(iBrowserChild); + if (iBrowserChild) { + browserChild = + static_cast(iBrowserChild.get()); + } + + // This id identifies the inner window's top-level document, + // which changes on every new load or navigation. + uint64_t contentWindowId = 0; + TimeStamp navigationStartTimeStamp; + if (browserChild) { + MOZ_ASSERT(browserChild->WebNavigation()); + if (RefPtr document = browserChild->GetTopLevelDocument()) { + contentWindowId = document->InnerWindowID(); + nsDOMNavigationTiming* navigationTiming = document->GetNavigationTiming(); + if (navigationTiming) { + navigationStartTimeStamp = + navigationTiming->GetNavigationStartTimeStamp(); + } + mTopLevelOuterContentWindowId = document->OuterWindowID(); + } + } + SetTopLevelContentWindowId(contentWindowId); + + HttpChannelOpenArgs openArgs; + // No access to HttpChannelOpenArgs members, but they each have a + // function with the struct name that returns a ref. + SerializeURI(mURI, openArgs.uri()); + SerializeURI(mOriginalURI, openArgs.original()); + SerializeURI(mDocumentURI, openArgs.doc()); + SerializeURI(mAPIRedirectToURI, openArgs.apiRedirectTo()); + openArgs.loadFlags() = mLoadFlags; + openArgs.requestHeaders() = mClientSetRequestHeaders; + mRequestHead.Method(openArgs.requestMethod()); + openArgs.preferredAlternativeTypes() = mPreferredCachedAltDataTypes.Clone(); + openArgs.referrerInfo() = mReferrerInfo; + + AutoIPCStream autoStream(openArgs.uploadStream()); + if (mUploadStream) { + autoStream.Serialize(mUploadStream, ContentChild::GetSingleton()); + autoStream.TakeOptionalValue(); + } + + Maybe optionalCorsPreflightArgs; + GetClientSetCorsPreflightParameters(optionalCorsPreflightArgs); + + // NB: This call forces us to cache mTopWindowURI if we haven't already. + nsCOMPtr uri; + GetTopWindowURI(mURI, getter_AddRefs(uri)); + + SerializeURI(mTopWindowURI, openArgs.topWindowURI()); + + openArgs.preflightArgs() = optionalCorsPreflightArgs; + + openArgs.uploadStreamHasHeaders() = LoadUploadStreamHasHeaders(); + openArgs.priority() = mPriority; + openArgs.classOfService() = mClassOfService; + openArgs.redirectionLimit() = mRedirectionLimit; + openArgs.allowSTS() = LoadAllowSTS(); + openArgs.thirdPartyFlags() = LoadThirdPartyFlags(); + openArgs.resumeAt() = mSendResumeAt; + openArgs.startPos() = mStartPos; + openArgs.entityID() = mEntityID; + openArgs.chooseApplicationCache() = LoadChooseApplicationCache(); + openArgs.appCacheClientID() = appCacheClientId; + openArgs.allowSpdy() = LoadAllowSpdy(); + openArgs.allowHttp3() = LoadAllowHttp3(); + openArgs.allowAltSvc() = LoadAllowAltSvc(); + openArgs.beConservative() = LoadBeConservative(); + openArgs.tlsFlags() = mTlsFlags; + openArgs.initialRwin() = mInitialRwin; + + openArgs.cacheKey() = mCacheKey; + + openArgs.blockAuthPrompt() = LoadBlockAuthPrompt(); + + openArgs.allowStaleCacheContent() = LoadAllowStaleCacheContent(); + openArgs.preferCacheLoadOverBypass() = LoadPreferCacheLoadOverBypass(); + + openArgs.contentTypeHint() = mContentTypeHint; + + rv = mozilla::ipc::LoadInfoToLoadInfoArgs(mLoadInfo, &openArgs.loadInfo()); + NS_ENSURE_SUCCESS(rv, rv); + + EnsureRequestContextID(); + openArgs.requestContextID() = mRequestContextID; + + openArgs.corsMode() = mCorsMode; + openArgs.redirectMode() = mRedirectMode; + + openArgs.channelId() = mChannelId; + + openArgs.integrityMetadata() = mIntegrityMetadata; + + openArgs.contentWindowId() = contentWindowId; + openArgs.topLevelOuterContentWindowId() = mTopLevelOuterContentWindowId; + + LOG(("HttpChannelChild::ContinueAsyncOpen this=%p gid=%" PRIu64 + " topwinid=%" PRIx64, + this, mChannelId, mTopLevelOuterContentWindowId)); + + if (browserChild && !browserChild->IPCOpen()) { + return NS_ERROR_FAILURE; + } + + ContentChild* cc = static_cast(gNeckoChild->Manager()); + if (cc->IsShuttingDown()) { + return NS_ERROR_FAILURE; + } + + openArgs.launchServiceWorkerStart() = mLaunchServiceWorkerStart; + openArgs.launchServiceWorkerEnd() = mLaunchServiceWorkerEnd; + openArgs.dispatchFetchEventStart() = mDispatchFetchEventStart; + openArgs.dispatchFetchEventEnd() = mDispatchFetchEventEnd; + openArgs.handleFetchEventStart() = mHandleFetchEventStart; + openArgs.handleFetchEventEnd() = mHandleFetchEventEnd; + + openArgs.forceMainDocumentChannel() = LoadForceMainDocumentChannel(); + + openArgs.navigationStartTimeStamp() = navigationStartTimeStamp; + openArgs.hasNonEmptySandboxingFlag() = GetHasNonEmptySandboxingFlag(); + + // This must happen before the constructor message is sent. Otherwise messages + // from the parent could arrive quickly and be delivered to the wrong event + // target. + SetEventTarget(); + + if (!gNeckoChild->SendPHttpChannelConstructor( + this, browserChild, IPC::SerializedLoadContext(this), openArgs)) { + return NS_ERROR_FAILURE; + } + + { + MutexAutoLock lock(mBgChildMutex); + + MOZ_RELEASE_ASSERT(gSocketTransportService); + + // Service worker might use the same HttpChannelChild to do async open + // twice. Need to disconnect with previous background channel before + // creating the new one, to prevent receiving further notification + // from it. + if (mBgChild) { + RefPtr prevBgChild = std::move(mBgChild); + gSocketTransportService->Dispatch( + NewRunnableMethod("HttpBackgroundChannelChild::OnChannelClosed", + prevBgChild, + &HttpBackgroundChannelChild::OnChannelClosed), + NS_DISPATCH_NORMAL); + } + + MOZ_ASSERT(!mBgInitFailCallback); + + mBgInitFailCallback = NewRunnableMethod( + "HttpChannelChild::FailedAsyncOpen", this, + &HttpChannelChild::FailedAsyncOpen, NS_ERROR_FAILURE); + + RefPtr bgChild = + new HttpBackgroundChannelChild(); + + RefPtr self = this; + nsresult rv = gSocketTransportService->Dispatch( + NewRunnableMethod>( + "HttpBackgroundChannelChild::Init", bgChild, + &HttpBackgroundChannelChild::Init, self), + NS_DISPATCH_NORMAL); + + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + mBgChild = std::move(bgChild); +#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED + mEverHadBgChildAtAsyncOpen = true; +#endif + } + + MaybeConnectToSocketProcess(); + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// HttpChannelChild::nsIHttpChannel +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +HttpChannelChild::SetRequestHeader(const nsACString& aHeader, + const nsACString& aValue, bool aMerge) { + LOG(("HttpChannelChild::SetRequestHeader [this=%p]\n", this)); + nsresult rv = HttpBaseChannel::SetRequestHeader(aHeader, aValue, aMerge); + if (NS_FAILED(rv)) return rv; + + RequestHeaderTuple* tuple = mClientSetRequestHeaders.AppendElement(); + if (!tuple) return NS_ERROR_OUT_OF_MEMORY; + + tuple->mHeader = aHeader; + tuple->mValue = aValue; + tuple->mMerge = aMerge; + tuple->mEmpty = false; + return NS_OK; +} + +NS_IMETHODIMP +HttpChannelChild::SetEmptyRequestHeader(const nsACString& aHeader) { + LOG(("HttpChannelChild::SetEmptyRequestHeader [this=%p]\n", this)); + nsresult rv = HttpBaseChannel::SetEmptyRequestHeader(aHeader); + if (NS_FAILED(rv)) return rv; + + RequestHeaderTuple* tuple = mClientSetRequestHeaders.AppendElement(); + if (!tuple) return NS_ERROR_OUT_OF_MEMORY; + + tuple->mHeader = aHeader; + tuple->mMerge = false; + tuple->mEmpty = true; + return NS_OK; +} + +NS_IMETHODIMP +HttpChannelChild::RedirectTo(nsIURI* newURI) { + // disabled until/unless addons run in child or something else needs this + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +HttpChannelChild::UpgradeToSecure() { + // disabled until/unless addons run in child or something else needs this + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +HttpChannelChild::GetProtocolVersion(nsACString& aProtocolVersion) { + aProtocolVersion = mProtocolVersion; + return NS_OK; +} + +//----------------------------------------------------------------------------- +// HttpChannelChild::nsIHttpChannelInternal +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +HttpChannelChild::SetupFallbackChannel(const char* aFallbackKey) { + DROP_DEAD(); +} + +NS_IMETHODIMP +HttpChannelChild::GetIsAuthChannel(bool* aIsAuthChannel) { DROP_DEAD(); } + +//----------------------------------------------------------------------------- +// HttpChannelChild::nsICacheInfoChannel +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +HttpChannelChild::GetCacheTokenFetchCount(int32_t* _retval) { + NS_ENSURE_ARG_POINTER(_retval); + MOZ_ASSERT(NS_IsMainThread()); + + if (!mCacheEntryAvailable && !mAltDataCacheEntryAvailable) { + return NS_ERROR_NOT_AVAILABLE; + } + + *_retval = mCacheFetchCount; + return NS_OK; +} + +NS_IMETHODIMP +HttpChannelChild::GetCacheTokenExpirationTime(uint32_t* _retval) { + NS_ENSURE_ARG_POINTER(_retval); + MOZ_ASSERT(NS_IsMainThread()); + + if (!mCacheEntryAvailable) return NS_ERROR_NOT_AVAILABLE; + + *_retval = mCacheExpirationTime; + return NS_OK; +} + +NS_IMETHODIMP +HttpChannelChild::IsFromCache(bool* value) { + if (!LoadIsPending()) return NS_ERROR_NOT_AVAILABLE; + + *value = mIsFromCache; + return NS_OK; +} + +NS_IMETHODIMP +HttpChannelChild::GetCacheEntryId(uint64_t* aCacheEntryId) { + bool fromCache = false; + if (NS_FAILED(IsFromCache(&fromCache)) || !fromCache || + !mCacheEntryAvailable) { + return NS_ERROR_NOT_AVAILABLE; + } + + *aCacheEntryId = mCacheEntryId; + return NS_OK; +} + +NS_IMETHODIMP +HttpChannelChild::IsRacing(bool* aIsRacing) { + if (!LoadAfterOnStartRequestBegun()) { + return NS_ERROR_NOT_AVAILABLE; + } + *aIsRacing = mIsRacing; + return NS_OK; +} + +NS_IMETHODIMP +HttpChannelChild::GetCacheKey(uint32_t* cacheKey) { + MOZ_ASSERT(NS_IsMainThread()); + + *cacheKey = mCacheKey; + return NS_OK; +} +NS_IMETHODIMP +HttpChannelChild::SetCacheKey(uint32_t cacheKey) { + ENSURE_CALLED_BEFORE_ASYNC_OPEN(); + + mCacheKey = cacheKey; + return NS_OK; +} + +NS_IMETHODIMP +HttpChannelChild::SetAllowStaleCacheContent(bool aAllowStaleCacheContent) { + StoreAllowStaleCacheContent(aAllowStaleCacheContent); + return NS_OK; +} +NS_IMETHODIMP +HttpChannelChild::GetAllowStaleCacheContent(bool* aAllowStaleCacheContent) { + NS_ENSURE_ARG(aAllowStaleCacheContent); + *aAllowStaleCacheContent = LoadAllowStaleCacheContent(); + return NS_OK; +} + +NS_IMETHODIMP +HttpChannelChild::SetPreferCacheLoadOverBypass( + bool aPreferCacheLoadOverBypass) { + StorePreferCacheLoadOverBypass(aPreferCacheLoadOverBypass); + return NS_OK; +} +NS_IMETHODIMP +HttpChannelChild::GetPreferCacheLoadOverBypass( + bool* aPreferCacheLoadOverBypass) { + NS_ENSURE_ARG(aPreferCacheLoadOverBypass); + *aPreferCacheLoadOverBypass = LoadPreferCacheLoadOverBypass(); + return NS_OK; +} + +NS_IMETHODIMP +HttpChannelChild::PreferAlternativeDataType(const nsACString& aType, + const nsACString& aContentType, + bool aDeliverAltData) { + ENSURE_CALLED_BEFORE_ASYNC_OPEN(); + + mPreferredCachedAltDataTypes.AppendElement(PreferredAlternativeDataTypeParams( + nsCString(aType), nsCString(aContentType), aDeliverAltData)); + return NS_OK; +} + +const nsTArray& +HttpChannelChild::PreferredAlternativeDataTypes() { + return mPreferredCachedAltDataTypes; +} + +NS_IMETHODIMP +HttpChannelChild::GetAlternativeDataType(nsACString& aType) { + // Must be called during or after OnStartRequest + if (!LoadAfterOnStartRequestBegun()) { + return NS_ERROR_NOT_AVAILABLE; + } + + aType = mAvailableCachedAltDataType; + return NS_OK; +} + +NS_IMETHODIMP +HttpChannelChild::OpenAlternativeOutputStream(const nsACString& aType, + int64_t aPredictedSize, + nsIAsyncOutputStream** _retval) { + MOZ_ASSERT(NS_IsMainThread(), "Main thread only"); + + if (!CanSend()) { + return NS_ERROR_NOT_AVAILABLE; + } + if (static_cast(gNeckoChild->Manager())->IsShuttingDown()) { + return NS_ERROR_NOT_AVAILABLE; + } + + nsCOMPtr neckoTarget = GetNeckoTarget(); + MOZ_ASSERT(neckoTarget); + + RefPtr stream = new AltDataOutputStreamChild(); + stream->AddIPDLReference(); + + gNeckoChild->SetEventTargetForActor(stream, neckoTarget); + + if (!gNeckoChild->SendPAltDataOutputStreamConstructor( + stream, nsCString(aType), aPredictedSize, this)) { + return NS_ERROR_FAILURE; + } + + stream.forget(_retval); + return NS_OK; +} + +NS_IMETHODIMP +HttpChannelChild::GetOriginalInputStream(nsIInputStreamReceiver* aReceiver) { + if (aReceiver == nullptr) { + return NS_ERROR_INVALID_ARG; + } + + if (!CanSend()) { + return NS_ERROR_NOT_AVAILABLE; + } + + mOriginalInputStreamReceiver = aReceiver; + Unused << SendOpenOriginalCacheInputStream(); + + return NS_OK; +} + +NS_IMETHODIMP +HttpChannelChild::GetAltDataInputStream(const nsACString& aType, + nsIInputStreamReceiver* aReceiver) { + if (aReceiver == nullptr) { + return NS_ERROR_INVALID_ARG; + } + + if (!CanSend()) { + return NS_ERROR_NOT_AVAILABLE; + } + + mAltDataInputStreamReceiver = aReceiver; + Unused << SendOpenAltDataCacheInputStream(nsCString(aType)); + + return NS_OK; +} + +mozilla::ipc::IPCResult HttpChannelChild::RecvOriginalCacheInputStreamAvailable( + const Maybe& aStream) { + nsCOMPtr stream = DeserializeIPCStream(aStream); + nsCOMPtr receiver; + receiver.swap(mOriginalInputStreamReceiver); + if (receiver) { + receiver->OnInputStreamReady(stream); + } + + return IPC_OK(); +} + +mozilla::ipc::IPCResult HttpChannelChild::RecvAltDataCacheInputStreamAvailable( + const Maybe& aStream) { + nsCOMPtr stream = DeserializeIPCStream(aStream); + nsCOMPtr receiver; + receiver.swap(mAltDataInputStreamReceiver); + if (receiver) { + receiver->OnInputStreamReady(stream); + } + + return IPC_OK(); +} + +//----------------------------------------------------------------------------- +// HttpChannelChild::nsIResumableChannel +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +HttpChannelChild::ResumeAt(uint64_t startPos, const nsACString& entityID) { + LOG(("HttpChannelChild::ResumeAt [this=%p]\n", this)); + ENSURE_CALLED_BEFORE_CONNECT(); + mStartPos = startPos; + mEntityID = entityID; + mSendResumeAt = true; + return NS_OK; +} + +// GetEntityID is shared in HttpBaseChannel + +//----------------------------------------------------------------------------- +// HttpChannelChild::nsISupportsPriority +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +HttpChannelChild::SetPriority(int32_t aPriority) { + LOG(("HttpChannelChild::SetPriority %p p=%d", this, aPriority)); + + int16_t newValue = clamped(aPriority, INT16_MIN, INT16_MAX); + if (mPriority == newValue) return NS_OK; + mPriority = newValue; + if (RemoteChannelExists()) SendSetPriority(mPriority); + return NS_OK; +} + +//----------------------------------------------------------------------------- +// HttpChannelChild::nsIClassOfService +//----------------------------------------------------------------------------- +NS_IMETHODIMP +HttpChannelChild::SetClassFlags(uint32_t inFlags) { + if (mClassOfService == inFlags) { + return NS_OK; + } + + mClassOfService = inFlags; + + LOG(("HttpChannelChild %p ClassOfService=%u", this, mClassOfService)); + + if (RemoteChannelExists()) { + SendSetClassOfService(mClassOfService); + } + return NS_OK; +} + +NS_IMETHODIMP +HttpChannelChild::AddClassFlags(uint32_t inFlags) { + mClassOfService |= inFlags; + + LOG(("HttpChannelChild %p ClassOfService=%u", this, mClassOfService)); + + if (RemoteChannelExists()) { + SendSetClassOfService(mClassOfService); + } + return NS_OK; +} + +NS_IMETHODIMP +HttpChannelChild::ClearClassFlags(uint32_t inFlags) { + mClassOfService &= ~inFlags; + + LOG(("HttpChannelChild %p ClassOfService=%u", this, mClassOfService)); + + if (RemoteChannelExists()) { + SendSetClassOfService(mClassOfService); + } + return NS_OK; +} + +//----------------------------------------------------------------------------- +// HttpChannelChild::nsIProxiedChannel +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +HttpChannelChild::GetProxyInfo(nsIProxyInfo** aProxyInfo) { DROP_DEAD(); } + +NS_IMETHODIMP HttpChannelChild::GetHttpProxyConnectResponseCode( + int32_t* aResponseCode) { + DROP_DEAD(); +} + +//----------------------------------------------------------------------------- +// HttpChannelChild::nsIApplicationCacheContainer +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +HttpChannelChild::GetApplicationCache(nsIApplicationCache** aApplicationCache) { + NS_IF_ADDREF(*aApplicationCache = mApplicationCache); + return NS_OK; +} +NS_IMETHODIMP +HttpChannelChild::SetApplicationCache(nsIApplicationCache* aApplicationCache) { + NS_ENSURE_TRUE(!LoadWasOpened(), NS_ERROR_ALREADY_OPENED); + + mApplicationCache = aApplicationCache; + return NS_OK; +} + +//----------------------------------------------------------------------------- +// HttpChannelChild::nsIApplicationCacheChannel +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +HttpChannelChild::GetApplicationCacheForWrite( + nsIApplicationCache** aApplicationCache) { + *aApplicationCache = nullptr; + return NS_OK; +} +NS_IMETHODIMP +HttpChannelChild::SetApplicationCacheForWrite( + nsIApplicationCache* aApplicationCache) { + NS_ENSURE_TRUE(!LoadWasOpened(), NS_ERROR_ALREADY_OPENED); + + // Child channels are not intended to be used for cache writes + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +HttpChannelChild::GetLoadedFromApplicationCache( + bool* aLoadedFromApplicationCache) { + *aLoadedFromApplicationCache = LoadLoadedFromApplicationCache(); + return NS_OK; +} + +NS_IMETHODIMP +HttpChannelChild::GetInheritApplicationCache(bool* aInherit) { + *aInherit = LoadInheritApplicationCache(); + return NS_OK; +} +NS_IMETHODIMP +HttpChannelChild::SetInheritApplicationCache(bool aInherit) { + StoreInheritApplicationCache(aInherit); + return NS_OK; +} + +NS_IMETHODIMP +HttpChannelChild::GetChooseApplicationCache(bool* aChoose) { + *aChoose = LoadChooseApplicationCache(); + return NS_OK; +} + +NS_IMETHODIMP +HttpChannelChild::SetChooseApplicationCache(bool aChoose) { + StoreChooseApplicationCache(aChoose); + return NS_OK; +} + +NS_IMETHODIMP +HttpChannelChild::MarkOfflineCacheEntryAsForeign() { + SendMarkOfflineCacheEntryAsForeign(); + return NS_OK; +} + +//----------------------------------------------------------------------------- +// HttpChannelChild::nsIHttpChannelChild +//----------------------------------------------------------------------------- + +NS_IMETHODIMP HttpChannelChild::AddCookiesToRequest() { + HttpBaseChannel::AddCookiesToRequest(); + return NS_OK; +} + +NS_IMETHODIMP HttpChannelChild::GetClientSetRequestHeaders( + RequestHeaderTuples** aRequestHeaders) { + *aRequestHeaders = &mClientSetRequestHeaders; + return NS_OK; +} + +void HttpChannelChild::GetClientSetCorsPreflightParameters( + Maybe& aArgs) { + if (LoadRequireCORSPreflight()) { + CorsPreflightArgs args; + args.unsafeHeaders() = mUnsafeHeaders.Clone(); + aArgs.emplace(args); + } else { + aArgs = Nothing(); + } +} + +NS_IMETHODIMP +HttpChannelChild::RemoveCorsPreflightCacheEntry( + nsIURI* aURI, nsIPrincipal* aPrincipal, + const OriginAttributes& aOriginAttributes) { + URIParams uri; + SerializeURI(aURI, uri); + PrincipalInfo principalInfo; + nsresult rv = PrincipalToPrincipalInfo(aPrincipal, &principalInfo); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + bool result = false; + // Be careful to not attempt to send a message to the parent after the + // actor has been destroyed. + if (CanSend()) { + result = SendRemoveCorsPreflightCacheEntry(uri, principalInfo, + aOriginAttributes); + } + return result ? NS_OK : NS_ERROR_FAILURE; +} + +//----------------------------------------------------------------------------- +// HttpChannelChild::nsIMuliPartChannel +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +HttpChannelChild::GetBaseChannel(nsIChannel** aBaseChannel) { + if (!mMultiPartID) { + MOZ_ASSERT(false, "Not a multipart channel"); + return NS_ERROR_NOT_AVAILABLE; + } + nsCOMPtr channel = this; + channel.forget(aBaseChannel); + return NS_OK; +} + +NS_IMETHODIMP +HttpChannelChild::GetPartID(uint32_t* aPartID) { + if (!mMultiPartID) { + MOZ_ASSERT(false, "Not a multipart channel"); + return NS_ERROR_NOT_AVAILABLE; + } + *aPartID = *mMultiPartID; + return NS_OK; +} + +NS_IMETHODIMP +HttpChannelChild::GetIsLastPart(bool* aIsLastPart) { + if (!mMultiPartID) { + return NS_ERROR_NOT_AVAILABLE; + } + *aIsLastPart = mIsLastPartOfMultiPart; + return NS_OK; +} + +//----------------------------------------------------------------------------- +// HttpChannelChild::nsIThreadRetargetableRequest +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +HttpChannelChild::RetargetDeliveryTo(nsIEventTarget* aNewTarget) { + LOG(("HttpChannelChild::RetargetDeliveryTo [this=%p, aNewTarget=%p]", this, + aNewTarget)); + + MOZ_ASSERT(NS_IsMainThread(), "Should be called on main thread only"); + MOZ_ASSERT(!mODATarget); + MOZ_ASSERT(aNewTarget); + + NS_ENSURE_ARG(aNewTarget); + if (aNewTarget->IsOnCurrentThread()) { + NS_WARNING("Retargeting delivery to same thread"); + mOMTResult = LABELS_HTTP_CHILD_OMT_STATS::successMainThread; + return NS_OK; + } + + if (mMultiPartID) { + // TODO: Maybe add a new label for this? Maybe it doesn't + // matter though, since we also blocked QI, so we shouldn't + // ever get here. + mOMTResult = LABELS_HTTP_CHILD_OMT_STATS::failListener; + return NS_ERROR_NO_INTERFACE; + } + + // Ensure that |mListener| and any subsequent listeners can be retargeted + // to another thread. + nsresult rv = NS_OK; + nsCOMPtr retargetableListener = + do_QueryInterface(mListener, &rv); + if (!retargetableListener || NS_FAILED(rv)) { + NS_WARNING("Listener is not retargetable"); + mOMTResult = LABELS_HTTP_CHILD_OMT_STATS::failListener; + return NS_ERROR_NO_INTERFACE; + } + + rv = retargetableListener->CheckListenerChain(); + if (NS_FAILED(rv)) { + NS_WARNING("Subsequent listeners are not retargetable"); + mOMTResult = LABELS_HTTP_CHILD_OMT_STATS::failListenerChain; + return rv; + } + + { + MutexAutoLock lock(mEventTargetMutex); + mODATarget = aNewTarget; + } + + mOMTResult = LABELS_HTTP_CHILD_OMT_STATS::success; + return NS_OK; +} + +NS_IMETHODIMP +HttpChannelChild::GetDeliveryTarget(nsIEventTarget** aEventTarget) { + MutexAutoLock lock(mEventTargetMutex); + + nsCOMPtr target = mODATarget; + if (!mODATarget) { + target = GetCurrentEventTarget(); + } + target.forget(aEventTarget); + return NS_OK; +} + +void HttpChannelChild::TrySendDeletingChannel() { + AUTO_PROFILER_LABEL("HttpChannelChild::TrySendDeletingChannel", NETWORK); + if (!mDeletingChannelSent.compareExchange(false, true)) { + // SendDeletingChannel is already sent. + return; + } + + if (NS_IsMainThread()) { + if (NS_WARN_IF(!CanSend())) { + // IPC actor is destroyed already, do not send more messages. + return; + } + + Unused << PHttpChannelChild::SendDeletingChannel(); + return; + } + + nsCOMPtr neckoTarget = GetNeckoTarget(); + MOZ_ASSERT(neckoTarget); + + DebugOnly rv = neckoTarget->Dispatch( + NewNonOwningRunnableMethod( + "net::HttpChannelChild::TrySendDeletingChannel", this, + &HttpChannelChild::TrySendDeletingChannel), + NS_DISPATCH_NORMAL); + MOZ_ASSERT(NS_SUCCEEDED(rv)); +} + +void HttpChannelChild::OnCopyComplete(nsresult aStatus) { + nsCOMPtr runnable = NewRunnableMethod( + "net::HttpBaseChannel::EnsureUploadStreamIsCloneableComplete", this, + &HttpChannelChild::EnsureUploadStreamIsCloneableComplete, aStatus); + nsCOMPtr neckoTarget = GetNeckoTarget(); + MOZ_ASSERT(neckoTarget); + + Unused << neckoTarget->Dispatch(runnable, NS_DISPATCH_NORMAL); +} + +nsresult HttpChannelChild::AsyncCallImpl( + void (HttpChannelChild::*funcPtr)(), + nsRunnableMethod** retval) { + nsresult rv; + + RefPtr> event = + NewRunnableMethod("net::HttpChannelChild::AsyncCall", this, funcPtr); + nsCOMPtr neckoTarget = GetNeckoTarget(); + MOZ_ASSERT(neckoTarget); + + rv = neckoTarget->Dispatch(event, NS_DISPATCH_NORMAL); + + if (NS_SUCCEEDED(rv) && retval) { + *retval = event; + } + + return rv; +} + +nsresult HttpChannelChild::SetReferrerHeader(const nsACString& aReferrer, + bool aRespectBeforeConnect) { + // Normally this would be ENSURE_CALLED_BEFORE_CONNECT, but since the + // "connect" is done in the main process, and LoadRequestObserversCalled() is + // never set in the ChannelChild, before connect basically means before + // asyncOpen. + if (aRespectBeforeConnect) { + ENSURE_CALLED_BEFORE_ASYNC_OPEN(); + } + + // remove old referrer if any + mClientSetRequestHeaders.RemoveElementsBy( + [](const auto& header) { return "Referer"_ns.Equals(header.mHeader); }); + + return HttpBaseChannel::SetReferrerHeader(aReferrer, aRespectBeforeConnect); +} + +void HttpChannelChild::CancelOnMainThread(nsresult aRv) { + LOG(("HttpChannelChild::CancelOnMainThread [this=%p]", this)); + + if (NS_IsMainThread()) { + Cancel(aRv); + return; + } + + mEventQ->Suspend(); + // Cancel is expected to preempt any other channel events, thus we put this + // event in the front of mEventQ to make sure nsIStreamListener not receiving + // any ODA/OnStopRequest callbacks. + mEventQ->PrependEvent(MakeUnique( + this, [self = UnsafePtr(this), aRv]() { + self->Cancel(aRv); + })); + mEventQ->Resume(); +} + +mozilla::ipc::IPCResult HttpChannelChild::RecvIssueDeprecationWarning( + const uint32_t& warning, const bool& asError) { + nsCOMPtr warner; + GetCallback(warner); + if (warner) { + warner->IssueWarning(warning, asError); + } + return IPC_OK(); +} + +mozilla::ipc::IPCResult HttpChannelChild::RecvSetPriority( + const int16_t& aPriority) { + mPriority = aPriority; + return IPC_OK(); +} + +// We don't have a copyable Endpoint and NeckoTargetChannelFunctionEvent takes +// std::function. It's not possible to avoid the copy from the type of +// lambda to std::function, so does the capture list. Hence, we're forced to +// use the old-fashioned channel event inheritance. +class AttachStreamFilterEvent : public ChannelEvent { + public: + AttachStreamFilterEvent(HttpChannelChild* aChild, + already_AddRefed aTarget, + Endpoint&& aEndpoint) + : mChild(aChild), mTarget(aTarget), mEndpoint(std::move(aEndpoint)) {} + + already_AddRefed GetEventTarget() override { + nsCOMPtr target = mTarget; + return target.forget(); + } + + void Run() override { + extensions::StreamFilterParent::Attach(mChild, std::move(mEndpoint)); + } + + private: + HttpChannelChild* mChild; + nsCOMPtr mTarget; + Endpoint mEndpoint; +}; + +void HttpChannelChild::ProcessAttachStreamFilter( + Endpoint&& aEndpoint) { + LOG(("HttpChannelChild::ProcessAttachStreamFilter [this=%p]\n", this)); + MOZ_ASSERT(OnSocketThread()); + + mEventQ->RunOrEnqueue(new AttachStreamFilterEvent(this, GetNeckoTarget(), + std::move(aEndpoint))); +} + +void HttpChannelChild::ActorDestroy(ActorDestroyReason aWhy) { + MOZ_ASSERT(NS_IsMainThread()); + +#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED + mActorDestroyReason.emplace(aWhy); +#endif + + // OnStartRequest might be dropped if IPDL is destroyed abnormally + // and BackgroundChild might have pending IPC messages. + // Clean up BackgroundChild at this time to prevent memleak. + if (aWhy != Deletion) { + // Make sure all the messages are processed. + AutoEventEnqueuer ensureSerialDispatch(mEventQ); + + mStatus = NS_ERROR_DOCSHELL_DYING; + HandleAsyncAbort(); + + // Cleanup the background channel before we resume the eventQ so we don't + // get any other events. + CleanupBackgroundChannel(); + + mIPCActorDeleted = true; + mCanceled = true; + } +} + +mozilla::ipc::IPCResult HttpChannelChild::RecvLogBlockedCORSRequest( + const nsString& aMessage, const nsCString& aCategory) { + Unused << LogBlockedCORSRequest(aMessage, aCategory); + return IPC_OK(); +} + +NS_IMETHODIMP +HttpChannelChild::LogBlockedCORSRequest(const nsAString& aMessage, + const nsACString& aCategory) { + uint64_t innerWindowID = mLoadInfo->GetInnerWindowID(); + bool privateBrowsing = !!mLoadInfo->GetOriginAttributes().mPrivateBrowsingId; + bool fromChromeContext = + mLoadInfo->TriggeringPrincipal()->IsSystemPrincipal(); + nsCORSListenerProxy::LogBlockedCORSRequest( + innerWindowID, privateBrowsing, fromChromeContext, aMessage, aCategory); + return NS_OK; +} + +mozilla::ipc::IPCResult HttpChannelChild::RecvLogMimeTypeMismatch( + const nsCString& aMessageName, const bool& aWarning, const nsString& aURL, + const nsString& aContentType) { + Unused << LogMimeTypeMismatch(aMessageName, aWarning, aURL, aContentType); + return IPC_OK(); +} + +NS_IMETHODIMP +HttpChannelChild::LogMimeTypeMismatch(const nsACString& aMessageName, + bool aWarning, const nsAString& aURL, + const nsAString& aContentType) { + RefPtr doc; + mLoadInfo->GetLoadingDocument(getter_AddRefs(doc)); + + AutoTArray params; + params.AppendElement(aURL); + params.AppendElement(aContentType); + nsContentUtils::ReportToConsole( + aWarning ? nsIScriptError::warningFlag : nsIScriptError::errorFlag, + "MIMEMISMATCH"_ns, doc, nsContentUtils::eSECURITY_PROPERTIES, + nsCString(aMessageName).get(), params); + return NS_OK; +} + +nsresult HttpChannelChild::CrossProcessRedirectFinished(nsresult aStatus) { + if (!CanSend()) { + return NS_BINDING_FAILED; + } + + if (!mCanceled && NS_SUCCEEDED(mStatus)) { + mStatus = aStatus; + } + + return mStatus; +} + +void HttpChannelChild::DoDiagnosticAssertWhenOnStopNotCalledOnDestroy() { +#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED + mDoDiagnosticAssertWhenOnStopNotCalledOnDestroy = true; +#endif +} + +void HttpChannelChild::MaybeConnectToSocketProcess() { + if (!nsIOService::UseSocketProcess()) { + return; + } + + if (!StaticPrefs::network_send_ODA_to_content_directly()) { + return; + } + + RefPtr bgChild = mBgChild; + SocketProcessBridgeChild::GetSocketProcessBridge()->Then( + GetCurrentSerialEventTarget(), __func__, + [bgChild]() { + gSocketTransportService->Dispatch( + NewRunnableMethod("HttpBackgroundChannelChild::CreateDataBridge", + bgChild, + &HttpBackgroundChannelChild::CreateDataBridge), + NS_DISPATCH_NORMAL); + }, + []() { NS_WARNING("Failed to create SocketProcessBridgeChild"); }); +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/HttpChannelChild.h b/netwerk/protocol/http/HttpChannelChild.h new file mode 100644 index 0000000000..fd44fa4c6d --- /dev/null +++ b/netwerk/protocol/http/HttpChannelChild.h @@ -0,0 +1,451 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et 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/. */ + +#ifndef mozilla_net_HttpChannelChild_h +#define mozilla_net_HttpChannelChild_h + +#include "mozilla/Mutex.h" +#include "mozilla/Telemetry.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/extensions/StreamFilterParent.h" +#include "mozilla/net/HttpBaseChannel.h" +#include "mozilla/net/NeckoTargetHolder.h" +#include "mozilla/net/PHttpChannelChild.h" +#include "mozilla/net/ChannelEventQueue.h" + +#include "nsIStreamListener.h" +#include "nsIInterfaceRequestor.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsIProgressEventSink.h" +#include "nsICacheInfoChannel.h" +#include "nsIApplicationCache.h" +#include "nsIApplicationCacheChannel.h" +#include "nsIResumableChannel.h" +#include "nsIProxiedChannel.h" +#include "nsIAsyncVerifyRedirectCallback.h" +#include "nsIChildChannel.h" +#include "nsIHttpChannelChild.h" +#include "nsIMultiPartChannel.h" +#include "nsIThreadRetargetableRequest.h" +#include "mozilla/net/DNS.h" + +using mozilla::Telemetry::LABELS_HTTP_CHILD_OMT_STATS; + +class nsIEventTarget; +class nsInputStreamPump; +class nsISerialEventTarget; +class nsIInterceptedBodyCallback; + +#define HTTP_CHANNEL_CHILD_IID \ + { \ + 0x321bd99e, 0x2242, 0x4dc6, { \ + 0xbb, 0xec, 0xd5, 0x06, 0x29, 0x7c, 0x39, 0x83 \ + } \ + } + +namespace mozilla { +namespace net { + +class HttpBackgroundChannelChild; + +class HttpChannelChild final : public PHttpChannelChild, + public HttpBaseChannel, + public HttpAsyncAborter, + public nsICacheInfoChannel, + public nsIProxiedChannel, + public nsIApplicationCacheChannel, + public nsIAsyncVerifyRedirectCallback, + public nsIChildChannel, + public nsIHttpChannelChild, + public nsIMultiPartChannel, + public nsIThreadRetargetableRequest, + public NeckoTargetHolder { + virtual ~HttpChannelChild(); + + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_NSICACHEINFOCHANNEL + NS_DECL_NSIPROXIEDCHANNEL + NS_DECL_NSIAPPLICATIONCACHECONTAINER + NS_DECL_NSIAPPLICATIONCACHECHANNEL + NS_DECL_NSIASYNCVERIFYREDIRECTCALLBACK + NS_DECL_NSICHILDCHANNEL + NS_DECL_NSIHTTPCHANNELCHILD + NS_DECL_NSIMULTIPARTCHANNEL + NS_DECL_NSITHREADRETARGETABLEREQUEST + NS_DECLARE_STATIC_IID_ACCESSOR(HTTP_CHANNEL_CHILD_IID) + + HttpChannelChild(); + + // Methods HttpBaseChannel didn't implement for us or that we override. + // + // nsIRequest + NS_IMETHOD Cancel(nsresult status) override; + NS_IMETHOD Suspend() override; + NS_IMETHOD Resume() override; + // nsIChannel + NS_IMETHOD GetSecurityInfo(nsISupports** aSecurityInfo) override; + NS_IMETHOD AsyncOpen(nsIStreamListener* aListener) override; + + // HttpBaseChannel::nsIHttpChannel + NS_IMETHOD SetRequestHeader(const nsACString& aHeader, + const nsACString& aValue, bool aMerge) override; + NS_IMETHOD SetEmptyRequestHeader(const nsACString& aHeader) override; + NS_IMETHOD RedirectTo(nsIURI* newURI) override; + NS_IMETHOD UpgradeToSecure() override; + NS_IMETHOD GetProtocolVersion(nsACString& aProtocolVersion) override; + void DoDiagnosticAssertWhenOnStopNotCalledOnDestroy() override; + // nsIHttpChannelInternal + NS_IMETHOD SetupFallbackChannel(const char* aFallbackKey) override; + NS_IMETHOD GetIsAuthChannel(bool* aIsAuthChannel) override; + // nsISupportsPriority + NS_IMETHOD SetPriority(int32_t value) override; + // nsIClassOfService + NS_IMETHOD SetClassFlags(uint32_t inFlags) override; + NS_IMETHOD AddClassFlags(uint32_t inFlags) override; + NS_IMETHOD ClearClassFlags(uint32_t inFlags) override; + // nsIResumableChannel + NS_IMETHOD ResumeAt(uint64_t startPos, const nsACString& entityID) override; + + nsresult SetReferrerHeader(const nsACString& aReferrer, + bool aRespectBeforeConnect) override; + + [[nodiscard]] bool IsSuspended(); + + void OnCopyComplete(nsresult aStatus) override; + + // Callback while background channel is ready. + void OnBackgroundChildReady(HttpBackgroundChannelChild* aBgChild); + // Callback while background channel is destroyed. + void OnBackgroundChildDestroyed(HttpBackgroundChannelChild* aBgChild); + + nsresult CrossProcessRedirectFinished(nsresult aStatus); + + protected: + mozilla::ipc::IPCResult RecvOnStartRequestSent() override; + mozilla::ipc::IPCResult RecvFailedAsyncOpen(const nsresult& status) override; + mozilla::ipc::IPCResult RecvRedirect1Begin( + const uint32_t& registrarId, const URIParams& newURI, + const uint32_t& newLoadFlags, const uint32_t& redirectFlags, + const ParentLoadInfoForwarderArgs& loadInfoForwarder, + const nsHttpResponseHead& responseHead, + const nsCString& securityInfoSerialization, const uint64_t& channelId, + const NetAddr& oldPeerAddr, + const ResourceTimingStructArgs& aTiming) override; + mozilla::ipc::IPCResult RecvRedirect3Complete() override; + mozilla::ipc::IPCResult RecvDeleteSelf() override; + + mozilla::ipc::IPCResult RecvReportSecurityMessage( + const nsString& messageTag, const nsString& messageCategory) override; + + mozilla::ipc::IPCResult RecvIssueDeprecationWarning( + const uint32_t& warning, const bool& asError) override; + + mozilla::ipc::IPCResult RecvSetPriority(const int16_t& aPriority) override; + + mozilla::ipc::IPCResult RecvOriginalCacheInputStreamAvailable( + const Maybe& aStream) override; + + mozilla::ipc::IPCResult RecvAltDataCacheInputStreamAvailable( + const Maybe& aStream) override; + + virtual void ActorDestroy(ActorDestroyReason aWhy) override; + + virtual void DoNotifyListenerCleanup() override; + + virtual void DoAsyncAbort(nsresult aStatus) override; + + nsresult AsyncCall( + void (HttpChannelChild::*funcPtr)(), + nsRunnableMethod** retval = nullptr) override { + // Normally, this method would just be implemented directly, but clang + // miscompiles the corresponding non-virtual thunk on linux x86. + // It however doesn't when going though a non-virtual method. + // https://bugs.llvm.org/show_bug.cgi?id=38466 + return AsyncCallImpl(funcPtr, retval); + }; + + // Get event target for processing network events. + already_AddRefed GetNeckoTarget() override; + + virtual mozilla::ipc::IPCResult RecvLogBlockedCORSRequest( + const nsString& aMessage, const nsCString& aCategory) override; + NS_IMETHOD LogBlockedCORSRequest(const nsAString& aMessage, + const nsACString& aCategory) override; + + virtual mozilla::ipc::IPCResult RecvLogMimeTypeMismatch( + const nsCString& aMessageName, const bool& aWarning, const nsString& aURL, + const nsString& aContentType) override; + NS_IMETHOD LogMimeTypeMismatch(const nsACString& aMessageName, bool aWarning, + const nsAString& aURL, + const nsAString& aContentType) override; + + private: + // We want to handle failure result of AsyncOpen, hence AsyncOpen calls the + // Internal method + nsresult AsyncOpenInternal(nsIStreamListener* aListener); + + nsresult AsyncCallImpl(void (HttpChannelChild::*funcPtr)(), + nsRunnableMethod** retval); + + // Sets the event target for future IPC messages. Messages will either be + // directed to the TabGroup or DocGroup, depending on the LoadInfo associated + // with the channel. Should be called when a new channel is being set up, + // before the constructor message is sent to the parent. + void SetEventTarget(); + + // Get event target for ODA. + already_AddRefed GetODATarget(); + + [[nodiscard]] nsresult ContinueAsyncOpen(); + void ProcessOnStartRequest(const nsHttpResponseHead& aResponseHead, + const bool& aUseResponseHead, + const nsHttpHeaderArray& aRequestHeaders, + const HttpChannelOnStartRequestArgs& aArgs); + + // Callbacks while receiving OnTransportAndData/OnStopRequest/OnProgress/ + // OnStatus/FlushedForDiversion/DivertMessages on background IPC channel. + void ProcessOnTransportAndData(const nsresult& aChannelStatus, + const nsresult& aStatus, + const uint64_t& aOffset, + const uint32_t& aCount, + const nsCString& aData); + void ProcessOnStopRequest(const nsresult& aChannelStatus, + const ResourceTimingStructArgs& aTiming, + const nsHttpHeaderArray& aResponseTrailers, + nsTArray&& aConsoleReports, + bool aFromSocketProcess); + void ProcessOnConsoleReport( + nsTArray&& aConsoleReports); + + void ProcessNotifyClassificationFlags(uint32_t aClassificationFlags, + bool aIsThirdParty); + void ProcessNotifyFlashPluginStateChanged( + nsIHttpChannel::FlashPluginState aState); + void ProcessSetClassifierMatchedInfo(const nsCString& aList, + const nsCString& aProvider, + const nsCString& aFullHash); + void ProcessSetClassifierMatchedTrackingInfo(const nsCString& aLists, + const nsCString& aFullHashes); + void ProcessOnAfterLastPart(const nsresult& aStatus); + void ProcessOnProgress(const int64_t& aProgress, const int64_t& aProgressMax); + + void ProcessOnStatus(const nsresult& aStatus); + + void ProcessAttachStreamFilter( + Endpoint&& aEndpoint); + + // Return true if we need to tell the parent the size of unreported received + // data + bool NeedToReportBytesRead(); + int32_t mUnreportBytesRead = 0; + + void DoOnConsoleReport(nsTArray&& aConsoleReports); + void DoOnStartRequest(nsIRequest* aRequest, nsISupports* aContext); + void DoOnStatus(nsIRequest* aRequest, nsresult status); + void DoOnProgress(nsIRequest* aRequest, int64_t progress, + int64_t progressMax); + void DoOnDataAvailable(nsIRequest* aRequest, nsISupports* aContext, + nsIInputStream* aStream, uint64_t offset, + uint32_t count); + void DoPreOnStopRequest(nsresult aStatus); + void DoOnStopRequest(nsIRequest* aRequest, nsresult aChannelStatus, + nsISupports* aContext); + void ContinueOnStopRequest(); + + // Try send DeletingChannel message to parent side. Dispatch an async task to + // main thread if invoking on non-main thread. + void TrySendDeletingChannel(); + + // Try invoke Cancel if on main thread, or prepend a CancelEvent in mEventQ to + // ensure Cacnel is processed before any other channel events. + void CancelOnMainThread(nsresult aRv); + + private: + // this section is for main-thread-only object + // all the references need to be proxy released on main thread. + nsCOMPtr mRedirectChannelChild; + + // Proxy release all members above on main thread. + void ReleaseMainThreadOnlyReferences(); + + private: + nsCString mProtocolVersion; + + RequestHeaderTuples mClientSetRequestHeaders; + RefPtr mEventQ; + + nsCOMPtr mOriginalInputStreamReceiver; + nsCOMPtr mAltDataInputStreamReceiver; + + // Used to ensure atomicity of mBgChild and mBgInitFailCallback + Mutex mBgChildMutex; + + // Associated HTTP background channel + RefPtr mBgChild; + + // Error handling procedure if failed to establish PBackground IPC + nsCOMPtr mBgInitFailCallback; + + // Remove the association with background channel after OnStopRequest + // or AsyncAbort. + void CleanupBackgroundChannel(); + + // Target thread for delivering ODA. + nsCOMPtr mODATarget; + // Used to ensure atomicity of mNeckoTarget / mODATarget; + Mutex mEventTargetMutex; + + TimeStamp mLastStatusReported; + + uint64_t mCacheEntryId; + + // The result of RetargetDeliveryTo for this channel. + // |notRequested| represents OMT is not requested by the channel owner. + LABELS_HTTP_CHILD_OMT_STATS mOMTResult = + LABELS_HTTP_CHILD_OMT_STATS::notRequested; + + uint32_t mCacheKey; + int32_t mCacheFetchCount; + uint32_t mCacheExpirationTime; + + // If we're handling a multi-part response, then this is set to the current + // part ID during OnStartRequest. + Maybe mMultiPartID; + + // To ensure only one SendDeletingChannel is triggered. + Atomic mDeletingChannelSent; + + Atomic mIsFromCache; + Atomic mIsRacing; + // Set if we get the result and cache |mNeedToReportBytesRead| + Atomic mCacheNeedToReportBytesReadInitialized; + // True if we need to tell the parent the size of unreported received data + Atomic mNeedToReportBytesRead; + +#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED + bool mDoDiagnosticAssertWhenOnStopNotCalledOnDestroy = false; + bool mAsyncOpenSucceeded = false; + bool mSuccesfullyRedirected = false; + bool mRemoteChannelExistedAtCancel = false; + bool mEverHadBgChildAtAsyncOpen = false; + bool mEverHadBgChildAtConnectParent = false; + bool mCreateBackgroundChannelFailed = false; + bool mBgInitFailCallbackTriggered = false; + bool mCanSendAtCancel = false; + // State of the HttpBackgroundChannelChild's event queue during destruction. + enum BckChildQueueStatus { + // BckChild never told us + BCKCHILD_UNKNOWN, + // BckChild was empty at the time of destruction + BCKCHILD_EMPTY, + // BckChild was keeping events in the queue at the destruction time! + BCKCHILD_NON_EMPTY + }; + Atomic mBackgroundChildQueueFinalState; + Maybe mActorDestroyReason; +#endif + + uint8_t mCacheEntryAvailable : 1; + uint8_t mAltDataCacheEntryAvailable : 1; + + // If ResumeAt is called before AsyncOpen, we need to send extra data upstream + uint8_t mSendResumeAt : 1; + + uint8_t mKeptAlive : 1; // IPC kept open, but only for security info + + // Set when ActorDestroy(ActorDestroyReason::Deletion) is called + // The channel must ignore any following OnStart/Stop/DataAvailable messages + uint8_t mIPCActorDeleted : 1; + + // Set if SendSuspend is called. Determines if SendResume is needed when + // diverting callbacks to parent. + uint8_t mSuspendSent : 1; + + // True if this channel is a multi-part channel, and the last part + // is currently being processed. + uint8_t mIsLastPartOfMultiPart : 1; + + // True if this channel is suspended by ConnectParent and not resumed by + // CompleteRedirectSetup/RecvDeleteSelf. + uint8_t mSuspendForWaitCompleteRedirectSetup : 1; + + // True if RecvOnStartRequestSent was received. + uint8_t mRecvOnStartRequestSentCalled : 1; + + // True if this channel is for a document and suspended by waiting for + // permission or cookie. That is, RecvOnStartRequestSent is received. + uint8_t mSuspendedByWaitingForPermissionCookie : 1; + + void CleanupRedirectingChannel(nsresult rv); + + // Calls OnStartRequest and/or OnStopRequest on our listener in case we didn't + // do that so far. If we already did, it will just release references to + // cleanup. + void NotifyOrReleaseListeners(nsresult rv); + + // true after successful AsyncOpen until OnStopRequest completes. + bool RemoteChannelExists() { return CanSend() && !mKeptAlive; } + + void AssociateApplicationCache(const nsCString& groupID, + const nsCString& clientID); + void OnStartRequest(const nsHttpResponseHead& aResponseHead, + const bool& aUseResponseHead, + const nsHttpHeaderArray& aRequestHeaders, + const HttpChannelOnStartRequestArgs& aArgs); + void OnTransportAndData(const nsresult& channelStatus, const nsresult& status, + const uint64_t& offset, const uint32_t& count, + const nsCString& data); + void OnStopRequest(const nsresult& channelStatus, + const ResourceTimingStructArgs& timing, + const nsHttpHeaderArray& aResponseTrailers); + void FailedAsyncOpen(const nsresult& status); + void HandleAsyncAbort(); + void Redirect1Begin(const uint32_t& registrarId, const URIParams& newUri, + const uint32_t& newLoadFlags, + const uint32_t& redirectFlags, + const ParentLoadInfoForwarderArgs& loadInfoForwarder, + const nsHttpResponseHead& responseHead, + const nsACString& securityInfoSerialization, + const uint64_t& channelId, + const ResourceTimingStructArgs& timing); + void Redirect3Complete(); + void DeleteSelf(); + void DoNotifyListener(); + void ContinueDoNotifyListener(); + void OnAfterLastPart(const nsresult& aStatus); + void MaybeConnectToSocketProcess(); + + // Create a a new channel to be used in a redirection, based on the provided + // response headers. + [[nodiscard]] nsresult SetupRedirect(nsIURI* uri, + const nsHttpResponseHead* responseHead, + const uint32_t& redirectFlags, + nsIChannel** outChannel); + + // Collect telemetry for the successful rate of OMT. + void CollectOMTTelemetry(); + + friend class HttpAsyncAborter; + friend class InterceptStreamListener; + friend class InterceptedChannelContent; + friend class HttpBackgroundChannelChild; + friend class NeckoTargetChannelFunctionEvent; +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(HttpChannelChild, HTTP_CHANNEL_CHILD_IID) + +//----------------------------------------------------------------------------- +// inline functions +//----------------------------------------------------------------------------- + +inline bool HttpChannelChild::IsSuspended() { return mSuspendCount != 0; } + +} // namespace net +} // namespace mozilla + +#endif // mozilla_net_HttpChannelChild_h diff --git a/netwerk/protocol/http/HttpChannelParams.ipdlh b/netwerk/protocol/http/HttpChannelParams.ipdlh new file mode 100644 index 0000000000..0fe36f42b9 --- /dev/null +++ b/netwerk/protocol/http/HttpChannelParams.ipdlh @@ -0,0 +1,57 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et tw=80 ft=cpp : */ + +/* 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 IPCServiceWorkerDescriptor; +include NeckoChannelParams; + +include "mozilla/dom/ReferrerInfoUtils.h"; +include "mozilla/net/NeckoMessageUtils.h"; + +using class mozilla::net::nsHttpHeaderArray from "nsHttpHeaderArray.h"; +using mozilla::net::NetAddr from "mozilla/net/DNS.h"; +using nsILoadInfo::CrossOriginOpenerPolicy from "nsILoadInfo.h"; +using refcounted class nsIReferrerInfo from "nsIReferrerInfo.h"; + +namespace mozilla { +namespace net { + +struct HttpChannelOnStartRequestArgs +{ + nsresult channelStatus; + ParentLoadInfoForwarderArgs loadInfoForwarder; + bool isFromCache; + bool isRacing; + bool cacheEntryAvailable; + uint64_t cacheEntryId; + int32_t cacheFetchCount; + uint32_t cacheExpirationTime; + nsCString securityInfoSerialization; + NetAddr selfAddr; + NetAddr peerAddr; + uint8_t redirectCount; + uint32_t cacheKey; + nsCString altDataType; + int64_t altDataLength; + bool deliveringAltData; + bool applyConversion; + bool isResolvedByTRR; + ResourceTimingStructArgs timing; + bool allRedirectsSameOrigin; + uint32_t? multiPartID; + bool isLastPartOfMultiPart; + CrossOriginOpenerPolicy openerPolicy; + nsCString appCacheGroupId; + nsCString appCacheClientId; + nsIReferrerInfo overrideReferrerInfo; + bool shouldWaitForOnStartRequestSent; + nsCString cookie; + bool dataFromSocketProcess; +}; + +} // namespace ipc +} // namespace mozilla diff --git a/netwerk/protocol/http/HttpChannelParent.cpp b/netwerk/protocol/http/HttpChannelParent.cpp new file mode 100644 index 0000000000..bc0fbab539 --- /dev/null +++ b/netwerk/protocol/http/HttpChannelParent.cpp @@ -0,0 +1,2145 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et 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/. */ + +// HttpLog.h should generally be included first +#include "HttpLog.h" + +#include "mozilla/ConsoleReportCollector.h" +#include "mozilla/ipc/FileDescriptorSetParent.h" +#include "mozilla/ipc/IPCStreamUtils.h" +#include "mozilla/net/HttpChannelParent.h" +#include "mozilla/dom/ContentParent.h" +#include "mozilla/dom/ContentProcessManager.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/ServiceWorkerUtils.h" +#include "mozilla/dom/BrowserParent.h" +#include "mozilla/net/NeckoParent.h" +#include "mozilla/InputStreamLengthHelper.h" +#include "mozilla/IntegerPrintfMacros.h" +#include "mozilla/StoragePrincipalHelper.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/Unused.h" +#include "HttpBackgroundChannelParent.h" +#include "ParentChannelListener.h" +#include "nsHttpHandler.h" +#include "nsNetCID.h" +#include "nsNetUtil.h" +#include "nsISupportsPriority.h" +#include "nsIAuthPromptProvider.h" +#include "mozilla/net/BackgroundChannelRegistrar.h" +#include "nsSerializationHelper.h" +#include "nsISerializable.h" +#include "nsIApplicationCacheService.h" +#include "mozilla/ipc/InputStreamUtils.h" +#include "mozilla/ipc/URIUtils.h" +#include "SerializedLoadContext.h" +#include "nsIAuthPrompt.h" +#include "nsIAuthPrompt2.h" +#include "mozilla/ipc/BackgroundUtils.h" +#include "mozilla/LoadInfo.h" +#include "nsQueryObject.h" +#include "mozilla/BasePrincipal.h" +#include "nsCORSListenerProxy.h" +#include "nsIIPCSerializableInputStream.h" +#include "nsIPrompt.h" +#include "nsIPromptFactory.h" +#include "mozilla/net/RedirectChannelRegistrar.h" +#include "nsIWindowWatcher.h" +#include "mozilla/dom/Document.h" +#include "nsISecureBrowserUI.h" +#include "nsStreamUtils.h" +#include "nsStringStream.h" +#include "nsThreadUtils.h" +#include "nsQueryObject.h" +#include "nsIMultiPartChannel.h" +#include "nsIViewSourceChannel.h" + +using mozilla::BasePrincipal; +using namespace mozilla::dom; +using namespace mozilla::ipc; + +namespace mozilla { +namespace net { + +HttpChannelParent::HttpChannelParent(dom::BrowserParent* iframeEmbedding, + nsILoadContext* aLoadContext, + PBOverrideStatus aOverrideStatus) + : mLoadContext(aLoadContext), + mIPCClosed(false), + mPBOverride(aOverrideStatus), + mStatus(NS_OK), + mIgnoreProgress(false), + mSentRedirect1BeginFailed(false), + mReceivedRedirect2Verify(false), + mHasSuspendedByBackPressure(false), + mCacheNeedFlowControlInitialized(false), + mNeedFlowControl(true), + mSuspendedForFlowControl(false), + mAfterOnStartRequestBegun(false), + mDataSentToChildProcess(false) { + LOG(("Creating HttpChannelParent [this=%p]\n", this)); + + // Ensure gHttpHandler is initialized: we need the atom table up and running. + nsCOMPtr dummyInitializer = + do_GetService(NS_NETWORK_PROTOCOL_CONTRACTID_PREFIX "http"); + + MOZ_ASSERT(gHttpHandler); + mHttpHandler = gHttpHandler; + + mBrowserParent = iframeEmbedding; + + mSendWindowSize = gHttpHandler->SendWindowSize(); + + mEventQ = + new ChannelEventQueue(static_cast(this)); +} + +HttpChannelParent::~HttpChannelParent() { + LOG(("Destroying HttpChannelParent [this=%p]\n", this)); + CleanupBackgroundChannel(); + + MOZ_ASSERT(!mRedirectCallback); + if (NS_WARN_IF(mRedirectCallback)) { + mRedirectCallback->OnRedirectVerifyCallback(NS_ERROR_UNEXPECTED); + mRedirectCallback = nullptr; + } +} + +void HttpChannelParent::ActorDestroy(ActorDestroyReason why) { + // We may still have refcount>0 if nsHttpChannel hasn't called OnStopRequest + // yet, but child process has crashed. We must not try to send any more msgs + // to child, or IPDL will kill chrome process, too. + mIPCClosed = true; + CleanupBackgroundChannel(); +} + +bool HttpChannelParent::Init(const HttpChannelCreationArgs& aArgs) { + LOG(("HttpChannelParent::Init [this=%p]\n", this)); + AUTO_PROFILER_LABEL("HttpChannelParent::Init", NETWORK); + switch (aArgs.type()) { + case HttpChannelCreationArgs::THttpChannelOpenArgs: { + const HttpChannelOpenArgs& a = aArgs.get_HttpChannelOpenArgs(); + return DoAsyncOpen( + a.uri(), a.original(), a.doc(), a.referrerInfo(), a.apiRedirectTo(), + a.topWindowURI(), a.loadFlags(), a.requestHeaders(), + a.requestMethod(), a.uploadStream(), a.uploadStreamHasHeaders(), + a.priority(), a.classOfService(), a.redirectionLimit(), a.allowSTS(), + a.thirdPartyFlags(), a.resumeAt(), a.startPos(), a.entityID(), + a.chooseApplicationCache(), a.appCacheClientID(), a.allowSpdy(), + a.allowHttp3(), a.allowAltSvc(), a.beConservative(), a.tlsFlags(), + a.loadInfo(), a.cacheKey(), a.requestContextID(), a.preflightArgs(), + a.initialRwin(), a.blockAuthPrompt(), a.allowStaleCacheContent(), + a.preferCacheLoadOverBypass(), a.contentTypeHint(), a.corsMode(), + a.redirectMode(), a.channelId(), a.integrityMetadata(), + a.contentWindowId(), a.preferredAlternativeTypes(), + a.topLevelOuterContentWindowId(), a.launchServiceWorkerStart(), + a.launchServiceWorkerEnd(), a.dispatchFetchEventStart(), + a.dispatchFetchEventEnd(), a.handleFetchEventStart(), + a.handleFetchEventEnd(), a.forceMainDocumentChannel(), + a.navigationStartTimeStamp(), a.hasNonEmptySandboxingFlag()); + } + case HttpChannelCreationArgs::THttpChannelConnectArgs: { + const HttpChannelConnectArgs& cArgs = aArgs.get_HttpChannelConnectArgs(); + return ConnectChannel(cArgs.registrarId()); + } + default: + MOZ_ASSERT_UNREACHABLE("unknown open type"); + return false; + } +} + +void HttpChannelParent::TryInvokeAsyncOpen(nsresult aRv) { + LOG(("HttpChannelParent::TryInvokeAsyncOpen [this=%p barrier=%u rv=%" PRIx32 + "]\n", + this, mAsyncOpenBarrier, static_cast(aRv))); + MOZ_ASSERT(NS_IsMainThread()); + AUTO_PROFILER_LABEL("HttpChannelParent::TryInvokeAsyncOpen", NETWORK); + + // TryInvokeAsyncOpen is called more than we expected. + // Assert in nightly build but ignore it in release channel. + MOZ_DIAGNOSTIC_ASSERT(mAsyncOpenBarrier > 0); + if (NS_WARN_IF(!mAsyncOpenBarrier)) { + return; + } + + if (--mAsyncOpenBarrier > 0 && NS_SUCCEEDED(aRv)) { + // Need to wait for more events. + return; + } + + InvokeAsyncOpen(aRv); +} + +void HttpChannelParent::OnBackgroundParentReady( + HttpBackgroundChannelParent* aBgParent) { + LOG(("HttpChannelParent::OnBackgroundParentReady [this=%p bgParent=%p]\n", + this, aBgParent)); + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!mBgParent); + + mBgParent = aBgParent; + + mPromise.ResolveIfExists(true, __func__); +} + +void HttpChannelParent::OnBackgroundParentDestroyed() { + LOG(("HttpChannelParent::OnBackgroundParentDestroyed [this=%p]\n", this)); + MOZ_ASSERT(NS_IsMainThread()); + + if (!mPromise.IsEmpty()) { + MOZ_ASSERT(!mBgParent); + mPromise.Reject(NS_ERROR_FAILURE, __func__); + return; + } + + if (!mBgParent) { + return; + } + + // Background channel is closed unexpectly, abort PHttpChannel operation. + mBgParent = nullptr; + Delete(); +} + +void HttpChannelParent::CleanupBackgroundChannel() { + LOG(("HttpChannelParent::CleanupBackgroundChannel [this=%p bgParent=%p]\n", + this, mBgParent.get())); + MOZ_ASSERT(NS_IsMainThread()); + + if (mBgParent) { + RefPtr bgParent = std::move(mBgParent); + bgParent->OnChannelClosed(); + return; + } + + // The nsHttpChannel may have a reference to this parent, release it + // to avoid circular references. + RefPtr httpChannelImpl = do_QueryObject(mChannel); + if (httpChannelImpl) { + httpChannelImpl->SetWarningReporter(nullptr); + } + + if (!mPromise.IsEmpty()) { + mRequest.DisconnectIfExists(); + mPromise.Reject(NS_ERROR_FAILURE, __func__); + + if (!mChannel) { + return; + } + + // This HttpChannelParent might still have a reference from + // BackgroundChannelRegistrar. + nsCOMPtr registrar = + BackgroundChannelRegistrar::GetOrCreate(); + MOZ_ASSERT(registrar); + + registrar->DeleteChannel(mChannel->ChannelId()); + + // If mAsyncOpenBarrier is greater than zero, it means AsyncOpen procedure + // is still on going. we need to abort AsyncOpen with failure to destroy + // PHttpChannel actor. + if (mAsyncOpenBarrier) { + TryInvokeAsyncOpen(NS_ERROR_FAILURE); + } + } +} + +base::ProcessId HttpChannelParent::OtherPid() const { + if (mIPCClosed) { + return 0; + } + return IProtocol::OtherPid(); +} + +//----------------------------------------------------------------------------- +// HttpChannelParent::nsISupports +//----------------------------------------------------------------------------- + +NS_IMPL_ADDREF(HttpChannelParent) +NS_IMPL_RELEASE(HttpChannelParent) +NS_INTERFACE_MAP_BEGIN(HttpChannelParent) + NS_INTERFACE_MAP_ENTRY(nsIInterfaceRequestor) + NS_INTERFACE_MAP_ENTRY(nsIProgressEventSink) + NS_INTERFACE_MAP_ENTRY(nsIRequestObserver) + NS_INTERFACE_MAP_ENTRY(nsIStreamListener) + NS_INTERFACE_MAP_ENTRY(nsIParentChannel) + NS_INTERFACE_MAP_ENTRY(nsIAuthPromptProvider) + NS_INTERFACE_MAP_ENTRY(nsIParentRedirectingChannel) + NS_INTERFACE_MAP_ENTRY(nsIDeprecationWarner) + NS_INTERFACE_MAP_ENTRY(nsIAsyncVerifyRedirectReadyCallback) + NS_INTERFACE_MAP_ENTRY(nsIChannelEventSink) + NS_INTERFACE_MAP_ENTRY(nsIRedirectResultListener) + NS_INTERFACE_MAP_ENTRY(nsIMultiPartChannelListener) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIParentRedirectingChannel) + NS_INTERFACE_MAP_ENTRY_CONCRETE(HttpChannelParent) +NS_INTERFACE_MAP_END + +//----------------------------------------------------------------------------- +// HttpChannelParent::nsIInterfaceRequestor +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +HttpChannelParent::GetInterface(const nsIID& aIID, void** result) { + // Only support nsIAuthPromptProvider in Content process + if (XRE_IsParentProcess() && aIID.Equals(NS_GET_IID(nsIAuthPromptProvider))) { + *result = nullptr; + return NS_OK; + } + + // A system XHR can be created without reference to a window, hence mTabParent + // may be null. In that case we want to let the window watcher pick a prompt + // directly. + if (!mBrowserParent && (aIID.Equals(NS_GET_IID(nsIAuthPrompt)) || + aIID.Equals(NS_GET_IID(nsIAuthPrompt2)))) { + nsresult rv; + nsCOMPtr wwatch = + do_GetService(NS_WINDOWWATCHER_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, NS_ERROR_NO_INTERFACE); + + bool hasWindowCreator = false; + Unused << wwatch->HasWindowCreator(&hasWindowCreator); + if (!hasWindowCreator) { + return NS_ERROR_NO_INTERFACE; + } + + nsCOMPtr factory = do_QueryInterface(wwatch); + if (!factory) { + return NS_ERROR_NO_INTERFACE; + } + rv = factory->GetPrompt(nullptr, aIID, reinterpret_cast(result)); + if (NS_FAILED(rv)) { + return NS_ERROR_NO_INTERFACE; + } + return NS_OK; + } + + // Only support nsILoadContext if child channel's callbacks did too + if (aIID.Equals(NS_GET_IID(nsILoadContext)) && mLoadContext) { + nsCOMPtr copy = mLoadContext; + copy.forget(result); + return NS_OK; + } + + return QueryInterface(aIID, result); +} + +//----------------------------------------------------------------------------- +// HttpChannelParent::PHttpChannelParent +//----------------------------------------------------------------------------- + +void HttpChannelParent::AsyncOpenFailed(nsresult aRv) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(NS_FAILED(aRv)); + + // Break the reference cycle among HttpChannelParent, + // ParentChannelListener, and nsHttpChannel to avoid memory leakage. + mChannel = nullptr; + mParentListener = nullptr; + + if (!mIPCClosed) { + Unused << SendFailedAsyncOpen(aRv); + } +} + +void HttpChannelParent::InvokeAsyncOpen(nsresult rv) { + LOG(("HttpChannelParent::InvokeAsyncOpen [this=%p rv=%" PRIx32 "]\n", this, + static_cast(rv))); + MOZ_ASSERT(NS_IsMainThread()); + + if (NS_FAILED(rv)) { + AsyncOpenFailed(rv); + return; + } + + rv = mChannel->AsyncOpen(mParentListener); + if (NS_FAILED(rv)) { + AsyncOpenFailed(rv); + } +} + +bool HttpChannelParent::DoAsyncOpen( + const URIParams& aURI, const Maybe& aOriginalURI, + const Maybe& aDocURI, nsIReferrerInfo* aReferrerInfo, + const Maybe& aAPIRedirectToURI, + const Maybe& aTopWindowURI, const uint32_t& aLoadFlags, + const RequestHeaderTuples& requestHeaders, const nsCString& requestMethod, + const Maybe& uploadStream, const bool& uploadStreamHasHeaders, + const int16_t& priority, const uint32_t& classOfService, + const uint8_t& redirectionLimit, const bool& allowSTS, + const uint32_t& thirdPartyFlags, const bool& doResumeAt, + const uint64_t& startPos, const nsCString& entityID, + const bool& chooseApplicationCache, const nsCString& appCacheClientID, + const bool& allowSpdy, const bool& allowHttp3, const bool& allowAltSvc, + const bool& beConservative, const uint32_t& tlsFlags, + const Maybe& aLoadInfoArgs, const uint32_t& aCacheKey, + const uint64_t& aRequestContextID, + const Maybe& aCorsPreflightArgs, + const uint32_t& aInitialRwin, const bool& aBlockAuthPrompt, + const bool& aAllowStaleCacheContent, const bool& aPreferCacheLoadOverBypass, + const nsCString& aContentTypeHint, const uint32_t& aCorsMode, + const uint32_t& aRedirectMode, const uint64_t& aChannelId, + const nsString& aIntegrityMetadata, const uint64_t& aContentWindowId, + const nsTArray& + aPreferredAlternativeTypes, + const uint64_t& aTopLevelOuterContentWindowId, + const TimeStamp& aLaunchServiceWorkerStart, + const TimeStamp& aLaunchServiceWorkerEnd, + const TimeStamp& aDispatchFetchEventStart, + const TimeStamp& aDispatchFetchEventEnd, + const TimeStamp& aHandleFetchEventStart, + const TimeStamp& aHandleFetchEventEnd, + const bool& aForceMainDocumentChannel, + const TimeStamp& aNavigationStartTimeStamp, + const bool& aHasNonEmptySandboxingFlag) { + nsCOMPtr uri = DeserializeURI(aURI); + if (!uri) { + // URIParams does MOZ_ASSERT if null, but we need to protect opt builds from + // null deref here. + return false; + } + nsCOMPtr originalUri = DeserializeURI(aOriginalURI); + nsCOMPtr docUri = DeserializeURI(aDocURI); + nsCOMPtr apiRedirectToUri = DeserializeURI(aAPIRedirectToURI); + nsCOMPtr topWindowUri = DeserializeURI(aTopWindowURI); + + LOG(("HttpChannelParent RecvAsyncOpen [this=%p uri=%s, gid=%" PRIu64 + " topwinid=%" PRIx64 "]\n", + this, uri->GetSpecOrDefault().get(), aChannelId, + aTopLevelOuterContentWindowId)); + + nsresult rv; + + nsCOMPtr ios(do_GetIOService(&rv)); + if (NS_FAILED(rv)) return SendFailedAsyncOpen(rv); + + nsCOMPtr loadInfo; + rv = mozilla::ipc::LoadInfoArgsToLoadInfo(aLoadInfoArgs, + getter_AddRefs(loadInfo)); + if (NS_FAILED(rv)) { + return SendFailedAsyncOpen(rv); + } + + nsCOMPtr channel; + rv = NS_NewChannelInternal(getter_AddRefs(channel), uri, loadInfo, nullptr, + nullptr, nullptr, aLoadFlags, ios); + if (NS_FAILED(rv)) { + return SendFailedAsyncOpen(rv); + } + + RefPtr httpChannel = do_QueryObject(channel, &rv); + if (NS_FAILED(rv)) { + return SendFailedAsyncOpen(rv); + } + + // Set attributes needed to create a FetchEvent from this channel. + httpChannel->SetCorsMode(aCorsMode); + httpChannel->SetRedirectMode(aRedirectMode); + + // Set the channelId allocated in child to the parent instance + httpChannel->SetChannelId(aChannelId); + httpChannel->SetTopLevelContentWindowId(aContentWindowId); + httpChannel->SetTopLevelOuterContentWindowId(aTopLevelOuterContentWindowId); + + httpChannel->SetIntegrityMetadata(aIntegrityMetadata); + + RefPtr httpChannelImpl = do_QueryObject(httpChannel); + if (httpChannelImpl) { + httpChannelImpl->SetWarningReporter(this); + } + httpChannel->SetTimingEnabled(true); + if (mPBOverride != kPBOverride_Unset) { + httpChannel->SetPrivate(mPBOverride == kPBOverride_Private ? true : false); + } + + if (doResumeAt) httpChannel->ResumeAt(startPos, entityID); + + if (originalUri) httpChannel->SetOriginalURI(originalUri); + if (docUri) httpChannel->SetDocumentURI(docUri); + if (aReferrerInfo) { + // Referrer header is computed in child no need to recompute here + rv = + httpChannel->SetReferrerInfoInternal(aReferrerInfo, false, false, true); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + + if (apiRedirectToUri) httpChannel->RedirectTo(apiRedirectToUri); + if (topWindowUri) { + httpChannel->SetTopWindowURI(topWindowUri); + } + + if (aLoadFlags != nsIRequest::LOAD_NORMAL) + httpChannel->SetLoadFlags(aLoadFlags); + + if (aForceMainDocumentChannel) { + httpChannel->SetIsMainDocumentChannel(true); + } + + if (aHasNonEmptySandboxingFlag) { + httpChannel->SetHasNonEmptySandboxingFlag(true); + } + + for (uint32_t i = 0; i < requestHeaders.Length(); i++) { + if (requestHeaders[i].mEmpty) { + httpChannel->SetEmptyRequestHeader(requestHeaders[i].mHeader); + } else { + httpChannel->SetRequestHeader(requestHeaders[i].mHeader, + requestHeaders[i].mValue, + requestHeaders[i].mMerge); + } + } + + RefPtr parentListener = new ParentChannelListener( + this, mBrowserParent ? mBrowserParent->GetBrowsingContext() : nullptr, + mLoadContext && mLoadContext->UsePrivateBrowsing()); + + httpChannel->SetRequestMethod(nsDependentCString(requestMethod.get())); + + if (aCorsPreflightArgs.isSome()) { + const CorsPreflightArgs& args = aCorsPreflightArgs.ref(); + httpChannel->SetCorsPreflightParameters(args.unsafeHeaders(), false); + } + + nsCOMPtr stream = DeserializeIPCStream(uploadStream); + if (stream) { + int64_t length; + if (InputStreamLengthHelper::GetSyncLength(stream, &length)) { + httpChannel->InternalSetUploadStreamLength(length >= 0 ? length : 0); + } else { + // Wait for the nputStreamLengthHelper::GetAsyncLength callback. + ++mAsyncOpenBarrier; + + // Let's resolve the size of the stream. The following operation is always + // async. + RefPtr self = this; + InputStreamLengthHelper::GetAsyncLength(stream, [self, httpChannel]( + int64_t aLength) { + httpChannel->InternalSetUploadStreamLength(aLength >= 0 ? aLength : 0); + self->TryInvokeAsyncOpen(NS_OK); + }); + } + + httpChannel->InternalSetUploadStream(stream); + httpChannel->SetUploadStreamHasHeaders(uploadStreamHasHeaders); + } + + nsCOMPtr cacheChannel = + do_QueryInterface(static_cast(httpChannel.get())); + if (cacheChannel) { + cacheChannel->SetCacheKey(aCacheKey); + for (auto& data : aPreferredAlternativeTypes) { + cacheChannel->PreferAlternativeDataType(data.type(), data.contentType(), + data.deliverAltData()); + } + + cacheChannel->SetAllowStaleCacheContent(aAllowStaleCacheContent); + cacheChannel->SetPreferCacheLoadOverBypass(aPreferCacheLoadOverBypass); + + // This is to mark that the results are going to the content process. + if (httpChannelImpl) { + httpChannelImpl->SetAltDataForChild(true); + } + } + + httpChannel->SetContentType(aContentTypeHint); + + if (priority != nsISupportsPriority::PRIORITY_NORMAL) { + httpChannel->SetPriority(priority); + } + if (classOfService) { + httpChannel->SetClassFlags(classOfService); + } + httpChannel->SetRedirectionLimit(redirectionLimit); + httpChannel->SetAllowSTS(allowSTS); + httpChannel->SetThirdPartyFlags(thirdPartyFlags); + httpChannel->SetAllowSpdy(allowSpdy); + httpChannel->SetAllowHttp3(allowHttp3); + httpChannel->SetAllowAltSvc(allowAltSvc); + httpChannel->SetBeConservative(beConservative); + httpChannel->SetTlsFlags(tlsFlags); + httpChannel->SetInitialRwin(aInitialRwin); + httpChannel->SetBlockAuthPrompt(aBlockAuthPrompt); + + httpChannel->SetLaunchServiceWorkerStart(aLaunchServiceWorkerStart); + httpChannel->SetLaunchServiceWorkerEnd(aLaunchServiceWorkerEnd); + httpChannel->SetDispatchFetchEventStart(aDispatchFetchEventStart); + httpChannel->SetDispatchFetchEventEnd(aDispatchFetchEventEnd); + httpChannel->SetHandleFetchEventStart(aHandleFetchEventStart); + httpChannel->SetHandleFetchEventEnd(aHandleFetchEventEnd); + + httpChannel->SetNavigationStartTimeStamp(aNavigationStartTimeStamp); + + nsCOMPtr appCacheChan = + do_QueryObject(httpChannel); + nsCOMPtr appCacheService = + do_GetService(NS_APPLICATIONCACHESERVICE_CONTRACTID); + + bool setChooseApplicationCache = chooseApplicationCache; + if (appCacheChan && appCacheService) { + // We might potentially want to drop this flag (that is TRUE by default) + // after we successfully associate the channel with an application cache + // reported by the channel child. Dropping it here may be too early. + appCacheChan->SetInheritApplicationCache(false); + if (!appCacheClientID.IsEmpty()) { + nsCOMPtr appCache; + rv = appCacheService->GetApplicationCache(appCacheClientID, + getter_AddRefs(appCache)); + if (NS_SUCCEEDED(rv)) { + appCacheChan->SetApplicationCache(appCache); + setChooseApplicationCache = false; + } + } + + if (setChooseApplicationCache) { + OriginAttributes attrs; + StoragePrincipalHelper::GetOriginAttributes( + httpChannel, attrs, StoragePrincipalHelper::eRegularPrincipal); + + nsCOMPtr principal = + BasePrincipal::CreateContentPrincipal(uri, attrs); + + bool chooseAppCache = false; + // This works because we've already called SetNotificationCallbacks and + // done mPBOverride logic by this point. + chooseAppCache = NS_ShouldCheckAppCache(principal); + + appCacheChan->SetChooseApplicationCache(chooseAppCache); + } + } + + httpChannel->SetRequestContextID(aRequestContextID); + + // Store the strong reference of channel and parent listener object until + // all the initialization procedure is complete without failure, to remove + // cycle reference in fail case and to avoid memory leakage. + mChannel = std::move(httpChannel); + mParentListener = std::move(parentListener); + mChannel->SetNotificationCallbacks(mParentListener); + + MOZ_ASSERT(!mBgParent); + MOZ_ASSERT(mPromise.IsEmpty()); + // Wait for HttpBackgrounChannel to continue the async open procedure. + ++mAsyncOpenBarrier; + RefPtr self = this; + WaitForBgParent() + ->Then( + GetMainThreadSerialEventTarget(), __func__, + [self]() { + self->mRequest.Complete(); + self->TryInvokeAsyncOpen(NS_OK); + }, + [self](nsresult aStatus) { + self->mRequest.Complete(); + self->TryInvokeAsyncOpen(aStatus); + }) + ->Track(mRequest); + + // The stream, received from the child process, must be cloneable and seekable + // in order to allow devtools to inspect its content. + nsCOMPtr r = + NS_NewRunnableFunction("HttpChannelParent::EnsureUploadStreamIsCloneable", + [self]() { self->TryInvokeAsyncOpen(NS_OK); }); + ++mAsyncOpenBarrier; + mChannel->EnsureUploadStreamIsCloneable(r); + return true; +} + +RefPtr HttpChannelParent::WaitForBgParent() { + LOG(("HttpChannelParent::WaitForBgParent [this=%p]\n", this)); + MOZ_ASSERT(!mBgParent); + + if (!mChannel) { + return GenericNonExclusivePromise::CreateAndReject(NS_ERROR_FAILURE, + __func__); + } + + nsCOMPtr registrar = + BackgroundChannelRegistrar::GetOrCreate(); + MOZ_ASSERT(registrar); + registrar->LinkHttpChannel(mChannel->ChannelId(), this); + + if (mBgParent) { + return GenericNonExclusivePromise::CreateAndResolve(true, __func__); + } + + return mPromise.Ensure(__func__); +} + +bool HttpChannelParent::ConnectChannel(const uint32_t& registrarId) { + nsresult rv; + + LOG( + ("HttpChannelParent::ConnectChannel: Looking for a registered channel " + "[this=%p, id=%" PRIu32 "]\n", + this, registrarId)); + nsCOMPtr channel; + rv = NS_LinkRedirectChannels(registrarId, this, getter_AddRefs(channel)); + if (NS_FAILED(rv)) { + NS_ERROR("Could not find the http channel to connect its IPC parent"); + // This makes the channel delete itself safely. It's the only thing + // we can do now, since this parent channel cannot be used and there is + // no other way to tell the child side there were something wrong. + Delete(); + return true; + } + + LOG((" found channel %p, rv=%08" PRIx32, channel.get(), + static_cast(rv))); + mChannel = do_QueryObject(channel); + if (!mChannel) { + LOG((" but it's not HttpBaseChannel")); + Delete(); + return true; + } + + LOG((" and it is HttpBaseChannel %p", mChannel.get())); + + RefPtr httpChannelImpl = do_QueryObject(mChannel); + if (httpChannelImpl) { + httpChannelImpl->SetWarningReporter(this); + } + + if (mPBOverride != kPBOverride_Unset) { + // redirected-to channel may not support PB + nsCOMPtr pbChannel = do_QueryObject(mChannel); + if (pbChannel) { + pbChannel->SetPrivate(mPBOverride == kPBOverride_Private ? true : false); + } + } + + MOZ_ASSERT(!mBgParent); + MOZ_ASSERT(mPromise.IsEmpty()); + // Waiting for background channel + RefPtr self = this; + WaitForBgParent() + ->Then( + GetMainThreadSerialEventTarget(), __func__, + [self]() { self->mRequest.Complete(); }, + [self](const nsresult& aResult) { + NS_ERROR("failed to establish the background channel"); + self->mRequest.Complete(); + }) + ->Track(mRequest); + return true; +} + +mozilla::ipc::IPCResult HttpChannelParent::RecvSetPriority( + const int16_t& priority) { + LOG(("HttpChannelParent::RecvSetPriority [this=%p, priority=%d]\n", this, + priority)); + AUTO_PROFILER_LABEL("HttpChannelParent::RecvSetPriority", NETWORK); + + if (mChannel) { + mChannel->SetPriority(priority); + } + + nsCOMPtr priorityRedirectChannel = + do_QueryInterface(mRedirectChannel); + if (priorityRedirectChannel) priorityRedirectChannel->SetPriority(priority); + + return IPC_OK(); +} + +mozilla::ipc::IPCResult HttpChannelParent::RecvSetClassOfService( + const uint32_t& cos) { + if (mChannel) { + mChannel->SetClassFlags(cos); + } + return IPC_OK(); +} + +mozilla::ipc::IPCResult HttpChannelParent::RecvSuspend() { + LOG(("HttpChannelParent::RecvSuspend [this=%p]\n", this)); + + if (mChannel) { + mChannel->Suspend(); + } + return IPC_OK(); +} + +mozilla::ipc::IPCResult HttpChannelParent::RecvResume() { + LOG(("HttpChannelParent::RecvResume [this=%p]\n", this)); + + if (mChannel) { + mChannel->Resume(); + } + return IPC_OK(); +} + +mozilla::ipc::IPCResult HttpChannelParent::RecvCancel( + const nsresult& status, const uint32_t& requestBlockingReason) { + LOG(("HttpChannelParent::RecvCancel [this=%p]\n", this)); + + // May receive cancel before channel has been constructed! + if (mChannel) { + mChannel->Cancel(status); + + if (MOZ_UNLIKELY(requestBlockingReason != + nsILoadInfo::BLOCKING_REASON_NONE)) { + nsCOMPtr loadInfo = mChannel->LoadInfo(); + loadInfo->SetRequestBlockingReason(requestBlockingReason); + } + + // Once we receive |Cancel|, child will stop sending RecvBytesRead. Force + // the channel resumed if needed. + if (mSuspendedForFlowControl) { + LOG((" resume the channel due to e10s backpressure relief by cancel")); + Unused << mChannel->Resume(); + mSuspendedForFlowControl = false; + } + } else if (!mIPCClosed) { + // Make sure that the child correctly delivers all stream listener + // notifications. + Unused << SendFailedAsyncOpen(status); + } + + // We won't need flow control anymore. Toggle the flag to avoid |Suspend| + // since OnDataAvailable could be off-main-thread. + mCacheNeedFlowControlInitialized = true; + mNeedFlowControl = false; + return IPC_OK(); +} + +mozilla::ipc::IPCResult HttpChannelParent::RecvRedirect2Verify( + const nsresult& aResult, const RequestHeaderTuples& changedHeaders, + const uint32_t& aSourceRequestBlockingReason, + const Maybe& aTargetLoadInfoForwarder, + const uint32_t& loadFlags, nsIReferrerInfo* aReferrerInfo, + const Maybe& aAPIRedirectURI, + const Maybe& aCorsPreflightArgs, + const bool& aChooseAppcache) { + LOG(("HttpChannelParent::RecvRedirect2Verify [this=%p result=%" PRIx32 "]\n", + this, static_cast(aResult))); + + // Result from the child. If something fails here, we might overwrite a + // success with a further failure. + nsresult result = aResult; + + // Local results. + nsresult rv; + + if (NS_SUCCEEDED(result)) { + nsCOMPtr newHttpChannel = + do_QueryInterface(mRedirectChannel); + + if (newHttpChannel) { + nsCOMPtr apiRedirectUri = DeserializeURI(aAPIRedirectURI); + + if (apiRedirectUri) { + rv = newHttpChannel->RedirectTo(apiRedirectUri); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + + for (uint32_t i = 0; i < changedHeaders.Length(); i++) { + if (changedHeaders[i].mEmpty) { + rv = newHttpChannel->SetEmptyRequestHeader(changedHeaders[i].mHeader); + } else { + rv = newHttpChannel->SetRequestHeader(changedHeaders[i].mHeader, + changedHeaders[i].mValue, + changedHeaders[i].mMerge); + } + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + + // A successfully redirected channel must have the LOAD_REPLACE flag. + MOZ_ASSERT(loadFlags & nsIChannel::LOAD_REPLACE); + if (loadFlags & nsIChannel::LOAD_REPLACE) { + newHttpChannel->SetLoadFlags(loadFlags); + } + + if (aCorsPreflightArgs.isSome()) { + nsCOMPtr newInternalChannel = + do_QueryInterface(newHttpChannel); + MOZ_RELEASE_ASSERT(newInternalChannel); + const CorsPreflightArgs& args = aCorsPreflightArgs.ref(); + newInternalChannel->SetCorsPreflightParameters(args.unsafeHeaders(), + false); + } + + if (aReferrerInfo) { + RefPtr baseChannel = do_QueryObject(newHttpChannel); + MOZ_ASSERT(baseChannel); + if (baseChannel) { + // Referrer header is computed in child no need to recompute here + rv = baseChannel->SetReferrerInfoInternal(aReferrerInfo, false, false, + true); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + } + + nsCOMPtr appCacheChannel = + do_QueryInterface(newHttpChannel); + if (appCacheChannel) { + bool setChooseAppCache = false; + if (aChooseAppcache) { + nsCOMPtr uri; + // Using GetURI because this is what DoAsyncOpen uses. + newHttpChannel->GetURI(getter_AddRefs(uri)); + + OriginAttributes attrs; + StoragePrincipalHelper::GetOriginAttributes( + newHttpChannel, attrs, StoragePrincipalHelper::eRegularPrincipal); + + nsCOMPtr principal = + BasePrincipal::CreateContentPrincipal(uri, attrs); + + setChooseAppCache = NS_ShouldCheckAppCache(principal); + } + + appCacheChannel->SetChooseApplicationCache(setChooseAppCache); + } + + if (aTargetLoadInfoForwarder.isSome()) { + nsCOMPtr newLoadInfo = newHttpChannel->LoadInfo(); + rv = MergeChildLoadInfoForwarder(aTargetLoadInfoForwarder.ref(), + newLoadInfo); + if (NS_FAILED(rv) && NS_SUCCEEDED(result)) { + result = rv; + } + } + } + } + + // If the redirect is vetoed, reason is set on the source (current) channel's + // load info, so we must carry iver the change. + // The channel may have already been cleaned up, so there is nothing we can + // do. + if (MOZ_UNLIKELY(aSourceRequestBlockingReason != + nsILoadInfo::BLOCKING_REASON_NONE) && + mChannel) { + nsCOMPtr sourceLoadInfo = mChannel->LoadInfo(); + sourceLoadInfo->SetRequestBlockingReason(aSourceRequestBlockingReason); + } + + // Continue the verification procedure if child has veto the redirection. + if (NS_FAILED(result)) { + ContinueRedirect2Verify(result); + return IPC_OK(); + } + + // Wait for background channel ready on target channel + nsCOMPtr redirectReg = + RedirectChannelRegistrar::GetOrCreate(); + MOZ_ASSERT(redirectReg); + + nsCOMPtr redirectParentChannel; + rv = redirectReg->GetParentChannel(mRedirectChannelId, + getter_AddRefs(redirectParentChannel)); + MOZ_ASSERT(redirectParentChannel); + if (!redirectParentChannel) { + ContinueRedirect2Verify(rv); + return IPC_OK(); + } + + nsCOMPtr redirectedParent = + do_QueryInterface(redirectParentChannel); + if (!redirectedParent) { + // Continue verification procedure if redirecting to non-Http protocol + ContinueRedirect2Verify(result); + return IPC_OK(); + } + + // Ask redirected channel if verification can proceed. + // ContinueRedirect2Verify will be invoked when redirected channel is ready. + redirectedParent->ContinueVerification(this); + + return IPC_OK(); +} + +// from nsIParentRedirectingChannel +NS_IMETHODIMP +HttpChannelParent::ContinueVerification( + nsIAsyncVerifyRedirectReadyCallback* aCallback) { + LOG(("HttpChannelParent::ContinueVerification [this=%p callback=%p]\n", this, + aCallback)); + + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aCallback); + + // Continue the verification procedure if background channel is ready. + if (mBgParent) { + aCallback->ReadyToVerify(NS_OK); + return NS_OK; + } + + // ConnectChannel must be received before Redirect2Verify. + MOZ_ASSERT(!mPromise.IsEmpty()); + + // Otherwise, wait for the background channel. + nsCOMPtr callback = aCallback; + WaitForBgParent()->Then( + GetMainThreadSerialEventTarget(), __func__, + [callback]() { callback->ReadyToVerify(NS_OK); }, + [callback](const nsresult& aResult) { + NS_ERROR("failed to establish the background channel"); + callback->ReadyToVerify(aResult); + }); + return NS_OK; +} + +void HttpChannelParent::ContinueRedirect2Verify(const nsresult& aResult) { + LOG(("HttpChannelParent::ContinueRedirect2Verify [this=%p result=%" PRIx32 + "]\n", + this, static_cast(aResult))); + + if (!mRedirectCallback) { + // This should according the logic never happen, log the situation. + if (mReceivedRedirect2Verify) { + LOG(("RecvRedirect2Verify[%p]: Duplicate fire", this)); + } + if (mSentRedirect1BeginFailed) { + LOG(("RecvRedirect2Verify[%p]: Send to child failed", this)); + } + if ((mRedirectChannelId > 0) && NS_FAILED(aResult)) { + LOG(("RecvRedirect2Verify[%p]: Redirect failed", this)); + } + if ((mRedirectChannelId > 0) && NS_SUCCEEDED(aResult)) { + LOG(("RecvRedirect2Verify[%p]: Redirect succeeded", this)); + } + if (!mRedirectChannel) { + LOG(("RecvRedirect2Verify[%p]: Missing redirect channel", this)); + } + + NS_ERROR( + "Unexpcted call to HttpChannelParent::RecvRedirect2Verify, " + "mRedirectCallback null"); + } + + mReceivedRedirect2Verify = true; + + if (mRedirectCallback) { + LOG( + ("HttpChannelParent::ContinueRedirect2Verify call " + "OnRedirectVerifyCallback" + " [this=%p result=%" PRIx32 ", mRedirectCallback=%p]\n", + this, static_cast(aResult), mRedirectCallback.get())); + mRedirectCallback->OnRedirectVerifyCallback(aResult); + mRedirectCallback = nullptr; + } +} + +mozilla::ipc::IPCResult HttpChannelParent::RecvDocumentChannelCleanup( + const bool& clearCacheEntry) { + CleanupBackgroundChannel(); // Background channel can be closed. + mChannel = nullptr; // Reclaim some memory sooner. + if (clearCacheEntry) { + mCacheEntry = nullptr; // Else we'll block other channels reading same URI + } + return IPC_OK(); +} + +mozilla::ipc::IPCResult +HttpChannelParent::RecvMarkOfflineCacheEntryAsForeign() { + if (mOfflineForeignMarker) { + mOfflineForeignMarker->MarkAsForeign(); + mOfflineForeignMarker = nullptr; + } + + return IPC_OK(); +} + +mozilla::ipc::IPCResult HttpChannelParent::RecvRemoveCorsPreflightCacheEntry( + const URIParams& uri, + const mozilla::ipc::PrincipalInfo& requestingPrincipal, + const OriginAttributes& originAttributes) { + nsCOMPtr deserializedURI = DeserializeURI(uri); + if (!deserializedURI) { + return IPC_FAIL_NO_REASON(this); + } + auto principalOrErr = PrincipalInfoToPrincipal(requestingPrincipal); + if (NS_WARN_IF(principalOrErr.isErr())) { + return IPC_FAIL_NO_REASON(this); + } + nsCOMPtr principal = principalOrErr.unwrap(); + nsCORSListenerProxy::RemoveFromCorsPreflightCache(deserializedURI, principal, + originAttributes); + return IPC_OK(); +} + +//----------------------------------------------------------------------------- +// HttpChannelParent::nsIRequestObserver +//----------------------------------------------------------------------------- + +static ResourceTimingStructArgs GetTimingAttributes(HttpBaseChannel* aChannel) { + ResourceTimingStructArgs args; + TimeStamp timeStamp; + aChannel->GetDomainLookupStart(&timeStamp); + args.domainLookupStart() = timeStamp; + aChannel->GetDomainLookupEnd(&timeStamp); + args.domainLookupEnd() = timeStamp; + aChannel->GetConnectStart(&timeStamp); + args.connectStart() = timeStamp; + aChannel->GetTcpConnectEnd(&timeStamp); + args.tcpConnectEnd() = timeStamp; + aChannel->GetSecureConnectionStart(&timeStamp); + args.secureConnectionStart() = timeStamp; + aChannel->GetConnectEnd(&timeStamp); + args.connectEnd() = timeStamp; + aChannel->GetRequestStart(&timeStamp); + args.requestStart() = timeStamp; + aChannel->GetResponseStart(&timeStamp); + args.responseStart() = timeStamp; + aChannel->GetResponseEnd(&timeStamp); + args.responseEnd() = timeStamp; + aChannel->GetAsyncOpen(&timeStamp); + args.fetchStart() = timeStamp; + aChannel->GetRedirectStart(&timeStamp); + args.redirectStart() = timeStamp; + aChannel->GetRedirectEnd(&timeStamp); + args.redirectEnd() = timeStamp; + + uint64_t size = 0; + aChannel->GetTransferSize(&size); + args.transferSize() = size; + + aChannel->GetEncodedBodySize(&size); + args.encodedBodySize() = size; + // decodedBodySize can be computed in the child process so it doesn't need + // to be passed down. + + nsCString protocolVersion; + aChannel->GetProtocolVersion(protocolVersion); + args.protocolVersion() = protocolVersion; + + aChannel->GetCacheReadStart(&timeStamp); + args.cacheReadStart() = timeStamp; + + aChannel->GetCacheReadEnd(&timeStamp); + args.cacheReadEnd() = timeStamp; + return args; +} + +NS_IMETHODIMP +HttpChannelParent::OnStartRequest(nsIRequest* aRequest) { + nsresult rv; + + LOG(("HttpChannelParent::OnStartRequest [this=%p, aRequest=%p]\n", this, + aRequest)); + MOZ_ASSERT(NS_IsMainThread()); + + Maybe multiPartID; + bool isLastPartOfMultiPart = false; + DebugOnly isMultiPart = false; + + RefPtr chan = do_QueryObject(aRequest); + if (!chan) { + if (nsCOMPtr multiPartChannel = + do_QueryInterface(aRequest)) { + isMultiPart = true; + nsCOMPtr baseChannel; + multiPartChannel->GetBaseChannel(getter_AddRefs(baseChannel)); + chan = do_QueryObject(baseChannel); + + uint32_t partID = 0; + multiPartChannel->GetPartID(&partID); + multiPartID = Some(partID); + multiPartChannel->GetIsLastPart(&isLastPartOfMultiPart); + } else if (nsCOMPtr viewSourceChannel = + do_QueryInterface(aRequest)) { + chan = do_QueryObject(viewSourceChannel->GetInnerChannel()); + } + } + MOZ_ASSERT(multiPartID || !isMultiPart, "Changed multi-part state?"); + + if (!chan) { + LOG((" aRequest is not HttpBaseChannel")); + NS_ERROR( + "Expecting only HttpBaseChannel as aRequest in " + "HttpChannelParent::OnStartRequest"); + return NS_ERROR_UNEXPECTED; + } + + mAfterOnStartRequestBegun = true; + + // Todo: re-enable when bug 1589749 is fixed. + /*MOZ_ASSERT(mChannel == chan, + "HttpChannelParent getting OnStartRequest from a different " + "HttpBaseChannel instance");*/ + + HttpChannelOnStartRequestArgs args; + + // Send down any permissions/cookies which are relevant to this URL if we are + // performing a document load. We can't do that if mIPCClosed is set. + if (!mIPCClosed) { + PContentParent* pcp = Manager()->Manager(); + MOZ_ASSERT(pcp, "We should have a manager if our IPC isn't closed"); + DebugOnly rv = + static_cast(pcp)->AboutToLoadHttpFtpDocumentForChild( + chan, &args.shouldWaitForOnStartRequestSent()); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + + args.multiPartID() = multiPartID; + args.isLastPartOfMultiPart() = isLastPartOfMultiPart; + + args.cacheExpirationTime() = nsICacheEntry::NO_EXPIRATION_TIME; + + RefPtr httpChannelImpl = do_QueryObject(chan); + + if (httpChannelImpl) { + httpChannelImpl->IsFromCache(&args.isFromCache()); + httpChannelImpl->IsRacing(&args.isRacing()); + httpChannelImpl->GetCacheEntryId(&args.cacheEntryId()); + httpChannelImpl->GetCacheTokenFetchCount(&args.cacheFetchCount()); + httpChannelImpl->GetCacheTokenExpirationTime(&args.cacheExpirationTime()); + + mDataSentToChildProcess = httpChannelImpl->DataSentToChildProcess(); + + // If RCWN is enabled and cache wins, we can't use the ODA from socket + // process. + if (args.isRacing()) { + mDataSentToChildProcess = + httpChannelImpl->DataSentToChildProcess() && !args.isFromCache(); + } + args.dataFromSocketProcess() = mDataSentToChildProcess; + + bool loadedFromApplicationCache = false; + httpChannelImpl->GetLoadedFromApplicationCache(&loadedFromApplicationCache); + if (loadedFromApplicationCache) { + mOfflineForeignMarker.reset( + httpChannelImpl->GetOfflineCacheEntryAsForeignMarker()); + nsCOMPtr appCache; + httpChannelImpl->GetApplicationCache(getter_AddRefs(appCache)); + nsCString appCacheGroupId; + nsCString appCacheClientId; + appCache->GetGroupID(args.appCacheGroupId()); + appCache->GetClientID(args.appCacheClientId()); + } + } + + // Propagate whether or not conversion should occur from the parent-side + // channel to the child-side channel. Then disable the parent-side + // conversion so that it only occurs in the child. + Unused << chan->GetApplyConversion(&args.applyConversion()); + chan->SetApplyConversion(false); + + // If we've already applied the conversion (as can happen if we installed + // a multipart converted), then don't apply it again on the child. + if (chan->HasAppliedConversion()) { + args.applyConversion() = false; + } + + chan->GetStatus(&args.channelStatus()); + + // Keep the cache entry for future use when opening alternative streams. + // It could be already released by nsHttpChannel at that time. + nsCOMPtr cacheEntry; + + if (httpChannelImpl) { + httpChannelImpl->GetCacheToken(getter_AddRefs(cacheEntry)); + mCacheEntry = do_QueryInterface(cacheEntry); + args.cacheEntryAvailable() = mCacheEntry ? true : false; + + httpChannelImpl->GetCacheKey(&args.cacheKey()); + httpChannelImpl->GetAlternativeDataType(args.altDataType()); + } + + args.altDataLength() = chan->GetAltDataLength(); + args.deliveringAltData() = chan->IsDeliveringAltData(); + + UpdateAndSerializeSecurityInfo(args.securityInfoSerialization()); + + chan->GetRedirectCount(&args.redirectCount()); + + nsCOMPtr loadInfo = chan->LoadInfo(); + mozilla::ipc::LoadInfoToParentLoadInfoForwarder(loadInfo, + &args.loadInfoForwarder()); + + nsHttpResponseHead* responseHead = chan->GetResponseHead(); + bool useResponseHead = !!responseHead; + nsHttpResponseHead cleanedUpResponseHead; + + if (responseHead && + (responseHead->HasHeader(nsHttp::Set_Cookie) || multiPartID)) { + cleanedUpResponseHead = *responseHead; + cleanedUpResponseHead.ClearHeader(nsHttp::Set_Cookie); + if (multiPartID) { + nsCOMPtr multiPartChannel = do_QueryInterface(aRequest); + // For the multipart channel, use the parsed subtype instead. Note that + // `chan` is the underlying base channel of the multipart channel in this + // case, which is different from `multiPartChannel`. + MOZ_ASSERT(multiPartChannel); + nsAutoCString contentType; + multiPartChannel->GetContentType(contentType); + cleanedUpResponseHead.SetContentType(contentType); + } + responseHead = &cleanedUpResponseHead; + } + + if (!responseHead) { + responseHead = &cleanedUpResponseHead; + } + + chan->GetIsResolvedByTRR(&args.isResolvedByTRR()); + chan->GetAllRedirectsSameOrigin(&args.allRedirectsSameOrigin()); + chan->GetCrossOriginOpenerPolicy(&args.openerPolicy()); + args.selfAddr() = chan->GetSelfAddr(); + args.peerAddr() = chan->GetPeerAddr(); + args.timing() = GetTimingAttributes(mChannel); + if (mOverrideReferrerInfo) { + args.overrideReferrerInfo() = ToRefPtr(std::move(mOverrideReferrerInfo)); + } + if (!mCookie.IsEmpty()) { + args.cookie() = std::move(mCookie); + } + + nsHttpRequestHead* requestHead = chan->GetRequestHead(); + // !!! We need to lock headers and please don't forget to unlock them !!! + requestHead->Enter(); + + nsHttpHeaderArray cleanedUpRequestHeaders; + bool cleanedUpRequest = false; + if (requestHead->HasHeader(nsHttp::Cookie)) { + cleanedUpRequestHeaders = requestHead->Headers(); + cleanedUpRequestHeaders.ClearHeader(nsHttp::Cookie); + cleanedUpRequest = true; + } + + rv = NS_OK; + + if (mIPCClosed || + !mBgParent->OnStartRequest( + *responseHead, useResponseHead, + cleanedUpRequest ? cleanedUpRequestHeaders : requestHead->Headers(), + args)) { + rv = NS_ERROR_UNEXPECTED; + } + requestHead->Exit(); + + // Need to wait for the cookies/permissions to content process, which is sent + // via PContent in AboutToLoadHttpFtpDocumentForChild. For multipart channel, + // send only one time since the cookies/permissions are the same. + if (NS_SUCCEEDED(rv) && args.shouldWaitForOnStartRequestSent() && + multiPartID.valueOr(0) == 0) { + LOG(("HttpChannelParent::SendOnStartRequestSent\n")); + Unused << SendOnStartRequestSent(); + } + + return rv; +} + +NS_IMETHODIMP +HttpChannelParent::OnStopRequest(nsIRequest* aRequest, nsresult aStatusCode) { + LOG(("HttpChannelParent::OnStopRequest: [this=%p aRequest=%p status=%" PRIx32 + "]\n", + this, aRequest, static_cast(aStatusCode))); + MOZ_ASSERT(NS_IsMainThread()); + + RefPtr httpChannelImpl = do_QueryObject(mChannel); + if (httpChannelImpl) { + httpChannelImpl->SetWarningReporter(nullptr); + } + + nsHttpHeaderArray* responseTrailer = mChannel->GetResponseTrailers(); + + nsTArray consoleReports; + + RefPtr httpChannel = do_QueryObject(mChannel); + if (httpChannel) { + httpChannel->StealConsoleReports(consoleReports); + } + + // Either IPC channel is closed or background channel + // is ready to send OnStopRequest. + MOZ_ASSERT(mIPCClosed || mBgParent); + + if (mDataSentToChildProcess) { + if (mIPCClosed || !mBgParent || + !mBgParent->OnConsoleReport(consoleReports)) { + return NS_ERROR_UNEXPECTED; + } + return NS_OK; + } + + // If we're handling a multi-part stream, then send this directly + // over PHttpChannel to make synchronization easier. + if (mIPCClosed || !mBgParent || + !mBgParent->OnStopRequest( + aStatusCode, GetTimingAttributes(mChannel), + responseTrailer ? *responseTrailer : nsHttpHeaderArray(), + consoleReports)) { + return NS_ERROR_UNEXPECTED; + } + + if (NeedFlowControl()) { + bool isLocal = false; + NetAddr peerAddr = mChannel->GetPeerAddr(); + +#if defined(XP_UNIX) + // Unix-domain sockets are always local. + isLocal = (peerAddr.raw.family == PR_AF_LOCAL); +#endif + + isLocal = isLocal || peerAddr.IsLoopbackAddr(); + + if (!isLocal) { + if (!mHasSuspendedByBackPressure) { + AccumulateCategorical( + Telemetry::LABELS_NETWORK_BACK_PRESSURE_SUSPENSION_RATE_V2:: + NotSuspended); + } else { + AccumulateCategorical( + Telemetry::LABELS_NETWORK_BACK_PRESSURE_SUSPENSION_RATE_V2:: + Suspended); + + // Only analyze non-local suspended cases, which we are interested in. + nsCOMPtr loadInfo = mChannel->LoadInfo(); + Telemetry::Accumulate( + Telemetry::NETWORK_BACK_PRESSURE_SUSPENSION_CP_TYPE, + loadInfo->InternalContentPolicyType()); + } + } else { + if (!mHasSuspendedByBackPressure) { + AccumulateCategorical( + Telemetry::LABELS_NETWORK_BACK_PRESSURE_SUSPENSION_RATE_V2:: + NotSuspendedLocal); + } else { + AccumulateCategorical( + Telemetry::LABELS_NETWORK_BACK_PRESSURE_SUSPENSION_RATE_V2:: + SuspendedLocal); + } + } + } + return NS_OK; +} + +//----------------------------------------------------------------------------- +// HttpChannelParent::nsIMultiPartChannelListener +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +HttpChannelParent::OnAfterLastPart(nsresult aStatus) { + LOG(("HttpChannelParent::OnAfterLastPart [this=%p]\n", this)); + MOZ_ASSERT(NS_IsMainThread()); + + // If IPC channel is closed, there is nothing we can do. Just return NS_OK. + if (mIPCClosed) { + return NS_OK; + } + + // If IPC channel is open, background channel should be ready to send + // OnAfterLastPart. + MOZ_ASSERT(mBgParent); + + if (!mBgParent || !mBgParent->OnAfterLastPart(aStatus)) { + return NS_ERROR_UNEXPECTED; + } + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// HttpChannelParent::nsIStreamListener +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +HttpChannelParent::OnDataAvailable(nsIRequest* aRequest, + nsIInputStream* aInputStream, + uint64_t aOffset, uint32_t aCount) { + LOG(("HttpChannelParent::OnDataAvailable [this=%p aRequest=%p offset=%" PRIu64 + " count=%" PRIu32 "]\n", + this, aRequest, aOffset, aCount)); + MOZ_ASSERT(NS_IsMainThread()); + + if (mDataSentToChildProcess) { + uint32_t n; + return aInputStream->ReadSegments(NS_DiscardSegment, nullptr, aCount, &n); + } + + nsresult channelStatus = NS_OK; + mChannel->GetStatus(&channelStatus); + + nsresult transportStatus = NS_NET_STATUS_RECEIVING_FROM; + RefPtr httpChannelImpl = do_QueryObject(mChannel); + if (httpChannelImpl) { + if (httpChannelImpl->IsReadingFromCache()) { + transportStatus = NS_NET_STATUS_READING; + } + } + + nsCString data; + nsresult rv = NS_ReadInputStreamToString(aInputStream, data, aCount); + if (NS_FAILED(rv)) { + return rv; + } + + // Either IPC channel is closed or background channel + // is ready to send OnTransportAndData. + MOZ_ASSERT(mIPCClosed || mBgParent); + + if (mIPCClosed || !mBgParent || + !mBgParent->OnTransportAndData(channelStatus, transportStatus, aOffset, + aCount, data)) { + return NS_ERROR_UNEXPECTED; + } + + int32_t count = static_cast(aCount); + + if (NeedFlowControl()) { + // We're going to run out of sending window size + if (mSendWindowSize > 0 && mSendWindowSize <= count) { + MOZ_ASSERT(!mSuspendedForFlowControl); + LOG((" suspend the channel due to e10s backpressure")); + Unused << mChannel->Suspend(); + mSuspendedForFlowControl = true; + mHasSuspendedByBackPressure = true; + } else if (!mResumedTimestamp.IsNull()) { + // Calculate the delay when the first packet arrived after resume + Telemetry::AccumulateTimeDelta( + Telemetry::NETWORK_BACK_PRESSURE_SUSPENSION_DELAY_TIME_MS, + mResumedTimestamp); + mResumedTimestamp = TimeStamp(); + } + mSendWindowSize -= count; + } + + return NS_OK; +} + +bool HttpChannelParent::NeedFlowControl() { + if (mCacheNeedFlowControlInitialized) { + return mNeedFlowControl; + } + + int64_t contentLength = -1; + + RefPtr httpChannelImpl = do_QueryObject(mChannel); + + // By design, we won't trigger the flow control if + // a. pref-out + // b. the resource is from cache or partial cache + // c. the resource is small + // d. data will be sent from socket process to child process directly + // Note that we served the cached resource first for partical cache, which is + // ignored here since we only take the first ODA into consideration. + if (gHttpHandler->SendWindowSize() == 0 || !httpChannelImpl || + httpChannelImpl->IsReadingFromCache() || + NS_FAILED(httpChannelImpl->GetContentLength(&contentLength)) || + contentLength < gHttpHandler->SendWindowSize() || + mDataSentToChildProcess) { + mNeedFlowControl = false; + } + mCacheNeedFlowControlInitialized = true; + return mNeedFlowControl; +} + +mozilla::ipc::IPCResult HttpChannelParent::RecvBytesRead( + const int32_t& aCount) { + if (!NeedFlowControl()) { + return IPC_OK(); + } + + LOG(("HttpChannelParent::RecvBytesRead [this=%p count=%" PRId32 "]\n", this, + aCount)); + + if (mSendWindowSize <= 0 && mSendWindowSize + aCount > 0) { + MOZ_ASSERT(mSuspendedForFlowControl); + LOG((" resume the channel due to e10s backpressure relief")); + Unused << mChannel->Resume(); + mSuspendedForFlowControl = false; + + mResumedTimestamp = TimeStamp::Now(); + } + mSendWindowSize += aCount; + return IPC_OK(); +} + +mozilla::ipc::IPCResult HttpChannelParent::RecvOpenOriginalCacheInputStream() { + if (mIPCClosed) { + return IPC_OK(); + } + AutoIPCStream autoStream; + if (mCacheEntry) { + nsCOMPtr inputStream; + nsresult rv = mCacheEntry->OpenInputStream(0, getter_AddRefs(inputStream)); + if (NS_SUCCEEDED(rv)) { + PContentParent* pcp = Manager()->Manager(); + Unused << autoStream.Serialize(inputStream, + static_cast(pcp)); + } + } + + Unused << SendOriginalCacheInputStreamAvailable( + autoStream.TakeOptionalValue()); + return IPC_OK(); +} + +mozilla::ipc::IPCResult HttpChannelParent::RecvOpenAltDataCacheInputStream( + const nsCString& aType) { + if (mIPCClosed) { + return IPC_OK(); + } + AutoIPCStream autoStream; + if (mCacheEntry) { + nsCOMPtr inputStream; + nsresult rv = mCacheEntry->OpenAlternativeInputStream( + aType, getter_AddRefs(inputStream)); + if (NS_SUCCEEDED(rv)) { + PContentParent* pcp = Manager()->Manager(); + Unused << autoStream.Serialize(inputStream, + static_cast(pcp)); + } + } + + Unused << SendAltDataCacheInputStreamAvailable( + autoStream.TakeOptionalValue()); + return IPC_OK(); +} + +//----------------------------------------------------------------------------- +// HttpChannelParent::nsIProgressEventSink +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +HttpChannelParent::OnProgress(nsIRequest* aRequest, int64_t aProgress, + int64_t aProgressMax) { + LOG(("HttpChannelParent::OnProgress [this=%p progress=%" PRId64 "max=%" PRId64 + "]\n", + this, aProgress, aProgressMax)); + MOZ_ASSERT(NS_IsMainThread()); + + // If IPC channel is closed, there is nothing we can do. Just return NS_OK. + if (mIPCClosed) { + return NS_OK; + } + + // If it indicates this precedes OnDataAvailable, child can derive the value + // in ODA. + if (mIgnoreProgress) { + mIgnoreProgress = false; + return NS_OK; + } + + // If IPC channel is open, background channel should be ready to send + // OnProgress. + MOZ_ASSERT(mBgParent); + + // Send OnProgress events to the child for data upload progress notifications + // (i.e. status == NS_NET_STATUS_SENDING_TO) or if the channel has + // LOAD_BACKGROUND set. + if (!mBgParent || !mBgParent->OnProgress(aProgress, aProgressMax)) { + return NS_ERROR_UNEXPECTED; + } + + return NS_OK; +} + +NS_IMETHODIMP +HttpChannelParent::OnStatus(nsIRequest* aRequest, nsresult aStatus, + const char16_t* aStatusArg) { + LOG(("HttpChannelParent::OnStatus [this=%p status=%" PRIx32 "]\n", this, + static_cast(aStatus))); + MOZ_ASSERT(NS_IsMainThread()); + + // If IPC channel is closed, there is nothing we can do. Just return NS_OK. + if (mIPCClosed) { + return NS_OK; + } + + // If this precedes OnDataAvailable, transportStatus will be derived in ODA. + if (aStatus == NS_NET_STATUS_RECEIVING_FROM || + aStatus == NS_NET_STATUS_READING) { + // The transport status and progress generated by ODA will be coalesced + // into one IPC message. Therefore, we can ignore the next OnProgress event + // since it is generated by ODA as well. + mIgnoreProgress = true; + return NS_OK; + } + + // If IPC channel is open, background channel should be ready to send + // OnStatus. + MOZ_ASSERT(mIPCClosed || mBgParent); + + // Otherwise, send to child now + if (!mBgParent || !mBgParent->OnStatus(aStatus)) { + return NS_ERROR_UNEXPECTED; + } + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// HttpChannelParent::nsIParentChannel +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +HttpChannelParent::SetParentListener(ParentChannelListener* aListener) { + LOG(("HttpChannelParent::SetParentListener [this=%p aListener=%p]\n", this, + aListener)); + MOZ_ASSERT(aListener); + MOZ_ASSERT(!mParentListener, + "SetParentListener should only be called for " + "new HttpChannelParents after a redirect, when " + "mParentListener is null."); + mParentListener = aListener; + return NS_OK; +} + +NS_IMETHODIMP +HttpChannelParent::SetClassifierMatchedInfo(const nsACString& aList, + const nsACString& aProvider, + const nsACString& aFullHash) { + LOG(("HttpChannelParent::SetClassifierMatchedInfo [this=%p]\n", this)); + if (!mIPCClosed) { + MOZ_ASSERT(mBgParent); + Unused << mBgParent->OnSetClassifierMatchedInfo(aList, aProvider, + aFullHash); + } + return NS_OK; +} + +NS_IMETHODIMP +HttpChannelParent::SetClassifierMatchedTrackingInfo( + const nsACString& aLists, const nsACString& aFullHashes) { + LOG(("HttpChannelParent::SetClassifierMatchedTrackingInfo [this=%p]\n", + this)); + if (!mIPCClosed) { + MOZ_ASSERT(mBgParent); + Unused << mBgParent->OnSetClassifierMatchedTrackingInfo(aLists, + aFullHashes); + } + return NS_OK; +} + +NS_IMETHODIMP +HttpChannelParent::NotifyClassificationFlags(uint32_t aClassificationFlags, + bool aIsThirdParty) { + LOG( + ("HttpChannelParent::NotifyClassificationFlags " + "classificationFlags=%" PRIu32 ", thirdparty=%d [this=%p]\n", + aClassificationFlags, static_cast(aIsThirdParty), this)); + if (!mIPCClosed) { + MOZ_ASSERT(mBgParent); + Unused << mBgParent->OnNotifyClassificationFlags(aClassificationFlags, + aIsThirdParty); + } + return NS_OK; +} + +NS_IMETHODIMP +HttpChannelParent::NotifyFlashPluginStateChanged( + nsIHttpChannel::FlashPluginState aState) { + LOG(("HttpChannelParent::NotifyFlashPluginStateChanged [this=%p]\n", this)); + if (!mIPCClosed) { + MOZ_ASSERT(mBgParent); + Unused << mBgParent->OnNotifyFlashPluginStateChanged(aState); + } + return NS_OK; +} + +NS_IMETHODIMP +HttpChannelParent::Delete() { + if (!mIPCClosed) Unused << DoSendDeleteSelf(); + + return NS_OK; +} + +NS_IMETHODIMP +HttpChannelParent::GetRemoteType(nsACString& aRemoteType) { + if (!CanSend()) { + return NS_ERROR_UNEXPECTED; + } + + dom::PContentParent* pcp = Manager()->Manager(); + aRemoteType = static_cast(pcp)->GetRemoteType(); + return NS_OK; +} + +//----------------------------------------------------------------------------- +// HttpChannelParent::nsIParentRedirectingChannel +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +HttpChannelParent::StartRedirect(nsIChannel* newChannel, uint32_t redirectFlags, + nsIAsyncVerifyRedirectCallback* callback) { + nsresult rv; + + LOG(("HttpChannelParent::StartRedirect [this=%p, newChannel=%p callback=%p]", + this, newChannel, callback)); + + // Register the new channel and obtain id for it + nsCOMPtr registrar = + RedirectChannelRegistrar::GetOrCreate(); + MOZ_ASSERT(registrar); + + mRedirectChannelId = nsContentUtils::GenerateLoadIdentifier(); + rv = registrar->RegisterChannel(newChannel, mRedirectChannelId); + NS_ENSURE_SUCCESS(rv, rv); + + LOG(("Registered %p channel under id=%" PRIx64, newChannel, + mRedirectChannelId)); + + if (mIPCClosed) { + return NS_BINDING_ABORTED; + } + + // If this is an internal redirect for service worker interception, then + // hide it from the child process. The original e10s interception code + // was not designed with this in mind and its not necessary to replace + // the HttpChannelChild/Parent objects in this case. + if (redirectFlags & nsIChannelEventSink::REDIRECT_INTERNAL) { + nsCOMPtr oldIntercepted = + do_QueryInterface(static_cast(mChannel.get())); + nsCOMPtr newIntercepted = + do_QueryInterface(newChannel); + + // We only want to hide the special internal redirect from nsHttpChannel + // to InterceptedHttpChannel. We want to allow through internal redirects + // initiated from the InterceptedHttpChannel even if they are to another + // InterceptedHttpChannel. + if (!oldIntercepted && newIntercepted) { + // We need to move across the reserved and initial client information + // to the new channel. Normally this would be handled by the child + // ClientChannelHelper, but that is not notified of this redirect since + // we're not propagating it back to the child process. + nsCOMPtr oldLoadInfo = mChannel->LoadInfo(); + + nsCOMPtr newLoadInfo = newChannel->LoadInfo(); + + Maybe reservedClientInfo( + oldLoadInfo->GetReservedClientInfo()); + if (reservedClientInfo.isSome()) { + newLoadInfo->SetReservedClientInfo(reservedClientInfo.ref()); + } + + Maybe initialClientInfo(oldLoadInfo->GetInitialClientInfo()); + if (initialClientInfo.isSome()) { + newLoadInfo->SetInitialClientInfo(initialClientInfo.ref()); + } + + // Re-link the HttpChannelParent to the new InterceptedHttpChannel. + nsCOMPtr linkedChannel; + rv = NS_LinkRedirectChannels(mRedirectChannelId, this, + getter_AddRefs(linkedChannel)); + NS_ENSURE_SUCCESS(rv, rv); + MOZ_ASSERT(linkedChannel == newChannel); + + // We immediately store the InterceptedHttpChannel as our nested + // mChannel. None of the redirect IPC messaging takes place. + mChannel = do_QueryObject(newChannel); + + callback->OnRedirectVerifyCallback(NS_OK); + return NS_OK; + } + } + + // Sending down the original URI, because that is the URI we have + // to construct the channel from - this is the URI we've been actually + // redirected to. URI of the channel may be an inner channel URI. + // URI of the channel will be reconstructed by the protocol handler + // on the child process, no need to send it then. + nsCOMPtr newOriginalURI; + newChannel->GetOriginalURI(getter_AddRefs(newOriginalURI)); + + URIParams uriParams; + SerializeURI(newOriginalURI, uriParams); + + uint32_t newLoadFlags = nsIRequest::LOAD_NORMAL; + MOZ_ALWAYS_SUCCEEDS(newChannel->GetLoadFlags(&newLoadFlags)); + + nsCString secInfoSerialization; + UpdateAndSerializeSecurityInfo(secInfoSerialization); + + // If the channel is a HTTP channel, we also want to inform the child + // about the parent's channelId attribute, so that both parent and child + // share the same ID. Useful for monitoring channel activity in devtools. + uint64_t channelId = 0; + nsCOMPtr httpChannel = do_QueryInterface(newChannel); + if (httpChannel) { + rv = httpChannel->GetChannelId(&channelId); + NS_ENSURE_SUCCESS(rv, NS_BINDING_ABORTED); + } + + nsCOMPtr loadInfo = mChannel->LoadInfo(); + + ParentLoadInfoForwarderArgs loadInfoForwarderArg; + mozilla::ipc::LoadInfoToParentLoadInfoForwarder(loadInfo, + &loadInfoForwarderArg); + + nsHttpResponseHead* responseHead = mChannel->GetResponseHead(); + + nsHttpResponseHead cleanedUpResponseHead; + if (responseHead && responseHead->HasHeader(nsHttp::Set_Cookie)) { + cleanedUpResponseHead = *responseHead; + cleanedUpResponseHead.ClearHeader(nsHttp::Set_Cookie); + responseHead = &cleanedUpResponseHead; + } + + if (!responseHead) { + responseHead = &cleanedUpResponseHead; + } + + bool result = false; + if (!mIPCClosed) { + result = SendRedirect1Begin( + mRedirectChannelId, uriParams, newLoadFlags, redirectFlags, + loadInfoForwarderArg, *responseHead, secInfoSerialization, channelId, + mChannel->GetPeerAddr(), GetTimingAttributes(mChannel)); + } + if (!result) { + // Bug 621446 investigation + mSentRedirect1BeginFailed = true; + return NS_BINDING_ABORTED; + } + + // Result is handled in RecvRedirect2Verify above + + mRedirectChannel = newChannel; + mRedirectCallback = callback; + return NS_OK; +} + +NS_IMETHODIMP +HttpChannelParent::CompleteRedirect(bool succeeded) { + LOG(("HttpChannelParent::CompleteRedirect [this=%p succeeded=%d]\n", this, + succeeded)); + + // If this was an internal redirect for a service worker interception then + // we will not have a redirecting channel here. Hide this redirect from + // the child. + if (!mRedirectChannel) { + return NS_OK; + } + + if (succeeded && !mIPCClosed) { + // TODO: check return value: assume child dead if failed + Unused << SendRedirect3Complete(); + } + + mRedirectChannel = nullptr; + return NS_OK; +} + +nsresult HttpChannelParent::OpenAlternativeOutputStream( + const nsACString& type, int64_t predictedSize, + nsIAsyncOutputStream** _retval) { + // We need to make sure the child does not call SendDocumentChannelCleanup() + // before opening the altOutputStream, because that clears mCacheEntry. + if (!mCacheEntry) { + return NS_ERROR_NOT_AVAILABLE; + } + nsresult rv = + mCacheEntry->OpenAlternativeOutputStream(type, predictedSize, _retval); + if (NS_SUCCEEDED(rv)) { + mCacheEntry->SetMetaDataElement("alt-data-from-child", "1"); + } + return rv; +} + +NS_IMETHODIMP +HttpChannelParent::GetAuthPrompt(uint32_t aPromptReason, const nsIID& iid, + void** aResult) { + nsCOMPtr prompt = + new NeckoParent::NestedFrameAuthPrompt(Manager(), TabId(0)); + prompt.forget(aResult); + return NS_OK; +} + +void HttpChannelParent::UpdateAndSerializeSecurityInfo( + nsACString& aSerializedSecurityInfoOut) { + nsCOMPtr secInfoSupp; + mChannel->GetSecurityInfo(getter_AddRefs(secInfoSupp)); + if (secInfoSupp) { + nsCOMPtr secInfoSer = do_QueryInterface(secInfoSupp); + if (secInfoSer) { + NS_SerializeToString(secInfoSer, aSerializedSecurityInfoOut); + } + } +} + +bool HttpChannelParent::DoSendDeleteSelf() { + mIPCClosed = true; + bool rv = SendDeleteSelf(); + + CleanupBackgroundChannel(); + + return rv; +} + +mozilla::ipc::IPCResult HttpChannelParent::RecvDeletingChannel() { + // We need to ensure that the parent channel will not be sending any more IPC + // messages after this, as the child is going away. DoSendDeleteSelf will + // set mIPCClosed = true; + if (!DoSendDeleteSelf()) { + return IPC_FAIL_NO_REASON(this); + } + return IPC_OK(); +} + +//----------------------------------------------------------------------------- +// HttpChannelSecurityWarningReporter +//----------------------------------------------------------------------------- + +nsresult HttpChannelParent::ReportSecurityMessage( + const nsAString& aMessageTag, const nsAString& aMessageCategory) { + if (mIPCClosed || NS_WARN_IF(!SendReportSecurityMessage( + nsString(aMessageTag), nsString(aMessageCategory)))) { + return NS_ERROR_UNEXPECTED; + } + return NS_OK; +} + +NS_IMETHODIMP +HttpChannelParent::IssueWarning(uint32_t aWarning, bool aAsError) { + Unused << SendIssueDeprecationWarning(aWarning, aAsError); + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsIAsyncVerifyRedirectReadyCallback +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +HttpChannelParent::ReadyToVerify(nsresult aResult) { + LOG(("HttpChannelParent::ReadyToVerify [this=%p result=%" PRIx32 "]\n", this, + static_cast(aResult))); + MOZ_ASSERT(NS_IsMainThread()); + + ContinueRedirect2Verify(aResult); + + return NS_OK; +} + +void HttpChannelParent::DoSendSetPriority(int16_t aValue) { + if (!mIPCClosed) { + Unused << SendSetPriority(aValue); + } +} + +nsresult HttpChannelParent::LogBlockedCORSRequest(const nsAString& aMessage, + const nsACString& aCategory) { + if (mIPCClosed || NS_WARN_IF(!SendLogBlockedCORSRequest( + nsString(aMessage), nsCString(aCategory)))) { + return NS_ERROR_UNEXPECTED; + } + return NS_OK; +} + +nsresult HttpChannelParent::LogMimeTypeMismatch(const nsACString& aMessageName, + bool aWarning, + const nsAString& aURL, + const nsAString& aContentType) { + if (mIPCClosed || NS_WARN_IF(!SendLogMimeTypeMismatch( + nsCString(aMessageName), aWarning, nsString(aURL), + nsString(aContentType)))) { + return NS_ERROR_UNEXPECTED; + } + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsIChannelEventSink +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +HttpChannelParent::AsyncOnChannelRedirect( + nsIChannel* aOldChannel, nsIChannel* aNewChannel, uint32_t aRedirectFlags, + nsIAsyncVerifyRedirectCallback* aCallback) { + LOG( + ("HttpChannelParent::AsyncOnChannelRedirect [this=%p, old=%p, " + "new=%p, flags=%u]", + this, aOldChannel, aNewChannel, aRedirectFlags)); + + return StartRedirect(aNewChannel, aRedirectFlags, aCallback); +} + +//----------------------------------------------------------------------------- +// nsIRedirectResultListener +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +HttpChannelParent::OnRedirectResult(bool succeeded) { + LOG(("HttpChannelParent::OnRedirectResult [this=%p, suc=%d]", this, + succeeded)); + + nsresult rv; + + nsCOMPtr redirectChannel; + if (mRedirectChannelId) { + nsCOMPtr registrar = + RedirectChannelRegistrar::GetOrCreate(); + MOZ_ASSERT(registrar); + + rv = registrar->GetParentChannel(mRedirectChannelId, + getter_AddRefs(redirectChannel)); + if (NS_FAILED(rv) || !redirectChannel) { + // Redirect might get canceled before we got AsyncOnChannelRedirect + LOG(("Registered parent channel not found under id=%" PRIx64, + mRedirectChannelId)); + + nsCOMPtr newChannel; + rv = registrar->GetRegisteredChannel(mRedirectChannelId, + getter_AddRefs(newChannel)); + MOZ_ASSERT(newChannel, "Already registered channel not found"); + + if (NS_SUCCEEDED(rv)) { + newChannel->Cancel(NS_BINDING_ABORTED); + } + } + + // Release all previously registered channels, they are no longer need to be + // kept in the registrar from this moment. + registrar->DeregisterChannels(mRedirectChannelId); + + mRedirectChannelId = 0; + } + + if (!redirectChannel) { + succeeded = false; + } + + CompleteRedirect(succeeded); + + if (succeeded) { + if (!SameCOMIdentity(redirectChannel, + static_cast(this))) { + Delete(); + mParentListener->SetListenerAfterRedirect(redirectChannel); + redirectChannel->SetParentListener(mParentListener); + } + } else if (redirectChannel) { + // Delete the redirect target channel: continue using old channel + redirectChannel->Delete(); + } + + return NS_OK; +} + +void HttpChannelParent::OverrideReferrerInfoDuringBeginConnect( + nsIReferrerInfo* aReferrerInfo) { + MOZ_ASSERT(aReferrerInfo); + MOZ_ASSERT(!mAfterOnStartRequestBegun); + + mOverrideReferrerInfo = aReferrerInfo; +} + +auto HttpChannelParent::AttachStreamFilter( + Endpoint&& aParentEndpoint, + Endpoint&& aChildEndpoint) + -> RefPtr { + LOG(("HttpChannelParent::AttachStreamFilter [this=%p]", this)); + MOZ_ASSERT(!mAfterOnStartRequestBegun); + + if (mIPCClosed) { + return ChildEndpointPromise::CreateAndReject(false, __func__); + } + + // If IPC channel is open, background channel should be ready to send + // SendAttachStreamFilter. + MOZ_ASSERT(mBgParent); + return InvokeAsync(mBgParent->GetBackgroundTarget(), mBgParent.get(), + __func__, &HttpBackgroundChannelParent::AttachStreamFilter, + std::move(aParentEndpoint), std::move(aChildEndpoint)); +} + +void HttpChannelParent::SetCookie(nsCString&& aCookie) { + LOG(("HttpChannelParent::SetCookie [this=%p]", this)); + MOZ_ASSERT(!mAfterOnStartRequestBegun); + MOZ_ASSERT(mCookie.IsEmpty()); + mCookie = std::move(aCookie); +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/HttpChannelParent.h b/netwerk/protocol/http/HttpChannelParent.h new file mode 100644 index 0000000000..7dd16f39c5 --- /dev/null +++ b/netwerk/protocol/http/HttpChannelParent.h @@ -0,0 +1,327 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et 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/. */ + +#ifndef mozilla_net_HttpChannelParent_h +#define mozilla_net_HttpChannelParent_h + +#include "nsHttp.h" +#include "mozilla/net/PHttpChannelParent.h" +#include "mozilla/net/NeckoCommon.h" +#include "mozilla/net/NeckoParent.h" +#include "mozilla/MozPromise.h" +#include "nsIParentRedirectingChannel.h" +#include "nsIProgressEventSink.h" +#include "nsIChannelEventSink.h" +#include "nsIRedirectResultListener.h" +#include "nsHttpChannel.h" +#include "nsIAuthPromptProvider.h" +#include "mozilla/dom/ipc/IdType.h" +#include "nsIDeprecationWarner.h" +#include "nsIMultiPartChannel.h" + +class nsICacheEntry; + +#define HTTP_CHANNEL_PARENT_IID \ + { \ + 0x982b2372, 0x7aa5, 0x4e8a, { \ + 0xbd, 0x9f, 0x89, 0x74, 0xd7, 0xf0, 0x58, 0xeb \ + } \ + } + +namespace mozilla { + +namespace dom { +class BrowserParent; +} // namespace dom + +namespace net { + +class HttpBackgroundChannelParent; +class ParentChannelListener; +class ChannelEventQueue; + +// Note: nsIInterfaceRequestor must be the first base so that do_QueryObject() +// works correctly on this object, as it's needed to compute a void* pointing to +// the beginning of this object. + +class HttpChannelParent final : public nsIInterfaceRequestor, + public PHttpChannelParent, + public nsIParentRedirectingChannel, + public nsIProgressEventSink, + public nsIAuthPromptProvider, + public nsIDeprecationWarner, + public HttpChannelSecurityWarningReporter, + public nsIAsyncVerifyRedirectReadyCallback, + public nsIChannelEventSink, + public nsIRedirectResultListener, + public nsIMultiPartChannelListener { + virtual ~HttpChannelParent(); + + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMLISTENER + NS_DECL_NSIPARENTCHANNEL + NS_DECL_NSIPARENTREDIRECTINGCHANNEL + NS_DECL_NSIPROGRESSEVENTSINK + NS_DECL_NSIINTERFACEREQUESTOR + NS_DECL_NSIAUTHPROMPTPROVIDER + NS_DECL_NSIDEPRECATIONWARNER + NS_DECL_NSIASYNCVERIFYREDIRECTREADYCALLBACK + NS_DECL_NSICHANNELEVENTSINK + NS_DECL_NSIREDIRECTRESULTLISTENER + NS_DECL_NSIMULTIPARTCHANNELLISTENER + + NS_DECLARE_STATIC_IID_ACCESSOR(HTTP_CHANNEL_PARENT_IID) + + HttpChannelParent(dom::BrowserParent* iframeEmbedding, + nsILoadContext* aLoadContext, PBOverrideStatus aStatus); + + [[nodiscard]] bool Init(const HttpChannelCreationArgs& aOpenArgs); + + // Forwarded to nsHttpChannel::SetApplyConversion. + void SetApplyConversion(bool aApplyConversion) { + if (mChannel) { + mChannel->SetApplyConversion(aApplyConversion); + } + } + + [[nodiscard]] nsresult OpenAlternativeOutputStream( + const nsACString& type, int64_t predictedSize, + nsIAsyncOutputStream** _retval); + + // Callbacks for each asynchronous tasks required in AsyncOpen + // procedure, will call InvokeAsyncOpen when all the expected + // tasks is finished successfully or when any failure happened. + // @see mAsyncOpenBarrier. + void TryInvokeAsyncOpen(nsresult aRv); + + void InvokeAsyncOpen(nsresult rv); + + // Calls SendSetPriority if mIPCClosed is false. + void DoSendSetPriority(int16_t aValue); + + // Callback while background channel is ready. + void OnBackgroundParentReady(HttpBackgroundChannelParent* aBgParent); + // Callback while background channel is destroyed. + void OnBackgroundParentDestroyed(); + + base::ProcessId OtherPid() const; + + // Inform the child actor that our referrer info was modified late during + // BeginConnect. + void OverrideReferrerInfoDuringBeginConnect(nsIReferrerInfo* aReferrerInfo); + + // Set the cookie string, which will be informed to the child actor during + // PHttpBackgroundChannel::OnStartRequest. Note that CookieService also sends + // the information to all actors via PContent, a main thread IPC, which could + // be slower than background IPC PHttpBackgroundChannel::OnStartRequest. + // Therefore, another cookie notification via PBackground is needed to + // guarantee the listener in child has the necessary cookies before + // OnStartRequest. + void SetCookie(nsCString&& aCookie); + + using ChildEndpointPromise = + MozPromise, bool, true>; + [[nodiscard]] RefPtr AttachStreamFilter( + Endpoint&& aParentEndpoint, + Endpoint&& aChildEndpoint); + + protected: + // used to connect redirected-to channel in parent with just created + // ChildChannel. Used during redirects. + [[nodiscard]] bool ConnectChannel(const uint32_t& channelId); + + [[nodiscard]] bool DoAsyncOpen( + const URIParams& uri, const Maybe& originalUri, + const Maybe& docUri, nsIReferrerInfo* aReferrerInfo, + const Maybe& internalRedirectUri, + const Maybe& topWindowUri, const uint32_t& loadFlags, + const RequestHeaderTuples& requestHeaders, const nsCString& requestMethod, + const Maybe& uploadStream, const bool& uploadStreamHasHeaders, + const int16_t& priority, const uint32_t& classOfService, + const uint8_t& redirectionLimit, const bool& allowSTS, + const uint32_t& thirdPartyFlags, const bool& doResumeAt, + const uint64_t& startPos, const nsCString& entityID, + const bool& chooseApplicationCache, const nsCString& appCacheClientID, + const bool& allowSpdy, const bool& allowHttp3, const bool& allowAltSvc, + const bool& beConservative, const uint32_t& tlsFlags, + const Maybe& aLoadInfoArgs, const uint32_t& aCacheKey, + const uint64_t& aRequestContextID, + const Maybe& aCorsPreflightArgs, + const uint32_t& aInitialRwin, const bool& aBlockAuthPrompt, + const bool& aAllowStaleCacheContent, + const bool& aPreferCacheLoadOverBypass, const nsCString& aContentTypeHint, + const uint32_t& aCorsMode, const uint32_t& aRedirectMode, + const uint64_t& aChannelId, const nsString& aIntegrityMetadata, + const uint64_t& aContentWindowId, + const nsTArray& + aPreferredAlternativeTypes, + const uint64_t& aTopLevelOuterContentWindowId, + const TimeStamp& aLaunchServiceWorkerStart, + const TimeStamp& aLaunchServiceWorkerEnd, + const TimeStamp& aDispatchFetchEventStart, + const TimeStamp& aDispatchFetchEventEnd, + const TimeStamp& aHandleFetchEventStart, + const TimeStamp& aHandleFetchEventEnd, + const bool& aForceMainDocumentChannel, + const TimeStamp& aNavigationStartTimeStamp, + const bool& hasNonEmptySandboxingFlag); + + virtual mozilla::ipc::IPCResult RecvSetPriority( + const int16_t& priority) override; + virtual mozilla::ipc::IPCResult RecvSetClassOfService( + const uint32_t& cos) override; + virtual mozilla::ipc::IPCResult RecvSuspend() override; + virtual mozilla::ipc::IPCResult RecvResume() override; + virtual mozilla::ipc::IPCResult RecvCancel( + const nsresult& status, const uint32_t& requestBlockingReason) override; + virtual mozilla::ipc::IPCResult RecvRedirect2Verify( + const nsresult& result, const RequestHeaderTuples& changedHeaders, + const uint32_t& aSourceRequestBlockingReason, + const Maybe& aTargetLoadInfoForwarder, + const uint32_t& loadFlags, nsIReferrerInfo* aReferrerInfo, + const Maybe& apiRedirectUri, + const Maybe& aCorsPreflightArgs, + const bool& aChooseAppcache) override; + virtual mozilla::ipc::IPCResult RecvDocumentChannelCleanup( + const bool& clearCacheEntry) override; + virtual mozilla::ipc::IPCResult RecvMarkOfflineCacheEntryAsForeign() override; + virtual mozilla::ipc::IPCResult RecvRemoveCorsPreflightCacheEntry( + const URIParams& uri, + const mozilla::ipc::PrincipalInfo& requestingPrincipal, + const OriginAttributes& originAttributes) override; + virtual mozilla::ipc::IPCResult RecvBytesRead(const int32_t& aCount) override; + virtual mozilla::ipc::IPCResult RecvOpenOriginalCacheInputStream() override; + virtual mozilla::ipc::IPCResult RecvOpenAltDataCacheInputStream( + const nsCString& aType) override; + virtual void ActorDestroy(ActorDestroyReason why) override; + + friend class ParentChannelListener; + RefPtr mBrowserParent; + + [[nodiscard]] nsresult ReportSecurityMessage( + const nsAString& aMessageTag, const nsAString& aMessageCategory) override; + nsresult LogBlockedCORSRequest(const nsAString& aMessage, + const nsACString& aCategory) override; + nsresult LogMimeTypeMismatch(const nsACString& aMessageName, bool aWarning, + const nsAString& aURL, + const nsAString& aContentType) override; + + // Calls SendDeleteSelf and sets mIPCClosed to true because we should not + // send any more messages after that. Bug 1274886 + [[nodiscard]] bool DoSendDeleteSelf(); + // Called to notify the parent channel to not send any more IPC messages. + virtual mozilla::ipc::IPCResult RecvDeletingChannel() override; + + private: + void UpdateAndSerializeSecurityInfo(nsACString& aSerializedSecurityInfoOut); + + // final step for Redirect2Verify procedure, will be invoked while both + // redirecting and redirected channel are ready or any error happened. + // OnRedirectVerifyCallback will be invoked for finishing the async + // redirect verification procedure. + void ContinueRedirect2Verify(const nsresult& aResult); + + void AsyncOpenFailed(nsresult aRv); + + // Request to pair with a HttpBackgroundChannelParent with the same channel + // id, a promise will be returned so the caller can append callbacks on it. + // If called multiple times before mBgParent is available, the same promise + // will be returned and the callbacks will be invoked in order. + [[nodiscard]] RefPtr WaitForBgParent(); + + // Remove the association with background channel after main-thread IPC + // is about to be destroyed or no further event is going to be sent, i.e., + // DocumentChannelCleanup. + void CleanupBackgroundChannel(); + + // Check if the channel needs to enable the flow control on the IPC channel. + // That is, we may suspend the channel if the ODA-s to child process are not + // consumed quickly enough. Otherwise, memory explosion could happen. + bool NeedFlowControl(); + int32_t mSendWindowSize; + + friend class HttpBackgroundChannelParent; + + RefPtr mChannel; + nsCOMPtr mCacheEntry; + + nsCOMPtr mRedirectChannel; + nsCOMPtr mRedirectCallback; + + UniquePtr + mOfflineForeignMarker; + nsCOMPtr mLoadContext; + RefPtr mHttpHandler; + + RefPtr mParentListener; + + RefPtr mEventQ; + + RefPtr mBgParent; + + MozPromiseHolder mPromise; + MozPromiseRequestHolder mRequest; + + // To calculate the delay caused by the e10s back-pressure suspension + TimeStamp mResumedTimestamp; + + Atomic mIPCClosed; // PHttpChannel actor has been Closed() + + // Corresponding redirect channel registrar Id. 0 means redirection is not + // started. + uint64_t mRedirectChannelId = 0; + + PBOverrideStatus mPBOverride; + + // Set to the canceled status value if the main channel was canceled. + nsresult mStatus; + + // The referrer info, set during nsHttpChannel::BeginConnect, to override the + // original one. This info will be sent in OnStartRequest. + nsCOMPtr mOverrideReferrerInfo; + + // The cookie string in Set-Cookie header. This info will be sent in + // OnStartRequest. + nsCString mCookie; + + // OnStatus is always called before OnProgress. + // Set true in OnStatus if next OnProgress can be ignored + // since the information can be recontructed from ODA. + uint8_t mIgnoreProgress : 1; + + uint8_t mSentRedirect1BeginFailed : 1; + uint8_t mReceivedRedirect2Verify : 1; + uint8_t mHasSuspendedByBackPressure : 1; + + // Set if we get the result of and cache |mNeedFlowControl| + uint8_t mCacheNeedFlowControlInitialized : 1; + uint8_t mNeedFlowControl : 1; + uint8_t mSuspendedForFlowControl : 1; + + // Defaults to false. Is set to true at the begining of OnStartRequest. + // Used to ensure methods can't be called before OnStartRequest. + uint8_t mAfterOnStartRequestBegun : 1; + + // Number of events to wait before actually invoking AsyncOpen on the main + // channel. For each asynchronous step required before InvokeAsyncOpen, should + // increase 1 to mAsyncOpenBarrier and invoke TryInvokeAsyncOpen after + // finished. This attribute is main thread only. + uint8_t mAsyncOpenBarrier = 0; + + // When true, ODAs are sent from the socket process to the child process + // directly. + uint8_t mDataSentToChildProcess : 1; +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(HttpChannelParent, HTTP_CHANNEL_PARENT_IID) + +} // namespace net +} // namespace mozilla + +#endif // mozilla_net_HttpChannelParent_h diff --git a/netwerk/protocol/http/HttpConnectionBase.cpp b/netwerk/protocol/http/HttpConnectionBase.cpp new file mode 100644 index 0000000000..747ac3cccf --- /dev/null +++ b/netwerk/protocol/http/HttpConnectionBase.cpp @@ -0,0 +1,70 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=4 sw=2 sts=2 et cin: */ +/* 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/. */ + +// HttpLog.h should generally be included first +#include "HttpLog.h" + +// Log on level :5, instead of default :4. +#undef LOG +#define LOG(args) LOG5(args) +#undef LOG_ENABLED +#define LOG_ENABLED() LOG5_ENABLED() + +#define TLS_EARLY_DATA_NOT_AVAILABLE 0 +#define TLS_EARLY_DATA_AVAILABLE_BUT_NOT_USED 1 +#define TLS_EARLY_DATA_AVAILABLE_AND_USED 2 + +#include "mozilla/Telemetry.h" +#include "HttpConnectionBase.h" +#include "nsHttpHandler.h" +#include "nsIClassOfService.h" +#include "nsIOService.h" +#include "nsISocketTransport.h" + +namespace mozilla { +namespace net { + +//----------------------------------------------------------------------------- +// nsHttpConnection +//----------------------------------------------------------------------------- + +HttpConnectionBase::HttpConnectionBase() + : mTransactionCaps(0), + mExperienced(false), + mBootstrappedTimingsSet(false), + mTotalBytesWritten(0), + mCallbacksLock("nsHttpConnection::mCallbacksLock"), + mRtt(0) { + LOG(("Creating HttpConnectionBase @%p\n", this)); +} + +void HttpConnectionBase::BootstrapTimings(TimingStruct times) { + mBootstrappedTimingsSet = true; + mBootstrappedTimings = times; +} + +void HttpConnectionBase::SetSecurityCallbacks( + nsIInterfaceRequestor* aCallbacks) { + MutexAutoLock lock(mCallbacksLock); + // This is called both on and off the main thread. For JS-implemented + // callbacks, we requires that the call happen on the main thread, but + // for C++-implemented callbacks we don't care. Use a pointer holder with + // strict checking disabled. + mCallbacks = new nsMainThreadPtrHolder( + "nsHttpConnection::mCallbacks", aCallbacks, false); +} + +void HttpConnectionBase::SetTrafficCategory(HttpTrafficCategory aCategory) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + if (aCategory == HttpTrafficCategory::eInvalid || + mTrafficCategory.Contains(aCategory)) { + return; + } + Unused << mTrafficCategory.AppendElement(aCategory); +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/HttpConnectionBase.h b/netwerk/protocol/http/HttpConnectionBase.h new file mode 100644 index 0000000000..cd503e0d69 --- /dev/null +++ b/netwerk/protocol/http/HttpConnectionBase.h @@ -0,0 +1,208 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef HttpConnectionBase_h__ +#define HttpConnectionBase_h__ + +#include "nsHttpConnectionInfo.h" +#include "nsHttpResponseHead.h" +#include "nsAHttpTransaction.h" +#include "nsCOMPtr.h" +#include "nsProxyRelease.h" +#include "prinrval.h" +#include "TunnelUtils.h" +#include "mozilla/Mutex.h" +#include "ARefBase.h" +#include "TimingStruct.h" +#include "HttpTrafficAnalyzer.h" + +#include "nsIAsyncInputStream.h" +#include "nsIAsyncOutputStream.h" +#include "nsIInterfaceRequestor.h" +#include "nsITimer.h" + +class nsISocketTransport; +class nsISSLSocketControl; + +namespace mozilla { +namespace net { + +class nsHttpHandler; +class ASpdySession; + +// 1dcc863e-db90-4652-a1fe-13fea0b54e46 +#define HTTPCONNECTIONBASE_IID \ + { \ + 0x437e7d26, 0xa2fd, 0x49f2, { \ + 0xb3, 0x7c, 0x84, 0x23, 0xf0, 0x94, 0x72, 0x36 \ + } \ + } + +//----------------------------------------------------------------------------- +// nsHttpConnection - represents a connection to a HTTP server (or proxy) +// +// NOTE: this objects lives on the socket thread only. it should not be +// accessed from any other thread. +//----------------------------------------------------------------------------- + +class HttpConnectionBase : public nsSupportsWeakReference { + public: + NS_DECLARE_STATIC_IID_ACCESSOR(HTTPCONNECTIONBASE_IID) + + HttpConnectionBase(); + + // Initialize the connection: + // info - specifies the connection parameters. + // maxHangTime - limits the amount of time this connection can spend on a + // single transaction before it should no longer be kept + // alive. a value of 0xffff indicates no limit. + [[nodiscard]] virtual nsresult Init( + nsHttpConnectionInfo* info, uint16_t maxHangTime, nsISocketTransport*, + nsIAsyncInputStream*, nsIAsyncOutputStream*, bool connectedTransport, + nsIInterfaceRequestor*, PRIntervalTime) = 0; + + // Activate causes the given transaction to be processed on this + // connection. It fails if there is already an existing transaction unless + // a multiplexing protocol such as SPDY is being used + [[nodiscard]] virtual nsresult Activate(nsAHttpTransaction*, uint32_t caps, + int32_t pri) = 0; + + // Close the underlying socket transport. + virtual void Close(nsresult reason, bool aIsShutdown = false) = 0; + + virtual bool CanReuse() = 0; // can this connection be reused? + virtual bool CanDirectlyActivate() = 0; + + virtual void DontReuse() = 0; + + nsISocketTransport* Transport() { return mSocketTransport; } + virtual nsAHttpTransaction* Transaction() = 0; + nsHttpConnectionInfo* ConnectionInfo() { return mConnInfo; } + + virtual void CloseTransaction(nsAHttpTransaction*, nsresult, + bool aIsShutdown = false) = 0; + + [[nodiscard]] virtual nsresult OnHeadersAvailable(nsAHttpTransaction*, + nsHttpRequestHead*, + nsHttpResponseHead*, + bool* reset) = 0; + + [[nodiscard]] virtual nsresult TakeTransport(nsISocketTransport**, + nsIAsyncInputStream**, + nsIAsyncOutputStream**) = 0; + + virtual bool UsingSpdy() { return false; } + virtual bool UsingHttp3() { return false; } + + virtual void SetTransactionCaps(uint32_t aCaps) { mTransactionCaps = aCaps; } + + virtual void PrintDiagnostics(nsCString& log) = 0; + + // IsExperienced() returns true when the connection has started at least one + // non null HTTP transaction of any version. + bool IsExperienced() { return mExperienced; } + + virtual bool TestJoinConnection(const nsACString& hostname, int32_t port) = 0; + virtual bool JoinConnection(const nsACString& hostname, int32_t port) = 0; + + // Return true when the socket this connection is using has not been + // authenticated using a client certificate. Before SSL negotiation + // has finished this returns false. + virtual bool NoClientCertAuth() const { return true; } + + // HTTP/2 websocket support + virtual bool CanAcceptWebsocket() { return false; } + + void GetConnectionInfo(nsHttpConnectionInfo** ci) { + NS_IF_ADDREF(*ci = mConnInfo); + } + virtual void GetSecurityInfo(nsISupports** result) = 0; + + [[nodiscard]] virtual nsresult ResumeSend() = 0; + [[nodiscard]] virtual nsresult ResumeRecv() = 0; + [[nodiscard]] virtual nsresult ForceSend() = 0; + [[nodiscard]] virtual nsresult ForceRecv() = 0; + virtual HttpVersion Version() = 0; + virtual bool IsProxyConnectInProgress() = 0; + virtual bool LastTransactionExpectedNoContent() = 0; + virtual void SetLastTransactionExpectedNoContent(bool) = 0; + int64_t BytesWritten() { return mTotalBytesWritten; } // includes TLS + void SetSecurityCallbacks(nsIInterfaceRequestor* aCallbacks); + void SetTrafficCategory(HttpTrafficCategory); + + void BootstrapTimings(TimingStruct times); + + virtual bool IsPersistent() = 0; + virtual bool IsReused() = 0; + [[nodiscard]] virtual nsresult PushBack(const char* data, + uint32_t length) = 0; + PRIntervalTime Rtt() { return mRtt; } + virtual void SetEvent(nsresult aStatus) = 0; + + protected: + nsCOMPtr mSocketTransport; + + // The capabailities associated with the most recent transaction + uint32_t mTransactionCaps; + + RefPtr mConnInfo; + + bool mExperienced; + + bool mBootstrappedTimingsSet; + TimingStruct mBootstrappedTimings; + + int64_t mTotalBytesWritten; // does not include CONNECT tunnel + + Mutex mCallbacksLock; + nsMainThreadPtrHandle mCallbacks; + + nsTArray mTrafficCategory; + PRIntervalTime mRtt; +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(HttpConnectionBase, HTTPCONNECTIONBASE_IID) + +#define NS_DECL_HTTPCONNECTIONBASE \ + [[nodiscard]] nsresult Init( \ + nsHttpConnectionInfo*, uint16_t, nsISocketTransport*, \ + nsIAsyncInputStream*, nsIAsyncOutputStream*, bool, \ + nsIInterfaceRequestor*, PRIntervalTime) override; \ + [[nodiscard]] nsresult Activate(nsAHttpTransaction*, uint32_t, int32_t) \ + override; \ + [[nodiscard]] nsresult OnHeadersAvailable( \ + nsAHttpTransaction*, nsHttpRequestHead*, nsHttpResponseHead*, \ + bool* reset) override; \ + [[nodiscard]] nsresult TakeTransport( \ + nsISocketTransport**, nsIAsyncInputStream**, nsIAsyncOutputStream**) \ + override; \ + void Close(nsresult, bool aIsShutdown = false) override; \ + bool CanReuse() override; \ + bool CanDirectlyActivate() override; \ + void DontReuse() override; \ + void CloseTransaction(nsAHttpTransaction*, nsresult, \ + bool aIsShutdown = false) override; \ + void PrintDiagnostics(nsCString&) override; \ + bool TestJoinConnection(const nsACString&, int32_t) override; \ + bool JoinConnection(const nsACString&, int32_t) override; \ + void GetSecurityInfo(nsISupports** result) override; \ + [[nodiscard]] nsresult ResumeSend() override; \ + [[nodiscard]] nsresult ResumeRecv() override; \ + [[nodiscard]] nsresult ForceSend() override; \ + [[nodiscard]] nsresult ForceRecv() override; \ + HttpVersion Version() override; \ + bool IsProxyConnectInProgress() override; \ + bool LastTransactionExpectedNoContent() override; \ + void SetLastTransactionExpectedNoContent(bool val) override; \ + bool IsPersistent() override; \ + bool IsReused() override; \ + [[nodiscard]] nsresult PushBack(const char* data, uint32_t length) override; \ + void SetEvent(nsresult aStatus) override; \ + virtual nsAHttpTransaction* Transaction() override; + +} // namespace net +} // namespace mozilla + +#endif // HttpConnectionBase_h__ diff --git a/netwerk/protocol/http/HttpConnectionMgrChild.cpp b/netwerk/protocol/http/HttpConnectionMgrChild.cpp new file mode 100644 index 0000000000..e76d170c8a --- /dev/null +++ b/netwerk/protocol/http/HttpConnectionMgrChild.cpp @@ -0,0 +1,182 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* vim:set ts=4 sw=4 sts=4 et cin: */ +/* 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/. */ + +// HttpLog.h should generally be included first +#include "HttpLog.h" + +#include "HttpConnectionMgrChild.h" +#include "HttpTransactionChild.h" +#include "AltSvcTransactionChild.h" +#include "EventTokenBucket.h" +#include "nsHttpConnectionInfo.h" +#include "nsHttpConnectionMgr.h" +#include "nsHttpHandler.h" +#include "nsISpeculativeConnect.h" + +namespace mozilla { +namespace net { + +HttpConnectionMgrChild::HttpConnectionMgrChild() + : mConnMgr(gHttpHandler->ConnMgr()) { + MOZ_ASSERT(mConnMgr); +} + +HttpConnectionMgrChild::~HttpConnectionMgrChild() { + LOG(("HttpConnectionMgrChild dtor:%p", this)); +} + +void HttpConnectionMgrChild::ActorDestroy(ActorDestroyReason aWhy) { + LOG(("HttpConnectionMgrChild::ActorDestroy [this=%p]\n", this)); +} + +mozilla::ipc::IPCResult +HttpConnectionMgrChild::RecvDoShiftReloadConnectionCleanupWithConnInfo( + const HttpConnectionInfoCloneArgs& aArgs) { + RefPtr cinfo = + nsHttpConnectionInfo::DeserializeHttpConnectionInfoCloneArgs(aArgs); + nsresult rv = mConnMgr->DoShiftReloadConnectionCleanupWithConnInfo(cinfo); + if (NS_FAILED(rv)) { + LOG( + ("HttpConnectionMgrChild::DoShiftReloadConnectionCleanupWithConnInfo " + "failed " + "(%08x)\n", + static_cast(rv))); + } + return IPC_OK(); +} + +mozilla::ipc::IPCResult +HttpConnectionMgrChild::RecvUpdateCurrentTopLevelOuterContentWindowId( + const uint64_t& aWindowId) { + mConnMgr->UpdateCurrentTopLevelOuterContentWindowId(aWindowId); + return IPC_OK(); +} + +nsHttpTransaction* ToRealHttpTransaction(PHttpTransactionChild* aTrans) { + HttpTransactionChild* transChild = static_cast(aTrans); + LOG(("ToRealHttpTransaction: [transChild=%p] \n", transChild)); + RefPtr trans = transChild->GetHttpTransaction(); + MOZ_ASSERT(trans); + return trans; +} + +mozilla::ipc::IPCResult HttpConnectionMgrChild::RecvAddTransaction( + PHttpTransactionChild* aTrans, const int32_t& aPriority) { + Unused << mConnMgr->AddTransaction(ToRealHttpTransaction(aTrans), aPriority); + return IPC_OK(); +} + +mozilla::ipc::IPCResult +HttpConnectionMgrChild::RecvAddTransactionWithStickyConn( + PHttpTransactionChild* aTrans, const int32_t& aPriority, + PHttpTransactionChild* aTransWithStickyConn) { + Unused << mConnMgr->AddTransactionWithStickyConn( + ToRealHttpTransaction(aTrans), aPriority, + ToRealHttpTransaction(aTransWithStickyConn)); + return IPC_OK(); +} + +mozilla::ipc::IPCResult HttpConnectionMgrChild::RecvRescheduleTransaction( + PHttpTransactionChild* aTrans, const int32_t& aPriority) { + Unused << mConnMgr->RescheduleTransaction(ToRealHttpTransaction(aTrans), + aPriority); + return IPC_OK(); +} + +mozilla::ipc::IPCResult +HttpConnectionMgrChild::RecvUpdateClassOfServiceOnTransaction( + PHttpTransactionChild* aTrans, const uint32_t& aClassOfService) { + mConnMgr->UpdateClassOfServiceOnTransaction(ToRealHttpTransaction(aTrans), + aClassOfService); + return IPC_OK(); +} + +mozilla::ipc::IPCResult HttpConnectionMgrChild::RecvCancelTransaction( + PHttpTransactionChild* aTrans, const nsresult& aReason) { + Unused << mConnMgr->CancelTransaction(ToRealHttpTransaction(aTrans), aReason); + return IPC_OK(); +} + +namespace { + +class SpeculativeConnectionOverrider final + : public nsIInterfaceRequestor, + public nsISpeculativeConnectionOverrider { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIINTERFACEREQUESTOR + NS_DECL_NSISPECULATIVECONNECTIONOVERRIDER + + explicit SpeculativeConnectionOverrider( + SpeculativeConnectionOverriderArgs&& aArgs) + : mArgs(std::move(aArgs)) {} + + private: + virtual ~SpeculativeConnectionOverrider() = default; + + SpeculativeConnectionOverriderArgs mArgs; +}; + +NS_IMPL_ISUPPORTS(SpeculativeConnectionOverrider, nsIInterfaceRequestor, + nsISpeculativeConnectionOverrider) + +NS_IMETHODIMP +SpeculativeConnectionOverrider::GetInterface(const nsIID& iid, void** result) { + if (NS_SUCCEEDED(QueryInterface(iid, result)) && *result) { + return NS_OK; + } + return NS_ERROR_NO_INTERFACE; +} + +NS_IMETHODIMP +SpeculativeConnectionOverrider::GetIgnoreIdle(bool* aIgnoreIdle) { + *aIgnoreIdle = mArgs.ignoreIdle(); + return NS_OK; +} + +NS_IMETHODIMP +SpeculativeConnectionOverrider::GetParallelSpeculativeConnectLimit( + uint32_t* aParallelSpeculativeConnectLimit) { + *aParallelSpeculativeConnectLimit = mArgs.parallelSpeculativeConnectLimit(); + return NS_OK; +} + +NS_IMETHODIMP +SpeculativeConnectionOverrider::GetIsFromPredictor(bool* aIsFromPredictor) { + *aIsFromPredictor = mArgs.isFromPredictor(); + return NS_OK; +} + +NS_IMETHODIMP +SpeculativeConnectionOverrider::GetAllow1918(bool* aAllow) { + *aAllow = mArgs.allow1918(); + return NS_OK; +} + +} // anonymous namespace + +mozilla::ipc::IPCResult HttpConnectionMgrChild::RecvSpeculativeConnect( + HttpConnectionInfoCloneArgs aConnInfo, + Maybe aOverriderArgs, uint32_t aCaps, + Maybe aTrans, const bool& aFetchHTTPSRR) { + RefPtr cinfo = + nsHttpConnectionInfo::DeserializeHttpConnectionInfoCloneArgs(aConnInfo); + nsCOMPtr overrider = + aOverriderArgs + ? new SpeculativeConnectionOverrider(std::move(aOverriderArgs.ref())) + : nullptr; + RefPtr trans; + if (aTrans) { + trans = static_cast(*aTrans)->CreateTransaction(); + } + + Unused << mConnMgr->SpeculativeConnect(cinfo, overrider, aCaps, trans, + aFetchHTTPSRR); + return IPC_OK(); +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/HttpConnectionMgrChild.h b/netwerk/protocol/http/HttpConnectionMgrChild.h new file mode 100644 index 0000000000..d382f8dece --- /dev/null +++ b/netwerk/protocol/http/HttpConnectionMgrChild.h @@ -0,0 +1,53 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* 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/. */ + +#ifndef HttpConnectionMgrChild_h__ +#define HttpConnectionMgrChild_h__ + +#include "mozilla/net/PHttpConnectionMgrChild.h" +#include "mozilla/RefPtr.h" + +namespace mozilla { +namespace net { + +class nsHttpConnectionMgr; + +class HttpConnectionMgrChild final : public PHttpConnectionMgrChild { + public: + NS_INLINE_DECL_REFCOUNTING(HttpConnectionMgrChild, override) + + explicit HttpConnectionMgrChild(); + void ActorDestroy(ActorDestroyReason aWhy) override; + + mozilla::ipc::IPCResult RecvDoShiftReloadConnectionCleanupWithConnInfo( + const HttpConnectionInfoCloneArgs& aArgs); + mozilla::ipc::IPCResult RecvUpdateCurrentTopLevelOuterContentWindowId( + const uint64_t& aWindowId); + mozilla::ipc::IPCResult RecvAddTransaction(PHttpTransactionChild* aTrans, + const int32_t& aPriority); + mozilla::ipc::IPCResult RecvAddTransactionWithStickyConn( + PHttpTransactionChild* aTrans, const int32_t& aPriority, + PHttpTransactionChild* aTransWithStickyConn); + mozilla::ipc::IPCResult RecvRescheduleTransaction( + PHttpTransactionChild* aTrans, const int32_t& aPriority); + mozilla::ipc::IPCResult RecvUpdateClassOfServiceOnTransaction( + PHttpTransactionChild* aTrans, const uint32_t& aClassOfService); + mozilla::ipc::IPCResult RecvCancelTransaction(PHttpTransactionChild* aTrans, + const nsresult& aReason); + mozilla::ipc::IPCResult RecvSpeculativeConnect( + HttpConnectionInfoCloneArgs aConnInfo, + Maybe aOverriderArgs, uint32_t aCaps, + Maybe aTrans, const bool& aFetchHTTPSRR); + + private: + virtual ~HttpConnectionMgrChild(); + + RefPtr mConnMgr; +}; + +} // namespace net +} // namespace mozilla + +#endif // HttpConnectionMgrChild_h__ diff --git a/netwerk/protocol/http/HttpConnectionMgrParent.cpp b/netwerk/protocol/http/HttpConnectionMgrParent.cpp new file mode 100644 index 0000000000..f50ce84af7 --- /dev/null +++ b/netwerk/protocol/http/HttpConnectionMgrParent.cpp @@ -0,0 +1,273 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* vim:set ts=4 sw=4 sts=4 et cin: */ +/* 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/. */ + +// HttpLog.h should generally be included first +#include "HttpLog.h" + +#include "HttpConnectionMgrParent.h" +#include "AltSvcTransactionParent.h" +#include "mozilla/net/HttpTransactionParent.h" +#include "nsHttpConnectionInfo.h" +#include "nsIInterfaceRequestor.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsISpeculativeConnect.h" +#include "nsIOService.h" +#include "nsQueryObject.h" + +namespace mozilla { +namespace net { + +NS_IMPL_ISUPPORTS0(HttpConnectionMgrParent) + +HttpConnectionMgrParent::HttpConnectionMgrParent() : mShutDown(false) {} + +nsresult HttpConnectionMgrParent::Init( + uint16_t maxUrgentExcessiveConns, uint16_t maxConnections, + uint16_t maxPersistentConnectionsPerHost, + uint16_t maxPersistentConnectionsPerProxy, uint16_t maxRequestDelay, + bool throttleEnabled, uint32_t throttleVersion, uint32_t throttleSuspendFor, + uint32_t throttleResumeFor, uint32_t throttleReadLimit, + uint32_t throttleReadInterval, uint32_t throttleHoldTime, + uint32_t throttleMaxTime, bool beConservativeForProxy) { + // We don't have to do anything here. nsHttpConnectionMgr in socket process is + // initialized by nsHttpHandler. + return NS_OK; +} + +nsresult HttpConnectionMgrParent::Shutdown() { + if (mShutDown) { + return NS_OK; + } + + if (!CanSend()) { + return NS_ERROR_NOT_AVAILABLE; + } + + mShutDown = true; + Unused << Send__delete__(this); + return NS_OK; +} + +nsresult HttpConnectionMgrParent::UpdateRequestTokenBucket( + EventTokenBucket* aBucket) { + // We don't have to do anything here. UpdateRequestTokenBucket() will be + // triggered by pref change in socket process. + return NS_OK; +} + +nsresult HttpConnectionMgrParent::DoShiftReloadConnectionCleanup() { + // Do nothing here. DoShiftReloadConnectionCleanup() will be triggered by + // observer notification or pref change in socket process. + return NS_OK; +} + +nsresult HttpConnectionMgrParent::DoShiftReloadConnectionCleanupWithConnInfo( + nsHttpConnectionInfo* aCi) { + if (!aCi) { + return NS_ERROR_INVALID_ARG; + } + + HttpConnectionInfoCloneArgs connInfoArgs; + nsHttpConnectionInfo::SerializeHttpConnectionInfo(aCi, connInfoArgs); + + RefPtr self = this; + auto task = [self, connInfoArgs{std::move(connInfoArgs)}]() { + Unused << self->SendDoShiftReloadConnectionCleanupWithConnInfo( + connInfoArgs); + }; + gIOService->CallOrWaitForSocketProcess(std::move(task)); + return NS_OK; +} + +nsresult HttpConnectionMgrParent::PruneDeadConnections() { + // Do nothing here. PruneDeadConnections() will be triggered by + // observer notification or pref change in socket process. + return NS_OK; +} + +void HttpConnectionMgrParent::AbortAndCloseAllConnections(int32_t, ARefBase*) { + // Do nothing here. AbortAndCloseAllConnections() will be triggered by + // observer notification in socket process. +} + +nsresult HttpConnectionMgrParent::UpdateParam(nsParamName name, + uint16_t value) { + // Do nothing here. UpdateParam() will be triggered by pref change in + // socket process. + return NS_OK; +} + +void HttpConnectionMgrParent::PrintDiagnostics() { + // Do nothing here. PrintDiagnostics() will be triggered by pref change in + // socket process. +} + +nsresult HttpConnectionMgrParent::UpdateCurrentTopLevelOuterContentWindowId( + uint64_t aWindowId) { + RefPtr self = this; + auto task = [self, aWindowId]() { + Unused << self->SendUpdateCurrentTopLevelOuterContentWindowId(aWindowId); + }; + gIOService->CallOrWaitForSocketProcess(std::move(task)); + return NS_OK; +} + +nsresult HttpConnectionMgrParent::AddTransaction(HttpTransactionShell* aTrans, + int32_t aPriority) { + MOZ_ASSERT(gIOService->SocketProcessReady()); + + if (!CanSend()) { + return NS_ERROR_NOT_AVAILABLE; + } + + Unused << SendAddTransaction(aTrans->AsHttpTransactionParent(), aPriority); + return NS_OK; +} + +nsresult HttpConnectionMgrParent::AddTransactionWithStickyConn( + HttpTransactionShell* aTrans, int32_t aPriority, + HttpTransactionShell* aTransWithStickyConn) { + MOZ_ASSERT(gIOService->SocketProcessReady()); + + if (!CanSend()) { + return NS_ERROR_NOT_AVAILABLE; + } + + Unused << SendAddTransactionWithStickyConn( + aTrans->AsHttpTransactionParent(), aPriority, + aTransWithStickyConn->AsHttpTransactionParent()); + return NS_OK; +} + +nsresult HttpConnectionMgrParent::RescheduleTransaction( + HttpTransactionShell* aTrans, int32_t aPriority) { + MOZ_ASSERT(gIOService->SocketProcessReady()); + + if (!CanSend()) { + return NS_ERROR_NOT_AVAILABLE; + } + + Unused << SendRescheduleTransaction(aTrans->AsHttpTransactionParent(), + aPriority); + return NS_OK; +} + +void HttpConnectionMgrParent::UpdateClassOfServiceOnTransaction( + HttpTransactionShell* aTrans, uint32_t aClassOfService) { + MOZ_ASSERT(gIOService->SocketProcessReady()); + + if (!CanSend()) { + return; + } + + Unused << SendUpdateClassOfServiceOnTransaction( + aTrans->AsHttpTransactionParent(), aClassOfService); +} + +nsresult HttpConnectionMgrParent::CancelTransaction( + HttpTransactionShell* aTrans, nsresult aReason) { + MOZ_ASSERT(gIOService->SocketProcessReady()); + + if (!CanSend()) { + return NS_ERROR_NOT_AVAILABLE; + } + + Unused << SendCancelTransaction(aTrans->AsHttpTransactionParent(), aReason); + return NS_OK; +} + +nsresult HttpConnectionMgrParent::ReclaimConnection(HttpConnectionBase*) { + MOZ_ASSERT_UNREACHABLE("ReclaimConnection should not be called"); + return NS_ERROR_NOT_IMPLEMENTED; +} + +nsresult HttpConnectionMgrParent::ProcessPendingQ(nsHttpConnectionInfo*) { + MOZ_ASSERT_UNREACHABLE("ProcessPendingQ should not be called"); + return NS_ERROR_NOT_IMPLEMENTED; +} + +nsresult HttpConnectionMgrParent::ProcessPendingQ() { + MOZ_ASSERT_UNREACHABLE("ProcessPendingQ should not be called"); + return NS_ERROR_NOT_IMPLEMENTED; +} + +nsresult HttpConnectionMgrParent::GetSocketThreadTarget(nsIEventTarget**) { + MOZ_ASSERT_UNREACHABLE("GetSocketThreadTarget should not be called"); + return NS_ERROR_NOT_IMPLEMENTED; +} + +nsresult HttpConnectionMgrParent::SpeculativeConnect( + nsHttpConnectionInfo* aConnInfo, nsIInterfaceRequestor* aCallbacks, + uint32_t aCaps, SpeculativeTransaction* aTransaction, bool aFetchHTTPSRR) { + NS_ENSURE_ARG_POINTER(aConnInfo); + + nsCOMPtr overrider = + do_GetInterface(aCallbacks); + Maybe overriderArgs; + if (overrider) { + overriderArgs.emplace(); + overriderArgs->parallelSpeculativeConnectLimit() = + overrider->GetParallelSpeculativeConnectLimit(); + overriderArgs->ignoreIdle() = overrider->GetIgnoreIdle(); + overriderArgs->isFromPredictor() = overrider->GetIsFromPredictor(); + overriderArgs->allow1918() = overrider->GetAllow1918(); + } + + HttpConnectionInfoCloneArgs connInfo; + nsHttpConnectionInfo::SerializeHttpConnectionInfo(aConnInfo, connInfo); + RefPtr trans = do_QueryObject(aTransaction); + RefPtr self = this; + auto task = [self, connInfo{std::move(connInfo)}, + overriderArgs{std::move(overriderArgs)}, aCaps, + trans{std::move(trans)}, aFetchHTTPSRR]() { + Maybe maybeTrans; + if (trans) { + maybeTrans.emplace(trans.get()); + } + Unused << self->SendSpeculativeConnect(connInfo, overriderArgs, aCaps, + maybeTrans, aFetchHTTPSRR); + }; + + gIOService->CallOrWaitForSocketProcess(std::move(task)); + return NS_OK; +} + +nsresult HttpConnectionMgrParent::VerifyTraffic() { + // Do nothing here. VerifyTraffic() will be triggered by observer notification + // in socket process. + return NS_OK; +} + +void HttpConnectionMgrParent::ExcludeHttp2(const nsHttpConnectionInfo* ci) { + MOZ_ASSERT_UNREACHABLE("ExcludeHttp2 should not be called"); +} + +void HttpConnectionMgrParent::ExcludeHttp3(const nsHttpConnectionInfo* ci) { + MOZ_ASSERT_UNREACHABLE("ExcludeHttp3 should not be called"); +} + +nsresult HttpConnectionMgrParent::ClearConnectionHistory() { + // Do nothing here. ClearConnectionHistory() will be triggered by + // observer notification in socket process. + return NS_OK; +} + +nsresult HttpConnectionMgrParent::CompleteUpgrade( + HttpTransactionShell* aTrans, nsIHttpUpgradeListener* aUpgradeListener) { + // TODO: fix this in bug 1497249 + return NS_ERROR_NOT_IMPLEMENTED; +} + +nsHttpConnectionMgr* HttpConnectionMgrParent::AsHttpConnectionMgr() { + return nullptr; +} + +HttpConnectionMgrParent* HttpConnectionMgrParent::AsHttpConnectionMgrParent() { + return this; +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/HttpConnectionMgrParent.h b/netwerk/protocol/http/HttpConnectionMgrParent.h new file mode 100644 index 0000000000..46d6670cdc --- /dev/null +++ b/netwerk/protocol/http/HttpConnectionMgrParent.h @@ -0,0 +1,34 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* 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/. */ + +#ifndef HttpConnectionMgrParent_h__ +#define HttpConnectionMgrParent_h__ + +#include "HttpConnectionMgrShell.h" +#include "mozilla/net/PHttpConnectionMgrParent.h" + +namespace mozilla { +namespace net { + +// HttpConnectionMgrParent plays the role of nsHttpConnectionMgr and delegates +// the work to the nsHttpConnectionMgr in socket process. +class HttpConnectionMgrParent final : public PHttpConnectionMgrParent, + public HttpConnectionMgrShell { + public: + NS_DECL_ISUPPORTS + NS_DECL_HTTPCONNECTIONMGRSHELL + + explicit HttpConnectionMgrParent(); + + private: + virtual ~HttpConnectionMgrParent() = default; + + bool mShutDown; +}; + +} // namespace net +} // namespace mozilla + +#endif // HttpConnectionMgrParent_h__ diff --git a/netwerk/protocol/http/HttpConnectionMgrShell.h b/netwerk/protocol/http/HttpConnectionMgrShell.h new file mode 100644 index 0000000000..da1944730d --- /dev/null +++ b/netwerk/protocol/http/HttpConnectionMgrShell.h @@ -0,0 +1,232 @@ +/* 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/. */ + +#ifndef HttpConnectionMgrShell_h__ +#define HttpConnectionMgrShell_h__ + +#include "nsISupports.h" + +class nsIEventTarget; +class nsIHttpUpgradeListener; +class nsIInterfaceRequestor; + +namespace mozilla { +namespace net { + +class ARefBase; +class EventTokenBucket; +class HttpTransactionShell; +class nsHttpConnectionInfo; +class HttpConnectionBase; +class nsHttpConnectionMgr; +class HttpConnectionMgrParent; +class SpeculativeTransaction; + +//---------------------------------------------------------------------------- +// Abstract base class for HTTP connection manager in chrome process +//---------------------------------------------------------------------------- + +// f5379ff9-2758-4bec-9992-2351c258aed6 +#define HTTPCONNECTIONMGRSHELL_IID \ + { \ + 0xf5379ff9, 0x2758, 0x4bec, { \ + 0x99, 0x92, 0x23, 0x51, 0xc2, 0x58, 0xae, 0xd6 \ + } \ + } + +class HttpConnectionMgrShell : public nsISupports { + public: + NS_DECLARE_STATIC_IID_ACCESSOR(HTTPCONNECTIONMGRSHELL_IID) + + enum nsParamName : uint32_t { + MAX_URGENT_START_Q, + MAX_CONNECTIONS, + MAX_PERSISTENT_CONNECTIONS_PER_HOST, + MAX_PERSISTENT_CONNECTIONS_PER_PROXY, + MAX_REQUEST_DELAY, + THROTTLING_ENABLED, + THROTTLING_SUSPEND_FOR, + THROTTLING_RESUME_FOR, + THROTTLING_READ_LIMIT, + THROTTLING_READ_INTERVAL, + THROTTLING_HOLD_TIME, + THROTTLING_MAX_TIME, + PROXY_BE_CONSERVATIVE + }; + + [[nodiscard]] virtual nsresult Init( + uint16_t maxUrgentExcessiveConns, uint16_t maxConnections, + uint16_t maxPersistentConnectionsPerHost, + uint16_t maxPersistentConnectionsPerProxy, uint16_t maxRequestDelay, + bool throttleEnabled, uint32_t throttleVersion, + uint32_t throttleSuspendFor, uint32_t throttleResumeFor, + uint32_t throttleReadLimit, uint32_t throttleReadInterval, + uint32_t throttleHoldTime, uint32_t throttleMaxTime, + bool beConservativeForProxy) = 0; + + [[nodiscard]] virtual nsresult Shutdown() = 0; + + // called from main thread to post a new request token bucket + // to the socket thread + [[nodiscard]] virtual nsresult UpdateRequestTokenBucket( + EventTokenBucket* aBucket) = 0; + + // Close all idle persistent connections and prevent any active connections + // from being reused. + [[nodiscard]] virtual nsresult DoShiftReloadConnectionCleanup() = 0; + + // Like DoShiftReloadConnectionCleanup() above, but also resets CI specific + // information such as Happy Eyeballs history. + [[nodiscard]] virtual nsresult DoShiftReloadConnectionCleanupWithConnInfo( + nsHttpConnectionInfo*) = 0; + + // called to force the connection manager to prune its list of idle + // connections. + [[nodiscard]] virtual nsresult PruneDeadConnections() = 0; + + // this cancels all outstanding transactions but does not shut down the mgr + virtual void AbortAndCloseAllConnections(int32_t, ARefBase*) = 0; + + // called to update a parameter after the connection manager has already + // been initialized. + [[nodiscard]] virtual nsresult UpdateParam(nsParamName name, + uint16_t value) = 0; + + // Causes a large amount of connection diagnostic information to be + // printed to the javascript console + virtual void PrintDiagnostics() = 0; + + virtual nsresult UpdateCurrentTopLevelOuterContentWindowId( + uint64_t aWindowId) = 0; + + // adds a transaction to the list of managed transactions. + [[nodiscard]] virtual nsresult AddTransaction(HttpTransactionShell*, + int32_t priority) = 0; + + // Add a new transaction with a sticky connection from |transWithStickyConn|. + [[nodiscard]] virtual nsresult AddTransactionWithStickyConn( + HttpTransactionShell* trans, int32_t priority, + HttpTransactionShell* transWithStickyConn) = 0; + + // called to reschedule the given transaction. it must already have been + // added to the connection manager via AddTransaction. + [[nodiscard]] virtual nsresult RescheduleTransaction(HttpTransactionShell*, + int32_t priority) = 0; + + void virtual UpdateClassOfServiceOnTransaction(HttpTransactionShell*, + uint32_t classOfService) = 0; + + // cancels a transaction w/ the given reason. + [[nodiscard]] virtual nsresult CancelTransaction(HttpTransactionShell*, + nsresult reason) = 0; + + // called when a connection is done processing a transaction. if the + // connection can be reused then it will be added to the idle list, else + // it will be closed. + [[nodiscard]] virtual nsresult ReclaimConnection( + HttpConnectionBase* conn) = 0; + + // called to force the transaction queue to be processed once more, giving + // preference to the specified connection. + [[nodiscard]] virtual nsresult ProcessPendingQ(nsHttpConnectionInfo*) = 0; + + // Try and process all pending transactions + [[nodiscard]] virtual nsresult ProcessPendingQ() = 0; + + // called to get a reference to the socket transport service. the socket + // transport service is not available when the connection manager is down. + [[nodiscard]] virtual nsresult GetSocketThreadTarget(nsIEventTarget**) = 0; + + // called to indicate a transaction for the connectionInfo is likely coming + // soon. The connection manager may use this information to start a TCP + // and/or SSL level handshake for that resource immediately so that it is + // ready when the transaction is submitted. No obligation is taken on by the + // connection manager, nor is the submitter obligated to actually submit a + // real transaction for this connectionInfo. + [[nodiscard]] virtual nsresult SpeculativeConnect( + nsHttpConnectionInfo*, nsIInterfaceRequestor*, uint32_t caps = 0, + SpeculativeTransaction* = nullptr, bool aFetchHTTPSRR = false) = 0; + + // "VerifyTraffic" means marking connections now, and then check again in + // N seconds to see if there's been any traffic and if not, kill + // that connection. + [[nodiscard]] virtual nsresult VerifyTraffic() = 0; + + virtual void ExcludeHttp2(const nsHttpConnectionInfo* ci) = 0; + virtual void ExcludeHttp3(const nsHttpConnectionInfo* ci) = 0; + + // clears the connection history mCT + [[nodiscard]] virtual nsresult ClearConnectionHistory() = 0; + + // called by the main thread to execute the taketransport() logic on the + // socket thread after a 101 response has been received and the socket + // needs to be transferred to an expectant upgrade listener such as + // websockets. + // @param aTrans: a transaction that contains a sticky connection. We'll + // take the transport of this connection. + [[nodiscard]] virtual nsresult CompleteUpgrade( + HttpTransactionShell* aTrans, + nsIHttpUpgradeListener* aUpgradeListener) = 0; + + virtual nsHttpConnectionMgr* AsHttpConnectionMgr() = 0; + virtual HttpConnectionMgrParent* AsHttpConnectionMgrParent() = 0; +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(HttpConnectionMgrShell, + HTTPCONNECTIONMGRSHELL_IID) + +#define NS_DECL_HTTPCONNECTIONMGRSHELL \ + virtual nsresult Init( \ + uint16_t maxUrgentExcessiveConns, uint16_t maxConnections, \ + uint16_t maxPersistentConnectionsPerHost, \ + uint16_t maxPersistentConnectionsPerProxy, uint16_t maxRequestDelay, \ + bool throttleEnabled, uint32_t throttleVersion, \ + uint32_t throttleSuspendFor, uint32_t throttleResumeFor, \ + uint32_t throttleReadLimit, uint32_t throttleReadInterval, \ + uint32_t throttleHoldTime, uint32_t throttleMaxTime, \ + bool beConservativeForProxy) override; \ + virtual nsresult Shutdown() override; \ + virtual nsresult UpdateRequestTokenBucket(EventTokenBucket* aBucket) \ + override; \ + virtual nsresult DoShiftReloadConnectionCleanup() override; \ + virtual nsresult DoShiftReloadConnectionCleanupWithConnInfo( \ + nsHttpConnectionInfo*) override; \ + virtual nsresult PruneDeadConnections() override; \ + virtual void AbortAndCloseAllConnections(int32_t, ARefBase*) override; \ + virtual nsresult UpdateParam(nsParamName name, uint16_t value) override; \ + virtual void PrintDiagnostics() override; \ + virtual nsresult UpdateCurrentTopLevelOuterContentWindowId( \ + uint64_t aWindowId) override; \ + virtual nsresult AddTransaction(HttpTransactionShell*, int32_t priority) \ + override; \ + virtual nsresult AddTransactionWithStickyConn( \ + HttpTransactionShell* trans, int32_t priority, \ + HttpTransactionShell* transWithStickyConn) override; \ + virtual nsresult RescheduleTransaction(HttpTransactionShell*, \ + int32_t priority) override; \ + void virtual UpdateClassOfServiceOnTransaction( \ + HttpTransactionShell*, uint32_t classOfService) override; \ + virtual nsresult CancelTransaction(HttpTransactionShell*, nsresult reason) \ + override; \ + virtual nsresult ReclaimConnection(HttpConnectionBase* conn) override; \ + virtual nsresult ProcessPendingQ(nsHttpConnectionInfo*) override; \ + virtual nsresult ProcessPendingQ() override; \ + virtual nsresult GetSocketThreadTarget(nsIEventTarget**) override; \ + virtual nsresult SpeculativeConnect( \ + nsHttpConnectionInfo*, nsIInterfaceRequestor*, uint32_t caps = 0, \ + SpeculativeTransaction* = nullptr, bool aFetchHTTPSRR = false) override; \ + virtual nsresult VerifyTraffic() override; \ + virtual void ExcludeHttp2(const nsHttpConnectionInfo* ci) override; \ + virtual void ExcludeHttp3(const nsHttpConnectionInfo* ci) override; \ + virtual nsresult ClearConnectionHistory() override; \ + virtual nsresult CompleteUpgrade(HttpTransactionShell* aTrans, \ + nsIHttpUpgradeListener* aUpgradeListener) \ + override; \ + nsHttpConnectionMgr* AsHttpConnectionMgr() override; \ + HttpConnectionMgrParent* AsHttpConnectionMgrParent() override; + +} // namespace net +} // namespace mozilla + +#endif // HttpConnectionMgrShell_h__ diff --git a/netwerk/protocol/http/HttpConnectionUDP.cpp b/netwerk/protocol/http/HttpConnectionUDP.cpp new file mode 100644 index 0000000000..4d25b24ef6 --- /dev/null +++ b/netwerk/protocol/http/HttpConnectionUDP.cpp @@ -0,0 +1,772 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=4 sw=2 sts=2 et cin: */ +/* 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/. */ + +// HttpLog.h should generally be included first +#include "HttpLog.h" + +// Log on level :5, instead of default :4. +#undef LOG +#define LOG(args) LOG5(args) +#undef LOG_ENABLED +#define LOG_ENABLED() LOG5_ENABLED() + +#define TLS_EARLY_DATA_NOT_AVAILABLE 0 +#define TLS_EARLY_DATA_AVAILABLE_BUT_NOT_USED 1 +#define TLS_EARLY_DATA_AVAILABLE_AND_USED 2 + +#include "ASpdySession.h" +#include "mozilla/ChaosMode.h" +#include "mozilla/Telemetry.h" +#include "HttpConnectionUDP.h" +#include "nsHttpHandler.h" +#include "nsHttpRequestHead.h" +#include "nsHttpResponseHead.h" +#include "nsIClassOfService.h" +#include "nsIOService.h" +#include "nsISocketTransport.h" +#include "nsSocketTransportService2.h" +#include "nsISSLSocketControl.h" +#include "nsISupportsPriority.h" +#include "nsPreloadedStream.h" +#include "nsProxyRelease.h" +#include "nsSocketTransport2.h" +#include "nsStringStream.h" +#include "mozpkix/pkixnss.h" +#include "sslt.h" +#include "NSSErrorsService.h" +#include "TunnelUtils.h" +#include "TCPFastOpenLayer.h" +#include "Http3Session.h" + +namespace mozilla { +namespace net { + +//----------------------------------------------------------------------------- +// HttpConnectionUDP +//----------------------------------------------------------------------------- + +HttpConnectionUDP::HttpConnectionUDP() + : mHttpHandler(gHttpHandler), + mLastReadTime(0), + mLastWriteTime(0), + mTotalBytesRead(0), + mConnectedTransport(false), + mDontReuse(false), + mIsReused(false), + mLastTransactionExpectedNoContent(false), + mPriority(nsISupportsPriority::PRIORITY_NORMAL), + mForceSendPending(false), + mLastRequestBytesSentTime(0) { + LOG(("Creating HttpConnectionUDP @%p\n", this)); + + mThroughCaptivePortal = gHttpHandler->GetThroughCaptivePortal(); +} + +HttpConnectionUDP::~HttpConnectionUDP() { + LOG(("Destroying HttpConnectionUDP @%p\n", this)); + + if (mThroughCaptivePortal) { + if (mTotalBytesRead || mTotalBytesWritten) { + auto total = + Clamp((mTotalBytesRead >> 10) + (mTotalBytesWritten >> 10), + 0, std::numeric_limits::max()); + Telemetry::ScalarAdd( + Telemetry::ScalarID::NETWORKING_DATA_TRANSFERRED_CAPTIVE_PORTAL, + total); + } + + Telemetry::ScalarAdd( + Telemetry::ScalarID::NETWORKING_HTTP_CONNECTIONS_CAPTIVE_PORTAL, 1); + } + + if (mForceSendTimer) { + mForceSendTimer->Cancel(); + mForceSendTimer = nullptr; + } +} + +nsresult HttpConnectionUDP::Init( + nsHttpConnectionInfo* info, uint16_t maxHangTime, + nsISocketTransport* transport, nsIAsyncInputStream* instream, + nsIAsyncOutputStream* outstream, bool connectedTransport, + nsIInterfaceRequestor* callbacks, PRIntervalTime rtt) { + LOG1(("HttpConnectionUDP::Init this=%p sockettransport=%p", this, transport)); + NS_ENSURE_ARG_POINTER(info); + NS_ENSURE_TRUE(!mConnInfo, NS_ERROR_ALREADY_INITIALIZED); + + mConnectedTransport = connectedTransport; + mConnInfo = info; + MOZ_ASSERT(mConnInfo); + + mLastWriteTime = mLastReadTime = PR_IntervalNow(); + mRtt = rtt; + + mSocketTransport = transport; + mSocketIn = instream; + mSocketOut = outstream; + + MOZ_ASSERT(mConnInfo->IsHttp3()); + mHttp3Session = new Http3Session(); + nsresult rv = mHttp3Session->Init(mConnInfo, mSocketTransport, this); + if (NS_FAILED(rv)) { + LOG( + ("HttpConnectionUDP::Init mHttp3Session->Init failed " + "[this=%p rv=%x]\n", + this, static_cast(rv))); + return rv; + } + + // See explanation for non-strictness of this operation in + // SetSecurityCallbacks. + mCallbacks = new nsMainThreadPtrHolder( + "HttpConnectionUDP::mCallbacks", callbacks, false); + + mSocketTransport->SetEventSink(this, nullptr); + mSocketTransport->SetSecurityCallbacks(this); + + return NS_OK; +} + +// called on the socket thread +nsresult HttpConnectionUDP::Activate(nsAHttpTransaction* trans, uint32_t caps, + int32_t pri) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG1(("HttpConnectionUDP::Activate [this=%p trans=%p caps=%x]\n", this, trans, + caps)); + + if (!mExperienced && !trans->IsNullTransaction()) { + // For QUIC and TFO we have HttpConnecitonUDP before the actual connection + // has been establish so wait fo TFO and TLS handshake to be finished before + // we mark the connection 'experienced'. + if (!mExperienced && mHttp3Session->IsConnected()) { + mExperienced = true; + } + if (mBootstrappedTimingsSet) { + mBootstrappedTimingsSet = false; + nsHttpTransaction* hTrans = trans->QueryHttpTransaction(); + if (hTrans) { + hTrans->BootstrapTimings(mBootstrappedTimings); + } + } + mBootstrappedTimings = TimingStruct(); + } + + mTransactionCaps = caps; + mPriority = pri; + + NS_ENSURE_ARG_POINTER(trans); + + // reset the read timers to wash away any idle time + mLastWriteTime = mLastReadTime = PR_IntervalNow(); + + // Connection failures are Activated() just like regular transacions. + // If we don't have a confirmation of a connected socket then test it + // with a write() to get relevant error code. + if (!mConnectedTransport) { + uint32_t count; + nsresult rv = NS_OK; + if (mSocketOut) { + rv = mSocketOut->Write("", 0, &count); + } + if (NS_FAILED(rv) && rv != NS_BASE_STREAM_WOULD_BLOCK) { + LOG(("HttpConnectionUDP::Activate [this=%p] Bad Socket %" PRIx32 "\n", + this, static_cast(rv))); + mSocketOut->AsyncWait(nullptr, 0, 0, nullptr); + CloseTransaction(mHttp3Session, rv); + trans->Close(rv); + return rv; + } + } + + if (!mHttp3Session->AddStream(trans, pri, mCallbacks)) { + MOZ_ASSERT(false); // this cannot happen! + trans->Close(NS_ERROR_ABORT); + return NS_ERROR_FAILURE; + } + + Unused << ResumeSend(); + return NS_OK; +} + +void HttpConnectionUDP::Close(nsresult reason, bool aIsShutdown) { + LOG(("HttpConnectionUDP::Close [this=%p reason=%" PRIx32 "]\n", this, + static_cast(reason))); + + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + if (mForceSendTimer) { + mForceSendTimer->Cancel(); + mForceSendTimer = nullptr; + } + + if (!mTrafficCategory.IsEmpty()) { + HttpTrafficAnalyzer* hta = gHttpHandler->GetHttpTrafficAnalyzer(); + if (hta) { + hta->IncrementHttpConnection(std::move(mTrafficCategory)); + MOZ_ASSERT(mTrafficCategory.IsEmpty()); + } + } + + if (mSocketTransport) { + mSocketTransport->SetEventSink(nullptr, nullptr); + mSocketTransport->SetSecurityCallbacks(nullptr); + mSocketTransport->Close(reason); + if (mSocketOut) { + mSocketOut->AsyncWait(nullptr, 0, 0, nullptr); + } + + if (mSocketIn) { + mSocketIn->AsyncWait(nullptr, 0, 0, nullptr); + } + } +} + +void HttpConnectionUDP::DontReuse() { + LOG(("HttpConnectionUDP::DontReuse %p http3session=%p\n", this, + mHttp3Session.get())); + mDontReuse = true; + if (mHttp3Session) { + mHttp3Session->DontReuse(); + } +} + +bool HttpConnectionUDP::TestJoinConnection(const nsACString& hostname, + int32_t port) { + if (mHttp3Session && CanDirectlyActivate()) { + return mHttp3Session->TestJoinConnection(hostname, port); + } + + return false; +} + +bool HttpConnectionUDP::JoinConnection(const nsACString& hostname, + int32_t port) { + if (mHttp3Session && CanDirectlyActivate()) { + return mHttp3Session->JoinConnection(hostname, port); + } + + return false; +} + +bool HttpConnectionUDP::CanReuse() { + if (!mSocketTransport || !mConnectedTransport) return false; + if (mDontReuse) { + return false; + } + + if (mHttp3Session) { + return mHttp3Session->CanReuse(); + } + return false; +} + +bool HttpConnectionUDP::CanDirectlyActivate() { + // return true if a new transaction can be addded to ths connection at any + // time through Activate(). In practice this means this is a healthy SPDY + // connection with room for more concurrent streams. + + if (mHttp3Session) { + return CanReuse(); + } + return false; +} + +//---------------------------------------------------------------------------- +// HttpConnectionUDP::nsAHttpConnection compatible methods +//---------------------------------------------------------------------------- + +nsresult HttpConnectionUDP::OnHeadersAvailable(nsAHttpTransaction* trans, + nsHttpRequestHead* requestHead, + nsHttpResponseHead* responseHead, + bool* reset) { + LOG( + ("HttpConnectionUDP::OnHeadersAvailable [this=%p trans=%p " + "response-head=%p]\n", + this, trans, responseHead)); + + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + NS_ENSURE_ARG_POINTER(trans); + MOZ_ASSERT(responseHead, "No response head?"); + + if (mHttp3Session) { + DebugOnly rv = responseHead->SetHeader( + nsHttp::X_Firefox_Http3, mHttp3Session->GetAlpnToken()); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + + // deal with 408 Server Timeouts + uint16_t responseStatus = responseHead->Status(); + static const PRIntervalTime k1000ms = PR_MillisecondsToInterval(1000); + if (responseStatus == 408) { + // If this error could be due to a persistent connection reuse then + // we pass an error code of NS_ERROR_NET_RESET to + // trigger the transaction 'restart' mechanism. We tell it to reset its + // response headers so that it will be ready to receive the new response. + if (mIsReused && ((PR_IntervalNow() - mLastWriteTime) < k1000ms)) { + Close(NS_ERROR_NET_RESET); + *reset = true; + return NS_OK; + } + } + + return NS_OK; +} + +bool HttpConnectionUDP::IsReused() { return mIsReused; } + +nsresult HttpConnectionUDP::TakeTransport( + nsISocketTransport** aTransport, nsIAsyncInputStream** aInputStream, + nsIAsyncOutputStream** aOutputStream) { + return NS_ERROR_FAILURE; +} + +void HttpConnectionUDP::GetSecurityInfo(nsISupports** secinfo) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG(("HttpConnectionUDP::GetSecurityInfo http3Session=%p socket=%p\n", + mHttp3Session.get(), mSocketTransport.get())); + + if (mHttp3Session && + NS_SUCCEEDED(mHttp3Session->GetTransactionSecurityInfo(secinfo))) { + return; + } + + if (mSocketTransport && + NS_SUCCEEDED(mSocketTransport->GetSecurityInfo(secinfo))) { + return; + } + + *secinfo = nullptr; +} + +nsresult HttpConnectionUDP::PushBack(const char* data, uint32_t length) { + LOG(("HttpConnectionUDP::PushBack [this=%p, length=%d]\n", this, length)); + + return NS_ERROR_UNEXPECTED; +} + +class HttpConnectionUDPForceIO : public Runnable { + public: + HttpConnectionUDPForceIO(HttpConnectionUDP* aConn, bool doRecv) + : Runnable("net::HttpConnectionUDPForceIO"), + mConn(aConn), + mDoRecv(doRecv) {} + + NS_IMETHOD Run() override { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + if (mDoRecv) { + if (!mConn->mSocketIn) return NS_OK; + return mConn->OnInputStreamReady(mConn->mSocketIn); + } + + MOZ_ASSERT(mConn->mForceSendPending); + mConn->mForceSendPending = false; + + if (!mConn->mSocketOut) { + return NS_OK; + } + return mConn->OnOutputStreamReady(mConn->mSocketOut); + } + + private: + RefPtr mConn; + bool mDoRecv; +}; + +nsresult HttpConnectionUDP::ResumeSend() { + LOG(("HttpConnectionUDP::ResumeSend [this=%p]\n", this)); + + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + if (mSocketOut) { + nsresult rv = mSocketOut->AsyncWait(this, 0, 0, nullptr); + LOG(("HttpConnectionUDP::ResumeSend [this=%p]\n", this)); + return rv; + } + + MOZ_ASSERT_UNREACHABLE("no socket output stream"); + return NS_ERROR_UNEXPECTED; +} + +nsresult HttpConnectionUDP::ResumeRecv() { + LOG(("HttpConnectionUDP::ResumeRecv [this=%p]\n", this)); + + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + // the mLastReadTime timestamp is used for finding slowish readers + // and can be pretty sensitive. For that reason we actually reset it + // when we ask to read (resume recv()) so that when we get called back + // with actual read data in OnSocketReadable() we are only measuring + // the latency between those two acts and not all the processing that + // may get done before the ResumeRecv() call + mLastReadTime = PR_IntervalNow(); + + if (mSocketIn) { + return mSocketIn->AsyncWait(this, 0, 0, nullptr); + } + + MOZ_ASSERT_UNREACHABLE("no socket input stream"); + return NS_ERROR_UNEXPECTED; +} + +void HttpConnectionUDP::ForceSendIO(nsITimer* aTimer, void* aClosure) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + HttpConnectionUDP* self = static_cast(aClosure); + MOZ_ASSERT(aTimer == self->mForceSendTimer); + self->mForceSendTimer = nullptr; + NS_DispatchToCurrentThread(new HttpConnectionUDPForceIO(self, false)); +} + +nsresult HttpConnectionUDP::MaybeForceSendIO() { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + // due to bug 1213084 sometimes real I/O events do not get serviced when + // NSPR derived I/O events are ready and this can cause a deadlock with + // https over https proxying. Normally we would expect the write callback to + // be invoked before this timer goes off, but set it at the old windows + // tick interval (kForceDelay) as a backup for those circumstances. + static const uint32_t kForceDelay = 17; // ms + + if (mForceSendPending) { + return NS_OK; + } + MOZ_ASSERT(!mForceSendTimer); + mForceSendPending = true; + return NS_NewTimerWithFuncCallback( + getter_AddRefs(mForceSendTimer), HttpConnectionUDP::ForceSendIO, this, + kForceDelay, nsITimer::TYPE_ONE_SHOT, + "net::HttpConnectionUDP::MaybeForceSendIO"); +} + +// trigger an asynchronous read +nsresult HttpConnectionUDP::ForceRecv() { + LOG(("HttpConnectionUDP::ForceRecv [this=%p]\n", this)); + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + return NS_DispatchToCurrentThread(new HttpConnectionUDPForceIO(this, true)); +} + +// trigger an asynchronous write +nsresult HttpConnectionUDP::ForceSend() { + LOG(("HttpConnectionUDP::ForceSend [this=%p]\n", this)); + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + return MaybeForceSendIO(); +} + +HttpVersion HttpConnectionUDP::Version() { return HttpVersion::v3_0; } + +//----------------------------------------------------------------------------- +// HttpConnectionUDP +//----------------------------------------------------------------------------- + +void HttpConnectionUDP::CloseTransaction(nsAHttpTransaction* trans, + nsresult reason, bool aIsShutdown) { + LOG(("HttpConnectionUDP::CloseTransaction[this=%p trans=%p reason=%" PRIx32 + "]\n", + this, trans, static_cast(reason))); + + MOZ_ASSERT(trans == mHttp3Session); + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + if (NS_SUCCEEDED(reason) || (reason == NS_BASE_STREAM_CLOSED)) { + MOZ_ASSERT(false); + return; + } + + // The connection and security errors clear out alt-svc mappings + // in case any previously validated ones are now invalid + if (((reason == NS_ERROR_NET_RESET) || + (NS_ERROR_GET_MODULE(reason) == NS_ERROR_MODULE_SECURITY)) && + mConnInfo && !(mTransactionCaps & NS_HTTP_ERROR_SOFTLY)) { + gHttpHandler->ClearHostMapping(mConnInfo); + } + + mDontReuse = true; + if (mHttp3Session) { + mHttp3Session->SetCleanShutdown(aIsShutdown); + mHttp3Session->Close(reason); + if (!mHttp3Session->IsClosed()) { + // During closing phase we still keep mHttp3Session session, + // to resend CLOSE_CONNECTION frames. + return; + } + } + + mHttp3Session = nullptr; + + { + MutexAutoLock lock(mCallbacksLock); + mCallbacks = nullptr; + } + + Close(reason, aIsShutdown); + + // flag the connection as reused here for convenience sake. certainly + // it might be going away instead ;-) + mIsReused = true; +} + +nsresult HttpConnectionUDP::OnReadSegment(const char* buf, uint32_t count, + uint32_t* countRead) { + LOG(("HttpConnectionUDP::OnReadSegment [this=%p]\n", this)); + if (count == 0) { + // some ReadSegments implementations will erroneously call the writer + // to consume 0 bytes worth of data. we must protect against this case + // or else we'd end up closing the socket prematurely. + NS_ERROR("bad ReadSegments implementation"); + return NS_ERROR_FAILURE; // stop iterating + } + + nsresult rv = mSocketOut->Write(buf, count, countRead); + if (NS_FAILED(rv)) { + return rv; + } + + if (*countRead == 0) { + return NS_BASE_STREAM_CLOSED; + } + + mLastWriteTime = PR_IntervalNow(); + mTotalBytesWritten += *countRead; + + return NS_OK; +} + +nsresult HttpConnectionUDP::OnSocketWritable() { + LOG(("HttpConnectionUDP::OnSocketWritable [this=%p] host=%s\n", this, + mConnInfo->Origin())); + + if (!mHttp3Session) { + LOG((" No session In OnSocketWritable\n")); + return NS_ERROR_FAILURE; + } + + uint32_t transactionBytes = 0; + bool again = true; + LOG((" writing transaction request stream\n")); + nsresult rv = mHttp3Session->ReadSegmentsAgain( + this, nsIOService::gDefaultSegmentSize, &transactionBytes, &again); + return rv; +} + +nsresult HttpConnectionUDP::OnWriteSegment(char* buf, uint32_t count, + uint32_t* countWritten) { + if (count == 0) { + // some WriteSegments implementations will erroneously call the reader + // to provide 0 bytes worth of data. we must protect against this case + // or else we'd end up closing the socket prematurely. + NS_ERROR("bad WriteSegments implementation"); + return NS_ERROR_FAILURE; // stop iterating + } + + if (ChaosMode::isActive(ChaosFeature::IOAmounts) && + ChaosMode::randomUint32LessThan(2)) { + // read 1...count bytes + count = ChaosMode::randomUint32LessThan(count) + 1; + } + + nsresult rv = mSocketIn->Read(buf, count, countWritten); + if (NS_FAILED(rv)) { + return rv; + } + + if (*countWritten == 0) { + return NS_BASE_STREAM_CLOSED; + } + + return NS_OK; +} + +void HttpConnectionUDP::OnQuicTimeout(nsITimer* aTimer, void* aClosure) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG(("HttpConnectionUDP::OnQuicTimeout [this=%p]\n", aClosure)); + + HttpConnectionUDP* self = static_cast(aClosure); + self->OnQuicTimeoutExpired(); +} + +void HttpConnectionUDP::OnQuicTimeoutExpired() { + // if the transaction was dropped... + if (!mHttp3Session) { + LOG((" no transaction; ignoring event\n")); + return; + } + + nsresult rv = mHttp3Session->ProcessOutputAndEvents(); + if (NS_FAILED(rv)) { + CloseTransaction(mHttp3Session, rv); + } +} + +nsresult HttpConnectionUDP::OnSocketReadable() { + LOG(("HttpConnectionUDP::OnSocketReadable [this=%p]\n", this)); + + if (!mHttp3Session) { + LOG((" No session In OnSocketReadable\n")); + return NS_ERROR_FAILURE; + } + + PRIntervalTime now = PR_IntervalNow(); + + // Reduce the estimate of the time since last read by up to 1 RTT to + // accommodate exhausted sender TCP congestion windows or minor I/O delays. + mLastReadTime = now; + + uint32_t n = 0; + bool again = true; + + nsresult rv = mHttp3Session->WriteSegmentsAgain( + this, nsIOService::gDefaultSegmentSize, &n, &again); + LOG(("HttpConnectionUDP::OnSocketReadable %p trans->ws rv=%" PRIx32 + " n=%d \n", + this, static_cast(rv), n)); + if (NS_FAILED(rv)) { + return rv; + } + + mTotalBytesRead += n; + + return rv; +} + +//----------------------------------------------------------------------------- +// HttpConnectionUDP::nsISupports +//----------------------------------------------------------------------------- + +NS_IMPL_ADDREF(HttpConnectionUDP) +NS_IMPL_RELEASE(HttpConnectionUDP) + +NS_INTERFACE_MAP_BEGIN(HttpConnectionUDP) + NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference) + NS_INTERFACE_MAP_ENTRY(nsIInputStreamCallback) + NS_INTERFACE_MAP_ENTRY(nsIOutputStreamCallback) + NS_INTERFACE_MAP_ENTRY(nsITransportEventSink) + NS_INTERFACE_MAP_ENTRY(nsIInterfaceRequestor) + NS_INTERFACE_MAP_ENTRY(HttpConnectionBase) + NS_INTERFACE_MAP_ENTRY_CONCRETE(HttpConnectionUDP) +NS_INTERFACE_MAP_END + +//----------------------------------------------------------------------------- +// HttpConnectionUDP::nsIInputStreamCallback +//----------------------------------------------------------------------------- + +// called on the socket transport thread +NS_IMETHODIMP +HttpConnectionUDP::OnInputStreamReady(nsIAsyncInputStream* in) { + MOZ_ASSERT(in == mSocketIn, "unexpected stream"); + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + // if the transaction was dropped... + if (!mHttp3Session) { + LOG((" no transaction; ignoring event\n")); + return NS_OK; + } + + nsresult rv = OnSocketReadable(); + if (NS_FAILED(rv)) CloseTransaction(mHttp3Session, rv); + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// HttpConnectionUDP::nsIOutputStreamCallback +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +HttpConnectionUDP::OnOutputStreamReady(nsIAsyncOutputStream* out) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + MOZ_ASSERT(out == mSocketOut, "unexpected socket"); + // if the transaction was dropped... + if (!mHttp3Session) { + LOG((" no transaction; ignoring event\n")); + return NS_OK; + } + + nsresult rv = OnSocketWritable(); + if (NS_FAILED(rv)) CloseTransaction(mHttp3Session, rv); + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// HttpConnectionUDP::nsITransportEventSink +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +HttpConnectionUDP::OnTransportStatus(nsITransport* trans, nsresult status, + int64_t progress, int64_t progressMax) { + if (mHttp3Session) mHttp3Session->OnTransportStatus(trans, status, progress); + return NS_OK; +} + +//----------------------------------------------------------------------------- +// HttpConnectionUDP::nsIInterfaceRequestor +//----------------------------------------------------------------------------- + +// not called on the socket transport thread +NS_IMETHODIMP +HttpConnectionUDP::GetInterface(const nsIID& iid, void** result) { + // NOTE: This function is only called on the UI thread via sync proxy from + // the socket transport thread. If that weren't the case, then we'd + // have to worry about the possibility of mHttp3Session going away + // part-way through this function call. See CloseTransaction. + + // NOTE - there is a bug here, the call to getinterface is proxied off the + // nss thread, not the ui thread as the above comment says. So there is + // indeed a chance of mSession going away. bug 615342 + + MOZ_ASSERT(!OnSocketThread(), "on socket thread"); + + nsCOMPtr callbacks; + { + MutexAutoLock lock(mCallbacksLock); + callbacks = mCallbacks; + } + if (callbacks) return callbacks->GetInterface(iid, result); + return NS_ERROR_NO_INTERFACE; +} + +void HttpConnectionUDP::SetEvent(nsresult aStatus) { + switch (aStatus) { + case NS_NET_STATUS_RESOLVING_HOST: + mBootstrappedTimings.domainLookupStart = TimeStamp::Now(); + break; + case NS_NET_STATUS_RESOLVED_HOST: + mBootstrappedTimings.domainLookupEnd = TimeStamp::Now(); + break; + case NS_NET_STATUS_CONNECTING_TO: + mBootstrappedTimings.connectStart = TimeStamp::Now(); + break; + case NS_NET_STATUS_CONNECTED_TO: + mBootstrappedTimings.connectEnd = TimeStamp::Now(); + break; + case NS_NET_STATUS_TLS_HANDSHAKE_STARTING: + mBootstrappedTimings.secureConnectionStart = TimeStamp::Now(); + break; + case NS_NET_STATUS_TLS_HANDSHAKE_ENDED: + mBootstrappedTimings.connectEnd = TimeStamp::Now(); + break; + default: + break; + } +} + +bool HttpConnectionUDP::IsProxyConnectInProgress() { return false; } + +bool HttpConnectionUDP::LastTransactionExpectedNoContent() { + return mLastTransactionExpectedNoContent; +} + +void HttpConnectionUDP::SetLastTransactionExpectedNoContent(bool val) { + mLastTransactionExpectedNoContent = val; +} + +bool HttpConnectionUDP::IsPersistent() { return !mDontReuse; } + +nsAHttpTransaction* HttpConnectionUDP::Transaction() { return mHttp3Session; } + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/HttpConnectionUDP.h b/netwerk/protocol/http/HttpConnectionUDP.h new file mode 100644 index 0000000000..fccc0707b5 --- /dev/null +++ b/netwerk/protocol/http/HttpConnectionUDP.h @@ -0,0 +1,131 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef HttpConnectionUDP_h__ +#define HttpConnectionUDP_h__ + +#include "HttpConnectionBase.h" +#include "nsHttpConnectionInfo.h" +#include "nsHttpResponseHead.h" +#include "nsAHttpTransaction.h" +#include "nsCOMPtr.h" +#include "nsProxyRelease.h" +#include "prinrval.h" +#include "TunnelUtils.h" +#include "mozilla/Mutex.h" +#include "ARefBase.h" +#include "TimingStruct.h" +#include "HttpTrafficAnalyzer.h" + +#include "nsIAsyncInputStream.h" +#include "nsIAsyncOutputStream.h" +#include "nsIInterfaceRequestor.h" +#include "nsITimer.h" +#include "Http3Session.h" + +class nsISocketTransport; +class nsISSLSocketControl; + +namespace mozilla { +namespace net { + +class nsHttpHandler; +class ASpdySession; + +// 1dcc863e-db90-4652-a1fe-13fea0b54e46 +#define HTTPCONNECTIONUDP_IID \ + { \ + 0xb97d2036, 0xb441, 0x48be, { \ + 0xb3, 0x1e, 0x25, 0x3e, 0xe8, 0x32, 0xdd, 0x67 \ + } \ + } + +//----------------------------------------------------------------------------- +// HttpConnectionUDP - represents a connection to a HTTP3 server +// +// NOTE: this objects lives on the socket thread only. it should not be +// accessed from any other thread. +//----------------------------------------------------------------------------- + +class HttpConnectionUDP final : public HttpConnectionBase, + public nsAHttpSegmentReader, + public nsAHttpSegmentWriter, + public nsIInputStreamCallback, + public nsIOutputStreamCallback, + public nsITransportEventSink, + public nsIInterfaceRequestor { + private: + virtual ~HttpConnectionUDP(); + + public: + NS_DECLARE_STATIC_IID_ACCESSOR(HTTPCONNECTIONUDP_IID) + NS_DECL_HTTPCONNECTIONBASE + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSAHTTPSEGMENTREADER + NS_DECL_NSAHTTPSEGMENTWRITER + NS_DECL_NSIINPUTSTREAMCALLBACK + NS_DECL_NSIOUTPUTSTREAMCALLBACK + NS_DECL_NSITRANSPORTEVENTSINK + NS_DECL_NSIINTERFACEREQUESTOR + + HttpConnectionUDP(); + + friend class HttpConnectionUDPForceIO; + + [[nodiscard]] static nsresult ReadFromStream(nsIInputStream*, void*, + const char*, uint32_t, uint32_t, + uint32_t*); + + bool UsingHttp3() override { return true; } + + static void OnQuicTimeout(nsITimer* aTimer, void* aClosure); + void OnQuicTimeoutExpired(); + + private: + [[nodiscard]] nsresult OnTransactionDone(nsresult reason); + [[nodiscard]] nsresult OnSocketWritable(); + [[nodiscard]] nsresult OnSocketReadable(); + + private: + nsCOMPtr mSocketIn; + nsCOMPtr mSocketOut; + + RefPtr mHttpHandler; // keep gHttpHandler alive + + PRIntervalTime mLastReadTime; + PRIntervalTime mLastWriteTime; + int64_t mTotalBytesRead; // total data read + + RefPtr mInputOverflow; + + bool mConnectedTransport; + bool mDontReuse; + bool mIsReused; + bool mLastTransactionExpectedNoContent; + + int32_t mPriority; + + private: + // For ForceSend() + static void ForceSendIO(nsITimer* aTimer, void* aClosure); + [[nodiscard]] nsresult MaybeForceSendIO(); + bool mForceSendPending; + nsCOMPtr mForceSendTimer; + + PRIntervalTime mLastRequestBytesSentTime; + + private: + bool mThroughCaptivePortal; + + // Http3 + RefPtr mHttp3Session; +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(HttpConnectionUDP, HTTPCONNECTIONUDP_IID) + +} // namespace net +} // namespace mozilla + +#endif // HttpConnectionUDP_h__ diff --git a/netwerk/protocol/http/HttpInfo.cpp b/netwerk/protocol/http/HttpInfo.cpp new file mode 100644 index 0000000000..b3574cab1c --- /dev/null +++ b/netwerk/protocol/http/HttpInfo.cpp @@ -0,0 +1,16 @@ +/* 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/. */ + +// HttpLog.h should generally be included first +#include "HttpLog.h" + +#include "nsHttpHandler.h" +#include "HttpInfo.h" + +void mozilla::net::HttpInfo::GetHttpConnectionData( + nsTArray* args) { + if (gHttpHandler && gHttpHandler->ConnMgr()) { + gHttpHandler->ConnMgr()->GetConnectionData(args); + } +} diff --git a/netwerk/protocol/http/HttpInfo.h b/netwerk/protocol/http/HttpInfo.h new file mode 100644 index 0000000000..981a3df56b --- /dev/null +++ b/netwerk/protocol/http/HttpInfo.h @@ -0,0 +1,24 @@ +/* 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/. */ + +#ifndef nsHttpInfo__ +#define nsHttpInfo__ + +#include "nsTArrayForwardDeclare.h" + +namespace mozilla { +namespace net { + +struct HttpRetParams; + +class HttpInfo { + public: + /* Calls getConnectionData method in nsHttpConnectionMgr. */ + static void GetHttpConnectionData(nsTArray*); +}; + +} // namespace net +} // namespace mozilla + +#endif // nsHttpInfo__ diff --git a/netwerk/protocol/http/HttpLog.h b/netwerk/protocol/http/HttpLog.h new file mode 100644 index 0000000000..0cb05e6afd --- /dev/null +++ b/netwerk/protocol/http/HttpLog.h @@ -0,0 +1,70 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=4 sw=2 sts=2 et cin: */ +/* 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/. */ + +#ifndef HttpLog_h__ +#define HttpLog_h__ + +/******************************************************************************* + * This file should ONLY be #included by source (.cpp) files in the /http + * directory, not headers (.h). If you need to use LOG() in a .h file, call + * PR_LOG directly. + * + * This file should also be the first #include in your file. + * + * Yes, this is kludgy. + *******************************************************************************/ + +#include "mozilla/net/NeckoChild.h" + +// Get rid of Chromium's LOG definition +#undef LOG + +// +// Log module for HTTP Protocol logging... +// +// To enable logging (see prlog.h for full details): +// +// set MOZ_LOG=nsHttp:5 +// set MOZ_LOG_FILE=http.log +// +// This enables LogLevel::Debug level information and places all output in +// the file http.log. +// +namespace mozilla { +namespace net { +void LogCallingScriptLocation(void* instance); +extern LazyLogModule gHttpLog; +} // namespace net +} // namespace mozilla + +// http logging +#define LOG1(args) \ + MOZ_LOG(mozilla::net::gHttpLog, mozilla::LogLevel::Error, args) +#define LOG2(args) \ + MOZ_LOG(mozilla::net::gHttpLog, mozilla::LogLevel::Warning, args) +#define LOG3(args) \ + MOZ_LOG(mozilla::net::gHttpLog, mozilla::LogLevel::Info, args) +#define LOG4(args) \ + MOZ_LOG(mozilla::net::gHttpLog, mozilla::LogLevel::Debug, args) +#define LOG5(args) \ + MOZ_LOG(mozilla::net::gHttpLog, mozilla::LogLevel::Verbose, args) +#define LOG(args) LOG4(args) +#define LOGTIME(start, args) \ + MOZ_LOG_TIME(mozilla::net::gHttpLog, mozilla::LogLevel::Debug, &start, args) + +#define LOG1_ENABLED() \ + MOZ_LOG_TEST(mozilla::net::gHttpLog, mozilla::LogLevel::Error) +#define LOG2_ENABLED() \ + MOZ_LOG_TEST(mozilla::net::gHttpLog, mozilla::LogLevel::Warning) +#define LOG3_ENABLED() \ + MOZ_LOG_TEST(mozilla::net::gHttpLog, mozilla::LogLevel::Info) +#define LOG4_ENABLED() \ + MOZ_LOG_TEST(mozilla::net::gHttpLog, mozilla::LogLevel::Debug) +#define LOG5_ENABLED() \ + MOZ_LOG_TEST(mozilla::net::gHttpLog, mozilla::LogLevel::Verbose) +#define LOG_ENABLED() LOG4_ENABLED() + +#endif // HttpLog_h__ diff --git a/netwerk/protocol/http/HttpTrafficAnalyzer.cpp b/netwerk/protocol/http/HttpTrafficAnalyzer.cpp new file mode 100644 index 0000000000..4fdabef14a --- /dev/null +++ b/netwerk/protocol/http/HttpTrafficAnalyzer.cpp @@ -0,0 +1,269 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 "HttpTrafficAnalyzer.h" +#include "HttpLog.h" + +#include "mozilla/StaticPrefs_network.h" +#include "mozilla/Telemetry.h" +#include "mozilla/Unused.h" +#include "nsSocketTransportService2.h" + +namespace mozilla { +namespace net { + +constexpr auto kInvalidCategory = "INVALID_CATEGORY"_ns; + +#define DEFINE_CATEGORY(_name, _idx) nsLiteralCString("Y" #_idx "_" #_name), +static const nsCString gKeyName[] = { +#include "HttpTrafficAnalyzer.inc" + kInvalidCategory, +}; +#undef DEFINE_CATEGORY + +#define DEFINE_CATEGORY(_name, _idx) \ + Telemetry::LABELS_HTTP_TRAFFIC_ANALYSIS_3::Y##_idx##_##_name, +static const Telemetry::LABELS_HTTP_TRAFFIC_ANALYSIS_3 gTelemetryLabel[] = { +#include "HttpTrafficAnalyzer.inc" +}; +#undef DEFINE_CATEGORY + +// ---------------------------------------------------- +// | Flags | Load Type | +// ---------------------------------------------------- +// | nsIClassOfService::Leader | A | +// | w/o nsIRequest::LOAD_BACKGROUND | B | +// | w/ nsIRequest::LOAD_BACKGROUND | C | +// ---------------------------------------------------- +// | Category | List Category | +// ---------------------------------------------------- +// | Basic Disconnected List | I | +// | Content | II | +// | Fingerprinting | III | +// ---------------------------------------------------- +// ==================================================== +// | Normal Mode | +// ---------------------------------------------------- +// | Y = 0 for system principals | +// | Y = 1 for first party | +// | Y = 2 for non-listed third party type | +// ---------------------------------------------------- +// | \Y\ | Type A | Type B | Type C | +// ---------------------------------------------------- +// | Category I | 3 | 4 | 5 | +// | Category II | 6 | 7 | 8 | +// | Category III | 9 | 10 | 11 | +// ==================================================== +// | Private Mode | +// ---------------------------------------------------- +// | Y = 12 for system principals | +// | Y = 13 for first party | +// | Y = 14 for non-listed third party type | +// ---------------------------------------------------- +// | \Y\ | Type A | Type B | Type C | +// ---------------------------------------------------- +// | Category I | 15 | 16 | 17 | +// | Category II | 18 | 19 | 20 | +// | Category III | 21 | 22 | 23 | +// ==================================================== + +HttpTrafficCategory HttpTrafficAnalyzer::CreateTrafficCategory( + bool aIsPrivateMode, bool aIsSystemPrincipal, bool aIsThirdParty, + ClassOfService aClassOfService, TrackingClassification aClassification) { + uint8_t category = aIsPrivateMode ? 12 : 0; + if (aIsSystemPrincipal) { + MOZ_ASSERT_IF(!aIsPrivateMode, + gKeyName[category].EqualsLiteral("Y0_N1Sys")); + MOZ_ASSERT_IF(aIsPrivateMode, + gKeyName[category].EqualsLiteral("Y12_P1Sys")); + return static_cast(category); + } + ++category; + + if (!aIsThirdParty) { + MOZ_ASSERT_IF(!aIsPrivateMode, gKeyName[category].EqualsLiteral("Y1_N1")); + MOZ_ASSERT_IF(aIsPrivateMode, gKeyName[category].EqualsLiteral("Y13_P1")); + return static_cast(category); + } + + switch (aClassification) { + case TrackingClassification::eNone: + ++category; + MOZ_ASSERT_IF(!aIsPrivateMode, + gKeyName[category].EqualsLiteral("Y2_N3Oth")); + MOZ_ASSERT_IF(aIsPrivateMode, + gKeyName[category].EqualsLiteral("Y14_P3Oth")); + return static_cast(category); + case TrackingClassification::eBasic: + category += 2; + break; + case TrackingClassification::eContent: + category += 5; + break; + case TrackingClassification::eFingerprinting: + category += 8; + break; + default: + MOZ_ASSERT(false, "incorrect classification"); + return HttpTrafficCategory::eInvalid; + } + + switch (aClassOfService) { + case ClassOfService::eLeader: + MOZ_ASSERT_IF( + !aIsPrivateMode, + (aClassification == TrackingClassification::eBasic && + gKeyName[category].EqualsLiteral("Y3_N3BasicLead")) || + (aClassification == TrackingClassification::eContent && + gKeyName[category].EqualsLiteral("Y6_N3ContentLead")) || + (aClassification == TrackingClassification::eFingerprinting && + gKeyName[category].EqualsLiteral("Y9_N3FpLead"))); + MOZ_ASSERT_IF( + aIsPrivateMode, + (aClassification == TrackingClassification::eBasic && + gKeyName[category].EqualsLiteral("Y15_P3BasicLead")) || + (aClassification == TrackingClassification::eContent && + gKeyName[category].EqualsLiteral("Y18_P3ContentLead")) || + (aClassification == TrackingClassification::eFingerprinting && + gKeyName[category].EqualsLiteral("Y21_P3FpLead"))); + return static_cast(category); + case ClassOfService::eBackground: + ++category; + + MOZ_ASSERT_IF( + !aIsPrivateMode, + (aClassification == TrackingClassification::eBasic && + gKeyName[category].EqualsLiteral("Y4_N3BasicBg")) || + (aClassification == TrackingClassification::eContent && + gKeyName[category].EqualsLiteral("Y7_N3ContentBg")) || + (aClassification == TrackingClassification::eFingerprinting && + gKeyName[category].EqualsLiteral("Y10_N3FpBg"))); + MOZ_ASSERT_IF( + aIsPrivateMode, + (aClassification == TrackingClassification::eBasic && + gKeyName[category].EqualsLiteral("Y16_P3BasicBg")) || + (aClassification == TrackingClassification::eContent && + gKeyName[category].EqualsLiteral("Y19_P3ContentBg")) || + (aClassification == TrackingClassification::eFingerprinting && + gKeyName[category].EqualsLiteral("Y22_P3FpBg"))); + + return static_cast(category); + case ClassOfService::eOther: + category += 2; + + MOZ_ASSERT_IF( + !aIsPrivateMode, + (aClassification == TrackingClassification::eBasic && + gKeyName[category].EqualsLiteral("Y5_N3BasicOth")) || + (aClassification == TrackingClassification::eContent && + gKeyName[category].EqualsLiteral("Y8_N3ContentOth")) || + (aClassification == TrackingClassification::eFingerprinting && + gKeyName[category].EqualsLiteral("Y11_N3FpOth"))); + MOZ_ASSERT_IF( + aIsPrivateMode, + (aClassification == TrackingClassification::eBasic && + gKeyName[category].EqualsLiteral("Y17_P3BasicOth")) || + (aClassification == TrackingClassification::eContent && + gKeyName[category].EqualsLiteral("Y20_P3ContentOth")) || + (aClassification == TrackingClassification::eFingerprinting && + gKeyName[category].EqualsLiteral("Y23_P3FpOth"))); + + return static_cast(category); + } + + MOZ_ASSERT(false, "incorrect class of service"); + return HttpTrafficCategory::eInvalid; +} + +void HttpTrafficAnalyzer::IncrementHttpTransaction( + HttpTrafficCategory aCategory) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + MOZ_ASSERT(StaticPrefs::network_traffic_analyzer_enabled()); + MOZ_ASSERT(aCategory != HttpTrafficCategory::eInvalid, "invalid category"); + + LOG(("HttpTrafficAnalyzer::IncrementHttpTransaction [%s] [this=%p]\n", + gKeyName[aCategory].get(), this)); + + Telemetry::AccumulateCategoricalKeyed("Transaction"_ns, + gTelemetryLabel[aCategory]); +} + +void HttpTrafficAnalyzer::IncrementHttpConnection( + HttpTrafficCategory aCategory) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + MOZ_ASSERT(StaticPrefs::network_traffic_analyzer_enabled()); + MOZ_ASSERT(aCategory != HttpTrafficCategory::eInvalid, "invalid category"); + + LOG(("HttpTrafficAnalyzer::IncrementHttpConnection [%s] [this=%p]\n", + gKeyName[aCategory].get(), this)); + + Telemetry::AccumulateCategoricalKeyed("Connection"_ns, + gTelemetryLabel[aCategory]); +} + +void HttpTrafficAnalyzer::IncrementHttpConnection( + nsTArray&& aCategories) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + MOZ_ASSERT(StaticPrefs::network_traffic_analyzer_enabled()); + MOZ_ASSERT(!aCategories.IsEmpty(), "empty category"); + + nsTArray categories(std::move(aCategories)); + + LOG(("HttpTrafficAnalyzer::IncrementHttpConnection size=%" PRIuPTR + " [this=%p]\n", + categories.Length(), this)); + + // divide categories into 4 parts: + // 1) normal 1st-party (Y in {0, 1}) + // 2) normal 3rd-party (1 < Y < 12) + // 3) private 1st-party (Y in {12, 13}) + // 4) private 3rd-party (13 < Y < 24) + // Normal and private transaction should not share the same connection, + // and we choose 3rd-party prior than 1st-party. + HttpTrafficCategory best = categories[0]; + for (auto category : categories) { + MOZ_ASSERT(category != HttpTrafficCategory::eInvalid, "invalid category"); + + if (category == 0 || category == 1 || category == 12 || category == 13) { + // first party + MOZ_ASSERT(gKeyName[category].EqualsLiteral("Y0_N1Sys") || + gKeyName[category].EqualsLiteral("Y1_N1") || + gKeyName[category].EqualsLiteral("Y12_P1Sys") || + gKeyName[category].EqualsLiteral("Y13_P1")); + continue; + } + // third party + MOZ_ASSERT(gKeyName[24].Equals(kInvalidCategory), + "category definition isn't consistent"); + best = category; + break; + } + + IncrementHttpConnection(best); +} + +#define CLAMP_U32(num) \ + Clamp(num, 0, std::numeric_limits::max()) + +void HttpTrafficAnalyzer::AccumulateHttpTransferredSize( + HttpTrafficCategory aCategory, uint64_t aBytesRead, uint64_t aBytesSent) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + MOZ_ASSERT(StaticPrefs::network_traffic_analyzer_enabled()); + MOZ_ASSERT(aCategory != HttpTrafficCategory::eInvalid, "invalid category"); + + LOG(("HttpTrafficAnalyzer::AccumulateHttpTransferredSize [%s] rb=%" PRIu64 " " + "sb=%" PRIu64 " [this=%p]\n", + gKeyName[aCategory].get(), aBytesRead, aBytesSent, this)); + + // Telemetry supports uint32_t only, and we send KB here. + auto total = CLAMP_U32((aBytesRead >> 10) + (aBytesSent >> 10)); + if (aBytesRead || aBytesSent) { + Telemetry::ScalarAdd(Telemetry::ScalarID::NETWORKING_DATA_TRANSFERRED_V3_KB, + NS_ConvertUTF8toUTF16(gKeyName[aCategory]), total); + } +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/HttpTrafficAnalyzer.h b/netwerk/protocol/http/HttpTrafficAnalyzer.h new file mode 100644 index 0000000000..e319a0697e --- /dev/null +++ b/netwerk/protocol/http/HttpTrafficAnalyzer.h @@ -0,0 +1,51 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef mozilla_netwerk_protocol_http_HttpTrafficAnalyzer_h +#define mozilla_netwerk_protocol_http_HttpTrafficAnalyzer_h + +#include +#include "nsTArrayForwardDeclare.h" + +namespace mozilla { +namespace net { + +#define DEFINE_CATEGORY(_name, _idx) e##_name = _idx##u, +enum HttpTrafficCategory : uint8_t { +#include "HttpTrafficAnalyzer.inc" + eInvalid, +}; +#undef DEFINE_CATEGORY + +class HttpTrafficAnalyzer final { + public: + enum ClassOfService : uint8_t { + eLeader = 0, + eBackground = 1, + eOther = 255, + }; + + enum TrackingClassification : uint8_t { + eNone = 0, + eBasic = 1, + eContent = 2, + eFingerprinting = 3, + }; + + static HttpTrafficCategory CreateTrafficCategory( + bool aIsPrivateMode, bool aIsSystemPrincipal, bool aIsThirdParty, + ClassOfService aClassOfService, TrackingClassification aClassification); + + void IncrementHttpTransaction(HttpTrafficCategory aCategory); + void IncrementHttpConnection(HttpTrafficCategory aCategory); + void IncrementHttpConnection(nsTArray&& aCategories); + void AccumulateHttpTransferredSize(HttpTrafficCategory aCategory, + uint64_t aBytesRead, uint64_t aBytesSent); +}; + +} // namespace net +} // namespace mozilla + +#endif // mozilla_netwerk_protocol_http_HttpTrafficAnalyzer_h diff --git a/netwerk/protocol/http/HttpTrafficAnalyzer.inc b/netwerk/protocol/http/HttpTrafficAnalyzer.inc new file mode 100644 index 0000000000..3311d33ee8 --- /dev/null +++ b/netwerk/protocol/http/HttpTrafficAnalyzer.inc @@ -0,0 +1,106 @@ +// Type A) Parser blocking script loads will have an mContentPolicyType +// indicating a script load and a nsIClassOfService::Leader class of service. +// Type B) I couldn’t find a class flag that corresponds to synchronous loads +// that block the load event, but I think a simple way to determine +// which ones they are is to examine whether the channel belongs to a +// load group and do not have a LOAD_BACKGROUND load flag. +// If that condition holds, then it is blocking the load event of some document. +// Type C) I think a simple way to find channels that don’t block document loads +// (e.g. XHR and fetch) is to look for those which are in a load group +// and have a LOAD_BACKGROUND load flag. + + +// Y=0 - all system principal connections. +DEFINE_CATEGORY(N1Sys, 0) + +// Y=1 - all requests/connections/bytes that are first party. +DEFINE_CATEGORY(N1, 1) + +// Y=2 - all requests/connections/bytes that are third party +// but don’t fall into other categories +DEFINE_CATEGORY(N3Oth, 2) + +// Y=3 - all requests/connections/bytes associated with third party loads that +// match the Analytics/Social/Advertising (Basic) Category and have a load of type (A) +DEFINE_CATEGORY(N3BasicLead, 3) + +// Y=4 - all requests/connections/bytes associated with third party loads that +// match the Analytics/Social/Advertising (Basic) Category and have a load of type (B) +DEFINE_CATEGORY(N3BasicBg, 4) + +// Y=5 - all requests/connections/bytes associated with third party loads that +// match the Analytics/Social/Advertising (Basic) Category and have a load of type (C) +DEFINE_CATEGORY(N3BasicOth, 5) + +// Y=6 - all requests/connections/bytes associated with third party loads that +// match the Content Category and have a load of type (A) +DEFINE_CATEGORY(N3ContentLead, 6) + +// Y=7 - all requests/connections/bytes associated with third party loads that +// match the Content Category and have a load of type (B) +DEFINE_CATEGORY(N3ContentBg, 7) + +// Y=8 - all requests/connections/bytes associated with third party loads that +// match the Content Category and have a load of type (C) +DEFINE_CATEGORY(N3ContentOth, 8) + +// Y=9 - all requests/connections/bytes associated with third party loads that +// match the Fingerprinting Category and have a load of type (A) +DEFINE_CATEGORY(N3FpLead, 9) + +// Y=10 - all requests/connections/bytes associated with third party loads that +// match the Fingerprinting Category and have a load of type (B) +DEFINE_CATEGORY(N3FpBg, 10) + +// Y=11 - all requests/connections/bytes associated with third party loads that +// match the Fingerprinting Category and have a load of type (C) +DEFINE_CATEGORY(N3FpOth, 11) + +// Y=12 - private mode system principal connections. +DEFINE_CATEGORY(P1Sys, 12) + +// Y=13 - private mode and all requests/connections/bytes that are first party. +DEFINE_CATEGORY(P1, 13) + +// Y=14 - private mode and all requests/connections/bytes that are third party +// but don’t fall into other categories +DEFINE_CATEGORY(P3Oth, 14) + +// Y=15 - private mode and all requests/connections/bytes associated with +// third party loads that match the Analytics/Social/Advertising (Basic) Category +// and have a load of type (A) +DEFINE_CATEGORY(P3BasicLead, 15) + +// Y=16 - private mode and all requests/connections/bytes associated with +// third party loads that match the Analytics/Social/Advertising (Basic) Category +// and have a load of type (B) +DEFINE_CATEGORY(P3BasicBg, 16) + +// Y=17 - private mode and all requests/connections/bytes associated with +// third party loads that match the Analytics/Social/Advertising (Basic) Category +// and have a load of type (C) +DEFINE_CATEGORY(P3BasicOth, 17) + +// Y=18 - private mode and all requests/connections/bytes associated with +// third party loads that match the Content Category and have a load of type (A) +DEFINE_CATEGORY(P3ContentLead, 18) + +// Y=19 - private mode and all requests/connections/bytes associated with +// third party loads that match the Content Category and have a load of type (B) +DEFINE_CATEGORY(P3ContentBg, 19) + +// Y=20 - private mode and all requests/connections/bytes associated with +// third party loads that match the Content Category and have a load of type (C) +DEFINE_CATEGORY(P3ContentOth, 20) + +// Y=21 - private mode and all requests/connections/bytes associated with +// third party loads that match the Fingerprinting Category and have a load of type (A) +DEFINE_CATEGORY(P3FpLead, 21) + +// Y=22 - private mode and all requests/connections/bytes associated with +// third party loads that match the Fingerprinting Category and have a load of type (B) +DEFINE_CATEGORY(P3FpBg, 22) + +// Y=23 - private mode and all requests/connections/bytes associated with +// third party loads that match the Fingerprinting Category and have a load of type (C) +DEFINE_CATEGORY(P3FpOth, 23) diff --git a/netwerk/protocol/http/HttpTransactionChild.cpp b/netwerk/protocol/http/HttpTransactionChild.cpp new file mode 100644 index 0000000000..f2ee2cdff7 --- /dev/null +++ b/netwerk/protocol/http/HttpTransactionChild.cpp @@ -0,0 +1,646 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* vim:set ts=4 sw=4 sts=4 et cin: */ +/* 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/. */ + +// HttpLog.h should generally be included first +#include "HttpLog.h" + +#include "HttpTransactionChild.h" + +#include "mozilla/ipc/IPCStreamUtils.h" +#include "mozilla/ipc/BackgroundParent.h" +#include "mozilla/net/BackgroundDataBridgeParent.h" +#include "mozilla/net/ChannelEventQueue.h" +#include "mozilla/net/InputChannelThrottleQueueChild.h" +#include "mozilla/net/SocketProcessChild.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/StaticPrefs_network.h" +#include "nsInputStreamPump.h" +#include "nsITransportSecurityInfo.h" +#include "nsHttpHandler.h" +#include "nsNetUtil.h" +#include "nsProxyInfo.h" +#include "nsProxyRelease.h" +#include "nsQueryObject.h" +#include "nsSerializationHelper.h" + +using mozilla::ipc::BackgroundParent; + +namespace mozilla { +namespace net { + +NS_IMPL_ISUPPORTS(HttpTransactionChild, nsIRequestObserver, nsIStreamListener, + nsITransportEventSink, nsIThrottledInputChannel, + nsIThreadRetargetableStreamListener); + +//----------------------------------------------------------------------------- +// HttpTransactionChild +//----------------------------------------------------------------------------- + +HttpTransactionChild::HttpTransactionChild() + : mCanceled(false), + mStatus(NS_OK), + mChannelId(0), + mIsDocumentLoad(false), + mLogicalOffset(0) { + LOG(("Creating HttpTransactionChild @%p\n", this)); +} + +HttpTransactionChild::~HttpTransactionChild() { + LOG(("Destroying HttpTransactionChild @%p\n", this)); +} + +static already_AddRefed CreateRequestContext( + uint64_t aRequestContextID) { + if (!aRequestContextID) { + return nullptr; + } + + nsIRequestContextService* rcsvc = gHttpHandler->GetRequestContextService(); + if (!rcsvc) { + return nullptr; + } + + nsCOMPtr requestContext; + rcsvc->GetRequestContext(aRequestContextID, getter_AddRefs(requestContext)); + + return requestContext.forget(); +} + +nsresult HttpTransactionChild::InitInternal( + uint32_t caps, const HttpConnectionInfoCloneArgs& infoArgs, + nsHttpRequestHead* requestHead, nsIInputStream* requestBody, + uint64_t requestContentLength, bool requestBodyHasHeaders, + uint64_t topLevelOuterContentWindowId, uint8_t httpTrafficCategory, + uint64_t requestContextID, uint32_t classOfService, uint32_t initialRwin, + bool responseTimeoutEnabled, uint64_t channelId, + bool aHasTransactionObserver, + const Maybe& aPushedStreamArg) { + LOG(("HttpTransactionChild::InitInternal [this=%p caps=%x]\n", this, caps)); + + RefPtr cinfo = + nsHttpConnectionInfo::DeserializeHttpConnectionInfoCloneArgs(infoArgs); + nsCOMPtr rc = CreateRequestContext(requestContextID); + + HttpTransactionShell::OnPushCallback pushCallback = nullptr; + if (caps & NS_HTTP_ONPUSH_LISTENER) { + RefPtr self = this; + pushCallback = [self](uint32_t aPushedStreamId, const nsACString& aUrl, + const nsACString& aRequestString, + HttpTransactionShell* aTransaction) { + bool res = false; + if (self->CanSend()) { + res = + self->SendOnH2PushStream(aPushedStreamId, PromiseFlatCString(aUrl), + PromiseFlatCString(aRequestString)); + } + return res ? NS_OK : NS_ERROR_FAILURE; + }; + } + + std::function observer; + if (aHasTransactionObserver) { + nsMainThreadPtrHandle handle( + new nsMainThreadPtrHolder( + "HttpTransactionChildProxy", this, false)); + observer = [handle](TransactionObserverResult&& aResult) { + handle->mTransactionObserverResult.emplace(std::move(aResult)); + }; + } + + RefPtr transWithPushedStream; + uint32_t pushedStreamId = 0; + if (aPushedStreamArg) { + HttpTransactionChild* transChild = static_cast( + aPushedStreamArg.ref().transWithPushedStreamChild()); + transWithPushedStream = transChild->GetHttpTransaction(); + pushedStreamId = aPushedStreamArg.ref().pushedStreamId(); + } + + nsresult rv = mTransaction->Init( + caps, cinfo, requestHead, requestBody, requestContentLength, + requestBodyHasHeaders, GetCurrentEventTarget(), + nullptr, // TODO: security callback, fix in bug 1512479. + this, topLevelOuterContentWindowId, + static_cast(httpTrafficCategory), rc, classOfService, + initialRwin, responseTimeoutEnabled, channelId, std::move(observer), + std::move(pushCallback), transWithPushedStream, pushedStreamId); + if (NS_WARN_IF(NS_FAILED(rv))) { + mTransaction = nullptr; + return rv; + } + + Unused << mTransaction->AsyncRead(this, getter_AddRefs(mTransactionPump)); + return rv; +} + +mozilla::ipc::IPCResult HttpTransactionChild::RecvCancelPump( + const nsresult& aStatus) { + LOG(("HttpTransactionChild::RecvCancelPump start [this=%p]\n", this)); + CancelInternal(aStatus); + return IPC_OK(); +} + +void HttpTransactionChild::CancelInternal(nsresult aStatus) { + MOZ_ASSERT(NS_FAILED(aStatus)); + + mCanceled = true; + mStatus = aStatus; + if (mTransactionPump) { + mTransactionPump->Cancel(mStatus); + } +} + +mozilla::ipc::IPCResult HttpTransactionChild::RecvSuspendPump() { + LOG(("HttpTransactionChild::RecvSuspendPump start [this=%p]\n", this)); + + if (mTransactionPump) { + mTransactionPump->Suspend(); + } + return IPC_OK(); +} + +mozilla::ipc::IPCResult HttpTransactionChild::RecvResumePump() { + LOG(("HttpTransactionChild::RecvResumePump start [this=%p]\n", this)); + + if (mTransactionPump) { + mTransactionPump->Resume(); + } + return IPC_OK(); +} + +mozilla::ipc::IPCResult HttpTransactionChild::RecvInit( + const uint32_t& aCaps, const HttpConnectionInfoCloneArgs& aArgs, + const nsHttpRequestHead& aReqHeaders, const Maybe& aRequestBody, + const uint64_t& aReqContentLength, const bool& aReqBodyIncludesHeaders, + const uint64_t& aTopLevelOuterContentWindowId, + const uint8_t& aHttpTrafficCategory, const uint64_t& aRequestContextID, + const uint32_t& aClassOfService, const uint32_t& aInitialRwin, + const bool& aResponseTimeoutEnabled, const uint64_t& aChannelId, + const bool& aHasTransactionObserver, + const Maybe& aPushedStreamArg, + const mozilla::Maybe& aThrottleQueue, + const bool& aIsDocumentLoad, const TimeStamp& aRedirectStart, + const TimeStamp& aRedirectEnd) { + mRequestHead = aReqHeaders; + if (aRequestBody) { + mUploadStream = mozilla::ipc::DeserializeIPCStream(aRequestBody); + } + + mTransaction = new nsHttpTransaction(); + mChannelId = aChannelId; + mIsDocumentLoad = aIsDocumentLoad; + mRedirectStart = aRedirectStart; + mRedirectEnd = aRedirectEnd; + + if (aThrottleQueue.isSome()) { + mThrottleQueue = + static_cast(aThrottleQueue.ref()); + } + + nsresult rv = InitInternal( + aCaps, aArgs, &mRequestHead, mUploadStream, aReqContentLength, + aReqBodyIncludesHeaders, aTopLevelOuterContentWindowId, + aHttpTrafficCategory, aRequestContextID, aClassOfService, aInitialRwin, + aResponseTimeoutEnabled, aChannelId, aHasTransactionObserver, + aPushedStreamArg); + if (NS_FAILED(rv)) { + LOG(("HttpTransactionChild::RecvInit: [this=%p] InitInternal failed!\n", + this)); + mTransaction = nullptr; + SendOnInitFailed(rv); + } + return IPC_OK(); +} + +mozilla::ipc::IPCResult HttpTransactionChild::RecvSetDNSWasRefreshed() { + LOG(("HttpTransactionChild::SetDNSWasRefreshed [this=%p]\n", this)); + if (mTransaction) { + mTransaction->SetDNSWasRefreshed(); + } + return IPC_OK(); +} + +mozilla::ipc::IPCResult HttpTransactionChild::RecvDontReuseConnection() { + LOG(("HttpTransactionChild::RecvDontReuseConnection [this=%p]\n", this)); + if (mTransaction) { + mTransaction->DontReuseConnection(); + } + return IPC_OK(); +} + +mozilla::ipc::IPCResult HttpTransactionChild::RecvSetH2WSConnRefTaken() { + LOG(("HttpTransactionChild::RecvSetH2WSConnRefTaken [this=%p]\n", this)); + if (mTransaction) { + mTransaction->SetH2WSConnRefTaken(); + } + return IPC_OK(); +} + +void HttpTransactionChild::ActorDestroy(ActorDestroyReason aWhy) { + LOG(("HttpTransactionChild::ActorDestroy [this=%p]\n", this)); + mTransaction = nullptr; + mTransactionPump = nullptr; +} + +nsHttpTransaction* HttpTransactionChild::GetHttpTransaction() { + return mTransaction.get(); +} + +//----------------------------------------------------------------------------- +// HttpTransactionChild +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +HttpTransactionChild::OnDataAvailable(nsIRequest* aRequest, + nsIInputStream* aInputStream, + uint64_t aOffset, uint32_t aCount) { + LOG(("HttpTransactionChild::OnDataAvailable [this=%p, aOffset= %" PRIu64 + " aCount=%" PRIu32 "]\n", + this, aOffset, aCount)); + + // Don't bother sending IPC if already canceled. + if (mCanceled) { + return mStatus; + } + + // TODO: send string data in chunks and handle errors. Bug 1600129. + nsCString data; + nsresult rv = NS_ReadInputStreamToString(aInputStream, data, aCount); + if (NS_FAILED(rv)) { + return rv; + } + + mLogicalOffset += aCount; + + if (NS_IsMainThread()) { + if (!CanSend()) { + return NS_ERROR_FAILURE; + } + + nsHttp::SendFunc sendFunc = + [self = UnsafePtr(this)]( + const nsCString& aData, uint64_t aOffset, uint32_t aCount) { + return self->SendOnDataAvailable(aData, aOffset, aCount); + }; + + LOG((" ODA to parent process")); + if (!nsHttp::SendDataInChunks(data, aOffset, aCount, sendFunc)) { + return NS_ERROR_FAILURE; + } + return NS_OK; + } + + ipc::AssertIsOnBackgroundThread(); + MOZ_ASSERT(mDataBridgeParent); + + if (!mDataBridgeParent->CanSend()) { + return NS_ERROR_FAILURE; + } + + nsHttp::SendFunc sendFunc = + [self = UnsafePtr(this)]( + const nsDependentCSubstring& aData, uint64_t aOffset, + uint32_t aCount) { + return self->mDataBridgeParent->SendOnTransportAndData(aOffset, aCount, + aData); + }; + + LOG((" ODA to content process")); + if (!nsHttp::SendDataInChunks(data, aOffset, aCount, sendFunc)) { + MOZ_ASSERT(false, "Send ODA to content process failed"); + return NS_ERROR_FAILURE; + } + + // We still need to send ODA to parent process, because the data needs to be + // saved in cache. Note that we set dataSentToChildProcess to true, to this + // ODA will not be sent to child process. + RefPtr self = this; + rv = NS_DispatchToMainThread( + NS_NewRunnableFunction( + "HttpTransactionChild::OnDataAvailable", + [self, offset(aOffset), count(aCount), data(data)]() { + nsHttp::SendFunc sendFunc = + [self](const nsCString& aData, uint64_t aOffset, + uint32_t aCount) { + return self->SendOnDataAvailable(aData, aOffset, aCount); + }; + + if (!nsHttp::SendDataInChunks(data, offset, count, sendFunc)) { + self->CancelInternal(NS_ERROR_FAILURE); + } + }), + NS_DISPATCH_NORMAL); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + return NS_OK; +} + +static TimingStructArgs ToTimingStructArgs(TimingStruct aTiming) { + TimingStructArgs args; + args.domainLookupStart() = aTiming.domainLookupStart; + args.domainLookupEnd() = aTiming.domainLookupEnd; + args.connectStart() = aTiming.connectStart; + args.tcpConnectEnd() = aTiming.tcpConnectEnd; + args.secureConnectionStart() = aTiming.secureConnectionStart; + args.connectEnd() = aTiming.connectEnd; + args.requestStart() = aTiming.requestStart; + args.responseStart() = aTiming.responseStart; + args.responseEnd() = aTiming.responseEnd; + return args; +} + +// The maximum number of bytes to consider when attempting to sniff. +// See https://mimesniff.spec.whatwg.org/#reading-the-resource-header. +static const uint32_t MAX_BYTES_SNIFFED = 1445; + +static void GetDataForSniffer(void* aClosure, const uint8_t* aData, + uint32_t aCount) { + nsTArray* outData = static_cast*>(aClosure); + outData->AppendElements(aData, std::min(aCount, MAX_BYTES_SNIFFED)); +} + +bool HttpTransactionChild::CanSendODAToContentProcessDirectly( + const Maybe& aHead) { + if (!StaticPrefs::network_send_ODA_to_content_directly()) { + return false; + } + + // If this is a document load, the content process that receives ODA is not + // decided yet, so don't bother to do the rest check. + if (mIsDocumentLoad) { + return false; + } + + if (!aHead) { + return false; + } + + // We only need to deliver ODA when the response is succeed. + if (aHead->Status() != 200) { + return false; + } + + // UnknownDecoder could be used in parent process, so we can't send ODA to + // content process. + if (!aHead->HasContentType()) { + return false; + } + + return true; +} + +NS_IMETHODIMP +HttpTransactionChild::OnStartRequest(nsIRequest* aRequest) { + LOG(("HttpTransactionChild::OnStartRequest start [this=%p] mTransaction=%p\n", + this, mTransaction.get())); + + // Don't bother sending IPC to parent process if already canceled. + if (mCanceled) { + return mStatus; + } + + if (!CanSend()) { + return NS_ERROR_FAILURE; + } + + MOZ_ASSERT(mTransaction); + + nsresult status; + aRequest->GetStatus(&status); + + mProtocolVersion.Truncate(); + + nsCString serializedSecurityInfoOut; + nsCOMPtr secInfoSupp = mTransaction->SecurityInfo(); + if (secInfoSupp) { + nsCOMPtr info = do_QueryInterface(secInfoSupp); + nsAutoCString protocol; + if (info && NS_SUCCEEDED(info->GetNegotiatedNPN(protocol)) && + !protocol.IsEmpty()) { + mProtocolVersion.Assign(protocol); + } + nsCOMPtr secInfoSer = do_QueryInterface(secInfoSupp); + if (secInfoSer) { + NS_SerializeToString(secInfoSer, serializedSecurityInfoOut); + } + } + + UniquePtr head(mTransaction->TakeResponseHead()); + Maybe optionalHead; + nsTArray dataForSniffer; + if (head) { + if (mProtocolVersion.IsEmpty()) { + HttpVersion version = head->Version(); + mProtocolVersion.Assign(nsHttp::GetProtocolVersion(version)); + } + optionalHead = Some(*head); + if (mTransaction->Caps() & NS_HTTP_CALL_CONTENT_SNIFFER) { + nsAutoCString contentTypeOptionsHeader; + if (!(head->GetContentTypeOptionsHeader(contentTypeOptionsHeader) && + contentTypeOptionsHeader.EqualsIgnoreCase("nosniff"))) { + RefPtr pump = do_QueryObject(mTransactionPump); + pump->PeekStream(GetDataForSniffer, &dataForSniffer); + } + } + } + + Maybe optionalAltSvcUsed; + nsCString altSvcUsed; + if (NS_SUCCEEDED(mTransaction->RequestHead()->GetHeader( + nsHttp::Alternate_Service_Used, altSvcUsed)) && + !altSvcUsed.IsEmpty()) { + optionalAltSvcUsed.emplace(altSvcUsed); + } + + if (CanSendODAToContentProcessDirectly(optionalHead)) { + Maybe> dataBridgeParent = + SocketProcessChild::GetSingleton()->GetAndRemoveDataBridge(mChannelId); + // Check if there is a registered BackgroundDataBridgeParent. + if (dataBridgeParent) { + mDataBridgeParent = std::move(dataBridgeParent.ref()); + + nsCOMPtr backgroundThread = + mDataBridgeParent->GetBackgroundThread(); + nsCOMPtr retargetableTransactionPump; + retargetableTransactionPump = do_QueryObject(mTransactionPump); + // nsInputStreamPump should implement this interface. + MOZ_ASSERT(retargetableTransactionPump); + + nsresult rv = + retargetableTransactionPump->RetargetDeliveryTo(backgroundThread); + LOG((" Retarget to background thread [this=%p rv=%08x]\n", this, + static_cast(rv))); + if (NS_FAILED(rv)) { + mDataBridgeParent->Destroy(); + mDataBridgeParent = nullptr; + } + } + } + + int32_t proxyConnectResponseCode = + mTransaction->GetProxyConnectResponseCode(); + + Unused << SendOnStartRequest( + status, optionalHead, serializedSecurityInfoOut, + mTransaction->ProxyConnectFailed(), + ToTimingStructArgs(mTransaction->Timings()), proxyConnectResponseCode, + dataForSniffer, optionalAltSvcUsed, !!mDataBridgeParent, + mTransaction->TakeRestartedState(), mTransaction->HTTPSSVCReceivedStage(), + mTransaction->GetSupportsHTTP3()); + return NS_OK; +} + +ResourceTimingStructArgs HttpTransactionChild::GetTimingAttributes() { + // Note that not all fields in ResourceTimingStructArgs are filled, since + // we only need some in HttpChannelChild::OnStopRequest. + ResourceTimingStructArgs args; + args.domainLookupStart() = mTransaction->GetDomainLookupStart(); + args.domainLookupEnd() = mTransaction->GetDomainLookupEnd(); + args.connectStart() = mTransaction->GetConnectStart(); + args.tcpConnectEnd() = mTransaction->GetTcpConnectEnd(); + args.secureConnectionStart() = mTransaction->GetSecureConnectionStart(); + args.connectEnd() = mTransaction->GetConnectEnd(); + args.requestStart() = mTransaction->GetRequestStart(); + args.responseStart() = mTransaction->GetResponseStart(); + args.responseEnd() = mTransaction->GetResponseEnd(); + args.transferSize() = mTransaction->GetTransferSize(); + args.encodedBodySize() = mLogicalOffset; + args.redirectStart() = mRedirectStart; + args.redirectEnd() = mRedirectEnd; + args.protocolVersion() = mProtocolVersion; + return args; +} + +NS_IMETHODIMP +HttpTransactionChild::OnStopRequest(nsIRequest* aRequest, nsresult aStatus) { + LOG(("HttpTransactionChild::OnStopRequest [this=%p]\n", this)); + + mTransactionPump = nullptr; + + auto onStopGuard = MakeScopeExit([&] { + LOG((" calling mDataBridgeParent->OnStopRequest by ScopeExit [this=%p]\n", + this)); + MOZ_ASSERT(NS_FAILED(mStatus), "This shoule be only called when failure"); + if (mDataBridgeParent) { + mDataBridgeParent->OnStopRequest(mStatus, ResourceTimingStructArgs(), + TimeStamp(), nsHttpHeaderArray()); + mDataBridgeParent = nullptr; + } + }); + + // Don't bother sending IPC to parent process if already canceled. + if (mCanceled) { + return mStatus; + } + + if (!CanSend()) { + mStatus = NS_ERROR_UNEXPECTED; + return mStatus; + } + + MOZ_ASSERT(mTransaction); + + UniquePtr headerArray( + mTransaction->TakeResponseTrailers()); + Maybe responseTrailers; + if (headerArray) { + responseTrailers.emplace(*headerArray); + } + + onStopGuard.release(); + + TimeStamp lastActTabOpt = nsHttp::GetLastActiveTabLoadOptimizationHit(); + + if (mDataBridgeParent) { + mDataBridgeParent->OnStopRequest( + aStatus, GetTimingAttributes(), lastActTabOpt, + responseTrailers ? *responseTrailers : nsHttpHeaderArray()); + mDataBridgeParent = nullptr; + } + + RefPtr connInfo = mTransaction->GetConnInfo(); + HttpConnectionInfoCloneArgs infoArgs; + nsHttpConnectionInfo::SerializeHttpConnectionInfo(connInfo, infoArgs); + Unused << SendOnStopRequest(aStatus, mTransaction->ResponseIsComplete(), + mTransaction->GetTransferSize(), + ToTimingStructArgs(mTransaction->Timings()), + responseTrailers, mTransactionObserverResult, + lastActTabOpt, mTransaction->Caps(), infoArgs); + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// HttpTransactionChild +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +HttpTransactionChild::OnTransportStatus(nsITransport* aTransport, + nsresult aStatus, int64_t aProgress, + int64_t aProgressMax) { + LOG(("HttpTransactionChild::OnTransportStatus [this=%p status=%" PRIx32 + " progress=%" PRId64 "]\n", + this, static_cast(aStatus), aProgress)); + + if (!CanSend()) { + return NS_OK; + } + + Maybe arg; + if (aStatus == NS_NET_STATUS_CONNECTED_TO || + aStatus == NS_NET_STATUS_WAITING_FOR) { + NetAddr selfAddr; + NetAddr peerAddr; + bool isTrr = false; + bool echConfigUsed = false; + if (mTransaction) { + mTransaction->GetNetworkAddresses(selfAddr, peerAddr, isTrr, + echConfigUsed); + } else { + nsCOMPtr socketTransport = + do_QueryInterface(aTransport); + if (socketTransport) { + socketTransport->GetSelfAddr(&selfAddr); + socketTransport->GetPeerAddr(&peerAddr); + socketTransport->ResolvedByTRR(&isTrr); + socketTransport->GetEchConfigUsed(&echConfigUsed); + } + } + arg.emplace(selfAddr, peerAddr, isTrr, echConfigUsed); + } + + Unused << SendOnTransportStatus(aStatus, aProgress, aProgressMax, arg); + return NS_OK; +} + +//----------------------------------------------------------------------------- +// HttpBaseChannel::nsIThrottledInputChannel +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +HttpTransactionChild::SetThrottleQueue(nsIInputChannelThrottleQueue* aQueue) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +HttpTransactionChild::GetThrottleQueue(nsIInputChannelThrottleQueue** aQueue) { + nsCOMPtr queue = + static_cast(mThrottleQueue.get()); + queue.forget(aQueue); + return NS_OK; +} + +//----------------------------------------------------------------------------- +// EventSourceImpl::nsIThreadRetargetableStreamListener +//----------------------------------------------------------------------------- +NS_IMETHODIMP +HttpTransactionChild::CheckListenerChain() { + MOZ_ASSERT(NS_IsMainThread(), "Should be on the main thread!"); + return NS_OK; +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/HttpTransactionChild.h b/netwerk/protocol/http/HttpTransactionChild.h new file mode 100644 index 0000000000..89129d9db1 --- /dev/null +++ b/netwerk/protocol/http/HttpTransactionChild.h @@ -0,0 +1,127 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* 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/. */ + +#ifndef HttpTransactionChild_h__ +#define HttpTransactionChild_h__ + +#include "mozilla/Atomics.h" +#include "mozilla/net/NeckoChannelParams.h" +#include "mozilla/net/PHttpTransactionChild.h" +#include "nsHttpRequestHead.h" +#include "nsIRequest.h" +#include "nsIStreamListener.h" +#include "nsIThreadRetargetableStreamListener.h" +#include "nsIThrottledInputChannel.h" +#include "nsITransport.h" + +class nsInputStreamPump; + +namespace mozilla { +namespace net { + +class BackgroundDataBridgeParent; +class InputChannelThrottleQueueChild; +class nsHttpConnectionInfo; +class nsHttpTransaction; +class nsProxyInfo; + +//----------------------------------------------------------------------------- +// HttpTransactionChild commutes between parent process and socket process, +// manages the real nsHttpTransaction and transaction pump. +//----------------------------------------------------------------------------- +class HttpTransactionChild final : public PHttpTransactionChild, + public nsIStreamListener, + public nsITransportEventSink, + public nsIThrottledInputChannel, + public nsIThreadRetargetableStreamListener { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMLISTENER + NS_DECL_NSITRANSPORTEVENTSINK + NS_DECL_NSITHROTTLEDINPUTCHANNEL + NS_DECL_NSITHREADRETARGETABLESTREAMLISTENER + + explicit HttpTransactionChild(); + + mozilla::ipc::IPCResult RecvInit( + const uint32_t& aCaps, const HttpConnectionInfoCloneArgs& aArgs, + const nsHttpRequestHead& aReqHeaders, + const Maybe& aRequestBody, const uint64_t& aReqContentLength, + const bool& aReqBodyIncludesHeaders, + const uint64_t& aTopLevelOuterContentWindowId, + const uint8_t& aHttpTrafficCategory, const uint64_t& aRequestContextID, + const uint32_t& aClassOfService, const uint32_t& aInitialRwin, + const bool& aResponseTimeoutEnabled, const uint64_t& aChannelId, + const bool& aHasTransactionObserver, + const Maybe& aPushedStreamArg, + const mozilla::Maybe& aThrottleQueue, + const bool& aIsDocumentLoad, const TimeStamp& aRedirectStart, + const TimeStamp& aRedirectEnd); + mozilla::ipc::IPCResult RecvCancelPump(const nsresult& aStatus); + mozilla::ipc::IPCResult RecvSuspendPump(); + mozilla::ipc::IPCResult RecvResumePump(); + mozilla::ipc::IPCResult RecvSetDNSWasRefreshed(); + mozilla::ipc::IPCResult RecvDontReuseConnection(); + mozilla::ipc::IPCResult RecvSetH2WSConnRefTaken(); + void ActorDestroy(ActorDestroyReason aWhy) override; + + nsHttpTransaction* GetHttpTransaction(); + + private: + virtual ~HttpTransactionChild(); + + nsProxyInfo* ProxyInfoCloneArgsToProxyInfo( + const nsTArray& aArgs); + already_AddRefed DeserializeHttpConnectionInfoCloneArgs( + const HttpConnectionInfoCloneArgs& aInfoArgs); + // Initialize the *real* nsHttpTransaction. See |nsHttpTransaction::Init| + // for the parameters. + [[nodiscard]] nsresult InitInternal( + uint32_t caps, const HttpConnectionInfoCloneArgs& aArgs, + nsHttpRequestHead* reqHeaders, + nsIInputStream* reqBody, // use the trick in bug 1277681 + uint64_t reqContentLength, bool reqBodyIncludesHeaders, + uint64_t topLevelOuterContentWindowId, uint8_t httpTrafficCategory, + uint64_t requestContextID, uint32_t classOfService, uint32_t initialRwin, + bool responseTimeoutEnabled, uint64_t channelId, + bool aHasTransactionObserver, + const Maybe& aPushedStreamArg); + + void CancelInternal(nsresult aStatus); + + bool CanSendODAToContentProcessDirectly( + const Maybe& aHead); + + ResourceTimingStructArgs GetTimingAttributes(); + + // Use Release-Acquire ordering to ensure the OMT ODA is ignored while + // transaction is canceled on main thread. + Atomic mCanceled; + Atomic mStatus; + uint64_t mChannelId; + nsHttpRequestHead mRequestHead; + bool mIsDocumentLoad; + uint64_t mLogicalOffset; + TimeStamp mRedirectStart; + TimeStamp mRedirectEnd; + nsCString mProtocolVersion; + + nsCOMPtr mUploadStream; + RefPtr mTransaction; + nsCOMPtr mTransactionPump; + Maybe mTransactionObserverResult; + RefPtr mThrottleQueue; + RefPtr mDataBridgeParent; +}; + +} // namespace net +} // namespace mozilla + +inline nsISupports* ToSupports(mozilla::net::HttpTransactionChild* p) { + return static_cast(p); +} + +#endif // nsHttpTransactionChild_h__ diff --git a/netwerk/protocol/http/HttpTransactionParent.cpp b/netwerk/protocol/http/HttpTransactionParent.cpp new file mode 100644 index 0000000000..53cb9d10cb --- /dev/null +++ b/netwerk/protocol/http/HttpTransactionParent.cpp @@ -0,0 +1,909 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* vim:set ts=4 sw=4 sts=4 et cin: */ +/* 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/. */ + +// HttpLog.h should generally be included first +#include "HttpLog.h" + +#include "HttpTransactionParent.h" +#include "HttpTrafficAnalyzer.h" +#include "mozilla/ipc/IPCStreamUtils.h" +#include "mozilla/net/InputChannelThrottleQueueParent.h" +#include "mozilla/net/ChannelEventQueue.h" +#include "mozilla/net/SocketProcessParent.h" +#include "nsHttpHandler.h" +#include "nsQueryObject.h" +#include "nsSerializationHelper.h" +#include "nsStreamUtils.h" +#include "nsStringStream.h" + +namespace mozilla { +namespace net { + +NS_IMPL_ADDREF(HttpTransactionParent) +NS_INTERFACE_MAP_BEGIN(HttpTransactionParent) + NS_INTERFACE_MAP_ENTRY(nsIRequest) + NS_INTERFACE_MAP_ENTRY(nsIThreadRetargetableRequest) + NS_INTERFACE_MAP_ENTRY_CONCRETE(HttpTransactionParent) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIRequest) +NS_INTERFACE_MAP_END + +NS_IMETHODIMP_(MozExternalRefCountType) HttpTransactionParent::Release(void) { + MOZ_ASSERT(int32_t(mRefCnt) > 0, "dup release"); + + if (!mRefCnt.isThreadSafe) { + NS_ASSERT_OWNINGTHREAD(HttpTransactionParent); + } + + nsrefcnt count = --mRefCnt; + NS_LOG_RELEASE(this, count, "HttpTransactionParent"); + + if (count == 0) { + if (!nsAutoRefCnt::isThreadSafe) { + NS_ASSERT_OWNINGTHREAD(HttpTransactionParent); + } + + mRefCnt = 1; /* stabilize */ + delete (this); + return 0; + } + + // When ref count goes down to 1 (held internally by IPDL), it means that + // we are done with this transaction. We should send a delete message + // to delete the transaction child in socket process. + if (count == 1 && CanSend()) { + if (!NS_IsMainThread()) { + RefPtr self = this; + MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread( + NS_NewRunnableFunction("HttpTransactionParent::Release", [self]() { + mozilla::Unused << self->Send__delete__(self); + // Make sure we can not send IPC after Send__delete__(). + MOZ_ASSERT(!self->CanSend()); + }))); + } else { + mozilla::Unused << Send__delete__(this); + } + return 1; + } + return count; +} + +//----------------------------------------------------------------------------- +// HttpTransactionParent +//----------------------------------------------------------------------------- + +HttpTransactionParent::HttpTransactionParent(bool aIsDocumentLoad) + : mEventTargetMutex("HttpTransactionParent::EventTargetMutex"), + mResponseIsComplete(false), + mTransferSize(0), + mRequestSize(0), + mProxyConnectFailed(false), + mCanceled(false), + mStatus(NS_OK), + mSuspendCount(0), + mResponseHeadTaken(false), + mResponseTrailersTaken(false), + mOnStartRequestCalled(false), + mOnStopRequestCalled(false), + mResolvedByTRR(false), + mEchConfigUsed(false), + mProxyConnectResponseCode(0), + mChannelId(0), + mDataSentToChildProcess(false), + mIsDocumentLoad(aIsDocumentLoad), + mRestarted(false), + mCaps(0) { + LOG(("Creating HttpTransactionParent @%p\n", this)); + + this->mSelfAddr.inet = {}; + this->mPeerAddr.inet = {}; + +#ifdef MOZ_VALGRIND + memset(&mSelfAddr, 0, sizeof(NetAddr)); + memset(&mPeerAddr, 0, sizeof(NetAddr)); +#endif + mSelfAddr.raw.family = PR_AF_UNSPEC; + mPeerAddr.raw.family = PR_AF_UNSPEC; + + mEventQ = new ChannelEventQueue(static_cast(this)); +} + +HttpTransactionParent::~HttpTransactionParent() { + LOG(("Destroying HttpTransactionParent @%p\n", this)); +} + +//----------------------------------------------------------------------------- +// HttpTransactionParent +//----------------------------------------------------------------------------- + +// Let socket process init the *real* nsHttpTransaction. +nsresult HttpTransactionParent::Init( + uint32_t caps, nsHttpConnectionInfo* cinfo, nsHttpRequestHead* requestHead, + nsIInputStream* requestBody, uint64_t requestContentLength, + bool requestBodyHasHeaders, nsIEventTarget* target, + nsIInterfaceRequestor* callbacks, nsITransportEventSink* eventsink, + uint64_t topLevelOuterContentWindowId, HttpTrafficCategory trafficCategory, + nsIRequestContext* requestContext, uint32_t classOfService, + uint32_t initialRwin, bool responseTimeoutEnabled, uint64_t channelId, + TransactionObserverFunc&& transactionObserver, + OnPushCallback&& aOnPushCallback, + HttpTransactionShell* aTransWithPushedStream, uint32_t aPushedStreamId) { + LOG(("HttpTransactionParent::Init [this=%p caps=%x]\n", this, caps)); + + if (!CanSend()) { + return NS_ERROR_FAILURE; + } + + mEventsink = eventsink; + mTargetThread = GetCurrentEventTarget(); + mChannelId = channelId; + mTransactionObserver = std::move(transactionObserver); + mOnPushCallback = std::move(aOnPushCallback); + mCaps = caps; + mConnInfo = cinfo->Clone(); + mIsHttp3Used = cinfo->IsHttp3(); + + HttpConnectionInfoCloneArgs infoArgs; + nsHttpConnectionInfo::SerializeHttpConnectionInfo(cinfo, infoArgs); + + mozilla::ipc::AutoIPCStream autoStream; + if (requestBody && + !autoStream.Serialize(requestBody, SocketProcessParent::GetSingleton())) { + return NS_ERROR_FAILURE; + } + + uint64_t requestContextID = requestContext ? requestContext->GetID() : 0; + + Maybe pushedStreamArg; + if (aTransWithPushedStream && aPushedStreamId) { + MOZ_ASSERT(aTransWithPushedStream->AsHttpTransactionParent()); + pushedStreamArg.emplace(); + pushedStreamArg.ref().transWithPushedStreamParent() = + aTransWithPushedStream->AsHttpTransactionParent(); + pushedStreamArg.ref().pushedStreamId() = aPushedStreamId; + } + + nsCOMPtr throttled = do_QueryInterface(mEventsink); + Maybe throttleQueue; + if (throttled) { + nsCOMPtr queue; + nsresult rv = throttled->GetThrottleQueue(getter_AddRefs(queue)); + // In case of failure, just carry on without throttling. + if (NS_SUCCEEDED(rv) && queue) { + LOG1(("HttpTransactionParent::Init %p using throttle queue %p\n", this, + queue.get())); + RefPtr tqParent = do_QueryObject(queue); + MOZ_ASSERT(tqParent); + throttleQueue.emplace(tqParent.get()); + } + } + + // TODO: Figure out if we have to implement nsIThreadRetargetableRequest in + // bug 1544378. + if (!SendInit(caps, infoArgs, *requestHead, + requestBody ? Some(autoStream.TakeValue()) : Nothing(), + requestContentLength, requestBodyHasHeaders, + topLevelOuterContentWindowId, + static_cast(trafficCategory), requestContextID, + classOfService, initialRwin, responseTimeoutEnabled, mChannelId, + !!mTransactionObserver, pushedStreamArg, throttleQueue, + mIsDocumentLoad, mRedirectStart, mRedirectEnd)) { + return NS_ERROR_FAILURE; + } + + nsCString reqHeaderBuf = nsHttp::ConvertRequestHeadToString( + *requestHead, !!requestBody, requestBodyHasHeaders, + cinfo->UsingConnect()); + requestContentLength += reqHeaderBuf.Length(); + + mRequestSize = InScriptableRange(requestContentLength) + ? static_cast(requestContentLength) + : -1; + + return NS_OK; +} + +nsresult HttpTransactionParent::AsyncRead(nsIStreamListener* listener, + nsIRequest** pump) { + MOZ_ASSERT(pump); + + *pump = do_AddRef(this).take(); + mChannel = listener; + return NS_OK; +} + +UniquePtr HttpTransactionParent::TakeResponseHead() { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!mResponseHeadTaken, "TakeResponseHead called 2x"); + + mResponseHeadTaken = true; + return std::move(mResponseHead); +} + +UniquePtr HttpTransactionParent::TakeResponseTrailers() { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!mResponseTrailersTaken, "TakeResponseTrailers called 2x"); + + mResponseTrailersTaken = true; + return std::move(mResponseTrailers); +} + +void HttpTransactionParent::SetSniffedTypeToChannel( + nsInputStreamPump::PeekSegmentFun aCallTypeSniffers, nsIChannel* aChannel) { + if (!mDataForSniffer.IsEmpty()) { + aCallTypeSniffers(aChannel, mDataForSniffer.Elements(), + mDataForSniffer.Length()); + } +} + +NS_IMETHODIMP +HttpTransactionParent::GetDeliveryTarget(nsIEventTarget** aEventTarget) { + MutexAutoLock lock(mEventTargetMutex); + + nsCOMPtr target = mODATarget; + if (!mODATarget) { + target = mTargetThread; + } + target.forget(aEventTarget); + return NS_OK; +} + +already_AddRefed HttpTransactionParent::GetODATarget() { + nsCOMPtr target; + { + MutexAutoLock lock(mEventTargetMutex); + target = mODATarget ? mODATarget : mTargetThread; + } + + if (!target) { + target = GetMainThreadEventTarget(); + } + return target.forget(); +} + +NS_IMETHODIMP HttpTransactionParent::RetargetDeliveryTo( + nsIEventTarget* aEventTarget) { + LOG(("HttpTransactionParent::RetargetDeliveryTo [this=%p, aTarget=%p]", this, + aEventTarget)); + + MOZ_ASSERT(NS_IsMainThread(), "Should be called on main thread only"); + MOZ_ASSERT(!mODATarget); + NS_ENSURE_ARG(aEventTarget); + + if (aEventTarget->IsOnCurrentThread()) { + NS_WARNING("Retargeting delivery to same thread"); + return NS_OK; + } + + nsresult rv = NS_OK; + nsCOMPtr retargetableListener = + do_QueryInterface(mChannel, &rv); + if (!retargetableListener || NS_FAILED(rv)) { + NS_WARNING("Listener is not retargetable"); + return NS_ERROR_NO_INTERFACE; + } + + rv = retargetableListener->CheckListenerChain(); + if (NS_FAILED(rv)) { + NS_WARNING("Subsequent listeners are not retargetable"); + return rv; + } + + { + MutexAutoLock lock(mEventTargetMutex); + mODATarget = aEventTarget; + } + + return NS_OK; +} + +void HttpTransactionParent::SetDNSWasRefreshed() { + MOZ_ASSERT(NS_IsMainThread(), "SetDNSWasRefreshed on main thread only!"); + Unused << SendSetDNSWasRefreshed(); +} + +void HttpTransactionParent::GetNetworkAddresses(NetAddr& self, NetAddr& peer, + bool& aResolvedByTRR, + bool& aEchConfigUsed) { + self = mSelfAddr; + peer = mPeerAddr; + aResolvedByTRR = mResolvedByTRR; + aEchConfigUsed = mEchConfigUsed; +} + +bool HttpTransactionParent::HasStickyConnection() const { + return mCaps & NS_HTTP_STICKY_CONNECTION; +} + +mozilla::TimeStamp HttpTransactionParent::GetDomainLookupStart() { + return mTimings.domainLookupStart; +} + +mozilla::TimeStamp HttpTransactionParent::GetDomainLookupEnd() { + return mTimings.domainLookupEnd; +} + +mozilla::TimeStamp HttpTransactionParent::GetConnectStart() { + return mTimings.connectStart; +} + +mozilla::TimeStamp HttpTransactionParent::GetTcpConnectEnd() { + return mTimings.tcpConnectEnd; +} + +mozilla::TimeStamp HttpTransactionParent::GetSecureConnectionStart() { + return mTimings.secureConnectionStart; +} + +mozilla::TimeStamp HttpTransactionParent::GetConnectEnd() { + return mTimings.connectEnd; +} + +mozilla::TimeStamp HttpTransactionParent::GetRequestStart() { + return mTimings.requestStart; +} + +mozilla::TimeStamp HttpTransactionParent::GetResponseStart() { + return mTimings.responseStart; +} + +mozilla::TimeStamp HttpTransactionParent::GetResponseEnd() { + return mTimings.responseEnd; +} + +const TimingStruct HttpTransactionParent::Timings() { return mTimings; } + +bool HttpTransactionParent::ResponseIsComplete() { return mResponseIsComplete; } + +int64_t HttpTransactionParent::GetTransferSize() { return mTransferSize; } + +int64_t HttpTransactionParent::GetRequestSize() { return mRequestSize; } + +bool HttpTransactionParent::IsHttp3Used() { return mIsHttp3Used; } + +bool HttpTransactionParent::DataSentToChildProcess() { + return mDataSentToChildProcess; +} + +already_AddRefed HttpTransactionParent::SecurityInfo() { + return do_AddRef(mSecurityInfo); +} + +bool HttpTransactionParent::ProxyConnectFailed() { return mProxyConnectFailed; } + +bool HttpTransactionParent::TakeRestartedState() { + bool result = mRestarted; + mRestarted = false; + return result; +} + +uint32_t HttpTransactionParent::HTTPSSVCReceivedStage() { + return mHTTPSSVCReceivedStage; +} + +void HttpTransactionParent::DontReuseConnection() { + MOZ_ASSERT(NS_IsMainThread()); + Unused << SendDontReuseConnection(); +} + +void HttpTransactionParent::SetH2WSConnRefTaken() { + MOZ_ASSERT(NS_IsMainThread()); + Unused << SendSetH2WSConnRefTaken(); +} + +void HttpTransactionParent::SetSecurityCallbacks( + nsIInterfaceRequestor* aCallbacks) { + // TODO: we might don't need to implement this. + // Will figure out in bug 1512479. +} + +void HttpTransactionParent::SetDomainLookupStart(mozilla::TimeStamp timeStamp, + bool onlyIfNull) { + mDomainLookupStart = timeStamp; + mTimings.domainLookupStart = mDomainLookupStart; +} +void HttpTransactionParent::SetDomainLookupEnd(mozilla::TimeStamp timeStamp, + bool onlyIfNull) { + mDomainLookupEnd = timeStamp; + mTimings.domainLookupEnd = mDomainLookupEnd; +} + +nsHttpTransaction* HttpTransactionParent::AsHttpTransaction() { + return nullptr; +} + +HttpTransactionParent* HttpTransactionParent::AsHttpTransactionParent() { + return this; +} + +int32_t HttpTransactionParent::GetProxyConnectResponseCode() { + return mProxyConnectResponseCode; +} + +bool HttpTransactionParent::Http2Disabled() const { + return mCaps & NS_HTTP_DISALLOW_SPDY; +} + +bool HttpTransactionParent::Http3Disabled() const { + return mCaps & NS_HTTP_DISALLOW_HTTP3; +} + +already_AddRefed HttpTransactionParent::GetConnInfo() + const { + RefPtr connInfo = mConnInfo->Clone(); + return connInfo.forget(); +} + +already_AddRefed HttpTransactionParent::GetNeckoTarget() { + nsCOMPtr target = GetMainThreadEventTarget(); + return target.forget(); +} + +mozilla::ipc::IPCResult HttpTransactionParent::RecvOnStartRequest( + const nsresult& aStatus, const Maybe& aResponseHead, + const nsCString& aSecurityInfoSerialization, + const bool& aProxyConnectFailed, const TimingStructArgs& aTimings, + const int32_t& aProxyConnectResponseCode, + nsTArray&& aDataForSniffer, const Maybe& aAltSvcUsed, + const bool& aDataToChildProcess, const bool& aRestarted, + const uint32_t& aHTTPSSVCReceivedStage, const bool& aSupportsHttp3) { + mEventQ->RunOrEnqueue(new NeckoTargetChannelFunctionEvent( + this, [self = UnsafePtr(this), aStatus, + aResponseHead, aSecurityInfoSerialization, aProxyConnectFailed, + aTimings, aProxyConnectResponseCode, + aDataForSniffer = CopyableTArray{std::move(aDataForSniffer)}, + aAltSvcUsed, aDataToChildProcess, aRestarted, + aHTTPSSVCReceivedStage, aSupportsHttp3]() mutable { + self->DoOnStartRequest( + aStatus, aResponseHead, aSecurityInfoSerialization, + aProxyConnectFailed, aTimings, aProxyConnectResponseCode, + std::move(aDataForSniffer), aAltSvcUsed, aDataToChildProcess, + aRestarted, aHTTPSSVCReceivedStage, aSupportsHttp3); + })); + return IPC_OK(); +} + +static void TimingStructArgsToTimingsStruct(const TimingStructArgs& aArgs, + TimingStruct& aTimings) { + // If domainLookupStart/End was set by the channel before, we use these + // timestamps instead the ones from the transaction. + if (aTimings.domainLookupStart.IsNull() && + aTimings.domainLookupEnd.IsNull()) { + aTimings.domainLookupStart = aArgs.domainLookupStart(); + aTimings.domainLookupEnd = aArgs.domainLookupEnd(); + } + aTimings.connectStart = aArgs.connectStart(); + aTimings.tcpConnectEnd = aArgs.tcpConnectEnd(); + aTimings.secureConnectionStart = aArgs.secureConnectionStart(); + aTimings.connectEnd = aArgs.connectEnd(); + aTimings.requestStart = aArgs.requestStart(); + aTimings.responseStart = aArgs.responseStart(); + aTimings.responseEnd = aArgs.responseEnd(); +} + +void HttpTransactionParent::DoOnStartRequest( + const nsresult& aStatus, const Maybe& aResponseHead, + const nsCString& aSecurityInfoSerialization, + const bool& aProxyConnectFailed, const TimingStructArgs& aTimings, + const int32_t& aProxyConnectResponseCode, + nsTArray&& aDataForSniffer, const Maybe& aAltSvcUsed, + const bool& aDataToChildProcess, const bool& aRestarted, + const uint32_t& aHTTPSSVCReceivedStage, const bool& aSupportsHttp3) { + LOG(("HttpTransactionParent::DoOnStartRequest [this=%p aStatus=%" PRIx32 + "]\n", + this, static_cast(aStatus))); + + if (mCanceled) { + return; + } + + MOZ_ASSERT(!mOnStartRequestCalled); + + mStatus = aStatus; + mDataSentToChildProcess = aDataToChildProcess; + mHTTPSSVCReceivedStage = aHTTPSSVCReceivedStage; + mSupportsHTTP3 = aSupportsHttp3; + + if (!aSecurityInfoSerialization.IsEmpty()) { + NS_DeserializeObject(aSecurityInfoSerialization, + getter_AddRefs(mSecurityInfo)); + } + + if (aResponseHead.isSome()) { + mResponseHead = MakeUnique(aResponseHead.ref()); + } + mProxyConnectFailed = aProxyConnectFailed; + TimingStructArgsToTimingsStruct(aTimings, mTimings); + + mProxyConnectResponseCode = aProxyConnectResponseCode; + mDataForSniffer = std::move(aDataForSniffer); + mRestarted = aRestarted; + + nsCOMPtr httpChannel = do_QueryInterface(mChannel); + MOZ_ASSERT(httpChannel, "mChannel is expected to implement nsIHttpChannel"); + if (httpChannel) { + if (aAltSvcUsed.isSome()) { + Unused << httpChannel->SetRequestHeader( + nsDependentCString(nsHttp::Alternate_Service_Used), aAltSvcUsed.ref(), + false); + } + } + + AutoEventEnqueuer ensureSerialDispatch(mEventQ); + nsresult rv = mChannel->OnStartRequest(this); + mOnStartRequestCalled = true; + if (NS_FAILED(rv)) { + Cancel(rv); + } +} + +mozilla::ipc::IPCResult HttpTransactionParent::RecvOnTransportStatus( + const nsresult& aStatus, const int64_t& aProgress, + const int64_t& aProgressMax, + Maybe&& aNetworkAddressArg) { + if (aNetworkAddressArg) { + mSelfAddr = aNetworkAddressArg->selfAddr(); + mPeerAddr = aNetworkAddressArg->peerAddr(); + mResolvedByTRR = aNetworkAddressArg->resolvedByTRR(); + mEchConfigUsed = aNetworkAddressArg->echConfigUsed(); + } + mEventsink->OnTransportStatus(nullptr, aStatus, aProgress, aProgressMax); + return IPC_OK(); +} + +mozilla::ipc::IPCResult HttpTransactionParent::RecvOnDataAvailable( + const nsCString& aData, const uint64_t& aOffset, const uint32_t& aCount) { + LOG(("HttpTransactionParent::RecvOnDataAvailable [this=%p, aOffset= %" PRIu64 + " aCount=%" PRIu32, + this, aOffset, aCount)); + + // The final transfer size is updated in OnStopRequest ipc message, but in the + // case that the socket process is crashed or something went wrong, we might + // not get the OnStopRequest. So, let's update the transfer size here. + mTransferSize += aCount; + + if (mCanceled) { + return IPC_OK(); + } + + mEventQ->RunOrEnqueue(new ChannelFunctionEvent( + [self = UnsafePtr(this)]() { + return self->GetODATarget(); + }, + [self = UnsafePtr(this), aData, aOffset, + aCount]() { self->DoOnDataAvailable(aData, aOffset, aCount); })); + return IPC_OK(); +} + +void HttpTransactionParent::DoOnDataAvailable(const nsCString& aData, + const uint64_t& aOffset, + const uint32_t& aCount) { + LOG(("HttpTransactionParent::DoOnDataAvailable [this=%p]\n", this)); + if (mCanceled) { + return; + } + + nsCOMPtr stringStream; + nsresult rv = + NS_NewByteInputStream(getter_AddRefs(stringStream), + Span(aData.get(), aCount), NS_ASSIGNMENT_DEPEND); + + if (NS_FAILED(rv)) { + CancelOnMainThread(rv); + return; + } + + AutoEventEnqueuer ensureSerialDispatch(mEventQ); + rv = mChannel->OnDataAvailable(this, stringStream, aOffset, aCount); + if (NS_FAILED(rv)) { + CancelOnMainThread(rv); + } +} + +// Note: Copied from HttpChannelChild. +void HttpTransactionParent::CancelOnMainThread(nsresult aRv) { + LOG(("HttpTransactionParent::CancelOnMainThread [this=%p]", this)); + + if (NS_IsMainThread()) { + Cancel(aRv); + return; + } + + mEventQ->Suspend(); + // Cancel is expected to preempt any other channel events, thus we put this + // event in the front of mEventQ to make sure nsIStreamListener not receiving + // any ODA/OnStopRequest callbacks. + mEventQ->PrependEvent(MakeUnique( + this, [self = UnsafePtr(this), aRv]() { + self->Cancel(aRv); + })); + mEventQ->Resume(); +} + +mozilla::ipc::IPCResult HttpTransactionParent::RecvOnStopRequest( + const nsresult& aStatus, const bool& aResponseIsComplete, + const int64_t& aTransferSize, const TimingStructArgs& aTimings, + const Maybe& aResponseTrailers, + Maybe&& aTransactionObserverResult, + const TimeStamp& aLastActiveTabOptHit, const uint32_t& aCaps, + const HttpConnectionInfoCloneArgs& aArgs) { + LOG(("HttpTransactionParent::RecvOnStopRequest [this=%p status=%" PRIx32 + "]\n", + this, static_cast(aStatus))); + + nsHttp::SetLastActiveTabLoadOptimizationHit(aLastActiveTabOptHit); + + if (mCanceled) { + return IPC_OK(); + } + RefPtr cinfo = + nsHttpConnectionInfo::DeserializeHttpConnectionInfoCloneArgs(aArgs); + mEventQ->RunOrEnqueue(new NeckoTargetChannelFunctionEvent( + this, [self = UnsafePtr(this), aStatus, + aResponseIsComplete, aTransferSize, aTimings, aResponseTrailers, + aTransactionObserverResult{std::move(aTransactionObserverResult)}, + aCaps, cinfo{std::move(cinfo)}]() mutable { + self->DoOnStopRequest(aStatus, aResponseIsComplete, aTransferSize, + aTimings, aResponseTrailers, + std::move(aTransactionObserverResult), aCaps, + cinfo); + })); + return IPC_OK(); +} + +void HttpTransactionParent::DoOnStopRequest( + const nsresult& aStatus, const bool& aResponseIsComplete, + const int64_t& aTransferSize, const TimingStructArgs& aTimings, + const Maybe& aResponseTrailers, + Maybe&& aTransactionObserverResult, + const uint32_t& aCaps, nsHttpConnectionInfo* aConnInfo) { + LOG(("HttpTransactionParent::DoOnStopRequest [this=%p]\n", this)); + if (mCanceled) { + return; + } + + MOZ_ASSERT(!mOnStopRequestCalled, "We should not call OnStopRequest twice"); + + mStatus = aStatus; + + nsCOMPtr deathGrip = this; + + mResponseIsComplete = aResponseIsComplete; + mTransferSize = aTransferSize; + + TimingStructArgsToTimingsStruct(aTimings, mTimings); + + if (aResponseTrailers.isSome()) { + mResponseTrailers = MakeUnique(aResponseTrailers.ref()); + } + mCaps = aCaps; + mConnInfo = aConnInfo; + if (aTransactionObserverResult.isSome()) { + TransactionObserverFunc obs = nullptr; + std::swap(obs, mTransactionObserver); + obs(std::move(*aTransactionObserverResult)); + } + + AutoEventEnqueuer ensureSerialDispatch(mEventQ); + Unused << mChannel->OnStopRequest(this, mStatus); + mOnStopRequestCalled = true; +} + +mozilla::ipc::IPCResult HttpTransactionParent::RecvOnInitFailed( + const nsresult& aStatus) { + nsCOMPtr request = do_QueryInterface(mEventsink); + if (request) { + request->Cancel(aStatus); + } + return IPC_OK(); +} + +mozilla::ipc::IPCResult HttpTransactionParent::RecvOnH2PushStream( + const uint32_t& aPushedStreamId, const nsCString& aResourceUrl, + const nsCString& aRequestString) { + MOZ_ASSERT(mOnPushCallback); + + mOnPushCallback(aPushedStreamId, aResourceUrl, aRequestString, this); + return IPC_OK(); +} // namespace net + +//----------------------------------------------------------------------------- +// HttpTransactionParent +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +HttpTransactionParent::GetName(nsACString& aResult) { + aResult.Truncate(); + return NS_OK; +} + +NS_IMETHODIMP +HttpTransactionParent::IsPending(bool* aRetval) { + *aRetval = false; + return NS_OK; +} + +NS_IMETHODIMP +HttpTransactionParent::GetStatus(nsresult* aStatus) { + *aStatus = mStatus; + return NS_OK; +} + +NS_IMETHODIMP +HttpTransactionParent::Cancel(nsresult aStatus) { + MOZ_ASSERT(NS_IsMainThread()); + + LOG(("HttpTransactionParent::Cancel [this=%p status=%" PRIx32 "]\n", this, + static_cast(aStatus))); + + if (mCanceled) { + LOG((" already canceled\n")); + return NS_OK; + } + + MOZ_ASSERT(NS_FAILED(aStatus), "cancel with non-failure status code"); + + mCanceled = true; + mStatus = aStatus; + if (CanSend()) { + Unused << SendCancelPump(mStatus); + } + + // Put DoNotifyListener() in front of the queue to avoid OnDataAvailable + // being called after cancellation. Note that + // HttpTransactionParent::OnStart/StopRequest are driven by IPC messages and + // HttpTransactionChild won't send IPC if already canceled. That's why we have + // to call DoNotifyListener(). + mEventQ->Suspend(); + mEventQ->PrependEvent(MakeUnique( + this, [self = UnsafePtr(this)]() { + self->DoNotifyListener(); + })); + mEventQ->Resume(); + return NS_OK; +} + +void HttpTransactionParent::DoNotifyListener() { + LOG(("HttpTransactionParent::DoNotifyListener this=%p", this)); + MOZ_ASSERT(NS_IsMainThread()); + + if (mChannel && !mOnStartRequestCalled) { + nsCOMPtr listener = mChannel; + mOnStartRequestCalled = true; + listener->OnStartRequest(this); + } + mOnStartRequestCalled = true; + + // This is to make sure that ODA in the event queue can be processed before + // OnStopRequest. + mEventQ->RunOrEnqueue(new NeckoTargetChannelFunctionEvent( + this, [self = UnsafePtr(this)] { + self->ContinueDoNotifyListener(); + })); +} + +void HttpTransactionParent::ContinueDoNotifyListener() { + LOG(("HttpTransactionParent::ContinueDoNotifyListener this=%p", this)); + MOZ_ASSERT(NS_IsMainThread()); + + if (mChannel && !mOnStopRequestCalled) { + nsCOMPtr listener = mChannel; + mOnStopRequestCalled = true; // avoid reentrancy bugs by setting this now + listener->OnStopRequest(this, mStatus); + } + mOnStopRequestCalled = true; + + mChannel = nullptr; +} + +NS_IMETHODIMP +HttpTransactionParent::Suspend() { + MOZ_ASSERT(NS_IsMainThread()); + + // SendSuspend only once, when suspend goes from 0 to 1. + if (!mSuspendCount++ && CanSend()) { + Unused << SendSuspendPump(); + } + mEventQ->Suspend(); + return NS_OK; +} + +NS_IMETHODIMP +HttpTransactionParent::Resume() { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mSuspendCount, "Resume called more than Suspend"); + + // SendResume only once, when suspend count drops to 0. + if (mSuspendCount && !--mSuspendCount) { + if (CanSend()) { + Unused << SendResumePump(); + } + + if (mCallOnResume) { + nsCOMPtr neckoTarget = GetNeckoTarget(); + MOZ_ASSERT(neckoTarget); + + RefPtr self = this; + std::function callOnResume = nullptr; + std::swap(callOnResume, mCallOnResume); + neckoTarget->Dispatch( + NS_NewRunnableFunction("net::HttpTransactionParent::mCallOnResume", + [callOnResume]() { callOnResume(); }), + NS_DISPATCH_NORMAL); + } + } + mEventQ->Resume(); + return NS_OK; +} + +NS_IMETHODIMP +HttpTransactionParent::GetLoadGroup(nsILoadGroup** aLoadGroup) { + MOZ_ASSERT(false, "Should not be called."); + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +HttpTransactionParent::SetLoadGroup(nsILoadGroup* aLoadGroup) { + MOZ_ASSERT(false, "Should not be called."); + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +HttpTransactionParent::GetLoadFlags(nsLoadFlags* aLoadFlags) { + MOZ_ASSERT(false, "Should not be called."); + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +HttpTransactionParent::SetLoadFlags(nsLoadFlags aLoadFlags) { + MOZ_ASSERT(false, "Should not be called."); + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +HttpTransactionParent::GetTRRMode(nsIRequest::TRRMode* aTRRMode) { + MOZ_ASSERT(false, "Should not be called."); + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +HttpTransactionParent::SetTRRMode(nsIRequest::TRRMode aTRRMode) { + MOZ_ASSERT(false, "Should not be called."); + return NS_ERROR_NOT_IMPLEMENTED; +} + +void HttpTransactionParent::ActorDestroy(ActorDestroyReason aWhy) { + LOG(("HttpTransactionParent::ActorDestroy [this=%p]\n", this)); + if (aWhy != Deletion) { + // Make sure all the messages are processed. + AutoEventEnqueuer ensureSerialDispatch(mEventQ); + + mStatus = NS_ERROR_FAILURE; + HandleAsyncAbort(); + + mCanceled = true; + } +} + +void HttpTransactionParent::HandleAsyncAbort() { + MOZ_ASSERT(!mCallOnResume, "How did that happen?"); + + if (mSuspendCount) { + LOG( + ("HttpTransactionParent Waiting until resume to do async notification " + "[this=%p]\n", + this)); + RefPtr self = this; + mCallOnResume = [self]() { self->HandleAsyncAbort(); }; + return; + } + + DoNotifyListener(); +} + +bool HttpTransactionParent::GetSupportsHTTP3() { return mSupportsHTTP3; } + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/HttpTransactionParent.h b/netwerk/protocol/http/HttpTransactionParent.h new file mode 100644 index 0000000000..ec7b032c67 --- /dev/null +++ b/netwerk/protocol/http/HttpTransactionParent.h @@ -0,0 +1,170 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* 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/. */ + +#ifndef HttpTransactionParent_h__ +#define HttpTransactionParent_h__ + +#include "mozilla/Atomics.h" +#include "mozilla/Mutex.h" +#include "mozilla/net/HttpTransactionShell.h" +#include "mozilla/net/NeckoChannelParams.h" +#include "mozilla/net/PHttpTransactionParent.h" +#include "nsHttp.h" +#include "nsCOMPtr.h" +#include "nsIThreadRetargetableRequest.h" +#include "nsITransport.h" +#include "nsIRequest.h" + +namespace mozilla { +namespace net { + +class ChannelEventQueue; +class nsHttpConnectionInfo; + +#define HTTP_TRANSACTION_PARENT_IID \ + { \ + 0xb83695cb, 0xc24b, 0x4c53, { \ + 0x85, 0x9b, 0x77, 0x77, 0x3e, 0xc5, 0x44, 0xe5 \ + } \ + } + +// HttpTransactionParent plays the role of nsHttpTransaction and delegates the +// work to the nsHttpTransaction in socket process. +class HttpTransactionParent final : public PHttpTransactionParent, + public HttpTransactionShell, + public nsIRequest, + public nsIThreadRetargetableRequest { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_HTTPTRANSACTIONSHELL + NS_DECL_NSIREQUEST + NS_DECL_NSITHREADRETARGETABLEREQUEST + NS_DECLARE_STATIC_IID_ACCESSOR(HTTP_TRANSACTION_PARENT_IID) + + explicit HttpTransactionParent(bool aIsDocumentLoad); + + void ActorDestroy(ActorDestroyReason aWhy) override; + + mozilla::ipc::IPCResult RecvOnStartRequest( + const nsresult& aStatus, const Maybe& aResponseHead, + const nsCString& aSecurityInfoSerialization, + const bool& aProxyConnectFailed, const TimingStructArgs& aTimings, + const int32_t& aProxyConnectResponseCode, + nsTArray&& aDataForSniffer, const Maybe& aAltSvcUsed, + const bool& aDataToChildProcess, const bool& aRestarted, + const uint32_t& aHTTPSSVCReceivedStage, const bool& aSupportsHttp3); + mozilla::ipc::IPCResult RecvOnTransportStatus( + const nsresult& aStatus, const int64_t& aProgress, + const int64_t& aProgressMax, + Maybe&& aNetworkAddressArg); + mozilla::ipc::IPCResult RecvOnDataAvailable(const nsCString& aData, + const uint64_t& aOffset, + const uint32_t& aCount); + mozilla::ipc::IPCResult RecvOnStopRequest( + const nsresult& aStatus, const bool& aResponseIsComplete, + const int64_t& aTransferSize, const TimingStructArgs& aTimings, + const Maybe& responseTrailers, + Maybe&& aTransactionObserverResult, + const TimeStamp& aLastActiveTabOptHit, const uint32_t& aCaps, + const HttpConnectionInfoCloneArgs& aArgs); + mozilla::ipc::IPCResult RecvOnInitFailed(const nsresult& aStatus); + + mozilla::ipc::IPCResult RecvOnH2PushStream(const uint32_t& aPushedStreamId, + const nsCString& aResourceUrl, + const nsCString& aRequestString); + + already_AddRefed GetNeckoTarget(); + + void SetSniffedTypeToChannel( + nsInputStreamPump::PeekSegmentFun aCallTypeSniffers, + nsIChannel* aChannel); + + void SetRedirectTimestamp(TimeStamp aRedirectStart, TimeStamp aRedirectEnd) { + mRedirectStart = aRedirectStart; + mRedirectEnd = aRedirectEnd; + } + + private: + virtual ~HttpTransactionParent(); + + void GetStructFromInfo(nsHttpConnectionInfo* aInfo, + HttpConnectionInfoCloneArgs& aArgs); + void DoOnStartRequest( + const nsresult& aStatus, const Maybe& aResponseHead, + const nsCString& aSecurityInfoSerialization, + const bool& aProxyConnectFailed, const TimingStructArgs& aTimings, + const int32_t& aProxyConnectResponseCode, + nsTArray&& aDataForSniffer, const Maybe& aAltSvcUsed, + const bool& aDataToChildProcess, const bool& aRestarted, + const uint32_t& aHTTPSSVCReceivedStage, const bool& aSupportsHttp3); + void DoOnDataAvailable(const nsCString& aData, const uint64_t& aOffset, + const uint32_t& aCount); + void DoOnStopRequest( + const nsresult& aStatus, const bool& aResponseIsComplete, + const int64_t& aTransferSize, const TimingStructArgs& aTimings, + const Maybe& responseTrailers, + Maybe&& aTransactionObserverResult, + const uint32_t& aCaps, nsHttpConnectionInfo* aConnInfo); + void DoNotifyListener(); + void ContinueDoNotifyListener(); + // Get event target for ODA. + already_AddRefed GetODATarget(); + void CancelOnMainThread(nsresult aRv); + void HandleAsyncAbort(); + + nsCOMPtr mEventsink; + nsCOMPtr mChannel; + nsCOMPtr mTargetThread; + nsCOMPtr mODATarget; + Mutex mEventTargetMutex; + nsCOMPtr mSecurityInfo; + UniquePtr mResponseHead; + UniquePtr mResponseTrailers; + RefPtr mEventQ; + + bool mResponseIsComplete; + int64_t mTransferSize; + int64_t mRequestSize; + bool mIsHttp3Used = false; + bool mProxyConnectFailed; + Atomic mCanceled; + Atomic mStatus; + int32_t mSuspendCount; + bool mResponseHeadTaken; + bool mResponseTrailersTaken; + bool mOnStartRequestCalled; + bool mOnStopRequestCalled; + bool mResolvedByTRR; + bool mEchConfigUsed = false; + int32_t mProxyConnectResponseCode; + uint64_t mChannelId; + bool mDataSentToChildProcess; + bool mIsDocumentLoad; + bool mRestarted; + uint32_t mCaps; + TimeStamp mRedirectStart; + TimeStamp mRedirectEnd; + + NetAddr mSelfAddr; + NetAddr mPeerAddr; + TimingStruct mTimings; + TimeStamp mDomainLookupStart; + TimeStamp mDomainLookupEnd; + TransactionObserverFunc mTransactionObserver; + OnPushCallback mOnPushCallback; + nsTArray mDataForSniffer; + std::function mCallOnResume; + uint32_t mHTTPSSVCReceivedStage; + RefPtr mConnInfo; + bool mSupportsHTTP3 = false; +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(HttpTransactionParent, + HTTP_TRANSACTION_PARENT_IID) + +} // namespace net +} // namespace mozilla + +#endif // nsHttpTransactionParent_h__ diff --git a/netwerk/protocol/http/HttpTransactionShell.h b/netwerk/protocol/http/HttpTransactionShell.h new file mode 100644 index 0000000000..00e2f198cb --- /dev/null +++ b/netwerk/protocol/http/HttpTransactionShell.h @@ -0,0 +1,227 @@ +/* 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/. */ + +#ifndef HttpTransactionShell_h__ +#define HttpTransactionShell_h__ + +#include +#include "nsISupports.h" +#include "TimingStruct.h" +#include "nsInputStreamPump.h" +#include "mozilla/Maybe.h" +#include "mozilla/UniquePtr.h" + +class nsIEventTraget; +class nsIInputStream; +class nsIInterfaceRequestor; +class nsIRequest; +class nsIRequestContext; +class nsITransportEventSink; + +namespace mozilla { +namespace net { + +enum HttpTrafficCategory : uint8_t; +class Http2PushedStreamWrapper; +class HttpTransactionParent; +class nsHttpConnectionInfo; +class nsHttpHeaderArray; +class nsHttpRequestHead; +class nsHttpTransaction; +class TransactionObserverResult; +union NetAddr; + +//---------------------------------------------------------------------------- +// Abstract base class for a HTTP transaction in the chrome process +//---------------------------------------------------------------------------- + +// 95e5a5b7-6aa2-4011-920a-0908b52f95d4 +#define HTTPTRANSACTIONSHELL_IID \ + { \ + 0x95e5a5b7, 0x6aa2, 0x4011, { \ + 0x92, 0x0a, 0x09, 0x08, 0xb5, 0x2f, 0x95, 0xd4 \ + } \ + } + +class HttpTransactionShell : public nsISupports { + public: + NS_DECLARE_STATIC_IID_ACCESSOR(HTTPTRANSACTIONSHELL_IID) + + using TransactionObserverFunc = + std::function; + using OnPushCallback = std::function; + + // + // called to initialize the transaction + // + // @param caps + // the transaction capabilities (see nsHttp.h) + // @param connInfo + // the connection type for this transaction. + // @param reqHeaders + // the request header struct + // @param reqBody + // the request body (POST or PUT data stream) + // @param reqBodyIncludesHeaders + // fun stuff to support NPAPI plugins. + // @param target + // the dispatch target were notifications should be sent. + // @param callbacks + // the notification callbacks to be given to PSM. + // @param topLevelOuterContentWindowId + // indicate the top level outer content window in which + // this transaction is being loaded. + [[nodiscard]] nsresult virtual Init( + uint32_t caps, nsHttpConnectionInfo* connInfo, + nsHttpRequestHead* reqHeaders, nsIInputStream* reqBody, + uint64_t reqContentLength, bool reqBodyIncludesHeaders, + nsIEventTarget* consumerTarget, nsIInterfaceRequestor* callbacks, + nsITransportEventSink* eventsink, uint64_t topLevelOuterContentWindowId, + HttpTrafficCategory trafficCategory, nsIRequestContext* requestContext, + uint32_t classOfService, uint32_t initialRwin, + bool responseTimeoutEnabled, uint64_t channelId, + TransactionObserverFunc&& transactionObserver, + OnPushCallback&& aOnPushCallback, + HttpTransactionShell* aTransWithPushedStream, + uint32_t aPushedStreamId) = 0; + + // @param aListener + // receives notifications. + // @param pump + // the pump that will contain the response data. async wait on this + // input stream for data. On first notification, headers should be + // available (check transaction status). + virtual nsresult AsyncRead(nsIStreamListener* listener, + nsIRequest** pump) = 0; + + // Called to take ownership of the response headers; the transaction + // will drop any reference to the response headers after this call. + virtual UniquePtr TakeResponseHead() = 0; + + // Called to take ownership of the trailer headers. + // Returning null if there is no trailer. + virtual UniquePtr TakeResponseTrailers() = 0; + + virtual already_AddRefed SecurityInfo() = 0; + virtual void SetSecurityCallbacks(nsIInterfaceRequestor* aCallbacks) = 0; + + virtual void GetNetworkAddresses(NetAddr& self, NetAddr& peer, + bool& aResolvedByTRR, + bool& aEchConfigUsed) = 0; + + // Functions for Timing interface + virtual mozilla::TimeStamp GetDomainLookupStart() = 0; + virtual mozilla::TimeStamp GetDomainLookupEnd() = 0; + virtual mozilla::TimeStamp GetConnectStart() = 0; + virtual mozilla::TimeStamp GetTcpConnectEnd() = 0; + virtual mozilla::TimeStamp GetSecureConnectionStart() = 0; + + virtual mozilla::TimeStamp GetConnectEnd() = 0; + virtual mozilla::TimeStamp GetRequestStart() = 0; + virtual mozilla::TimeStamp GetResponseStart() = 0; + virtual mozilla::TimeStamp GetResponseEnd() = 0; + + virtual void SetDomainLookupStart(mozilla::TimeStamp timeStamp, + bool onlyIfNull = false) = 0; + virtual void SetDomainLookupEnd(mozilla::TimeStamp timeStamp, + bool onlyIfNull = false) = 0; + + virtual const TimingStruct Timings() = 0; + + // Called to set/find out if the transaction generated a complete response. + virtual bool ResponseIsComplete() = 0; + virtual int64_t GetTransferSize() = 0; + virtual int64_t GetRequestSize() = 0; + virtual bool IsHttp3Used() = 0; + + // Called to notify that a requested DNS cache entry was refreshed. + virtual void SetDNSWasRefreshed() = 0; + + virtual void DontReuseConnection() = 0; + virtual bool HasStickyConnection() const = 0; + + virtual void SetH2WSConnRefTaken() = 0; + + virtual bool ProxyConnectFailed() = 0; + virtual int32_t GetProxyConnectResponseCode() = 0; + + virtual bool DataSentToChildProcess() = 0; + + virtual nsHttpTransaction* AsHttpTransaction() = 0; + virtual HttpTransactionParent* AsHttpTransactionParent() = 0; + + virtual bool TakeRestartedState() = 0; + virtual uint32_t HTTPSSVCReceivedStage() = 0; + + virtual bool Http2Disabled() const = 0; + virtual bool Http3Disabled() const = 0; + virtual already_AddRefed GetConnInfo() const = 0; + + virtual bool GetSupportsHTTP3() = 0; +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(HttpTransactionShell, HTTPTRANSACTIONSHELL_IID) + +#define NS_DECL_HTTPTRANSACTIONSHELL \ + virtual nsresult Init( \ + uint32_t caps, nsHttpConnectionInfo* connInfo, \ + nsHttpRequestHead* reqHeaders, nsIInputStream* reqBody, \ + uint64_t reqContentLength, bool reqBodyIncludesHeaders, \ + nsIEventTarget* consumerTarget, nsIInterfaceRequestor* callbacks, \ + nsITransportEventSink* eventsink, uint64_t topLevelOuterContentWindowId, \ + HttpTrafficCategory trafficCategory, nsIRequestContext* requestContext, \ + uint32_t classOfService, uint32_t initialRwin, \ + bool responseTimeoutEnabled, uint64_t channelId, \ + TransactionObserverFunc&& transactionObserver, \ + OnPushCallback&& aOnPushCallback, \ + HttpTransactionShell* aTransWithPushedStream, uint32_t aPushedStreamId) \ + override; \ + virtual nsresult AsyncRead(nsIStreamListener* listener, nsIRequest** pump) \ + override; \ + virtual UniquePtr TakeResponseHead() override; \ + virtual UniquePtr TakeResponseTrailers() override; \ + virtual already_AddRefed SecurityInfo() override; \ + virtual void SetSecurityCallbacks(nsIInterfaceRequestor* aCallbacks) \ + override; \ + virtual void GetNetworkAddresses(NetAddr& self, NetAddr& peer, \ + bool& aResolvedByTRR, bool& aEchConfigUsed) \ + override; \ + virtual mozilla::TimeStamp GetDomainLookupStart() override; \ + virtual mozilla::TimeStamp GetDomainLookupEnd() override; \ + virtual mozilla::TimeStamp GetConnectStart() override; \ + virtual mozilla::TimeStamp GetTcpConnectEnd() override; \ + virtual mozilla::TimeStamp GetSecureConnectionStart() override; \ + virtual mozilla::TimeStamp GetConnectEnd() override; \ + virtual mozilla::TimeStamp GetRequestStart() override; \ + virtual mozilla::TimeStamp GetResponseStart() override; \ + virtual mozilla::TimeStamp GetResponseEnd() override; \ + virtual void SetDomainLookupStart(mozilla::TimeStamp timeStamp, \ + bool onlyIfNull = false) override; \ + virtual void SetDomainLookupEnd(mozilla::TimeStamp timeStamp, \ + bool onlyIfNull = false) override; \ + virtual const TimingStruct Timings() override; \ + virtual bool ResponseIsComplete() override; \ + virtual int64_t GetTransferSize() override; \ + virtual int64_t GetRequestSize() override; \ + virtual bool IsHttp3Used() override; \ + virtual void SetDNSWasRefreshed() override; \ + virtual void DontReuseConnection() override; \ + virtual bool HasStickyConnection() const override; \ + virtual void SetH2WSConnRefTaken() override; \ + virtual bool ProxyConnectFailed() override; \ + virtual int32_t GetProxyConnectResponseCode() override; \ + virtual bool DataSentToChildProcess() override; \ + virtual nsHttpTransaction* AsHttpTransaction() override; \ + virtual HttpTransactionParent* AsHttpTransactionParent() override; \ + virtual bool TakeRestartedState() override; \ + virtual uint32_t HTTPSSVCReceivedStage() override; \ + virtual bool Http2Disabled() const override; \ + virtual bool Http3Disabled() const override; \ + virtual already_AddRefed GetConnInfo() const override; \ + virtual bool GetSupportsHTTP3() override; +} // namespace net +} // namespace mozilla + +#endif // HttpTransactionShell_h__ diff --git a/netwerk/protocol/http/InterceptedChannel.cpp b/netwerk/protocol/http/InterceptedChannel.cpp new file mode 100644 index 0000000000..09a0f701ee --- /dev/null +++ b/netwerk/protocol/http/InterceptedChannel.cpp @@ -0,0 +1,232 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: -*- */ +/* vim:set expandtab ts=2 sw=2 sts=2 cin: */ +/* 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 "HttpLog.h" + +#include "InterceptedChannel.h" +#include "nsInputStreamPump.h" +#include "nsITimedChannel.h" +#include "nsHttpChannel.h" +#include "HttpChannelChild.h" +#include "nsHttpResponseHead.h" +#include "nsNetUtil.h" +#include "mozilla/ConsoleReportCollector.h" +#include "mozilla/dom/ChannelInfo.h" +#include "nsThreadUtils.h" + +namespace mozilla { +namespace net { + +extern nsresult DoUpdateExpirationTime(nsHttpChannel* aSelf, + nsICacheEntry* aCacheEntry, + nsHttpResponseHead* aResponseHead, + uint32_t& aExpirationTime); +extern nsresult DoAddCacheEntryHeaders(nsHttpChannel* self, + nsICacheEntry* entry, + nsHttpRequestHead* requestHead, + nsHttpResponseHead* responseHead, + nsISupports* securityInfo); + +NS_IMPL_ISUPPORTS(InterceptedChannelBase, nsIInterceptedChannel) + +InterceptedChannelBase::InterceptedChannelBase( + nsINetworkInterceptController* aController) + : mController(aController), + mReportCollector(new ConsoleReportCollector()), + mClosed(false), + mSynthesizedOrReset(Invalid) {} + +void InterceptedChannelBase::EnsureSynthesizedResponse() { + if (mSynthesizedResponseHead.isNothing()) { + mSynthesizedResponseHead.emplace(new nsHttpResponseHead()); + } +} + +void InterceptedChannelBase::DoNotifyController() { + nsresult rv = NS_OK; + + if (NS_WARN_IF(!mController)) { + rv = ResetInterception(); + if (NS_FAILED(rv)) { + NS_WARNING("Failed to resume intercepted network request"); + CancelInterception(rv); + } + return; + } + + rv = mController->ChannelIntercepted(this); + if (NS_WARN_IF(NS_FAILED(rv))) { + rv = ResetInterception(); + if (NS_FAILED(rv)) { + NS_WARNING("Failed to resume intercepted network request"); + CancelInterception(rv); + } + } + mController = nullptr; +} + +void InterceptedChannelBase::DoSynthesizeStatus(uint16_t aStatus, + const nsACString& aReason) { + EnsureSynthesizedResponse(); + + // Always assume HTTP 1.1 for synthesized responses. + nsAutoCString statusLine; + statusLine.AppendLiteral("HTTP/1.1 "); + statusLine.AppendInt(aStatus); + statusLine.AppendLiteral(" "); + statusLine.Append(aReason); + + (*mSynthesizedResponseHead)->ParseStatusLine(statusLine); +} + +nsresult InterceptedChannelBase::DoSynthesizeHeader(const nsACString& aName, + const nsACString& aValue) { + EnsureSynthesizedResponse(); + + nsAutoCString header = aName + ": "_ns + aValue; + // Overwrite any existing header. + return (*mSynthesizedResponseHead)->ParseHeaderLine(header); +} + +NS_IMETHODIMP +InterceptedChannelBase::GetConsoleReportCollector( + nsIConsoleReportCollector** aCollectorOut) { + MOZ_ASSERT(aCollectorOut); + nsCOMPtr ref = mReportCollector; + ref.forget(aCollectorOut); + return NS_OK; +} + +NS_IMETHODIMP +InterceptedChannelBase::SetReleaseHandle(nsISupports* aHandle) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!mReleaseHandle); + MOZ_ASSERT(aHandle); + + // We need to keep it and mChannel alive until destructor clear it up. + mReleaseHandle = aHandle; + return NS_OK; +} + +NS_IMETHODIMP +InterceptedChannelBase::SaveTimeStamps() { + MOZ_ASSERT(NS_IsMainThread()); + + // If we were not able to start the fetch event for some reason (like + // corrupted scripts), then just do nothing here. + if (mHandleFetchEventStart.IsNull()) { + return NS_OK; + } + + nsCOMPtr underlyingChannel; + nsresult rv = GetChannel(getter_AddRefs(underlyingChannel)); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + nsCOMPtr timedChannel = do_QueryInterface(underlyingChannel); + MOZ_ASSERT(timedChannel); + + rv = timedChannel->SetLaunchServiceWorkerStart(mLaunchServiceWorkerStart); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + rv = timedChannel->SetLaunchServiceWorkerEnd(mLaunchServiceWorkerEnd); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + rv = timedChannel->SetDispatchFetchEventStart(mDispatchFetchEventStart); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + rv = timedChannel->SetDispatchFetchEventEnd(mDispatchFetchEventEnd); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + rv = timedChannel->SetHandleFetchEventStart(mHandleFetchEventStart); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + rv = timedChannel->SetHandleFetchEventEnd(mHandleFetchEventEnd); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + nsCOMPtr channel; + GetChannel(getter_AddRefs(channel)); + if (NS_WARN_IF(!channel)) { + return NS_ERROR_FAILURE; + } + + bool isNonSubresourceRequest = + nsContentUtils::IsNonSubresourceRequest(channel); + nsCString navigationOrSubresource = + isNonSubresourceRequest ? "navigation"_ns : "subresource"_ns; + + nsAutoCString subresourceKey(""_ns); + GetSubresourceTimeStampKey(channel, subresourceKey); + + // We may have null timestamps if the fetch dispatch runnable was cancelled + // and we defaulted to resuming the request. + if (!mFinishResponseStart.IsNull() && !mFinishResponseEnd.IsNull()) { + MOZ_ASSERT(mSynthesizedOrReset != Invalid); + + Telemetry::HistogramID id = + (mSynthesizedOrReset == Synthesized) + ? Telemetry:: + SERVICE_WORKER_FETCH_EVENT_FINISH_SYNTHESIZED_RESPONSE_MS + : Telemetry::SERVICE_WORKER_FETCH_EVENT_CHANNEL_RESET_MS; + Telemetry::Accumulate( + id, navigationOrSubresource, + static_cast( + (mFinishResponseEnd - mFinishResponseStart).ToMilliseconds())); + if (!isNonSubresourceRequest && !subresourceKey.IsEmpty()) { + Telemetry::Accumulate( + id, subresourceKey, + static_cast( + (mFinishResponseEnd - mFinishResponseStart).ToMilliseconds())); + } + } + + Telemetry::Accumulate( + Telemetry::SERVICE_WORKER_FETCH_EVENT_DISPATCH_MS, + navigationOrSubresource, + static_cast((mHandleFetchEventStart - mDispatchFetchEventStart) + .ToMilliseconds())); + + if (!isNonSubresourceRequest && !subresourceKey.IsEmpty()) { + Telemetry::Accumulate(Telemetry::SERVICE_WORKER_FETCH_EVENT_DISPATCH_MS, + subresourceKey, + static_cast((mHandleFetchEventStart - + mDispatchFetchEventStart) + .ToMilliseconds())); + } + + if (!mFinishResponseEnd.IsNull()) { + Telemetry::Accumulate( + Telemetry::SERVICE_WORKER_FETCH_INTERCEPTION_DURATION_MS, + navigationOrSubresource, + static_cast( + (mFinishResponseEnd - mDispatchFetchEventStart).ToMilliseconds())); + if (!isNonSubresourceRequest && !subresourceKey.IsEmpty()) { + Telemetry::Accumulate( + Telemetry::SERVICE_WORKER_FETCH_INTERCEPTION_DURATION_MS, + subresourceKey, + static_cast((mFinishResponseEnd - mDispatchFetchEventStart) + .ToMilliseconds())); + } + } + + return rv; +} + +/* static */ +already_AddRefed InterceptedChannelBase::SecureUpgradeChannelURI( + nsIChannel* aChannel) { + nsCOMPtr uri; + nsresult rv = aChannel->GetURI(getter_AddRefs(uri)); + NS_ENSURE_SUCCESS(rv, nullptr); + + nsCOMPtr upgradedURI; + rv = NS_GetSecureUpgradedURI(uri, getter_AddRefs(upgradedURI)); + NS_ENSURE_SUCCESS(rv, nullptr); + + return upgradedURI.forget(); +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/InterceptedChannel.h b/netwerk/protocol/http/InterceptedChannel.h new file mode 100644 index 0000000000..53e96bfe30 --- /dev/null +++ b/netwerk/protocol/http/InterceptedChannel.h @@ -0,0 +1,155 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set expandtab ts=2 sw=2 sts=2 cin: */ +/* 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/. */ + +#ifndef InterceptedChannel_h +#define InterceptedChannel_h + +#include "nsINetworkInterceptController.h" +#include "mozilla/RefPtr.h" +#include "mozilla/Maybe.h" + +class nsICacheEntry; +class nsInputStreamPump; +class nsIStreamListener; + +namespace mozilla { +namespace net { + +class nsHttpChannel; +class HttpChannelChild; +class nsHttpResponseHead; +class InterceptStreamListener; + +// An object representing a channel that has been intercepted. This avoids +// complicating the actual channel implementation with the details of +// synthesizing responses. +class InterceptedChannelBase : public nsIInterceptedChannel { + protected: + // The interception controller to notify about the successful channel + // interception + nsCOMPtr mController; + + // Response head for use when synthesizing + Maybe> mSynthesizedResponseHead; + + nsCOMPtr mReportCollector; + nsCOMPtr mReleaseHandle; + + bool mClosed; + + void EnsureSynthesizedResponse(); + void DoNotifyController(); + void DoSynthesizeStatus(uint16_t aStatus, const nsACString& aReason); + [[nodiscard]] nsresult DoSynthesizeHeader(const nsACString& aName, + const nsACString& aValue); + + TimeStamp mLaunchServiceWorkerStart; + TimeStamp mLaunchServiceWorkerEnd; + TimeStamp mDispatchFetchEventStart; + TimeStamp mDispatchFetchEventEnd; + TimeStamp mHandleFetchEventStart; + TimeStamp mHandleFetchEventEnd; + + TimeStamp mFinishResponseStart; + TimeStamp mFinishResponseEnd; + enum { Invalid = 0, Synthesized, Reset } mSynthesizedOrReset; + + virtual ~InterceptedChannelBase() = default; + + public: + explicit InterceptedChannelBase(nsINetworkInterceptController* aController); + + // Notify the interception controller that the channel has been intercepted + // and prepare the response body output stream. + virtual void NotifyController() = 0; + + NS_DECL_ISUPPORTS + + NS_IMETHOD GetConsoleReportCollector( + nsIConsoleReportCollector** aCollectorOut) override; + NS_IMETHOD SetReleaseHandle(nsISupports* aHandle) override; + + NS_IMETHODIMP + SetLaunchServiceWorkerStart(TimeStamp aTimeStamp) override { + mLaunchServiceWorkerStart = aTimeStamp; + return NS_OK; + } + + NS_IMETHODIMP + GetLaunchServiceWorkerStart(TimeStamp* aTimeStamp) override { + MOZ_DIAGNOSTIC_ASSERT(aTimeStamp); + *aTimeStamp = mLaunchServiceWorkerStart; + return NS_OK; + } + + NS_IMETHODIMP + SetLaunchServiceWorkerEnd(TimeStamp aTimeStamp) override { + mLaunchServiceWorkerEnd = aTimeStamp; + return NS_OK; + } + + NS_IMETHODIMP + GetLaunchServiceWorkerEnd(TimeStamp* aTimeStamp) override { + MOZ_DIAGNOSTIC_ASSERT(aTimeStamp); + *aTimeStamp = mLaunchServiceWorkerEnd; + return NS_OK; + } + + NS_IMETHODIMP + SetDispatchFetchEventStart(TimeStamp aTimeStamp) override { + mDispatchFetchEventStart = aTimeStamp; + return NS_OK; + } + + NS_IMETHODIMP + SetDispatchFetchEventEnd(TimeStamp aTimeStamp) override { + mDispatchFetchEventEnd = aTimeStamp; + return NS_OK; + } + + NS_IMETHODIMP + SetHandleFetchEventStart(TimeStamp aTimeStamp) override { + mHandleFetchEventStart = aTimeStamp; + return NS_OK; + } + + NS_IMETHODIMP + SetHandleFetchEventEnd(TimeStamp aTimeStamp) override { + mHandleFetchEventEnd = aTimeStamp; + return NS_OK; + } + + NS_IMETHODIMP + SetFinishResponseStart(TimeStamp aTimeStamp) override { + mFinishResponseStart = aTimeStamp; + return NS_OK; + } + + NS_IMETHODIMP + SetFinishSynthesizedResponseEnd(TimeStamp aTimeStamp) override { + MOZ_ASSERT(mSynthesizedOrReset == Invalid); + mSynthesizedOrReset = Synthesized; + mFinishResponseEnd = aTimeStamp; + return NS_OK; + } + + NS_IMETHODIMP + SetChannelResetEnd(TimeStamp aTimeStamp) override { + MOZ_ASSERT(mSynthesizedOrReset == Invalid); + mSynthesizedOrReset = Reset; + mFinishResponseEnd = aTimeStamp; + return NS_OK; + } + + NS_IMETHODIMP SaveTimeStamps() override; + + static already_AddRefed SecureUpgradeChannelURI(nsIChannel* aChannel); +}; + +} // namespace net +} // namespace mozilla + +#endif // InterceptedChannel_h diff --git a/netwerk/protocol/http/InterceptedHttpChannel.cpp b/netwerk/protocol/http/InterceptedHttpChannel.cpp new file mode 100644 index 0000000000..667990b9d4 --- /dev/null +++ b/netwerk/protocol/http/InterceptedHttpChannel.cpp @@ -0,0 +1,1311 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et 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 "InterceptedHttpChannel.h" +#include "nsContentSecurityManager.h" +#include "nsEscape.h" +#include "mozilla/SchedulerGroup.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/dom/ChannelInfo.h" +#include "mozilla/dom/PerformanceStorage.h" +#include "nsHttpChannel.h" +#include "nsIRedirectResultListener.h" +#include "nsStringStream.h" +#include "nsStreamUtils.h" + +namespace mozilla { +namespace net { + +NS_IMPL_ISUPPORTS_INHERITED(InterceptedHttpChannel, HttpBaseChannel, + nsIInterceptedChannel, nsICacheInfoChannel, + nsIAsyncVerifyRedirectCallback, nsIRequestObserver, + nsIStreamListener, nsIThreadRetargetableRequest, + nsIThreadRetargetableStreamListener) + +InterceptedHttpChannel::InterceptedHttpChannel( + PRTime aCreationTime, const TimeStamp& aCreationTimestamp, + const TimeStamp& aAsyncOpenTimestamp) + : HttpAsyncAborter(this), + mProgress(0), + mProgressReported(0), + mSynthesizedStreamLength(-1), + mResumeStartPos(0), + mSynthesizedOrReset(Invalid), + mCallingStatusAndProgress(false) { + // Pre-set the creation and AsyncOpen times based on the original channel + // we are intercepting. We don't want our extra internal redirect to mask + // any time spent processing the channel. + mChannelCreationTime = aCreationTime; + mChannelCreationTimestamp = aCreationTimestamp; + mAsyncOpenTime = aAsyncOpenTimestamp; +} + +void InterceptedHttpChannel::ReleaseListeners() { + if (mLoadGroup) { + mLoadGroup->RemoveRequest(this, nullptr, mStatus); + } + HttpBaseChannel::ReleaseListeners(); + mSynthesizedResponseHead.reset(); + mRedirectChannel = nullptr; + mBodyReader = nullptr; + mReleaseHandle = nullptr; + mProgressSink = nullptr; + mBodyCallback = nullptr; + mPump = nullptr; + + MOZ_DIAGNOSTIC_ASSERT(!LoadIsPending()); +} + +nsresult InterceptedHttpChannel::SetupReplacementChannel( + nsIURI* aURI, nsIChannel* aChannel, bool aPreserveMethod, + uint32_t aRedirectFlags) { + nsresult rv = HttpBaseChannel::SetupReplacementChannel( + aURI, aChannel, aPreserveMethod, aRedirectFlags); + if (NS_FAILED(rv)) { + return rv; + } + + rv = CheckRedirectLimit(aRedirectFlags); + NS_ENSURE_SUCCESS(rv, rv); + + // While we can't resume an synthetic response, we can still propagate + // the resume params across redirects for other channels to handle. + if (mResumeStartPos > 0) { + nsCOMPtr resumable = do_QueryInterface(aChannel); + if (!resumable) { + return NS_ERROR_NOT_RESUMABLE; + } + + resumable->ResumeAt(mResumeStartPos, mResumeEntityId); + } + + return NS_OK; +} + +void InterceptedHttpChannel::AsyncOpenInternal() { + // If an error occurs in this file we must ensure mListener callbacks are + // invoked in some way. We either Cancel() or ResetInterception below + // depending on which path we take. + nsresult rv = NS_OK; + + // We should have pre-set the AsyncOpen time based on the original channel if + // timings are enabled. + if (LoadTimingEnabled()) { + MOZ_DIAGNOSTIC_ASSERT(!mAsyncOpenTime.IsNull()); + } + + StoreIsPending(true); + StoreResponseCouldBeSynthesized(true); + + if (mLoadGroup) { + mLoadGroup->AddRequest(this, nullptr); + } + + // If we already have a synthesized body then we are pre-synthesized. + // This can happen for two reasons: + // 1. We have a pre-synthesized redirect in e10s mode. In this case + // we should follow the redirect. + // 2. We are handling a "fake" redirect for an opaque response. Here + // we should just process the synthetic body. + if (mBodyReader) { + // If we fail in this path, then cancel the channel. We don't want + // to ResetInterception() after a synthetic result has already been + // produced by the ServiceWorker. + auto autoCancel = MakeScopeExit([&] { + if (NS_FAILED(rv)) { + Cancel(rv); + } + }); + + if (ShouldRedirect()) { + rv = FollowSyntheticRedirect(); + return; + } + + rv = StartPump(); + return; + } + + // If we fail the initial interception, then attempt to ResetInterception + // to fall back to network. We only cancel if the reset fails. + auto autoReset = MakeScopeExit([&] { + if (NS_FAILED(rv)) { + rv = ResetInterception(); + if (NS_WARN_IF(NS_FAILED(rv))) { + Cancel(rv); + } + } + }); + + // Otherwise we need to trigger a FetchEvent in a ServiceWorker. + nsCOMPtr controller; + GetCallback(controller); + + if (NS_WARN_IF(!controller)) { + rv = NS_ERROR_DOM_INVALID_STATE_ERR; + return; + } + + rv = controller->ChannelIntercepted(this); + NS_ENSURE_SUCCESS_VOID(rv); +} + +bool InterceptedHttpChannel::ShouldRedirect() const { + // Determine if the synthetic response requires us to perform a real redirect. + return nsHttpChannel::WillRedirect(*mResponseHead) && + !mLoadInfo->GetDontFollowRedirects(); +} + +nsresult InterceptedHttpChannel::FollowSyntheticRedirect() { + // Perform a real redirect based on the synthetic response. + + nsCOMPtr ioService; + nsresult rv = gHttpHandler->GetIOService(getter_AddRefs(ioService)); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString location; + rv = mResponseHead->GetHeader(nsHttp::Location, location); + NS_ENSURE_SUCCESS(rv, NS_ERROR_FAILURE); + + // make sure non-ASCII characters in the location header are escaped. + nsAutoCString locationBuf; + if (NS_EscapeURL(location.get(), -1, esc_OnlyNonASCII | esc_Spaces, + locationBuf)) { + location = locationBuf; + } + + nsCOMPtr redirectURI; + rv = ioService->NewURI(nsDependentCString(location.get()), nullptr, mURI, + getter_AddRefs(redirectURI)); + NS_ENSURE_SUCCESS(rv, NS_ERROR_CORRUPTED_CONTENT); + + uint32_t redirectFlags = nsIChannelEventSink::REDIRECT_TEMPORARY; + if (nsHttp::IsPermanentRedirect(mResponseHead->Status())) { + redirectFlags = nsIChannelEventSink::REDIRECT_PERMANENT; + } + + PropagateReferenceIfNeeded(mURI, redirectURI); + + bool rewriteToGET = ShouldRewriteRedirectToGET(mResponseHead->Status(), + mRequestHead.ParsedMethod()); + + nsCOMPtr newChannel; + nsCOMPtr redirectLoadInfo = + CloneLoadInfoForRedirect(redirectURI, redirectFlags); + rv = NS_NewChannelInternal(getter_AddRefs(newChannel), redirectURI, + redirectLoadInfo, + nullptr, // PerformanceStorage + nullptr, // aLoadGroup + nullptr, // aCallbacks + mLoadFlags, ioService); + NS_ENSURE_SUCCESS(rv, rv); + + rv = SetupReplacementChannel(redirectURI, newChannel, !rewriteToGET, + redirectFlags); + NS_ENSURE_SUCCESS(rv, rv); + + mRedirectChannel = std::move(newChannel); + + rv = gHttpHandler->AsyncOnChannelRedirect(this, mRedirectChannel, + redirectFlags); + + if (NS_WARN_IF(NS_FAILED(rv))) { + OnRedirectVerifyCallback(rv); + } + + return rv; +} + +nsresult InterceptedHttpChannel::RedirectForResponseURL( + nsIURI* aResponseURI, bool aResponseRedirected) { + // Perform a service worker redirect to another InterceptedHttpChannel using + // the given response URL. It allows content to see the final URL where + // appropriate and also helps us enforce cross-origin restrictions. The + // resulting channel will then process the synthetic response as normal. This + // extra redirect is performed so that listeners treat the result as unsafe + // cross-origin data. + + nsresult rv = NS_OK; + + // We want to pass ownership of the body callback to the new synthesized + // channel. We need to hold a reference to the callbacks on the stack + // as well, though, so we can call them if a failure occurs. + nsCOMPtr bodyCallback = std::move(mBodyCallback); + + RefPtr newChannel = CreateForSynthesis( + mResponseHead.get(), mBodyReader, bodyCallback, mChannelCreationTime, + mChannelCreationTimestamp, mAsyncOpenTime); + + // If the response has been redirected, propagate all the URLs to content. + // Thus, the exact value of the redirect flag does not matter as long as it's + // not REDIRECT_INTERNAL. + uint32_t flags = aResponseRedirected ? nsIChannelEventSink::REDIRECT_TEMPORARY + : nsIChannelEventSink::REDIRECT_INTERNAL; + + nsCOMPtr redirectLoadInfo = + CloneLoadInfoForRedirect(aResponseURI, flags); + + ExtContentPolicyType contentPolicyType = + redirectLoadInfo->GetExternalContentPolicyType(); + + rv = newChannel->Init( + aResponseURI, mCaps, static_cast(mProxyInfo.get()), + mProxyResolveFlags, mProxyURI, mChannelId, contentPolicyType); + + newChannel->SetLoadInfo(redirectLoadInfo); + NS_ENSURE_SUCCESS(rv, rv); + + // Normally we don't propagate the LoadInfo's service worker tainting + // synthesis flag on redirect. A real redirect normally will want to allow + // normal tainting to proceed from its starting taint. For this particular + // redirect, though, we are performing a redirect to communicate the URL of + // the service worker synthetic response itself. This redirect still + // represents the synthetic response, so we must preserve the flag. + if (redirectLoadInfo && mLoadInfo && + mLoadInfo->GetServiceWorkerTaintingSynthesized()) { + redirectLoadInfo->SynthesizeServiceWorkerTainting(mLoadInfo->GetTainting()); + } + + rv = SetupReplacementChannel(aResponseURI, newChannel, true, flags); + NS_ENSURE_SUCCESS(rv, rv); + + mRedirectChannel = newChannel; + + MOZ_ASSERT(mBodyReader); + MOZ_ASSERT(!LoadApplyConversion()); + newChannel->SetApplyConversion(false); + + rv = gHttpHandler->AsyncOnChannelRedirect(this, mRedirectChannel, flags); + + if (NS_FAILED(rv)) { + // Make sure to call the body callback since we took ownership + // above. Neither the new channel or our standard + // OnRedirectVerifyCallback() code will invoke the callback. Do it here. + bodyCallback->BodyComplete(rv); + + OnRedirectVerifyCallback(rv); + } + + return rv; +} + +nsresult InterceptedHttpChannel::StartPump() { + MOZ_DIAGNOSTIC_ASSERT(!mPump); + MOZ_DIAGNOSTIC_ASSERT(mBodyReader); + + // We don't support resuming an intercepted channel. We can't guarantee the + // ServiceWorker will always return the same data and we can't rely on the + // http cache code to detect changes. For now, just force the channel to + // NS_ERROR_NOT_RESUMABLE which should cause the front-end to recreate the + // channel without calling ResumeAt(). + // + // It would also be possible to convert this information to a range request, + // but its unclear if we should do that for ServiceWorker FetchEvents. See: + // + // https://github.com/w3c/ServiceWorker/issues/1201 + if (mResumeStartPos > 0) { + return NS_ERROR_NOT_RESUMABLE; + } + + // For progress we trust the content-length for the "maximum" size. + // We can't determine the full size from the stream itself since + // we may only receive the data incrementally. We can't trust + // Available() here. + // TODO: We could implement an nsIFixedLengthInputStream interface and + // QI to it here. This would let us determine the total length + // for streams that support it. See bug 1388774. + Unused << GetContentLength(&mSynthesizedStreamLength); + + nsresult rv = + nsInputStreamPump::Create(getter_AddRefs(mPump), mBodyReader, 0, 0, true); + NS_ENSURE_SUCCESS(rv, rv); + + rv = mPump->AsyncRead(this); + NS_ENSURE_SUCCESS(rv, rv); + + uint32_t suspendCount = mSuspendCount; + while (suspendCount--) { + mPump->Suspend(); + } + + MOZ_DIAGNOSTIC_ASSERT(!mCanceled); + + return rv; +} + +nsresult InterceptedHttpChannel::OpenRedirectChannel() { + nsresult rv = NS_OK; + + if (NS_FAILED(mStatus)) { + return mStatus; + } + + if (!mRedirectChannel) { + return NS_ERROR_DOM_ABORT_ERR; + } + + // Make sure to do this after we received redirect veto answer, + // i.e. after all sinks had been notified + mRedirectChannel->SetOriginalURI(mOriginalURI); + + // open new channel + rv = mRedirectChannel->AsyncOpen(mListener); + NS_ENSURE_SUCCESS(rv, rv); + + mStatus = NS_BINDING_REDIRECTED; + + return rv; +} + +void InterceptedHttpChannel::MaybeCallStatusAndProgress() { + // OnStatus() and OnProgress() must only be called on the main thread. If + // we are on a separate thread, then we maybe need to schedule a runnable + // to call them asynchronousnly. + if (!NS_IsMainThread()) { + // Check to see if we are already trying to call OnStatus/OnProgress + // asynchronously. If we are, then don't queue up another runnable. + // We don't want to flood the main thread. + if (mCallingStatusAndProgress) { + return; + } + mCallingStatusAndProgress = true; + + nsCOMPtr r = NewRunnableMethod( + "InterceptedHttpChannel::MaybeCallStatusAndProgress", this, + &InterceptedHttpChannel::MaybeCallStatusAndProgress); + MOZ_ALWAYS_SUCCEEDS( + SchedulerGroup::Dispatch(TaskCategory::Other, r.forget())); + + return; + } + + MOZ_ASSERT(NS_IsMainThread()); + + // We are about to capture out progress position. Clear the flag we use + // to de-duplicate progress report runnables. We want any further progress + // updates to trigger another runnable. We do this before capture the + // progress value since we're using atomics and not a mutex lock. + mCallingStatusAndProgress = false; + + // Capture the current status from our atomic count. + int64_t progress = mProgress; + + MOZ_DIAGNOSTIC_ASSERT(progress >= mProgressReported); + + // Do nothing if we've already made the calls for this amount of progress + // or if the channel is not configured for these calls. Note, the check + // for mProgressSink here means we will not fire any spurious late calls + // after ReleaseListeners() is executed. + if (progress <= mProgressReported || mCanceled || !mProgressSink || + (mLoadFlags & HttpBaseChannel::LOAD_BACKGROUND)) { + return; + } + + // Capture the host name on the first set of calls to avoid doing this + // string processing repeatedly. + if (mProgressReported == 0) { + nsAutoCString host; + MOZ_ALWAYS_SUCCEEDS(mURI->GetHost(host)); + CopyUTF8toUTF16(host, mStatusHost); + } + + mProgressSink->OnStatus(this, NS_NET_STATUS_READING, mStatusHost.get()); + + mProgressSink->OnProgress(this, progress, mSynthesizedStreamLength); + + mProgressReported = progress; +} + +void InterceptedHttpChannel::MaybeCallBodyCallback() { + nsCOMPtr callback = std::move(mBodyCallback); + if (callback) { + callback->BodyComplete(mStatus); + } +} + +// static +already_AddRefed +InterceptedHttpChannel::CreateForInterception( + PRTime aCreationTime, const TimeStamp& aCreationTimestamp, + const TimeStamp& aAsyncOpenTimestamp) { + // Create an InterceptedHttpChannel that will trigger a FetchEvent + // in a ServiceWorker when opened. + RefPtr ref = new InterceptedHttpChannel( + aCreationTime, aCreationTimestamp, aAsyncOpenTimestamp); + + return ref.forget(); +} + +// static +already_AddRefed +InterceptedHttpChannel::CreateForSynthesis( + const nsHttpResponseHead* aHead, nsIInputStream* aBody, + nsIInterceptedBodyCallback* aBodyCallback, PRTime aCreationTime, + const TimeStamp& aCreationTimestamp, const TimeStamp& aAsyncOpenTimestamp) { + MOZ_DIAGNOSTIC_ASSERT(aHead); + MOZ_DIAGNOSTIC_ASSERT(aBody); + + // Create an InterceptedHttpChannel that already has a synthesized response. + // The synthetic response will be processed when opened. A FetchEvent + // will not be triggered. + RefPtr ref = new InterceptedHttpChannel( + aCreationTime, aCreationTimestamp, aAsyncOpenTimestamp); + + ref->mResponseHead = MakeUnique(*aHead); + ref->mBodyReader = aBody; + ref->mBodyCallback = aBodyCallback; + + return ref.forget(); +} + +NS_IMETHODIMP +InterceptedHttpChannel::Cancel(nsresult aStatus) { + // Note: This class has been designed to send all error results through + // Cancel(). Don't add calls directly to AsyncAbort() or + // DoNotifyListener(). Instead call Cancel(). + + if (mCanceled) { + return NS_OK; + } + mCanceled = true; + + MOZ_DIAGNOSTIC_ASSERT(NS_FAILED(aStatus)); + if (NS_SUCCEEDED(mStatus)) { + mStatus = aStatus; + } + + if (mPump) { + return mPump->Cancel(mStatus); + } + + return AsyncAbort(mStatus); +} + +NS_IMETHODIMP +InterceptedHttpChannel::Suspend(void) { + ++mSuspendCount; + if (mPump) { + return mPump->Suspend(); + } + return NS_OK; +} + +NS_IMETHODIMP +InterceptedHttpChannel::Resume(void) { + --mSuspendCount; + if (mPump) { + return mPump->Resume(); + } + return NS_OK; +} + +NS_IMETHODIMP +InterceptedHttpChannel::GetSecurityInfo(nsISupports** aSecurityInfo) { + nsCOMPtr ref(mSecurityInfo); + ref.forget(aSecurityInfo); + return NS_OK; +} + +NS_IMETHODIMP +InterceptedHttpChannel::AsyncOpen(nsIStreamListener* aListener) { + nsCOMPtr listener(aListener); + nsresult rv = + nsContentSecurityManager::doContentSecurityCheck(this, listener); + if (NS_WARN_IF(NS_FAILED(rv))) { + Cancel(rv); + return rv; + } + if (mCanceled) { + return mStatus; + } + + // After this point we should try to return NS_OK and notify the listener + // of the result. + mListener = aListener; + + AsyncOpenInternal(); + + return NS_OK; +} + +NS_IMETHODIMP +InterceptedHttpChannel::LogBlockedCORSRequest(const nsAString& aMessage, + const nsACString& aCategory) { + // Synthetic responses should not trigger CORS blocking. + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +InterceptedHttpChannel::LogMimeTypeMismatch(const nsACString& aMessageName, + bool aWarning, + const nsAString& aURL, + const nsAString& aContentType) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +InterceptedHttpChannel::SetupFallbackChannel(const char* aFallbackKey) { + // AppCache should not be used with service worker intercepted channels. + // This should never be called. + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +InterceptedHttpChannel::GetIsAuthChannel(bool* aIsAuthChannel) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +InterceptedHttpChannel::SetPriority(int32_t aPriority) { + mPriority = clamped(aPriority, INT16_MIN, INT16_MAX); + return NS_OK; +} + +NS_IMETHODIMP +InterceptedHttpChannel::SetClassFlags(uint32_t aClassFlags) { + mClassOfService = aClassFlags; + return NS_OK; +} + +NS_IMETHODIMP +InterceptedHttpChannel::ClearClassFlags(uint32_t aClassFlags) { + mClassOfService &= ~aClassFlags; + return NS_OK; +} + +NS_IMETHODIMP +InterceptedHttpChannel::AddClassFlags(uint32_t aClassFlags) { + mClassOfService |= aClassFlags; + return NS_OK; +} + +NS_IMETHODIMP +InterceptedHttpChannel::ResumeAt(uint64_t aStartPos, + const nsACString& aEntityId) { + // We don't support resuming synthesized responses, but we do track this + // information so it can be passed on to the resulting nsHttpChannel if + // ResetInterception is called. + mResumeStartPos = aStartPos; + mResumeEntityId = aEntityId; + return NS_OK; +} + +void InterceptedHttpChannel::DoNotifyListenerCleanup() { + // Prefer to cleanup in ReleaseListeners() as it seems to be called + // more consistently in necko. +} + +void InterceptedHttpChannel::DoAsyncAbort(nsresult aStatus) { + Unused << AsyncAbort(aStatus); +} + +NS_IMETHODIMP +InterceptedHttpChannel::ResetInterception(void) { + if (mCanceled) { + return mStatus; + } + + uint32_t flags = nsIChannelEventSink::REDIRECT_INTERNAL; + + nsCOMPtr newChannel; + nsCOMPtr redirectLoadInfo = + CloneLoadInfoForRedirect(mURI, flags); + nsresult rv = + NS_NewChannelInternal(getter_AddRefs(newChannel), mURI, redirectLoadInfo, + nullptr, // PerformanceStorage + nullptr, // aLoadGroup + nullptr, // aCallbacks + mLoadFlags); + NS_ENSURE_SUCCESS(rv, rv); + + rv = SetupReplacementChannel(mURI, newChannel, true, flags); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr newTimedChannel = do_QueryInterface(newChannel); + if (newTimedChannel) { + if (!mAsyncOpenTime.IsNull()) { + newTimedChannel->SetAsyncOpen(mAsyncOpenTime); + } + if (!mChannelCreationTimestamp.IsNull()) { + newTimedChannel->SetChannelCreation(mChannelCreationTimestamp); + } + } + + if (mRedirectMode != nsIHttpChannelInternal::REDIRECT_MODE_MANUAL) { + nsLoadFlags loadFlags = nsIRequest::LOAD_NORMAL; + rv = newChannel->GetLoadFlags(&loadFlags); + NS_ENSURE_SUCCESS(rv, rv); + loadFlags |= nsIChannel::LOAD_BYPASS_SERVICE_WORKER; + rv = newChannel->SetLoadFlags(loadFlags); + NS_ENSURE_SUCCESS(rv, rv); + } + + mRedirectChannel = std::move(newChannel); + + rv = gHttpHandler->AsyncOnChannelRedirect(this, mRedirectChannel, flags); + + if (NS_FAILED(rv)) { + OnRedirectVerifyCallback(rv); + } + + return rv; +} + +NS_IMETHODIMP +InterceptedHttpChannel::SynthesizeStatus(uint16_t aStatus, + const nsACString& aReason) { + if (mCanceled) { + return mStatus; + } + + if (!mSynthesizedResponseHead) { + mSynthesizedResponseHead.reset(new nsHttpResponseHead()); + } + + nsAutoCString statusLine; + statusLine.AppendLiteral("HTTP/1.1 "); + statusLine.AppendInt(aStatus); + statusLine.AppendLiteral(" "); + statusLine.Append(aReason); + + mSynthesizedResponseHead->ParseStatusLine(statusLine); + return NS_OK; +} + +NS_IMETHODIMP +InterceptedHttpChannel::SynthesizeHeader(const nsACString& aName, + const nsACString& aValue) { + if (mCanceled) { + return mStatus; + } + + if (!mSynthesizedResponseHead) { + mSynthesizedResponseHead.reset(new nsHttpResponseHead()); + } + + nsAutoCString header = aName + ": "_ns + aValue; + // Overwrite any existing header. + nsresult rv = mSynthesizedResponseHead->ParseHeaderLine(header); + NS_ENSURE_SUCCESS(rv, rv); + return NS_OK; +} + +NS_IMETHODIMP +InterceptedHttpChannel::StartSynthesizedResponse( + nsIInputStream* aBody, nsIInterceptedBodyCallback* aBodyCallback, + nsICacheInfoChannel* aSynthesizedCacheInfo, const nsACString& aFinalURLSpec, + bool aResponseRedirected) { + nsresult rv = NS_OK; + + auto autoCleanup = MakeScopeExit([&] { + // Auto-cancel on failure. Do this first to get mStatus set, if necessary. + if (NS_FAILED(rv)) { + Cancel(rv); + } + + // If we early exit before taking ownership of the body, then automatically + // invoke the callback. This could be due to an error or because we're not + // going to consume it due to a redirect, etc. + if (aBodyCallback) { + aBodyCallback->BodyComplete(mStatus); + } + }); + + if (NS_FAILED(mStatus)) { + // Return NS_OK. The channel should fire callbacks with an error code + // if it was cancelled before this point. + return NS_OK; + } + + // Take ownership of the body callbacks If a failure occurs we will + // automatically Cancel() the channel. This will then invoke OnStopRequest() + // which will invoke the correct callback. In the case of an opaque response + // redirect we pass ownership of the callback to the new channel. + mBodyCallback = aBodyCallback; + aBodyCallback = nullptr; + + mSynthesizedCacheInfo = aSynthesizedCacheInfo; + + if (!mSynthesizedResponseHead) { + mSynthesizedResponseHead.reset(new nsHttpResponseHead()); + } + + mResponseHead = std::move(mSynthesizedResponseHead); + + if (ShouldRedirect()) { + rv = FollowSyntheticRedirect(); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; + } + + // Intercepted responses should already be decoded. + SetApplyConversion(false); + + // Errors and redirects may not have a body. Synthesize an empty string + // stream here so later code can be simpler. + mBodyReader = aBody; + if (!mBodyReader) { + rv = NS_NewCStringInputStream(getter_AddRefs(mBodyReader), ""_ns); + NS_ENSURE_SUCCESS(rv, rv); + } + + nsCOMPtr responseURI; + if (!aFinalURLSpec.IsEmpty()) { + rv = NS_NewURI(getter_AddRefs(responseURI), aFinalURLSpec); + NS_ENSURE_SUCCESS(rv, rv); + } else { + responseURI = mURI; + } + + bool equal = false; + Unused << mURI->Equals(responseURI, &equal); + if (!equal) { + rv = RedirectForResponseURL(responseURI, aResponseRedirected); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; + } + + rv = StartPump(); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +NS_IMETHODIMP +InterceptedHttpChannel::FinishSynthesizedResponse() { + if (mCanceled) { + // Return NS_OK. The channel should fire callbacks with an error code + // if it was cancelled before this point. + return NS_OK; + } + + // TODO: Remove this API after interception moves to the parent process in + // e10s mode. + + return NS_OK; +} + +NS_IMETHODIMP +InterceptedHttpChannel::CancelInterception(nsresult aStatus) { + return Cancel(aStatus); +} + +NS_IMETHODIMP +InterceptedHttpChannel::GetChannel(nsIChannel** aChannel) { + nsCOMPtr ref(this); + ref.forget(aChannel); + return NS_OK; +} + +NS_IMETHODIMP +InterceptedHttpChannel::GetSecureUpgradedChannelURI( + nsIURI** aSecureUpgradedChannelURI) { + nsCOMPtr ref(mURI); + ref.forget(aSecureUpgradedChannelURI); + return NS_OK; +} + +NS_IMETHODIMP +InterceptedHttpChannel::SetChannelInfo( + mozilla::dom::ChannelInfo* aChannelInfo) { + return aChannelInfo->ResurrectInfoOnChannel(this); +} + +NS_IMETHODIMP +InterceptedHttpChannel::GetInternalContentPolicyType( + nsContentPolicyType* aPolicyType) { + if (mLoadInfo) { + *aPolicyType = mLoadInfo->InternalContentPolicyType(); + } + return NS_OK; +} + +NS_IMETHODIMP +InterceptedHttpChannel::GetConsoleReportCollector( + nsIConsoleReportCollector** aConsoleReportCollector) { + nsCOMPtr ref(this); + ref.forget(aConsoleReportCollector); + return NS_OK; +} + +NS_IMETHODIMP +InterceptedHttpChannel::GetLaunchServiceWorkerStart( + mozilla::TimeStamp* aTimeStamp) { + return HttpBaseChannel::GetLaunchServiceWorkerStart(aTimeStamp); +} + +NS_IMETHODIMP +InterceptedHttpChannel::SetLaunchServiceWorkerStart( + mozilla::TimeStamp aTimeStamp) { + return HttpBaseChannel::SetLaunchServiceWorkerStart(aTimeStamp); +} + +NS_IMETHODIMP +InterceptedHttpChannel::GetLaunchServiceWorkerEnd( + mozilla::TimeStamp* aTimeStamp) { + return HttpBaseChannel::GetLaunchServiceWorkerEnd(aTimeStamp); +} + +NS_IMETHODIMP +InterceptedHttpChannel::SetLaunchServiceWorkerEnd( + mozilla::TimeStamp aTimeStamp) { + return HttpBaseChannel::SetLaunchServiceWorkerEnd(aTimeStamp); +} + +NS_IMETHODIMP +InterceptedHttpChannel::SetDispatchFetchEventStart( + mozilla::TimeStamp aTimeStamp) { + return HttpBaseChannel::SetDispatchFetchEventStart(aTimeStamp); +} + +NS_IMETHODIMP +InterceptedHttpChannel::SetDispatchFetchEventEnd( + mozilla::TimeStamp aTimeStamp) { + return HttpBaseChannel::SetDispatchFetchEventEnd(aTimeStamp); +} + +NS_IMETHODIMP +InterceptedHttpChannel::SetHandleFetchEventStart( + mozilla::TimeStamp aTimeStamp) { + return HttpBaseChannel::SetHandleFetchEventStart(aTimeStamp); +} + +NS_IMETHODIMP +InterceptedHttpChannel::SetHandleFetchEventEnd(mozilla::TimeStamp aTimeStamp) { + return HttpBaseChannel::SetHandleFetchEventEnd(aTimeStamp); +} + +NS_IMETHODIMP +InterceptedHttpChannel::SetFinishResponseStart(mozilla::TimeStamp aTimeStamp) { + mFinishResponseStart = aTimeStamp; + return NS_OK; +} + +NS_IMETHODIMP +InterceptedHttpChannel::SetFinishSynthesizedResponseEnd( + mozilla::TimeStamp aTimeStamp) { + MOZ_ASSERT(mSynthesizedOrReset == Invalid); + mSynthesizedOrReset = Synthesized; + mFinishResponseEnd = aTimeStamp; + return NS_OK; +} + +NS_IMETHODIMP +InterceptedHttpChannel::SetChannelResetEnd(mozilla::TimeStamp aTimeStamp) { + MOZ_ASSERT(mSynthesizedOrReset == Invalid); + mSynthesizedOrReset = Reset; + mFinishResponseEnd = aTimeStamp; + return NS_OK; +} + +NS_IMETHODIMP +InterceptedHttpChannel::SaveTimeStamps(void) { + // If we were not able to start the fetch event for some reason (like + // corrupted scripts), then just do nothing here. + if (mHandleFetchEventStart.IsNull()) { + return NS_OK; + } + + bool isNonSubresourceRequest = nsContentUtils::IsNonSubresourceRequest(this); + nsCString navigationOrSubresource = + isNonSubresourceRequest ? "navigation"_ns : "subresource"_ns; + + nsAutoCString subresourceKey(""_ns); + GetSubresourceTimeStampKey(this, subresourceKey); + + // We may have null timestamps if the fetch dispatch runnable was cancelled + // and we defaulted to resuming the request. + if (!mFinishResponseStart.IsNull() && !mFinishResponseEnd.IsNull()) { + Telemetry::HistogramID id = + (mSynthesizedOrReset == Synthesized) + ? Telemetry:: + SERVICE_WORKER_FETCH_EVENT_FINISH_SYNTHESIZED_RESPONSE_MS + : Telemetry::SERVICE_WORKER_FETCH_EVENT_CHANNEL_RESET_MS; + Telemetry::Accumulate( + id, navigationOrSubresource, + static_cast( + (mFinishResponseEnd - mFinishResponseStart).ToMilliseconds())); + if (!isNonSubresourceRequest && !subresourceKey.IsEmpty()) { + Telemetry::Accumulate( + id, subresourceKey, + static_cast( + (mFinishResponseEnd - mFinishResponseStart).ToMilliseconds())); + } + } + + Telemetry::Accumulate( + Telemetry::SERVICE_WORKER_FETCH_EVENT_DISPATCH_MS, + navigationOrSubresource, + static_cast((mHandleFetchEventStart - mDispatchFetchEventStart) + .ToMilliseconds())); + + if (!isNonSubresourceRequest && !subresourceKey.IsEmpty()) { + Telemetry::Accumulate(Telemetry::SERVICE_WORKER_FETCH_EVENT_DISPATCH_MS, + subresourceKey, + static_cast((mHandleFetchEventStart - + mDispatchFetchEventStart) + .ToMilliseconds())); + } + + if (!mFinishResponseEnd.IsNull()) { + Telemetry::Accumulate( + Telemetry::SERVICE_WORKER_FETCH_INTERCEPTION_DURATION_MS, + navigationOrSubresource, + static_cast( + (mFinishResponseEnd - mDispatchFetchEventStart).ToMilliseconds())); + if (!isNonSubresourceRequest && !subresourceKey.IsEmpty()) { + Telemetry::Accumulate( + Telemetry::SERVICE_WORKER_FETCH_INTERCEPTION_DURATION_MS, + subresourceKey, + static_cast((mFinishResponseEnd - mDispatchFetchEventStart) + .ToMilliseconds())); + } + } + + return NS_OK; +} + +NS_IMETHODIMP +InterceptedHttpChannel::SetReleaseHandle(nsISupports* aHandle) { + mReleaseHandle = aHandle; + return NS_OK; +} + +NS_IMETHODIMP +InterceptedHttpChannel::OnRedirectVerifyCallback(nsresult rv) { + MOZ_ASSERT(NS_IsMainThread()); + + if (NS_SUCCEEDED(rv)) { + rv = OpenRedirectChannel(); + } + + nsCOMPtr hook; + GetCallback(hook); + if (hook) { + hook->OnRedirectResult(NS_SUCCEEDED(rv)); + } + + if (NS_FAILED(rv)) { + Cancel(rv); + } + + MaybeCallBodyCallback(); + + StoreIsPending(false); + // We can only release listeners after the redirected channel really owns + // mListener. Otherwise, the OnStart/OnStopRequest functions of mListener will + // not be called. + if (NS_SUCCEEDED(rv)) { + ReleaseListeners(); + } + + return NS_OK; +} + +NS_IMETHODIMP +InterceptedHttpChannel::OnStartRequest(nsIRequest* aRequest) { + MOZ_ASSERT(NS_IsMainThread()); + + if (!mProgressSink) { + GetCallback(mProgressSink); + } + + if (mPump && mLoadFlags & LOAD_CALL_CONTENT_SNIFFERS) { + mPump->PeekStream(CallTypeSniffers, static_cast(this)); + } + + nsresult rv = ProcessCrossOriginEmbedderPolicyHeader(); + if (NS_FAILED(rv)) { + mStatus = NS_ERROR_BLOCKED_BY_POLICY; + Cancel(mStatus); + } + + rv = ProcessCrossOriginResourcePolicyHeader(); + if (NS_FAILED(rv)) { + mStatus = NS_ERROR_DOM_CORP_FAILED; + Cancel(mStatus); + } + + rv = ComputeCrossOriginOpenerPolicyMismatch(); + if (rv == NS_ERROR_BLOCKED_BY_POLICY) { + mStatus = NS_ERROR_BLOCKED_BY_POLICY; + Cancel(mStatus); + } + + rv = ValidateMIMEType(); + if (NS_FAILED(rv)) { + mStatus = rv; + Cancel(mStatus); + } + + StoreOnStartRequestCalled(true); + if (mListener) { + return mListener->OnStartRequest(this); + } + return NS_OK; +} + +NS_IMETHODIMP +InterceptedHttpChannel::OnStopRequest(nsIRequest* aRequest, nsresult aStatus) { + MOZ_ASSERT(NS_IsMainThread()); + + if (NS_SUCCEEDED(mStatus)) { + mStatus = aStatus; + } + + MaybeCallBodyCallback(); + + // Its possible that we have any async runnable queued to report some + // progress when OnStopRequest() is triggered. Report any left over + // progress immediately. The extra runnable will then do nothing thanks + // to the ReleaseListeners() call below. + MaybeCallStatusAndProgress(); + + StoreIsPending(false); + + // Register entry to the PerformanceStorage resource timing + MaybeReportTimingData(); + + nsresult rv = NS_OK; + if (mListener) { + rv = mListener->OnStopRequest(this, mStatus); + } + + gHttpHandler->OnStopRequest(this); + + ReleaseListeners(); + + return rv; +} + +NS_IMETHODIMP +InterceptedHttpChannel::OnDataAvailable(nsIRequest* aRequest, + nsIInputStream* aInputStream, + uint64_t aOffset, uint32_t aCount) { + // Any thread if the channel has been retargeted. + + if (mCanceled || !mListener) { + // If there is no listener, we still need to drain the stream in order + // maintain necko invariants. + uint32_t unused = 0; + aInputStream->ReadSegments(NS_DiscardSegment, nullptr, aCount, &unused); + return mStatus; + } + if (mProgressSink) { + if (!(mLoadFlags & HttpBaseChannel::LOAD_BACKGROUND)) { + mProgress = aOffset + aCount; + MaybeCallStatusAndProgress(); + } + } + + return mListener->OnDataAvailable(this, aInputStream, aOffset, aCount); +} + +NS_IMETHODIMP +InterceptedHttpChannel::RetargetDeliveryTo(nsIEventTarget* aNewTarget) { + MOZ_ASSERT(NS_IsMainThread()); + NS_ENSURE_ARG(aNewTarget); + + // If retargeting to the main thread, do nothing. + if (aNewTarget->IsOnCurrentThread()) { + return NS_OK; + } + + // Retargeting is only valid during OnStartRequest for nsIChannels. So + // we should only be called if we have a pump. + if (!mPump) { + return NS_ERROR_NOT_AVAILABLE; + } + + return mPump->RetargetDeliveryTo(aNewTarget); +} + +NS_IMETHODIMP +InterceptedHttpChannel::GetDeliveryTarget(nsIEventTarget** aEventTarget) { + if (!mPump) { + return NS_ERROR_NOT_AVAILABLE; + } + return mPump->GetDeliveryTarget(aEventTarget); +} + +NS_IMETHODIMP +InterceptedHttpChannel::CheckListenerChain() { + MOZ_ASSERT(NS_IsMainThread()); + nsresult rv = NS_OK; + nsCOMPtr retargetableListener = + do_QueryInterface(mListener, &rv); + if (retargetableListener) { + rv = retargetableListener->CheckListenerChain(); + } + return rv; +} + +//----------------------------------------------------------------------------- +// InterceptedHttpChannel::nsICacheInfoChannel +//----------------------------------------------------------------------------- +// InterceptedHttpChannel does not really implement the nsICacheInfoChannel +// interface, we tranfers parameters to the saved +// nsICacheInfoChannel(mSynthesizedCacheInfo) from StartSynthesizedResponse. And +// we return false in IsFromCache and NS_ERROR_NOT_AVAILABLE for all other +// methods while the saved mSynthesizedCacheInfo does not exist. +NS_IMETHODIMP +InterceptedHttpChannel::IsFromCache(bool* value) { + if (mSynthesizedCacheInfo) { + return mSynthesizedCacheInfo->IsFromCache(value); + } + *value = false; + return NS_OK; +} + +NS_IMETHODIMP +InterceptedHttpChannel::IsRacing(bool* value) { + if (mSynthesizedCacheInfo) { + return mSynthesizedCacheInfo->IsRacing(value); + } + *value = false; + return NS_OK; +} + +NS_IMETHODIMP +InterceptedHttpChannel::GetCacheEntryId(uint64_t* aCacheEntryId) { + if (mSynthesizedCacheInfo) { + return mSynthesizedCacheInfo->GetCacheEntryId(aCacheEntryId); + } + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP +InterceptedHttpChannel::GetCacheTokenFetchCount(int32_t* _retval) { + NS_ENSURE_ARG_POINTER(_retval); + + if (mSynthesizedCacheInfo) { + return mSynthesizedCacheInfo->GetCacheTokenFetchCount(_retval); + } + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP +InterceptedHttpChannel::GetCacheTokenExpirationTime(uint32_t* _retval) { + NS_ENSURE_ARG_POINTER(_retval); + + if (mSynthesizedCacheInfo) { + return mSynthesizedCacheInfo->GetCacheTokenExpirationTime(_retval); + } + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP +InterceptedHttpChannel::SetAllowStaleCacheContent( + bool aAllowStaleCacheContent) { + if (mSynthesizedCacheInfo) { + return mSynthesizedCacheInfo->SetAllowStaleCacheContent( + aAllowStaleCacheContent); + } + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP +InterceptedHttpChannel::GetAllowStaleCacheContent( + bool* aAllowStaleCacheContent) { + if (mSynthesizedCacheInfo) { + return mSynthesizedCacheInfo->GetAllowStaleCacheContent( + aAllowStaleCacheContent); + } + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP +InterceptedHttpChannel::GetPreferCacheLoadOverBypass( + bool* aPreferCacheLoadOverBypass) { + if (mSynthesizedCacheInfo) { + return mSynthesizedCacheInfo->GetPreferCacheLoadOverBypass( + aPreferCacheLoadOverBypass); + } + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP +InterceptedHttpChannel::SetPreferCacheLoadOverBypass( + bool aPreferCacheLoadOverBypass) { + if (mSynthesizedCacheInfo) { + return mSynthesizedCacheInfo->SetPreferCacheLoadOverBypass( + aPreferCacheLoadOverBypass); + } + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP +InterceptedHttpChannel::PreferAlternativeDataType( + const nsACString& aType, const nsACString& aContentType, + bool aDeliverAltData) { + ENSURE_CALLED_BEFORE_ASYNC_OPEN(); + mPreferredCachedAltDataTypes.AppendElement(PreferredAlternativeDataTypeParams( + nsCString(aType), nsCString(aContentType), aDeliverAltData)); + return NS_OK; +} + +const nsTArray& +InterceptedHttpChannel::PreferredAlternativeDataTypes() { + return mPreferredCachedAltDataTypes; +} + +NS_IMETHODIMP +InterceptedHttpChannel::GetAlternativeDataType(nsACString& aType) { + if (mSynthesizedCacheInfo) { + return mSynthesizedCacheInfo->GetAlternativeDataType(aType); + } + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP +InterceptedHttpChannel::OpenAlternativeOutputStream( + const nsACString& type, int64_t predictedSize, + nsIAsyncOutputStream** _retval) { + if (mSynthesizedCacheInfo) { + return mSynthesizedCacheInfo->OpenAlternativeOutputStream( + type, predictedSize, _retval); + } + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP +InterceptedHttpChannel::GetOriginalInputStream( + nsIInputStreamReceiver* aReceiver) { + if (mSynthesizedCacheInfo) { + return mSynthesizedCacheInfo->GetOriginalInputStream(aReceiver); + } + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP +InterceptedHttpChannel::GetAltDataInputStream( + const nsACString& aType, nsIInputStreamReceiver* aReceiver) { + if (mSynthesizedCacheInfo) { + return mSynthesizedCacheInfo->GetAltDataInputStream(aType, aReceiver); + } + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP +InterceptedHttpChannel::GetCacheKey(uint32_t* key) { + if (mSynthesizedCacheInfo) { + return mSynthesizedCacheInfo->GetCacheKey(key); + } + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP +InterceptedHttpChannel::SetCacheKey(uint32_t key) { + if (mSynthesizedCacheInfo) { + return mSynthesizedCacheInfo->SetCacheKey(key); + } + return NS_ERROR_NOT_AVAILABLE; +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/InterceptedHttpChannel.h b/netwerk/protocol/http/InterceptedHttpChannel.h new file mode 100644 index 0000000000..9ceadb9a42 --- /dev/null +++ b/netwerk/protocol/http/InterceptedHttpChannel.h @@ -0,0 +1,188 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et 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/. */ + +#ifndef mozilla_net_InterceptedHttpChannel_h +#define mozilla_net_InterceptedHttpChannel_h + +#include "HttpBaseChannel.h" +#include "nsIAsyncVerifyRedirectCallback.h" +#include "nsINetworkInterceptController.h" +#include "nsIInputStream.h" +#include "nsICacheInfoChannel.h" +#include "nsIThreadRetargetableRequest.h" +#include "nsIThreadRetargetableStreamListener.h" + +namespace mozilla { +namespace net { + +// This class represents an http channel that is being intercepted by a +// ServiceWorker. This means that when the channel is opened a FetchEvent +// will be fired on the ServiceWorker thread. The channel will complete +// depending on what the worker does. The options are: +// +// 1. If the ServiceWorker does not handle the FetchEvent or does not call +// FetchEvent.respondWith(), then the channel needs to fall back to a +// normal request. When this happens ResetInterception() is called and +// the channel will perform an internal redirect back to an nsHttpChannel. +// +// 2. If the ServiceWorker provides a Response to FetchEvent.respondWith() +// then the status, headers, and body must be synthesized. When +// FinishSynthesizedResponse() is called the synthesized data must be +// reported back to the channel listener. This is handled in a few +// different ways: +// a. If a redirect was synthesized, then we perform the redirect to +// a new nsHttpChannel. This new channel might trigger yet another +// interception. +// b. If a same-origin or CORS Response was synthesized, then we simply +// crate an nsInputStreamPump to process it and call back to the +// listener. +// c. If an opaque Response was synthesized, then we perform an internal +// redirect to a new InterceptedHttpChannel using the cross-origin URL. +// When this new channel is opened, it then creates a pump as in case +// (b). The extra redirect here is to make sure the various listeners +// treat the result as unsafe cross-origin data. +// +// 3. If an error occurs, such as the ServiceWorker passing garbage to +// FetchEvent.respondWith(), then CancelInterception() is called. This is +// handled the same as a normal nsIChannel::Cancel() call. We abort the +// channel and end up calling OnStopRequest() with an error code. +class InterceptedHttpChannel final + : public HttpBaseChannel, + public HttpAsyncAborter, + public nsIInterceptedChannel, + public nsICacheInfoChannel, + public nsIAsyncVerifyRedirectCallback, + public nsIStreamListener, + public nsIThreadRetargetableRequest, + public nsIThreadRetargetableStreamListener { + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_NSIINTERCEPTEDCHANNEL + NS_DECL_NSICACHEINFOCHANNEL + NS_DECL_NSIASYNCVERIFYREDIRECTCALLBACK + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMLISTENER + NS_DECL_NSITHREADRETARGETABLEREQUEST + NS_DECL_NSITHREADRETARGETABLESTREAMLISTENER + + private: + friend class HttpAsyncAborter; + + UniquePtr mSynthesizedResponseHead; + nsCOMPtr mRedirectChannel; + nsCOMPtr mBodyReader; + nsCOMPtr mReleaseHandle; + nsCOMPtr mProgressSink; + nsCOMPtr mBodyCallback; + nsCOMPtr mSynthesizedCacheInfo; + RefPtr mPump; + TimeStamp mFinishResponseStart; + TimeStamp mFinishResponseEnd; + Atomic mProgress; + int64_t mProgressReported; + int64_t mSynthesizedStreamLength; + uint64_t mResumeStartPos; + nsCString mResumeEntityId; + nsString mStatusHost; + enum { Invalid = 0, Synthesized, Reset } mSynthesizedOrReset; + Atomic mCallingStatusAndProgress; + + InterceptedHttpChannel(PRTime aCreationTime, + const TimeStamp& aCreationTimestamp, + const TimeStamp& aAsyncOpenTimestamp); + ~InterceptedHttpChannel() = default; + + virtual void ReleaseListeners() override; + + [[nodiscard]] virtual nsresult SetupReplacementChannel( + nsIURI* aURI, nsIChannel* aChannel, bool aPreserveMethod, + uint32_t aRedirectFlags) override; + + void AsyncOpenInternal(); + + bool ShouldRedirect() const; + + nsresult FollowSyntheticRedirect(); + + // If the response's URL is different from the request's then do a service + // worker redirect. If Response.redirected is false we do an internal + // redirect. Otherwise, if Response.redirect is true do a non-internal + // redirect so end consumers detect the redirected state. + nsresult RedirectForResponseURL(nsIURI* aResponseURI, + bool aResponseRedirected); + + nsresult StartPump(); + + nsresult OpenRedirectChannel(); + + void MaybeCallStatusAndProgress(); + + void MaybeCallBodyCallback(); + + public: + static already_AddRefed CreateForInterception( + PRTime aCreationTime, const TimeStamp& aCreationTimestamp, + const TimeStamp& aAsyncOpenTimestamp); + + static already_AddRefed CreateForSynthesis( + const nsHttpResponseHead* aHead, nsIInputStream* aBody, + nsIInterceptedBodyCallback* aBodyCallback, PRTime aCreationTime, + const TimeStamp& aCreationTimestamp, + const TimeStamp& aAsyncOpenTimestamp); + + NS_IMETHOD + Cancel(nsresult aStatus) override; + + NS_IMETHOD + Suspend(void) override; + + NS_IMETHOD + Resume(void) override; + + NS_IMETHOD + GetSecurityInfo(nsISupports** aSecurityInfo) override; + + NS_IMETHOD + AsyncOpen(nsIStreamListener* aListener) override; + + NS_IMETHOD + LogBlockedCORSRequest(const nsAString& aMessage, + const nsACString& aCategory) override; + + NS_IMETHOD + LogMimeTypeMismatch(const nsACString& aMessageName, bool aWarning, + const nsAString& aURL, + const nsAString& aContentType) override; + + NS_IMETHOD + SetupFallbackChannel(const char* aFallbackKey) override; + + NS_IMETHOD + GetIsAuthChannel(bool* aIsAuthChannel) override; + + NS_IMETHOD + SetPriority(int32_t aPriority) override; + + NS_IMETHOD + SetClassFlags(uint32_t aClassFlags) override; + + NS_IMETHOD + ClearClassFlags(uint32_t flags) override; + + NS_IMETHOD + AddClassFlags(uint32_t flags) override; + + NS_IMETHOD + ResumeAt(uint64_t startPos, const nsACString& entityID) override; + + void DoNotifyListenerCleanup() override; + + void DoAsyncAbort(nsresult aStatus) override; +}; + +} // namespace net +} // namespace mozilla + +#endif // mozilla_net_InterceptedHttpChannel_h diff --git a/netwerk/protocol/http/NullHttpChannel.cpp b/netwerk/protocol/http/NullHttpChannel.cpp new file mode 100644 index 0000000000..f818bd8117 --- /dev/null +++ b/netwerk/protocol/http/NullHttpChannel.cpp @@ -0,0 +1,867 @@ +/* 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 "NullHttpChannel.h" +#include "nsContentUtils.h" +#include "nsContentSecurityManager.h" +#include "nsIScriptSecurityManager.h" +#include "nsIStreamListener.h" + +namespace mozilla { +namespace net { + +NS_IMPL_ISUPPORTS(NullHttpChannel, nsINullChannel, nsIHttpChannel, + nsIIdentChannel, nsITimedChannel) + +NullHttpChannel::NullHttpChannel() + : mAllRedirectsSameOrigin(false), mAllRedirectsPassTimingAllowCheck(false) { + mChannelCreationTime = PR_Now(); + mChannelCreationTimestamp = TimeStamp::Now(); + mAsyncOpenTime = TimeStamp::Now(); +} + +NullHttpChannel::NullHttpChannel(nsIHttpChannel* chan) + : mAllRedirectsSameOrigin(false), mAllRedirectsPassTimingAllowCheck(false) { + nsIScriptSecurityManager* ssm = nsContentUtils::GetSecurityManager(); + ssm->GetChannelURIPrincipal(chan, getter_AddRefs(mResourcePrincipal)); + + Unused << chan->GetResponseHeader("Timing-Allow-Origin"_ns, + mTimingAllowOriginHeader); + chan->GetURI(getter_AddRefs(mURI)); + chan->GetOriginalURI(getter_AddRefs(mOriginalURI)); + + mChannelCreationTime = PR_Now(); + mChannelCreationTimestamp = TimeStamp::Now(); + mAsyncOpenTime = TimeStamp::Now(); + + nsCOMPtr timedChanel(do_QueryInterface(chan)); + if (timedChanel) { + timedChanel->GetInitiatorType(mInitiatorType); + } +} + +nsresult NullHttpChannel::Init(nsIURI* aURI, uint32_t aCaps, + nsProxyInfo* aProxyInfo, + uint32_t aProxyResolveFlags, nsIURI* aProxyURI) { + mURI = aURI; + mOriginalURI = aURI; + return NS_OK; +} + +//----------------------------------------------------------------------------- +// NullHttpChannel::nsIHttpChannel +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +NullHttpChannel::GetChannelId(uint64_t* aChannelId) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +NullHttpChannel::SetChannelId(uint64_t aChannelId) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +NullHttpChannel::GetTopLevelContentWindowId(uint64_t* aWindowId) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +NullHttpChannel::SetTopLevelContentWindowId(uint64_t aWindowId) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +NullHttpChannel::GetTopLevelOuterContentWindowId(uint64_t* aWindowId) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +NullHttpChannel::SetTopLevelOuterContentWindowId(uint64_t aWindowId) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +NullHttpChannel::GetFlashPluginState( + nsIHttpChannel::FlashPluginState* aResult) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +NullHttpChannel::GetTransferSize(uint64_t* aTransferSize) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +NullHttpChannel::GetRequestSize(uint64_t* aRequestSize) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +NullHttpChannel::GetDecodedBodySize(uint64_t* aDecodedBodySize) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +NullHttpChannel::GetRequestMethod(nsACString& aRequestMethod) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +NullHttpChannel::SetRequestMethod(const nsACString& aRequestMethod) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +NullHttpChannel::GetReferrerInfo(nsIReferrerInfo** aReferrerInfo) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +NullHttpChannel::SetReferrerInfo(nsIReferrerInfo* aReferrerInfo) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +NullHttpChannel::SetReferrerInfoWithoutClone(nsIReferrerInfo* aReferrerInfo) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +NullHttpChannel::GetRequestHeader(const nsACString& aHeader, + nsACString& _retval) { + _retval.Truncate(); + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +NullHttpChannel::SetRequestHeader(const nsACString& aHeader, + const nsACString& aValue, bool aMerge) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +NullHttpChannel::SetNewReferrerInfo(const nsACString& aUrl, + nsIReferrerInfo::ReferrerPolicyIDL aPolicy, + bool aSendReferrer) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +NullHttpChannel::SetEmptyRequestHeader(const nsACString& aHeader) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +NullHttpChannel::VisitRequestHeaders(nsIHttpHeaderVisitor* aVisitor) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +NullHttpChannel::VisitNonDefaultRequestHeaders(nsIHttpHeaderVisitor* aVisitor) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +NullHttpChannel::GetAllowPipelining(bool* aAllowPipelining) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +NullHttpChannel::SetAllowPipelining(bool aAllowPipelining) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +NullHttpChannel::GetAllowSTS(bool* aAllowSTS) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +NullHttpChannel::SetAllowSTS(bool aAllowSTS) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +NullHttpChannel::GetRedirectionLimit(uint32_t* aRedirectionLimit) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +NullHttpChannel::SetRedirectionLimit(uint32_t aRedirectionLimit) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +NullHttpChannel::GetResponseStatus(uint32_t* aResponseStatus) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +NullHttpChannel::GetResponseStatusText(nsACString& aResponseStatusText) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +NullHttpChannel::GetRequestSucceeded(bool* aRequestSucceeded) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +NullHttpChannel::GetResponseHeader(const nsACString& header, + nsACString& _retval) { + _retval.Truncate(); + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +NullHttpChannel::SetResponseHeader(const nsACString& header, + const nsACString& value, bool merge) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +NullHttpChannel::VisitResponseHeaders(nsIHttpHeaderVisitor* aVisitor) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +NullHttpChannel::GetOriginalResponseHeader(const nsACString& header, + nsIHttpHeaderVisitor* aVisitor) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +NullHttpChannel::VisitOriginalResponseHeaders(nsIHttpHeaderVisitor* aVisitor) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +NullHttpChannel::ShouldStripRequestBodyHeader(const nsACString& aMethod, + bool* aResult) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +NullHttpChannel::IsNoStoreResponse(bool* _retval) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +NullHttpChannel::IsNoCacheResponse(bool* _retval) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +NullHttpChannel::IsPrivateResponse(bool* _retval) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +NullHttpChannel::RedirectTo(nsIURI* aNewURI) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +NullHttpChannel::UpgradeToSecure() { return NS_ERROR_NOT_IMPLEMENTED; } + +NS_IMETHODIMP +NullHttpChannel::GetRequestContextID(uint64_t* _retval) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +NullHttpChannel::SetRequestContextID(uint64_t rcID) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +NullHttpChannel::GetProtocolVersion(nsACString& aProtocolVersion) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +NullHttpChannel::GetEncodedBodySize(uint64_t* aEncodedBodySize) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +//----------------------------------------------------------------------------- +// NullHttpChannel::nsIChannel +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +NullHttpChannel::GetOriginalURI(nsIURI** aOriginalURI) { + NS_IF_ADDREF(*aOriginalURI = mOriginalURI); + return NS_OK; +} + +NS_IMETHODIMP +NullHttpChannel::SetOriginalURI(nsIURI* aOriginalURI) { + mOriginalURI = aOriginalURI; + return NS_OK; +} + +NS_IMETHODIMP +NullHttpChannel::GetURI(nsIURI** aURI) { + NS_IF_ADDREF(*aURI = mURI); + return NS_OK; +} + +NS_IMETHODIMP +NullHttpChannel::GetOwner(nsISupports** aOwner) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +NullHttpChannel::SetOwner(nsISupports* aOwner) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +NullHttpChannel::GetNotificationCallbacks( + nsIInterfaceRequestor** aNotificationCallbacks) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +NullHttpChannel::SetNotificationCallbacks( + nsIInterfaceRequestor* aNotificationCallbacks) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +NullHttpChannel::GetSecurityInfo(nsISupports** aSecurityInfo) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +NullHttpChannel::GetContentType(nsACString& aContentType) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +NullHttpChannel::SetContentType(const nsACString& aContentType) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +NullHttpChannel::GetContentCharset(nsACString& aContentCharset) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +NullHttpChannel::SetContentCharset(const nsACString& aContentCharset) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +NullHttpChannel::GetContentLength(int64_t* aContentLength) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +NullHttpChannel::SetContentLength(int64_t aContentLength) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +NullHttpChannel::Open(nsIInputStream** aStream) { + nsCOMPtr listener; + nsresult rv = + nsContentSecurityManager::doContentSecurityCheck(this, listener); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +NullHttpChannel::AsyncOpen(nsIStreamListener* aListener) { + nsCOMPtr listener = aListener; + nsresult rv = + nsContentSecurityManager::doContentSecurityCheck(this, listener); + NS_ENSURE_SUCCESS(rv, rv); + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +NullHttpChannel::GetContentDisposition(uint32_t* aContentDisposition) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +NullHttpChannel::SetContentDisposition(uint32_t aContentDisposition) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +NullHttpChannel::GetContentDispositionFilename( + nsAString& aContentDispositionFilename) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +NullHttpChannel::SetContentDispositionFilename( + const nsAString& aContentDispositionFilename) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +NullHttpChannel::GetContentDispositionHeader( + nsACString& aContentDispositionHeader) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +NullHttpChannel::GetLoadInfo(nsILoadInfo** aLoadInfo) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +NullHttpChannel::SetLoadInfo(nsILoadInfo* aLoadInfo) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +//----------------------------------------------------------------------------- +// NullHttpChannel::nsIRequest +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +NullHttpChannel::GetName(nsACString& aName) { return NS_ERROR_NOT_IMPLEMENTED; } + +NS_IMETHODIMP +NullHttpChannel::IsPending(bool* _retval) { return NS_ERROR_NOT_IMPLEMENTED; } + +NS_IMETHODIMP +NullHttpChannel::GetStatus(nsresult* aStatus) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +NullHttpChannel::Cancel(nsresult aStatus) { return NS_ERROR_NOT_IMPLEMENTED; } + +NS_IMETHODIMP +NullHttpChannel::GetCanceled(bool* aCanceled) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +NullHttpChannel::Suspend() { return NS_ERROR_NOT_IMPLEMENTED; } + +NS_IMETHODIMP +NullHttpChannel::Resume() { return NS_ERROR_NOT_IMPLEMENTED; } + +NS_IMETHODIMP +NullHttpChannel::GetLoadGroup(nsILoadGroup** aLoadGroup) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +NullHttpChannel::SetLoadGroup(nsILoadGroup* aLoadGroup) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +NullHttpChannel::GetLoadFlags(nsLoadFlags* aLoadFlags) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +NullHttpChannel::GetTRRMode(nsIRequest::TRRMode* aTRRMode) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +NullHttpChannel::SetTRRMode(nsIRequest::TRRMode aTRRMode) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +NullHttpChannel::SetLoadFlags(nsLoadFlags aLoadFlags) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +NullHttpChannel::GetIsDocument(bool* aIsDocument) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +//----------------------------------------------------------------------------- +// NullHttpChannel::nsITimedChannel +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +NullHttpChannel::GetTimingEnabled(bool* aTimingEnabled) { + // We don't want to report timing for null channels. + *aTimingEnabled = false; + return NS_OK; +} + +NS_IMETHODIMP +NullHttpChannel::SetTimingEnabled(bool aTimingEnabled) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +NullHttpChannel::GetRedirectCount(uint8_t* aRedirectCount) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +NullHttpChannel::SetRedirectCount(uint8_t aRedirectCount) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +NullHttpChannel::GetInternalRedirectCount(uint8_t* aRedirectCount) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +NullHttpChannel::SetInternalRedirectCount(uint8_t aRedirectCount) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +NullHttpChannel::GetChannelCreation(mozilla::TimeStamp* aChannelCreation) { + *aChannelCreation = mChannelCreationTimestamp; + return NS_OK; +} + +NS_IMETHODIMP +NullHttpChannel::SetChannelCreation(TimeStamp aValue) { + MOZ_DIAGNOSTIC_ASSERT(!aValue.IsNull()); + TimeDuration adjust = aValue - mChannelCreationTimestamp; + mChannelCreationTimestamp = aValue; + mChannelCreationTime += (PRTime)adjust.ToMicroseconds(); + return NS_OK; +} + +NS_IMETHODIMP +NullHttpChannel::GetAsyncOpen(mozilla::TimeStamp* aAsyncOpen) { + *aAsyncOpen = mAsyncOpenTime; + return NS_OK; +} + +NS_IMETHODIMP +NullHttpChannel::SetAsyncOpen(TimeStamp aValue) { + MOZ_DIAGNOSTIC_ASSERT(!aValue.IsNull()); + mAsyncOpenTime = aValue; + return NS_OK; +} + +NS_IMETHODIMP +NullHttpChannel::GetLaunchServiceWorkerStart(mozilla::TimeStamp* _retval) { + MOZ_ASSERT(_retval); + *_retval = mAsyncOpenTime; + return NS_OK; +} + +NS_IMETHODIMP +NullHttpChannel::SetLaunchServiceWorkerStart(mozilla::TimeStamp aTimeStamp) { + return NS_OK; +} + +NS_IMETHODIMP +NullHttpChannel::GetLaunchServiceWorkerEnd(mozilla::TimeStamp* _retval) { + MOZ_ASSERT(_retval); + *_retval = mAsyncOpenTime; + return NS_OK; +} + +NS_IMETHODIMP +NullHttpChannel::SetLaunchServiceWorkerEnd(mozilla::TimeStamp aTimeStamp) { + return NS_OK; +} + +NS_IMETHODIMP +NullHttpChannel::GetDispatchFetchEventStart(mozilla::TimeStamp* _retval) { + MOZ_ASSERT(_retval); + *_retval = mAsyncOpenTime; + return NS_OK; +} + +NS_IMETHODIMP +NullHttpChannel::SetDispatchFetchEventStart(mozilla::TimeStamp aTimeStamp) { + return NS_OK; +} + +NS_IMETHODIMP +NullHttpChannel::GetDispatchFetchEventEnd(mozilla::TimeStamp* _retval) { + MOZ_ASSERT(_retval); + *_retval = mAsyncOpenTime; + return NS_OK; +} + +NS_IMETHODIMP +NullHttpChannel::SetDispatchFetchEventEnd(mozilla::TimeStamp aTimeStamp) { + return NS_OK; +} + +NS_IMETHODIMP +NullHttpChannel::GetHandleFetchEventStart(mozilla::TimeStamp* _retval) { + MOZ_ASSERT(_retval); + *_retval = mAsyncOpenTime; + return NS_OK; +} + +NS_IMETHODIMP +NullHttpChannel::SetHandleFetchEventStart(mozilla::TimeStamp aTimeStamp) { + return NS_OK; +} + +NS_IMETHODIMP +NullHttpChannel::GetHandleFetchEventEnd(mozilla::TimeStamp* _retval) { + MOZ_ASSERT(_retval); + *_retval = mAsyncOpenTime; + return NS_OK; +} + +NS_IMETHODIMP +NullHttpChannel::SetHandleFetchEventEnd(mozilla::TimeStamp aTimeStamp) { + return NS_OK; +} + +NS_IMETHODIMP +NullHttpChannel::GetDomainLookupStart(mozilla::TimeStamp* aDomainLookupStart) { + *aDomainLookupStart = mAsyncOpenTime; + return NS_OK; +} + +NS_IMETHODIMP +NullHttpChannel::GetDomainLookupEnd(mozilla::TimeStamp* aDomainLookupEnd) { + *aDomainLookupEnd = mAsyncOpenTime; + return NS_OK; +} + +NS_IMETHODIMP +NullHttpChannel::GetConnectStart(mozilla::TimeStamp* aConnectStart) { + *aConnectStart = mAsyncOpenTime; + return NS_OK; +} + +NS_IMETHODIMP +NullHttpChannel::GetTcpConnectEnd(mozilla::TimeStamp* aTcpConnectEnd) { + *aTcpConnectEnd = mAsyncOpenTime; + return NS_OK; +} + +NS_IMETHODIMP +NullHttpChannel::GetSecureConnectionStart( + mozilla::TimeStamp* aSecureConnectionStart) { + *aSecureConnectionStart = mAsyncOpenTime; + return NS_OK; +} + +NS_IMETHODIMP +NullHttpChannel::GetConnectEnd(mozilla::TimeStamp* aConnectEnd) { + *aConnectEnd = mAsyncOpenTime; + return NS_OK; +} + +NS_IMETHODIMP +NullHttpChannel::GetRequestStart(mozilla::TimeStamp* aRequestStart) { + *aRequestStart = mAsyncOpenTime; + return NS_OK; +} + +NS_IMETHODIMP +NullHttpChannel::GetResponseStart(mozilla::TimeStamp* aResponseStart) { + *aResponseStart = mAsyncOpenTime; + return NS_OK; +} + +NS_IMETHODIMP +NullHttpChannel::GetResponseEnd(mozilla::TimeStamp* aResponseEnd) { + *aResponseEnd = mAsyncOpenTime; + return NS_OK; +} + +NS_IMETHODIMP +NullHttpChannel::GetRedirectStart(mozilla::TimeStamp* aRedirectStart) { + *aRedirectStart = mAsyncOpenTime; + return NS_OK; +} + +NS_IMETHODIMP +NullHttpChannel::SetRedirectStart(mozilla::TimeStamp aRedirectStart) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +NullHttpChannel::GetRedirectEnd(mozilla::TimeStamp* aRedirectEnd) { + *aRedirectEnd = mAsyncOpenTime; + return NS_OK; +} + +NS_IMETHODIMP +NullHttpChannel::SetRedirectEnd(mozilla::TimeStamp aRedirectEnd) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +NullHttpChannel::GetInitiatorType(nsAString& aInitiatorType) { + aInitiatorType = mInitiatorType; + return NS_OK; +} + +NS_IMETHODIMP +NullHttpChannel::SetInitiatorType(const nsAString& aInitiatorType) { + mInitiatorType = aInitiatorType; + return NS_OK; +} + +NS_IMETHODIMP +NullHttpChannel::GetAllRedirectsSameOrigin(bool* aAllRedirectsSameOrigin) { + *aAllRedirectsSameOrigin = mAllRedirectsSameOrigin; + return NS_OK; +} + +NS_IMETHODIMP +NullHttpChannel::SetAllRedirectsSameOrigin(bool aAllRedirectsSameOrigin) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +NullHttpChannel::GetAllRedirectsPassTimingAllowCheck( + bool* aAllRedirectsPassTimingAllowCheck) { + *aAllRedirectsPassTimingAllowCheck = mAllRedirectsPassTimingAllowCheck; + return NS_OK; +} + +NS_IMETHODIMP +NullHttpChannel::SetAllRedirectsPassTimingAllowCheck( + bool aAllRedirectsPassTimingAllowCheck) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +NullHttpChannel::TimingAllowCheck(nsIPrincipal* aOrigin, bool* _retval) { + if (!mResourcePrincipal || !aOrigin) { + *_retval = false; + return NS_OK; + } + + bool sameOrigin = false; + nsresult rv = mResourcePrincipal->Equals(aOrigin, &sameOrigin); + if (NS_SUCCEEDED(rv) && sameOrigin) { + *_retval = true; + return NS_OK; + } + + if (mTimingAllowOriginHeader == "*") { + *_retval = true; + return NS_OK; + } + + nsAutoCString origin; + aOrigin->GetAsciiOrigin(origin); + + if (mTimingAllowOriginHeader == origin) { + *_retval = true; + return NS_OK; + } + + *_retval = false; + return NS_OK; +} + +NS_IMETHODIMP +NullHttpChannel::GetCacheReadStart(mozilla::TimeStamp* aCacheReadStart) { + *aCacheReadStart = mAsyncOpenTime; + return NS_OK; +} + +NS_IMETHODIMP +NullHttpChannel::GetCacheReadEnd(mozilla::TimeStamp* aCacheReadEnd) { + *aCacheReadEnd = mAsyncOpenTime; + return NS_OK; +} + +NS_IMETHODIMP +NullHttpChannel::GetIsMainDocumentChannel(bool* aValue) { + *aValue = false; + return NS_OK; +} + +NS_IMETHODIMP +NullHttpChannel::SetIsMainDocumentChannel(bool aValue) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +NullHttpChannel::LogBlockedCORSRequest(const nsAString& aMessage, + const nsACString& aCategory) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +NullHttpChannel::LogMimeTypeMismatch(const nsACString& aMessageName, + bool aWarning, const nsAString& aURL, + const nsAString& aContentType) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +NullHttpChannel::SetReportResourceTiming(bool enabled) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +NullHttpChannel::GetReportResourceTiming(bool* _retval) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +NullHttpChannel::GetServerTiming(nsIArray** aServerTiming) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +NullHttpChannel::GetNativeServerTiming( + nsTArray>& aServerTiming) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +#define IMPL_TIMING_ATTR(name) \ + NS_IMETHODIMP \ + NullHttpChannel::Get##name##Time(PRTime* _retval) { \ + TimeStamp stamp; \ + Get##name(&stamp); \ + if (stamp.IsNull()) { \ + *_retval = 0; \ + return NS_OK; \ + } \ + *_retval = \ + mChannelCreationTime + \ + (PRTime)((stamp - mChannelCreationTimestamp).ToSeconds() * 1e6); \ + return NS_OK; \ + } + +IMPL_TIMING_ATTR(ChannelCreation) +IMPL_TIMING_ATTR(AsyncOpen) +IMPL_TIMING_ATTR(LaunchServiceWorkerStart) +IMPL_TIMING_ATTR(LaunchServiceWorkerEnd) +IMPL_TIMING_ATTR(DispatchFetchEventStart) +IMPL_TIMING_ATTR(DispatchFetchEventEnd) +IMPL_TIMING_ATTR(HandleFetchEventStart) +IMPL_TIMING_ATTR(HandleFetchEventEnd) +IMPL_TIMING_ATTR(DomainLookupStart) +IMPL_TIMING_ATTR(DomainLookupEnd) +IMPL_TIMING_ATTR(ConnectStart) +IMPL_TIMING_ATTR(TcpConnectEnd) +IMPL_TIMING_ATTR(SecureConnectionStart) +IMPL_TIMING_ATTR(ConnectEnd) +IMPL_TIMING_ATTR(RequestStart) +IMPL_TIMING_ATTR(ResponseStart) +IMPL_TIMING_ATTR(ResponseEnd) +IMPL_TIMING_ATTR(CacheReadStart) +IMPL_TIMING_ATTR(CacheReadEnd) +IMPL_TIMING_ATTR(RedirectStart) +IMPL_TIMING_ATTR(RedirectEnd) + +#undef IMPL_TIMING_ATTR + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/NullHttpChannel.h b/netwerk/protocol/http/NullHttpChannel.h new file mode 100644 index 0000000000..5625796a31 --- /dev/null +++ b/netwerk/protocol/http/NullHttpChannel.h @@ -0,0 +1,64 @@ +/* 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/. */ + +#ifndef mozilla_net_NullHttpChannel_h +#define mozilla_net_NullHttpChannel_h + +#include "nsINullChannel.h" +#include "nsIHttpChannel.h" +#include "nsITimedChannel.h" +#include "nsIURI.h" +#include "nsCOMPtr.h" +#include "mozilla/TimeStamp.h" +#include "nsString.h" +#include "prtime.h" + +namespace mozilla { +namespace net { + +class nsProxyInfo; + +class NullHttpChannel final : public nsINullChannel, + public nsIHttpChannel, + public nsITimedChannel { + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSINULLCHANNEL + NS_DECL_NSIHTTPCHANNEL + NS_DECL_NSITIMEDCHANNEL + NS_DECL_NSIREQUEST + NS_DECL_NSICHANNEL + NS_DECL_NSIIDENTCHANNEL + + NullHttpChannel(); + + // Copies the URI, Principal and Timing-Allow-Origin headers from the + // passed channel to this object, to be used for resource timing checks + explicit NullHttpChannel(nsIHttpChannel* chan); + + // Same signature as nsHttpChannel::Init + [[nodiscard]] nsresult Init(nsIURI* aURI, uint32_t aCaps, + nsProxyInfo* aProxyInfo, + uint32_t aProxyResolveFlags, nsIURI* aProxyURI); + + private: + ~NullHttpChannel() = default; + + protected: + nsCOMPtr mURI; + nsCOMPtr mOriginalURI; + + nsString mInitiatorType; + PRTime mChannelCreationTime; + TimeStamp mAsyncOpenTime; + TimeStamp mChannelCreationTimestamp; + nsCOMPtr mResourcePrincipal; + nsCString mTimingAllowOriginHeader; + bool mAllRedirectsSameOrigin; + bool mAllRedirectsPassTimingAllowCheck; +}; + +} // namespace net +} // namespace mozilla + +#endif // mozilla_net_NullHttpChannel_h diff --git a/netwerk/protocol/http/NullHttpTransaction.cpp b/netwerk/protocol/http/NullHttpTransaction.cpp new file mode 100644 index 0000000000..8871fd9912 --- /dev/null +++ b/netwerk/protocol/http/NullHttpTransaction.cpp @@ -0,0 +1,220 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et 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/. */ + +// HttpLog.h should generally be included first +#include "HttpLog.h" + +#include "mozilla/net/NeckoChannelParams.h" // For HttpActivityArgs. +#include "nsHttp.h" +#include "NullHttpTransaction.h" +#include "nsHttpHandler.h" +#include "nsHttpRequestHead.h" +#include "nsIHttpActivityObserver.h" +#include "nsQueryObject.h" +#include "nsNetUtil.h" +#include "TCPFastOpenLayer.h" + +namespace mozilla { +namespace net { + +NS_IMPL_ISUPPORTS(NullHttpTransaction, NullHttpTransaction, + nsISupportsWeakReference) + +NullHttpTransaction::NullHttpTransaction(nsHttpConnectionInfo* ci, + nsIInterfaceRequestor* callbacks, + uint32_t caps) + : mStatus(NS_OK), + mCaps(caps | NS_HTTP_ALLOW_KEEPALIVE), + mRequestHead(nullptr), + mIsDone(false), + mClaimed(false), + mFastOpenStatus(TFO_NOT_TRIED), + mCallbacks(callbacks), + mConnectionInfo(ci) { + nsresult rv; + mActivityDistributor = + do_GetService(NS_HTTPACTIVITYDISTRIBUTOR_CONTRACTID, &rv); + if (NS_FAILED(rv)) { + return; + } + + bool activityDistributorActive; + rv = mActivityDistributor->GetIsActive(&activityDistributorActive); + if (NS_SUCCEEDED(rv) && activityDistributorActive) { + // There are some observers registered at activity distributor. + LOG( + ("NulHttpTransaction::NullHttpTransaction() " + "mActivityDistributor is active " + "[this=%p, %s]", + this, ci->GetOrigin().get())); + } else { + // There is no observer, so don't use it. + mActivityDistributor = nullptr; + } +} + +NullHttpTransaction::~NullHttpTransaction() { + mCallbacks = nullptr; + delete mRequestHead; +} + +bool NullHttpTransaction::Claim() { + if (mClaimed) { + return false; + } + mClaimed = true; + return true; +} + +void NullHttpTransaction::Unclaim() { mClaimed = false; } + +void NullHttpTransaction::SetConnection(nsAHttpConnection* conn) { + mConnection = conn; +} + +nsAHttpConnection* NullHttpTransaction::Connection() { + return mConnection.get(); +} + +void NullHttpTransaction::GetSecurityCallbacks(nsIInterfaceRequestor** outCB) { + nsCOMPtr copyCB(mCallbacks); + *outCB = copyCB.forget().take(); +} + +void NullHttpTransaction::OnTransportStatus(nsITransport* transport, + nsresult status, int64_t progress) { + if (status == NS_NET_STATUS_RESOLVING_HOST) { + if (mTimings.domainLookupStart.IsNull()) { + mTimings.domainLookupStart = TimeStamp::Now(); + } + } else if (status == NS_NET_STATUS_RESOLVED_HOST) { + if (mTimings.domainLookupEnd.IsNull()) { + mTimings.domainLookupEnd = TimeStamp::Now(); + } + } else if (status == NS_NET_STATUS_CONNECTING_TO) { + if (mTimings.connectStart.IsNull()) { + mTimings.connectStart = TimeStamp::Now(); + } + } else if (status == NS_NET_STATUS_CONNECTED_TO) { + TimeStamp tnow = TimeStamp::Now(); + if (mTimings.connectEnd.IsNull()) { + mTimings.connectEnd = tnow; + } + if (mTimings.tcpConnectEnd.IsNull()) { + mTimings.tcpConnectEnd = tnow; + } + // After a socket is connected we know for sure whether data has been + // sent on SYN packet and if not we should update TLS start timing. + if ((mFastOpenStatus != TFO_DATA_SENT) && + !mTimings.secureConnectionStart.IsNull()) { + mTimings.secureConnectionStart = tnow; + } + } else if (status == NS_NET_STATUS_TLS_HANDSHAKE_STARTING) { + if (mTimings.secureConnectionStart.IsNull()) { + mTimings.secureConnectionStart = TimeStamp::Now(); + } + } else if (status == NS_NET_STATUS_TLS_HANDSHAKE_ENDED) { + mTimings.connectEnd = TimeStamp::Now(); + ; + } + + if (mActivityDistributor) { + Unused << mActivityDistributor->ObserveActivityWithArgs( + HttpActivity(mConnectionInfo->GetOrigin(), + mConnectionInfo->OriginPort(), + mConnectionInfo->EndToEndSSL()), + NS_HTTP_ACTIVITY_TYPE_SOCKET_TRANSPORT, static_cast(status), + PR_Now(), progress, ""_ns); + } +} + +bool NullHttpTransaction::IsDone() { return mIsDone; } + +nsresult NullHttpTransaction::Status() { return mStatus; } + +uint32_t NullHttpTransaction::Caps() { return mCaps; } + +nsresult NullHttpTransaction::ReadSegments(nsAHttpSegmentReader* reader, + uint32_t count, + uint32_t* countRead) { + *countRead = 0; + mIsDone = true; + return NS_BASE_STREAM_CLOSED; +} + +nsresult NullHttpTransaction::WriteSegments(nsAHttpSegmentWriter* writer, + uint32_t count, + uint32_t* countWritten) { + *countWritten = 0; + return NS_BASE_STREAM_CLOSED; +} + +uint32_t NullHttpTransaction::Http1xTransactionCount() { return 0; } + +nsHttpRequestHead* NullHttpTransaction::RequestHead() { + // We suport a requesthead at all so that a CONNECT tunnel transaction + // can obtain a Host header from it, but we lazy-popualate that header. + + if (!mRequestHead) { + mRequestHead = new nsHttpRequestHead(); + + nsAutoCString hostHeader; + nsCString host(mConnectionInfo->GetOrigin()); + nsresult rv = nsHttpHandler::GenerateHostPort( + host, mConnectionInfo->OriginPort(), hostHeader); + if (NS_SUCCEEDED(rv)) { + rv = mRequestHead->SetHeader(nsHttp::Host, hostHeader); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + if (mActivityDistributor) { + // Report request headers. + nsCString reqHeaderBuf; + mRequestHead->Flatten(reqHeaderBuf, false); + Unused << mActivityDistributor->ObserveActivityWithArgs( + HttpActivity(mConnectionInfo->GetOrigin(), + mConnectionInfo->OriginPort(), + mConnectionInfo->EndToEndSSL()), + NS_HTTP_ACTIVITY_TYPE_HTTP_TRANSACTION, + NS_HTTP_ACTIVITY_SUBTYPE_REQUEST_HEADER, PR_Now(), 0, reqHeaderBuf); + } + } + + // CONNECT tunnels may also want Proxy-Authorization but that is a lot + // harder to determine, so for now we will let those connections fail in + // the NullHttpTransaction and let them be retried from the pending queue + // with a bound transaction + } + + return mRequestHead; +} + +nsresult NullHttpTransaction::TakeSubTransactions( + nsTArray >& outTransactions) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +void NullHttpTransaction::SetProxyConnectFailed() {} + +void NullHttpTransaction::Close(nsresult reason) { + mStatus = reason; + mConnection = nullptr; + mIsDone = true; + if (mActivityDistributor) { + // Report that this transaction is closing. + Unused << mActivityDistributor->ObserveActivityWithArgs( + HttpActivity(mConnectionInfo->GetOrigin(), + mConnectionInfo->OriginPort(), + mConnectionInfo->EndToEndSSL()), + NS_HTTP_ACTIVITY_TYPE_HTTP_TRANSACTION, + NS_HTTP_ACTIVITY_SUBTYPE_TRANSACTION_CLOSE, PR_Now(), 0, ""_ns); + } +} + +nsHttpConnectionInfo* NullHttpTransaction::ConnectionInfo() { + return mConnectionInfo; +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/NullHttpTransaction.h b/netwerk/protocol/http/NullHttpTransaction.h new file mode 100644 index 0000000000..c040e4671b --- /dev/null +++ b/netwerk/protocol/http/NullHttpTransaction.h @@ -0,0 +1,99 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ + +/* 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/. */ + +#ifndef mozilla_net_NullHttpTransaction_h +#define mozilla_net_NullHttpTransaction_h + +#include "nsAHttpTransaction.h" +#include "mozilla/Attributes.h" +#include "TimingStruct.h" + +// This is the minimal nsAHttpTransaction implementation. A NullHttpTransaction +// can be used to drive connection level semantics (such as SSL handshakes +// tunnels) so that a nsHttpConnection becomes fully established in +// anticipation of a real transaction needing to use it soon. + +class nsIHttpActivityObserver; + +namespace mozilla { +namespace net { + +class nsAHttpConnection; +class nsHttpConnectionInfo; +class nsHttpRequestHead; + +// 6c445340-3b82-4345-8efa-4902c3b8805a +#define NS_NULLHTTPTRANSACTION_IID \ + { \ + 0x6c445340, 0x3b82, 0x4345, { \ + 0x8e, 0xfa, 0x49, 0x02, 0xc3, 0xb8, 0x80, 0x5a \ + } \ + } + +class NullHttpTransaction : public nsAHttpTransaction { + public: + NS_DECLARE_STATIC_IID_ACCESSOR(NS_NULLHTTPTRANSACTION_IID) + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSAHTTPTRANSACTION + + NullHttpTransaction(nsHttpConnectionInfo* ci, + nsIInterfaceRequestor* callbacks, uint32_t caps); + + [[nodiscard]] bool Claim(); + void Unclaim(); + + // Overload of nsAHttpTransaction methods + bool IsNullTransaction() final { return true; } + NullHttpTransaction* QueryNullTransaction() final { return this; } + bool ResponseTimeoutEnabled() const final { return true; } + PRIntervalTime ResponseTimeout() final { return PR_SecondsToInterval(15); } + + // We have to override this function because |mTransaction| in + // nsHalfOpenSocket could be either nsHttpTransaction or NullHttpTransaction. + // NullHttpTransaction will be activated on the connection immediately after + // creation and be never put in a pending queue, so it's OK to just return 0. + uint64_t TopLevelOuterContentWindowId() override { return 0; } + + TimingStruct Timings() { return mTimings; } + + mozilla::TimeStamp GetTcpConnectEnd() { return mTimings.tcpConnectEnd; } + mozilla::TimeStamp GetSecureConnectionStart() { + return mTimings.secureConnectionStart; + } + + void SetFastOpenStatus(uint8_t aStatus) override { + mFastOpenStatus = aStatus; + } + + protected: + virtual ~NullHttpTransaction(); + + private: + nsresult mStatus; + + protected: + uint32_t mCaps; + nsHttpRequestHead* mRequestHead; + + private: + bool mIsDone; + bool mClaimed; + TimingStruct mTimings; + uint8_t mFastOpenStatus; + + protected: + RefPtr mConnection; + nsCOMPtr mCallbacks; + RefPtr mConnectionInfo; + nsCOMPtr mActivityDistributor; +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(NullHttpTransaction, NS_NULLHTTPTRANSACTION_IID) + +} // namespace net +} // namespace mozilla + +#endif // mozilla_net_NullHttpTransaction_h diff --git a/netwerk/protocol/http/PAltDataOutputStream.ipdl b/netwerk/protocol/http/PAltDataOutputStream.ipdl new file mode 100644 index 0000000000..f8234a8abe --- /dev/null +++ b/netwerk/protocol/http/PAltDataOutputStream.ipdl @@ -0,0 +1,41 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et tw=80 ft=cpp : */ + +/* 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 protocol PNecko; + +namespace mozilla { +namespace net { + +protocol PAltDataOutputStream +{ + manager PNecko; + +parent: + // Sends data from the child to the parent that will be written to the cache. + async WriteData(nsCString data); + // Signals that writing to the output stream is done. + async Close(nsresult status); + + async __delete__(); + +child: + // The parent calls this method to signal that an error has ocurred. + // This may mean that opening the output stream has failed or that writing to + // the stream has returned an error. + async Error(nsresult err); + +both: + // After sending this message, the parent will respond by sending DeleteSelf + // back to the child, after which it is guaranteed to not send any more IPC + // messages. + // When receiving this message, the child will send __delete__ tearing down + // the IPC channel. + async DeleteSelf(); +}; + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/PAltService.ipdl b/netwerk/protocol/http/PAltService.ipdl new file mode 100644 index 0000000000..ea96c5b5b1 --- /dev/null +++ b/netwerk/protocol/http/PAltService.ipdl @@ -0,0 +1,41 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et 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 protocol PSocketProcess; +include NeckoChannelParams; + +using mozilla::OriginAttributes from "mozilla/ipc/BackgroundUtils.h"; + +namespace mozilla { +namespace net { + +refcounted protocol PAltService +{ + manager PSocketProcess; + +parent: + async ClearHostMapping(nsCString host, int32_t port, + OriginAttributes originAttributes, + nsCString topWindowOrigin); + + async ProcessHeader(nsCString buf, + nsCString originScheme, + nsCString originHost, + int32_t originPort, + nsCString username, + nsCString topWindowOrigin, + bool privateBrowsing, + bool isolated, + ProxyInfoCloneArgs[] proxyInfo, + uint32_t caps, + OriginAttributes originAttributes); + +child: + async __delete__(); +}; + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/PAltSvcTransaction.ipdl b/netwerk/protocol/http/PAltSvcTransaction.ipdl new file mode 100644 index 0000000000..1b8a81db30 --- /dev/null +++ b/netwerk/protocol/http/PAltSvcTransaction.ipdl @@ -0,0 +1,23 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et tw=80 ft=cpp : */ + +/* 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 protocol PSocketProcess; + +namespace mozilla { +namespace net { + +refcounted protocol PAltSvcTransaction +{ + manager PSocketProcess; + +parent: + async __delete__(bool aValidateResult); + async OnTransactionClose(bool aValidateResult); +}; + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/PBackgroundDataBridge.ipdl b/netwerk/protocol/http/PBackgroundDataBridge.ipdl new file mode 100644 index 0000000000..31bacc8435 --- /dev/null +++ b/netwerk/protocol/http/PBackgroundDataBridge.ipdl @@ -0,0 +1,32 @@ +/* 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 protocol PBackground; +include HttpChannelParams; +include NeckoChannelParams; + +namespace mozilla { +namespace net { + +//------------------------------------------------------------------- +async refcounted protocol PBackgroundDataBridge +{ + manager PBackground; + +child: + async OnTransportAndData(uint64_t offset, + uint32_t count, + nsDependentCSubstring data); + + async OnStopRequest(nsresult aStatus, + ResourceTimingStructArgs timing, + TimeStamp lastActiveTabOptimization, + nsHttpHeaderArray responseTrailers); + +both: + async __delete__(); +}; + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/PClassifierDummyChannel.ipdl b/netwerk/protocol/http/PClassifierDummyChannel.ipdl new file mode 100644 index 0000000000..6530b1465c --- /dev/null +++ b/netwerk/protocol/http/PClassifierDummyChannel.ipdl @@ -0,0 +1,47 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et tw=80 ft=cpp : */ + +/* 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 protocol PNecko; + +namespace mozilla { +namespace net { + +// This protocol provides a mechanism for the "child intercept" mode of +// ServiceWorker operation to work correctly with Classified channels. +// ServiceWorkers should not be allowed for third-party iframes which are +// annotated as tracking origins. +// +// In child intercept mode, the decision to intercept a channel is made in the +// child process without consulting the parent process. The decision is based +// on whether there is a ServiceWorker with a scope covering the URL in question +// and whether storage is allowed for the origin/URL. When the +// "network.cookie.cookieBehavior" preference is set to BEHAVIOR_REJECT_TRACKER, +// annotated channels are denied storage which means that the ServiceWorker +// should not intercept the channel. However, the decision for tracking +// protection to annotate a channel only occurs in the parent process. The +// dummy channel is a hack to allow the intercept decision process to ask the +// parent process if the channel should be annotated. Because this round-trip +// to the parent has overhead, the dummy channel is only created 1) if the +// ServiceWorker initially determines that the channel should be intercepted and +// 2) it's a navigation request. +// +// This hack can be removed once Bug 1231208's new "parent intercept" mechanism +// fully lands, the pref is enabled by default it stays enabled for long enough +// to be confident we will never need/want to turn it off. Then as part of bug +// 1496997 we can remove this implementation. Bug 1498259 covers removing this +// hack in particular. +protocol PClassifierDummyChannel +{ + manager PNecko; + +child: + async __delete__(uint32_t aClassificationFlags); +}; + +} // namespace net +} // namespace mozilla + diff --git a/netwerk/protocol/http/PHttpBackgroundChannel.ipdl b/netwerk/protocol/http/PHttpBackgroundChannel.ipdl new file mode 100644 index 0000000000..d52649506b --- /dev/null +++ b/netwerk/protocol/http/PHttpBackgroundChannel.ipdl @@ -0,0 +1,80 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et tw=80 ft=cpp : */ + +/* 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 protocol PBackground; +include protocol PStreamFilter; +include NeckoChannelParams; +include HttpChannelParams; +include PURLClassifierInfo; + +include "mozilla/net/NeckoMessageUtils.h"; + +using nsIHttpChannel::FlashPluginState from "nsIHttpChannel.h"; + +namespace mozilla { +namespace net { + +//------------------------------------------------------------------- +async refcounted protocol PHttpBackgroundChannel +{ + manager PBackground; + +child: + async OnStartRequest(nsHttpResponseHead responseHead, + bool useResponseHead, + nsHttpHeaderArray requestHeaders, + HttpChannelOnStartRequestArgs args); + + // Combines a single OnDataAvailable and its associated OnProgress & + // OnStatus calls into one IPDL message + async OnTransportAndData(nsresult channelStatus, + nsresult transportStatus, + uint64_t offset, + uint32_t count, + nsDependentCSubstring data, + bool dataFromSocketProcess); + + + async OnStopRequest(nsresult channelStatus, + ResourceTimingStructArgs timing, + TimeStamp lastActiveTabOptimization, + nsHttpHeaderArray responseTrailers, + ConsoleReportCollected[] consoleReport, + bool fromSocketProcess); + + async OnConsoleReport(ConsoleReportCollected[] consoleReport); + + async OnProgress(int64_t progress, int64_t progressMax); + + async OnStatus(nsresult status); + + // Forwards nsIMultiPartChannelListener::onAfterLastPart. Make sure we've + // disconnected our listeners, since we might not have on the last + // OnStopRequest. + async OnAfterLastPart(nsresult aStatus); + + // Tell the child that the resource being loaded has been classified. + async NotifyClassificationFlags(uint32_t aClassificationFlags, bool aIsThirdParty); + + // Tell the child that the current channel's document is not allowed to load + // flash content. + async NotifyFlashPluginStateChanged(FlashPluginState aState); + + // Tell the child information of matched URL againts SafeBrowsing list + async SetClassifierMatchedInfo(ClassifierInfo info); + + // Tell the child information of matched URL againts SafeBrowsing tracking list + async SetClassifierMatchedTrackingInfo(ClassifierInfo info); + + async AttachStreamFilter(Endpoint aEndpoint); + + async __delete__(); + +}; + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/PHttpChannel.ipdl b/netwerk/protocol/http/PHttpChannel.ipdl new file mode 100644 index 0000000000..7eab2f7562 --- /dev/null +++ b/netwerk/protocol/http/PHttpChannel.ipdl @@ -0,0 +1,152 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et tw=80 ft=cpp : */ + +/* 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 protocol PNecko; +include InputStreamParams; +include URIParams; +include PBackgroundSharedTypes; +include NeckoChannelParams; +include IPCServiceWorkerDescriptor; +include IPCStream; +include HttpChannelParams; + +include "mozilla/dom/ReferrerInfoUtils.h"; +include "mozilla/net/NeckoMessageUtils.h"; + +using mozilla::OriginAttributes from "mozilla/ipc/BackgroundUtils.h"; + +namespace mozilla { +namespace net { + +//------------------------------------------------------------------- +refcounted protocol PHttpChannel +{ + manager PNecko; + +parent: + // Note: channels are opened during construction, so no open method here: + // see PNecko.ipdl + + async SetClassOfService(uint32_t cos); + + async Suspend(); + async Resume(); + + async Cancel(nsresult status, uint32_t requestBlockingReason); + + // Reports approval/veto of redirect by child process redirect observers + async Redirect2Verify(nsresult result, RequestHeaderTuples changedHeaders, + uint32_t sourceRequestBlockingReason, + ChildLoadInfoForwarderArgs? targetLoadInfoForwarder, + uint32_t loadFlags, nsIReferrerInfo referrerInfo, + URIParams? apiRedirectTo, + CorsPreflightArgs? corsPreflightArgs, + bool chooseAppcache); + + // For document loads we keep this protocol open after child's + // OnStopRequest, and send this msg (instead of __delete__) to allow + // partial cleanup on parent. + async DocumentChannelCleanup(bool clearCacheEntry); + + // This might have to be sync. If this fails we must fail the document load + // to avoid endless loop. + // + // Explanation: the document loaded was loaded from the offline cache. But + // the cache group id (the manifest URL) of the cache group it was loaded + // from is different then the manifest the document refers to in the html + // tag. If we detect this during the cache selection algorithm, we must not + // load this document from the offline cache group it was just loaded from. + // Marking the cache entry as foreign in its cache group will prevent + // the document to load from the bad offline cache group. After it is marked, + // we reload the document to take the effect. If we fail to mark the entry + // as foreign, we will end up in the same situation and reload again and + // again, indefinitely. + async MarkOfflineCacheEntryAsForeign(); + + // Child has detected a CORS check failure, so needs to tell the parent + // to remove any matching entry from the CORS preflight cache. + async RemoveCorsPreflightCacheEntry(URIParams uri, + PrincipalInfo requestingPrincipal, + OriginAttributes originAttributes); + + // After receiving this message, the parent calls SendDeleteSelf, and makes + // sure not to send any more messages after that. + async DeletingChannel(); + + // Called to get the input stream when altData was delivered. + async OpenOriginalCacheInputStream(); + + // Called to get the input stream when altData is available. + async OpenAltDataCacheInputStream(nsCString aType); + + // Tell the parent the amount bytes read by child for the e10s back pressure + // flow control + async BytesRead(int32_t count); + + async __delete__(); + +child: + // Used to cancel child channel if we hit errors during creating and + // AsyncOpen of nsHttpChannel on the parent. + async FailedAsyncOpen(nsresult status); + + // OnStartRequest is sent over PHttpBackgroundChannel. However, sometime we + // need to wait for some PContent IPCs, e.g., permission, cookies. Those IPC + // are sent just before the background thread OnStartRequest, which is racy. + // Therefore, need one main thread IPC event for synchronizing the event + // sequence. + async OnStartRequestSent(); + + // Called to initiate content channel redirect, starts talking to sinks + // on the content process and reports result via Redirect2Verify above + async Redirect1Begin(uint32_t registrarId, + URIParams newOriginalUri, + uint32_t newLoadFlags, + uint32_t redirectFlags, + ParentLoadInfoForwarderArgs loadInfoForwarder, + nsHttpResponseHead responseHead, + nsCString securityInfoSerialization, + uint64_t channelId, + NetAddr oldPeerAddr, + ResourceTimingStructArgs timing); + + // Called if redirect successful so that child can complete setup. + async Redirect3Complete(); + + // Report a security message to the console associated with this + // channel. + async ReportSecurityMessage(nsString messageTag, nsString messageCategory); + + // Tell child to delete channel (all IPDL deletes must be done from child to + // avoid races: see bug 591708). + async DeleteSelf(); + + // Tell the child to issue a deprecation warning. + async IssueDeprecationWarning(uint32_t warning, bool asError); + + // When CORS blocks the request in the parent process, it doesn't have the + // correct window ID, so send the message to the child for logging to the web + // console. + async LogBlockedCORSRequest(nsString message, nsCString category); + + async LogMimeTypeMismatch(nsCString messageName, + bool warning, + nsString url, + nsString contentType); + + async OriginalCacheInputStreamAvailable(IPCStream? stream); + + async AltDataCacheInputStreamAvailable(IPCStream? stream); + + +both: + async SetPriority(int16_t priority); +}; + + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/PHttpChannelParams.h b/netwerk/protocol/http/PHttpChannelParams.h new file mode 100644 index 0000000000..ef7582dfe5 --- /dev/null +++ b/netwerk/protocol/http/PHttpChannelParams.h @@ -0,0 +1,280 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et 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/. */ + +#ifndef mozilla_net_PHttpChannelParams_h +#define mozilla_net_PHttpChannelParams_h + +#define ALLOW_LATE_NSHTTP_H_INCLUDE 1 +#include "base/basictypes.h" + +#include "ipc/IPCMessageUtils.h" +#include "ipc/IPCMessageUtilsSpecializations.h" +#include "nsHttp.h" +#include "nsHttpHeaderArray.h" +#include "nsHttpRequestHead.h" +#include "nsHttpResponseHead.h" +#include "mozilla/Tuple.h" + +namespace mozilla { +namespace net { + +struct RequestHeaderTuple { + nsCString mHeader; + nsCString mValue; + bool mMerge; + bool mEmpty; + + bool operator==(const RequestHeaderTuple& other) const { + return mHeader.Equals(other.mHeader) && mValue.Equals(other.mValue) && + mMerge == other.mMerge && mEmpty == other.mEmpty; + } +}; + +typedef CopyableTArray RequestHeaderTuples; + +} // namespace net +} // namespace mozilla + +namespace IPC { + +template <> +struct ParamTraits { + typedef mozilla::net::RequestHeaderTuple paramType; + + static void Write(Message* aMsg, const paramType& aParam) { + WriteParam(aMsg, aParam.mHeader); + WriteParam(aMsg, aParam.mValue); + WriteParam(aMsg, aParam.mMerge); + WriteParam(aMsg, aParam.mEmpty); + } + + static bool Read(const Message* aMsg, PickleIterator* aIter, + paramType* aResult) { + if (!ReadParam(aMsg, aIter, &aResult->mHeader) || + !ReadParam(aMsg, aIter, &aResult->mValue) || + !ReadParam(aMsg, aIter, &aResult->mMerge) || + !ReadParam(aMsg, aIter, &aResult->mEmpty)) + return false; + + return true; + } +}; + +template <> +struct ParamTraits { + typedef mozilla::net::nsHttpAtom paramType; + + static void Write(Message* aMsg, const paramType& aParam) { + // aParam.get() cannot be null. + MOZ_ASSERT(aParam.get(), "null nsHTTPAtom value"); + nsAutoCString value(aParam.get()); + WriteParam(aMsg, value); + } + + static bool Read(const Message* aMsg, PickleIterator* aIter, + paramType* aResult) { + nsAutoCString value; + if (!ReadParam(aMsg, aIter, &value)) return false; + + *aResult = mozilla::net::nsHttp::ResolveAtom(value.get()); + MOZ_ASSERT(aResult->get(), "atom table not initialized"); + return true; + } +}; + +template <> +struct ParamTraits { + typedef mozilla::net::nsHttpHeaderArray::nsEntry paramType; + + static void Write(Message* aMsg, const paramType& aParam) { + if (aParam.headerNameOriginal.IsEmpty()) { + WriteParam(aMsg, aParam.header); + } else { + WriteParam(aMsg, aParam.headerNameOriginal); + } + WriteParam(aMsg, aParam.value); + switch (aParam.variety) { + case mozilla::net::nsHttpHeaderArray::eVarietyUnknown: + WriteParam(aMsg, (uint8_t)0); + break; + case mozilla::net::nsHttpHeaderArray::eVarietyRequestOverride: + WriteParam(aMsg, (uint8_t)1); + break; + case mozilla::net::nsHttpHeaderArray::eVarietyRequestDefault: + WriteParam(aMsg, (uint8_t)2); + break; + case mozilla::net::nsHttpHeaderArray:: + eVarietyResponseNetOriginalAndResponse: + WriteParam(aMsg, (uint8_t)3); + break; + case mozilla::net::nsHttpHeaderArray::eVarietyResponseNetOriginal: + WriteParam(aMsg, (uint8_t)4); + break; + case mozilla::net::nsHttpHeaderArray::eVarietyResponse: + WriteParam(aMsg, (uint8_t)5); + } + } + + static bool Read(const Message* aMsg, PickleIterator* aIter, + paramType* aResult) { + uint8_t variety; + nsAutoCString header; + if (!ReadParam(aMsg, aIter, &header) || + !ReadParam(aMsg, aIter, &aResult->value) || + !ReadParam(aMsg, aIter, &variety)) + return false; + + mozilla::net::nsHttpAtom atom = mozilla::net::nsHttp::ResolveAtom(header); + aResult->header = atom; + if (!header.Equals(atom.get())) { + aResult->headerNameOriginal = header; + } + + switch (variety) { + case 0: + aResult->variety = mozilla::net::nsHttpHeaderArray::eVarietyUnknown; + break; + case 1: + aResult->variety = + mozilla::net::nsHttpHeaderArray::eVarietyRequestOverride; + break; + case 2: + aResult->variety = + mozilla::net::nsHttpHeaderArray::eVarietyRequestDefault; + break; + case 3: + aResult->variety = mozilla::net::nsHttpHeaderArray:: + eVarietyResponseNetOriginalAndResponse; + break; + case 4: + aResult->variety = + mozilla::net::nsHttpHeaderArray::eVarietyResponseNetOriginal; + break; + case 5: + aResult->variety = mozilla::net::nsHttpHeaderArray::eVarietyResponse; + break; + default: + return false; + } + + return true; + } +}; + +template <> +struct ParamTraits { + typedef mozilla::net::nsHttpHeaderArray paramType; + + static void Write(Message* aMsg, const paramType& aParam) { + paramType& p = const_cast(aParam); + + WriteParam(aMsg, p.mHeaders); + } + + static bool Read(const Message* aMsg, PickleIterator* aIter, + paramType* aResult) { + if (!ReadParam(aMsg, aIter, &aResult->mHeaders)) return false; + + return true; + } +}; + +template <> +struct ParamTraits { + typedef mozilla::net::nsHttpRequestHead paramType; + + static void Write(Message* aMsg, const paramType& aParam) { + WriteParam(aMsg, aParam.mHeaders); + WriteParam(aMsg, aParam.mMethod); + WriteParam(aMsg, static_cast(aParam.mVersion)); + WriteParam(aMsg, aParam.mRequestURI); + WriteParam(aMsg, aParam.mPath); + WriteParam(aMsg, aParam.mOrigin); + WriteParam(aMsg, static_cast(aParam.mParsedMethod)); + WriteParam(aMsg, aParam.mHTTPS); + } + + static bool Read(const Message* aMsg, PickleIterator* aIter, + paramType* aResult) { + uint32_t version; + uint8_t method; + if (!ReadParam(aMsg, aIter, &aResult->mHeaders) || + !ReadParam(aMsg, aIter, &aResult->mMethod) || + !ReadParam(aMsg, aIter, &version) || + !ReadParam(aMsg, aIter, &aResult->mRequestURI) || + !ReadParam(aMsg, aIter, &aResult->mPath) || + !ReadParam(aMsg, aIter, &aResult->mOrigin) || + !ReadParam(aMsg, aIter, &method) || + !ReadParam(aMsg, aIter, &aResult->mHTTPS)) { + return false; + } + + aResult->mVersion = static_cast(version); + aResult->mParsedMethod = + static_cast(method); + return true; + } +}; + +// Note that the code below MUST be synchronized with the code in +// nsHttpRequestHead's copy constructor. +template <> +struct ParamTraits { + typedef mozilla::net::nsHttpResponseHead paramType; + + static void Write(Message* aMsg, const paramType& aParam) { + WriteParam(aMsg, aParam.mHeaders); + WriteParam(aMsg, static_cast(aParam.mVersion)); + WriteParam(aMsg, aParam.mStatus); + WriteParam(aMsg, aParam.mStatusText); + WriteParam(aMsg, aParam.mContentLength); + WriteParam(aMsg, aParam.mContentType); + WriteParam(aMsg, aParam.mContentCharset); + WriteParam(aMsg, aParam.mHasCacheControl); + WriteParam(aMsg, aParam.mCacheControlPublic); + WriteParam(aMsg, aParam.mCacheControlPrivate); + WriteParam(aMsg, aParam.mCacheControlNoStore); + WriteParam(aMsg, aParam.mCacheControlNoCache); + WriteParam(aMsg, aParam.mCacheControlImmutable); + WriteParam(aMsg, aParam.mCacheControlStaleWhileRevalidateSet); + WriteParam(aMsg, aParam.mCacheControlStaleWhileRevalidate); + WriteParam(aMsg, aParam.mCacheControlMaxAgeSet); + WriteParam(aMsg, aParam.mCacheControlMaxAge); + WriteParam(aMsg, aParam.mPragmaNoCache); + } + + static bool Read(const Message* aMsg, PickleIterator* aIter, + paramType* aResult) { + uint32_t version; + if (!ReadParam(aMsg, aIter, &aResult->mHeaders) || + !ReadParam(aMsg, aIter, &version) || + !ReadParam(aMsg, aIter, &aResult->mStatus) || + !ReadParam(aMsg, aIter, &aResult->mStatusText) || + !ReadParam(aMsg, aIter, &aResult->mContentLength) || + !ReadParam(aMsg, aIter, &aResult->mContentType) || + !ReadParam(aMsg, aIter, &aResult->mContentCharset) || + !ReadParam(aMsg, aIter, &aResult->mHasCacheControl) || + !ReadParam(aMsg, aIter, &aResult->mCacheControlPublic) || + !ReadParam(aMsg, aIter, &aResult->mCacheControlPrivate) || + !ReadParam(aMsg, aIter, &aResult->mCacheControlNoStore) || + !ReadParam(aMsg, aIter, &aResult->mCacheControlNoCache) || + !ReadParam(aMsg, aIter, &aResult->mCacheControlImmutable) || + !ReadParam(aMsg, aIter, + &aResult->mCacheControlStaleWhileRevalidateSet) || + !ReadParam(aMsg, aIter, &aResult->mCacheControlStaleWhileRevalidate) || + !ReadParam(aMsg, aIter, &aResult->mCacheControlMaxAgeSet) || + !ReadParam(aMsg, aIter, &aResult->mCacheControlMaxAge) || + !ReadParam(aMsg, aIter, &aResult->mPragmaNoCache)) + return false; + + aResult->mVersion = static_cast(version); + return true; + } +}; + +} // namespace IPC + +#endif // mozilla_net_PHttpChannelParams_h diff --git a/netwerk/protocol/http/PHttpConnectionMgr.ipdl b/netwerk/protocol/http/PHttpConnectionMgr.ipdl new file mode 100644 index 0000000000..535ab34c68 --- /dev/null +++ b/netwerk/protocol/http/PHttpConnectionMgr.ipdl @@ -0,0 +1,40 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et tw=80 ft=cpp : */ + +/* 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 protocol PAltSvcTransaction; +include protocol PSocketProcess; +include protocol PHttpTransaction; + +include NeckoChannelParams; + +namespace mozilla { +namespace net { + +async refcounted protocol PHttpConnectionMgr +{ + manager PSocketProcess; + +child: + async __delete__(); + + async DoShiftReloadConnectionCleanupWithConnInfo(HttpConnectionInfoCloneArgs aArgs); + async UpdateCurrentTopLevelOuterContentWindowId(uint64_t aWindowId); + async AddTransaction(PHttpTransaction aTrans, int32_t aPriority); + async AddTransactionWithStickyConn(PHttpTransaction aTrans, int32_t aPriority, + PHttpTransaction aTransWithStickyConn); + async RescheduleTransaction(PHttpTransaction aTrans, int32_t aPriority); + async UpdateClassOfServiceOnTransaction(PHttpTransaction aTrans, + uint32_t aClassOfService); + async CancelTransaction(PHttpTransaction aTrans, nsresult aReason); + async SpeculativeConnect(HttpConnectionInfoCloneArgs aConnInfo, + SpeculativeConnectionOverriderArgs? aOverriderArgs, + uint32_t aCaps, PAltSvcTransaction? aTrans, + bool aFetchHTTPSRR); +}; + +} // namespace net +} // namespace mozilla \ No newline at end of file diff --git a/netwerk/protocol/http/PHttpTransaction.ipdl b/netwerk/protocol/http/PHttpTransaction.ipdl new file mode 100644 index 0000000000..0628dd6f93 --- /dev/null +++ b/netwerk/protocol/http/PHttpTransaction.ipdl @@ -0,0 +1,106 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et tw=80 ft=cpp : */ + +/* 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 protocol PSocketProcess; +include protocol PFileDescriptorSet; // FIXME: bug #792908 +include protocol PInputChannelThrottleQueue; + +include IPCStream; +include NeckoChannelParams; + +include "mozilla/net/NeckoMessageUtils.h"; + +using class mozilla::net::nsHttpRequestHead from "nsHttpRequestHead.h"; +using class nsHttpHeaderArray from "nsHttpHeaderArray.h"; +using mozilla::net::NetAddr from "mozilla/net/DNS.h"; + +namespace mozilla { +namespace net { + +struct H2PushedStreamArg { + PHttpTransaction transWithPushedStream; + uint32_t pushedStreamId; +}; + +struct NetworkAddressArg { + NetAddr selfAddr; + NetAddr peerAddr; + bool resolvedByTRR; + bool echConfigUsed; +}; + +refcounted protocol PHttpTransaction +{ + manager PSocketProcess; + +parent: + async OnStartRequest(nsresult status, + nsHttpResponseHead? responseHead, + nsCString securityInfoSerialization, + bool proxyConnectFailed, + TimingStructArgs timings, + int32_t proxyConnectResponseCode, + uint8_t[] dataForSniffer, + nsCString? altSvcUsed, + bool dataToChildProcess, + bool restarted, + uint32_t HTTPSSVCReceivedStage, + bool supportsHttp3); + async OnTransportStatus(nsresult status, + int64_t progress, + int64_t progressMax, + NetworkAddressArg? networkAddressArg); + async OnDataAvailable(nsCString data, + uint64_t offset, + uint32_t count); + async OnStopRequest(nsresult status, + bool responseIsComplete, + int64_t transferSize, + TimingStructArgs timings, + nsHttpHeaderArray? responseTrailers, + TransactionObserverResult? transactionObserverResult, + TimeStamp lastActiveTabOptimization, + uint32_t caps, + HttpConnectionInfoCloneArgs connInfoArgs); + async OnInitFailed(nsresult status); + async OnH2PushStream(uint32_t pushedStreamId, + nsCString resourceUrl, + nsCString requestString); + +child: + async __delete__(); + async Init(uint32_t caps, + HttpConnectionInfoCloneArgs aArgs, + nsHttpRequestHead reqHeaders, + IPCStream? requestBody, + uint64_t reqContentLength, + bool reqBodyIncludesHeaders, + uint64_t topLevelOuterContentWindowId, + uint8_t httpTrafficCategory, + uint64_t requestContextID, + uint32_t classOfService, + uint32_t initialRwin, + bool responseTimeoutEnabled, + uint64_t channelId, + bool hasTransactionObserver, + H2PushedStreamArg? pushedStreamArg, + PInputChannelThrottleQueue? throttleQueue, + bool aIsDocumentLoad, + TimeStamp aRedirectStart, + TimeStamp aRedirectEnd); + + async CancelPump(nsresult status); + async SuspendPump(); + async ResumePump(); + + async SetDNSWasRefreshed(); + async DontReuseConnection(); + async SetH2WSConnRefTaken(); +}; + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/PSpdyPush.h b/netwerk/protocol/http/PSpdyPush.h new file mode 100644 index 0000000000..1f04430745 --- /dev/null +++ b/netwerk/protocol/http/PSpdyPush.h @@ -0,0 +1,56 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +// SPDY Server Push + +/* + A pushed stream is put into a memory buffer (The SpdyPushTransactionBuffer) + and spooled there until a GET is made that can be matched up with it. At + that time we have two spdy streams - the GET (aka the sink) and the PUSH + (aka the source). Data is copied between those two streams for the lifetime + of the transaction. This is true even if the transaction buffer is empty, + partly complete, or totally loaded at the time the GET correspondence is made. + + correspondence is done through a hash table of the full url, the spdy session, + and the load group. The load group is implicit because that's where the + hash is stored, the other items comprise the hash key. + + Pushed streams are subject to aggressive flow control before they are matched + with a GET at which point flow control is effectively disabled to match the + client pull behavior. +*/ + +#ifndef mozilla_net_SpdyPush_Public_h +#define mozilla_net_SpdyPush_Public_h + +#include "nsDataHashtable.h" +#include "nsISupports.h" +#include "nsStringFwd.h" + +namespace mozilla { +namespace net { + +class Http2PushedStream; + +// One cache per load group +class SpdyPushCache { + public: + // The cache holds only weak pointers - no references + SpdyPushCache() = default; + virtual ~SpdyPushCache(); + [[nodiscard]] bool RegisterPushedStreamHttp2(const nsCString& key, + Http2PushedStream* stream); + Http2PushedStream* RemovePushedStreamHttp2(const nsCString& key); + Http2PushedStream* RemovePushedStreamHttp2ByID(const nsCString& key, + const uint32_t& streamID); + + private: + nsDataHashtable mHashHttp2; +}; + +} // namespace net +} // namespace mozilla + +#endif // mozilla_net_SpdyPush_Public_h diff --git a/netwerk/protocol/http/ParentChannelListener.cpp b/netwerk/protocol/http/ParentChannelListener.cpp new file mode 100644 index 0000000000..09a3432da0 --- /dev/null +++ b/netwerk/protocol/http/ParentChannelListener.cpp @@ -0,0 +1,282 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et 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/. */ + +// HttpLog.h should generally be included first +#include "HttpLog.h" + +#include "ParentChannelListener.h" +#include "mozilla/dom/ContentParent.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/ServiceWorkerInterceptController.h" +#include "mozilla/dom/ServiceWorkerUtils.h" +#include "mozilla/net/HttpChannelParent.h" +#include "mozilla/net/RedirectChannelRegistrar.h" +#include "mozilla/SchedulerGroup.h" +#include "mozilla/Unused.h" +#include "nsIHttpHeaderVisitor.h" +#include "nsIPrompt.h" +#include "nsISecureBrowserUI.h" +#include "nsIWindowWatcher.h" +#include "nsQueryObject.h" +#include "nsIAuthPrompt.h" +#include "nsIAuthPrompt2.h" +#include "nsIPromptFactory.h" +#include "Element.h" +#include "nsILoginManagerAuthPrompter.h" +#include "mozilla/dom/CanonicalBrowsingContext.h" +#include "mozilla/dom/LoadURIOptionsBinding.h" +#include "nsIWebNavigation.h" + +using mozilla::Unused; +using mozilla::dom::ServiceWorkerInterceptController; +using mozilla::dom::ServiceWorkerParentInterceptEnabled; + +namespace mozilla { +namespace net { + +ParentChannelListener::ParentChannelListener( + nsIStreamListener* aListener, + dom::CanonicalBrowsingContext* aBrowsingContext, bool aUsePrivateBrowsing) + : mNextListener(aListener), mBrowsingContext(aBrowsingContext) { + LOG(("ParentChannelListener::ParentChannelListener [this=%p, next=%p]", this, + aListener)); + + if (ServiceWorkerParentInterceptEnabled()) { + mInterceptController = new ServiceWorkerInterceptController(); + } +} + +ParentChannelListener::~ParentChannelListener() { + LOG(("ParentChannelListener::~ParentChannelListener %p", this)); +} + +//----------------------------------------------------------------------------- +// ParentChannelListener::nsISupports +//----------------------------------------------------------------------------- + +NS_IMPL_ADDREF(ParentChannelListener) +NS_IMPL_RELEASE(ParentChannelListener) +NS_INTERFACE_MAP_BEGIN(ParentChannelListener) + NS_INTERFACE_MAP_ENTRY(nsIInterfaceRequestor) + NS_INTERFACE_MAP_ENTRY(nsIStreamListener) + NS_INTERFACE_MAP_ENTRY(nsIRequestObserver) + NS_INTERFACE_MAP_ENTRY(nsIMultiPartChannelListener) + NS_INTERFACE_MAP_ENTRY(nsINetworkInterceptController) + NS_INTERFACE_MAP_ENTRY(nsIThreadRetargetableStreamListener) + NS_INTERFACE_MAP_ENTRY_CONDITIONAL(nsIAuthPromptProvider, mBrowsingContext) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIInterfaceRequestor) + NS_INTERFACE_MAP_ENTRY_CONCRETE(ParentChannelListener) +NS_INTERFACE_MAP_END + +//----------------------------------------------------------------------------- +// ParentChannelListener::nsIRequestObserver +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +ParentChannelListener::OnStartRequest(nsIRequest* aRequest) { + if (!mNextListener) return NS_ERROR_UNEXPECTED; + + // If we're not a multi-part channel, then we can drop mListener and break the + // reference cycle. If we are, then this might be called again, so wait for + // OnAfterLastPart instead. + nsCOMPtr multiPartChannel = do_QueryInterface(aRequest); + if (multiPartChannel) { + mIsMultiPart = true; + } + + LOG(("ParentChannelListener::OnStartRequest [this=%p]\n", this)); + return mNextListener->OnStartRequest(aRequest); +} + +NS_IMETHODIMP +ParentChannelListener::OnStopRequest(nsIRequest* aRequest, + nsresult aStatusCode) { + if (!mNextListener) return NS_ERROR_UNEXPECTED; + + LOG(("ParentChannelListener::OnStopRequest: [this=%p status=%" PRIu32 "]\n", + this, static_cast(aStatusCode))); + nsresult rv = mNextListener->OnStopRequest(aRequest, aStatusCode); + + if (!mIsMultiPart) { + mNextListener = nullptr; + } + return rv; +} + +//----------------------------------------------------------------------------- +// ParentChannelListener::nsIStreamListener +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +ParentChannelListener::OnDataAvailable(nsIRequest* aRequest, + nsIInputStream* aInputStream, + uint64_t aOffset, uint32_t aCount) { + if (!mNextListener) return NS_ERROR_UNEXPECTED; + + LOG(("ParentChannelListener::OnDataAvailable [this=%p]\n", this)); + return mNextListener->OnDataAvailable(aRequest, aInputStream, aOffset, + aCount); +} + +//----------------------------------------------------------------------------- +// ParentChannelListener::nsIMultiPartChannelListener +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +ParentChannelListener::OnAfterLastPart(nsresult aStatus) { + nsCOMPtr multiListener = + do_QueryInterface(mNextListener); + if (multiListener) { + multiListener->OnAfterLastPart(aStatus); + } + + mNextListener = nullptr; + return NS_OK; +} + +//----------------------------------------------------------------------------- +// ParentChannelListener::nsIInterfaceRequestor +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +ParentChannelListener::GetInterface(const nsIID& aIID, void** result) { + if (aIID.Equals(NS_GET_IID(nsINetworkInterceptController))) { + return QueryInterface(aIID, result); + } + + if (mBrowsingContext && aIID.Equals(NS_GET_IID(nsIPrompt))) { + nsCOMPtr frameElement = + mBrowsingContext->Top()->GetEmbedderElement(); + if (frameElement) { + nsCOMPtr win = frameElement->OwnerDoc()->GetWindow(); + NS_ENSURE_TRUE(win, NS_ERROR_NO_INTERFACE); + + nsresult rv; + nsCOMPtr wwatch = + do_GetService(NS_WINDOWWATCHER_CONTRACTID, &rv); + + if (NS_WARN_IF(!NS_SUCCEEDED(rv))) { + return NS_ERROR_NO_INTERFACE; + } + + nsCOMPtr prompt; + rv = wwatch->GetNewPrompter(win, getter_AddRefs(prompt)); + if (NS_WARN_IF(!NS_SUCCEEDED(rv))) { + return NS_ERROR_NO_INTERFACE; + } + + prompt.forget(result); + return NS_OK; + } + } + + if (mBrowsingContext && (aIID.Equals(NS_GET_IID(nsIAuthPrompt)) || + aIID.Equals(NS_GET_IID(nsIAuthPrompt2)))) { + nsresult rv = + GetAuthPrompt(nsIAuthPromptProvider::PROMPT_NORMAL, aIID, result); + if (NS_FAILED(rv)) { + return NS_ERROR_NO_INTERFACE; + } + return NS_OK; + } + + nsCOMPtr ir; + if (mNextListener && NS_SUCCEEDED(CallQueryInterface(mNextListener.get(), + getter_AddRefs(ir)))) { + return ir->GetInterface(aIID, result); + } + + return NS_NOINTERFACE; +} + +void ParentChannelListener::SetListenerAfterRedirect( + nsIStreamListener* aListener) { + mNextListener = aListener; +} + +//----------------------------------------------------------------------------- +// ParentChannelListener::nsINetworkInterceptController +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +ParentChannelListener::ShouldPrepareForIntercept(nsIURI* aURI, + nsIChannel* aChannel, + bool* aShouldIntercept) { + // If parent-side interception is enabled just forward to the real + // network controler. + if (mInterceptController) { + return mInterceptController->ShouldPrepareForIntercept(aURI, aChannel, + aShouldIntercept); + } + *aShouldIntercept = false; + return NS_OK; +} + +NS_IMETHODIMP +ParentChannelListener::ChannelIntercepted(nsIInterceptedChannel* aChannel) { + // If parent-side interception is enabled just forward to the real + // network controler. + if (mInterceptController) { + return mInterceptController->ChannelIntercepted(aChannel); + } + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// ParentChannelListener::nsIAuthPromptProvider +// + +NS_IMETHODIMP +ParentChannelListener::GetAuthPrompt(uint32_t aPromptReason, const nsIID& iid, + void** aResult) { + if (!mBrowsingContext) { + return NS_ERROR_NOT_AVAILABLE; + } + // we're either allowing auth, or it's a proxy request + nsresult rv; + nsCOMPtr wwatch = + do_GetService(NS_WINDOWWATCHER_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr window; + RefPtr frame = mBrowsingContext->Top()->GetEmbedderElement(); + if (frame) window = frame->OwnerDoc()->GetWindow(); + + // Get an auth prompter for our window so that the parenting + // of the dialogs works as it should when using tabs. + nsCOMPtr prompt; + rv = wwatch->GetPrompt(window, iid, getter_AddRefs(prompt)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr prompter = do_QueryInterface(prompt); + if (prompter) { + prompter->SetBrowser(frame); + } + + *aResult = prompt.forget().take(); + return NS_OK; +} + +//----------------------------------------------------------------------------- +// ParentChannelListener::nsIThreadRetargetableStreamListener +// + +NS_IMETHODIMP +ParentChannelListener::CheckListenerChain() { + MOZ_ASSERT(NS_IsMainThread()); + + nsCOMPtr listener = + do_QueryInterface(mNextListener); + if (!listener) { + return NS_ERROR_NO_INTERFACE; + } + + return listener->CheckListenerChain(); +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/ParentChannelListener.h b/netwerk/protocol/http/ParentChannelListener.h new file mode 100644 index 0000000000..1b33c0da77 --- /dev/null +++ b/netwerk/protocol/http/ParentChannelListener.h @@ -0,0 +1,92 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et 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/. */ + +#ifndef mozilla_net_ParentChannelListener_h +#define mozilla_net_ParentChannelListener_h + +#include "mozilla/dom/CanonicalBrowsingContext.h" +#include "nsIAuthPromptProvider.h" +#include "nsIInterfaceRequestor.h" +#include "nsIMultiPartChannel.h" +#include "nsINetworkInterceptController.h" +#include "nsIStreamListener.h" +#include "nsIThreadRetargetableStreamListener.h" + +namespace mozilla { +namespace net { + +#define PARENT_CHANNEL_LISTENER \ + { \ + 0xa4e2c10c, 0xceba, 0x457f, { \ + 0xa8, 0x0d, 0x78, 0x2b, 0x23, 0xba, 0xbd, 0x16 \ + } \ + } + +// Note: nsIInterfaceRequestor must be the first base so that do_QueryObject() +// works correctly on this object, as it's needed to compute a void* pointing to +// the beginning of this object. + +class ParentChannelListener final : public nsIInterfaceRequestor, + public nsIStreamListener, + public nsIMultiPartChannelListener, + public nsINetworkInterceptController, + public nsIThreadRetargetableStreamListener, + private nsIAuthPromptProvider { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIINTERFACEREQUESTOR + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMLISTENER + NS_DECL_NSIMULTIPARTCHANNELLISTENER + NS_DECL_NSINETWORKINTERCEPTCONTROLLER + NS_DECL_NSIAUTHPROMPTPROVIDER + NS_DECL_NSITHREADRETARGETABLESTREAMLISTENER + + NS_DECLARE_STATIC_IID_ACCESSOR(PARENT_CHANNEL_LISTENER) + + explicit ParentChannelListener( + nsIStreamListener* aListener, + dom::CanonicalBrowsingContext* aBrowsingContext, + bool aUsePrivateBrowsing); + + // Called to set a new listener which replaces the old one after a redirect. + void SetListenerAfterRedirect(nsIStreamListener* aListener); + + dom::CanonicalBrowsingContext* GetBrowsingContext() const { + return mBrowsingContext; + } + + private: + virtual ~ParentChannelListener(); + + // Can be the original HttpChannelParent that created this object (normal + // case), a different {HTTP|FTP}ChannelParent that we've been redirected to, + // or some other listener that we have been diverted to via + // nsIDivertableChannel. + nsCOMPtr mNextListener; + + // This will be populated with a real network controller if parent-side + // interception is enabled. + nsCOMPtr mInterceptController; + + RefPtr mBrowsingContext; + + // True if we received OnStartRequest for a nsIMultiPartChannel, and are + // expected AllPartsStopped to be called when complete. + bool mIsMultiPart = false; +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(ParentChannelListener, PARENT_CHANNEL_LISTENER) + +inline nsISupports* ToSupports(ParentChannelListener* aDoc) { + return static_cast(aDoc); +} + +} // namespace net +} // namespace mozilla + +#endif // mozilla_net_ParentChannelListener_h diff --git a/netwerk/protocol/http/PendingTransactionInfo.cpp b/netwerk/protocol/http/PendingTransactionInfo.cpp new file mode 100644 index 0000000000..448d8757e4 --- /dev/null +++ b/netwerk/protocol/http/PendingTransactionInfo.cpp @@ -0,0 +1,135 @@ +/* vim:t ts=4 sw=2 sts=2 et cin: */ +/* 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/. */ + +// HttpLog.h should generally be included first +#include "HttpLog.h" + +#include "PendingTransactionInfo.h" +#include "HalfOpenSocket.h" + +// Log on level :5, instead of default :4. +#undef LOG +#define LOG(args) LOG5(args) +#undef LOG_ENABLED +#define LOG_ENABLED() LOG5_ENABLED() + +namespace mozilla { +namespace net { + +PendingTransactionInfo::~PendingTransactionInfo() { + if (mHalfOpen) { + RefPtr halfOpen = do_QueryReferent(mHalfOpen); + LOG( + ("PendingTransactionInfo::PendingTransactionInfo " + "[trans=%p halfOpen=%p]", + mTransaction.get(), halfOpen.get())); + if (halfOpen) { + halfOpen->Unclaim(); + } + mHalfOpen = nullptr; + } else if (mActiveConn) { + RefPtr activeConn = do_QueryReferent(mActiveConn); + if (activeConn && activeConn->Transaction() && + activeConn->Transaction()->IsNullTransaction()) { + NullHttpTransaction* nullTrans = + activeConn->Transaction()->QueryNullTransaction(); + nullTrans->Unclaim(); + LOG(( + "PendingTransactionInfo::PendingTransactionInfo - mark %p unclaimed.", + activeConn.get())); + } + } +} + +bool PendingTransactionInfo::IsAlreadyClaimedInitializingConn() { + LOG( + ("PendingTransactionInfo::IsAlreadyClaimedInitializingConn " + "[trans=%p, halfOpen=%p, activeConn=%p]\n", + mTransaction.get(), mHalfOpen.get(), mActiveConn.get())); + + // When this transaction has already established a half-open + // connection, we want to prevent any duplicate half-open + // connections from being established and bound to this + // transaction. Allow only use of an idle persistent connection + // (if found) for transactions referred by a half-open connection. + bool alreadyHalfOpenOrWaitingForTLS = false; + if (mHalfOpen) { + MOZ_ASSERT(!mActiveConn); + RefPtr halfOpen = do_QueryReferent(mHalfOpen); + LOG( + ("PendingTransactionInfo::IsAlreadyClaimedInitializingConn " + "[trans=%p, halfOpen=%p]\n", + mTransaction.get(), halfOpen.get())); + if (halfOpen) { + alreadyHalfOpenOrWaitingForTLS = true; + } else { + // If we have not found the halfOpen socket, remove the pointer. + mHalfOpen = nullptr; + } + } else if (mActiveConn) { + MOZ_ASSERT(!mHalfOpen); + RefPtr activeConn = do_QueryReferent(mActiveConn); + LOG( + ("PendingTransactionInfo::IsAlreadyClaimedInitializingConn " + "[trans=%p, activeConn=%p]\n", + mTransaction.get(), activeConn.get())); + // Check if this transaction claimed a connection that is still + // performing tls handshake with a NullHttpTransaction or it is between + // finishing tls and reclaiming (When nullTrans finishes tls handshake, + // httpConnection does not have a transaction any more and a + // ReclaimConnection is dispatched). But if an error occurred the + // connection will be closed, it will exist but CanReused will be + // false. + if (activeConn && + ((activeConn->Transaction() && + activeConn->Transaction()->IsNullTransaction()) || + (!activeConn->Transaction() && activeConn->CanReuse()))) { + alreadyHalfOpenOrWaitingForTLS = true; + } else { + // If we have not found the connection, remove the pointer. + mActiveConn = nullptr; + } + } + + return alreadyHalfOpenOrWaitingForTLS; +} + +void PendingTransactionInfo::AbandonHalfOpenAndForgetActiveConn() { + // Abandon all half-open sockets belonging to the given transaction. + RefPtr half = do_QueryReferent(mHalfOpen); + if (half) { + half->Abandon(); + } + mHalfOpen = nullptr; + mActiveConn = nullptr; +} + +bool PendingTransactionInfo::TryClaimingHalfOpen(HalfOpenSocket* sock) { + if (sock->Claim()) { + mHalfOpen = + do_GetWeakReference(static_cast(sock)); + return true; + } + return false; +} + +bool PendingTransactionInfo::TryClaimingActiveConn(HttpConnectionBase* conn) { + nsAHttpTransaction* activeTrans = conn->Transaction(); + NullHttpTransaction* nullTrans = + activeTrans ? activeTrans->QueryNullTransaction() : nullptr; + if (nullTrans && nullTrans->Claim()) { + mActiveConn = + do_GetWeakReference(static_cast(conn)); + return true; + } + return false; +} + +void PendingTransactionInfo::AddHalfOpen(HalfOpenSocket* sock) { + mHalfOpen = do_GetWeakReference(static_cast(sock)); +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/PendingTransactionInfo.h b/netwerk/protocol/http/PendingTransactionInfo.h new file mode 100644 index 0000000000..73f3a16089 --- /dev/null +++ b/netwerk/protocol/http/PendingTransactionInfo.h @@ -0,0 +1,60 @@ +/* vim:t ts=4 sw=2 sts=2 et cin: */ +/* 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/. */ + +#ifndef PendingTransactionInfo_h__ +#define PendingTransactionInfo_h__ + +#include "HalfOpenSocket.h" + +namespace mozilla { +namespace net { + +class PendingTransactionInfo final : public ARefBase { + public: + explicit PendingTransactionInfo(nsHttpTransaction* trans) + : mTransaction(trans) {} + + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(PendingTransactionInfo, override) + + void PrintDiagnostics(nsCString& log); + + // Return true if the transaction has claimed a HalfOpen socket or + // a connection in TLS handshake phase. + bool IsAlreadyClaimedInitializingConn(); + + void AbandonHalfOpenAndForgetActiveConn(); + + // Try to claim a halfOpen socket. We can only claim it if it is not + // claimed yet. + bool TryClaimingHalfOpen(HalfOpenSocket* sock); + // Similar as above, but for a ActiveConn that is performing a TLS handshake + // and has only a NullTransaction associated. + bool TryClaimingActiveConn(HttpConnectionBase* conn); + // It is similar as above, but in tihs case the halfOpen is made for this + // PendingTransactionInfo and it is already claimed. + void AddHalfOpen(HalfOpenSocket* sock); + + nsHttpTransaction* Transaction() const { return mTransaction; } + + private: + RefPtr mTransaction; + nsWeakPtr mHalfOpen; + nsWeakPtr mActiveConn; + + ~PendingTransactionInfo(); +}; + +class PendingComparator { + public: + bool Equals(const PendingTransactionInfo* aPendingTrans, + const nsAHttpTransaction* aTrans) const { + return aPendingTrans->Transaction() == aTrans; + } +}; + +} // namespace net +} // namespace mozilla + +#endif // !PendingTransactionInfo_h__ diff --git a/netwerk/protocol/http/PendingTransactionQueue.cpp b/netwerk/protocol/http/PendingTransactionQueue.cpp new file mode 100644 index 0000000000..6fea59be03 --- /dev/null +++ b/netwerk/protocol/http/PendingTransactionQueue.cpp @@ -0,0 +1,290 @@ +/* vim:set ts=4 sw=2 sts=2 et cin: */ +/* 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/. */ + +// HttpLog.h should generally be included first +#include "HttpLog.h" + +// Log on level :5, instead of default :4. +#undef LOG +#define LOG(args) LOG5(args) +#undef LOG_ENABLED +#define LOG_ENABLED() LOG5_ENABLED() + +#include "PendingTransactionQueue.h" +#include "nsHttpHandler.h" +#include "mozilla/ChaosMode.h" + +namespace mozilla { +namespace net { + +static uint64_t TabIdForQueuing(nsAHttpTransaction* transaction) { + return gHttpHandler->ActiveTabPriority() + ? transaction->TopLevelOuterContentWindowId() + : 0; +} + +// This function decides the transaction's order in the pending queue. +// Given two transactions t1 and t2, returning true means that t2 is +// more important than t1 and thus should be dispatched first. +static bool TransactionComparator(nsHttpTransaction* t1, + nsHttpTransaction* t2) { + bool t1Blocking = + t1->Caps() & (NS_HTTP_LOAD_AS_BLOCKING | NS_HTTP_LOAD_UNBLOCKED); + bool t2Blocking = + t2->Caps() & (NS_HTTP_LOAD_AS_BLOCKING | NS_HTTP_LOAD_UNBLOCKED); + + if (t1Blocking > t2Blocking) { + return false; + } + + if (t2Blocking > t1Blocking) { + return true; + } + + return t1->Priority() >= t2->Priority(); +} + +void PendingTransactionQueue::InsertTransactionNormal( + PendingTransactionInfo* info, + bool aInsertAsFirstForTheSamePriority /*= false*/) { + LOG( + ("PendingTransactionQueue::InsertTransactionNormal" + " trans=%p, windowId=%" PRIu64 "\n", + info->Transaction(), + info->Transaction()->TopLevelOuterContentWindowId())); + + uint64_t windowId = TabIdForQueuing(info->Transaction()); + nsTArray>* infoArray; + if (!mPendingTransactionTable.Get(windowId, &infoArray)) { + infoArray = new nsTArray>(); + mPendingTransactionTable.Put(windowId, infoArray); + } + + InsertTransactionSorted(*infoArray, info, aInsertAsFirstForTheSamePriority); +} + +void PendingTransactionQueue::InsertTransactionSorted( + nsTArray>& pendingQ, + PendingTransactionInfo* pendingTransInfo, + bool aInsertAsFirstForTheSamePriority /*= false*/) { + // insert the transaction into the front of the queue based on following + // rules: + // 1. The transaction has NS_HTTP_LOAD_AS_BLOCKING or NS_HTTP_LOAD_UNBLOCKED. + // 2. The transaction's priority is higher. + // + // search in reverse order under the assumption that many of the + // existing transactions will have the same priority (usually 0). + + nsHttpTransaction* trans = pendingTransInfo->Transaction(); + + for (int32_t i = pendingQ.Length() - 1; i >= 0; --i) { + nsHttpTransaction* t = pendingQ[i]->Transaction(); + if (TransactionComparator(trans, t)) { + if (ChaosMode::isActive(ChaosFeature::NetworkScheduling) || + aInsertAsFirstForTheSamePriority) { + int32_t samePriorityCount; + for (samePriorityCount = 0; i - samePriorityCount >= 0; + ++samePriorityCount) { + if (pendingQ[i - samePriorityCount]->Transaction()->Priority() != + trans->Priority()) { + break; + } + } + if (aInsertAsFirstForTheSamePriority) { + i -= samePriorityCount; + } else { + // skip over 0...all of the elements with the same priority. + i -= ChaosMode::randomUint32LessThan(samePriorityCount + 1); + } + } + pendingQ.InsertElementAt(i + 1, pendingTransInfo); + return; + } + } + pendingQ.InsertElementAt(0, pendingTransInfo); +} + +void PendingTransactionQueue::InsertTransaction( + PendingTransactionInfo* pendingTransInfo, + bool aInsertAsFirstForTheSamePriority /* = false */) { + if (pendingTransInfo->Transaction()->Caps() & NS_HTTP_URGENT_START) { + LOG( + (" adding transaction to pending queue " + "[trans=%p urgent-start-count=%zu]\n", + pendingTransInfo->Transaction(), mUrgentStartQ.Length() + 1)); + // put this transaction on the urgent-start queue... + InsertTransactionSorted(mUrgentStartQ, pendingTransInfo); + } else { + LOG( + (" adding transaction to pending queue " + "[trans=%p pending-count=%zu]\n", + pendingTransInfo->Transaction(), PendingQueueLength() + 1)); + // put this transaction on the pending queue... + InsertTransactionNormal(pendingTransInfo); + } +} + +nsTArray>* +PendingTransactionQueue::GetTransactionPendingQHelper( + nsAHttpTransaction* trans) { + nsTArray>* pendingQ = nullptr; + int32_t caps = trans->Caps(); + if (caps & NS_HTTP_URGENT_START) { + pendingQ = &(mUrgentStartQ); + } else { + pendingQ = mPendingTransactionTable.Get(TabIdForQueuing(trans)); + } + return pendingQ; +} + +void PendingTransactionQueue::AppendPendingUrgentStartQ( + nsTArray>& result) { + result.InsertElementsAt(0, mUrgentStartQ.Elements(), mUrgentStartQ.Length()); + mUrgentStartQ.Clear(); +} + +void PendingTransactionQueue::AppendPendingQForFocusedWindow( + uint64_t windowId, nsTArray>& result, + uint32_t maxCount) { + nsTArray>* infoArray = nullptr; + if (!mPendingTransactionTable.Get(windowId, &infoArray)) { + result.Clear(); + return; + } + + uint32_t countToAppend = maxCount; + countToAppend = countToAppend > infoArray->Length() || countToAppend == 0 + ? infoArray->Length() + : countToAppend; + + result.InsertElementsAt(result.Length(), infoArray->Elements(), + countToAppend); + infoArray->RemoveElementsAt(0, countToAppend); + + LOG( + ("PendingTransactionQueue::AppendPendingQForFocusedWindow, " + "pendingQ count=%zu window.count=%zu for focused window (id=%" PRIu64 + ")\n", + result.Length(), infoArray->Length(), windowId)); +} + +void PendingTransactionQueue::AppendPendingQForNonFocusedWindows( + uint64_t windowId, nsTArray>& result, + uint32_t maxCount) { + // XXX Adjust the order of transactions in a smarter manner. + uint32_t totalCount = 0; + for (auto it = mPendingTransactionTable.Iter(); !it.Done(); it.Next()) { + if (windowId && it.Key() == windowId) { + continue; + } + + uint32_t count = 0; + for (; count < it.UserData()->Length(); ++count) { + if (maxCount && totalCount == maxCount) { + break; + } + + // Because elements in |result| could come from multiple penndingQ, + // call |InsertTransactionSorted| to make sure the order is correct. + InsertTransactionSorted(result, it.UserData()->ElementAt(count)); + ++totalCount; + } + it.UserData()->RemoveElementsAt(0, count); + + if (maxCount && totalCount == maxCount) { + if (it.UserData()->Length()) { + // There are still some pending transactions for background + // tabs but we limit their dispatch. This is considered as + // an active tab optimization. + nsHttp::NotifyActiveTabLoadOptimization(); + } + break; + } + } +} + +void PendingTransactionQueue::ReschedTransaction(nsHttpTransaction* aTrans) { + nsTArray>* pendingQ = + GetTransactionPendingQHelper(aTrans); + + int32_t index = + pendingQ ? pendingQ->IndexOf(aTrans, 0, PendingComparator()) : -1; + if (index >= 0) { + RefPtr pendingTransInfo = (*pendingQ)[index]; + pendingQ->RemoveElementAt(index); + InsertTransactionSorted(*pendingQ, pendingTransInfo); + } +} + +void PendingTransactionQueue::RemoveEmptyPendingQ() { + for (auto it = mPendingTransactionTable.Iter(); !it.Done(); it.Next()) { + if (it.UserData()->IsEmpty()) { + it.Remove(); + } + } +} + +size_t PendingTransactionQueue::PendingQueueLength() const { + size_t length = 0; + for (auto it = mPendingTransactionTable.ConstIter(); !it.Done(); it.Next()) { + length += it.UserData()->Length(); + } + + return length; +} + +size_t PendingTransactionQueue::PendingQueueLengthForWindow( + uint64_t windowId) const { + auto* pendingQ = mPendingTransactionTable.Get(windowId); + return (pendingQ) ? pendingQ->Length() : 0; +} + +size_t PendingTransactionQueue::UrgentStartQueueLength() { + return mUrgentStartQ.Length(); +} + +void PendingTransactionQueue::PrintPendingQ() { + LOG(("urgent queue [")); + for (const auto& info : mUrgentStartQ) { + LOG((" %p", info->Transaction())); + } + for (auto it = mPendingTransactionTable.Iter(); !it.Done(); it.Next()) { + LOG(("] window id = %" PRIx64 " queue [", it.Key())); + for (const auto& info : *it.UserData()) { + LOG((" %p", info->Transaction())); + } + } + LOG(("]")); +} + +void PendingTransactionQueue::Compact() { + mUrgentStartQ.Compact(); + for (auto it = mPendingTransactionTable.Iter(); !it.Done(); it.Next()) { + it.UserData()->Compact(); + } +} + +void PendingTransactionQueue::CancelAllTransactions(nsresult reason) { + for (const auto& pendingTransInfo : mUrgentStartQ) { + LOG(("PendingTransactionQueue::CancelAllTransactions %p\n", + pendingTransInfo->Transaction())); + pendingTransInfo->Transaction()->Close(reason); + } + mUrgentStartQ.Clear(); + + for (auto it = mPendingTransactionTable.Iter(); !it.Done(); it.Next()) { + for (const auto& pendingTransInfo : *it.UserData()) { + LOG(("PendingTransactionQueue::CancelAllTransactions %p\n", + pendingTransInfo->Transaction())); + pendingTransInfo->Transaction()->Close(reason); + } + it.UserData()->Clear(); + } + + mPendingTransactionTable.Clear(); +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/PendingTransactionQueue.h b/netwerk/protocol/http/PendingTransactionQueue.h new file mode 100644 index 0000000000..0b0d51e2cd --- /dev/null +++ b/netwerk/protocol/http/PendingTransactionQueue.h @@ -0,0 +1,92 @@ +/* vim:t ts=4 sw=2 sts=2 et cin: */ +/* 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/. */ + +#ifndef PendingTransactionQueue_h__ +#define PendingTransactionQueue_h__ + +#include "nsClassHashtable.h" +#include "nsHttpTransaction.h" +#include "PendingTransactionInfo.h" + +namespace mozilla { +namespace net { + +class PendingTransactionQueue { + public: + PendingTransactionQueue() = default; + + void ReschedTransaction(nsHttpTransaction* aTrans); + + nsTArray>* GetTransactionPendingQHelper( + nsAHttpTransaction* trans); + + void InsertTransactionSorted( + nsTArray>& pendingQ, + PendingTransactionInfo* pendingTransInfo, + bool aInsertAsFirstForTheSamePriority = false); + + // Add a transaction information into the pending queue in + // |mPendingTransactionTable| according to the transaction's + // top level outer content window id. + void InsertTransaction(PendingTransactionInfo* pendingTransInfo, + bool aInsertAsFirstForTheSamePriority = false); + + void AppendPendingUrgentStartQ( + nsTArray>& result); + + // Append transactions to the |result| whose window id + // is equal to |windowId|. + // NOTE: maxCount == 0 will get all transactions in the queue. + void AppendPendingQForFocusedWindow( + uint64_t windowId, nsTArray>& result, + uint32_t maxCount = 0); + + // Append transactions whose window id isn't equal to |windowId|. + // NOTE: windowId == 0 will get all transactions for both + // focused and non-focused windows. + void AppendPendingQForNonFocusedWindows( + uint64_t windowId, nsTArray>& result, + uint32_t maxCount = 0); + + // Return the count of pending transactions for all window ids. + size_t PendingQueueLength() const; + size_t PendingQueueLengthForWindow(uint64_t windowId) const; + + // Remove the empty pendingQ in |mPendingTransactionTable|. + void RemoveEmptyPendingQ(); + + void PrintDiagnostics(nsCString& log); + + size_t UrgentStartQueueLength(); + + void PrintPendingQ(); + + void Compact(); + + void CancelAllTransactions(nsresult reason); + + ~PendingTransactionQueue() = default; + + private: + void InsertTransactionNormal(PendingTransactionInfo* info, + bool aInsertAsFirstForTheSamePriority = false); + + nsTArray> + mUrgentStartQ; // the urgent start transaction queue + + // This table provides a mapping from top level outer content window id + // to a queue of pending transaction information. + // The transaction's order in pending queue is decided by whether it's a + // blocking transaction and its priority. + // Note that the window id could be 0 if the http request + // is initialized without a window. + nsClassHashtable>> + mPendingTransactionTable; +}; + +} // namespace net +} // namespace mozilla + +#endif // !PendingTransactionQueue_h__ diff --git a/netwerk/protocol/http/QuicSocketControl.cpp b/netwerk/protocol/http/QuicSocketControl.cpp new file mode 100644 index 0000000000..a9a142d2b5 --- /dev/null +++ b/netwerk/protocol/http/QuicSocketControl.cpp @@ -0,0 +1,138 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * 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 "QuicSocketControl.h" + +#include "Http3Session.h" +#include "SharedCertVerifier.h" +#include "nsISocketProvider.h" +#include "nsIWebProgressListener.h" +#include "nsNSSComponent.h" +#include "nsWeakReference.h" +#include "nsSocketTransportService2.h" +#include "nsThreadUtils.h" +#include "sslt.h" +#include "ssl.h" + +namespace mozilla { +namespace net { + +NS_IMPL_ISUPPORTS_INHERITED(QuicSocketControl, TransportSecurityInfo, + nsISSLSocketControl, QuicSocketControl) + +QuicSocketControl::QuicSocketControl(uint32_t aProviderFlags) + : CommonSocketControl(aProviderFlags) {} + +void QuicSocketControl::SetCertVerificationResult(PRErrorCode errorCode) { + if (errorCode) { + mFailedVerification = true; + SetCanceled(errorCode); + } + + if (OnSocketThread()) { + CallAuthenticated(); + } else { + DebugOnly rv = gSocketTransportService->Dispatch( + NewRunnableMethod("QuicSocketControl::CallAuthenticated", this, + &QuicSocketControl::CallAuthenticated), + NS_DISPATCH_NORMAL); + } +} + +NS_IMETHODIMP +QuicSocketControl::GetSSLVersionOffered(int16_t* aSSLVersionOffered) { + *aSSLVersionOffered = nsISSLSocketControl::TLS_VERSION_1_3; + return NS_OK; +} + +void QuicSocketControl::CallAuthenticated() { + RefPtr http3Session = do_QueryReferent(mHttp3Session); + if (http3Session) { + http3Session->Authenticated(GetErrorCode()); + } + mHttp3Session = nullptr; +} + +void QuicSocketControl::SetAuthenticationCallback(Http3Session* aHttp3Session) { + mHttp3Session = do_GetWeakReference( + static_cast(aHttp3Session)); +} + +void QuicSocketControl::HandshakeCompleted() { + psm::RememberCertErrorsTable::GetInstance().LookupCertErrorBits(this); + + uint32_t state = nsIWebProgressListener::STATE_IS_SECURE; + + bool distrustImminent; + MutexAutoLock lock(mMutex); + nsresult rv = + IsCertificateDistrustImminent(mSucceededCertChain, distrustImminent); + + if (NS_SUCCEEDED(rv) && distrustImminent) { + state |= nsIWebProgressListener::STATE_CERT_DISTRUST_IMMINENT; + } + + // If we're here, the TLS handshake has succeeded. Thus if any of these + // booleans are true, the user has added an override for a certificate error. + if (mIsDomainMismatch || mIsUntrusted || mIsNotValidAtThisTime) { + state |= nsIWebProgressListener::STATE_CERT_USER_OVERRIDDEN; + } + + SetSecurityState(state); + mHandshakeCompleted = true; +} + +void QuicSocketControl::SetNegotiatedNPN(const nsACString& aValue) { + MutexAutoLock lock(mMutex); + mNegotiatedNPN = aValue; + mNPNCompleted = true; +} + +void QuicSocketControl::SetInfo(uint16_t aCipherSuite, + uint16_t aProtocolVersion, uint16_t aKeaGroup, + uint16_t aSignatureScheme) { + SSLCipherSuiteInfo cipherInfo; + if (SSL_GetCipherSuiteInfo(aCipherSuite, &cipherInfo, sizeof cipherInfo) == + SECSuccess) { + MutexAutoLock lock(mMutex); + mHaveCipherSuiteAndProtocol = true; + mCipherSuite = aCipherSuite; + mProtocolVersion = aProtocolVersion & 0xFF; + mKeaGroup = getKeaGroupName(aKeaGroup); + mSignatureSchemeName = getSignatureName(aSignatureScheme); + } +} + +NS_IMETHODIMP QuicSocketControl::GetPeerId(nsACString& aResult) { + if (!mPeerId.IsEmpty()) { + aResult.Assign(mPeerId); + return NS_OK; + } + + if (mProviderFlags & + nsISocketProvider::ANONYMOUS_CONNECT) { // See bug 466080 + mPeerId.AppendLiteral("anon:"); + } + if (mProviderFlags & nsISocketProvider::NO_PERMANENT_STORAGE) { + mPeerId.AppendLiteral("private:"); + } + if (mProviderFlags & nsISocketProvider::BE_CONSERVATIVE) { + mPeerId.AppendLiteral("beConservative:"); + } + + mPeerId.Append(GetHostName()); + mPeerId.Append(':'); + mPeerId.AppendInt(GetPort()); + nsAutoCString suffix; + GetOriginAttributes().CreateSuffix(suffix); + mPeerId.Append(suffix); + + aResult.Assign(mPeerId); + return NS_OK; +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/QuicSocketControl.h b/netwerk/protocol/http/QuicSocketControl.h new file mode 100644 index 0000000000..b1484eae01 --- /dev/null +++ b/netwerk/protocol/http/QuicSocketControl.h @@ -0,0 +1,62 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * 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/. */ + +#ifndef QuicSocketControl_h +#define QuicSocketControl_h + +#include "CommonSocketControl.h" +#include "nsIWeakReferenceUtils.h" + +namespace mozilla { +namespace net { + +class Http3Session; + +// IID for the QuicSocketControl interface +#define NS_QUICSOCKETCONTROL_IID \ + { \ + 0xdbc67fd0, 0x1ac6, 0x457b, { \ + 0x91, 0x4e, 0x4c, 0x86, 0x60, 0xff, 0x00, 0x69 \ + } \ + } + +class QuicSocketControl final : public CommonSocketControl { + public: + NS_DECLARE_STATIC_IID_ACCESSOR(NS_QUICSOCKETCONTROL_IID) + + NS_DECL_ISUPPORTS_INHERITED + + NS_IMETHOD GetSSLVersionOffered(int16_t* aSSLVersionOffered) override; + + explicit QuicSocketControl(uint32_t providerFlags); + + void SetNegotiatedNPN(const nsACString& aValue); + void SetInfo(uint16_t aCipherSuite, uint16_t aProtocolVersion, + uint16_t aKeaGroup, uint16_t aSignatureScheme); + + void SetAuthenticationCallback(Http3Session* aHttp3Session); + void CallAuthenticated(); + + void HandshakeCompleted(); + void SetCertVerificationResult(PRErrorCode errorCode) override; + + NS_IMETHOD GetPeerId(nsACString& aResult) override; + + private: + ~QuicSocketControl() = default; + + // For Authentication done callback. + nsWeakPtr mHttp3Session; + + nsCString mPeerId; +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(QuicSocketControl, NS_QUICSOCKETCONTROL_IID) + +} // namespace net +} // namespace mozilla + +#endif // QuicSocketControl_h diff --git a/netwerk/protocol/http/README b/netwerk/protocol/http/README new file mode 100644 index 0000000000..621e9e950c --- /dev/null +++ b/netwerk/protocol/http/README @@ -0,0 +1,119 @@ + Darin Fisher + darin@netscape.com + 8/8/2001 + + HTTP DESIGN NOTES + + +CLASS BREAKDOWN + + nsHttpHandler + - implements nsIProtocolHandler + - manages preferences + - owns the authentication cache + - holds references to frequently used services + + nsHttpChannel + - implements nsIHttpChannel + - talks to the cache + - initiates http transactions + - processes http response codes + - intercepts progress notifications + + nsHttpConnection + - implements nsIStreamListener & nsIStreamProvider + - talks to the socket transport service + - feeds data to its transaction object + - routes progress notifications + + nsHttpConnectionInfo + - identifies a connection + + nsHttpTransaction + - implements nsIRequest + - encapsulates a http request and response + - parses incoming data + + nsHttpChunkedDecoder + - owned by a transaction + - removes chunked decoding + + nsHttpRequestHead + - owns a nsHttpHeaderArray + - knows how to fill a request buffer + + nsHttpResponseHead + - owns a nsHttpHeaderArray + - knows how to parse response lines + - performs common header manipulations/calculations + + nsHttpHeaderArray + - stores http "
:" pairs + + nsHttpAuthCache + - stores authentication credentials for http auth domains + + nsHttpBasicAuth + - implements nsIHttpAuthenticator + - generates BASIC auth credentials from user:pass + + +ATOMS + + nsHttp:: (header namespace) + + eg. nsHttp::Content_Length + + +TRANSACTION MODEL + + InitiateTransaction -> ActivateConnection -> AsyncWrite, AsyncRead + + The channel creates transactions, and passes them to the handler via + InitiateTransaction along with a nsHttpConnectionInfo object + identifying the requested connection. The handler either dispatches + the transaction immediately or queues it up to be dispatched later, + depending on whether or not the limit on the number of connections + to the requested server has been reached. Once the transaction can + be run, the handler looks for an idle connection or creates a new + connection, and then (re)activates the connection, assigning it the + new transaction. + + Once activated the connection ensures that it has a socket transport, + and then calls AsyncWrite and AsyncRead on the socket transport. This + begins the process of talking to the server. To minimize buffering, + socket transport thread-proxying is completely disabled (using the flags + DONT_PROXY_LISTENER | DONT_PROXY_PROVIDER | DONT_PROXY_OBSERVER with + both AsyncWrite and AsyncRead). This means that the nsHttpConnection's + OnStartRequest, OnDataAvailable, OnDataWritable, and OnStopRequest + methods will execute on the socket transport thread. + + The transaction defines (non-virtual) OnDataReadable, OnDataWritable, and + OnStopTransaction methods, which the connection calls in response to + its OnDataAvailable, OnDataWritable, and OnStopRequest methods, respectively. + The transaction owns a nsStreamListenerProxy created by the channel, which + it uses to transfer data from the socket thread over to the client's thread. + To mimize buffering, the transaction implements nsIInputStream, and passes + itself to the stream listener proxy's OnDataAvailable. In this way, we + have effectively wedged the response parsing between the socket and the + thread proxy's buffer. When read, the transaction turns around and reads + from the socket using the buffer passed to it. The transaction scans the + buffer for headers, removes them as they are detected, and copies the headers + into its nsHttpResponseHead object. The rest of the data remains in the + buffer, and is proxied over to the client's thread to be handled first by the + http channel and eventually by the client. + + There are several other major design factors, including: + + - transaction cancelation + - progress notification + - SSL tunneling + - chunked decoding + - thread safety + - premature EOF detection and transaction restarting + - pipelining (not yet implemented) + + +CACHING + + diff --git a/netwerk/protocol/http/SpeculativeTransaction.cpp b/netwerk/protocol/http/SpeculativeTransaction.cpp new file mode 100644 index 0000000000..de7fbc6d30 --- /dev/null +++ b/netwerk/protocol/http/SpeculativeTransaction.cpp @@ -0,0 +1,84 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +// HttpLog.h should generally be included first +#include "HttpLog.h" + +#include "SpeculativeTransaction.h" +#include "HTTPSRecordResolver.h" +#include "nsICachingChannel.h" +#include "nsHttpHandler.h" + +namespace mozilla { +namespace net { + +SpeculativeTransaction::SpeculativeTransaction( + nsHttpConnectionInfo* aConnInfo, nsIInterfaceRequestor* aCallbacks, + uint32_t aCaps, std::function&& aCallback) + : NullHttpTransaction(aConnInfo, aCallbacks, aCaps), + mTriedToWrite(false), + mCloseCallback(std::move(aCallback)) {} + +SpeculativeTransaction::~SpeculativeTransaction() {} + +already_AddRefed +SpeculativeTransaction::CreateWithNewConnInfo(nsHttpConnectionInfo* aConnInfo) { + RefPtr trans = + new SpeculativeTransaction(aConnInfo, mCallbacks, mCaps); + trans->mParallelSpeculativeConnectLimit = mParallelSpeculativeConnectLimit; + trans->mIgnoreIdle = mIgnoreIdle; + trans->mIsFromPredictor = mIsFromPredictor; + trans->mAllow1918 = mAllow1918; + return trans.forget(); +} + +nsresult SpeculativeTransaction::FetchHTTPSRR() { + LOG(("SpeculativeTransaction::FetchHTTPSRR [this=%p]", this)); + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + RefPtr resolver = new HTTPSRecordResolver(this); + nsCOMPtr dnsRequest; + return resolver->FetchHTTPSRRInternal(GetCurrentEventTarget(), + getter_AddRefs(dnsRequest)); +} + +nsresult SpeculativeTransaction::OnHTTPSRRAvailable( + nsIDNSHTTPSSVCRecord* aHTTPSSVCRecord, + nsISVCBRecord* aHighestPriorityRecord) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG(("SpeculativeTransaction::OnHTTPSRRAvailable [this=%p]", this)); + + if (!aHTTPSSVCRecord || !aHighestPriorityRecord) { + return NS_OK; + } + + RefPtr connInfo = ConnectionInfo(); + RefPtr newInfo = + connInfo->CloneAndAdoptHTTPSSVCRecord(aHighestPriorityRecord); + RefPtr newTrans = CreateWithNewConnInfo(newInfo); + gHttpHandler->ConnMgr()->DoSpeculativeConnection(newTrans, false); + return NS_OK; +} + +nsresult SpeculativeTransaction::ReadSegments(nsAHttpSegmentReader* aReader, + uint32_t aCount, + uint32_t* aCountRead) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + mTriedToWrite = true; + return NullHttpTransaction::ReadSegments(aReader, aCount, aCountRead); +} + +void SpeculativeTransaction::Close(nsresult aReason) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + NullHttpTransaction::Close(aReason); + + if (mCloseCallback) { + mCloseCallback(mTriedToWrite && aReason == NS_BASE_STREAM_CLOSED); + mCloseCallback = nullptr; + } +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/SpeculativeTransaction.h b/netwerk/protocol/http/SpeculativeTransaction.h new file mode 100644 index 0000000000..98b6143eab --- /dev/null +++ b/netwerk/protocol/http/SpeculativeTransaction.h @@ -0,0 +1,69 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* vim:set ts=4 sw=4 sts=4 et cin: */ +/* 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/. */ + +#ifndef SpeculativeTransaction_h__ +#define SpeculativeTransaction_h__ + +#include "mozilla/Maybe.h" +#include "NullHttpTransaction.h" + +namespace mozilla { +namespace net { + +class HTTPSRecordResolver; + +class SpeculativeTransaction : public NullHttpTransaction { + public: + SpeculativeTransaction(nsHttpConnectionInfo* aConnInfo, + nsIInterfaceRequestor* aCallbacks, uint32_t aCaps, + std::function&& aCallback = nullptr); + + already_AddRefed CreateWithNewConnInfo( + nsHttpConnectionInfo* aConnInfo); + + virtual nsresult FetchHTTPSRR() override; + + virtual nsresult OnHTTPSRRAvailable( + nsIDNSHTTPSSVCRecord* aHTTPSSVCRecord, + nsISVCBRecord* aHighestPriorityRecord) override; + + void SetParallelSpeculativeConnectLimit(uint32_t aLimit) { + mParallelSpeculativeConnectLimit.emplace(aLimit); + } + void SetIgnoreIdle(bool aIgnoreIdle) { mIgnoreIdle.emplace(aIgnoreIdle); } + void SetIsFromPredictor(bool aIsFromPredictor) { + mIsFromPredictor.emplace(aIsFromPredictor); + } + void SetAllow1918(bool aAllow1918) { mAllow1918.emplace(aAllow1918); } + + const Maybe& ParallelSpeculativeConnectLimit() { + return mParallelSpeculativeConnectLimit; + } + const Maybe& IgnoreIdle() { return mIgnoreIdle; } + const Maybe& IsFromPredictor() { return mIsFromPredictor; } + const Maybe& Allow1918() { return mAllow1918; } + + void Close(nsresult aReason) override; + nsresult ReadSegments(nsAHttpSegmentReader* aReader, uint32_t aCount, + uint32_t* aCountRead) override; + + protected: + virtual ~SpeculativeTransaction(); + + private: + Maybe mParallelSpeculativeConnectLimit; + Maybe mIgnoreIdle; + Maybe mIsFromPredictor; + Maybe mAllow1918; + + bool mTriedToWrite = false; + std::function mCloseCallback; +}; + +} // namespace net +} // namespace mozilla + +#endif // SpeculativeTransaction_h__ diff --git a/netwerk/protocol/http/TRRServiceChannel.cpp b/netwerk/protocol/http/TRRServiceChannel.cpp new file mode 100644 index 0000000000..cecc632724 --- /dev/null +++ b/netwerk/protocol/http/TRRServiceChannel.cpp @@ -0,0 +1,1539 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et 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 "TRRServiceChannel.h" + +#include "HttpLog.h" +#include "AltServiceChild.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/StaticPrefs_network.h" +#include "mozilla/Unused.h" +#include "nsDNSPrefetch.h" +#include "nsEscape.h" +#include "nsHttpTransaction.h" +#include "nsICancelable.h" +#include "nsICachingChannel.h" +#include "nsIHttpPushListener.h" +#include "nsIProtocolProxyService2.h" +#include "nsIOService.h" +#include "nsISeekableStream.h" +#include "nsURLHelper.h" +#include "ProxyConfigLookup.h" +#include "TRRLoadInfo.h" +#include "ReferrerInfo.h" +#include "TRR.h" + +namespace mozilla { +namespace net { + +NS_IMPL_ADDREF(TRRServiceChannel) + +// Because nsSupportsWeakReference isn't thread-safe we must ensure that +// TRRServiceChannel is destroyed on the target thread. Any Release() called +// on a different thread is dispatched to the target thread. +bool TRRServiceChannel::DispatchRelease() { + if (mCurrentEventTarget->IsOnCurrentThread()) { + return false; + } + + mCurrentEventTarget->Dispatch( + NewNonOwningRunnableMethod("net::TRRServiceChannel::Release", this, + &TRRServiceChannel::Release), + NS_DISPATCH_NORMAL); + + return true; +} + +NS_IMETHODIMP_(MozExternalRefCountType) +TRRServiceChannel::Release() { + nsrefcnt count = mRefCnt - 1; + if (DispatchRelease()) { + // Redispatched to the target thread. + return count; + } + + MOZ_ASSERT(0 != mRefCnt, "dup release"); + count = --mRefCnt; + NS_LOG_RELEASE(this, count, "TRRServiceChannel"); + + if (0 == count) { + mRefCnt = 1; + delete (this); + return 0; + } + + return count; +} + +NS_INTERFACE_MAP_BEGIN(TRRServiceChannel) + NS_INTERFACE_MAP_ENTRY(nsIRequest) + NS_INTERFACE_MAP_ENTRY(nsIChannel) + NS_INTERFACE_MAP_ENTRY(nsIHttpChannel) + NS_INTERFACE_MAP_ENTRY(nsIHttpChannelInternal) + NS_INTERFACE_MAP_ENTRY(nsISupportsPriority) + NS_INTERFACE_MAP_ENTRY(nsIClassOfService) + NS_INTERFACE_MAP_ENTRY(nsIProxiedChannel) + NS_INTERFACE_MAP_ENTRY(nsIProtocolProxyCallback) + NS_INTERFACE_MAP_ENTRY(nsIStreamListener) + NS_INTERFACE_MAP_ENTRY(nsIRequestObserver) + NS_INTERFACE_MAP_ENTRY(nsITransportEventSink) + NS_INTERFACE_MAP_ENTRY(nsIDNSListener) + NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference) + NS_INTERFACE_MAP_ENTRY_CONCRETE(TRRServiceChannel) +NS_INTERFACE_MAP_END_INHERITING(HttpBaseChannel) + +TRRServiceChannel::TRRServiceChannel() + : HttpAsyncAborter(this), + mTopWindowOriginComputed(false), + mPushedStreamId(0), + mProxyRequest(nullptr, "TRRServiceChannel::mProxyRequest"), + mCurrentEventTarget(GetCurrentEventTarget()) { + LOG(("TRRServiceChannel ctor [this=%p]\n", this)); +} + +TRRServiceChannel::~TRRServiceChannel() { + LOG(("TRRServiceChannel dtor [this=%p]\n", this)); +} + +NS_IMETHODIMP +TRRServiceChannel::Cancel(nsresult status) { + LOG(("TRRServiceChannel::Cancel [this=%p status=%" PRIx32 "]\n", this, + static_cast(status))); + if (mCanceled) { + LOG((" ignoring; already canceled\n")); + return NS_OK; + } + + mCanceled = true; + mStatus = status; + + nsCOMPtr proxyRequest; + { + auto req = mProxyRequest.Lock(); + proxyRequest.swap(*req); + } + + if (proxyRequest) { + NS_DispatchToMainThread( + NS_NewRunnableFunction( + "CancelProxyRequest", + [proxyRequest, status]() { proxyRequest->Cancel(status); }), + NS_DISPATCH_NORMAL); + } + + CancelNetworkRequest(status); + return NS_OK; +} + +void TRRServiceChannel::CancelNetworkRequest(nsresult aStatus) { + if (mTransaction) { + nsresult rv = gHttpHandler->CancelTransaction(mTransaction, aStatus); + if (NS_FAILED(rv)) { + LOG(("failed to cancel the transaction\n")); + } + } + if (mTransactionPump) mTransactionPump->Cancel(aStatus); +} + +NS_IMETHODIMP +TRRServiceChannel::Suspend() { + LOG(("TRRServiceChannel::SuspendInternal [this=%p]\n", this)); + + if (mTransactionPump) { + return mTransactionPump->Suspend(); + } + + return NS_OK; +} + +NS_IMETHODIMP +TRRServiceChannel::Resume() { + LOG(("TRRServiceChannel::Resume [this=%p]\n", this)); + + if (mTransactionPump) { + return mTransactionPump->Resume(); + } + + return NS_OK; +} + +NS_IMETHODIMP +TRRServiceChannel::GetSecurityInfo(nsISupports** securityInfo) { + NS_ENSURE_ARG_POINTER(securityInfo); + *securityInfo = mSecurityInfo; + NS_IF_ADDREF(*securityInfo); + return NS_OK; +} + +NS_IMETHODIMP +TRRServiceChannel::AsyncOpen(nsIStreamListener* aListener) { + NS_ENSURE_ARG_POINTER(aListener); + NS_ENSURE_TRUE(!LoadIsPending(), NS_ERROR_IN_PROGRESS); + NS_ENSURE_TRUE(!LoadWasOpened(), NS_ERROR_ALREADY_OPENED); + + if (mCanceled) { + ReleaseListeners(); + return mStatus; + } + + // HttpBaseChannel::MaybeWaitForUploadStreamLength can only be used on main + // thread, so we can only return an error here. +#ifdef NIGHTLY_BUILD + MOZ_ASSERT(!LoadPendingInputStreamLengthOperation()); +#endif + if (LoadPendingInputStreamLengthOperation()) { + return NS_ERROR_FAILURE; + } + + if (!gHttpHandler->Active()) { + LOG((" after HTTP shutdown...")); + ReleaseListeners(); + return NS_ERROR_NOT_AVAILABLE; + } + + nsAutoCString scheme; + mURI->GetScheme(scheme); + if (!scheme.LowerCaseEqualsLiteral("https")) { + return NS_ERROR_FAILURE; + } + + nsresult rv = NS_CheckPortSafety(mURI); + if (NS_FAILED(rv)) { + ReleaseListeners(); + return rv; + } + + StoreIsPending(true); + StoreWasOpened(true); + + mListener = aListener; + + mAsyncOpenTime = TimeStamp::Now(); + + rv = MaybeResolveProxyAndBeginConnect(); + if (NS_FAILED(rv)) { + Unused << AsyncAbort(rv); + } + + return NS_OK; +} + +nsresult TRRServiceChannel::MaybeResolveProxyAndBeginConnect() { + nsresult rv; + + // The common case for HTTP channels is to begin proxy resolution and return + // at this point. The only time we know mProxyInfo already is if we're + // proxying a non-http protocol like ftp. We don't need to discover proxy + // settings if we are never going to make a network connection. + if (!mProxyInfo && + !(mLoadFlags & (nsICachingChannel::LOAD_ONLY_FROM_CACHE | + nsICachingChannel::LOAD_NO_NETWORK_IO)) && + NS_SUCCEEDED(ResolveProxy())) { + return NS_OK; + } + + rv = BeginConnect(); + if (NS_FAILED(rv)) { + Unused << AsyncAbort(rv); + } + + return NS_OK; +} + +nsresult TRRServiceChannel::ResolveProxy() { + LOG(("TRRServiceChannel::ResolveProxy [this=%p]\n", this)); + if (!NS_IsMainThread()) { + return NS_DispatchToMainThread( + NewRunnableMethod("TRRServiceChannel::ResolveProxy", this, + &TRRServiceChannel::ResolveProxy), + NS_DISPATCH_NORMAL); + } + + MOZ_ASSERT(NS_IsMainThread()); + + // TODO: bug 1625171. Consider moving proxy resolution to socket process. + RefPtr self = this; + nsCOMPtr proxyRequest; + nsresult rv = ProxyConfigLookup::Create( + [self](nsIProxyInfo* aProxyInfo, nsresult aStatus) { + self->OnProxyAvailable(nullptr, nullptr, aProxyInfo, aStatus); + }, + mURI, mProxyResolveFlags, getter_AddRefs(proxyRequest)); + + if (NS_FAILED(rv)) { + if (!mCurrentEventTarget->IsOnCurrentThread()) { + return mCurrentEventTarget->Dispatch( + NewRunnableMethod("TRRServiceChannel::AsyncAbort", this, + &TRRServiceChannel::AsyncAbort, rv), + NS_DISPATCH_NORMAL); + } + } + + { + auto req = mProxyRequest.Lock(); + // We only set mProxyRequest if the channel hasn't already been cancelled + // on another thread. + if (!mCanceled) { + *req = proxyRequest.forget(); + } + } + + // If the channel has been cancelled, we go ahead and cancel the proxy + // request right here. + if (proxyRequest) { + proxyRequest->Cancel(mStatus); + } + + return rv; +} + +NS_IMETHODIMP +TRRServiceChannel::OnProxyAvailable(nsICancelable* request, nsIChannel* channel, + nsIProxyInfo* pi, nsresult status) { + LOG(("TRRServiceChannel::OnProxyAvailable [this=%p pi=%p status=%" PRIx32 + " mStatus=%" PRIx32 "]\n", + this, pi, static_cast(status), + static_cast(static_cast(mStatus)))); + + if (!mCurrentEventTarget->IsOnCurrentThread()) { + RefPtr self = this; + nsCOMPtr info = pi; + return mCurrentEventTarget->Dispatch( + NS_NewRunnableFunction("TRRServiceChannel::OnProxyAvailable", + [self, info, status]() { + self->OnProxyAvailable(nullptr, nullptr, info, + status); + }), + NS_DISPATCH_NORMAL); + } + + MOZ_ASSERT(mCurrentEventTarget->IsOnCurrentThread()); + + { + auto proxyRequest = mProxyRequest.Lock(); + *proxyRequest = nullptr; + } + + nsresult rv; + + // If status is a failure code, then it means that we failed to resolve + // proxy info. That is a non-fatal error assuming it wasn't because the + // request was canceled. We just failover to DIRECT when proxy resolution + // fails (failure can mean that the PAC URL could not be loaded). + + if (NS_SUCCEEDED(status)) mProxyInfo = pi; + + if (!gHttpHandler->Active()) { + LOG( + ("nsHttpChannel::OnProxyAvailable [this=%p] " + "Handler no longer active.\n", + this)); + rv = NS_ERROR_NOT_AVAILABLE; + } else { + rv = BeginConnect(); + } + + if (NS_FAILED(rv)) { + Unused << AsyncAbort(rv); + } + return rv; +} + +const nsCString& TRRServiceChannel::GetTopWindowOrigin() { + if (mTopWindowOriginComputed) { + return mTopWindowOrigin; + } + + nsresult rv = nsContentUtils::GetASCIIOrigin(mURI, mTopWindowOrigin); + NS_ENSURE_SUCCESS(rv, mTopWindowOrigin); + + mTopWindowOriginComputed = true; + return mTopWindowOrigin; +} + +nsresult TRRServiceChannel::BeginConnect() { + LOG(("TRRServiceChannel::BeginConnect [this=%p]\n", this)); + nsresult rv; + + // Construct connection info object + nsAutoCString host; + nsAutoCString scheme; + int32_t port = -1; + bool isHttps = mURI->SchemeIs("https"); + + rv = mURI->GetScheme(scheme); + if (NS_SUCCEEDED(rv)) rv = mURI->GetAsciiHost(host); + if (NS_SUCCEEDED(rv)) rv = mURI->GetPort(&port); + if (NS_SUCCEEDED(rv)) rv = mURI->GetAsciiSpec(mSpec); + if (NS_FAILED(rv)) { + return rv; + } + + // Just a warning here because some nsIURIs do not implement this method. + Unused << NS_WARN_IF(NS_FAILED(mURI->GetUsername(mUsername))); + + // Reject the URL if it doesn't specify a host + if (host.IsEmpty()) { + rv = NS_ERROR_MALFORMED_URI; + return rv; + } + LOG(("host=%s port=%d\n", host.get(), port)); + LOG(("uri=%s\n", mSpec.get())); + + nsCOMPtr proxyInfo; + if (mProxyInfo) proxyInfo = do_QueryInterface(mProxyInfo); + + mRequestHead.SetHTTPS(isHttps); + mRequestHead.SetOrigin(scheme, host, port); + + RefPtr connInfo = new nsHttpConnectionInfo( + host, port, ""_ns, mUsername, GetTopWindowOrigin(), proxyInfo, + OriginAttributes(), isHttps); + // TODO: Bug 1622778 for using AltService in socket process. + StoreAllowAltSvc(XRE_IsParentProcess() && LoadAllowAltSvc()); + bool http2Allowed = !gHttpHandler->IsHttp2Excluded(connInfo); + bool http3Allowed = !mUpgradeProtocolCallback && !mProxyInfo && + !(mCaps & NS_HTTP_BE_CONSERVATIVE) && + !LoadBeConservative(); + + RefPtr mapping; + if (!mConnectionInfo && LoadAllowAltSvc() && // per channel + (http2Allowed || http3Allowed) && !(mLoadFlags & LOAD_FRESH_CONNECTION) && + AltSvcMapping::AcceptableProxy(proxyInfo) && + (scheme.EqualsLiteral("http") || scheme.EqualsLiteral("https")) && + (mapping = gHttpHandler->GetAltServiceMapping( + scheme, host, port, mPrivateBrowsing, IsIsolated(), + GetTopWindowOrigin(), OriginAttributes(), http2Allowed, + http3Allowed))) { + LOG(("TRRServiceChannel %p Alt Service Mapping Found %s://%s:%d [%s]\n", + this, scheme.get(), mapping->AlternateHost().get(), + mapping->AlternatePort(), mapping->HashKey().get())); + + if (!(mLoadFlags & LOAD_ANONYMOUS) && !mPrivateBrowsing) { + nsAutoCString altUsedLine(mapping->AlternateHost()); + bool defaultPort = + mapping->AlternatePort() == + (isHttps ? NS_HTTPS_DEFAULT_PORT : NS_HTTP_DEFAULT_PORT); + if (!defaultPort) { + altUsedLine.AppendLiteral(":"); + altUsedLine.AppendInt(mapping->AlternatePort()); + } + rv = mRequestHead.SetHeader(nsHttp::Alternate_Service_Used, altUsedLine); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + + LOG(("TRRServiceChannel %p Using connection info from altsvc mapping", + this)); + mapping->GetConnectionInfo(getter_AddRefs(mConnectionInfo), proxyInfo, + OriginAttributes()); + Telemetry::Accumulate(Telemetry::HTTP_TRANSACTION_USE_ALTSVC, true); + Telemetry::Accumulate(Telemetry::HTTP_TRANSACTION_USE_ALTSVC_OE, !isHttps); + } else if (mConnectionInfo) { + LOG(("TRRServiceChannel %p Using channel supplied connection info", this)); + Telemetry::Accumulate(Telemetry::HTTP_TRANSACTION_USE_ALTSVC, false); + } else { + LOG(("TRRServiceChannel %p Using default connection info", this)); + + mConnectionInfo = connInfo; + Telemetry::Accumulate(Telemetry::HTTP_TRANSACTION_USE_ALTSVC, false); + } + + // Need to re-ask the handler, since mConnectionInfo may not be the connInfo + // we used earlier + if (gHttpHandler->IsHttp2Excluded(mConnectionInfo)) { + StoreAllowSpdy(0); + mCaps |= NS_HTTP_DISALLOW_SPDY; + mConnectionInfo->SetNoSpdy(true); + } + + // If TimingEnabled flag is not set after OnModifyRequest() then + // clear the already recorded AsyncOpen value for consistency. + if (!LoadTimingEnabled()) mAsyncOpenTime = TimeStamp(); + + // if this somehow fails we can go on without it + Unused << gHttpHandler->AddConnectionHeader(&mRequestHead, mCaps); + + // Adjust mCaps according to our request headers: + // - If "Connection: close" is set as a request header, then do not bother + // trying to establish a keep-alive connection. + if (mRequestHead.HasHeaderValue(nsHttp::Connection, "close")) + mCaps &= ~(NS_HTTP_ALLOW_KEEPALIVE); + + if (gHttpHandler->CriticalRequestPrioritization()) { + if (mClassOfService & nsIClassOfService::Leader) { + mCaps |= NS_HTTP_LOAD_AS_BLOCKING; + } + if (mClassOfService & nsIClassOfService::Unblocked) { + mCaps |= NS_HTTP_LOAD_UNBLOCKED; + } + if (mClassOfService & nsIClassOfService::UrgentStart && + gHttpHandler->IsUrgentStartEnabled()) { + mCaps |= NS_HTTP_URGENT_START; + SetPriority(nsISupportsPriority::PRIORITY_HIGHEST); + } + } + + // Force-Reload should reset the persistent connection pool for this host + if (mLoadFlags & LOAD_FRESH_CONNECTION) { + // just the initial document resets the whole pool + if (mLoadFlags & LOAD_INITIAL_DOCUMENT_URI) { + gHttpHandler->AltServiceCache()->ClearAltServiceMappings(); + rv = gHttpHandler->ConnMgr()->DoShiftReloadConnectionCleanupWithConnInfo( + mConnectionInfo); + if (NS_FAILED(rv)) { + LOG(( + "TRRServiceChannel::BeginConnect " + "DoShiftReloadConnectionCleanupWithConnInfo failed: %08x [this=%p]", + static_cast(rv), this)); + } + } + } + + if (mCanceled) { + return mStatus; + } + + MaybeStartDNSPrefetch(); + + rv = ContinueOnBeforeConnect(); + if (NS_FAILED(rv)) { + return rv; + } + + return NS_OK; +} + +nsresult TRRServiceChannel::ContinueOnBeforeConnect() { + LOG(("TRRServiceChannel::ContinueOnBeforeConnect [this=%p]\n", this)); + + // ensure that we are using a valid hostname + if (!net_IsValidHostName(nsDependentCString(mConnectionInfo->Origin()))) + return NS_ERROR_UNKNOWN_HOST; + + if (LoadIsTRRServiceChannel()) { + mCaps |= NS_HTTP_LARGE_KEEPALIVE; + } + + mCaps |= NS_HTTP_TRR_FLAGS_FROM_MODE(nsIRequest::GetTRRMode()); + + // Finalize ConnectionInfo flags before SpeculativeConnect + mConnectionInfo->SetAnonymous((mLoadFlags & LOAD_ANONYMOUS) != 0); + mConnectionInfo->SetPrivate(mPrivateBrowsing); + mConnectionInfo->SetIsolated(IsIsolated()); + mConnectionInfo->SetNoSpdy(mCaps & NS_HTTP_DISALLOW_SPDY); + mConnectionInfo->SetBeConservative((mCaps & NS_HTTP_BE_CONSERVATIVE) || + LoadBeConservative()); + mConnectionInfo->SetTlsFlags(mTlsFlags); + mConnectionInfo->SetIsTrrServiceChannel(LoadIsTRRServiceChannel()); + mConnectionInfo->SetTRRMode(nsIRequest::GetTRRMode()); + mConnectionInfo->SetIPv4Disabled(mCaps & NS_HTTP_DISABLE_IPV4); + mConnectionInfo->SetIPv6Disabled(mCaps & NS_HTTP_DISABLE_IPV6); + + return Connect(); +} + +nsresult TRRServiceChannel::Connect() { + LOG(("TRRServiceChannel::Connect [this=%p]\n", this)); + + nsresult rv = SetupTransaction(); + if (NS_FAILED(rv)) { + return rv; + } + + rv = gHttpHandler->InitiateTransaction(mTransaction, mPriority); + if (NS_FAILED(rv)) { + return rv; + } + + return mTransaction->AsyncRead(this, getter_AddRefs(mTransactionPump)); +} + +nsresult TRRServiceChannel::SetupTransaction() { + LOG(("TRRServiceChannel::SetupTransaction [this=%p, cos=%u, prio=%d]\n", this, + mClassOfService, mPriority)); + + NS_ENSURE_TRUE(!mTransaction, NS_ERROR_ALREADY_INITIALIZED); + + nsresult rv; + + if (!LoadAllowSpdy()) { + mCaps |= NS_HTTP_DISALLOW_SPDY; + } + if (!LoadAllowHttp3()) { + mCaps |= NS_HTTP_DISALLOW_HTTP3; + } + if (LoadBeConservative()) { + mCaps |= NS_HTTP_BE_CONSERVATIVE; + } + + // Use the URI path if not proxying (transparent proxying such as proxy + // CONNECT does not count here). Also figure out what HTTP version to use. + nsAutoCString buf, path; + nsCString* requestURI; + + // This is the normal e2e H1 path syntax "/index.html" + rv = mURI->GetPathQueryRef(path); + if (NS_FAILED(rv)) { + return rv; + } + + // path may contain UTF-8 characters, so ensure that they're escaped. + if (NS_EscapeURL(path.get(), path.Length(), esc_OnlyNonASCII | esc_Spaces, + buf)) { + requestURI = &buf; + } else { + requestURI = &path; + } + + // trim off the #ref portion if any... + int32_t ref1 = requestURI->FindChar('#'); + if (ref1 != kNotFound) { + requestURI->SetLength(ref1); + } + + if (mConnectionInfo->UsingConnect() || !mConnectionInfo->UsingHttpProxy()) { + mRequestHead.SetVersion(gHttpHandler->HttpVersion()); + } else { + mRequestHead.SetPath(*requestURI); + + // RequestURI should be the absolute uri H1 proxy syntax + // "http://foo/index.html" so we will overwrite the relative version in + // requestURI + rv = mURI->GetUserPass(buf); + if (NS_FAILED(rv)) return rv; + if (!buf.IsEmpty() && ((strncmp(mSpec.get(), "http:", 5) == 0) || + strncmp(mSpec.get(), "https:", 6) == 0)) { + nsCOMPtr tempURI = nsIOService::CreateExposableURI(mURI); + rv = tempURI->GetAsciiSpec(path); + if (NS_FAILED(rv)) return rv; + requestURI = &path; + } else { + requestURI = &mSpec; + } + + // trim off the #ref portion if any... + int32_t ref2 = requestURI->FindChar('#'); + if (ref2 != kNotFound) { + requestURI->SetLength(ref2); + } + + mRequestHead.SetVersion(gHttpHandler->ProxyHttpVersion()); + } + + mRequestHead.SetRequestURI(*requestURI); + + // Force setting no-cache header for TRRServiceChannel. + // We need to send 'Pragma:no-cache' to inhibit proxy caching even if + // no proxy is configured since we might be talking with a transparent + // proxy, i.e. one that operates at the network level. See bug #14772. + rv = mRequestHead.SetHeaderOnce(nsHttp::Pragma, "no-cache", true); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + // If we're configured to speak HTTP/1.1 then also send 'Cache-control: + // no-cache' + if (mRequestHead.Version() >= HttpVersion::v1_1) { + rv = mRequestHead.SetHeaderOnce(nsHttp::Cache_Control, "no-cache", true); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + + // create wrapper for this channel's notification callbacks + nsCOMPtr callbacks; + NS_NewNotificationCallbacksAggregation(mCallbacks, mLoadGroup, + getter_AddRefs(callbacks)); + + // create the transaction object + mTransaction = new nsHttpTransaction(); + LOG1(("TRRServiceChannel %p created nsHttpTransaction %p\n", this, + mTransaction.get())); + + // See bug #466080. Transfer LOAD_ANONYMOUS flag to socket-layer. + if (mLoadFlags & LOAD_ANONYMOUS) mCaps |= NS_HTTP_LOAD_ANONYMOUS; + + if (mLoadFlags & LOAD_CALL_CONTENT_SNIFFERS) { + mCaps |= NS_HTTP_CALL_CONTENT_SNIFFER; + } + + if (LoadTimingEnabled()) mCaps |= NS_HTTP_TIMING_ENABLED; + + nsCOMPtr pushListener; + NS_QueryNotificationCallbacks(mCallbacks, mLoadGroup, + NS_GET_IID(nsIHttpPushListener), + getter_AddRefs(pushListener)); + HttpTransactionShell::OnPushCallback pushCallback = nullptr; + if (pushListener) { + mCaps |= NS_HTTP_ONPUSH_LISTENER; + nsWeakPtr weakPtrThis( + do_GetWeakReference(static_cast(this))); + pushCallback = [weakPtrThis](uint32_t aPushedStreamId, + const nsACString& aUrl, + const nsACString& aRequestString, + HttpTransactionShell* aTransaction) { + if (nsCOMPtr channel = do_QueryReferent(weakPtrThis)) { + return static_cast(channel.get()) + ->OnPush(aPushedStreamId, aUrl, aRequestString, aTransaction); + } + return NS_ERROR_NOT_AVAILABLE; + }; + } + + EnsureRequestContext(); + + rv = mTransaction->Init( + mCaps, mConnectionInfo, &mRequestHead, mUploadStream, mReqContentLength, + LoadUploadStreamHasHeaders(), mCurrentEventTarget, callbacks, this, + mTopLevelOuterContentWindowId, HttpTrafficCategory::eInvalid, + mRequestContext, mClassOfService, mInitialRwin, + LoadResponseTimeoutEnabled(), mChannelId, nullptr, + std::move(pushCallback), mTransWithPushedStream, mPushedStreamId); + + mTransWithPushedStream = nullptr; + + if (NS_FAILED(rv)) { + mTransaction = nullptr; + return rv; + } + + return rv; +} + +void TRRServiceChannel::SetPushedStreamTransactionAndId( + HttpTransactionShell* aTransWithPushedStream, uint32_t aPushedStreamId) { + MOZ_ASSERT(!mTransWithPushedStream); + LOG(("TRRServiceChannel::SetPushedStreamTransaction [this=%p] trans=%p", this, + aTransWithPushedStream)); + + mTransWithPushedStream = aTransWithPushedStream; + mPushedStreamId = aPushedStreamId; +} + +nsresult TRRServiceChannel::OnPush(uint32_t aPushedStreamId, + const nsACString& aUrl, + const nsACString& aRequestString, + HttpTransactionShell* aTransaction) { + MOZ_ASSERT(aTransaction); + LOG(("TRRServiceChannel::OnPush [this=%p, trans=%p]\n", this, aTransaction)); + + MOZ_ASSERT(mCaps & NS_HTTP_ONPUSH_LISTENER); + nsCOMPtr pushListener; + NS_QueryNotificationCallbacks(mCallbacks, mLoadGroup, + NS_GET_IID(nsIHttpPushListener), + getter_AddRefs(pushListener)); + + if (!pushListener) { + LOG( + ("TRRServiceChannel::OnPush [this=%p] notification callbacks do not " + "implement nsIHttpPushListener\n", + this)); + return NS_ERROR_NOT_AVAILABLE; + } + + nsCOMPtr pushResource; + nsresult rv; + + // Create a Channel for the Push Resource + rv = NS_NewURI(getter_AddRefs(pushResource), aUrl); + if (NS_FAILED(rv)) { + return NS_ERROR_FAILURE; + } + + nsCOMPtr loadInfo = + static_cast(mLoadInfo.get())->Clone(); + nsCOMPtr pushHttpChannel; + rv = gHttpHandler->CreateTRRServiceChannel(pushResource, nullptr, 0, nullptr, + loadInfo, + getter_AddRefs(pushHttpChannel)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = pushHttpChannel->SetLoadFlags(mLoadFlags); + NS_ENSURE_SUCCESS(rv, rv); + + RefPtr channel; + CallQueryInterface(pushHttpChannel, channel.StartAssignment()); + MOZ_ASSERT(channel); + if (!channel) { + return NS_ERROR_UNEXPECTED; + } + + // new channel needs mrqeuesthead and headers from pushedStream + channel->mRequestHead.ParseHeaderSet(aRequestString.BeginReading()); + channel->mLoadGroup = mLoadGroup; + channel->mCallbacks = mCallbacks; + + // Link the pushed stream with the new channel and call listener + channel->SetPushedStreamTransactionAndId(aTransaction, aPushedStreamId); + rv = pushListener->OnPush(this, channel); + return rv; +} + +void TRRServiceChannel::MaybeStartDNSPrefetch() { + if (mConnectionInfo->UsingHttpProxy() || + (mLoadFlags & (nsICachingChannel::LOAD_NO_NETWORK_IO | + nsICachingChannel::LOAD_ONLY_FROM_CACHE))) { + return; + } + + LOG( + ("TRRServiceChannel::MaybeStartDNSPrefetch [this=%p] " + "prefetching%s\n", + this, mCaps & NS_HTTP_REFRESH_DNS ? ", refresh requested" : "")); + + OriginAttributes originAttributes; + mDNSPrefetch = + new nsDNSPrefetch(mURI, originAttributes, nsIRequest::GetTRRMode(), this, + LoadTimingEnabled()); + mDNSPrefetch->PrefetchHigh(mCaps & NS_HTTP_REFRESH_DNS); +} + +NS_IMETHODIMP +TRRServiceChannel::OnTransportStatus(nsITransport* trans, nsresult status, + int64_t progress, int64_t progressMax) { + return NS_OK; +} + +nsresult TRRServiceChannel::CallOnStartRequest() { + LOG(("TRRServiceChannel::CallOnStartRequest [this=%p]", this)); + + if (LoadOnStartRequestCalled()) { + LOG(("CallOnStartRequest already invoked before")); + return mStatus; + } + + nsresult rv = NS_OK; + StoreTracingEnabled(false); + + // Ensure mListener->OnStartRequest will be invoked before exiting + // this function. + auto onStartGuard = MakeScopeExit([&] { + LOG( + (" calling mListener->OnStartRequest by ScopeExit [this=%p, " + "listener=%p]\n", + this, mListener.get())); + MOZ_ASSERT(!LoadOnStartRequestCalled()); + + if (mListener) { + nsCOMPtr deleteProtector(mListener); + StoreOnStartRequestCalled(true); + deleteProtector->OnStartRequest(this); + } + StoreOnStartRequestCalled(true); + }); + + if (mResponseHead && !mResponseHead->HasContentCharset()) + mResponseHead->SetContentCharset(mContentCharsetHint); + + LOG((" calling mListener->OnStartRequest [this=%p, listener=%p]\n", this, + mListener.get())); + + // About to call OnStartRequest, dismiss the guard object. + onStartGuard.release(); + + if (mListener) { + MOZ_ASSERT(!LoadOnStartRequestCalled(), + "We should not call OsStartRequest twice"); + nsCOMPtr deleteProtector(mListener); + StoreOnStartRequestCalled(true); + rv = deleteProtector->OnStartRequest(this); + if (NS_FAILED(rv)) return rv; + } else { + NS_WARNING("OnStartRequest skipped because of null listener"); + StoreOnStartRequestCalled(true); + } + + if (!mResponseHead) { + return NS_OK; + } + + nsAutoCString contentEncoding; + rv = mResponseHead->GetHeader(nsHttp::Content_Encoding, contentEncoding); + if (NS_FAILED(rv) || contentEncoding.IsEmpty()) { + return NS_OK; + } + + // DoApplyContentConversions can only be called on the main thread. + if (NS_IsMainThread()) { + nsCOMPtr listener; + rv = + DoApplyContentConversions(mListener, getter_AddRefs(listener), nullptr); + if (NS_FAILED(rv)) { + return rv; + } + + AfterApplyContentConversions(rv, listener); + return NS_OK; + } + + Suspend(); + + RefPtr self = this; + rv = NS_DispatchToMainThread( + NS_NewRunnableFunction("TRRServiceChannel::DoApplyContentConversions", + [self]() { + nsCOMPtr listener; + nsresult rv = self->DoApplyContentConversions( + self->mListener, getter_AddRefs(listener), + nullptr); + self->AfterApplyContentConversions(rv, listener); + }), + NS_DISPATCH_NORMAL); + if (NS_FAILED(rv)) { + Resume(); + return rv; + } + + return NS_OK; +} + +void TRRServiceChannel::AfterApplyContentConversions( + nsresult aResult, nsIStreamListener* aListener) { + LOG(("TRRServiceChannel::AfterApplyContentConversions [this=%p]", this)); + if (!mCurrentEventTarget->IsOnCurrentThread()) { + RefPtr self = this; + nsCOMPtr listener = aListener; + self->mCurrentEventTarget->Dispatch( + NS_NewRunnableFunction( + "TRRServiceChannel::AfterApplyContentConversions", + [self, aResult, listener]() { + self->Resume(); + self->AfterApplyContentConversions(aResult, listener); + }), + NS_DISPATCH_NORMAL); + return; + } + + if (mCanceled) { + return; + } + + if (NS_FAILED(aResult)) { + Unused << AsyncAbort(aResult); + return; + } + + if (aListener) { + mListener = aListener; + mCompressListener = aListener; + StoreHasAppliedConversion(true); + } +} + +void TRRServiceChannel::ProcessAltService() { + // e.g. Alt-Svc: h2=":443"; ma=60 + // e.g. Alt-Svc: h2="otherhost:443" + // Alt-Svc = 1#( alternative *( OWS ";" OWS parameter ) ) + // alternative = protocol-id "=" alt-authority + // protocol-id = token ; percent-encoded ALPN protocol identifier + // alt-authority = quoted-string ; containing [ uri-host ] ":" port + + if (!LoadAllowAltSvc()) { // per channel opt out + return; + } + + if (!gHttpHandler->AllowAltSvc() || (mCaps & NS_HTTP_DISALLOW_SPDY)) { + return; + } + + nsCString scheme; + mURI->GetScheme(scheme); + bool isHttp = scheme.EqualsLiteral("http"); + if (!isHttp && !scheme.EqualsLiteral("https")) { + return; + } + + nsCString altSvc; + Unused << mResponseHead->GetHeader(nsHttp::Alternate_Service, altSvc); + if (altSvc.IsEmpty()) { + return; + } + + if (!nsHttp::IsReasonableHeaderValue(altSvc)) { + LOG(("Alt-Svc Response Header seems unreasonable - skipping\n")); + return; + } + + nsCString originHost; + int32_t originPort = 80; + mURI->GetPort(&originPort); + if (NS_FAILED(mURI->GetAsciiHost(originHost))) { + return; + } + + nsCOMPtr callbacks; + nsCOMPtr proxyInfo; + NS_NewNotificationCallbacksAggregation(mCallbacks, mLoadGroup, + getter_AddRefs(callbacks)); + if (mProxyInfo) { + proxyInfo = do_QueryInterface(mProxyInfo); + } + + nsCString topWindowOrigin = GetTopWindowOrigin(); + bool isIsolated = IsIsolated(); + auto processHeaderTask = [altSvc, scheme, originHost, originPort, + userName(mUsername), topWindowOrigin, + privateBrowsing(mPrivateBrowsing), isIsolated, + callbacks, proxyInfo, caps(mCaps)]() { + if (XRE_IsSocketProcess()) { + AltServiceChild::ProcessHeader( + altSvc, scheme, originHost, originPort, userName, topWindowOrigin, + privateBrowsing, isIsolated, callbacks, proxyInfo, + caps & NS_HTTP_DISALLOW_SPDY, OriginAttributes()); + return; + } + + AltSvcMapping::ProcessHeader( + altSvc, scheme, originHost, originPort, userName, topWindowOrigin, + privateBrowsing, isIsolated, callbacks, proxyInfo, + caps & NS_HTTP_DISALLOW_SPDY, OriginAttributes()); + }; + + if (NS_IsMainThread()) { + processHeaderTask(); + return; + } + + NS_DispatchToMainThread(NS_NewRunnableFunction( + "TRRServiceChannel::ProcessAltService", std::move(processHeaderTask))); +} + +NS_IMETHODIMP +TRRServiceChannel::OnStartRequest(nsIRequest* request) { + LOG(("TRRServiceChannel::OnStartRequest [this=%p request=%p status=%" PRIx32 + "]\n", + this, request, static_cast(static_cast(mStatus)))); + + if (!(mCanceled || NS_FAILED(mStatus))) { + // capture the request's status, so our consumers will know ASAP of any + // connection failures, etc - bug 93581 + nsresult status; + request->GetStatus(&status); + mStatus = status; + } + + MOZ_ASSERT(request == mTransactionPump, "Unexpected request"); + + StoreAfterOnStartRequestBegun(true); + if (mTransaction) { + if (!mSecurityInfo) { + // grab the security info from the connection object; the transaction + // is guaranteed to own a reference to the connection. + mSecurityInfo = mTransaction->SecurityInfo(); + } + } + + if (NS_SUCCEEDED(mStatus) && mTransaction) { + // mTransactionPump doesn't hit OnInputStreamReady and call this until + // all of the response headers have been acquired, so we can take + // ownership of them from the transaction. + mResponseHead = mTransaction->TakeResponseHead(); + if (mResponseHead) { + uint32_t httpStatus = mResponseHead->Status(); + if ((httpStatus < 500) && (httpStatus != 421) && (httpStatus != 407)) { + ProcessAltService(); + } + + if (httpStatus == 300 || httpStatus == 301 || httpStatus == 302 || + httpStatus == 303 || httpStatus == 307 || httpStatus == 308) { + nsresult rv = SyncProcessRedirection(httpStatus); + if (NS_SUCCEEDED(rv)) { + return rv; + } + + mStatus = rv; + DoNotifyListener(); + return rv; + } + } else { + NS_WARNING("No response head in OnStartRequest"); + } + } + + // avoid crashing if mListener happens to be null... + if (!mListener) { + MOZ_ASSERT_UNREACHABLE("mListener is null"); + return NS_OK; + } + + return CallOnStartRequest(); +} + +nsresult TRRServiceChannel::SyncProcessRedirection(uint32_t aHttpStatus) { + nsAutoCString location; + + // if a location header was not given, then we can't perform the redirect, + // so just carry on as though this were a normal response. + if (NS_FAILED(mResponseHead->GetHeader(nsHttp::Location, location))) { + return NS_ERROR_FAILURE; + } + + // make sure non-ASCII characters in the location header are escaped. + nsAutoCString locationBuf; + if (NS_EscapeURL(location.get(), -1, esc_OnlyNonASCII | esc_Spaces, + locationBuf)) { + location = locationBuf; + } + + LOG(("redirecting to: %s [redirection-limit=%u]\n", location.get(), + uint32_t(mRedirectionLimit))); + + nsCOMPtr redirectURI; + nsresult rv = NS_NewURI(getter_AddRefs(redirectURI), location); + + if (NS_FAILED(rv)) { + LOG(("Invalid URI for redirect: Location: %s\n", location.get())); + return NS_ERROR_CORRUPTED_CONTENT; + } + + // move the reference of the old location to the new one if the new + // one has none. + PropagateReferenceIfNeeded(mURI, redirectURI); + + bool rewriteToGET = + ShouldRewriteRedirectToGET(aHttpStatus, mRequestHead.ParsedMethod()); + + // Let's not rewrite the method to GET for TRR requests. + if (rewriteToGET) { + return NS_ERROR_FAILURE; + } + + // If the method is not safe (such as POST, PUT, DELETE, ...) + if (!mRequestHead.IsSafeMethod()) { + LOG(("TRRServiceChannel: unsafe redirect to:%s\n", location.get())); + } + + uint32_t redirectFlags; + if (nsHttp::IsPermanentRedirect(aHttpStatus)) { + redirectFlags = nsIChannelEventSink::REDIRECT_PERMANENT; + } else { + redirectFlags = nsIChannelEventSink::REDIRECT_TEMPORARY; + } + + nsCOMPtr newChannel; + nsCOMPtr redirectLoadInfo = + static_cast(mLoadInfo.get())->Clone(); + rv = gHttpHandler->CreateTRRServiceChannel(redirectURI, nullptr, 0, nullptr, + redirectLoadInfo, + getter_AddRefs(newChannel)); + if (NS_FAILED(rv)) { + return rv; + } + + rv = SetupReplacementChannel(redirectURI, newChannel, !rewriteToGET, + redirectFlags); + if (NS_FAILED(rv)) { + return rv; + } + + // Make sure to do this after we received redirect veto answer, + // i.e. after all sinks had been notified + newChannel->SetOriginalURI(mOriginalURI); + + rv = newChannel->AsyncOpen(mListener); + LOG((" new channel AsyncOpen returned %" PRIX32, static_cast(rv))); + + // close down this channel + Cancel(NS_BINDING_REDIRECTED); + + ReleaseListeners(); + + return NS_OK; +} + +nsresult TRRServiceChannel::SetupReplacementChannel(nsIURI* aNewURI, + nsIChannel* aNewChannel, + bool aPreserveMethod, + uint32_t aRedirectFlags) { + LOG( + ("TRRServiceChannel::SetupReplacementChannel " + "[this=%p newChannel=%p preserveMethod=%d]", + this, aNewChannel, aPreserveMethod)); + + nsresult rv = HttpBaseChannel::SetupReplacementChannel( + aNewURI, aNewChannel, aPreserveMethod, aRedirectFlags); + if (NS_FAILED(rv)) { + return rv; + } + + rv = CheckRedirectLimit(aRedirectFlags); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr httpChannel = do_QueryInterface(aNewChannel); + if (!httpChannel) { + MOZ_ASSERT(false); + return NS_ERROR_FAILURE; + } + + // convey the ApplyConversion flag (bug 91862) + nsCOMPtr encodedChannel = do_QueryInterface(httpChannel); + if (encodedChannel) { + encodedChannel->SetApplyConversion(LoadApplyConversion()); + } + + // Apply TRR specific settings. + return TRR::SetupTRRServiceChannelInternal( + httpChannel, + mRequestHead.ParsedMethod() == nsHttpRequestHead::kMethod_Get); +} + +NS_IMETHODIMP +TRRServiceChannel::OnDataAvailable(nsIRequest* request, nsIInputStream* input, + uint64_t offset, uint32_t count) { + LOG(("TRRServiceChannel::OnDataAvailable [this=%p request=%p offset=%" PRIu64 + " count=%" PRIu32 "]\n", + this, request, offset, count)); + + // don't send out OnDataAvailable notifications if we've been canceled. + if (mCanceled) return mStatus; + + MOZ_ASSERT(mResponseHead, "No response head in ODA!!"); + + if (mListener) { + return mListener->OnDataAvailable(this, input, offset, count); + } + + return NS_ERROR_ABORT; +} + +NS_IMETHODIMP +TRRServiceChannel::OnStopRequest(nsIRequest* request, nsresult status) { + LOG(("TRRServiceChannel::OnStopRequest [this=%p request=%p status=%" PRIx32 + "]\n", + this, request, static_cast(status))); + + if (mCanceled || NS_FAILED(mStatus)) status = mStatus; + + mTransactionTimings = mTransaction->Timings(); + mTransaction = nullptr; + mTransactionPump = nullptr; + + if (mListener) { + LOG(("TRRServiceChannel %p calling OnStopRequest\n", this)); + MOZ_ASSERT(LoadOnStartRequestCalled(), + "OnStartRequest should be called before OnStopRequest"); + MOZ_ASSERT(!LoadOnStopRequestCalled(), + "We should not call OnStopRequest twice"); + StoreOnStopRequestCalled(true); + mListener->OnStopRequest(this, status); + } + StoreOnStopRequestCalled(true); + + mDNSPrefetch = nullptr; + + if (mLoadGroup) { + mLoadGroup->RemoveRequest(this, nullptr, status); + } + + ReleaseListeners(); + return NS_OK; +} + +NS_IMETHODIMP +TRRServiceChannel::OnLookupComplete(nsICancelable* request, nsIDNSRecord* rec, + nsresult status) { + LOG( + ("TRRServiceChannel::OnLookupComplete [this=%p] prefetch complete%s: " + "%s status[0x%" PRIx32 "]\n", + this, mCaps & NS_HTTP_REFRESH_DNS ? ", refresh requested" : "", + NS_SUCCEEDED(status) ? "success" : "failure", + static_cast(status))); + + // We no longer need the dns prefetch object. Note: mDNSPrefetch could be + // validly null if OnStopRequest has already been called. + // We only need the domainLookup timestamps when not loading from cache + if (mDNSPrefetch && mDNSPrefetch->TimingsValid() && mTransaction) { + TimeStamp connectStart = mTransaction->GetConnectStart(); + TimeStamp requestStart = mTransaction->GetRequestStart(); + // We only set the domainLookup timestamps if we're not using a + // persistent connection. + if (requestStart.IsNull() && connectStart.IsNull()) { + mTransaction->SetDomainLookupStart(mDNSPrefetch->StartTimestamp()); + mTransaction->SetDomainLookupEnd(mDNSPrefetch->EndTimestamp()); + } + } + mDNSPrefetch = nullptr; + + // Unset DNS cache refresh if it was requested, + if (mCaps & NS_HTTP_REFRESH_DNS) { + mCaps &= ~NS_HTTP_REFRESH_DNS; + if (mTransaction) { + mTransaction->SetDNSWasRefreshed(); + } + } + + return NS_OK; +} + +NS_IMETHODIMP +TRRServiceChannel::LogBlockedCORSRequest(const nsAString& aMessage, + const nsACString& aCategory) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +TRRServiceChannel::LogMimeTypeMismatch(const nsACString& aMessageName, + bool aWarning, const nsAString& aURL, + const nsAString& aContentType) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +TRRServiceChannel::SetupFallbackChannel(const char* aFallbackKey) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +TRRServiceChannel::GetIsAuthChannel(bool* aIsAuthChannel) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +TRRServiceChannel::SetNotificationCallbacks(nsIInterfaceRequestor* aCallbacks) { + mCallbacks = aCallbacks; + return NS_OK; +} + +NS_IMETHODIMP +TRRServiceChannel::SetPriority(int32_t value) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +void TRRServiceChannel::OnClassOfServiceUpdated() { + LOG(("TRRServiceChannel::OnClassOfServiceUpdated this=%p, cos=%u", this, + mClassOfService)); + + if (mTransaction) { + gHttpHandler->UpdateClassOfServiceOnTransaction(mTransaction, + mClassOfService); + } +} + +NS_IMETHODIMP +TRRServiceChannel::SetClassFlags(uint32_t inFlags) { + uint32_t previous = mClassOfService; + mClassOfService = inFlags; + if (previous != mClassOfService) { + OnClassOfServiceUpdated(); + } + return NS_OK; +} + +NS_IMETHODIMP +TRRServiceChannel::AddClassFlags(uint32_t inFlags) { + uint32_t previous = mClassOfService; + mClassOfService |= inFlags; + if (previous != mClassOfService) { + OnClassOfServiceUpdated(); + } + return NS_OK; +} + +NS_IMETHODIMP +TRRServiceChannel::ClearClassFlags(uint32_t inFlags) { + uint32_t previous = mClassOfService; + mClassOfService &= ~inFlags; + if (previous != mClassOfService) { + OnClassOfServiceUpdated(); + } + return NS_OK; +} + +NS_IMETHODIMP +TRRServiceChannel::ResumeAt(uint64_t aStartPos, const nsACString& aEntityID) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +void TRRServiceChannel::DoAsyncAbort(nsresult aStatus) { + Unused << AsyncAbort(aStatus); +} + +NS_IMETHODIMP +TRRServiceChannel::GetProxyInfo(nsIProxyInfo** result) { + if (!mConnectionInfo) + *result = mProxyInfo; + else + *result = mConnectionInfo->ProxyInfo(); + NS_IF_ADDREF(*result); + return NS_OK; +} + +NS_IMETHODIMP TRRServiceChannel::GetHttpProxyConnectResponseCode( + int32_t* aResponseCode) { + NS_ENSURE_ARG_POINTER(aResponseCode); + + *aResponseCode = -1; + return NS_OK; +} + +NS_IMETHODIMP +TRRServiceChannel::GetLoadFlags(nsLoadFlags* aLoadFlags) { + return HttpBaseChannel::GetLoadFlags(aLoadFlags); +} + +NS_IMETHODIMP +TRRServiceChannel::SetLoadFlags(nsLoadFlags aLoadFlags) { + if (aLoadFlags & (nsICachingChannel::LOAD_ONLY_FROM_CACHE | LOAD_FROM_CACHE | + nsICachingChannel::LOAD_NO_NETWORK_IO | + nsICachingChannel::LOAD_CHECK_OFFLINE_CACHE)) { + MOZ_ASSERT(false, "Wrong load flags!"); + return NS_ERROR_FAILURE; + } + + return HttpBaseChannel::SetLoadFlags(aLoadFlags); +} + +NS_IMETHODIMP +TRRServiceChannel::GetURI(nsIURI** aURI) { + return HttpBaseChannel::GetURI(aURI); +} + +NS_IMETHODIMP +TRRServiceChannel::GetNotificationCallbacks( + nsIInterfaceRequestor** aCallbacks) { + return HttpBaseChannel::GetNotificationCallbacks(aCallbacks); +} + +NS_IMETHODIMP +TRRServiceChannel::GetLoadGroup(nsILoadGroup** aLoadGroup) { + return HttpBaseChannel::GetLoadGroup(aLoadGroup); +} + +NS_IMETHODIMP +TRRServiceChannel::GetRequestMethod(nsACString& aMethod) { + return HttpBaseChannel::GetRequestMethod(aMethod); +} + +void TRRServiceChannel::DoNotifyListener() { + LOG(("TRRServiceChannel::DoNotifyListener this=%p", this)); + + // In case nsHttpChannel::OnStartRequest wasn't called (e.g. due to flag + // LOAD_ONLY_IF_MODIFIED) we want to set AfterOnStartRequestBegun to true + // before notifying listener. + if (!LoadAfterOnStartRequestBegun()) { + StoreAfterOnStartRequestBegun(true); + } + + if (mListener && !LoadOnStartRequestCalled()) { + nsCOMPtr listener = mListener; + StoreOnStartRequestCalled(true); + listener->OnStartRequest(this); + } + StoreOnStartRequestCalled(true); + + // Make sure IsPending is set to false. At this moment we are done from + // the point of view of our consumer and we have to report our self + // as not-pending. + StoreIsPending(false); + + if (mListener && !LoadOnStopRequestCalled()) { + nsCOMPtr listener = mListener; + StoreOnStopRequestCalled(true); + listener->OnStopRequest(this, mStatus); + } + StoreOnStopRequestCalled(true); + + // We have to make sure to drop the references to listeners and callbacks + // no longer needed. + ReleaseListeners(); + + DoNotifyListenerCleanup(); +} + +void TRRServiceChannel::DoNotifyListenerCleanup() {} + +NS_IMETHODIMP +TRRServiceChannel::GetDomainLookupStart(TimeStamp* _retval) { + if (mTransaction) + *_retval = mTransaction->GetDomainLookupStart(); + else + *_retval = mTransactionTimings.domainLookupStart; + return NS_OK; +} + +NS_IMETHODIMP +TRRServiceChannel::GetDomainLookupEnd(TimeStamp* _retval) { + if (mTransaction) + *_retval = mTransaction->GetDomainLookupEnd(); + else + *_retval = mTransactionTimings.domainLookupEnd; + return NS_OK; +} + +NS_IMETHODIMP +TRRServiceChannel::GetConnectStart(TimeStamp* _retval) { + if (mTransaction) + *_retval = mTransaction->GetConnectStart(); + else + *_retval = mTransactionTimings.connectStart; + return NS_OK; +} + +NS_IMETHODIMP +TRRServiceChannel::GetTcpConnectEnd(TimeStamp* _retval) { + if (mTransaction) + *_retval = mTransaction->GetTcpConnectEnd(); + else + *_retval = mTransactionTimings.tcpConnectEnd; + return NS_OK; +} + +NS_IMETHODIMP +TRRServiceChannel::GetSecureConnectionStart(TimeStamp* _retval) { + if (mTransaction) + *_retval = mTransaction->GetSecureConnectionStart(); + else + *_retval = mTransactionTimings.secureConnectionStart; + return NS_OK; +} + +NS_IMETHODIMP +TRRServiceChannel::GetConnectEnd(TimeStamp* _retval) { + if (mTransaction) + *_retval = mTransaction->GetConnectEnd(); + else + *_retval = mTransactionTimings.connectEnd; + return NS_OK; +} + +NS_IMETHODIMP +TRRServiceChannel::GetRequestStart(TimeStamp* _retval) { + if (mTransaction) + *_retval = mTransaction->GetRequestStart(); + else + *_retval = mTransactionTimings.requestStart; + return NS_OK; +} + +NS_IMETHODIMP +TRRServiceChannel::GetResponseStart(TimeStamp* _retval) { + if (mTransaction) + *_retval = mTransaction->GetResponseStart(); + else + *_retval = mTransactionTimings.responseStart; + return NS_OK; +} + +NS_IMETHODIMP +TRRServiceChannel::GetResponseEnd(TimeStamp* _retval) { + if (mTransaction) + *_retval = mTransaction->GetResponseEnd(); + else + *_retval = mTransactionTimings.responseEnd; + return NS_OK; +} + +NS_IMETHODIMP TRRServiceChannel::SetLoadGroup(nsILoadGroup* aLoadGroup) { + return NS_OK; +} + +NS_IMETHODIMP +TRRServiceChannel::TimingAllowCheck(nsIPrincipal* aOrigin, bool* aResult) { + NS_ENSURE_ARG_POINTER(aResult); + *aResult = true; + return NS_OK; +} + +bool TRRServiceChannel::SameOriginWithOriginalUri(nsIURI* aURI) { return true; } + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/TRRServiceChannel.h b/netwerk/protocol/http/TRRServiceChannel.h new file mode 100644 index 0000000000..2e2cb08571 --- /dev/null +++ b/netwerk/protocol/http/TRRServiceChannel.h @@ -0,0 +1,169 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et 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/. */ + +#ifndef mozilla_net_TRRServiceChannel_h +#define mozilla_net_TRRServiceChannel_h + +#include "HttpBaseChannel.h" +#include "mozilla/DataMutex.h" +#include "nsIDNSListener.h" +#include "nsIProtocolProxyCallback.h" +#include "nsIProxiedChannel.h" +#include "nsIStreamListener.h" +#include "nsWeakReference.h" + +class nsDNSPrefetch; + +namespace mozilla { +namespace net { + +class HttpTransactionShell; +class nsHttpHandler; + +// Use to support QI nsIChannel to TRRServiceChannel +#define NS_TRRSERVICECHANNEL_IID \ + { \ + 0x361c4bb1, 0xd6b2, 0x493b, { \ + 0x86, 0xbc, 0x88, 0xd3, 0x5d, 0x16, 0x38, 0xfa \ + } \ + } + +// TRRServiceChannel is designed to fetch DNS data from DoH server. This channel +// MUST only be used by TRR. +class TRRServiceChannel : public HttpBaseChannel, + public HttpAsyncAborter, + public nsIDNSListener, + public nsIStreamListener, + public nsITransportEventSink, + public nsIProxiedChannel, + public nsIProtocolProxyCallback, + public nsSupportsWeakReference { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_NSIDNSLISTENER + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMLISTENER + NS_DECL_NSITRANSPORTEVENTSINK + NS_DECL_NSIPROXIEDCHANNEL + NS_DECL_NSIPROTOCOLPROXYCALLBACK + NS_DECLARE_STATIC_IID_ACCESSOR(NS_TRRSERVICECHANNEL_IID) + + // nsIRequest + NS_IMETHOD Cancel(nsresult status) override; + NS_IMETHOD Suspend() override; + NS_IMETHOD Resume() override; + NS_IMETHOD GetLoadFlags(nsLoadFlags* aLoadFlags) override; + NS_IMETHOD SetLoadFlags(nsLoadFlags aLoadFlags) override; + NS_IMETHOD GetURI(nsIURI** aURI) override; + NS_IMETHOD GetNotificationCallbacks( + nsIInterfaceRequestor** aCallbacks) override; + NS_IMETHOD GetLoadGroup(nsILoadGroup** aLoadGroup) override; + NS_IMETHOD GetRequestMethod(nsACString& aMethod) override; + // nsIChannel + NS_IMETHOD GetSecurityInfo(nsISupports** aSecurityInfo) override; + NS_IMETHOD AsyncOpen(nsIStreamListener* aListener) override; + + NS_IMETHOD LogBlockedCORSRequest(const nsAString& aMessage, + const nsACString& aCategory) override; + NS_IMETHOD LogMimeTypeMismatch(const nsACString& aMessageName, bool aWarning, + const nsAString& aURL, + const nsAString& aContentType) override; + NS_IMETHOD SetupFallbackChannel(const char* aFallbackKey) override; + NS_IMETHOD GetIsAuthChannel(bool* aIsAuthChannel) override; + + NS_IMETHOD SetNotificationCallbacks( + nsIInterfaceRequestor* aCallbacks) override; + // nsISupportsPriority + NS_IMETHOD SetPriority(int32_t value) override; + // nsIClassOfService + NS_IMETHOD SetClassFlags(uint32_t inFlags) override; + NS_IMETHOD AddClassFlags(uint32_t inFlags) override; + NS_IMETHOD ClearClassFlags(uint32_t inFlags) override; + // nsIResumableChannel + NS_IMETHOD ResumeAt(uint64_t startPos, const nsACString& entityID) override; + + [[nodiscard]] nsresult OnPush(uint32_t aPushedStreamId, + const nsACString& aUrl, + const nsACString& aRequestString, + HttpTransactionShell* aTransaction); + void SetPushedStreamTransactionAndId( + HttpTransactionShell* aTransWithPushedStream, uint32_t aPushedStreamId); + + // nsITimedChannel + NS_IMETHOD GetDomainLookupStart( + mozilla::TimeStamp* aDomainLookupStart) override; + NS_IMETHOD GetDomainLookupEnd(mozilla::TimeStamp* aDomainLookupEnd) override; + NS_IMETHOD GetConnectStart(mozilla::TimeStamp* aConnectStart) override; + NS_IMETHOD GetTcpConnectEnd(mozilla::TimeStamp* aTcpConnectEnd) override; + NS_IMETHOD GetSecureConnectionStart( + mozilla::TimeStamp* aSecureConnectionStart) override; + NS_IMETHOD GetConnectEnd(mozilla::TimeStamp* aConnectEnd) override; + NS_IMETHOD GetRequestStart(mozilla::TimeStamp* aRequestStart) override; + NS_IMETHOD GetResponseStart(mozilla::TimeStamp* aResponseStart) override; + NS_IMETHOD GetResponseEnd(mozilla::TimeStamp* aResponseEnd) override; + NS_IMETHOD SetLoadGroup(nsILoadGroup* aLoadGroup) override; + NS_IMETHOD TimingAllowCheck(nsIPrincipal* aOrigin, bool* aResult) override; + + protected: + TRRServiceChannel(); + virtual ~TRRServiceChannel(); + + void CancelNetworkRequest(nsresult aStatus); + const nsCString& GetTopWindowOrigin(); + nsresult BeginConnect(); + nsresult ContinueOnBeforeConnect(); + nsresult Connect(); + nsresult SetupTransaction(); + void OnClassOfServiceUpdated(); + virtual void DoNotifyListenerCleanup() override; + virtual void DoAsyncAbort(nsresult aStatus) override; + bool IsIsolated() { return false; }; + void ProcessAltService(); + nsresult CallOnStartRequest(); + + void MaybeStartDNSPrefetch(); + void DoNotifyListener(); + nsresult MaybeResolveProxyAndBeginConnect(); + nsresult ResolveProxy(); + void AfterApplyContentConversions(nsresult aResult, + nsIStreamListener* aListener); + nsresult SyncProcessRedirection(uint32_t aHttpStatus); + [[nodiscard]] virtual nsresult SetupReplacementChannel( + nsIURI* aNewURI, nsIChannel* aNewChannel, bool aPreserveMethod, + uint32_t aRedirectFlags) override; + + virtual bool SameOriginWithOriginalUri(nsIURI* aURI) override; + bool DispatchRelease(); + + // True only when we have computed the value of the top window origin. + bool mTopWindowOriginComputed; + + nsCString mUsername; + // The origin of the top window, only valid when mTopWindowOriginComputed is + // true. + nsCString mTopWindowOrigin; + + // Needed for accurate DNS timing + RefPtr mDNSPrefetch; + + nsCOMPtr mTransactionPump; + RefPtr mTransaction; + uint32_t mPushedStreamId; + RefPtr mTransWithPushedStream; + DataMutex> mProxyRequest; + nsCOMPtr mCurrentEventTarget; + + friend class HttpAsyncAborter; + friend class nsHttpHandler; +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(TRRServiceChannel, NS_TRRSERVICECHANNEL_IID) + +} // namespace net +} // namespace mozilla + +#endif // mozilla_net_TRRServiceChannel_h diff --git a/netwerk/protocol/http/TimingStruct.h b/netwerk/protocol/http/TimingStruct.h new file mode 100644 index 0000000000..14468a310e --- /dev/null +++ b/netwerk/protocol/http/TimingStruct.h @@ -0,0 +1,44 @@ +/* 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/. */ + +#ifndef TimingStruct_h_ +#define TimingStruct_h_ + +#include "mozilla/TimeStamp.h" +#include "nsString.h" + +namespace mozilla { +namespace net { + +struct TimingStruct { + TimeStamp domainLookupStart; + TimeStamp domainLookupEnd; + TimeStamp connectStart; + TimeStamp tcpConnectEnd; + TimeStamp secureConnectionStart; + TimeStamp connectEnd; + TimeStamp requestStart; + TimeStamp responseStart; + TimeStamp responseEnd; +}; + +struct ResourceTimingStruct : TimingStruct { + TimeStamp fetchStart; + TimeStamp redirectStart; + TimeStamp redirectEnd; + uint64_t transferSize; + uint64_t encodedBodySize; + nsCString protocolVersion; + + // Not actually part of resource timing, but not part of the transaction + // timings either. These need to be passed to HttpChannelChild along with + // the rest of the timings so the timing information in the child is complete. + TimeStamp cacheReadStart; + TimeStamp cacheReadEnd; +}; + +} // namespace net +} // namespace mozilla + +#endif diff --git a/netwerk/protocol/http/TunnelUtils.cpp b/netwerk/protocol/http/TunnelUtils.cpp new file mode 100644 index 0000000000..b62b1dd92c --- /dev/null +++ b/netwerk/protocol/http/TunnelUtils.cpp @@ -0,0 +1,2172 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et 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/. */ + +// HttpLog.h should generally be included first +#include "HttpLog.h" + +#include "Http2Session.h" +#include "nsHttp.h" +#include "nsHttpHandler.h" +#include "nsHttpRequestHead.h" +#include "TCPFastOpen.h" +#include "nsISocketProvider.h" +#include "nsSocketProviderService.h" +#include "nsISSLSocketControl.h" +#include "nsISocketTransport.h" +#include "nsISupportsPriority.h" +#include "nsNetAddr.h" +#include "prerror.h" +#include "prio.h" +#include "TunnelUtils.h" +#include "nsNetCID.h" +#include "nsServiceManagerUtils.h" +#include "nsComponentManagerUtils.h" +#include "nsSocketTransport2.h" +#include "nsSocketTransportService2.h" +#include "mozilla/AutoRestore.h" +#include "mozilla/Mutex.h" + +#if defined(FUZZING) +# include "FuzzySecurityInfo.h" +# include "mozilla/StaticPrefs_fuzzing.h" +#endif + +namespace mozilla { +namespace net { + +static PRDescIdentity sLayerIdentity; +static PRIOMethods sLayerMethods; +static PRIOMethods* sLayerMethodsPtr = nullptr; + +TLSFilterTransaction::TLSFilterTransaction(nsAHttpTransaction* aWrapped, + const char* aTLSHost, + int32_t aTLSPort, + nsAHttpSegmentReader* aReader, + nsAHttpSegmentWriter* aWriter) + : mTransaction(aWrapped), + mEncryptedTextUsed(0), + mEncryptedTextSize(0), + mSegmentReader(aReader), + mSegmentWriter(aWriter), + mFilterReadCode(NS_ERROR_NOT_INITIALIZED), + mFilterReadAmount(0), + mInOnReadSegment(false), + mForce(false), + mReadSegmentReturnValue(NS_OK), + mCloseReason(NS_ERROR_UNEXPECTED), + mNudgeCounter(0) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG(("TLSFilterTransaction ctor %p\n", this)); + + nsCOMPtr provider; + nsCOMPtr spserv = + nsSocketProviderService::GetOrCreate(); + + if (spserv) { + spserv->GetSocketProvider("ssl", getter_AddRefs(provider)); + } + + // Install an NSPR layer to handle getpeername() with a failure. This is kind + // of silly, but the default one used by the pipe asserts when called and the + // nss code calls it to see if we are connected to a real socket or not. + if (!sLayerMethodsPtr) { + // one time initialization + sLayerIdentity = PR_GetUniqueIdentity("TLSFilterTransaction Layer"); + sLayerMethods = *PR_GetDefaultIOMethods(); + sLayerMethods.getpeername = GetPeerName; + sLayerMethods.getsocketoption = GetSocketOption; + sLayerMethods.setsocketoption = SetSocketOption; + sLayerMethods.read = FilterRead; + sLayerMethods.write = FilterWrite; + sLayerMethods.send = FilterSend; + sLayerMethods.recv = FilterRecv; + sLayerMethods.close = FilterClose; + sLayerMethodsPtr = &sLayerMethods; + } + + mFD = PR_CreateIOLayerStub(sLayerIdentity, &sLayerMethods); + + bool addTLSLayer = true; +#ifdef FUZZING + addTLSLayer = !StaticPrefs::fuzzing_necko_enabled(); + if (!addTLSLayer) { + SOCKET_LOG(("Skipping TLS layer in TLSFilterTransaction for fuzzing.\n")); + + mSecInfo = static_cast( + static_cast(new FuzzySecurityInfo())); + } +#endif + + if (provider && mFD) { + mFD->secret = reinterpret_cast(this); + + if (addTLSLayer) { + provider->AddToSocket(PR_AF_INET, aTLSHost, aTLSPort, nullptr, + OriginAttributes(), 0, 0, mFD, + getter_AddRefs(mSecInfo)); + } + } + + if (mTransaction) { + nsCOMPtr callbacks; + mTransaction->GetSecurityCallbacks(getter_AddRefs(callbacks)); + nsCOMPtr secCtrl(do_QueryInterface(mSecInfo)); + if (secCtrl) { + secCtrl->SetNotificationCallbacks(callbacks); + } + } +} + +TLSFilterTransaction::~TLSFilterTransaction() { + LOG(("TLSFilterTransaction dtor %p\n", this)); + + // Prevent call to OnReadSegment from FilterOutput, our mSegmentReader is now + // an invalid pointer. + mInOnReadSegment = true; + + Cleanup(); +} + +void TLSFilterTransaction::Cleanup() { + LOG(("TLSFilterTransaction::Cleanup %p", this)); + + if (mTransaction) { + mTransaction->Close(NS_ERROR_ABORT); + mTransaction = nullptr; + } + + if (mFD) { + PR_Close(mFD); + mFD = nullptr; + } + mSecInfo = nullptr; + if (mTimer) { + mTimer->Cancel(); + mTimer = nullptr; + } +} + +void TLSFilterTransaction::Close(nsresult aReason) { + LOG(("TLSFilterTransaction::Close %p %" PRIx32, this, + static_cast(aReason))); + + if (!mTransaction) { + return; + } + + if (mTimer) { + mTimer->Cancel(); + mTimer = nullptr; + } + mTransaction->Close(aReason); + mTransaction = nullptr; + + if (gHttpHandler->Bug1563538()) { + if (NS_FAILED(aReason)) { + mCloseReason = aReason; + } else { + mCloseReason = NS_BASE_STREAM_CLOSED; + } + } else { + MOZ_ASSERT(NS_ERROR_UNEXPECTED == mCloseReason); + } +} + +nsresult TLSFilterTransaction::OnReadSegment(const char* aData, uint32_t aCount, + uint32_t* outCountRead) { + LOG(("TLSFilterTransaction %p OnReadSegment %d (buffered %d)\n", this, aCount, + mEncryptedTextUsed)); + + mReadSegmentReturnValue = NS_OK; + MOZ_ASSERT(mSegmentReader); + if (!mSecInfo) { + return NS_ERROR_FAILURE; + } + + nsresult rv; + *outCountRead = 0; + + // get rid of buffer first + if (mEncryptedTextUsed) { + rv = mSegmentReader->CommitToSegmentSize(mEncryptedTextUsed, mForce); + if (rv == NS_BASE_STREAM_WOULD_BLOCK) { + return rv; + } + + uint32_t amt; + rv = mSegmentReader->OnReadSegment(mEncryptedText.get(), mEncryptedTextUsed, + &amt); + if (NS_FAILED(rv)) { + return rv; + } + + mEncryptedTextUsed -= amt; + if (mEncryptedTextUsed) { + memmove(mEncryptedText.get(), &mEncryptedText[amt], mEncryptedTextUsed); + return NS_OK; + } + } + + // encrypt for network write + // write aData down the SSL layer into the FilterWrite() method where it will + // be queued into mEncryptedText. We need to copy it like this in order to + // guarantee atomic writes + + EnsureBuffer(mEncryptedText, aCount + 4096, 0, mEncryptedTextSize); + + // Prevents call to OnReadSegment from inside FilterOutput, as we handle it + // here. + AutoRestore inOnReadSegment(mInOnReadSegment); + mInOnReadSegment = true; + + while (aCount > 0) { + int32_t written = PR_Write(mFD, aData, aCount); + LOG(("TLSFilterTransaction %p OnReadSegment PRWrite(%d) = %d %d\n", this, + aCount, written, PR_GetError() == PR_WOULD_BLOCK_ERROR)); + + if (written < 1) { + if (*outCountRead) { + return NS_OK; + } + // mTransaction ReadSegments actually obscures this code, so + // keep it in a member var for this::ReadSegments to inspect. Similar + // to nsHttpConnection::mSocketOutCondition + PRErrorCode code = PR_GetError(); + mReadSegmentReturnValue = ErrorAccordingToNSPR(code); + + return mReadSegmentReturnValue; + } + aCount -= written; + aData += written; + *outCountRead += written; + mNudgeCounter = 0; + } + + LOG(("TLSFilterTransaction %p OnReadSegment2 (buffered %d)\n", this, + mEncryptedTextUsed)); + + uint32_t amt = 0; + if (mEncryptedTextUsed) { + // If we are tunneled on spdy CommitToSegmentSize will prevent partial + // writes that could interfere with multiplexing. H1 is fine with + // partial writes. + rv = mSegmentReader->CommitToSegmentSize(mEncryptedTextUsed, mForce); + if (rv != NS_BASE_STREAM_WOULD_BLOCK) { + rv = mSegmentReader->OnReadSegment(mEncryptedText.get(), + mEncryptedTextUsed, &amt); + } + + if (rv == NS_BASE_STREAM_WOULD_BLOCK) { + // return OK because all the data was consumed and stored in this buffer + // It is fine if the connection is null. We are likely a websocket and + // thus writing push is ensured by the caller. + if (Connection()) { + Connection()->TransactionHasDataToWrite(this); + } + return NS_OK; + } + if (NS_FAILED(rv)) { + return rv; + } + } + + if (amt == mEncryptedTextUsed) { + mEncryptedText = nullptr; + mEncryptedTextUsed = 0; + mEncryptedTextSize = 0; + } else { + memmove(mEncryptedText.get(), &mEncryptedText[amt], + mEncryptedTextUsed - amt); + mEncryptedTextUsed -= amt; + } + return NS_OK; +} + +int32_t TLSFilterTransaction::FilterOutput(const char* aBuf, int32_t aAmount) { + EnsureBuffer(mEncryptedText, mEncryptedTextUsed + aAmount, mEncryptedTextUsed, + mEncryptedTextSize); + memcpy(&mEncryptedText[mEncryptedTextUsed], aBuf, aAmount); + mEncryptedTextUsed += aAmount; + + LOG(("TLSFilterTransaction::FilterOutput %p %d buffered=%u mSegmentReader=%p", + this, aAmount, mEncryptedTextUsed, mSegmentReader)); + + if (!mInOnReadSegment) { + // When called externally, we must make sure any newly written data is + // actually sent to the higher level connection. + // This also covers the case when PR_Read() wrote a re-negotioation + // response. + uint32_t notUsed; + Unused << OnReadSegment("", 0, ¬Used); + } + + return aAmount; +} + +nsresult TLSFilterTransaction::CommitToSegmentSize(uint32_t size, + bool forceCommitment) { + if (!mSegmentReader) { + return NS_ERROR_FAILURE; + } + + // pad the commit by a little bit to leave room for encryption overhead + // this isn't foolproof and we may still have to buffer, but its a good start + mForce = forceCommitment; + return mSegmentReader->CommitToSegmentSize(size + 1024, forceCommitment); +} + +nsresult TLSFilterTransaction::OnWriteSegment(char* aData, uint32_t aCount, + uint32_t* outCountRead) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + MOZ_ASSERT(mSegmentWriter); + LOG(("TLSFilterTransaction::OnWriteSegment %p max=%d\n", this, aCount)); + if (!mSecInfo) { + return NS_ERROR_FAILURE; + } + + // this will call through to FilterInput to get data from the higher + // level connection before removing the local TLS layer + mFilterReadCode = NS_OK; + mFilterReadAmount = 0; + int32_t bytesRead = PR_Read(mFD, aData, aCount); + if (bytesRead == -1) { + PRErrorCode code = PR_GetError(); + if (code == PR_WOULD_BLOCK_ERROR) { + LOG( + ("TLSFilterTransaction::OnWriteSegment %p PR_Read would block, " + "actual read: %d\n", + this, mFilterReadAmount)); + + if (mFilterReadAmount == 0 && NS_SUCCEEDED(mFilterReadCode)) { + // No reading happened, but also no error occured, hence there is no + // condition to break the `again` loop, propagate WOULD_BLOCK through + // mFilterReadCode to break it and poll the socket again for reading. + mFilterReadCode = NS_BASE_STREAM_WOULD_BLOCK; + } + return NS_BASE_STREAM_WOULD_BLOCK; + } + // If reading from the socket succeeded (NS_SUCCEEDED(mFilterReadCode)), + // but the nss layer encountered an error remember the error. + if (NS_SUCCEEDED(mFilterReadCode)) { + mFilterReadCode = ErrorAccordingToNSPR(code); + LOG(("TLSFilterTransaction::OnWriteSegment %p nss error %" PRIx32 ".\n", + this, static_cast(mFilterReadCode))); + } + return mFilterReadCode; + } + *outCountRead = bytesRead; + + if (NS_SUCCEEDED(mFilterReadCode) && !bytesRead) { + LOG( + ("TLSFilterTransaction::OnWriteSegment %p " + "Second layer of TLS stripping results in STREAM_CLOSED\n", + this)); + mFilterReadCode = NS_BASE_STREAM_CLOSED; + } + + LOG(("TLSFilterTransaction::OnWriteSegment %p rv=%" PRIx32 " didread=%d " + "2 layers of ssl stripped to plaintext\n", + this, static_cast(mFilterReadCode), bytesRead)); + return mFilterReadCode; +} + +int32_t TLSFilterTransaction::FilterInput(char* aBuf, int32_t aAmount) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + MOZ_ASSERT(mSegmentWriter); + LOG(("TLSFilterTransaction::FilterInput max=%d\n", aAmount)); + + uint32_t outCountRead = 0; + mFilterReadCode = + mSegmentWriter->OnWriteSegment(aBuf, aAmount, &outCountRead); + if (NS_SUCCEEDED(mFilterReadCode) && outCountRead) { + LOG(("TLSFilterTransaction::FilterInput rv=%" PRIx32 + " read=%d input from net " + "1 layer stripped, 1 still on\n", + static_cast(mFilterReadCode), outCountRead)); + if (mReadSegmentReturnValue == NS_BASE_STREAM_WOULD_BLOCK) { + mNudgeCounter = 0; + } + + mFilterReadAmount += outCountRead; + } + if (mFilterReadCode == NS_BASE_STREAM_WOULD_BLOCK) { + PR_SetError(PR_WOULD_BLOCK_ERROR, 0); + return -1; + } + return outCountRead; +} + +nsresult TLSFilterTransaction::ReadSegments(nsAHttpSegmentReader* aReader, + uint32_t aCount, + uint32_t* outCountRead) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG(("TLSFilterTransaction::ReadSegments %p max=%d\n", this, aCount)); + + if (!mTransaction) { + return mCloseReason; + } + + mReadSegmentReturnValue = NS_OK; + mSegmentReader = aReader; + nsresult rv = mTransaction->ReadSegments(this, aCount, outCountRead); + + // mSegmentReader is left assigned (not nullified) because we want to be able + // to call OnReadSegment directly, it expects mSegmentReader be non-null. + + LOG(("TLSFilterTransaction %p called trans->ReadSegments rv=%" PRIx32 " %d\n", + this, static_cast(rv), *outCountRead)); + if (NS_SUCCEEDED(rv) && + (mReadSegmentReturnValue == NS_BASE_STREAM_WOULD_BLOCK)) { + LOG(("TLSFilterTransaction %p read segment blocked found rv=%" PRIx32 "\n", + this, static_cast(rv))); + if (Connection()) { + Unused << Connection()->ForceSend(); + } + } + + return NS_SUCCEEDED(rv) ? mReadSegmentReturnValue : rv; +} + +nsresult TLSFilterTransaction::WriteSegmentsAgain(nsAHttpSegmentWriter* aWriter, + uint32_t aCount, + uint32_t* outCountWritten, + bool* again) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG(("TLSFilterTransaction::WriteSegmentsAgain %p max=%d\n", this, aCount)); + + if (!mTransaction) { + return mCloseReason; + } + + mSegmentWriter = aWriter; + + nsresult rv = + mTransaction->WriteSegmentsAgain(this, aCount, outCountWritten, again); + + if (NS_SUCCEEDED(rv) && !(*outCountWritten) && NS_FAILED(mFilterReadCode)) { + // nsPipe turns failures into silent OK.. undo that! + rv = mFilterReadCode; + if (Connection() && (mFilterReadCode == NS_BASE_STREAM_WOULD_BLOCK)) { + Unused << Connection()->ResumeRecv(); + } + } + LOG(("TLSFilterTransaction %p called trans->WriteSegments rv=%" PRIx32 + " %d\n", + this, static_cast(rv), *outCountWritten)); + return rv; +} + +nsresult TLSFilterTransaction::WriteSegments(nsAHttpSegmentWriter* aWriter, + uint32_t aCount, + uint32_t* outCountWritten) { + bool again = false; + return WriteSegmentsAgain(aWriter, aCount, outCountWritten, &again); +} + +nsresult TLSFilterTransaction::GetTransactionSecurityInfo( + nsISupports** outSecInfo) { + if (!mSecInfo) { + return NS_ERROR_FAILURE; + } + + nsCOMPtr temp(mSecInfo); + temp.forget(outSecInfo); + return NS_OK; +} + +nsresult TLSFilterTransaction::NudgeTunnel(NudgeTunnelCallback* aCallback) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG(("TLSFilterTransaction %p NudgeTunnel\n", this)); + mNudgeCallback = nullptr; + + if (!mSecInfo) { + return NS_ERROR_FAILURE; + } + + nsCOMPtr ssl(do_QueryInterface(mSecInfo)); + nsresult rv = ssl ? ssl->DriveHandshake() : NS_ERROR_FAILURE; + if (NS_FAILED(rv) && rv != NS_BASE_STREAM_WOULD_BLOCK) { + // fatal handshake failure + LOG(("TLSFilterTransaction %p Fatal Handshake Failure: %d\n", this, + PR_GetError())); + return NS_ERROR_FAILURE; + } + + uint32_t notUsed; + Unused << OnReadSegment("", 0, ¬Used); + + // The SSL Layer does some unusual things with PR_Poll that makes it a bad + // match for multiplexed SSL sessions. We work around this by manually polling + // for the moment during the brief handshake phase or otherwise blocked on + // write. Thankfully this is a pretty unusual state. NSPR doesn't help us here + // - asserting when polling without the NSPR IO layer on the bottom of the + // stack. As a follow-on we can do some NSPR and maybe libssl changes to make + // this more event driven, but this is acceptable for getting started. + + uint32_t counter = mNudgeCounter++; + uint32_t delay; + + if (!counter) { + delay = 0; + } else if (counter < 8) { // up to 48ms at 6 + delay = 6; + } else if (counter < 34) { // up to 499 ms at 17ms + delay = 17; + } else { // after that at 51ms (3 old windows ticks) + delay = 51; + } + + if (!mTimer) { + mTimer = NS_NewTimer(); + } + + mNudgeCallback = aCallback; + if (!mTimer || NS_FAILED(mTimer->InitWithCallback(this, delay, + nsITimer::TYPE_ONE_SHOT))) { + return StartTimerCallback(); + } + + LOG(("TLSFilterTransaction %p NudgeTunnel timer started\n", this)); + return NS_OK; +} + +NS_IMETHODIMP +TLSFilterTransaction::Notify(nsITimer* timer) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG(("TLSFilterTransaction %p NudgeTunnel notify\n", this)); + + if (timer != mTimer) { + return NS_ERROR_UNEXPECTED; + } + nsresult rv = StartTimerCallback(); + if (NS_FAILED(rv)) { + Close(rv); + } + return NS_OK; +} + +NS_IMETHODIMP +TLSFilterTransaction::GetName(nsACString& aName) { + aName.AssignLiteral("TLSFilterTransaction"); + return NS_OK; +} + +nsresult TLSFilterTransaction::StartTimerCallback() { + LOG(("TLSFilterTransaction %p NudgeTunnel StartTimerCallback %p\n", this, + mNudgeCallback.get())); + + if (mNudgeCallback) { + // This class can be called re-entrantly, so cleanup m* before ->on() + RefPtr cb(mNudgeCallback); + mNudgeCallback = nullptr; + return cb->OnTunnelNudged(this); + } + return NS_OK; +} + +bool TLSFilterTransaction::HasDataToRecv() { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + if (!mFD) { + return false; + } + int32_t n = 0; + char c; + n = PR_Recv(mFD, &c, 1, PR_MSG_PEEK, 0); + return n > 0; +} + +PRStatus TLSFilterTransaction::GetPeerName(PRFileDesc* aFD, PRNetAddr* addr) { + NetAddr peeraddr; + TLSFilterTransaction* self = + reinterpret_cast(aFD->secret); + + if (!self->mTransaction || + NS_FAILED(self->mTransaction->Connection()->Transport()->GetPeerAddr( + &peeraddr))) { + return PR_FAILURE; + } + NetAddrToPRNetAddr(&peeraddr, addr); + return PR_SUCCESS; +} + +PRStatus TLSFilterTransaction::GetSocketOption(PRFileDesc* aFD, + PRSocketOptionData* aOpt) { + if (aOpt->option == PR_SockOpt_Nonblocking) { + aOpt->value.non_blocking = PR_TRUE; + return PR_SUCCESS; + } + return PR_FAILURE; +} + +PRStatus TLSFilterTransaction::SetSocketOption(PRFileDesc* aFD, + const PRSocketOptionData* aOpt) { + return PR_FAILURE; +} + +PRStatus TLSFilterTransaction::FilterClose(PRFileDesc* aFD) { + return PR_SUCCESS; +} + +int32_t TLSFilterTransaction::FilterWrite(PRFileDesc* aFD, const void* aBuf, + int32_t aAmount) { + TLSFilterTransaction* self = + reinterpret_cast(aFD->secret); + return self->FilterOutput(static_cast(aBuf), aAmount); +} + +int32_t TLSFilterTransaction::FilterSend(PRFileDesc* aFD, const void* aBuf, + int32_t aAmount, int, PRIntervalTime) { + return FilterWrite(aFD, aBuf, aAmount); +} + +int32_t TLSFilterTransaction::FilterRead(PRFileDesc* aFD, void* aBuf, + int32_t aAmount) { + TLSFilterTransaction* self = + reinterpret_cast(aFD->secret); + return self->FilterInput(static_cast(aBuf), aAmount); +} + +int32_t TLSFilterTransaction::FilterRecv(PRFileDesc* aFD, void* aBuf, + int32_t aAmount, int, PRIntervalTime) { + return FilterRead(aFD, aBuf, aAmount); +} + +///// +// The other methods of TLSFilterTransaction just call mTransaction->method +///// + +void TLSFilterTransaction::SetConnection(nsAHttpConnection* aConnection) { + if (!mTransaction) { + return; + } + + mTransaction->SetConnection(aConnection); +} + +nsAHttpConnection* TLSFilterTransaction::Connection() { + if (!mTransaction) { + return nullptr; + } + return mTransaction->Connection(); +} + +void TLSFilterTransaction::GetSecurityCallbacks(nsIInterfaceRequestor** outCB) { + if (!mTransaction) { + return; + } + mTransaction->GetSecurityCallbacks(outCB); +} + +void TLSFilterTransaction::OnTransportStatus(nsITransport* aTransport, + nsresult aStatus, + int64_t aProgress) { + if (!mTransaction) { + return; + } + mTransaction->OnTransportStatus(aTransport, aStatus, aProgress); +} + +nsHttpConnectionInfo* TLSFilterTransaction::ConnectionInfo() { + if (!mTransaction) { + return nullptr; + } + return mTransaction->ConnectionInfo(); +} + +bool TLSFilterTransaction::IsDone() { + if (!mTransaction) { + return true; + } + return mTransaction->IsDone(); +} + +nsresult TLSFilterTransaction::Status() { + if (!mTransaction) { + return NS_ERROR_UNEXPECTED; + } + + return mTransaction->Status(); +} + +uint32_t TLSFilterTransaction::Caps() { + if (!mTransaction) { + return 0; + } + + return mTransaction->Caps(); +} + +void TLSFilterTransaction::SetProxyConnectFailed() { + if (!mTransaction) { + return; + } + + mTransaction->SetProxyConnectFailed(); +} + +nsHttpRequestHead* TLSFilterTransaction::RequestHead() { + if (!mTransaction) { + return nullptr; + } + + return mTransaction->RequestHead(); +} + +uint32_t TLSFilterTransaction::Http1xTransactionCount() { + if (!mTransaction) { + return 0; + } + + return mTransaction->Http1xTransactionCount(); +} + +nsresult TLSFilterTransaction::TakeSubTransactions( + nsTArray >& outTransactions) { + LOG(("TLSFilterTransaction::TakeSubTransactions [this=%p] mTransaction %p\n", + this, mTransaction.get())); + + if (!mTransaction) { + return NS_ERROR_UNEXPECTED; + } + + if (mTransaction->TakeSubTransactions(outTransactions) == + NS_ERROR_NOT_IMPLEMENTED) { + outTransactions.AppendElement(mTransaction); + } + mTransaction = nullptr; + + return NS_OK; +} + +nsresult TLSFilterTransaction::SetProxiedTransaction( + nsAHttpTransaction* aTrans, nsAHttpTransaction* aSpdyConnectTransaction) { + LOG( + ("TLSFilterTransaction::SetProxiedTransaction [this=%p] aTrans=%p, " + "aSpdyConnectTransaction=%p\n", + this, aTrans, aSpdyConnectTransaction)); + + mTransaction = aTrans; + + // Reverting mCloseReason to the default value for consistency to indicate we + // are no longer in closed state. + mCloseReason = NS_ERROR_UNEXPECTED; + + nsCOMPtr callbacks; + mTransaction->GetSecurityCallbacks(getter_AddRefs(callbacks)); + nsCOMPtr secCtrl(do_QueryInterface(mSecInfo)); + if (secCtrl && callbacks) { + secCtrl->SetNotificationCallbacks(callbacks); + } + + mWeakTrans = do_GetWeakReference(aSpdyConnectTransaction); + + return NS_OK; +} + +bool TLSFilterTransaction::IsNullTransaction() { + if (!mTransaction) { + return false; + } + return mTransaction->IsNullTransaction(); +} + +NullHttpTransaction* TLSFilterTransaction::QueryNullTransaction() { + if (!mTransaction) { + return nullptr; + } + return mTransaction->QueryNullTransaction(); +} + +nsHttpTransaction* TLSFilterTransaction::QueryHttpTransaction() { + if (!mTransaction) { + return nullptr; + } + return mTransaction->QueryHttpTransaction(); +} + +class SocketInWrapper : public nsIAsyncInputStream, + public nsAHttpSegmentWriter { + NS_DECL_THREADSAFE_ISUPPORTS + NS_FORWARD_NSIASYNCINPUTSTREAM(mStream->) + + SocketInWrapper(nsIAsyncInputStream* aWrapped, TLSFilterTransaction* aFilter) + : mStream(aWrapped), mTLSFilter(aFilter) {} + + NS_IMETHOD Close() override { + mTLSFilter = nullptr; + return mStream->Close(); + } + + NS_IMETHOD Available(uint64_t* _retval) override { + return mStream->Available(_retval); + } + + NS_IMETHOD IsNonBlocking(bool* _retval) override { + return mStream->IsNonBlocking(_retval); + } + + NS_IMETHOD ReadSegments(nsWriteSegmentFun aWriter, void* aClosure, + uint32_t aCount, uint32_t* _retval) override { + return mStream->ReadSegments(aWriter, aClosure, aCount, _retval); + } + + // finally, ones that don't get forwarded :) + NS_IMETHOD Read(char* aBuf, uint32_t aCount, uint32_t* _retval) override; + virtual nsresult OnWriteSegment(char* segment, uint32_t count, + uint32_t* countWritten) override; + + private: + virtual ~SocketInWrapper() = default; + ; + + nsCOMPtr mStream; + RefPtr mTLSFilter; +}; + +nsresult SocketInWrapper::OnWriteSegment(char* segment, uint32_t count, + uint32_t* countWritten) { + LOG(("SocketInWrapper OnWriteSegment %d %p filter=%p\n", count, this, + mTLSFilter.get())); + + nsresult rv = mStream->Read(segment, count, countWritten); + LOG(("SocketInWrapper OnWriteSegment %p wrapped read %" PRIx32 " %d\n", this, + static_cast(rv), *countWritten)); + return rv; +} + +NS_IMETHODIMP +SocketInWrapper::Read(char* aBuf, uint32_t aCount, uint32_t* _retval) { + LOG(("SocketInWrapper Read %d %p filter=%p\n", aCount, this, + mTLSFilter.get())); + + if (!mTLSFilter) { + return NS_ERROR_UNEXPECTED; // protect potentially dangling mTLSFilter + } + + // mTLSFilter->mSegmentWriter MUST be this at ctor time + return mTLSFilter->OnWriteSegment(aBuf, aCount, _retval); +} + +class SocketOutWrapper : public nsIAsyncOutputStream, + public nsAHttpSegmentReader { + NS_DECL_THREADSAFE_ISUPPORTS + NS_FORWARD_NSIASYNCOUTPUTSTREAM(mStream->) + + SocketOutWrapper(nsIAsyncOutputStream* aWrapped, + TLSFilterTransaction* aFilter) + : mStream(aWrapped), mTLSFilter(aFilter) {} + + NS_IMETHOD Close() override { + mTLSFilter = nullptr; + return mStream->Close(); + } + + NS_IMETHOD Flush() override { return mStream->Flush(); } + + NS_IMETHOD IsNonBlocking(bool* _retval) override { + return mStream->IsNonBlocking(_retval); + } + + NS_IMETHOD WriteSegments(nsReadSegmentFun aReader, void* aClosure, + uint32_t aCount, uint32_t* _retval) override { + return mStream->WriteSegments(aReader, aClosure, aCount, _retval); + } + + NS_IMETHOD WriteFrom(nsIInputStream* aFromStream, uint32_t aCount, + uint32_t* _retval) override { + return mStream->WriteFrom(aFromStream, aCount, _retval); + } + + // finally, ones that don't get forwarded :) + NS_IMETHOD Write(const char* aBuf, uint32_t aCount, + uint32_t* _retval) override; + virtual nsresult OnReadSegment(const char* segment, uint32_t count, + uint32_t* countRead) override; + + private: + virtual ~SocketOutWrapper() = default; + ; + + nsCOMPtr mStream; + RefPtr mTLSFilter; +}; + +nsresult SocketOutWrapper::OnReadSegment(const char* segment, uint32_t count, + uint32_t* countWritten) { + return mStream->Write(segment, count, countWritten); +} + +NS_IMETHODIMP +SocketOutWrapper::Write(const char* aBuf, uint32_t aCount, uint32_t* _retval) { + LOG(("SocketOutWrapper Write %d %p filter=%p\n", aCount, this, + mTLSFilter.get())); + + // mTLSFilter->mSegmentReader MUST be this at ctor time + if (!mTLSFilter) { + return NS_ERROR_UNEXPECTED; // protect potentially dangling mTLSFilter + } + + return mTLSFilter->OnReadSegment(aBuf, aCount, _retval); +} + +void TLSFilterTransaction::newIODriver(nsIAsyncInputStream* aSocketIn, + nsIAsyncOutputStream* aSocketOut, + nsIAsyncInputStream** outSocketIn, + nsIAsyncOutputStream** outSocketOut) { + SocketInWrapper* inputWrapper = new SocketInWrapper(aSocketIn, this); + mSegmentWriter = inputWrapper; + nsCOMPtr newIn(inputWrapper); + newIn.forget(outSocketIn); + + SocketOutWrapper* outputWrapper = new SocketOutWrapper(aSocketOut, this); + mSegmentReader = outputWrapper; + nsCOMPtr newOut(outputWrapper); + newOut.forget(outSocketOut); +} + +SpdyConnectTransaction* TLSFilterTransaction::QuerySpdyConnectTransaction() { + if (!mTransaction) { + return nullptr; + } + return mTransaction->QuerySpdyConnectTransaction(); +} + +class WeakTransProxy final : public nsISupports { + public: + NS_DECL_THREADSAFE_ISUPPORTS + + explicit WeakTransProxy(SpdyConnectTransaction* aTrans) { + MOZ_ASSERT(OnSocketThread()); + mWeakTrans = do_GetWeakReference(aTrans); + } + + already_AddRefed QueryTransaction() { + MOZ_ASSERT(OnSocketThread()); + RefPtr trans = do_QueryReferent(mWeakTrans); + return trans.forget(); + } + + private: + ~WeakTransProxy() { MOZ_ASSERT(OnSocketThread()); } + + nsWeakPtr mWeakTrans; +}; + +NS_IMPL_ISUPPORTS(WeakTransProxy, nsISupports) + +class WeakTransFreeProxy final : public Runnable { + public: + explicit WeakTransFreeProxy(WeakTransProxy* proxy) + : Runnable("WeakTransFreeProxy"), mProxy(proxy) {} + + NS_IMETHOD Run() override { + MOZ_ASSERT(OnSocketThread()); + mProxy = nullptr; + return NS_OK; + } + + void Dispatch() { + MOZ_ASSERT(!OnSocketThread()); + nsCOMPtr sts = + do_GetService("@mozilla.org/network/socket-transport-service;1"); + Unused << sts->Dispatch(this, nsIEventTarget::DISPATCH_NORMAL); + } + + private: + RefPtr mProxy; +}; + +class SocketTransportShim : public nsISocketTransport { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSITRANSPORT + NS_DECL_NSISOCKETTRANSPORT + + explicit SocketTransportShim(SpdyConnectTransaction* aTrans, + nsISocketTransport* aWrapped, bool aIsWebsocket) + : mWrapped(aWrapped), mIsWebsocket(aIsWebsocket) { + mWeakTrans = new WeakTransProxy(aTrans); + } + + private: + virtual ~SocketTransportShim() { + if (!OnSocketThread()) { + RefPtr p = new WeakTransFreeProxy(mWeakTrans); + mWeakTrans = nullptr; + p->Dispatch(); + } + } + + nsCOMPtr mWrapped; + bool mIsWebsocket; + nsCOMPtr mSecurityCallbacks; + RefPtr mWeakTrans; // SpdyConnectTransaction * +}; + +class OutputStreamShim : public nsIAsyncOutputStream { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIOUTPUTSTREAM + NS_DECL_NSIASYNCOUTPUTSTREAM + + friend class SpdyConnectTransaction; + friend class WebsocketHasDataToWrite; + friend class OutputCloseTransaction; + + OutputStreamShim(SpdyConnectTransaction* aTrans, bool aIsWebsocket) + : mCallback(nullptr), + mStatus(NS_OK), + mMutex("OutputStreamShim"), + mIsWebsocket(aIsWebsocket) { + mWeakTrans = new WeakTransProxy(aTrans); + } + + already_AddRefed TakeCallback(); + + private: + virtual ~OutputStreamShim() { + if (!OnSocketThread()) { + RefPtr p = new WeakTransFreeProxy(mWeakTrans); + mWeakTrans = nullptr; + p->Dispatch(); + } + } + + RefPtr mWeakTrans; // SpdyConnectTransaction * + nsCOMPtr mCallback; + nsresult mStatus; + mozilla::Mutex mMutex; + + // Websockets + bool mIsWebsocket; + nsresult CallTransactionHasDataToWrite(); + nsresult CloseTransaction(nsresult reason); +}; + +class InputStreamShim : public nsIAsyncInputStream { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIINPUTSTREAM + NS_DECL_NSIASYNCINPUTSTREAM + + friend class SpdyConnectTransaction; + friend class InputCloseTransaction; + + InputStreamShim(SpdyConnectTransaction* aTrans, bool aIsWebsocket) + : mCallback(nullptr), + mStatus(NS_OK), + mMutex("InputStreamShim"), + mIsWebsocket(aIsWebsocket) { + mWeakTrans = new WeakTransProxy(aTrans); + } + + already_AddRefed TakeCallback(); + bool HasCallback(); + + private: + virtual ~InputStreamShim() { + if (!OnSocketThread()) { + RefPtr p = new WeakTransFreeProxy(mWeakTrans); + mWeakTrans = nullptr; + p->Dispatch(); + } + } + + RefPtr mWeakTrans; // SpdyConnectTransaction * + nsCOMPtr mCallback; + nsresult mStatus; + mozilla::Mutex mMutex; + + // Websockets + bool mIsWebsocket; + nsresult CloseTransaction(nsresult reason); +}; + +SpdyConnectTransaction::SpdyConnectTransaction( + nsHttpConnectionInfo* ci, nsIInterfaceRequestor* callbacks, uint32_t caps, + nsHttpTransaction* trans, nsAHttpConnection* session, bool isWebsocket) + : NullHttpTransaction(ci, callbacks, caps | NS_HTTP_ALLOW_KEEPALIVE), + mConnectStringOffset(0), + mSession(session), + mSegmentReader(nullptr), + mInputDataSize(0), + mInputDataUsed(0), + mInputDataOffset(0), + mOutputDataSize(0), + mOutputDataUsed(0), + mOutputDataOffset(0), + mForcePlainText(false), + mIsWebsocket(isWebsocket), + mConnRefTaken(false), + mCreateShimErrorCalled(false) { + LOG(("SpdyConnectTransaction ctor %p\n", this)); + + mTimestampSyn = TimeStamp::Now(); + mRequestHead = new nsHttpRequestHead(); + if (mIsWebsocket) { + // Ensure our request head has all the websocket headers duplicated from the + // original transaction before calling the boilerplate stuff to create the + // rest of the CONNECT headers. + trans->RequestHead()->Enter(); + mRequestHead->SetHeaders(trans->RequestHead()->Headers()); + trans->RequestHead()->Exit(); + } + DebugOnly rv = nsHttpConnection::MakeConnectString( + trans, mRequestHead, mConnectString, mIsWebsocket); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + mDrivingTransaction = trans; +} + +SpdyConnectTransaction::~SpdyConnectTransaction() { + LOG(("SpdyConnectTransaction dtor %p\n", this)); + + MOZ_ASSERT(OnSocketThread()); + + if (mDrivingTransaction) { + // requeue it I guess. This should be gone. + mDrivingTransaction->SetH2WSTransaction(nullptr); + Unused << gHttpHandler->InitiateTransaction( + mDrivingTransaction, mDrivingTransaction->Priority()); + } +} + +void SpdyConnectTransaction::ForcePlainText() { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + MOZ_ASSERT(!mInputDataUsed && !mInputDataSize && !mInputDataOffset); + MOZ_ASSERT(!mForcePlainText); + MOZ_ASSERT(!mTunnelTransport, "call before mapstreamtohttpconnection"); + MOZ_ASSERT(!mIsWebsocket); + + mForcePlainText = true; +} + +bool SpdyConnectTransaction::MapStreamToHttpConnection( + nsISocketTransport* aTransport, nsHttpConnectionInfo* aConnInfo, + const nsACString& aFlat407Headers, int32_t aHttpResponseCode) { + MOZ_ASSERT(OnSocketThread()); + + if (aHttpResponseCode >= 100 && aHttpResponseCode < 200) { + LOG( + ("SpdyConnectTransaction::MapStreamToHttpConnection %p skip " + "pre-response with response code %d", + this, aHttpResponseCode)); + return false; + } + + mTunnelTransport = new SocketTransportShim(this, aTransport, mIsWebsocket); + mTunnelStreamIn = new InputStreamShim(this, mIsWebsocket); + mTunnelStreamOut = new OutputStreamShim(this, mIsWebsocket); + mTunneledConn = new nsHttpConnection(); + + // If aHttpResponseCode is -1, it means that proxy connect is not used. We + // should not call HttpProxyResponseToErrorCode(), since this will create a + // shim error. + if (aHttpResponseCode > 0 && aHttpResponseCode != 200) { + nsresult err = HttpProxyResponseToErrorCode(aHttpResponseCode); + if (NS_FAILED(err)) { + CreateShimError(err); + } + } + + // this new http connection has a specific hashkey (i.e. to a particular + // host via the tunnel) and is associated with the tunnel streams + LOG(("SpdyConnectTransaction %p new httpconnection %p %s\n", this, + mTunneledConn.get(), aConnInfo->HashKey().get())); + + nsCOMPtr callbacks; + GetSecurityCallbacks(getter_AddRefs(callbacks)); + mTunneledConn->SetTransactionCaps(Caps()); + MOZ_ASSERT(aConnInfo->UsingHttpsProxy() || mIsWebsocket); + TimeDuration rtt = TimeStamp::Now() - mTimestampSyn; + DebugOnly rv = mTunneledConn->Init( + aConnInfo, gHttpHandler->ConnMgr()->MaxRequestDelay(), mTunnelTransport, + mTunnelStreamIn, mTunnelStreamOut, true, callbacks, + PR_MillisecondsToInterval(static_cast(rtt.ToMilliseconds()))); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + if (mForcePlainText) { + mTunneledConn->ForcePlainText(); + } else if (mIsWebsocket) { + LOG(("SpdyConnectTransaction::MapStreamToHttpConnection %p websocket", + this)); + // Let the root transaction know about us, so it can pass our own conn + // to the websocket. + mDrivingTransaction->SetH2WSTransaction(this); + } else { + mTunneledConn->SetupSecondaryTLS(this); + mTunneledConn->SetInSpdyTunnel(true); + } + + // make the originating transaction stick to the tunneled conn + RefPtr wrappedConn = + gHttpHandler->ConnMgr()->MakeConnectionHandle(mTunneledConn); + mDrivingTransaction->SetConnection(wrappedConn); + mDrivingTransaction->MakeSticky(); + + if (!mIsWebsocket) { + mDrivingTransaction->OnProxyConnectComplete(aHttpResponseCode); + + if (aHttpResponseCode == 407) { + mDrivingTransaction->SetFlat407Headers(aFlat407Headers); + mDrivingTransaction->SetProxyConnectFailed(); + } + + // jump the priority and start the dispatcher + Unused << gHttpHandler->InitiateTransaction( + mDrivingTransaction, nsISupportsPriority::PRIORITY_HIGHEST - 60); + mDrivingTransaction = nullptr; + } + + return true; +} + +nsresult SpdyConnectTransaction::Flush(uint32_t count, uint32_t* countRead) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG(("SpdyConnectTransaction::Flush %p count %d avail %d\n", this, count, + mOutputDataUsed - mOutputDataOffset)); + + if (!mSegmentReader) { + return NS_ERROR_UNEXPECTED; + } + + *countRead = 0; + count = std::min(count, (mOutputDataUsed - mOutputDataOffset)); + if (count) { + nsresult rv; + rv = mSegmentReader->OnReadSegment(&mOutputData[mOutputDataOffset], count, + countRead); + if (NS_FAILED(rv) && (rv != NS_BASE_STREAM_WOULD_BLOCK)) { + LOG(("SpdyConnectTransaction::Flush %p Error %" PRIx32 "\n", this, + static_cast(rv))); + CreateShimError(rv); + return rv; + } + } + + mOutputDataOffset += *countRead; + if (mOutputDataOffset == mOutputDataUsed) { + mOutputDataOffset = mOutputDataUsed = 0; + } + if (!(*countRead)) { + return NS_BASE_STREAM_WOULD_BLOCK; + } + + if (mOutputDataUsed != mOutputDataOffset) { + LOG(("SpdyConnectTransaction::Flush %p Incomplete %d\n", this, + mOutputDataUsed - mOutputDataOffset)); + mSession->TransactionHasDataToWrite(this); + } + + return NS_OK; +} + +nsresult SpdyConnectTransaction::ReadSegments(nsAHttpSegmentReader* reader, + uint32_t count, + uint32_t* countRead) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG(("SpdyConnectTransaction::ReadSegments %p count %d conn %p\n", this, + count, mTunneledConn.get())); + + mSegmentReader = reader; + + // spdy stream carrying tunnel is not setup yet. + if (!mTunneledConn) { + uint32_t toWrite = mConnectString.Length() - mConnectStringOffset; + toWrite = std::min(toWrite, count); + *countRead = toWrite; + if (toWrite) { + nsresult rv = mSegmentReader->OnReadSegment( + mConnectString.BeginReading() + mConnectStringOffset, toWrite, + countRead); + if (NS_FAILED(rv) && (rv != NS_BASE_STREAM_WOULD_BLOCK)) { + LOG( + ("SpdyConnectTransaction::ReadSegments %p OnReadSegmentError " + "%" PRIx32 "\n", + this, static_cast(rv))); + CreateShimError(rv); + } else { + mConnectStringOffset += toWrite; + if (mConnectString.Length() == mConnectStringOffset) { + mConnectString.Truncate(); + mConnectStringOffset = 0; + } + } + return rv; + } + + LOG(("SpdyConnectTransaciton::ReadSegments %p connect request consumed", + this)); + return NS_BASE_STREAM_WOULD_BLOCK; + } + + if (mForcePlainText) { + // this path just ignores sending the request so that we can + // send a synthetic reply in writesegments() + LOG( + ("SpdyConnectTransaciton::ReadSegments %p dropping %d output bytes " + "due to synthetic reply\n", + this, mOutputDataUsed - mOutputDataOffset)); + *countRead = mOutputDataUsed - mOutputDataOffset; + mOutputDataOffset = mOutputDataUsed = 0; + mTunneledConn->DontReuse(); + return NS_OK; + } + + *countRead = 0; + nsresult rv = Flush(count, countRead); + if (!(*countRead)) { + return NS_BASE_STREAM_WOULD_BLOCK; + } + + nsCOMPtr cb = mTunnelStreamOut->TakeCallback(); + if (!cb) { + return NS_BASE_STREAM_WOULD_BLOCK; + } + + // See if there is any more data available + rv = cb->OnOutputStreamReady(mTunnelStreamOut); + if (NS_FAILED(rv)) { + return rv; + } + + // Write out anything that may have come out of the stream just above + uint32_t subtotal; + count -= *countRead; + rv = Flush(count, &subtotal); + *countRead += subtotal; + + return rv; +} + +void SpdyConnectTransaction::CreateShimError(nsresult code) { + LOG(("SpdyConnectTransaction::CreateShimError %p 0x%08" PRIx32, this, + static_cast(code))); + + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + MOZ_ASSERT(NS_FAILED(code)); + + MOZ_ASSERT(!mCreateShimErrorCalled); + if (mCreateShimErrorCalled) { + return; + } + mCreateShimErrorCalled = true; + + if (mTunnelStreamOut && NS_SUCCEEDED(mTunnelStreamOut->mStatus)) { + mTunnelStreamOut->mStatus = code; + } + + if (mTunnelStreamIn && NS_SUCCEEDED(mTunnelStreamIn->mStatus)) { + mTunnelStreamIn->mStatus = code; + } + + if (mTunnelStreamIn) { + nsCOMPtr cb = mTunnelStreamIn->TakeCallback(); + if (cb) { + cb->OnInputStreamReady(mTunnelStreamIn); + } + } + + if (mTunnelStreamOut) { + nsCOMPtr cb = mTunnelStreamOut->TakeCallback(); + if (cb) { + cb->OnOutputStreamReady(mTunnelStreamOut); + } + } + mCreateShimErrorCalled = false; +} + +nsresult SpdyConnectTransaction::WriteDataToBuffer(nsAHttpSegmentWriter* writer, + uint32_t count, + uint32_t* countWritten) { + EnsureBuffer(mInputData, mInputDataUsed + count, mInputDataUsed, + mInputDataSize); + nsresult rv = + writer->OnWriteSegment(&mInputData[mInputDataUsed], count, countWritten); + if (NS_FAILED(rv)) { + if (rv != NS_BASE_STREAM_WOULD_BLOCK) { + LOG( + ("SpdyConnectTransaction::WriteSegments wrapped writer %p Error " + "%" PRIx32 "\n", + this, static_cast(rv))); + CreateShimError(rv); + } + return rv; + } + mInputDataUsed += *countWritten; + LOG( + ("SpdyConnectTransaction %p %d new bytes [%d total] of ciphered data " + "buffered\n", + this, *countWritten, mInputDataUsed - mInputDataOffset)); + + return rv; +} + +nsresult SpdyConnectTransaction::WriteSegments(nsAHttpSegmentWriter* writer, + uint32_t count, + uint32_t* countWritten) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG(("SpdyConnectTransaction::WriteSegments %p max=%d", this, count)); + + // For websockets, we need to forward the initial response through to the base + // transaction so the normal websocket plumbing can do all the things it needs + // to do. + if (mIsWebsocket) { + return WebsocketWriteSegments(writer, count, countWritten); + } + + // first call into the tunnel stream to get the demux'd data out of the + // spdy session. + nsresult rv = WriteDataToBuffer(writer, count, countWritten); + if (NS_FAILED(rv)) { + return rv; + } + + nsCOMPtr cb = + mTunneledConn ? mTunnelStreamIn->TakeCallback() : nullptr; + LOG(("SpdyConnectTransaction::WriteSegments %p cb=%p", this, cb.get())); + + if (!cb) { + return NS_BASE_STREAM_WOULD_BLOCK; + } + + rv = cb->OnInputStreamReady(mTunnelStreamIn); + LOG( + ("SpdyConnectTransaction::WriteSegments %p " + "after InputStreamReady callback %d total of ciphered data buffered " + "rv=%" PRIx32 "\n", + this, mInputDataUsed - mInputDataOffset, static_cast(rv))); + LOG( + ("SpdyConnectTransaction::WriteSegments %p " + "goodput %p out %" PRId64 "\n", + this, mTunneledConn.get(), mTunneledConn->ContentBytesWritten())); + if (NS_SUCCEEDED(rv) && !mTunneledConn->ContentBytesWritten()) { + nsCOMPtr ocb = mTunnelStreamOut->TakeCallback(); + mTunnelStreamOut->AsyncWait(ocb, 0, 0, nullptr); + } + return rv; +} + +nsresult SpdyConnectTransaction::WebsocketWriteSegments( + nsAHttpSegmentWriter* writer, uint32_t count, uint32_t* countWritten) { + MOZ_ASSERT(OnSocketThread()); + MOZ_ASSERT(mIsWebsocket); + LOG(("SpdyConnectTransaction::WebsocketWriteSegments %p max=%d", this, + count)); + + if (mDrivingTransaction && !mDrivingTransaction->IsDone()) { + // Transaction hasn't received end of headers yet, so keep passing data to + // it until it has. Then we can take over. + nsresult rv = + mDrivingTransaction->WriteSegments(writer, count, countWritten); + if (NS_SUCCEEDED(rv) && mDrivingTransaction->IsDone() && !mConnRefTaken) { + mDrivingTransaction->Close(NS_OK); + } + } + + if (!mConnRefTaken) { + // Force driving transaction to finish so the websocket channel can get its + // notifications correctly and start driving. + MOZ_ASSERT(mDrivingTransaction); + mDrivingTransaction->Close(NS_OK); + } + + nsresult rv = WriteDataToBuffer(writer, count, countWritten); + if (NS_SUCCEEDED(rv)) { + if (!mTunneledConn) { + return NS_BASE_STREAM_WOULD_BLOCK; + } + nsCOMPtr cb = mTunnelStreamIn->TakeCallback(); + if (!cb) { + return NS_BASE_STREAM_WOULD_BLOCK; + } + rv = cb->OnInputStreamReady(mTunnelStreamIn); + } + + return rv; +} + +bool SpdyConnectTransaction::ConnectedReadyForInput() { + return mTunneledConn && mTunnelStreamIn->HasCallback(); +} + +nsHttpRequestHead* SpdyConnectTransaction::RequestHead() { + return mRequestHead; +} + +void SpdyConnectTransaction::Close(nsresult code) { + LOG(("SpdyConnectTransaction close %p %" PRIx32 "\n", this, + static_cast(code))); + + MOZ_ASSERT(OnSocketThread()); + + if (mIsWebsocket && mDrivingTransaction) { + mDrivingTransaction->SetH2WSTransaction(nullptr); + if (!mConnRefTaken) { + // This indicates that the websocket failed to set up, so just close down + // the transaction as usual. + mDrivingTransaction->Close(code); + mDrivingTransaction = nullptr; + } + } + NullHttpTransaction::Close(code); + if (NS_FAILED(code) && (code != NS_BASE_STREAM_WOULD_BLOCK)) { + CreateShimError(code); + } else { + CreateShimError(NS_BASE_STREAM_CLOSED); + } +} + +void SpdyConnectTransaction::SetConnRefTaken() { + MOZ_ASSERT(OnSocketThread()); + + mConnRefTaken = true; + mDrivingTransaction = nullptr; // Just in case +} + +already_AddRefed OutputStreamShim::TakeCallback() { + mozilla::MutexAutoLock lock(mMutex); + return mCallback.forget(); +} + +class WebsocketHasDataToWrite final : public Runnable { + public: + explicit WebsocketHasDataToWrite(OutputStreamShim* shim) + : Runnable("WebsocketHasDataToWrite"), mShim(shim) {} + + ~WebsocketHasDataToWrite() = default; + + NS_IMETHOD Run() override { return mShim->CallTransactionHasDataToWrite(); } + + [[nodiscard]] nsresult Dispatch() { + if (OnSocketThread()) { + return Run(); + } + + nsCOMPtr sts = + do_GetService("@mozilla.org/network/socket-transport-service;1"); + return sts->Dispatch(this, nsIEventTarget::DISPATCH_NORMAL); + } + + private: + RefPtr mShim; +}; + +NS_IMETHODIMP +OutputStreamShim::AsyncWait(nsIOutputStreamCallback* callback, + unsigned int flags, unsigned int requestedCount, + nsIEventTarget* target) { + if (mIsWebsocket) { + // With websockets, AsyncWait may be called from the main thread, but the + // target is on the socket thread. That's all we really care about. + nsCOMPtr sts = + do_GetService(NS_SOCKETTRANSPORTSERVICE_CONTRACTID); + MOZ_ASSERT((!target && !callback) || (target == sts)); + if (target && (target != sts)) { + return NS_ERROR_FAILURE; + } + } else { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + bool currentThread; + + if (target && (NS_FAILED(target->IsOnCurrentThread(¤tThread)) || + !currentThread)) { + return NS_ERROR_FAILURE; + } + } + + LOG(("OutputStreamShim::AsyncWait %p callback %p\n", this, callback)); + + { + mozilla::MutexAutoLock lock(mMutex); + mCallback = callback; + } + + RefPtr wsdw = new WebsocketHasDataToWrite(this); + Unused << wsdw->Dispatch(); + + return NS_OK; +} + +class OutputCloseTransaction final : public Runnable { + public: + OutputCloseTransaction(OutputStreamShim* shim, nsresult reason) + : Runnable("OutputCloseTransaction"), mShim(shim), mReason(reason) {} + + ~OutputCloseTransaction() = default; + + NS_IMETHOD Run() override { return mShim->CloseTransaction(mReason); } + + private: + RefPtr mShim; + nsresult mReason; +}; + +NS_IMETHODIMP +OutputStreamShim::CloseWithStatus(nsresult reason) { + if (!OnSocketThread()) { + RefPtr oct = + new OutputCloseTransaction(this, reason); + nsCOMPtr sts = + do_GetService("@mozilla.org/network/socket-transport-service;1"); + return sts->Dispatch(oct, nsIEventTarget::DISPATCH_NORMAL); + } + + return CloseTransaction(reason); +} + +nsresult OutputStreamShim::CloseTransaction(nsresult reason) { + MOZ_ASSERT(OnSocketThread()); + RefPtr baseTrans = mWeakTrans->QueryTransaction(); + if (!baseTrans) { + return NS_ERROR_FAILURE; + } + SpdyConnectTransaction* trans = baseTrans->QuerySpdyConnectTransaction(); + MOZ_ASSERT(trans); + if (!trans) { + return NS_ERROR_UNEXPECTED; + } + + trans->mSession->CloseTransaction(trans, reason); + return NS_OK; +} + +NS_IMETHODIMP +OutputStreamShim::Close() { return CloseWithStatus(NS_OK); } + +NS_IMETHODIMP +OutputStreamShim::Flush() { + MOZ_ASSERT(OnSocketThread()); + RefPtr baseTrans = mWeakTrans->QueryTransaction(); + if (!baseTrans) { + return NS_ERROR_FAILURE; + } + SpdyConnectTransaction* trans = baseTrans->QuerySpdyConnectTransaction(); + MOZ_ASSERT(trans); + if (!trans) { + return NS_ERROR_UNEXPECTED; + } + + uint32_t count = trans->mOutputDataUsed - trans->mOutputDataOffset; + if (!count) { + return NS_OK; + } + + uint32_t countRead; + nsresult rv = trans->Flush(count, &countRead); + LOG(("OutputStreamShim::Flush %p before %d after %d\n", this, count, + trans->mOutputDataUsed - trans->mOutputDataOffset)); + return rv; +} + +nsresult OutputStreamShim::CallTransactionHasDataToWrite() { + MOZ_ASSERT(OnSocketThread()); + RefPtr baseTrans = mWeakTrans->QueryTransaction(); + if (!baseTrans) { + return NS_ERROR_FAILURE; + } + SpdyConnectTransaction* trans = baseTrans->QuerySpdyConnectTransaction(); + MOZ_ASSERT(trans); + if (!trans) { + return NS_ERROR_UNEXPECTED; + } + trans->mSession->TransactionHasDataToWrite(trans); + return NS_OK; +} + +NS_IMETHODIMP +OutputStreamShim::Write(const char* aBuf, uint32_t aCount, uint32_t* _retval) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + if (NS_FAILED(mStatus)) { + return mStatus; + } + + RefPtr baseTrans = mWeakTrans->QueryTransaction(); + if (!baseTrans) { + return NS_ERROR_FAILURE; + } + SpdyConnectTransaction* trans = baseTrans->QuerySpdyConnectTransaction(); + MOZ_ASSERT(trans); + if (!trans) { + return NS_ERROR_UNEXPECTED; + } + + if ((trans->mOutputDataUsed + aCount) >= 512000) { + *_retval = 0; + // time for some flow control; + return NS_BASE_STREAM_WOULD_BLOCK; + } + + EnsureBuffer(trans->mOutputData, trans->mOutputDataUsed + aCount, + trans->mOutputDataUsed, trans->mOutputDataSize); + memcpy(&trans->mOutputData[trans->mOutputDataUsed], aBuf, aCount); + trans->mOutputDataUsed += aCount; + *_retval = aCount; + LOG(("OutputStreamShim::Write %p new %d total %d\n", this, aCount, + trans->mOutputDataUsed)); + + trans->mSession->TransactionHasDataToWrite(trans); + + return NS_OK; +} + +NS_IMETHODIMP +OutputStreamShim::WriteFrom(nsIInputStream* aFromStream, uint32_t aCount, + uint32_t* _retval) { + if (mIsWebsocket) { + LOG3(("WARNING: OutputStreamShim::WriteFrom %p", this)); + } + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +OutputStreamShim::WriteSegments(nsReadSegmentFun aReader, void* aClosure, + uint32_t aCount, uint32_t* _retval) { + if (mIsWebsocket) { + LOG3(("WARNING: OutputStreamShim::WriteSegments %p", this)); + } + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +OutputStreamShim::IsNonBlocking(bool* _retval) { + *_retval = true; + return NS_OK; +} + +already_AddRefed InputStreamShim::TakeCallback() { + mozilla::MutexAutoLock lock(mMutex); + return mCallback.forget(); +} + +bool InputStreamShim::HasCallback() { + mozilla::MutexAutoLock lock(mMutex); + return mCallback != nullptr; +} + +class CheckAvailData final : public Runnable { + public: + explicit CheckAvailData(InputStreamShim* shim) + : Runnable("CheckAvailData"), mShim(shim) {} + + ~CheckAvailData() = default; + + NS_IMETHOD Run() override { + uint64_t avail = 0; + if (NS_SUCCEEDED(mShim->Available(&avail)) && avail) { + nsCOMPtr cb = mShim->TakeCallback(); + if (cb) { + cb->OnInputStreamReady(mShim); + } + } + return NS_OK; + } + + [[nodiscard]] nsresult Dispatch() { + // Dispatch the event even if we're on socket thread to avoid closing and + // destructing Http2Session in case this call is comming from + // Http2Session::ReadSegments() and the callback closes the transaction in + // OnInputStreamRead(). + nsCOMPtr sts = + do_GetService("@mozilla.org/network/socket-transport-service;1"); + return sts->Dispatch(this, nsIEventTarget::DISPATCH_NORMAL); + } + + private: + RefPtr mShim; +}; + +NS_IMETHODIMP +InputStreamShim::AsyncWait(nsIInputStreamCallback* callback, unsigned int flags, + unsigned int requestedCount, + nsIEventTarget* target) { + if (mIsWebsocket) { + // With websockets, AsyncWait may be called from the main thread, but the + // target is on the socket thread. That's all we really care about. + nsCOMPtr sts = + do_GetService(NS_SOCKETTRANSPORTSERVICE_CONTRACTID); + MOZ_ASSERT((!target && !callback) || (target == sts)); + if (target && (target != sts)) { + return NS_ERROR_FAILURE; + } + } else { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + bool currentThread; + + if (target && (NS_FAILED(target->IsOnCurrentThread(¤tThread)) || + !currentThread)) { + return NS_ERROR_FAILURE; + } + } + + LOG(("InputStreamShim::AsyncWait %p callback %p\n", this, callback)); + + { + mozilla::MutexAutoLock lock(mMutex); + mCallback = callback; + } + + if (callback) { + RefPtr cad = new CheckAvailData(this); + Unused << cad->Dispatch(); + } + + return NS_OK; +} + +class InputCloseTransaction final : public Runnable { + public: + InputCloseTransaction(InputStreamShim* shim, nsresult reason) + : Runnable("InputCloseTransaction"), mShim(shim), mReason(reason) {} + + ~InputCloseTransaction() = default; + + NS_IMETHOD Run() override { return mShim->CloseTransaction(mReason); } + + private: + RefPtr mShim; + nsresult mReason; +}; + +NS_IMETHODIMP +InputStreamShim::CloseWithStatus(nsresult reason) { + if (!OnSocketThread()) { + RefPtr ict = new InputCloseTransaction(this, reason); + nsCOMPtr sts = + do_GetService("@mozilla.org/network/socket-transport-service;1"); + return sts->Dispatch(ict, nsIEventTarget::DISPATCH_NORMAL); + } + + return CloseTransaction(reason); +} + +nsresult InputStreamShim::CloseTransaction(nsresult reason) { + MOZ_ASSERT(OnSocketThread()); + RefPtr baseTrans = mWeakTrans->QueryTransaction(); + if (!baseTrans) { + return NS_ERROR_FAILURE; + } + SpdyConnectTransaction* trans = baseTrans->QuerySpdyConnectTransaction(); + MOZ_ASSERT(trans); + if (!trans) { + return NS_ERROR_UNEXPECTED; + } + + trans->mSession->CloseTransaction(trans, reason); + return NS_OK; +} + +NS_IMETHODIMP +InputStreamShim::Close() { return CloseWithStatus(NS_OK); } + +NS_IMETHODIMP +InputStreamShim::Available(uint64_t* _retval) { + RefPtr baseTrans = mWeakTrans->QueryTransaction(); + if (!baseTrans) { + return NS_ERROR_FAILURE; + } + SpdyConnectTransaction* trans = baseTrans->QuerySpdyConnectTransaction(); + MOZ_ASSERT(trans); + if (!trans) { + return NS_ERROR_UNEXPECTED; + } + + *_retval = trans->mInputDataUsed - trans->mInputDataOffset; + return NS_OK; +} + +NS_IMETHODIMP +InputStreamShim::Read(char* aBuf, uint32_t aCount, uint32_t* _retval) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + if (NS_FAILED(mStatus)) { + return mStatus; + } + + RefPtr baseTrans = mWeakTrans->QueryTransaction(); + if (!baseTrans) { + return NS_ERROR_FAILURE; + } + SpdyConnectTransaction* trans = baseTrans->QuerySpdyConnectTransaction(); + MOZ_ASSERT(trans); + if (!trans) { + return NS_ERROR_UNEXPECTED; + } + + uint32_t avail = trans->mInputDataUsed - trans->mInputDataOffset; + uint32_t tocopy = std::min(aCount, avail); + *_retval = tocopy; + memcpy(aBuf, &trans->mInputData[trans->mInputDataOffset], tocopy); + trans->mInputDataOffset += tocopy; + if (trans->mInputDataOffset == trans->mInputDataUsed) { + trans->mInputDataOffset = trans->mInputDataUsed = 0; + } + + return tocopy ? NS_OK : NS_BASE_STREAM_WOULD_BLOCK; +} + +NS_IMETHODIMP +InputStreamShim::ReadSegments(nsWriteSegmentFun aWriter, void* aClosure, + uint32_t aCount, uint32_t* _retval) { + if (mIsWebsocket) { + LOG3(("WARNING: InputStreamShim::ReadSegments %p", this)); + } + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +InputStreamShim::IsNonBlocking(bool* _retval) { + *_retval = true; + return NS_OK; +} + +NS_IMETHODIMP +SocketTransportShim::SetKeepaliveEnabled(bool aKeepaliveEnabled) { + if (mIsWebsocket) { + LOG3(("WARNING: SocketTransportShim::SetKeepaliveEnabled %p called", this)); + } + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +SocketTransportShim::SetKeepaliveVals(int32_t keepaliveIdleTime, + int32_t keepaliveRetryInterval) { + if (mIsWebsocket) { + LOG3(("WARNING: SocketTransportShim::SetKeepaliveVals %p called", this)); + } + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +SocketTransportShim::GetSecurityCallbacks( + nsIInterfaceRequestor** aSecurityCallbacks) { + if (mIsWebsocket) { + nsCOMPtr out(mSecurityCallbacks); + *aSecurityCallbacks = out.forget().take(); + return NS_OK; + } + + return mWrapped->GetSecurityCallbacks(aSecurityCallbacks); +} + +NS_IMETHODIMP +SocketTransportShim::SetSecurityCallbacks( + nsIInterfaceRequestor* aSecurityCallbacks) { + if (mIsWebsocket) { + mSecurityCallbacks = aSecurityCallbacks; + return NS_OK; + } + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +SocketTransportShim::OpenInputStream(uint32_t aFlags, uint32_t aSegmentSize, + uint32_t aSegmentCount, + nsIInputStream** _retval) { + if (mIsWebsocket) { + LOG3(("WARNING: SocketTransportShim::OpenInputStream %p", this)); + } + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +SocketTransportShim::OpenOutputStream(uint32_t aFlags, uint32_t aSegmentSize, + uint32_t aSegmentCount, + nsIOutputStream** _retval) { + if (mIsWebsocket) { + LOG3(("WARNING: SocketTransportShim::OpenOutputStream %p", this)); + } + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +SocketTransportShim::Close(nsresult aReason) { + if (mIsWebsocket) { + LOG3(("WARNING: SocketTransportShim::Close %p", this)); + } else { + LOG(("SocketTransportShim::Close %p", this)); + } + + if (gHttpHandler->Bug1563538()) { + // Must always post, because mSession->CloseTransaction releases the + // Http2Stream which is still on stack. + RefPtr self(this); + + nsCOMPtr sts = + do_GetService("@mozilla.org/network/socket-transport-service;1"); + Unused << sts->Dispatch(NS_NewRunnableFunction( + "SocketTransportShim::Close", [self = std::move(self), aReason]() { + RefPtr baseTrans = + self->mWeakTrans->QueryTransaction(); + if (!baseTrans) { + return; + } + SpdyConnectTransaction* trans = + baseTrans->QuerySpdyConnectTransaction(); + MOZ_ASSERT(trans); + if (!trans) { + return; + } + + trans->mSession->CloseTransaction(trans, aReason); + })); + return NS_OK; + } + + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +SocketTransportShim::SetEventSink(nsITransportEventSink* aSink, + nsIEventTarget* aEventTarget) { + if (mIsWebsocket) { + // Need to pretend, since websockets expect this to work + return NS_OK; + } + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +SocketTransportShim::Bind(NetAddr* aLocalAddr) { + if (mIsWebsocket) { + LOG3(("WARNING: SocketTransportShim::Bind %p", this)); + } + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +SocketTransportShim::GetFirstRetryError(nsresult* aFirstRetryError) { + if (mIsWebsocket) { + LOG3(("WARNING: SocketTransportShim::GetFirstRetryError %p", this)); + } + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +SocketTransportShim::GetEchConfigUsed(bool* aEchConfigUsed) { + if (mIsWebsocket) { + LOG3(("WARNING: SocketTransportShim::GetEchConfigUsed %p", this)); + } + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +SocketTransportShim::SetEchConfig(const nsACString& aEchConfig) { + if (mIsWebsocket) { + LOG3(("WARNING: SocketTransportShim::SetEchConfig %p", this)); + } + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +SocketTransportShim::ResolvedByTRR(bool* aResolvedByTRR) { + if (mIsWebsocket) { + LOG3(("WARNING: SocketTransportShim::IsTRR %p", this)); + } + return NS_ERROR_NOT_IMPLEMENTED; +} + +#define FWD_TS_PTR(fx, ts) \ + NS_IMETHODIMP \ + SocketTransportShim::fx(ts* arg) { return mWrapped->fx(arg); } + +#define FWD_TS_ADDREF(fx, ts) \ + NS_IMETHODIMP \ + SocketTransportShim::fx(ts** arg) { return mWrapped->fx(arg); } + +#define FWD_TS(fx, ts) \ + NS_IMETHODIMP \ + SocketTransportShim::fx(ts arg) { return mWrapped->fx(arg); } + +FWD_TS_PTR(GetKeepaliveEnabled, bool); +FWD_TS_PTR(GetSendBufferSize, uint32_t); +FWD_TS(SetSendBufferSize, uint32_t); +FWD_TS_PTR(GetPort, int32_t); +FWD_TS_PTR(GetPeerAddr, mozilla::net::NetAddr); +FWD_TS_PTR(GetSelfAddr, mozilla::net::NetAddr); +FWD_TS_ADDREF(GetScriptablePeerAddr, nsINetAddr); +FWD_TS_ADDREF(GetScriptableSelfAddr, nsINetAddr); +FWD_TS_ADDREF(GetSecurityInfo, nsISupports); +FWD_TS_PTR(IsAlive, bool); +FWD_TS_PTR(GetConnectionFlags, uint32_t); +FWD_TS(SetConnectionFlags, uint32_t); +FWD_TS(SetIsPrivate, bool); +FWD_TS_PTR(GetTlsFlags, uint32_t); +FWD_TS(SetTlsFlags, uint32_t); +FWD_TS_PTR(GetRecvBufferSize, uint32_t); +FWD_TS(SetRecvBufferSize, uint32_t); +FWD_TS_PTR(GetResetIPFamilyPreference, bool); + +nsresult SocketTransportShim::GetOriginAttributes( + mozilla::OriginAttributes* aOriginAttributes) { + return mWrapped->GetOriginAttributes(aOriginAttributes); +} + +nsresult SocketTransportShim::SetOriginAttributes( + const mozilla::OriginAttributes& aOriginAttributes) { + return mWrapped->SetOriginAttributes(aOriginAttributes); +} + +NS_IMETHODIMP +SocketTransportShim::GetScriptableOriginAttributes( + JSContext* aCx, JS::MutableHandle aOriginAttributes) { + return mWrapped->GetScriptableOriginAttributes(aCx, aOriginAttributes); +} + +NS_IMETHODIMP +SocketTransportShim::SetScriptableOriginAttributes( + JSContext* aCx, JS::Handle aOriginAttributes) { + return mWrapped->SetScriptableOriginAttributes(aCx, aOriginAttributes); +} + +NS_IMETHODIMP +SocketTransportShim::GetHost(nsACString& aHost) { + return mWrapped->GetHost(aHost); +} + +NS_IMETHODIMP +SocketTransportShim::GetTimeout(uint32_t aType, uint32_t* _retval) { + return mWrapped->GetTimeout(aType, _retval); +} + +NS_IMETHODIMP +SocketTransportShim::SetTimeout(uint32_t aType, uint32_t aValue) { + return mWrapped->SetTimeout(aType, aValue); +} + +NS_IMETHODIMP +SocketTransportShim::SetReuseAddrPort(bool aReuseAddrPort) { + return mWrapped->SetReuseAddrPort(aReuseAddrPort); +} + +NS_IMETHODIMP +SocketTransportShim::SetLinger(bool aPolarity, int16_t aTimeout) { + return mWrapped->SetLinger(aPolarity, aTimeout); +} + +NS_IMETHODIMP +SocketTransportShim::GetQoSBits(uint8_t* aQoSBits) { + return mWrapped->GetQoSBits(aQoSBits); +} + +NS_IMETHODIMP +SocketTransportShim::SetQoSBits(uint8_t aQoSBits) { + return mWrapped->SetQoSBits(aQoSBits); +} + +NS_IMETHODIMP +SocketTransportShim::SetFastOpenCallback(TCPFastOpen* aFastOpen) { + return mWrapped->SetFastOpenCallback(aFastOpen); +} + +NS_IMPL_ISUPPORTS(TLSFilterTransaction, nsITimerCallback, nsINamed) +NS_IMPL_ISUPPORTS(SocketTransportShim, nsISocketTransport, nsITransport) +NS_IMPL_ISUPPORTS(InputStreamShim, nsIInputStream, nsIAsyncInputStream) +NS_IMPL_ISUPPORTS(OutputStreamShim, nsIOutputStream, nsIAsyncOutputStream) +NS_IMPL_ISUPPORTS(SocketInWrapper, nsIAsyncInputStream) +NS_IMPL_ISUPPORTS(SocketOutWrapper, nsIAsyncOutputStream) + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/TunnelUtils.h b/netwerk/protocol/http/TunnelUtils.h new file mode 100644 index 0000000000..834d0888c3 --- /dev/null +++ b/netwerk/protocol/http/TunnelUtils.h @@ -0,0 +1,302 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ + +/* 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/. */ + +#ifndef mozilla_net_TLSFilterTransaction_h +#define mozilla_net_TLSFilterTransaction_h + +#include "mozilla/Attributes.h" +#include "mozilla/UniquePtr.h" +#include "nsAHttpTransaction.h" +#include "nsIAsyncInputStream.h" +#include "nsIAsyncOutputStream.h" +#include "nsINamed.h" +#include "nsISocketTransport.h" +#include "nsITimer.h" +#include "NullHttpTransaction.h" +#include "mozilla/TimeStamp.h" +#include "prio.h" + +// a TLSFilterTransaction wraps another nsAHttpTransaction but +// applies a encode/decode filter of TLS onto the ReadSegments +// and WriteSegments data. It is not used for basic https:// +// but it is used for supplemental TLS tunnels - such as those +// needed by CONNECT tunnels in HTTP/2 or even CONNECT tunnels when +// the underlying proxy connection is already running TLS +// +// HTTP/2 CONNECT tunnels cannot use pushed IO layers because of +// the multiplexing involved on the base stream. i.e. the base stream +// once it is decrypted may have parts that are encrypted with a +// variety of keys, or none at all + +/* ************************************************************************ +The input path of http over a spdy CONNECT tunnel once it is established as a +stream + +note the "real http transaction" can be either a http/1 transaction or another +spdy session inside the tunnel. + + nsHttpConnection::OnInputStreamReady (real socket) + nsHttpConnection::OnSocketReadable() + SpdySession::WriteSegment() + SpdyStream::WriteSegment (tunnel stream) + SpdyConnectTransaction::WriteSegment + SpdyStream::OnWriteSegment(tunnel stream) + SpdySession::OnWriteSegment() + SpdySession::NetworkRead() + nsHttpConnection::OnWriteSegment (real socket) + realSocketIn->Read() return data from network + +now pop the stack back up to SpdyConnectTransaction::WriteSegment, the data +that has been read is stored mInputData + + SpdyConnectTransaction.mTunneledConn::OnInputStreamReady(mTunnelStreamIn) + SpdyConnectTransaction.mTunneledConn::OnSocketReadable() + TLSFilterTransaction::WriteSegment() + nsHttpTransaction::WriteSegment(real http transaction) + TLSFilterTransaction::OnWriteSegment() removes tls on way back up stack + SpdyConnectTransaction.mTunneledConn::OnWriteSegment() + // gets data from mInputData + SpdyConnectTransaction.mTunneledConn.mTunnelStreamIn->Read() + +The output path works similarly: + nsHttpConnection::OnOutputStreamReady (real socket) + nsHttpConnection::OnSocketWritable() + SpdySession::ReadSegments (locates tunnel) + SpdyStream::ReadSegments (tunnel stream) + SpdyConnectTransaction::ReadSegments() + SpdyConnectTransaction.mTunneledConn::OnOutputStreamReady (tunnel connection) + SpdyConnectTransaction.mTunneledConn::OnSocketWritable (tunnel connection) + TLSFilterTransaction::ReadSegment() + nsHttpTransaction::ReadSegment (real http transaction generates plaintext on + way down) + TLSFilterTransaction::OnReadSegment (BUF and LEN gets encrypted here on way + down) + SpdyConnectTransaction.mTunneledConn::OnReadSegment (BUF and LEN) + (tunnel connection) + SpdyConnectTransaction.mTunneledConn.mTunnelStreamOut->Write(BUF, LEN) .. + get stored in mOutputData + +Now pop the stack back up to SpdyConnectTransaction::ReadSegment(), where it has +the encrypted text available in mOutputData + + SpdyStream->OnReadSegment(BUF,LEN) from mOutputData. Tunnel stream + SpdySession->OnReadSegment() // encrypted data gets put in a data frame + nsHttpConnection->OnReadSegment() + realSocketOut->write() writes data to network + +**************************************************************************/ + +struct PRSocketOptionData; + +namespace mozilla { +namespace net { + +class nsHttpRequestHead; +class NullHttpTransaction; +class TLSFilterTransaction; + +class NudgeTunnelCallback : public nsISupports { + public: + virtual nsresult OnTunnelNudged(TLSFilterTransaction*) = 0; +}; + +#define NS_DECL_NUDGETUNNELCALLBACK \ + nsresult OnTunnelNudged(TLSFilterTransaction*) override; + +class TLSFilterTransaction final : public nsAHttpTransaction, + public nsAHttpSegmentReader, + public nsAHttpSegmentWriter, + public nsITimerCallback, + public nsINamed { + ~TLSFilterTransaction(); + + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSAHTTPTRANSACTION + NS_DECL_NSAHTTPSEGMENTREADER + NS_DECL_NSAHTTPSEGMENTWRITER + NS_DECL_NSITIMERCALLBACK + NS_DECL_NSINAMED + + TLSFilterTransaction(nsAHttpTransaction* aWrappedTransaction, + const char* tlsHost, int32_t tlsPort, + nsAHttpSegmentReader* reader, + nsAHttpSegmentWriter* writer); + + const nsAHttpTransaction* Transaction() const { return mTransaction.get(); } + [[nodiscard]] nsresult CommitToSegmentSize(uint32_t size, + bool forceCommitment) override; + [[nodiscard]] nsresult GetTransactionSecurityInfo(nsISupports**) override; + [[nodiscard]] nsresult NudgeTunnel(NudgeTunnelCallback* callback); + [[nodiscard]] nsresult SetProxiedTransaction( + nsAHttpTransaction* aTrans, + nsAHttpTransaction* aSpdyConnectTransaction = nullptr); + void newIODriver(nsIAsyncInputStream* aSocketIn, + nsIAsyncOutputStream* aSocketOut, + nsIAsyncInputStream** outSocketIn, + nsIAsyncOutputStream** outSocketOut); + + // nsAHttpTransaction overloads + bool IsNullTransaction() override; + NullHttpTransaction* QueryNullTransaction() override; + nsHttpTransaction* QueryHttpTransaction() override; + SpdyConnectTransaction* QuerySpdyConnectTransaction() override; + [[nodiscard]] nsresult WriteSegmentsAgain(nsAHttpSegmentWriter* writer, + uint32_t count, + uint32_t* countWritten, + bool* again) override; + + bool HasDataToRecv(); + + private: + [[nodiscard]] nsresult StartTimerCallback(); + void Cleanup(); + int32_t FilterOutput(const char* aBuf, int32_t aAmount); + int32_t FilterInput(char* aBuf, int32_t aAmount); + + static PRStatus GetPeerName(PRFileDesc* fd, PRNetAddr* addr); + static PRStatus GetSocketOption(PRFileDesc* fd, PRSocketOptionData* data); + static PRStatus SetSocketOption(PRFileDesc* fd, + const PRSocketOptionData* data); + static int32_t FilterWrite(PRFileDesc* fd, const void* buf, int32_t amount); + static int32_t FilterRead(PRFileDesc* fd, void* buf, int32_t amount); + static int32_t FilterSend(PRFileDesc* fd, const void* buf, int32_t amount, + int flags, PRIntervalTime timeout); + static int32_t FilterRecv(PRFileDesc* fd, void* buf, int32_t amount, + int flags, PRIntervalTime timeout); + static PRStatus FilterClose(PRFileDesc* fd); + + private: + RefPtr mTransaction; + nsWeakPtr mWeakTrans; // SpdyConnectTransaction * + nsCOMPtr mSecInfo; + nsCOMPtr mTimer; + RefPtr mNudgeCallback; + + // buffered network output, after encryption + UniquePtr mEncryptedText; + uint32_t mEncryptedTextUsed; + uint32_t mEncryptedTextSize; + + PRFileDesc* mFD; + nsAHttpSegmentReader* mSegmentReader; + nsAHttpSegmentWriter* mSegmentWriter; + + nsresult mFilterReadCode; + int32_t mFilterReadAmount; + // Set only when we are calling PR_Write from inside OnReadSegment. Prevents + // calling back to OnReadSegment from inside FilterOutput. + bool mInOnReadSegment; + bool mForce; + nsresult mReadSegmentReturnValue; + // Before Close() is called this is NS_ERROR_UNEXPECTED, in Close() we either + // take the reason, if it is a failure, or we change to + // NS_ERROR_BASE_STREAM_CLOSE. This is returned when Write/ReadSegments is + // called after Close, when we don't have mTransaction any more. + nsresult mCloseReason; + uint32_t mNudgeCounter; +}; + +class SocketTransportShim; +class InputStreamShim; +class OutputStreamShim; +class nsHttpConnection; + +class SpdyConnectTransaction final : public NullHttpTransaction { + public: + SpdyConnectTransaction(nsHttpConnectionInfo* ci, + nsIInterfaceRequestor* callbacks, uint32_t caps, + nsHttpTransaction* trans, nsAHttpConnection* session, + bool isWebsocket); + ~SpdyConnectTransaction(); + + SpdyConnectTransaction* QuerySpdyConnectTransaction() override { + return this; + } + + // A transaction is forced into plaintext when it is intended to be used as a + // CONNECT tunnel but the setup fails. The plaintext only carries the CONNECT + // error. + void ForcePlainText(); + // True if we successfully map stream to a nsHttpConnection. Currently we skip + // 1xx response only. + bool MapStreamToHttpConnection(nsISocketTransport* aTransport, + nsHttpConnectionInfo* aConnInfo, + const nsACString& aFlat407Headers, + int32_t aHttpResponseCode); + + [[nodiscard]] nsresult ReadSegments(nsAHttpSegmentReader* reader, + uint32_t count, + uint32_t* countRead) final; + [[nodiscard]] nsresult WriteSegments(nsAHttpSegmentWriter* writer, + uint32_t count, + uint32_t* countWritten) final; + nsHttpRequestHead* RequestHead() final; + void Close(nsresult reason) final; + + // ConnectedReadyForInput() tests whether the spdy connect transaction is + // attached to an nsHttpConnection that can properly deal with flow control, + // etc.. + bool ConnectedReadyForInput(); + + bool IsWebsocket() { return mIsWebsocket; } + void SetConnRefTaken(); + + private: + friend class SocketTransportShim; + friend class InputStreamShim; + friend class OutputStreamShim; + + [[nodiscard]] nsresult Flush(uint32_t count, uint32_t* countRead); + void CreateShimError(nsresult code); + + nsCString mConnectString; + uint32_t mConnectStringOffset; + + nsAHttpConnection* mSession; + nsAHttpSegmentReader* mSegmentReader; + + UniquePtr mInputData; + uint32_t mInputDataSize; + uint32_t mInputDataUsed; + uint32_t mInputDataOffset; + + UniquePtr mOutputData; + uint32_t mOutputDataSize; + uint32_t mOutputDataUsed; + uint32_t mOutputDataOffset; + + bool mForcePlainText; + TimeStamp mTimestampSyn; + + // mTunneledConn, mTunnelTransport, mTunnelStreamIn, mTunnelStreamOut + // are the connectors to the "real" http connection. They are created + // together when the tunnel setup is complete and a static reference is held + // for the lifetime of the tunnel. + RefPtr mTunneledConn; + RefPtr mTunnelTransport; + RefPtr mTunnelStreamIn; + RefPtr mTunnelStreamOut; + RefPtr mDrivingTransaction; + + // This is all for websocket support + bool mIsWebsocket; + bool mConnRefTaken; + nsCOMPtr mInputShimPipe; + nsCOMPtr mOutputShimPipe; + nsresult WriteDataToBuffer(nsAHttpSegmentWriter* writer, uint32_t count, + uint32_t* countWritten); + [[nodiscard]] nsresult WebsocketWriteSegments(nsAHttpSegmentWriter* writer, + uint32_t count, + uint32_t* countWritten); + + bool mCreateShimErrorCalled; +}; + +} // namespace net +} // namespace mozilla + +#endif // mozilla_net_TLSFilterTransaction_h diff --git a/netwerk/protocol/http/WellKnownOpportunisticUtils.jsm b/netwerk/protocol/http/WellKnownOpportunisticUtils.jsm new file mode 100644 index 0000000000..cf3c7e78ce --- /dev/null +++ b/netwerk/protocol/http/WellKnownOpportunisticUtils.jsm @@ -0,0 +1,30 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/. */ + +"use strict"; + +function WellKnownOpportunisticUtils() { + this.valid = false; + this.mixed = false; + this.lifetime = 0; +} + +WellKnownOpportunisticUtils.prototype = { + QueryInterface: ChromeUtils.generateQI(["nsIWellKnownOpportunisticUtils"]), + + verify(aJSON, aOrigin) { + try { + let arr = JSON.parse(aJSON.toLowerCase()); + if (!arr.includes(aOrigin.toLowerCase())) { + throw new Error("invalid origin"); + } + } catch (e) { + return; + } + this.valid = true; + }, +}; + +var EXPORTED_SYMBOLS = ["WellKnownOpportunisticUtils"]; diff --git a/netwerk/protocol/http/components.conf b/netwerk/protocol/http/components.conf new file mode 100644 index 0000000000..d7af273d16 --- /dev/null +++ b/netwerk/protocol/http/components.conf @@ -0,0 +1,14 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +Classes = [ + { + 'cid': '{b4f96c89-5238-450c-8bda-e12c26f1d150}', + 'contract_ids': ['@mozilla.org/network/well-known-opportunistic-utils;1'], + 'jsm': 'resource://gre/modules/WellKnownOpportunisticUtils.jsm', + 'constructor': 'WellKnownOpportunisticUtils', + }, +] diff --git a/netwerk/protocol/http/http2_huffman_table.txt b/netwerk/protocol/http/http2_huffman_table.txt new file mode 100644 index 0000000000..bfde068cd9 --- /dev/null +++ b/netwerk/protocol/http/http2_huffman_table.txt @@ -0,0 +1,257 @@ + ( 0) |11111111|11000 1ff8 [13] + ( 1) |11111111|11111111|1011000 7fffd8 [23] + ( 2) |11111111|11111111|11111110|0010 fffffe2 [28] + ( 3) |11111111|11111111|11111110|0011 fffffe3 [28] + ( 4) |11111111|11111111|11111110|0100 fffffe4 [28] + ( 5) |11111111|11111111|11111110|0101 fffffe5 [28] + ( 6) |11111111|11111111|11111110|0110 fffffe6 [28] + ( 7) |11111111|11111111|11111110|0111 fffffe7 [28] + ( 8) |11111111|11111111|11111110|1000 fffffe8 [28] + ( 9) |11111111|11111111|11101010 ffffea [24] + ( 10) |11111111|11111111|11111111|111100 3ffffffc [30] + ( 11) |11111111|11111111|11111110|1001 fffffe9 [28] + ( 12) |11111111|11111111|11111110|1010 fffffea [28] + ( 13) |11111111|11111111|11111111|111101 3ffffffd [30] + ( 14) |11111111|11111111|11111110|1011 fffffeb [28] + ( 15) |11111111|11111111|11111110|1100 fffffec [28] + ( 16) |11111111|11111111|11111110|1101 fffffed [28] + ( 17) |11111111|11111111|11111110|1110 fffffee [28] + ( 18) |11111111|11111111|11111110|1111 fffffef [28] + ( 19) |11111111|11111111|11111111|0000 ffffff0 [28] + ( 20) |11111111|11111111|11111111|0001 ffffff1 [28] + ( 21) |11111111|11111111|11111111|0010 ffffff2 [28] + ( 22) |11111111|11111111|11111111|111110 3ffffffe [30] + ( 23) |11111111|11111111|11111111|0011 ffffff3 [28] + ( 24) |11111111|11111111|11111111|0100 ffffff4 [28] + ( 25) |11111111|11111111|11111111|0101 ffffff5 [28] + ( 26) |11111111|11111111|11111111|0110 ffffff6 [28] + ( 27) |11111111|11111111|11111111|0111 ffffff7 [28] + ( 28) |11111111|11111111|11111111|1000 ffffff8 [28] + ( 29) |11111111|11111111|11111111|1001 ffffff9 [28] + ( 30) |11111111|11111111|11111111|1010 ffffffa [28] + ( 31) |11111111|11111111|11111111|1011 ffffffb [28] + ' ' ( 32) |010100 14 [ 6] + '!' ( 33) |11111110|00 3f8 [10] + '"' ( 34) |11111110|01 3f9 [10] + '#' ( 35) |11111111|1010 ffa [12] + '$' ( 36) |11111111|11001 1ff9 [13] + '%' ( 37) |010101 15 [ 6] + '&' ( 38) |11111000 f8 [ 8] + ''' ( 39) |11111111|010 7fa [11] + '(' ( 40) |11111110|10 3fa [10] + ')' ( 41) |11111110|11 3fb [10] + '*' ( 42) |11111001 f9 [ 8] + '+' ( 43) |11111111|011 7fb [11] + ',' ( 44) |11111010 fa [ 8] + '-' ( 45) |010110 16 [ 6] + '.' ( 46) |010111 17 [ 6] + '/' ( 47) |011000 18 [ 6] + '0' ( 48) |00000 0 [ 5] + '1' ( 49) |00001 1 [ 5] + '2' ( 50) |00010 2 [ 5] + '3' ( 51) |011001 19 [ 6] + '4' ( 52) |011010 1a [ 6] + '5' ( 53) |011011 1b [ 6] + '6' ( 54) |011100 1c [ 6] + '7' ( 55) |011101 1d [ 6] + '8' ( 56) |011110 1e [ 6] + '9' ( 57) |011111 1f [ 6] + ':' ( 58) |1011100 5c [ 7] + ';' ( 59) |11111011 fb [ 8] + '<' ( 60) |11111111|1111100 7ffc [15] + '=' ( 61) |100000 20 [ 6] + '>' ( 62) |11111111|1011 ffb [12] + '?' ( 63) |11111111|00 3fc [10] + '@' ( 64) |11111111|11010 1ffa [13] + 'A' ( 65) |100001 21 [ 6] + 'B' ( 66) |1011101 5d [ 7] + 'C' ( 67) |1011110 5e [ 7] + 'D' ( 68) |1011111 5f [ 7] + 'E' ( 69) |1100000 60 [ 7] + 'F' ( 70) |1100001 61 [ 7] + 'G' ( 71) |1100010 62 [ 7] + 'H' ( 72) |1100011 63 [ 7] + 'I' ( 73) |1100100 64 [ 7] + 'J' ( 74) |1100101 65 [ 7] + 'K' ( 75) |1100110 66 [ 7] + 'L' ( 76) |1100111 67 [ 7] + 'M' ( 77) |1101000 68 [ 7] + 'N' ( 78) |1101001 69 [ 7] + 'O' ( 79) |1101010 6a [ 7] + 'P' ( 80) |1101011 6b [ 7] + 'Q' ( 81) |1101100 6c [ 7] + 'R' ( 82) |1101101 6d [ 7] + 'S' ( 83) |1101110 6e [ 7] + 'T' ( 84) |1101111 6f [ 7] + 'U' ( 85) |1110000 70 [ 7] + 'V' ( 86) |1110001 71 [ 7] + 'W' ( 87) |1110010 72 [ 7] + 'X' ( 88) |11111100 fc [ 8] + 'Y' ( 89) |1110011 73 [ 7] + 'Z' ( 90) |11111101 fd [ 8] + '[' ( 91) |11111111|11011 1ffb [13] + '\' ( 92) |11111111|11111110|000 7fff0 [19] + ']' ( 93) |11111111|11100 1ffc [13] + '^' ( 94) |11111111|111100 3ffc [14] + '_' ( 95) |100010 22 [ 6] + '`' ( 96) |11111111|1111101 7ffd [15] + 'a' ( 97) |00011 3 [ 5] + 'b' ( 98) |100011 23 [ 6] + 'c' ( 99) |00100 4 [ 5] + 'd' (100) |100100 24 [ 6] + 'e' (101) |00101 5 [ 5] + 'f' (102) |100101 25 [ 6] + 'g' (103) |100110 26 [ 6] + 'h' (104) |100111 27 [ 6] + 'i' (105) |00110 6 [ 5] + 'j' (106) |1110100 74 [ 7] + 'k' (107) |1110101 75 [ 7] + 'l' (108) |101000 28 [ 6] + 'm' (109) |101001 29 [ 6] + 'n' (110) |101010 2a [ 6] + 'o' (111) |00111 7 [ 5] + 'p' (112) |101011 2b [ 6] + 'q' (113) |1110110 76 [ 7] + 'r' (114) |101100 2c [ 6] + 's' (115) |01000 8 [ 5] + 't' (116) |01001 9 [ 5] + 'u' (117) |101101 2d [ 6] + 'v' (118) |1110111 77 [ 7] + 'w' (119) |1111000 78 [ 7] + 'x' (120) |1111001 79 [ 7] + 'y' (121) |1111010 7a [ 7] + 'z' (122) |1111011 7b [ 7] + '{' (123) |11111111|1111110 7ffe [15] + '|' (124) |11111111|100 7fc [11] + '}' (125) |11111111|111101 3ffd [14] + '~' (126) |11111111|11101 1ffd [13] + (127) |11111111|11111111|11111111|1100 ffffffc [28] + (128) |11111111|11111110|0110 fffe6 [20] + (129) |11111111|11111111|010010 3fffd2 [22] + (130) |11111111|11111110|0111 fffe7 [20] + (131) |11111111|11111110|1000 fffe8 [20] + (132) |11111111|11111111|010011 3fffd3 [22] + (133) |11111111|11111111|010100 3fffd4 [22] + (134) |11111111|11111111|010101 3fffd5 [22] + (135) |11111111|11111111|1011001 7fffd9 [23] + (136) |11111111|11111111|010110 3fffd6 [22] + (137) |11111111|11111111|1011010 7fffda [23] + (138) |11111111|11111111|1011011 7fffdb [23] + (139) |11111111|11111111|1011100 7fffdc [23] + (140) |11111111|11111111|1011101 7fffdd [23] + (141) |11111111|11111111|1011110 7fffde [23] + (142) |11111111|11111111|11101011 ffffeb [24] + (143) |11111111|11111111|1011111 7fffdf [23] + (144) |11111111|11111111|11101100 ffffec [24] + (145) |11111111|11111111|11101101 ffffed [24] + (146) |11111111|11111111|010111 3fffd7 [22] + (147) |11111111|11111111|1100000 7fffe0 [23] + (148) |11111111|11111111|11101110 ffffee [24] + (149) |11111111|11111111|1100001 7fffe1 [23] + (150) |11111111|11111111|1100010 7fffe2 [23] + (151) |11111111|11111111|1100011 7fffe3 [23] + (152) |11111111|11111111|1100100 7fffe4 [23] + (153) |11111111|11111110|11100 1fffdc [21] + (154) |11111111|11111111|011000 3fffd8 [22] + (155) |11111111|11111111|1100101 7fffe5 [23] + (156) |11111111|11111111|011001 3fffd9 [22] + (157) |11111111|11111111|1100110 7fffe6 [23] + (158) |11111111|11111111|1100111 7fffe7 [23] + (159) |11111111|11111111|11101111 ffffef [24] + (160) |11111111|11111111|011010 3fffda [22] + (161) |11111111|11111110|11101 1fffdd [21] + (162) |11111111|11111110|1001 fffe9 [20] + (163) |11111111|11111111|011011 3fffdb [22] + (164) |11111111|11111111|011100 3fffdc [22] + (165) |11111111|11111111|1101000 7fffe8 [23] + (166) |11111111|11111111|1101001 7fffe9 [23] + (167) |11111111|11111110|11110 1fffde [21] + (168) |11111111|11111111|1101010 7fffea [23] + (169) |11111111|11111111|011101 3fffdd [22] + (170) |11111111|11111111|011110 3fffde [22] + (171) |11111111|11111111|11110000 fffff0 [24] + (172) |11111111|11111110|11111 1fffdf [21] + (173) |11111111|11111111|011111 3fffdf [22] + (174) |11111111|11111111|1101011 7fffeb [23] + (175) |11111111|11111111|1101100 7fffec [23] + (176) |11111111|11111111|00000 1fffe0 [21] + (177) |11111111|11111111|00001 1fffe1 [21] + (178) |11111111|11111111|100000 3fffe0 [22] + (179) |11111111|11111111|00010 1fffe2 [21] + (180) |11111111|11111111|1101101 7fffed [23] + (181) |11111111|11111111|100001 3fffe1 [22] + (182) |11111111|11111111|1101110 7fffee [23] + (183) |11111111|11111111|1101111 7fffef [23] + (184) |11111111|11111110|1010 fffea [20] + (185) |11111111|11111111|100010 3fffe2 [22] + (186) |11111111|11111111|100011 3fffe3 [22] + (187) |11111111|11111111|100100 3fffe4 [22] + (188) |11111111|11111111|1110000 7ffff0 [23] + (189) |11111111|11111111|100101 3fffe5 [22] + (190) |11111111|11111111|100110 3fffe6 [22] + (191) |11111111|11111111|1110001 7ffff1 [23] + (192) |11111111|11111111|11111000|00 3ffffe0 [26] + (193) |11111111|11111111|11111000|01 3ffffe1 [26] + (194) |11111111|11111110|1011 fffeb [20] + (195) |11111111|11111110|001 7fff1 [19] + (196) |11111111|11111111|100111 3fffe7 [22] + (197) |11111111|11111111|1110010 7ffff2 [23] + (198) |11111111|11111111|101000 3fffe8 [22] + (199) |11111111|11111111|11110110|0 1ffffec [25] + (200) |11111111|11111111|11111000|10 3ffffe2 [26] + (201) |11111111|11111111|11111000|11 3ffffe3 [26] + (202) |11111111|11111111|11111001|00 3ffffe4 [26] + (203) |11111111|11111111|11111011|110 7ffffde [27] + (204) |11111111|11111111|11111011|111 7ffffdf [27] + (205) |11111111|11111111|11111001|01 3ffffe5 [26] + (206) |11111111|11111111|11110001 fffff1 [24] + (207) |11111111|11111111|11110110|1 1ffffed [25] + (208) |11111111|11111110|010 7fff2 [19] + (209) |11111111|11111111|00011 1fffe3 [21] + (210) |11111111|11111111|11111001|10 3ffffe6 [26] + (211) |11111111|11111111|11111100|000 7ffffe0 [27] + (212) |11111111|11111111|11111100|001 7ffffe1 [27] + (213) |11111111|11111111|11111001|11 3ffffe7 [26] + (214) |11111111|11111111|11111100|010 7ffffe2 [27] + (215) |11111111|11111111|11110010 fffff2 [24] + (216) |11111111|11111111|00100 1fffe4 [21] + (217) |11111111|11111111|00101 1fffe5 [21] + (218) |11111111|11111111|11111010|00 3ffffe8 [26] + (219) |11111111|11111111|11111010|01 3ffffe9 [26] + (220) |11111111|11111111|11111111|1101 ffffffd [28] + (221) |11111111|11111111|11111100|011 7ffffe3 [27] + (222) |11111111|11111111|11111100|100 7ffffe4 [27] + (223) |11111111|11111111|11111100|101 7ffffe5 [27] + (224) |11111111|11111110|1100 fffec [20] + (225) |11111111|11111111|11110011 fffff3 [24] + (226) |11111111|11111110|1101 fffed [20] + (227) |11111111|11111111|00110 1fffe6 [21] + (228) |11111111|11111111|101001 3fffe9 [22] + (229) |11111111|11111111|00111 1fffe7 [21] + (230) |11111111|11111111|01000 1fffe8 [21] + (231) |11111111|11111111|1110011 7ffff3 [23] + (232) |11111111|11111111|101010 3fffea [22] + (233) |11111111|11111111|101011 3fffeb [22] + (234) |11111111|11111111|11110111|0 1ffffee [25] + (235) |11111111|11111111|11110111|1 1ffffef [25] + (236) |11111111|11111111|11110100 fffff4 [24] + (237) |11111111|11111111|11110101 fffff5 [24] + (238) |11111111|11111111|11111010|10 3ffffea [26] + (239) |11111111|11111111|1110100 7ffff4 [23] + (240) |11111111|11111111|11111010|11 3ffffeb [26] + (241) |11111111|11111111|11111100|110 7ffffe6 [27] + (242) |11111111|11111111|11111011|00 3ffffec [26] + (243) |11111111|11111111|11111011|01 3ffffed [26] + (244) |11111111|11111111|11111100|111 7ffffe7 [27] + (245) |11111111|11111111|11111101|000 7ffffe8 [27] + (246) |11111111|11111111|11111101|001 7ffffe9 [27] + (247) |11111111|11111111|11111101|010 7ffffea [27] + (248) |11111111|11111111|11111101|011 7ffffeb [27] + (249) |11111111|11111111|11111111|1110 ffffffe [28] + (250) |11111111|11111111|11111101|100 7ffffec [27] + (251) |11111111|11111111|11111101|101 7ffffed [27] + (252) |11111111|11111111|11111101|110 7ffffee [27] + (253) |11111111|11111111|11111101|111 7ffffef [27] + (254) |11111111|11111111|11111110|000 7fffff0 [27] + (255) |11111111|11111111|11111011|10 3ffffee [26] + EOS (256) |11111111|11111111|11111111|111111 3fffffff [30] diff --git a/netwerk/protocol/http/make_incoming_tables.py b/netwerk/protocol/http/make_incoming_tables.py new file mode 100644 index 0000000000..be917a6f0b --- /dev/null +++ b/netwerk/protocol/http/make_incoming_tables.py @@ -0,0 +1,202 @@ +# This script exists to auto-generate Http2HuffmanIncoming.h from the table +# contained in the HPACK spec. It's pretty simple to run: +# python make_incoming_tables.py < http2_huffman_table.txt > Http2HuffmanIncoming.h +# where huff_incoming.txt is copy/pasted text from the latest version of the +# HPACK spec, with all non-relevant lines removed (the most recent version +# of huff_incoming.txt also lives in this directory as an example). +import sys + + +def char_cmp(x, y): + rv = cmp(x["nbits"], y["nbits"]) + if not rv: + rv = cmp(x["bpat"], y["bpat"]) + if not rv: + rv = cmp(x["ascii"], y["ascii"]) + return rv + + +characters = [] + +for line in sys.stdin: + line = line.rstrip() + obracket = line.rfind("[") + nbits = int(line[obracket + 1 : -1]) + + oparen = line.find(" (") + ascii = int(line[oparen + 2 : oparen + 5].strip()) + + bar = line.find("|", oparen) + space = line.find(" ", bar) + bpat = line[bar + 1 : space].strip().rstrip("|") + + characters.append({"ascii": ascii, "nbits": nbits, "bpat": bpat}) + +characters.sort(cmp=char_cmp) +raw_entries = [] +for c in characters: + raw_entries.append((c["ascii"], c["bpat"])) + + +class DefaultList(list): + def __init__(self, default=None): + self.__default = default + + def __ensure_size(self, sz): + while sz > len(self): + self.append(self.__default) + + def __getitem__(self, idx): + self.__ensure_size(idx + 1) + rv = super(DefaultList, self).__getitem__(idx) + return rv + + def __setitem__(self, idx, val): + self.__ensure_size(idx + 1) + super(DefaultList, self).__setitem__(idx, val) + + +def expand_to_8bit(bstr): + while len(bstr) < 8: + bstr += "0" + return int(bstr, 2) + + +table = DefaultList() +for r in raw_entries: + ascii, bpat = r + ascii = int(ascii) + bstrs = bpat.split("|") + curr_table = table + while len(bstrs) > 1: + idx = expand_to_8bit(bstrs[0]) + if curr_table[idx] is None: + curr_table[idx] = DefaultList() + curr_table = curr_table[idx] + bstrs.pop(0) + + idx = expand_to_8bit(bstrs[0]) + curr_table[idx] = { + "prefix_len": len(bstrs[0]), + "mask": int(bstrs[0], 2), + "value": ascii, + } + + +def output_table(table, name_suffix=""): + max_prefix_len = 0 + for i, t in enumerate(table): + if isinstance(t, dict): + if t["prefix_len"] > max_prefix_len: + max_prefix_len = t["prefix_len"] + elif t is not None: + output_table(t, "%s_%s" % (name_suffix, i)) + + tablename = "HuffmanIncoming%s" % (name_suffix if name_suffix else "Root",) + entriestable = tablename.replace("HuffmanIncoming", "HuffmanIncomingEntries") + nexttable = tablename.replace("HuffmanIncoming", "HuffmanIncomingNextTables") + sys.stdout.write("static const HuffmanIncomingEntry %s[] = {\n" % (entriestable,)) + prefix_len = 0 + value = 0 + i = 0 + while i < 256: + t = table[i] + if isinstance(t, dict): + value = t["value"] + prefix_len = t["prefix_len"] + elif t is not None: + break + + sys.stdout.write(" { %s, %s }" % (value, prefix_len)) + sys.stdout.write(",\n") + i += 1 + + indexOfFirstNextTable = i + if i < 256: + sys.stdout.write("};\n") + sys.stdout.write("\n") + sys.stdout.write("static const HuffmanIncomingTable* %s[] = {\n" % (nexttable,)) + while i < 256: + subtable = "%s_%s" % (name_suffix, i) + ptr = "&HuffmanIncoming%s" % (subtable,) + sys.stdout.write(" %s" % (ptr)) + sys.stdout.write(",\n") + i += 1 + else: + nexttable = "nullptr" + + sys.stdout.write("};\n") + sys.stdout.write("\n") + sys.stdout.write("static const HuffmanIncomingTable %s = {\n" % (tablename,)) + sys.stdout.write(" %s,\n" % (entriestable,)) + sys.stdout.write(" %s,\n" % (nexttable,)) + sys.stdout.write(" %s,\n" % (indexOfFirstNextTable,)) + sys.stdout.write(" %s\n" % (max_prefix_len,)) + sys.stdout.write("};\n") + sys.stdout.write("\n") + + +sys.stdout.write( + """/* + * THIS FILE IS AUTO-GENERATED. DO NOT EDIT! + */ +#ifndef mozilla__net__Http2HuffmanIncoming_h +#define mozilla__net__Http2HuffmanIncoming_h + +namespace mozilla { +namespace net { + +struct HuffmanIncomingTable; + +struct HuffmanIncomingEntry { + uint16_t mValue:9; // 9 bits so it can hold 0..256 + uint16_t mPrefixLen:7; // only holds 1..8 +}; + +// The data members are public only so they can be statically constructed. All +// accesses should be done through the functions. +struct HuffmanIncomingTable { + // The normal entries, for indices in the range 0..(mNumEntries-1). + const HuffmanIncomingEntry* const mEntries; + + // The next tables, for indices in the range mNumEntries..255. Must be + // |nullptr| if mIndexOfFirstNextTable is 256. + const HuffmanIncomingTable** const mNextTables; + + // The index of the first next table (equal to the number of entries in + // mEntries). This cannot be a uint8_t because it can have the value 256, + // in which case there are no next tables and mNextTables must be |nullptr|. + const uint16_t mIndexOfFirstNextTable; + + const uint8_t mPrefixLen; + + bool IndexHasANextTable(uint8_t aIndex) const + { + return aIndex >= mIndexOfFirstNextTable; + } + + const HuffmanIncomingEntry* Entry(uint8_t aIndex) const + { + MOZ_ASSERT(aIndex < mIndexOfFirstNextTable); + return &mEntries[aIndex]; + } + + const HuffmanIncomingTable* NextTable(uint8_t aIndex) const + { + MOZ_ASSERT(aIndex >= mIndexOfFirstNextTable); + return mNextTables[aIndex - mIndexOfFirstNextTable]; + } +}; + +""" +) + +output_table(table) + +sys.stdout.write( + """} // namespace net +} // namespace mozilla + +#endif // mozilla__net__Http2HuffmanIncoming_h +""" +) diff --git a/netwerk/protocol/http/make_outgoing_tables.py b/netwerk/protocol/http/make_outgoing_tables.py new file mode 100644 index 0000000000..b9a4268202 --- /dev/null +++ b/netwerk/protocol/http/make_outgoing_tables.py @@ -0,0 +1,58 @@ +# This script exists to auto-generate Http2HuffmanOutgoing.h from the table +# contained in the HPACK spec. It's pretty simple to run: +# python make_outgoing_tables.py < http2_huffman_table.txt > Http2HuffmanOutgoing.h +# where huff_outgoing.txt is copy/pasted text from the latest version of the +# HPACK spec, with all non-relevant lines removed (the most recent version +# of huff_outgoing.txt also lives in this directory as an example). +import sys + +sys.stdout.write( + """/* + * THIS FILE IS AUTO-GENERATED. DO NOT EDIT! + */ +#ifndef mozilla__net__Http2HuffmanOutgoing_h +#define mozilla__net__Http2HuffmanOutgoing_h + +namespace mozilla { +namespace net { + +struct HuffmanOutgoingEntry { + uint32_t mValue; + uint8_t mLength; +}; + +static const HuffmanOutgoingEntry HuffmanOutgoing[] = { +""" +) + +entries = [] +for line in sys.stdin: + line = line.strip() + obracket = line.rfind("[") + nbits = int(line[obracket + 1 : -1]) + + lastbar = line.rfind("|") + space = line.find(" ", lastbar) + encend = line.rfind(" ", 0, obracket) + + enc = line[space:encend].strip() + val = int(enc, 16) + + entries.append({"length": nbits, "value": val}) + +line = [] +for i, e in enumerate(entries): + sys.stdout.write(" { 0x%08x, %s }" % (e["value"], e["length"])) + if i < (len(entries) - 1): + sys.stdout.write(",") + sys.stdout.write("\n") + +sys.stdout.write( + """}; + +} // namespace net +} // namespace mozilla + +#endif // mozilla__net__Http2HuffmanOutgoing_h +""" +) diff --git a/netwerk/protocol/http/moz.build b/netwerk/protocol/http/moz.build new file mode 100644 index 0000000000..056feb224f --- /dev/null +++ b/netwerk/protocol/http/moz.build @@ -0,0 +1,199 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +with Files("**"): + BUG_COMPONENT = ("Core", "Networking: HTTP") + +XPIDL_SOURCES += [ + "nsIBackgroundChannelRegistrar.idl", + "nsIHttpActivityObserver.idl", + "nsIHttpAuthenticableChannel.idl", + "nsIHttpAuthenticator.idl", + "nsIHttpAuthManager.idl", + "nsIHttpChannel.idl", + "nsIHttpChannelAuthProvider.idl", + "nsIHttpChannelChild.idl", + "nsIHttpChannelInternal.idl", + "nsIHttpHeaderVisitor.idl", + "nsIHttpProtocolHandler.idl", + "nsIRaceCacheWithNetwork.idl", + "nsIWellKnownOpportunisticUtils.idl", +] + +XPIDL_MODULE = "necko_http" + +EXPORTS += [ + "nsCORSListenerProxy.h", + "nsHttp.h", + "nsHttpAtomList.h", + "nsHttpHeaderArray.h", + "nsHttpRequestHead.h", + "nsHttpResponseHead.h", +] + +EXPORTS.mozilla.net += [ + "AltDataOutputStreamChild.h", + "AltDataOutputStreamParent.h", + "AltServiceChild.h", + "AltServiceParent.h", + "AltSvcTransactionChild.h", + "AltSvcTransactionParent.h", + "BackgroundChannelRegistrar.h", + "BackgroundDataBridgeChild.h", + "BackgroundDataBridgeParent.h", + "ClassifierDummyChannel.h", + "ClassifierDummyChannelChild.h", + "ClassifierDummyChannelParent.h", + "HttpAuthUtils.h", + "HttpBackgroundChannelChild.h", + "HttpBackgroundChannelParent.h", + "HttpBaseChannel.h", + "HttpChannelChild.h", + "HttpChannelParent.h", + "HttpConnectionMgrChild.h", + "HttpConnectionMgrParent.h", + "HttpConnectionMgrShell.h", + "HttpInfo.h", + "HttpTransactionChild.h", + "HttpTransactionParent.h", + "HttpTransactionShell.h", + "nsAHttpTransaction.h", + "nsServerTiming.h", + "NullHttpChannel.h", + "NullHttpTransaction.h", + "ParentChannelListener.h", + "PHttpChannelParams.h", + "PSpdyPush.h", + "SpeculativeTransaction.h", + "TimingStruct.h", +] + +SOURCES += [ + "nsHttpChannelAuthProvider.cpp", # redefines GetAuthType +] + +UNIFIED_SOURCES += [ + "AltDataOutputStreamChild.cpp", + "AltDataOutputStreamParent.cpp", + "AlternateServices.cpp", + "AltServiceChild.cpp", + "AltServiceParent.cpp", + "AltSvcTransactionChild.cpp", + "AltSvcTransactionParent.cpp", + "ASpdySession.cpp", + "BackgroundChannelRegistrar.cpp", + "BackgroundDataBridgeChild.cpp", + "BackgroundDataBridgeParent.cpp", + "CacheControlParser.cpp", + "CachePushChecker.cpp", + "ClassifierDummyChannel.cpp", + "ClassifierDummyChannelChild.cpp", + "ClassifierDummyChannelParent.cpp", + "ConnectionDiagnostics.cpp", + "ConnectionEntry.cpp", + "ConnectionHandle.cpp", + "DelayHttpChannelQueue.cpp", + "HalfOpenSocket.cpp", + "Http2Compression.cpp", + "Http2Push.cpp", + "Http2Session.cpp", + "Http2Stream.cpp", + "Http3Session.cpp", + "Http3Stream.cpp", + "HttpAuthUtils.cpp", + "HttpBackgroundChannelChild.cpp", + "HttpBackgroundChannelParent.cpp", + "HttpBaseChannel.cpp", + "HttpChannelChild.cpp", + "HttpChannelParent.cpp", + "HttpConnectionBase.cpp", + "HttpConnectionMgrChild.cpp", + "HttpConnectionMgrParent.cpp", + "HttpConnectionUDP.cpp", + "HttpInfo.cpp", + "HTTPSRecordResolver.cpp", + "HttpTrafficAnalyzer.cpp", + "HttpTransactionChild.cpp", + "HttpTransactionParent.cpp", + "InterceptedChannel.cpp", + "InterceptedHttpChannel.cpp", + "nsCORSListenerProxy.cpp", + "nsHttp.cpp", + "nsHttpActivityDistributor.cpp", + "nsHttpAuthCache.cpp", + "nsHttpAuthManager.cpp", + "nsHttpBasicAuth.cpp", + "nsHttpChannel.cpp", + "nsHttpChunkedDecoder.cpp", + "nsHttpConnection.cpp", + "nsHttpConnectionInfo.cpp", + "nsHttpConnectionMgr.cpp", + "nsHttpDigestAuth.cpp", + "nsHttpHeaderArray.cpp", + "nsHttpNTLMAuth.cpp", + "nsHttpRequestHead.cpp", + "nsHttpResponseHead.cpp", + "nsHttpTransaction.cpp", + "nsServerTiming.cpp", + "NullHttpChannel.cpp", + "NullHttpTransaction.cpp", + "ParentChannelListener.cpp", + "PendingTransactionInfo.cpp", + "PendingTransactionQueue.cpp", + "QuicSocketControl.cpp", + "SpeculativeTransaction.cpp", + "TRRServiceChannel.cpp", + "TunnelUtils.cpp", +] + +# These files cannot be built in unified mode because of OS X headers. +SOURCES += [ + "nsHttpHandler.cpp", +] + +IPDL_SOURCES += [ + "HttpChannelParams.ipdlh", + "PAltDataOutputStream.ipdl", + "PAltService.ipdl", + "PAltSvcTransaction.ipdl", + "PBackgroundDataBridge.ipdl", + "PClassifierDummyChannel.ipdl", + "PHttpBackgroundChannel.ipdl", + "PHttpChannel.ipdl", + "PHttpConnectionMgr.ipdl", + "PHttpTransaction.ipdl", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" + +LOCAL_INCLUDES += [ + "/dom/base", + "/extensions/auth", + "/netwerk/base", + "/netwerk/cookie", + "/netwerk/dns", + "/netwerk/ipc", + "/netwerk/socket/neqo_glue", + "/netwerk/url-classifier", +] + +EXTRA_JS_MODULES += [ + "WellKnownOpportunisticUtils.jsm", +] + +XPCOM_MANIFESTS += [ + "components.conf", +] + +if CONFIG["OS_TARGET"] == "Darwin": + if not CONFIG["HOST_MAJOR_VERSION"]: + DEFINES["HAS_CONNECTX"] = True + elif CONFIG["HOST_MAJOR_VERSION"] >= "15": + DEFINES["HAS_CONNECTX"] = True + +include("/tools/fuzzing/libfuzzer-config.mozbuild") diff --git a/netwerk/protocol/http/nsAHttpConnection.h b/netwerk/protocol/http/nsAHttpConnection.h new file mode 100644 index 0000000000..49ca1f8d95 --- /dev/null +++ b/netwerk/protocol/http/nsAHttpConnection.h @@ -0,0 +1,248 @@ +/* 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/. */ + +#ifndef nsAHttpConnection_h__ +#define nsAHttpConnection_h__ + +#include "nsHttp.h" +#include "nsISupports.h" +#include "nsAHttpTransaction.h" +#include "HttpTrafficAnalyzer.h" + +class nsISocketTransport; +class nsIAsyncInputStream; +class nsIAsyncOutputStream; + +namespace mozilla { +namespace net { + +class nsHttpConnectionInfo; +class HttpConnectionBase; +class nsHttpRequestHead; +class nsHttpResponseHead; + +//----------------------------------------------------------------------------- +// Abstract base class for a HTTP connection +//----------------------------------------------------------------------------- + +// 5a66aed7-eede-468b-ac2b-e5fb431fcc5c +#define NS_AHTTPCONNECTION_IID \ + { \ + 0x5a66aed7, 0xeede, 0x468b, { \ + 0xac, 0x2b, 0xe5, 0xfb, 0x43, 0x1f, 0xcc, 0x5c \ + } \ + } + +class nsAHttpConnection : public nsISupports { + public: + NS_DECLARE_STATIC_IID_ACCESSOR(NS_AHTTPCONNECTION_IID) + + //------------------------------------------------------------------------- + // NOTE: these methods may only be called on the socket thread. + //------------------------------------------------------------------------- + + // + // called by a transaction when the response headers have all been read. + // the connection can force the transaction to reset it's response headers, + // and prepare for a new set of response headers, by setting |*reset=TRUE|. + // + // @return failure code to close the transaction. + // + [[nodiscard]] virtual nsresult OnHeadersAvailable(nsAHttpTransaction*, + nsHttpRequestHead*, + nsHttpResponseHead*, + bool* reset) = 0; + + // + // called by a transaction to resume either sending or receiving data + // after a transaction returned NS_BASE_STREAM_WOULD_BLOCK from its + // ReadSegments/WriteSegments methods. + // + [[nodiscard]] virtual nsresult ResumeSend() = 0; + [[nodiscard]] virtual nsresult ResumeRecv() = 0; + + // called by a transaction to force a "send/recv from network" iteration + // even if not scheduled by socket associated with connection + [[nodiscard]] virtual nsresult ForceSend() = 0; + [[nodiscard]] virtual nsresult ForceRecv() = 0; + + // After a connection has had ResumeSend() called by a transaction, + // and it is ready to write to the network it may need to know the + // transaction that has data to write. This is only an issue for + // multiplexed protocols like SPDY - h1 + // implicitly has this information in a 1:1 relationship with the + // transaction(s) they manage. + virtual void TransactionHasDataToWrite(nsAHttpTransaction*) { + // by default do nothing - only multiplexed protocols need to overload + } + + // This is the companion to *HasDataToWrite() for the case + // when a gecko caller has called ResumeRecv() after being paused + virtual void TransactionHasDataToRecv(nsAHttpTransaction*) { + // by default do nothing - only multiplexed protocols need to overload + } + + // called by the connection manager to close a transaction being processed + // by this connection. + // + // @param transaction + // the transaction being closed. + // @param reason + // the reason for closing the transaction. NS_BASE_STREAM_CLOSED + // is equivalent to NS_OK. + // + virtual void CloseTransaction(nsAHttpTransaction* transaction, + nsresult reason) = 0; + + // get a reference to the connection's connection info object. + virtual void GetConnectionInfo(nsHttpConnectionInfo**) = 0; + + // get the transport level information for this connection. This may fail + // if it is in use. + [[nodiscard]] virtual nsresult TakeTransport(nsISocketTransport**, + nsIAsyncInputStream**, + nsIAsyncOutputStream**) = 0; + + // called by a transaction to get the security info from the socket. + virtual void GetSecurityInfo(nsISupports**) = 0; + + // called by a transaction to determine whether or not the connection is + // persistent... important in determining the end of a response. + virtual bool IsPersistent() = 0; + + // called to determine or set if a connection has been reused. + virtual bool IsReused() = 0; + virtual void DontReuse() = 0; + + // called by a transaction when the transaction reads more from the socket + // than it should have (eg. containing part of the next response). + [[nodiscard]] virtual nsresult PushBack(const char* data, + uint32_t length) = 0; + + // Used to determine if the connection wants read events even though + // it has not written out a transaction. Used when a connection has issued + // a preamble such as a proxy ssl CONNECT sequence. + virtual bool IsProxyConnectInProgress() = 0; + + // Used by a transaction to manage the state of previous response bodies on + // the same connection and work around buggy servers. + virtual bool LastTransactionExpectedNoContent() = 0; + virtual void SetLastTransactionExpectedNoContent(bool) = 0; + + // Transfer the base http connection object along with a + // reference to it to the caller. + virtual already_AddRefed TakeHttpConnection() = 0; + + // Like TakeHttpConnection() but do not drop our own ref + virtual already_AddRefed HttpConnection() = 0; + + // Get the nsISocketTransport used by the connection without changing + // references or ownership. + virtual nsISocketTransport* Transport() = 0; + + // The number of transaction bytes written out on this HTTP Connection, does + // not count CONNECT tunnel setup + virtual int64_t BytesWritten() = 0; + + // Update the callbacks used to provide security info. May be called on + // any thread. + virtual void SetSecurityCallbacks(nsIInterfaceRequestor* aCallbacks) = 0; + + // nsHttp.h version + virtual HttpVersion Version() = 0; + + // A notification of the current active tab id change. + virtual void TopLevelOuterContentWindowIdChanged(uint64_t windowId) = 0; + + // categories set by nsHttpTransaction to identify how this connection is + // being used. + virtual void SetTrafficCategory(HttpTrafficCategory) = 0; +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(nsAHttpConnection, NS_AHTTPCONNECTION_IID) + +#define NS_DECL_NSAHTTPCONNECTION(fwdObject) \ + [[nodiscard]] nsresult OnHeadersAvailable( \ + nsAHttpTransaction*, nsHttpRequestHead*, nsHttpResponseHead*, \ + bool* reset) override; \ + void CloseTransaction(nsAHttpTransaction*, nsresult) override; \ + [[nodiscard]] nsresult TakeTransport( \ + nsISocketTransport**, nsIAsyncInputStream**, nsIAsyncOutputStream**) \ + override; \ + bool IsPersistent() override; \ + bool IsReused() override; \ + void DontReuse() override; \ + [[nodiscard]] nsresult PushBack(const char*, uint32_t) override; \ + already_AddRefed TakeHttpConnection() override; \ + already_AddRefed HttpConnection() override; \ + void TopLevelOuterContentWindowIdChanged(uint64_t windowId) override; \ + /* \ + Thes methods below have automatic definitions that just forward the \ + function to a lower level connection object \ + */ \ + void GetConnectionInfo(nsHttpConnectionInfo** result) override { \ + if (!(fwdObject)) { \ + *result = nullptr; \ + return; \ + } \ + return (fwdObject)->GetConnectionInfo(result); \ + } \ + void GetSecurityInfo(nsISupports** result) override { \ + if (!(fwdObject)) { \ + *result = nullptr; \ + return; \ + } \ + return (fwdObject)->GetSecurityInfo(result); \ + } \ + [[nodiscard]] nsresult ResumeSend() override { \ + if (!(fwdObject)) return NS_ERROR_FAILURE; \ + return (fwdObject)->ResumeSend(); \ + } \ + [[nodiscard]] nsresult ResumeRecv() override { \ + if (!(fwdObject)) return NS_ERROR_FAILURE; \ + return (fwdObject)->ResumeRecv(); \ + } \ + [[nodiscard]] nsresult ForceSend() override { \ + if (!(fwdObject)) return NS_ERROR_FAILURE; \ + return (fwdObject)->ForceSend(); \ + } \ + [[nodiscard]] nsresult ForceRecv() override { \ + if (!(fwdObject)) return NS_ERROR_FAILURE; \ + return (fwdObject)->ForceRecv(); \ + } \ + nsISocketTransport* Transport() override { \ + if (!(fwdObject)) return nullptr; \ + return (fwdObject)->Transport(); \ + } \ + HttpVersion Version() override { \ + return (fwdObject) ? (fwdObject)->Version() \ + : mozilla::net::HttpVersion::UNKNOWN; \ + } \ + bool IsProxyConnectInProgress() override { \ + return (!fwdObject) ? false : (fwdObject)->IsProxyConnectInProgress(); \ + } \ + bool LastTransactionExpectedNoContent() override { \ + return (!fwdObject) ? false \ + : (fwdObject)->LastTransactionExpectedNoContent(); \ + } \ + void SetLastTransactionExpectedNoContent(bool val) override { \ + if (fwdObject) (fwdObject)->SetLastTransactionExpectedNoContent(val); \ + } \ + int64_t BytesWritten() override { \ + return fwdObject ? (fwdObject)->BytesWritten() : 0; \ + } \ + void SetSecurityCallbacks(nsIInterfaceRequestor* aCallbacks) override { \ + if (fwdObject) (fwdObject)->SetSecurityCallbacks(aCallbacks); \ + } \ + void SetTrafficCategory(HttpTrafficCategory aCategory) override { \ + if (fwdObject) (fwdObject)->SetTrafficCategory(aCategory); \ + } + +// ThrottleResponse deliberately ommited since we want different implementation +// for h1 and h2 connections. + +} // namespace net +} // namespace mozilla + +#endif // nsAHttpConnection_h__ diff --git a/netwerk/protocol/http/nsAHttpTransaction.h b/netwerk/protocol/http/nsAHttpTransaction.h new file mode 100644 index 0000000000..36992e2b98 --- /dev/null +++ b/netwerk/protocol/http/nsAHttpTransaction.h @@ -0,0 +1,307 @@ +/* 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/. */ + +#ifndef nsAHttpTransaction_h__ +#define nsAHttpTransaction_h__ + +#include "nsISupports.h" +#include "nsTArray.h" +#include "nsWeakReference.h" + +#ifdef Status +/* Xlib headers insist on this for some reason... Nuke it because + it'll override our member name */ +typedef Status __StatusTmp; +# undef Status +typedef __StatusTmp Status; +#endif + +class nsIDNSHTTPSSVCRecord; +class nsIInterfaceRequestor; +class nsISVCBRecord; +class nsITransport; +class nsIRequestContext; + +namespace mozilla { +namespace net { + +class nsAHttpConnection; +class nsAHttpSegmentReader; +class nsAHttpSegmentWriter; +class nsHttpTransaction; +class nsHttpRequestHead; +class nsHttpConnectionInfo; +class NullHttpTransaction; +class SpdyConnectTransaction; + +//---------------------------------------------------------------------------- +// Abstract base class for a HTTP transaction: +// +// A transaction is a "sink" for the response data. The connection pushes +// data to the transaction by writing to it. The transaction supports +// WriteSegments and may refuse to accept data if its buffers are full (its +// write function returns NS_BASE_STREAM_WOULD_BLOCK in this case). +//---------------------------------------------------------------------------- + +// 2af6d634-13e3-494c-8903-c9dce5c22fc0 +#define NS_AHTTPTRANSACTION_IID \ + { \ + 0x2af6d634, 0x13e3, 0x494c, { \ + 0x89, 0x03, 0xc9, 0xdc, 0xe5, 0xc2, 0x2f, 0xc0 \ + } \ + } + +class nsAHttpTransaction : public nsSupportsWeakReference { + public: + NS_DECLARE_STATIC_IID_ACCESSOR(NS_AHTTPTRANSACTION_IID) + + // called by the connection when it takes ownership of the transaction. + virtual void SetConnection(nsAHttpConnection*) = 0; + + // called by the connection after a successfull activation of this transaction + // in other words, tells the transaction it transitioned to the "active" + // state. + virtual void OnActivated() {} + + // used to obtain the connection associated with this transaction + virtual nsAHttpConnection* Connection() = 0; + + // called by the connection to get security callbacks to set on the + // socket transport. + virtual void GetSecurityCallbacks(nsIInterfaceRequestor**) = 0; + + // called to report socket status (see nsITransportEventSink) + virtual void OnTransportStatus(nsITransport* transport, nsresult status, + int64_t progress) = 0; + + // called to check the transaction status. + virtual bool IsDone() = 0; + virtual nsresult Status() = 0; + virtual uint32_t Caps() = 0; + + // called to read request data from the transaction. + [[nodiscard]] virtual nsresult ReadSegments(nsAHttpSegmentReader* reader, + uint32_t count, + uint32_t* countRead) = 0; + + // called to write response data to the transaction. + [[nodiscard]] virtual nsresult WriteSegments(nsAHttpSegmentWriter* writer, + uint32_t count, + uint32_t* countWritten) = 0; + + // These versions of the functions allow the overloader to specify whether or + // not it is safe to call *Segments() in a loop while they return OK. + // The callee should turn again to false if it is not, otherwise leave + // untouched + [[nodiscard]] virtual nsresult ReadSegmentsAgain(nsAHttpSegmentReader* reader, + uint32_t count, + uint32_t* countRead, + bool* again) { + return ReadSegments(reader, count, countRead); + } + [[nodiscard]] virtual nsresult WriteSegmentsAgain( + nsAHttpSegmentWriter* writer, uint32_t count, uint32_t* countWritten, + bool* again) { + return WriteSegments(writer, count, countWritten); + } + + // called to close the transaction + virtual void Close(nsresult reason) = 0; + + // called to indicate a failure with proxy CONNECT + virtual void SetProxyConnectFailed() = 0; + + // called to retrieve the request headers of the transaction + virtual nsHttpRequestHead* RequestHead() = 0; + + // determine the number of real http/1.x transactions on this + // abstract object. Pipelines had multiple, SPDY has 0, + // normal http transactions have 1. + virtual uint32_t Http1xTransactionCount() = 0; + + // called to remove the unused sub transactions from an object that can + // handle multiple transactions simultaneously (i.e. h2). + // + // Returns NS_ERROR_NOT_IMPLEMENTED if the object does not implement + // sub-transactions. + // + // Returns NS_ERROR_ALREADY_OPENED if the subtransactions have been + // at least partially written and cannot be moved. + // + [[nodiscard]] virtual nsresult TakeSubTransactions( + nsTArray >& outTransactions) = 0; + + // Occasionally the abstract interface has to give way to base implementations + // to respect differences between spdy, h2, etc.. + // These Query* (and IsNullTransaction()) functions provide a way to do + // that without using xpcom or rtti. Any calling code that can't deal with + // a null response from one of them probably shouldn't be using + // nsAHttpTransaction + + // equivalent to !!dynamic_cast(this) + // A null transaction is expected to return BASE_STREAM_CLOSED on all of + // its IO functions all the time. + virtual bool IsNullTransaction() { return false; } + virtual NullHttpTransaction* QueryNullTransaction() { return nullptr; } + + // If we used rtti this would be the result of doing + // dynamic_cast(this).. i.e. it can be nullptr for + // non nsHttpTransaction implementations of nsAHttpTransaction + virtual nsHttpTransaction* QueryHttpTransaction() { return nullptr; } + + // If we used rtti this would be the result of doing + // dynamic_cast(this).. i.e. it can be nullptr for + // other types + virtual SpdyConnectTransaction* QuerySpdyConnectTransaction() { + return nullptr; + } + + // return the request context associated with the transaction + virtual nsIRequestContext* RequestContext() { return nullptr; } + + // return the connection information associated with the transaction + virtual nsHttpConnectionInfo* ConnectionInfo() = 0; + + // The base definition of these is done in nsHttpTransaction.cpp + virtual bool ResponseTimeoutEnabled() const; + virtual PRIntervalTime ResponseTimeout(); + + // conceptually the security info is part of the connection, but sometimes + // in the case of TLS tunneled within TLS the transaction might present + // a more specific security info that cannot be represented as a layer in + // the connection due to multiplexing. This interface represents such an + // overload. If it returns NS_FAILURE the connection should be considered + // authoritative. + [[nodiscard]] virtual nsresult GetTransactionSecurityInfo(nsISupports**) { + return NS_ERROR_NOT_IMPLEMENTED; + } + + virtual void DisableSpdy() {} + virtual void DisableHttp3() {} + virtual void MakeNonSticky() {} + virtual void ReuseConnectionOnRestartOK(bool) {} + + // We call this function if we want to use alt-svc host again on the next + // restart. If this function is not called on the next restart the + // transaction will use the original route. + // For example in case we receive a GOAWAY frame from a server, we can + // restart and use the same alt-svc. If we get VersionFallback we do not + // want to use the alt-svc on the restart. + virtual void DoNotRemoveAltSvc() {} + + // Returns true if early-data or fast open is possible. + [[nodiscard]] virtual bool CanDo0RTT() { return false; } + // Returns true if early-data is possible and transaction will remember + // that it is in 0RTT mode (to know should it rewide transaction or not + // in the case of an error). + [[nodiscard]] virtual bool Do0RTT() { return false; } + // This function will be called when a tls handshake has been finished and + // we know whether early-data that was sent has been accepted or not, e.g. + // do we need to restart a transaction. This will be called only if Do0RTT + // returns true. + // If aRestart parameter is true we need to restart the transaction, + // otherwise the erly-data has been accepted and we can continue the + // transaction. + // If aAlpnChanged is true (and we were assuming http/2), we'll need to take + // the transactions out of the session, rewind them all, and start them back + // over as http/1 transactions + // The function will return success or failure of the transaction restart. + [[nodiscard]] virtual nsresult Finish0RTT(bool aRestart, bool aAlpnChanged) { + return NS_ERROR_NOT_IMPLEMENTED; + } + + [[nodiscard]] virtual nsresult RestartOnFastOpenError() { + return NS_ERROR_NOT_IMPLEMENTED; + } + + virtual uint64_t TopLevelOuterContentWindowId() { + MOZ_ASSERT(false); + return 0; + } + + virtual void SetFastOpenStatus(uint8_t aStatus) {} + + virtual void OnProxyConnectComplete(int32_t aResponseCode) {} + + virtual nsresult FetchHTTPSRR() { return NS_ERROR_NOT_IMPLEMENTED; } + virtual nsresult OnHTTPSRRAvailable(nsIDNSHTTPSSVCRecord* aHTTPSSVCRecord, + nsISVCBRecord* aHighestPriorityRecord) { + return NS_ERROR_NOT_IMPLEMENTED; + } +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(nsAHttpTransaction, NS_AHTTPTRANSACTION_IID) + +#define NS_DECL_NSAHTTPTRANSACTION \ + void SetConnection(nsAHttpConnection*) override; \ + nsAHttpConnection* Connection() override; \ + void GetSecurityCallbacks(nsIInterfaceRequestor**) override; \ + void OnTransportStatus(nsITransport* transport, nsresult status, \ + int64_t progress) override; \ + bool IsDone() override; \ + nsresult Status() override; \ + uint32_t Caps() override; \ + [[nodiscard]] virtual nsresult ReadSegments(nsAHttpSegmentReader*, uint32_t, \ + uint32_t*) override; \ + [[nodiscard]] virtual nsresult WriteSegments(nsAHttpSegmentWriter*, \ + uint32_t, uint32_t*) override; \ + virtual void Close(nsresult reason) override; \ + nsHttpConnectionInfo* ConnectionInfo() override; \ + void SetProxyConnectFailed() override; \ + virtual nsHttpRequestHead* RequestHead() override; \ + uint32_t Http1xTransactionCount() override; \ + [[nodiscard]] nsresult TakeSubTransactions( \ + nsTArray >& outTransactions) override; + +//----------------------------------------------------------------------------- +// nsAHttpSegmentReader +//----------------------------------------------------------------------------- + +class nsAHttpSegmentReader { + NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING + + public: + // any returned failure code stops segment iteration + [[nodiscard]] virtual nsresult OnReadSegment(const char* segment, + uint32_t count, + uint32_t* countRead) = 0; + + // Ask the segment reader to commit to accepting size bytes of + // data from subsequent OnReadSegment() calls or throw hard + // (i.e. not wouldblock) exceptions. Implementations + // can return NS_ERROR_FAILURE if they never make commitments of that size + // (the default), NS_OK if they make the commitment, or + // NS_BASE_STREAM_WOULD_BLOCK if they cannot make the + // commitment now but might in the future and forceCommitment is not true . + // (forceCommitment requires a hard failure or OK at this moment.) + // + // SpdySession uses this to make sure frames are atomic. + [[nodiscard]] virtual nsresult CommitToSegmentSize(uint32_t size, + bool forceCommitment) { + return NS_ERROR_FAILURE; + } +}; + +#define NS_DECL_NSAHTTPSEGMENTREADER \ + [[nodiscard]] nsresult OnReadSegment(const char*, uint32_t, uint32_t*) \ + override; + +//----------------------------------------------------------------------------- +// nsAHttpSegmentWriter +//----------------------------------------------------------------------------- + +class nsAHttpSegmentWriter { + public: + // any returned failure code stops segment iteration + [[nodiscard]] virtual nsresult OnWriteSegment(char* segment, uint32_t count, + uint32_t* countWritten) = 0; +}; + +#define NS_DECL_NSAHTTPSEGMENTWRITER \ + [[nodiscard]] nsresult OnWriteSegment(char*, uint32_t, uint32_t*) override; + +} // namespace net +} // namespace mozilla + +#endif // nsAHttpTransaction_h__ diff --git a/netwerk/protocol/http/nsCORSListenerProxy.cpp b/netwerk/protocol/http/nsCORSListenerProxy.cpp new file mode 100644 index 0000000000..2912bacee2 --- /dev/null +++ b/netwerk/protocol/http/nsCORSListenerProxy.cpp @@ -0,0 +1,1599 @@ +/* -*- 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/Assertions.h" +#include "mozilla/LinkedList.h" +#include "mozilla/StaticPrefs_content.h" +#include "mozilla/StoragePrincipalHelper.h" + +#include "nsCORSListenerProxy.h" +#include "nsIChannel.h" +#include "nsIHttpChannel.h" +#include "HttpChannelChild.h" +#include "nsIHttpChannelInternal.h" +#include "nsError.h" +#include "nsContentUtils.h" +#include "nsNetUtil.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsServiceManagerUtils.h" +#include "nsMimeTypes.h" +#include "nsStringStream.h" +#include "nsGkAtoms.h" +#include "nsWhitespaceTokenizer.h" +#include "nsIChannelEventSink.h" +#include "nsIAsyncVerifyRedirectCallback.h" +#include "nsCharSeparatedTokenizer.h" +#include "nsAsyncRedirectVerifyHelper.h" +#include "nsClassHashtable.h" +#include "nsHashKeys.h" +#include "nsStreamUtils.h" +#include "mozilla/Preferences.h" +#include "nsIScriptError.h" +#include "nsILoadGroup.h" +#include "nsILoadContext.h" +#include "nsIConsoleService.h" +#include "nsINetworkInterceptController.h" +#include "nsICorsPreflightCallback.h" +#include "nsISupportsImpl.h" +#include "nsHttpChannel.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/LoadInfo.h" +#include "mozilla/NullPrincipal.h" +#include "nsIHttpHeaderVisitor.h" +#include "nsQueryObject.h" +#include "mozilla/StaticPrefs_network.h" +#include "mozilla/StaticPrefs_dom.h" +#include "mozilla/dom/nsHTTPSOnlyUtils.h" +#include + +using namespace mozilla; +using namespace mozilla::net; + +#define PREFLIGHT_CACHE_SIZE 100 +// 5 seconds is chosen to be compatible with Chromium. +#define PREFLIGHT_DEFAULT_EXPIRY_SECONDS 5 + +static void LogBlockedRequest(nsIRequest* aRequest, const char* aProperty, + const char16_t* aParam, uint32_t aBlockingReason, + nsIHttpChannel* aCreatingChannel) { + nsresult rv = NS_OK; + + nsCOMPtr channel = do_QueryInterface(aRequest); + + NS_SetRequestBlockingReason(channel, aBlockingReason); + + nsCOMPtr aUri; + channel->GetURI(getter_AddRefs(aUri)); + nsAutoCString spec; + if (aUri) { + spec = aUri->GetSpecOrDefault(); + } + + // Generate the error message + nsAutoString blockedMessage; + AutoTArray params; + CopyUTF8toUTF16(spec, *params.AppendElement()); + if (aParam) { + params.AppendElement(aParam); + } + NS_ConvertUTF8toUTF16 specUTF16(spec); + rv = nsContentUtils::FormatLocalizedString( + nsContentUtils::eSECURITY_PROPERTIES, aProperty, params, blockedMessage); + + if (NS_FAILED(rv)) { + NS_WARNING("Failed to log blocked cross-site request (no formalizedStr"); + return; + } + + nsAutoString msg(blockedMessage.get()); + nsDependentCString category(aProperty); + + if (XRE_IsParentProcess()) { + if (aCreatingChannel) { + rv = aCreatingChannel->LogBlockedCORSRequest(msg, category); + if (NS_SUCCEEDED(rv)) { + return; + } + } + NS_WARNING( + "Failed to log blocked cross-site request to web console from " + "parent->child, falling back to browser console"); + } + + bool privateBrowsing = false; + if (aRequest) { + nsCOMPtr loadGroup; + rv = aRequest->GetLoadGroup(getter_AddRefs(loadGroup)); + NS_ENSURE_SUCCESS_VOID(rv); + privateBrowsing = nsContentUtils::IsInPrivateBrowsing(loadGroup); + } + + bool fromChromeContext = false; + if (channel) { + nsCOMPtr loadInfo = channel->LoadInfo(); + fromChromeContext = loadInfo->TriggeringPrincipal()->IsSystemPrincipal(); + } + + // we are passing aProperty as the category so we can link to the + // appropriate MDN docs depending on the specific error. + uint64_t innerWindowID = nsContentUtils::GetInnerWindowID(aRequest); + // The |innerWindowID| could be 0 if this request is created from script. + // We can always try top level content window id in this case, + // since the window id can lead to current top level window's web console. + if (!innerWindowID) { + nsCOMPtr httpChannel = do_QueryInterface(aRequest); + if (httpChannel) { + Unused << httpChannel->GetTopLevelContentWindowId(&innerWindowID); + } + } + nsCORSListenerProxy::LogBlockedCORSRequest(innerWindowID, privateBrowsing, + fromChromeContext, msg, category); +} + +////////////////////////////////////////////////////////////////////////// +// Preflight cache + +class nsPreflightCache { + public: + struct TokenTime { + nsCString token; + TimeStamp expirationTime; + }; + + struct CacheEntry : public LinkedListElement { + explicit CacheEntry(nsCString& aKey) : mKey(aKey) { + MOZ_COUNT_CTOR(nsPreflightCache::CacheEntry); + } + + ~CacheEntry() { MOZ_COUNT_DTOR(nsPreflightCache::CacheEntry); } + + void PurgeExpired(TimeStamp now); + bool CheckRequest(const nsCString& aMethod, + const nsTArray& aCustomHeaders); + + nsCString mKey; + nsTArray mMethods; + nsTArray mHeaders; + }; + + MOZ_COUNTED_DEFAULT_CTOR(nsPreflightCache) + + ~nsPreflightCache() { + Clear(); + MOZ_COUNT_DTOR(nsPreflightCache); + } + + bool Initialize() { return true; } + + CacheEntry* GetEntry(nsIURI* aURI, nsIPrincipal* aPrincipal, + bool aWithCredentials, + const OriginAttributes& aOriginAttributes, bool aCreate); + void RemoveEntries(nsIURI* aURI, nsIPrincipal* aPrincipal, + const OriginAttributes& aOriginAttributes); + + void Clear(); + + private: + nsClassHashtable mTable; + LinkedList mList; +}; + +// Will be initialized in EnsurePreflightCache. +static nsPreflightCache* sPreflightCache = nullptr; + +static bool EnsurePreflightCache() { + if (sPreflightCache) return true; + + UniquePtr newCache(new nsPreflightCache()); + + if (newCache->Initialize()) { + sPreflightCache = newCache.release(); + return true; + } + + return false; +} + +void nsPreflightCache::CacheEntry::PurgeExpired(TimeStamp now) { + for (uint32_t i = 0, len = mMethods.Length(); i < len; ++i) { + if (now >= mMethods[i].expirationTime) { + mMethods.UnorderedRemoveElementAt(i); + --i; // Examine the element again, if necessary. + --len; + } + } + for (uint32_t i = 0, len = mHeaders.Length(); i < len; ++i) { + if (now >= mHeaders[i].expirationTime) { + mHeaders.UnorderedRemoveElementAt(i); + --i; // Examine the element again, if necessary. + --len; + } + } +} + +bool nsPreflightCache::CacheEntry::CheckRequest( + const nsCString& aMethod, const nsTArray& aHeaders) { + PurgeExpired(TimeStamp::NowLoRes()); + + if (!aMethod.EqualsLiteral("GET") && !aMethod.EqualsLiteral("POST")) { + struct CheckToken { + bool Equals(const TokenTime& e, const nsCString& method) const { + return e.token.Equals(method); + } + }; + + if (!mMethods.Contains(aMethod, CheckToken())) { + return false; + } + } + + struct CheckHeaderToken { + bool Equals(const TokenTime& e, const nsCString& header) const { + return e.token.Equals(header, nsCaseInsensitiveCStringComparator); + } + } checker; + for (uint32_t i = 0; i < aHeaders.Length(); ++i) { + if (!mHeaders.Contains(aHeaders[i], checker)) { + return false; + } + } + + return true; +} + +nsPreflightCache::CacheEntry* nsPreflightCache::GetEntry( + nsIURI* aURI, nsIPrincipal* aPrincipal, bool aWithCredentials, + const OriginAttributes& aOriginAttributes, bool aCreate) { + nsCString key; + if (NS_FAILED(aPrincipal->GetPrefLightCacheKey(aURI, aWithCredentials, + aOriginAttributes, key))) { + NS_WARNING("Invalid cache key!"); + return nullptr; + } + + CacheEntry* existingEntry = nullptr; + + if (mTable.Get(key, &existingEntry)) { + // Entry already existed so just return it. Also update the LRU list. + + // Move to the head of the list. + existingEntry->removeFrom(mList); + mList.insertFront(existingEntry); + + return existingEntry; + } + + if (!aCreate) { + return nullptr; + } + + // This is a new entry, allocate and insert into the table now so that any + // failures don't cause items to be removed from a full cache. + CacheEntry* newEntry = new CacheEntry(key); + if (!newEntry) { + NS_WARNING("Failed to allocate new cache entry!"); + return nullptr; + } + + NS_ASSERTION(mTable.Count() <= PREFLIGHT_CACHE_SIZE, + "Something is borked, too many entries in the cache!"); + + // Now enforce the max count. + if (mTable.Count() == PREFLIGHT_CACHE_SIZE) { + // Try to kick out all the expired entries. + TimeStamp now = TimeStamp::NowLoRes(); + for (auto iter = mTable.Iter(); !iter.Done(); iter.Next()) { + auto entry = iter.UserData(); + entry->PurgeExpired(now); + + if (entry->mHeaders.IsEmpty() && entry->mMethods.IsEmpty()) { + // Expired, remove from the list as well as the hash table. + entry->removeFrom(sPreflightCache->mList); + iter.Remove(); + } + } + + // If that didn't remove anything then kick out the least recently used + // entry. + if (mTable.Count() == PREFLIGHT_CACHE_SIZE) { + CacheEntry* lruEntry = static_cast(mList.popLast()); + MOZ_ASSERT(lruEntry); + + // This will delete 'lruEntry'. + mTable.Remove(lruEntry->mKey); + + NS_ASSERTION(mTable.Count() == PREFLIGHT_CACHE_SIZE - 1, + "Somehow tried to remove an entry that was never added!"); + } + } + + mTable.Put(key, newEntry); + mList.insertFront(newEntry); + + return newEntry; +} + +void nsPreflightCache::RemoveEntries( + nsIURI* aURI, nsIPrincipal* aPrincipal, + const OriginAttributes& aOriginAttributes) { + CacheEntry* entry; + nsCString key; + if (NS_SUCCEEDED(aPrincipal->GetPrefLightCacheKey(aURI, true, + aOriginAttributes, key)) && + mTable.Get(key, &entry)) { + entry->removeFrom(mList); + mTable.Remove(key); + } + + if (NS_SUCCEEDED(aPrincipal->GetPrefLightCacheKey(aURI, false, + aOriginAttributes, key)) && + mTable.Get(key, &entry)) { + entry->removeFrom(mList); + mTable.Remove(key); + } +} + +void nsPreflightCache::Clear() { + mList.clear(); + mTable.Clear(); +} + +////////////////////////////////////////////////////////////////////////// +// nsCORSListenerProxy + +NS_IMPL_ISUPPORTS(nsCORSListenerProxy, nsIStreamListener, nsIRequestObserver, + nsIChannelEventSink, nsIInterfaceRequestor, + nsIThreadRetargetableStreamListener) + +/* static */ +void nsCORSListenerProxy::Shutdown() { + delete sPreflightCache; + sPreflightCache = nullptr; +} + +nsCORSListenerProxy::nsCORSListenerProxy(nsIStreamListener* aOuter, + nsIPrincipal* aRequestingPrincipal, + bool aWithCredentials) + : mOuterListener(aOuter), + mRequestingPrincipal(aRequestingPrincipal), + mOriginHeaderPrincipal(aRequestingPrincipal), + mWithCredentials(aWithCredentials), + mRequestApproved(false), + mHasBeenCrossSite(false), +#ifdef DEBUG + mInited(false), +#endif + mMutex("nsCORSListenerProxy") { +} + +nsresult nsCORSListenerProxy::Init(nsIChannel* aChannel, + DataURIHandling aAllowDataURI) { + aChannel->GetNotificationCallbacks( + getter_AddRefs(mOuterNotificationCallbacks)); + aChannel->SetNotificationCallbacks(this); + + nsresult rv = UpdateChannel(aChannel, aAllowDataURI, UpdateType::Default); + if (NS_FAILED(rv)) { + { + MutexAutoLock lock(mMutex); + mOuterListener = nullptr; + } + mRequestingPrincipal = nullptr; + mOriginHeaderPrincipal = nullptr; + mOuterNotificationCallbacks = nullptr; + mHttpChannel = nullptr; + } +#ifdef DEBUG + mInited = true; +#endif + return rv; +} + +NS_IMETHODIMP +nsCORSListenerProxy::OnStartRequest(nsIRequest* aRequest) { + MOZ_ASSERT(mInited, "nsCORSListenerProxy has not been initialized properly"); + nsresult rv = CheckRequestApproved(aRequest); + mRequestApproved = NS_SUCCEEDED(rv); + if (!mRequestApproved) { + nsCOMPtr channel = do_QueryInterface(aRequest); + if (channel) { + nsCOMPtr uri; + NS_GetFinalChannelURI(channel, getter_AddRefs(uri)); + if (uri) { + OriginAttributes attrs; + StoragePrincipalHelper::GetOriginAttributesForNetworkState(channel, + attrs); + + if (sPreflightCache) { + // OK to use mRequestingPrincipal since preflights never get + // redirected. + sPreflightCache->RemoveEntries(uri, mRequestingPrincipal, attrs); + } else { + nsCOMPtr httpChannelChild = + do_QueryInterface(channel); + if (httpChannelChild) { + rv = httpChannelChild->RemoveCorsPreflightCacheEntry( + uri, mRequestingPrincipal, attrs); + if (NS_FAILED(rv)) { + // Only warn here to ensure we fall through the request Cancel() + // and outer listener OnStartRequest() calls. + NS_WARNING("Failed to remove CORS preflight cache entry!"); + } + } + } + } + } + + aRequest->Cancel(NS_ERROR_DOM_BAD_URI); + nsCOMPtr listener; + { + MutexAutoLock lock(mMutex); + listener = mOuterListener; + } + listener->OnStartRequest(aRequest); + + // Reason for NS_ERROR_DOM_BAD_URI already logged in CheckRequestApproved() + return NS_ERROR_DOM_BAD_URI; + } + + nsCOMPtr listener; + { + MutexAutoLock lock(mMutex); + listener = mOuterListener; + } + return listener->OnStartRequest(aRequest); +} + +namespace { +class CheckOriginHeader final : public nsIHttpHeaderVisitor { + public: + NS_DECL_ISUPPORTS + + CheckOriginHeader() : mHeaderCount(0) {} + + NS_IMETHOD + VisitHeader(const nsACString& aHeader, const nsACString& aValue) override { + if (aHeader.EqualsLiteral("Access-Control-Allow-Origin")) { + mHeaderCount++; + } + + if (mHeaderCount > 1) { + return NS_ERROR_DOM_BAD_URI; + } + return NS_OK; + } + + private: + uint32_t mHeaderCount; + + ~CheckOriginHeader() = default; +}; + +NS_IMPL_ISUPPORTS(CheckOriginHeader, nsIHttpHeaderVisitor) +} // namespace + +nsresult nsCORSListenerProxy::CheckRequestApproved(nsIRequest* aRequest) { + // Check if this was actually a cross domain request + if (!mHasBeenCrossSite) { + return NS_OK; + } + nsCOMPtr topChannel; + topChannel.swap(mHttpChannel); + + if (StaticPrefs::content_cors_disable()) { + LogBlockedRequest(aRequest, "CORSDisabled", nullptr, + nsILoadInfo::BLOCKING_REASON_CORSDISABLED, topChannel); + return NS_ERROR_DOM_BAD_URI; + } + + // Check if the request failed + nsresult status; + nsresult rv = aRequest->GetStatus(&status); + if (NS_FAILED(rv)) { + LogBlockedRequest(aRequest, "CORSDidNotSucceed", nullptr, + nsILoadInfo::BLOCKING_REASON_CORSDIDNOTSUCCEED, + topChannel); + return rv; + } + + if (NS_FAILED(status)) { + if (NS_BINDING_ABORTED != status) { + // Don't want to log mere cancellation as an error. + LogBlockedRequest(aRequest, "CORSDidNotSucceed", nullptr, + nsILoadInfo::BLOCKING_REASON_CORSDIDNOTSUCCEED, + topChannel); + } + return status; + } + + // Test that things worked on a HTTP level + nsCOMPtr http = do_QueryInterface(aRequest); + if (!http) { + LogBlockedRequest(aRequest, "CORSRequestNotHttp", nullptr, + nsILoadInfo::BLOCKING_REASON_CORSREQUESTNOTHTTP, + topChannel); + return NS_ERROR_DOM_BAD_URI; + } + + nsCOMPtr loadInfo = http->LoadInfo(); + if (loadInfo->GetServiceWorkerTaintingSynthesized()) { + // For synthesized responses, we don't need to perform any checks. + // Note: This would be unsafe if we ever changed our behavior to allow + // service workers to intercept CORS preflights. + return NS_OK; + } + if (loadInfo->GetBypassCORSChecks()) { + // This flag gets set if a WebExtention redirects a channel + // @onBeforeRequest. At this point no request has been made so we don't have + // the "Access-Control-Allow-Origin" header yet and the redirect would fail. + // So we're skipping the CORS check in that case. + return NS_OK; + } + + // Check the Access-Control-Allow-Origin header + RefPtr visitor = new CheckOriginHeader(); + nsAutoCString allowedOriginHeader; + + // check for duplicate headers + rv = http->VisitOriginalResponseHeaders(visitor); + if (NS_FAILED(rv)) { + LogBlockedRequest( + aRequest, "CORSMultipleAllowOriginNotAllowed", nullptr, + nsILoadInfo::BLOCKING_REASON_CORSMULTIPLEALLOWORIGINNOTALLOWED, + topChannel); + return rv; + } + + rv = http->GetResponseHeader("Access-Control-Allow-Origin"_ns, + allowedOriginHeader); + if (NS_FAILED(rv)) { + LogBlockedRequest(aRequest, "CORSMissingAllowOrigin", nullptr, + nsILoadInfo::BLOCKING_REASON_CORSMISSINGALLOWORIGIN, + topChannel); + return rv; + } + + // Bug 1210985 - Explicitly point out the error that the credential is + // not supported if the allowing origin is '*'. Note that this check + // has to be done before the condition + // + // >> if (mWithCredentials || !allowedOriginHeader.EqualsLiteral("*")) + // + // below since "if (A && B)" is included in "if (A || !B)". + // + if (mWithCredentials && allowedOriginHeader.EqualsLiteral("*")) { + LogBlockedRequest(aRequest, "CORSNotSupportingCredentials", nullptr, + nsILoadInfo::BLOCKING_REASON_CORSNOTSUPPORTINGCREDENTIALS, + topChannel); + return NS_ERROR_DOM_BAD_URI; + } + + if (mWithCredentials || !allowedOriginHeader.EqualsLiteral("*")) { + MOZ_ASSERT(!nsContentUtils::IsExpandedPrincipal(mOriginHeaderPrincipal)); + nsAutoCString origin; + mOriginHeaderPrincipal->GetAsciiOrigin(origin); + + if (!allowedOriginHeader.Equals(origin)) { + LogBlockedRequest( + aRequest, "CORSAllowOriginNotMatchingOrigin", + NS_ConvertUTF8toUTF16(allowedOriginHeader).get(), + nsILoadInfo::BLOCKING_REASON_CORSALLOWORIGINNOTMATCHINGORIGIN, + topChannel); + return NS_ERROR_DOM_BAD_URI; + } + } + + // Check Access-Control-Allow-Credentials header + if (mWithCredentials) { + nsAutoCString allowCredentialsHeader; + rv = http->GetResponseHeader("Access-Control-Allow-Credentials"_ns, + allowCredentialsHeader); + + if (!allowCredentialsHeader.EqualsLiteral("true")) { + LogBlockedRequest( + aRequest, "CORSMissingAllowCredentials", nullptr, + nsILoadInfo::BLOCKING_REASON_CORSMISSINGALLOWCREDENTIALS, topChannel); + return NS_ERROR_DOM_BAD_URI; + } + } + + return NS_OK; +} + +NS_IMETHODIMP +nsCORSListenerProxy::OnStopRequest(nsIRequest* aRequest, nsresult aStatusCode) { + MOZ_ASSERT(mInited, "nsCORSListenerProxy has not been initialized properly"); + nsCOMPtr listener; + { + MutexAutoLock lock(mMutex); + listener = std::move(mOuterListener); + } + nsresult rv = listener->OnStopRequest(aRequest, aStatusCode); + mOuterNotificationCallbacks = nullptr; + mHttpChannel = nullptr; + return rv; +} + +NS_IMETHODIMP +nsCORSListenerProxy::OnDataAvailable(nsIRequest* aRequest, + nsIInputStream* aInputStream, + uint64_t aOffset, uint32_t aCount) { + // NB: This can be called on any thread! But we're guaranteed that it is + // called between OnStartRequest and OnStopRequest, so we don't need to worry + // about races. + + MOZ_ASSERT(mInited, "nsCORSListenerProxy has not been initialized properly"); + if (!mRequestApproved) { + // Reason for NS_ERROR_DOM_BAD_URI already logged in CheckRequestApproved() + return NS_ERROR_DOM_BAD_URI; + } + nsCOMPtr listener; + { + MutexAutoLock lock(mMutex); + listener = mOuterListener; + } + return listener->OnDataAvailable(aRequest, aInputStream, aOffset, aCount); +} + +void nsCORSListenerProxy::SetInterceptController( + nsINetworkInterceptController* aInterceptController) { + mInterceptController = aInterceptController; +} + +NS_IMETHODIMP +nsCORSListenerProxy::GetInterface(const nsIID& aIID, void** aResult) { + if (aIID.Equals(NS_GET_IID(nsIChannelEventSink))) { + *aResult = static_cast(this); + NS_ADDREF_THIS(); + + return NS_OK; + } + + if (aIID.Equals(NS_GET_IID(nsINetworkInterceptController)) && + mInterceptController) { + nsCOMPtr copy(mInterceptController); + *aResult = copy.forget().take(); + + return NS_OK; + } + + return mOuterNotificationCallbacks + ? mOuterNotificationCallbacks->GetInterface(aIID, aResult) + : NS_ERROR_NO_INTERFACE; +} + +NS_IMETHODIMP +nsCORSListenerProxy::AsyncOnChannelRedirect( + nsIChannel* aOldChannel, nsIChannel* aNewChannel, uint32_t aFlags, + nsIAsyncVerifyRedirectCallback* aCb) { + nsresult rv; + if (NS_IsInternalSameURIRedirect(aOldChannel, aNewChannel, aFlags) || + NS_IsHSTSUpgradeRedirect(aOldChannel, aNewChannel, aFlags)) { + // Internal redirects still need to be updated in order to maintain + // the correct headers. We use DataURIHandling::Allow, since unallowed + // data URIs should have been blocked before we got to the internal + // redirect. + rv = UpdateChannel(aNewChannel, DataURIHandling::Allow, + UpdateType::InternalOrHSTSRedirect); + if (NS_FAILED(rv)) { + NS_WARNING( + "nsCORSListenerProxy::AsyncOnChannelRedirect: " + "internal redirect UpdateChannel() returned failure"); + aOldChannel->Cancel(rv); + return rv; + } + } else { + // A real, external redirect. Perform CORS checking on new URL. + rv = CheckRequestApproved(aOldChannel); + if (NS_FAILED(rv)) { + nsCOMPtr oldURI; + NS_GetFinalChannelURI(aOldChannel, getter_AddRefs(oldURI)); + if (oldURI) { + OriginAttributes attrs; + StoragePrincipalHelper::GetOriginAttributesForNetworkState(aOldChannel, + attrs); + if (sPreflightCache) { + // OK to use mRequestingPrincipal since preflights never get + // redirected. + sPreflightCache->RemoveEntries(oldURI, mRequestingPrincipal, attrs); + } else { + nsCOMPtr httpChannelChild = + do_QueryInterface(aOldChannel); + if (httpChannelChild) { + rv = httpChannelChild->RemoveCorsPreflightCacheEntry( + oldURI, mRequestingPrincipal, attrs); + if (NS_FAILED(rv)) { + // Only warn here to ensure we call the channel Cancel() below + NS_WARNING("Failed to remove CORS preflight cache entry!"); + } + } + } + } + aOldChannel->Cancel(NS_ERROR_DOM_BAD_URI); + // Reason for NS_ERROR_DOM_BAD_URI already logged in + // CheckRequestApproved() + return NS_ERROR_DOM_BAD_URI; + } + + if (mHasBeenCrossSite) { + // Once we've been cross-site, cross-origin redirects reset our source + // origin. Note that we need to call GetChannelURIPrincipal() because + // we are looking for the principal that is actually being loaded and not + // the principal that initiated the load. + nsCOMPtr oldChannelPrincipal; + nsContentUtils::GetSecurityManager()->GetChannelURIPrincipal( + aOldChannel, getter_AddRefs(oldChannelPrincipal)); + nsCOMPtr newChannelPrincipal; + nsContentUtils::GetSecurityManager()->GetChannelURIPrincipal( + aNewChannel, getter_AddRefs(newChannelPrincipal)); + if (!oldChannelPrincipal || !newChannelPrincipal) { + rv = NS_ERROR_OUT_OF_MEMORY; + } + + if (NS_SUCCEEDED(rv)) { + bool equal; + rv = oldChannelPrincipal->Equals(newChannelPrincipal, &equal); + if (NS_SUCCEEDED(rv) && !equal) { + // Spec says to set our source origin to a unique origin. + mOriginHeaderPrincipal = + NullPrincipal::CreateWithInheritedAttributes(oldChannelPrincipal); + } + } + + if (NS_FAILED(rv)) { + aOldChannel->Cancel(rv); + return rv; + } + } + + bool rewriteToGET = false; + nsCOMPtr oldHttpChannel = do_QueryInterface(aOldChannel); + if (oldHttpChannel) { + nsAutoCString method; + Unused << oldHttpChannel->GetRequestMethod(method); + Unused << oldHttpChannel->ShouldStripRequestBodyHeader(method, + &rewriteToGET); + } + + rv = UpdateChannel(aNewChannel, DataURIHandling::Disallow, + rewriteToGET ? UpdateType::StripRequestBodyHeader + : UpdateType::Default); + if (NS_FAILED(rv)) { + NS_WARNING( + "nsCORSListenerProxy::AsyncOnChannelRedirect: " + "UpdateChannel() returned failure"); + aOldChannel->Cancel(rv); + return rv; + } + } + + nsCOMPtr outer = + do_GetInterface(mOuterNotificationCallbacks); + if (outer) { + return outer->AsyncOnChannelRedirect(aOldChannel, aNewChannel, aFlags, aCb); + } + + aCb->OnRedirectVerifyCallback(NS_OK); + + return NS_OK; +} + +NS_IMETHODIMP +nsCORSListenerProxy::CheckListenerChain() { + MOZ_ASSERT(NS_IsMainThread()); + + nsCOMPtr retargetableListener; + { + MutexAutoLock lock(mMutex); + retargetableListener = do_QueryInterface(mOuterListener); + } + if (!retargetableListener) { + return NS_ERROR_NO_INTERFACE; + } + + return retargetableListener->CheckListenerChain(); +} + +// Please note that the CSP directive 'upgrade-insecure-requests' and the +// HTTPS-Only Mode are relying on the promise that channels get updated from +// http: to https: before the channel fetches any data from the netwerk. Such +// channels should not be blocked by CORS and marked as cross origin requests. +// E.g.: toplevel page: https://www.example.com loads +// xhr: http://www.example.com/foo which gets updated to +// https://www.example.com/foo +// In such a case we should bail out of CORS and rely on the promise that +// nsHttpChannel::Connect() upgrades the request from http to https. +bool CheckInsecureUpgradePreventsCORS(nsIPrincipal* aRequestingPrincipal, + nsIChannel* aChannel) { + nsCOMPtr channelURI; + nsresult rv = NS_GetFinalChannelURI(aChannel, getter_AddRefs(channelURI)); + NS_ENSURE_SUCCESS(rv, false); + + // upgrade insecure requests is only applicable to http requests + if (!channelURI->SchemeIs("http")) { + return false; + } + + nsCOMPtr originalURI; + rv = aChannel->GetOriginalURI(getter_AddRefs(originalURI)); + NS_ENSURE_SUCCESS(rv, false); + + nsAutoCString principalHost, channelHost, origChannelHost; + + // if we can not query a host from the uri, there is nothing to do + if (NS_FAILED(aRequestingPrincipal->GetAsciiHost(principalHost)) || + NS_FAILED(channelURI->GetAsciiHost(channelHost)) || + NS_FAILED(originalURI->GetAsciiHost(origChannelHost))) { + return false; + } + + // if the hosts do not match, there is nothing to do + if (!principalHost.EqualsIgnoreCase(channelHost.get())) { + return false; + } + + // also check that uri matches the one of the originalURI + if (!channelHost.EqualsIgnoreCase(origChannelHost.get())) { + return false; + } + + return true; +} + +nsresult nsCORSListenerProxy::UpdateChannel(nsIChannel* aChannel, + DataURIHandling aAllowDataURI, + UpdateType aUpdateType) { + nsCOMPtr uri, originalURI; + nsresult rv = NS_GetFinalChannelURI(aChannel, getter_AddRefs(uri)); + NS_ENSURE_SUCCESS(rv, rv); + rv = aChannel->GetOriginalURI(getter_AddRefs(originalURI)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr loadInfo = aChannel->LoadInfo(); + + // exempt data URIs from the same origin check. + if (aAllowDataURI == DataURIHandling::Allow && originalURI == uri) { + if (uri->SchemeIs("data")) { + return NS_OK; + } + if (loadInfo->GetAboutBlankInherits() && NS_IsAboutBlank(uri)) { + return NS_OK; + } + } + + // Set CORS attributes on channel so that intercepted requests get correct + // values. We have to do this here because the CheckMayLoad checks may lead + // to early return. We can't be sure this is an http channel though, so we + // can't return early on failure. + nsCOMPtr internal = do_QueryInterface(aChannel); + if (internal) { + rv = internal->SetCorsMode(nsIHttpChannelInternal::CORS_MODE_CORS); + NS_ENSURE_SUCCESS(rv, rv); + rv = internal->SetCorsIncludeCredentials(mWithCredentials); + NS_ENSURE_SUCCESS(rv, rv); + } + + // TODO: Bug 1353683 + // consider calling SetBlockedRequest in nsCORSListenerProxy::UpdateChannel + // + // Check that the uri is ok to load + uint32_t flags = loadInfo->CheckLoadURIFlags(); + rv = nsContentUtils::GetSecurityManager()->CheckLoadURIWithPrincipal( + mRequestingPrincipal, uri, flags, loadInfo->GetInnerWindowID()); + NS_ENSURE_SUCCESS(rv, rv); + + if (originalURI != uri) { + rv = nsContentUtils::GetSecurityManager()->CheckLoadURIWithPrincipal( + mRequestingPrincipal, originalURI, flags, loadInfo->GetInnerWindowID()); + NS_ENSURE_SUCCESS(rv, rv); + } + + if (!mHasBeenCrossSite && + NS_SUCCEEDED(mRequestingPrincipal->CheckMayLoad(uri, false)) && + (originalURI == uri || + NS_SUCCEEDED(mRequestingPrincipal->CheckMayLoad(originalURI, false)))) { + return NS_OK; + } + + // If the CSP directive 'upgrade-insecure-requests' is used or the HTTPS-Only + // Mode is enabled then we should not incorrectly require CORS if the only + // difference of a subresource request and the main page is the scheme. e.g. + // toplevel page: https://www.example.com loads + // xhr: http://www.example.com/somefoo, + // then the xhr request will be upgraded to https before it fetches any data + // from the netwerk, hence we shouldn't require CORS in that specific case. + if (CheckInsecureUpgradePreventsCORS(mRequestingPrincipal, aChannel)) { + // Check if https-only mode upgrades this later anyway + nsCOMPtr loadinfo = aChannel->LoadInfo(); + if (nsHTTPSOnlyUtils::IsSafeToAcceptCORSOrMixedContent(loadinfo)) { + return NS_OK; + } + // Check if 'upgrade-insecure-requests' is used + if (loadInfo->GetUpgradeInsecureRequests() || + loadInfo->GetBrowserUpgradeInsecureRequests()) { + return NS_OK; + } + } + + // Check if we need to do a preflight, and if so set one up. This must be + // called once we know that the request is going, or has gone, cross-origin. + rv = CheckPreflightNeeded(aChannel, aUpdateType); + NS_ENSURE_SUCCESS(rv, rv); + + // It's a cross site load + mHasBeenCrossSite = true; + + nsCString userpass; + uri->GetUserPass(userpass); + NS_ENSURE_TRUE(userpass.IsEmpty(), NS_ERROR_DOM_BAD_URI); + + // If we have an expanded principal here, we'll reject the CORS request, + // because we can't send a useful Origin header which is required for CORS. + if (nsContentUtils::IsExpandedPrincipal(mOriginHeaderPrincipal)) { + nsCOMPtr httpChannel = do_QueryInterface(aChannel); + LogBlockedRequest(aChannel, "CORSOriginHeaderNotAdded", nullptr, + nsILoadInfo::BLOCKING_REASON_CORSORIGINHEADERNOTADDED, + httpChannel); + return NS_ERROR_DOM_BAD_URI; + } + + // Add the Origin header + nsAutoCString origin; + rv = mOriginHeaderPrincipal->GetAsciiOrigin(origin); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr http = do_QueryInterface(aChannel); + NS_ENSURE_TRUE(http, NS_ERROR_FAILURE); + + // hide the Origin header when requesting from .onion and requesting CORS + if (StaticPrefs::network_http_referer_hideOnionSource()) { + if (mOriginHeaderPrincipal->GetIsOnion()) { + origin.AssignLiteral("null"); + } + } + + rv = http->SetRequestHeader(nsDependentCString(net::nsHttp::Origin), origin, + false); + NS_ENSURE_SUCCESS(rv, rv); + + // Make cookie-less if needed. We don't need to do anything here if the + // channel was opened with AsyncOpen, since then AsyncOpen will take + // care of the cookie policy for us. + if (!mWithCredentials) { + nsLoadFlags flags; + rv = http->GetLoadFlags(&flags); + NS_ENSURE_SUCCESS(rv, rv); + + flags |= nsIRequest::LOAD_ANONYMOUS; + rv = http->SetLoadFlags(flags); + NS_ENSURE_SUCCESS(rv, rv); + } + + mHttpChannel = http; + + return NS_OK; +} + +nsresult nsCORSListenerProxy::CheckPreflightNeeded(nsIChannel* aChannel, + UpdateType aUpdateType) { + // If this caller isn't using AsyncOpen, or if this *is* a preflight channel, + // then we shouldn't initiate preflight for this channel. + nsCOMPtr loadInfo = aChannel->LoadInfo(); + if (loadInfo->GetSecurityMode() != + nsILoadInfo::SEC_REQUIRE_CORS_INHERITS_SEC_CONTEXT || + loadInfo->GetIsPreflight()) { + return NS_OK; + } + + bool doPreflight = loadInfo->GetForcePreflight(); + + nsCOMPtr http = do_QueryInterface(aChannel); + if (!http) { + LogBlockedRequest(aChannel, "CORSRequestNotHttp", nullptr, + nsILoadInfo::BLOCKING_REASON_CORSREQUESTNOTHTTP, + mHttpChannel); + return NS_ERROR_DOM_BAD_URI; + } + + nsAutoCString method; + Unused << http->GetRequestMethod(method); + if (!method.LowerCaseEqualsLiteral("get") && + !method.LowerCaseEqualsLiteral("post") && + !method.LowerCaseEqualsLiteral("head")) { + doPreflight = true; + } + + // Avoid copying the array here + const nsTArray& loadInfoHeaders = loadInfo->CorsUnsafeHeaders(); + if (!loadInfoHeaders.IsEmpty()) { + doPreflight = true; + } + + // Add Content-Type header if needed + nsTArray headers; + nsAutoCString contentTypeHeader; + nsresult rv = http->GetRequestHeader("Content-Type"_ns, contentTypeHeader); + // GetRequestHeader return an error if the header is not set. Don't add + // "content-type" to the list if that's the case. + if (NS_SUCCEEDED(rv) && + !nsContentUtils::IsAllowedNonCorsContentType(contentTypeHeader) && + !loadInfoHeaders.Contains("content-type"_ns, + nsCaseInsensitiveCStringArrayComparator())) { + headers.AppendElements(loadInfoHeaders); + headers.AppendElement("content-type"_ns); + doPreflight = true; + } + + if (!doPreflight) { + return NS_OK; + } + + nsCOMPtr internal = do_QueryInterface(http); + if (!internal) { + LogBlockedRequest(aChannel, "CORSDidNotSucceed", nullptr, + nsILoadInfo::BLOCKING_REASON_CORSDIDNOTSUCCEED, + mHttpChannel); + return NS_ERROR_DOM_BAD_URI; + } + + internal->SetCorsPreflightParameters( + headers.IsEmpty() ? loadInfoHeaders : headers, + aUpdateType == UpdateType::StripRequestBodyHeader); + + return NS_OK; +} + +////////////////////////////////////////////////////////////////////////// +// Preflight proxy + +// Class used as streamlistener and notification callback when +// doing the initial OPTIONS request for a CORS check +class nsCORSPreflightListener final : public nsIStreamListener, + public nsIInterfaceRequestor, + public nsIChannelEventSink { + public: + nsCORSPreflightListener(nsIPrincipal* aReferrerPrincipal, + nsICorsPreflightCallback* aCallback, + nsILoadContext* aLoadContext, bool aWithCredentials, + const nsCString& aPreflightMethod, + const nsTArray& aPreflightHeaders) + : mPreflightMethod(aPreflightMethod), + mPreflightHeaders(aPreflightHeaders.Clone()), + mReferrerPrincipal(aReferrerPrincipal), + mCallback(aCallback), + mLoadContext(aLoadContext), + mWithCredentials(aWithCredentials) {} + + NS_DECL_ISUPPORTS + NS_DECL_NSISTREAMLISTENER + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSIINTERFACEREQUESTOR + NS_DECL_NSICHANNELEVENTSINK + + nsresult CheckPreflightRequestApproved(nsIRequest* aRequest); + + private: + ~nsCORSPreflightListener() = default; + + void AddResultToCache(nsIRequest* aRequest); + + nsCString mPreflightMethod; + nsTArray mPreflightHeaders; + nsCOMPtr mReferrerPrincipal; + nsCOMPtr mCallback; + nsCOMPtr mLoadContext; + bool mWithCredentials; +}; + +NS_IMPL_ISUPPORTS(nsCORSPreflightListener, nsIStreamListener, + nsIRequestObserver, nsIInterfaceRequestor, + nsIChannelEventSink) + +void nsCORSPreflightListener::AddResultToCache(nsIRequest* aRequest) { + nsCOMPtr http = do_QueryInterface(aRequest); + NS_ASSERTION(http, "Request was not http"); + + // The "Access-Control-Max-Age" header should return an age in seconds. + nsAutoCString headerVal; + uint32_t age = 0; + Unused << http->GetResponseHeader("Access-Control-Max-Age"_ns, headerVal); + if (headerVal.IsEmpty()) { + age = PREFLIGHT_DEFAULT_EXPIRY_SECONDS; + } else { + // Sanitize the string. We only allow 'delta-seconds' as specified by + // http://dev.w3.org/2006/waf/access-control (digits 0-9 with no leading or + // trailing non-whitespace characters). + nsACString::const_char_iterator iter, end; + headerVal.BeginReading(iter); + headerVal.EndReading(end); + while (iter != end) { + if (*iter < '0' || *iter > '9') { + return; + } + age = age * 10 + (*iter - '0'); + // Cap at 24 hours. This also avoids overflow + age = std::min(age, 86400U); + ++iter; + } + } + + if (!age || !EnsurePreflightCache()) { + return; + } + + // String seems fine, go ahead and cache. + // Note that we have already checked that these headers follow the correct + // syntax. + + nsCOMPtr uri; + NS_GetFinalChannelURI(http, getter_AddRefs(uri)); + + TimeStamp expirationTime = + TimeStamp::NowLoRes() + TimeDuration::FromSeconds(age); + + OriginAttributes attrs; + StoragePrincipalHelper::GetOriginAttributesForNetworkState(http, attrs); + + nsPreflightCache::CacheEntry* entry = sPreflightCache->GetEntry( + uri, mReferrerPrincipal, mWithCredentials, attrs, true); + if (!entry) { + return; + } + + // The "Access-Control-Allow-Methods" header contains a comma separated + // list of method names. + Unused << http->GetResponseHeader("Access-Control-Allow-Methods"_ns, + headerVal); + + for (const nsACString& method : + nsCCharSeparatedTokenizer(headerVal, ',').ToRange()) { + if (method.IsEmpty()) { + continue; + } + uint32_t i; + for (i = 0; i < entry->mMethods.Length(); ++i) { + if (entry->mMethods[i].token.Equals(method)) { + entry->mMethods[i].expirationTime = expirationTime; + break; + } + } + if (i == entry->mMethods.Length()) { + nsPreflightCache::TokenTime* newMethod = entry->mMethods.AppendElement(); + if (!newMethod) { + return; + } + + newMethod->token = method; + newMethod->expirationTime = expirationTime; + } + } + + // The "Access-Control-Allow-Headers" header contains a comma separated + // list of method names. + Unused << http->GetResponseHeader("Access-Control-Allow-Headers"_ns, + headerVal); + + for (const nsACString& header : + nsCCharSeparatedTokenizer(headerVal, ',').ToRange()) { + if (header.IsEmpty()) { + continue; + } + uint32_t i; + for (i = 0; i < entry->mHeaders.Length(); ++i) { + if (entry->mHeaders[i].token.Equals(header)) { + entry->mHeaders[i].expirationTime = expirationTime; + break; + } + } + if (i == entry->mHeaders.Length()) { + nsPreflightCache::TokenTime* newHeader = entry->mHeaders.AppendElement(); + if (!newHeader) { + return; + } + + newHeader->token = header; + newHeader->expirationTime = expirationTime; + } + } +} + +NS_IMETHODIMP +nsCORSPreflightListener::OnStartRequest(nsIRequest* aRequest) { +#ifdef DEBUG + { + nsCOMPtr channel = do_QueryInterface(aRequest); + nsCOMPtr loadInfo = channel ? channel->LoadInfo() : nullptr; + MOZ_ASSERT(!loadInfo || !loadInfo->GetServiceWorkerTaintingSynthesized()); + } +#endif + + nsresult rv = CheckPreflightRequestApproved(aRequest); + + if (NS_SUCCEEDED(rv)) { + // Everything worked, try to cache and then fire off the actual request. + AddResultToCache(aRequest); + + mCallback->OnPreflightSucceeded(); + } else { + mCallback->OnPreflightFailed(rv); + } + + return rv; +} + +NS_IMETHODIMP +nsCORSPreflightListener::OnStopRequest(nsIRequest* aRequest, nsresult aStatus) { + mCallback = nullptr; + return NS_OK; +} + +/** nsIStreamListener methods **/ + +NS_IMETHODIMP +nsCORSPreflightListener::OnDataAvailable(nsIRequest* aRequest, + nsIInputStream* inStr, + uint64_t sourceOffset, + uint32_t count) { + uint32_t totalRead; + return inStr->ReadSegments(NS_DiscardSegment, nullptr, count, &totalRead); +} + +NS_IMETHODIMP +nsCORSPreflightListener::AsyncOnChannelRedirect( + nsIChannel* aOldChannel, nsIChannel* aNewChannel, uint32_t aFlags, + nsIAsyncVerifyRedirectCallback* callback) { + // Only internal redirects allowed for now. + if (!NS_IsInternalSameURIRedirect(aOldChannel, aNewChannel, aFlags) && + !NS_IsHSTSUpgradeRedirect(aOldChannel, aNewChannel, aFlags)) { + nsCOMPtr httpChannel = do_QueryInterface(aOldChannel); + LogBlockedRequest( + aOldChannel, "CORSExternalRedirectNotAllowed", nullptr, + nsILoadInfo::BLOCKING_REASON_CORSEXTERNALREDIRECTNOTALLOWED, + httpChannel); + return NS_ERROR_DOM_BAD_URI; + } + + callback->OnRedirectVerifyCallback(NS_OK); + return NS_OK; +} + +nsresult nsCORSPreflightListener::CheckPreflightRequestApproved( + nsIRequest* aRequest) { + nsresult status; + nsresult rv = aRequest->GetStatus(&status); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_SUCCESS(status, status); + + // Test that things worked on a HTTP level + nsCOMPtr http = do_QueryInterface(aRequest); + nsCOMPtr internal = do_QueryInterface(aRequest); + NS_ENSURE_STATE(internal); + nsCOMPtr parentHttpChannel = do_QueryInterface(mCallback); + + bool succeedded; + rv = http->GetRequestSucceeded(&succeedded); + if (NS_FAILED(rv) || !succeedded) { + LogBlockedRequest(aRequest, "CORSPreflightDidNotSucceed2", nullptr, + nsILoadInfo::BLOCKING_REASON_CORSPREFLIGHTDIDNOTSUCCEED, + parentHttpChannel); + return NS_ERROR_DOM_BAD_URI; + } + + nsAutoCString headerVal; + // The "Access-Control-Allow-Methods" header contains a comma separated + // list of method names. + Unused << http->GetResponseHeader("Access-Control-Allow-Methods"_ns, + headerVal); + bool foundMethod = mPreflightMethod.EqualsLiteral("GET") || + mPreflightMethod.EqualsLiteral("HEAD") || + mPreflightMethod.EqualsLiteral("POST"); + for (const nsACString& method : + nsCCharSeparatedTokenizer(headerVal, ',').ToRange()) { + if (method.IsEmpty()) { + continue; + } + if (!NS_IsValidHTTPToken(method)) { + LogBlockedRequest(aRequest, "CORSInvalidAllowMethod", + NS_ConvertUTF8toUTF16(method).get(), + nsILoadInfo::BLOCKING_REASON_CORSINVALIDALLOWMETHOD, + parentHttpChannel); + return NS_ERROR_DOM_BAD_URI; + } + + if (method.EqualsLiteral("*") && !mWithCredentials) { + foundMethod = true; + } else { + foundMethod |= mPreflightMethod.Equals(method); + } + } + if (!foundMethod) { + LogBlockedRequest(aRequest, "CORSMethodNotFound", nullptr, + nsILoadInfo::BLOCKING_REASON_CORSMETHODNOTFOUND, + parentHttpChannel); + return NS_ERROR_DOM_BAD_URI; + } + + // The "Access-Control-Allow-Headers" header contains a comma separated + // list of header names. + Unused << http->GetResponseHeader("Access-Control-Allow-Headers"_ns, + headerVal); + nsTArray headers; + bool allowAllHeaders = false; + for (const nsACString& header : + nsCCharSeparatedTokenizer(headerVal, ',').ToRange()) { + if (header.IsEmpty()) { + continue; + } + if (!NS_IsValidHTTPToken(header)) { + LogBlockedRequest(aRequest, "CORSInvalidAllowHeader", + NS_ConvertUTF8toUTF16(header).get(), + nsILoadInfo::BLOCKING_REASON_CORSINVALIDALLOWHEADER, + parentHttpChannel); + return NS_ERROR_DOM_BAD_URI; + } + if (header.EqualsLiteral("*") && !mWithCredentials) { + allowAllHeaders = true; + } else { + headers.AppendElement(header); + } + } + + if (!allowAllHeaders) { + for (uint32_t i = 0; i < mPreflightHeaders.Length(); ++i) { + const auto& comparator = nsCaseInsensitiveCStringArrayComparator(); + if (!headers.Contains(mPreflightHeaders[i], comparator)) { + LogBlockedRequest( + aRequest, "CORSMissingAllowHeaderFromPreflight2", + NS_ConvertUTF8toUTF16(mPreflightHeaders[i]).get(), + nsILoadInfo::BLOCKING_REASON_CORSMISSINGALLOWHEADERFROMPREFLIGHT, + parentHttpChannel); + return NS_ERROR_DOM_BAD_URI; + } + } + } + + return NS_OK; +} + +NS_IMETHODIMP +nsCORSPreflightListener::GetInterface(const nsIID& aIID, void** aResult) { + if (aIID.Equals(NS_GET_IID(nsILoadContext)) && mLoadContext) { + nsCOMPtr copy = mLoadContext; + copy.forget(aResult); + return NS_OK; + } + + return QueryInterface(aIID, aResult); +} + +void nsCORSListenerProxy::RemoveFromCorsPreflightCache( + nsIURI* aURI, nsIPrincipal* aRequestingPrincipal, + const OriginAttributes& aOriginAttributes) { + MOZ_ASSERT(XRE_IsParentProcess()); + if (sPreflightCache) { + sPreflightCache->RemoveEntries(aURI, aRequestingPrincipal, + aOriginAttributes); + } +} + +// static +nsresult nsCORSListenerProxy::StartCORSPreflight( + nsIChannel* aRequestChannel, nsICorsPreflightCallback* aCallback, + nsTArray& aUnsafeHeaders, nsIChannel** aPreflightChannel) { + *aPreflightChannel = nullptr; + + if (StaticPrefs::content_cors_disable()) { + nsCOMPtr http = do_QueryInterface(aRequestChannel); + LogBlockedRequest(aRequestChannel, "CORSDisabled", nullptr, + nsILoadInfo::BLOCKING_REASON_CORSDISABLED, http); + return NS_ERROR_DOM_BAD_URI; + } + + nsAutoCString method; + nsCOMPtr httpChannel(do_QueryInterface(aRequestChannel)); + NS_ENSURE_TRUE(httpChannel, NS_ERROR_UNEXPECTED); + Unused << httpChannel->GetRequestMethod(method); + + nsCOMPtr uri; + nsresult rv = NS_GetFinalChannelURI(aRequestChannel, getter_AddRefs(uri)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr originalLoadInfo = aRequestChannel->LoadInfo(); + MOZ_ASSERT(originalLoadInfo->GetSecurityMode() == + nsILoadInfo::SEC_REQUIRE_CORS_INHERITS_SEC_CONTEXT, + "how did we end up here?"); + + nsCOMPtr principal = originalLoadInfo->GetLoadingPrincipal(); + MOZ_ASSERT(principal && originalLoadInfo->GetExternalContentPolicyType() != + ExtContentPolicy::TYPE_DOCUMENT, + "Should not do CORS loads for top-level loads, so a " + "loadingPrincipal should always exist."); + bool withCredentials = + originalLoadInfo->GetCookiePolicy() == nsILoadInfo::SEC_COOKIES_INCLUDE; + + nsPreflightCache::CacheEntry* entry = nullptr; + + // Disable cache if devtools says so. + bool disableCache = Preferences::GetBool("devtools.cache.disabled"); + + if (sPreflightCache && !disableCache) { + OriginAttributes attrs; + StoragePrincipalHelper::GetOriginAttributesForNetworkState(aRequestChannel, + attrs); + entry = sPreflightCache->GetEntry(uri, principal, withCredentials, attrs, + false); + } + + if (entry && entry->CheckRequest(method, aUnsafeHeaders)) { + aCallback->OnPreflightSucceeded(); + return NS_OK; + } + + // Either it wasn't cached or the cached result has expired. Build a + // channel for the OPTIONS request. + + nsCOMPtr loadInfo = + static_cast(originalLoadInfo.get()) + ->CloneForNewRequest(); + static_cast(loadInfo.get())->SetIsPreflight(); + + nsCOMPtr loadGroup; + rv = aRequestChannel->GetLoadGroup(getter_AddRefs(loadGroup)); + NS_ENSURE_SUCCESS(rv, rv); + + // We want to give the preflight channel's notification callbacks the same + // load context as the original channel's notification callbacks had. We + // don't worry about a load context provided via the loadgroup here, since + // they have the same loadgroup. + nsCOMPtr callbacks; + rv = aRequestChannel->GetNotificationCallbacks(getter_AddRefs(callbacks)); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr loadContext = do_GetInterface(callbacks); + + nsLoadFlags loadFlags; + rv = aRequestChannel->GetLoadFlags(&loadFlags); + NS_ENSURE_SUCCESS(rv, rv); + + // Preflight requests should never be intercepted by service workers and + // are always anonymous. + // NOTE: We ignore CORS checks on synthesized responses (see the CORS + // preflights, then we need to extend the GetResponseSynthesized() check in + // nsCORSListenerProxy::CheckRequestApproved()). If we change our behavior + // here and allow service workers to intercept CORS preflights, then that + // check won't be safe any more. + loadFlags |= + nsIChannel::LOAD_BYPASS_SERVICE_WORKER | nsIRequest::LOAD_ANONYMOUS; + + nsCOMPtr preflightChannel; + rv = NS_NewChannelInternal(getter_AddRefs(preflightChannel), uri, loadInfo, + nullptr, // PerformanceStorage + loadGroup, + nullptr, // aCallbacks + loadFlags); + NS_ENSURE_SUCCESS(rv, rv); + + // Set method and headers + nsCOMPtr preHttp = do_QueryInterface(preflightChannel); + NS_ASSERTION(preHttp, "Failed to QI to nsIHttpChannel!"); + + rv = preHttp->SetRequestMethod("OPTIONS"_ns); + NS_ENSURE_SUCCESS(rv, rv); + + rv = preHttp->SetRequestHeader("Access-Control-Request-Method"_ns, method, + false); + NS_ENSURE_SUCCESS(rv, rv); + + // Set the CORS preflight channel's warning reporter to be the same as the + // requesting channel so that all log messages are able to be reported through + // the warning reporter. + RefPtr reqCh = do_QueryObject(aRequestChannel); + RefPtr preCh = do_QueryObject(preHttp); + if (preCh && reqCh) { // there are other implementers of nsIHttpChannel + preCh->SetWarningReporter(reqCh->GetWarningReporter()); + } + + nsTArray preflightHeaders; + if (!aUnsafeHeaders.IsEmpty()) { + for (uint32_t i = 0; i < aUnsafeHeaders.Length(); ++i) { + preflightHeaders.AppendElement(); + ToLowerCase(aUnsafeHeaders[i], preflightHeaders[i]); + } + preflightHeaders.Sort(); + nsAutoCString headers; + for (uint32_t i = 0; i < preflightHeaders.Length(); ++i) { + if (i != 0) { + headers += ','; + } + headers += preflightHeaders[i]; + } + rv = preHttp->SetRequestHeader("Access-Control-Request-Headers"_ns, headers, + false); + NS_ENSURE_SUCCESS(rv, rv); + } + + // Set up listener which will start the original channel + RefPtr preflightListener = + new nsCORSPreflightListener(principal, aCallback, loadContext, + withCredentials, method, preflightHeaders); + + rv = preflightChannel->SetNotificationCallbacks(preflightListener); + NS_ENSURE_SUCCESS(rv, rv); + + if (preCh && reqCh) { + // Per https://fetch.spec.whatwg.org/#cors-preflight-fetch step 1, the + // request's referrer and referrer policy should match the original request. + nsCOMPtr referrerInfo; + rv = reqCh->GetReferrerInfo(getter_AddRefs(referrerInfo)); + NS_ENSURE_SUCCESS(rv, rv); + if (referrerInfo) { + nsCOMPtr newReferrerInfo = + static_cast(referrerInfo.get())->Clone(); + rv = preCh->SetReferrerInfo(newReferrerInfo); + NS_ENSURE_SUCCESS(rv, rv); + } + } + + // Start preflight + rv = preflightChannel->AsyncOpen(preflightListener); + NS_ENSURE_SUCCESS(rv, rv); + + // Return newly created preflight channel + preflightChannel.forget(aPreflightChannel); + + return NS_OK; +} + +// static +void nsCORSListenerProxy::LogBlockedCORSRequest(uint64_t aInnerWindowID, + bool aPrivateBrowsing, + bool aFromChromeContext, + const nsAString& aMessage, + const nsACString& aCategory) { + nsresult rv = NS_OK; + + // Build the error object and log it to the console + nsCOMPtr console( + do_GetService(NS_CONSOLESERVICE_CONTRACTID, &rv)); + if (NS_FAILED(rv)) { + NS_WARNING("Failed to log blocked cross-site request (no console)"); + return; + } + + nsCOMPtr scriptError = + do_CreateInstance(NS_SCRIPTERROR_CONTRACTID, &rv); + if (NS_FAILED(rv)) { + NS_WARNING("Failed to log blocked cross-site request (no scriptError)"); + return; + } + + // query innerWindowID and log to web console, otherwise log to + // the error to the browser console. + if (aInnerWindowID > 0) { + rv = scriptError->InitWithSanitizedSource(aMessage, + u""_ns, // sourceName + u""_ns, // sourceLine + 0, // lineNumber + 0, // columnNumber + nsIScriptError::errorFlag, + aCategory, aInnerWindowID); + } else { + nsCString category = PromiseFlatCString(aCategory); + rv = scriptError->Init(aMessage, + u""_ns, // sourceName + u""_ns, // sourceLine + 0, // lineNumber + 0, // columnNumber + nsIScriptError::errorFlag, category.get(), + aPrivateBrowsing, + aFromChromeContext); // From chrome context + } + if (NS_FAILED(rv)) { + NS_WARNING( + "Failed to log blocked cross-site request (scriptError init failed)"); + return; + } + console->LogMessage(scriptError); +} diff --git a/netwerk/protocol/http/nsCORSListenerProxy.h b/netwerk/protocol/http/nsCORSListenerProxy.h new file mode 100644 index 0000000000..ea74f470e5 --- /dev/null +++ b/netwerk/protocol/http/nsCORSListenerProxy.h @@ -0,0 +1,127 @@ +/* -*- 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/. */ + +#ifndef nsCORSListenerProxy_h__ +#define nsCORSListenerProxy_h__ + +#include "nsIStreamListener.h" +#include "nsIInterfaceRequestor.h" +#include "nsCOMPtr.h" +#include "nsString.h" +#include "nsIURI.h" +#include "nsTArray.h" +#include "nsIInterfaceRequestor.h" +#include "nsIChannelEventSink.h" +#include "nsIThreadRetargetableStreamListener.h" +#include "mozilla/Attributes.h" +#include "mozilla/Atomics.h" +#include "mozilla/Mutex.h" + +class nsIHttpChannel; +class nsIURI; +class nsIPrincipal; +class nsINetworkInterceptController; +class nsICorsPreflightCallback; + +namespace mozilla { +namespace net { +class HttpChannelParent; +class nsHttpChannel; +} // namespace net +} // namespace mozilla + +enum class DataURIHandling { Allow, Disallow }; + +enum class UpdateType { + Default, + StripRequestBodyHeader, + InternalOrHSTSRedirect +}; + +class nsCORSListenerProxy final : public nsIStreamListener, + public nsIInterfaceRequestor, + public nsIChannelEventSink, + public nsIThreadRetargetableStreamListener { + public: + nsCORSListenerProxy(nsIStreamListener* aOuter, + nsIPrincipal* aRequestingPrincipal, + bool aWithCredentials); + + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMLISTENER + NS_DECL_NSIINTERFACEREQUESTOR + NS_DECL_NSICHANNELEVENTSINK + NS_DECL_NSITHREADRETARGETABLESTREAMLISTENER + + static void Shutdown(); + + [[nodiscard]] nsresult Init(nsIChannel* aChannel, + DataURIHandling aAllowDataURI); + + void SetInterceptController( + nsINetworkInterceptController* aInterceptController); + + // When CORS blocks a request, log the message to the web console, or the + // browser console if no valid inner window ID is found. + static void LogBlockedCORSRequest(uint64_t aInnerWindowID, + bool aPrivateBrowsing, + bool aFromChromeContext, + const nsAString& aMessage, + const nsACString& aCategory); + + private: + // Only HttpChannelParent can call RemoveFromCorsPreflightCache + friend class mozilla::net::HttpChannelParent; + // Only nsHttpChannel can invoke CORS preflights + friend class mozilla::net::nsHttpChannel; + + static void RemoveFromCorsPreflightCache( + nsIURI* aURI, nsIPrincipal* aRequestingPrincipal, + const mozilla::OriginAttributes& aOriginAttributes); + [[nodiscard]] static nsresult StartCORSPreflight( + nsIChannel* aRequestChannel, nsICorsPreflightCallback* aCallback, + nsTArray& aACUnsafeHeaders, nsIChannel** aPreflightChannel); + + ~nsCORSListenerProxy() = default; + + [[nodiscard]] nsresult UpdateChannel(nsIChannel* aChannel, + DataURIHandling aAllowDataURI, + UpdateType aUpdateType); + [[nodiscard]] nsresult CheckRequestApproved(nsIRequest* aRequest); + [[nodiscard]] nsresult CheckPreflightNeeded(nsIChannel* aChannel, + UpdateType aUpdateType); + + nsCOMPtr mOuterListener; + // The principal that originally kicked off the request + nsCOMPtr mRequestingPrincipal; + // The principal to use for our Origin header ("source origin" in spec terms). + // This can get changed during redirects, unlike mRequestingPrincipal. + nsCOMPtr mOriginHeaderPrincipal; + nsCOMPtr mOuterNotificationCallbacks; + nsCOMPtr mInterceptController; + bool mWithCredentials; + mozilla::Atomic mRequestApproved; + // Please note that the member variable mHasBeenCrossSite may rely on the + // promise that the CSP directive 'upgrade-insecure-requests' upgrades + // an http: request to https: in nsHttpChannel::Connect() and hence + // a request might not be marked as cross site request based on that promise. + bool mHasBeenCrossSite; + // Under e10s, logging happens in the child process. Keep a reference to the + // creator nsIHttpChannel in order to find the way back to the child. Released + // in OnStopRequest(). + nsCOMPtr mHttpChannel; +#ifdef DEBUG + bool mInited; +#endif + + // only locking mOuterListener, because it can be used on different threads. + // We guarantee that OnStartRequest, OnDataAvailable and OnStopReques will be + // called in order, but to make tsan happy we will lock mOuterListener. + mutable mozilla::Mutex mMutex; +}; + +#endif diff --git a/netwerk/protocol/http/nsHttp.cpp b/netwerk/protocol/http/nsHttp.cpp new file mode 100644 index 0000000000..c8346ef459 --- /dev/null +++ b/netwerk/protocol/http/nsHttp.cpp @@ -0,0 +1,1045 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=4 sw=2 sts=2 et cin: */ +/* 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/. */ + +// HttpLog.h should generally be included first +#include "HttpLog.h" + +#include "nsHttp.h" +#include "CacheControlParser.h" +#include "PLDHashTable.h" +#include "mozilla/Mutex.h" +#include "mozilla/HashFunctions.h" +#include "mozilla/Preferences.h" +#include "mozilla/StaticPrefs_network.h" +#include "nsCRT.h" +#include "nsContentUtils.h" +#include "nsHttpRequestHead.h" +#include "nsHttpResponseHead.h" +#include "nsHttpHandler.h" +#include "nsICacheEntry.h" +#include "nsIRequest.h" +#include "nsJSUtils.h" +#include +#include + +namespace mozilla { +namespace net { + +const uint32_t kHttp3VersionCount = 4; +const nsCString kHttp3Versions[] = {"h3-27"_ns, "h3-28"_ns, "h3-29"_ns, + "h3-30"_ns}; + +// define storage for all atoms +namespace nsHttp { +#define HTTP_ATOM(_name, _value) nsHttpAtom _name(_value); +#include "nsHttpAtomList.h" +#undef HTTP_ATOM +} // namespace nsHttp + +// find out how many atoms we have +#define HTTP_ATOM(_name, _value) Unused_##_name, +enum { +#include "nsHttpAtomList.h" + NUM_HTTP_ATOMS +}; +#undef HTTP_ATOM + +// we keep a linked list of atoms allocated on the heap for easy clean up when +// the atom table is destroyed. The structure and value string are allocated +// as one contiguous block. + +struct HttpHeapAtom { + struct HttpHeapAtom* next; + char value[1]; +}; + +static PLDHashTable* sAtomTable; +static struct HttpHeapAtom* sHeapAtoms = nullptr; +static Mutex* sLock = nullptr; + +HttpHeapAtom* NewHeapAtom(const char* value) { + int len = strlen(value); + + HttpHeapAtom* a = reinterpret_cast(malloc(sizeof(*a) + len)); + if (!a) return nullptr; + memcpy(a->value, value, len + 1); + + // add this heap atom to the list of all heap atoms + a->next = sHeapAtoms; + sHeapAtoms = a; + + return a; +} + +// Hash string ignore case, based on PL_HashString +static PLDHashNumber StringHash(const void* key) { + PLDHashNumber h = 0; + for (const char* s = reinterpret_cast(key); *s; ++s) + h = AddToHash(h, nsCRT::ToLower(*s)); + return h; +} + +static bool StringCompare(const PLDHashEntryHdr* entry, const void* testKey) { + const void* entryKey = reinterpret_cast(entry)->key; + + return PL_strcasecmp(reinterpret_cast(entryKey), + reinterpret_cast(testKey)) == 0; +} + +static const PLDHashTableOps ops = {StringHash, StringCompare, + PLDHashTable::MoveEntryStub, + PLDHashTable::ClearEntryStub, nullptr}; + +// We put the atoms in a hash table for speedy lookup.. see ResolveAtom. +namespace nsHttp { +nsresult CreateAtomTable() { + MOZ_ASSERT(!sAtomTable, "atom table already initialized"); + + if (!sLock) { + sLock = new Mutex("nsHttp.sLock"); + } + + // The initial length for this table is a value greater than the number of + // known atoms (NUM_HTTP_ATOMS) because we expect to encounter a few random + // headers right off the bat. + sAtomTable = + new PLDHashTable(&ops, sizeof(PLDHashEntryStub), NUM_HTTP_ATOMS + 10); + + // fill the table with our known atoms + const char* const atoms[] = { +#define HTTP_ATOM(_name, _value) _name._val, +#include "nsHttpAtomList.h" +#undef HTTP_ATOM + nullptr}; + + for (int i = 0; atoms[i]; ++i) { + auto stub = + static_cast(sAtomTable->Add(atoms[i], fallible)); + if (!stub) return NS_ERROR_OUT_OF_MEMORY; + + MOZ_ASSERT(!stub->key, "duplicate static atom"); + stub->key = atoms[i]; + } + + return NS_OK; +} + +void DestroyAtomTable() { + delete sAtomTable; + sAtomTable = nullptr; + + while (sHeapAtoms) { + HttpHeapAtom* next = sHeapAtoms->next; + free(sHeapAtoms); + sHeapAtoms = next; + } + + delete sLock; + sLock = nullptr; +} + +Mutex* GetLock() { return sLock; } + +// this function may be called from multiple threads +nsHttpAtom ResolveAtom(const char* str) { + nsHttpAtom atom; + + if (!str || !sAtomTable) return atom; + + MutexAutoLock lock(*sLock); + + auto stub = static_cast(sAtomTable->Add(str, fallible)); + if (!stub) return atom; // out of memory + + if (stub->key) { + atom._val = reinterpret_cast(stub->key); + return atom; + } + + // if the atom could not be found in the atom table, then we'll go + // and allocate a new atom on the heap. + HttpHeapAtom* heapAtom = NewHeapAtom(str); + if (!heapAtom) return atom; // out of memory + + stub->key = atom._val = heapAtom->value; + return atom; +} + +// +// From section 2.2 of RFC 2616, a token is defined as: +// +// token = 1* +// CHAR = +// separators = "(" | ")" | "<" | ">" | "@" +// | "," | ";" | ":" | "\" | <"> +// | "/" | "[" | "]" | "?" | "=" +// | "{" | "}" | SP | HT +// CTL = +// SP = +// HT = +// +static const char kValidTokenMap[128] = { + 0, 0, 0, 0, 0, 0, 0, 0, // 0 + 0, 0, 0, 0, 0, 0, 0, 0, // 8 + 0, 0, 0, 0, 0, 0, 0, 0, // 16 + 0, 0, 0, 0, 0, 0, 0, 0, // 24 + + 0, 1, 0, 1, 1, 1, 1, 1, // 32 + 0, 0, 1, 1, 0, 1, 1, 0, // 40 + 1, 1, 1, 1, 1, 1, 1, 1, // 48 + 1, 1, 0, 0, 0, 0, 0, 0, // 56 + + 0, 1, 1, 1, 1, 1, 1, 1, // 64 + 1, 1, 1, 1, 1, 1, 1, 1, // 72 + 1, 1, 1, 1, 1, 1, 1, 1, // 80 + 1, 1, 1, 0, 0, 0, 1, 1, // 88 + + 1, 1, 1, 1, 1, 1, 1, 1, // 96 + 1, 1, 1, 1, 1, 1, 1, 1, // 104 + 1, 1, 1, 1, 1, 1, 1, 1, // 112 + 1, 1, 1, 0, 1, 0, 1, 0 // 120 +}; +bool IsValidToken(const char* start, const char* end) { + if (start == end) return false; + + for (; start != end; ++start) { + const unsigned char idx = *start; + if (idx > 127 || !kValidTokenMap[idx]) return false; + } + + return true; +} + +const char* GetProtocolVersion(HttpVersion pv) { + switch (pv) { + case HttpVersion::v3_0: + return "h3"; + case HttpVersion::v2_0: + return "h2"; + case HttpVersion::v1_0: + return "http/1.0"; + case HttpVersion::v1_1: + return "http/1.1"; + default: + NS_WARNING(nsPrintfCString("Unkown protocol version: 0x%X. " + "Please file a bug", + static_cast(pv)) + .get()); + return "http/1.1"; + } +} + +// static +void TrimHTTPWhitespace(const nsACString& aSource, nsACString& aDest) { + nsAutoCString str(aSource); + + // HTTP whitespace 0x09: '\t', 0x0A: '\n', 0x0D: '\r', 0x20: ' ' + static const char kHTTPWhitespace[] = "\t\n\r "; + str.Trim(kHTTPWhitespace); + aDest.Assign(str); +} + +// static +bool IsReasonableHeaderValue(const nsACString& s) { + // Header values MUST NOT contain line-breaks. RFC 2616 technically + // permits CTL characters, including CR and LF, in header values provided + // they are quoted. However, this can lead to problems if servers do not + // interpret quoted strings properly. Disallowing CR and LF here seems + // reasonable and keeps things simple. We also disallow a null byte. + const nsACString::char_type* end = s.EndReading(); + for (const nsACString::char_type* i = s.BeginReading(); i != end; ++i) { + if (*i == '\r' || *i == '\n' || *i == '\0') { + return false; + } + } + return true; +} + +const char* FindToken(const char* input, const char* token, const char* seps) { + if (!input) return nullptr; + + int inputLen = strlen(input); + int tokenLen = strlen(token); + + if (inputLen < tokenLen) return nullptr; + + const char* inputTop = input; + const char* inputEnd = input + inputLen - tokenLen; + for (; input <= inputEnd; ++input) { + if (PL_strncasecmp(input, token, tokenLen) == 0) { + if (input > inputTop && !strchr(seps, *(input - 1))) continue; + if (input < inputEnd && !strchr(seps, *(input + tokenLen))) continue; + return input; + } + } + + return nullptr; +} + +bool ParseInt64(const char* input, const char** next, int64_t* r) { + MOZ_ASSERT(input); + MOZ_ASSERT(r); + + char* end = nullptr; + errno = 0; // Clear errno to make sure its value is set by strtoll + int64_t value = strtoll(input, &end, /* base */ 10); + + // Fail if: - the parsed number overflows. + // - the end points to the start of the input string. + // - we parsed a negative value. Consumers don't expect that. + if (errno != 0 || end == input || value < 0) { + LOG(("nsHttp::ParseInt64 value=%" PRId64 " errno=%d", value, errno)); + return false; + } + + if (next) { + *next = end; + } + *r = value; + return true; +} + +bool IsPermanentRedirect(uint32_t httpStatus) { + return httpStatus == 301 || httpStatus == 308; +} + +bool ValidationRequired(bool isForcedValid, + nsHttpResponseHead* cachedResponseHead, + uint32_t loadFlags, bool allowStaleCacheContent, + bool isImmutable, bool customConditionalRequest, + nsHttpRequestHead& requestHead, nsICacheEntry* entry, + CacheControlParser& cacheControlRequest, + bool fromPreviousSession, + bool* performBackgroundRevalidation) { + if (performBackgroundRevalidation) { + *performBackgroundRevalidation = false; + } + + // Check isForcedValid to see if it is possible to skip validation. + // Don't skip validation if we have serious reason to believe that this + // content is invalid (it's expired). + // See netwerk/cache2/nsICacheEntry.idl for details + if (isForcedValid && (!cachedResponseHead->ExpiresInPast() || + !cachedResponseHead->MustValidateIfExpired())) { + LOG(("NOT validating based on isForcedValid being true.\n")); + return false; + } + + // If the LOAD_FROM_CACHE flag is set, any cached data can simply be used + if (loadFlags & nsIRequest::LOAD_FROM_CACHE || allowStaleCacheContent) { + LOG(("NOT validating based on LOAD_FROM_CACHE load flag\n")); + return false; + } + + // If the VALIDATE_ALWAYS flag is set, any cached data won't be used until + // it's revalidated with the server. + if ((loadFlags & nsIRequest::VALIDATE_ALWAYS) && !isImmutable) { + LOG(("Validating based on VALIDATE_ALWAYS load flag\n")); + return true; + } + + // Even if the VALIDATE_NEVER flag is set, there are still some cases in + // which we must validate the cached response with the server. + if (loadFlags & nsIRequest::VALIDATE_NEVER) { + LOG(("VALIDATE_NEVER set\n")); + // if no-store validate cached response (see bug 112564) + if (cachedResponseHead->NoStore()) { + LOG(("Validating based on no-store logic\n")); + return true; + } + LOG(("NOT validating based on VALIDATE_NEVER load flag\n")); + return false; + } + + // check if validation is strictly required... + if (cachedResponseHead->MustValidate()) { + LOG(("Validating based on MustValidate() returning TRUE\n")); + return true; + } + + // possibly serve from cache for a custom If-Match/If-Unmodified-Since + // conditional request + if (customConditionalRequest && !requestHead.HasHeader(nsHttp::If_Match) && + !requestHead.HasHeader(nsHttp::If_Unmodified_Since)) { + LOG(("Validating based on a custom conditional request\n")); + return true; + } + + // previously we also checked for a query-url w/out expiration + // and didn't do heuristic on it. but defacto that is allowed now. + // + // Check if the cache entry has expired... + + bool doValidation = true; + uint32_t now = NowInSeconds(); + + uint32_t age = 0; + nsresult rv = cachedResponseHead->ComputeCurrentAge(now, now, &age); + if (NS_FAILED(rv)) { + return true; + } + + uint32_t freshness = 0; + rv = cachedResponseHead->ComputeFreshnessLifetime(&freshness); + if (NS_FAILED(rv)) { + return true; + } + + uint32_t expiration = 0; + rv = entry->GetExpirationTime(&expiration); + if (NS_FAILED(rv)) { + return true; + } + + uint32_t maxAgeRequest, maxStaleRequest, minFreshRequest; + + LOG((" NowInSeconds()=%u, expiration time=%u, freshness lifetime=%u, age=%u", + now, expiration, freshness, age)); + + if (cacheControlRequest.NoCache()) { + LOG((" validating, no-cache request")); + doValidation = true; + } else if (cacheControlRequest.MaxStale(&maxStaleRequest)) { + uint32_t staleTime = age > freshness ? age - freshness : 0; + doValidation = staleTime > maxStaleRequest; + LOG((" validating=%d, max-stale=%u requested", doValidation, + maxStaleRequest)); + } else if (cacheControlRequest.MaxAge(&maxAgeRequest)) { + // The input information for age and freshness calculation are in seconds. + // Hence, the internal logic can't have better resolution than seconds too. + // To make max-age=0 case work even for requests made in less than a second + // after the last response has been received, we use >= for compare. This + // is correct because of the rounding down of the age calculated value. + doValidation = age >= maxAgeRequest; + LOG((" validating=%d, max-age=%u requested", doValidation, maxAgeRequest)); + } else if (cacheControlRequest.MinFresh(&minFreshRequest)) { + uint32_t freshTime = freshness > age ? freshness - age : 0; + doValidation = freshTime < minFreshRequest; + LOG((" validating=%d, min-fresh=%u requested", doValidation, + minFreshRequest)); + } else if (now < expiration) { + doValidation = false; + LOG((" not validating, expire time not in the past")); + } else if (cachedResponseHead->MustValidateIfExpired()) { + doValidation = true; + } else if (cachedResponseHead->StaleWhileRevalidate(now, expiration) && + StaticPrefs::network_http_stale_while_revalidate_enabled()) { + LOG((" not validating, in the stall-while-revalidate window")); + doValidation = false; + if (performBackgroundRevalidation) { + *performBackgroundRevalidation = true; + } + } else if (loadFlags & nsIRequest::VALIDATE_ONCE_PER_SESSION) { + // If the cached response does not include expiration infor- + // mation, then we must validate the response, despite whether + // or not this is the first access this session. This behavior + // is consistent with existing browsers and is generally expected + // by web authors. + if (freshness == 0) + doValidation = true; + else + doValidation = fromPreviousSession; + } else { + doValidation = true; + } + + LOG(("%salidating based on expiration time\n", doValidation ? "V" : "Not v")); + return doValidation; +} + +nsresult GetHttpResponseHeadFromCacheEntry( + nsICacheEntry* entry, nsHttpResponseHead* cachedResponseHead) { + nsCString buf; + // A "original-response-headers" metadata element holds network original + // headers, i.e. the headers in the form as they arrieved from the network. We + // need to get the network original headers first, because we need to keep + // them in order. + nsresult rv = entry->GetMetaDataElement("original-response-headers", + getter_Copies(buf)); + if (NS_SUCCEEDED(rv)) { + rv = cachedResponseHead->ParseCachedOriginalHeaders((char*)buf.get()); + if (NS_FAILED(rv)) { + LOG((" failed to parse original-response-headers\n")); + } + } + + buf.Adopt(nullptr); + // A "response-head" metadata element holds response head, e.g. response + // status line and headers in the form Firefox uses them internally (no + // dupicate headers, etc.). + rv = entry->GetMetaDataElement("response-head", getter_Copies(buf)); + NS_ENSURE_SUCCESS(rv, rv); + + // Parse string stored in a "response-head" metadata element. + // These response headers will be merged with the orignal headers (i.e. the + // headers stored in a "original-response-headers" metadata element). + rv = cachedResponseHead->ParseCachedHead(buf.get()); + NS_ENSURE_SUCCESS(rv, rv); + buf.Adopt(nullptr); + + return NS_OK; +} + +nsresult CheckPartial(nsICacheEntry* aEntry, int64_t* aSize, + int64_t* aContentLength, + nsHttpResponseHead* responseHead) { + nsresult rv; + + rv = aEntry->GetDataSize(aSize); + + if (NS_ERROR_IN_PROGRESS == rv) { + *aSize = -1; + rv = NS_OK; + } + + NS_ENSURE_SUCCESS(rv, rv); + + if (!responseHead) { + return NS_ERROR_UNEXPECTED; + } + + *aContentLength = responseHead->ContentLength(); + + return NS_OK; +} + +void DetermineFramingAndImmutability(nsICacheEntry* entry, + nsHttpResponseHead* responseHead, + bool isHttps, bool* weaklyFramed, + bool* isImmutable) { + nsCString framedBuf; + nsresult rv = + entry->GetMetaDataElement("strongly-framed", getter_Copies(framedBuf)); + // describe this in terms of explicitly weakly framed so as to be backwards + // compatible with old cache contents which dont have strongly-framed makers + *weaklyFramed = NS_SUCCEEDED(rv) && framedBuf.EqualsLiteral("0"); + *isImmutable = !*weaklyFramed && isHttps && responseHead->Immutable(); +} + +bool IsBeforeLastActiveTabLoadOptimization(TimeStamp const& when) { + return gHttpHandler && + gHttpHandler->IsBeforeLastActiveTabLoadOptimization(when); +} + +nsCString ConvertRequestHeadToString(nsHttpRequestHead& aRequestHead, + bool aHasRequestBody, + bool aRequestBodyHasHeaders, + bool aUsingConnect) { + // Make sure that there is "Content-Length: 0" header in the requestHead + // in case of POST and PUT methods when there is no requestBody and + // requestHead doesn't contain "Transfer-Encoding" header. + // + // RFC1945 section 7.2.2: + // HTTP/1.0 requests containing an entity body must include a valid + // Content-Length header field. + // + // RFC2616 section 4.4: + // For compatibility with HTTP/1.0 applications, HTTP/1.1 requests + // containing a message-body MUST include a valid Content-Length header + // field unless the server is known to be HTTP/1.1 compliant. + if ((aRequestHead.IsPost() || aRequestHead.IsPut()) && !aHasRequestBody && + !aRequestHead.HasHeader(nsHttp::Transfer_Encoding)) { + DebugOnly rv = + aRequestHead.SetHeader(nsHttp::Content_Length, "0"_ns); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + + nsCString reqHeaderBuf; + reqHeaderBuf.Truncate(); + + // make sure we eliminate any proxy specific headers from + // the request if we are using CONNECT + aRequestHead.Flatten(reqHeaderBuf, aUsingConnect); + + if (!aRequestBodyHasHeaders || !aHasRequestBody) { + reqHeaderBuf.AppendLiteral("\r\n"); + } + + return reqHeaderBuf; +} + +void NotifyActiveTabLoadOptimization() { + if (gHttpHandler) { + gHttpHandler->NotifyActiveTabLoadOptimization(); + } +} + +TimeStamp const GetLastActiveTabLoadOptimizationHit() { + return gHttpHandler ? gHttpHandler->GetLastActiveTabLoadOptimizationHit() + : TimeStamp(); +} + +void SetLastActiveTabLoadOptimizationHit(TimeStamp const& when) { + if (gHttpHandler) { + gHttpHandler->SetLastActiveTabLoadOptimizationHit(when); + } +} + +} // namespace nsHttp + +template +void localEnsureBuffer(UniquePtr& buf, uint32_t newSize, uint32_t preserve, + uint32_t& objSize) { + if (objSize >= newSize) return; + + // Leave a little slop on the new allocation - add 2KB to + // what we need and then round the result up to a 4KB (page) + // boundary. + + objSize = (newSize + 2048 + 4095) & ~4095; + + static_assert(sizeof(T) == 1, "sizeof(T) must be 1"); + auto tmp = MakeUnique(objSize); + if (preserve) { + memcpy(tmp.get(), buf.get(), preserve); + } + buf = std::move(tmp); +} + +void EnsureBuffer(UniquePtr& buf, uint32_t newSize, uint32_t preserve, + uint32_t& objSize) { + localEnsureBuffer(buf, newSize, preserve, objSize); +} + +void EnsureBuffer(UniquePtr& buf, uint32_t newSize, + uint32_t preserve, uint32_t& objSize) { + localEnsureBuffer(buf, newSize, preserve, objSize); +} + +static bool IsTokenSymbol(signed char chr) { + if (chr < 33 || chr == 127 || chr == '(' || chr == ')' || chr == '<' || + chr == '>' || chr == '@' || chr == ',' || chr == ';' || chr == ':' || + chr == '"' || chr == '/' || chr == '[' || chr == ']' || chr == '?' || + chr == '=' || chr == '{' || chr == '}' || chr == '\\') { + return false; + } + return true; +} + +ParsedHeaderPair::ParsedHeaderPair(const char* name, int32_t nameLen, + const char* val, int32_t valLen, + bool isQuotedValue) + : mName(nsDependentCSubstring(nullptr, 0u)), + mValue(nsDependentCSubstring(nullptr, 0u)), + mIsQuotedValue(isQuotedValue) { + if (nameLen > 0) { + mName.Rebind(name, name + nameLen); + } + if (valLen > 0) { + if (mIsQuotedValue) { + RemoveQuotedStringEscapes(val, valLen); + mValue.Rebind(mUnquotedValue.BeginWriting(), mUnquotedValue.Length()); + } else { + mValue.Rebind(val, val + valLen); + } + } +} + +void ParsedHeaderPair::RemoveQuotedStringEscapes(const char* val, + int32_t valLen) { + mUnquotedValue.Truncate(); + const char* c = val; + for (int32_t i = 0; i < valLen; ++i) { + if (c[i] == '\\' && c[i + 1]) { + ++i; + } + mUnquotedValue.Append(c[i]); + } +} + +static void Tokenize( + const char* input, uint32_t inputLen, const char token, + const std::function& consumer) { + auto trimWhitespace = [](const char* in, uint32_t inLen, const char** out, + uint32_t* outLen) { + *out = in; + *outLen = inLen; + if (inLen == 0) { + return; + } + + // Trim leading space + while (nsCRT::IsAsciiSpace(**out)) { + (*out)++; + --(*outLen); + } + + // Trim tailing space + for (const char* i = *out + *outLen - 1; i >= *out; --i) { + if (!nsCRT::IsAsciiSpace(*i)) { + break; + } + --(*outLen); + } + }; + + const char* first = input; + bool inQuote = false; + const char* result = nullptr; + uint32_t resultLen = 0; + for (uint32_t index = 0; index < inputLen; ++index) { + if (inQuote && input[index] == '\\' && input[index + 1]) { + index++; + continue; + } + if (input[index] == '"') { + inQuote = !inQuote; + continue; + } + if (inQuote) { + continue; + } + if (input[index] == token) { + trimWhitespace(first, (input + index) - first, &result, &resultLen); + consumer(result, resultLen); + first = input + index + 1; + } + } + + trimWhitespace(first, (input + inputLen) - first, &result, &resultLen); + consumer(result, resultLen); +} + +ParsedHeaderValueList::ParsedHeaderValueList(const char* t, uint32_t len, + bool allowInvalidValue) { + if (!len) { + return; + } + + ParsedHeaderValueList* self = this; + auto consumer = [=](const char* output, uint32_t outputLength) { + self->ParseNameAndValue(output, allowInvalidValue); + }; + + Tokenize(t, len, ';', consumer); +} + +void ParsedHeaderValueList::ParseNameAndValue(const char* input, + bool allowInvalidValue) { + const char* nameStart = input; + const char* nameEnd = nullptr; + const char* valueStart = input; + const char* valueEnd = nullptr; + bool isQuotedString = false; + bool invalidValue = false; + + for (; *input && *input != ';' && *input != ',' && + !nsCRT::IsAsciiSpace(*input) && *input != '='; + input++) + ; + + nameEnd = input; + + if (!(nameEnd - nameStart)) { + return; + } + + // Check whether param name is a valid token. + for (const char* c = nameStart; c < nameEnd; c++) { + if (!IsTokenSymbol(*c)) { + nameEnd = c; + break; + } + } + + if (!(nameEnd - nameStart)) { + return; + } + + while (nsCRT::IsAsciiSpace(*input)) { + ++input; + } + + if (!*input || *input++ != '=') { + mValues.AppendElement( + ParsedHeaderPair(nameStart, nameEnd - nameStart, nullptr, 0, false)); + return; + } + + while (nsCRT::IsAsciiSpace(*input)) { + ++input; + } + + if (*input != '"') { + // The value is a token, not a quoted string. + valueStart = input; + for (valueEnd = input; *valueEnd && !nsCRT::IsAsciiSpace(*valueEnd) && + *valueEnd != ';' && *valueEnd != ','; + valueEnd++) + ; + if (!allowInvalidValue) { + for (const char* c = valueStart; c < valueEnd; c++) { + if (!IsTokenSymbol(*c)) { + valueEnd = c; + break; + } + } + } + } else { + bool foundQuotedEnd = false; + isQuotedString = true; + + ++input; + valueStart = input; + for (valueEnd = input; *valueEnd; ++valueEnd) { + if (*valueEnd == '\\' && *(valueEnd + 1)) { + ++valueEnd; + } else if (*valueEnd == '"') { + foundQuotedEnd = true; + break; + } + } + if (!foundQuotedEnd) { + invalidValue = true; + } + + input = valueEnd; + // *valueEnd != null means that *valueEnd is quote character. + if (*valueEnd) { + input++; + } + } + + if (invalidValue) { + valueEnd = valueStart; + } + + mValues.AppendElement(ParsedHeaderPair(nameStart, nameEnd - nameStart, + valueStart, valueEnd - valueStart, + isQuotedString)); +} + +ParsedHeaderValueListList::ParsedHeaderValueListList( + const nsCString& fullHeader, bool allowInvalidValue) + : mFull(fullHeader) { + auto& values = mValues; + auto consumer = [&values, allowInvalidValue](const char* output, + uint32_t outputLength) { + values.AppendElement( + ParsedHeaderValueList(output, outputLength, allowInvalidValue)); + }; + + Tokenize(mFull.BeginReading(), mFull.Length(), ',', consumer); +} + +void LogCallingScriptLocation(void* instance) { + if (!LOG4_ENABLED()) { + return; + } + + JSContext* cx = nsContentUtils::GetCurrentJSContext(); + if (!cx) { + return; + } + + nsAutoCString fileNameString; + uint32_t line = 0, col = 0; + if (!nsJSUtils::GetCallingLocation(cx, fileNameString, &line, &col)) { + return; + } + + LOG(("%p called from script: %s:%u:%u", instance, fileNameString.get(), line, + col)); +} + +void LogHeaders(const char* lineStart) { + nsAutoCString buf; + char* endOfLine; + while ((endOfLine = PL_strstr(lineStart, "\r\n"))) { + buf.Assign(lineStart, endOfLine - lineStart); + if (StaticPrefs::network_http_sanitize_headers_in_logs() && + (PL_strcasestr(buf.get(), "authorization: ") || + PL_strcasestr(buf.get(), "proxy-authorization: "))) { + char* p = PL_strchr(buf.get(), ' '); + while (p && *++p) { + *p = '*'; + } + } + LOG1((" %s\n", buf.get())); + lineStart = endOfLine + 2; + } +} + +nsresult HttpProxyResponseToErrorCode(uint32_t aStatusCode) { + // In proxy CONNECT case, we treat every response code except 200 as an error. + // Even if the proxy server returns other 2xx codes (i.e. 206), this function + // still returns an error code. + MOZ_ASSERT(aStatusCode != 200); + + nsresult rv; + switch (aStatusCode) { + case 300: + case 301: + case 302: + case 303: + case 307: + case 308: + // Bad redirect: not top-level, or it's a POST, bad/missing Location, + // or ProcessRedirect() failed for some other reason. Legal + // redirects that fail because site not available, etc., are handled + // elsewhere, in the regular codepath. + rv = NS_ERROR_CONNECTION_REFUSED; + break; + // Squid sends 404 if DNS fails (regular 404 from target is tunneled) + case 404: // HTTP/1.1: "Not Found" + // RFC 2616: "some deployed proxies are known to return 400 or + // 500 when DNS lookups time out." (Squid uses 500 if it runs + // out of sockets: so we have a conflict here). + case 400: // HTTP/1.1 "Bad Request" + case 500: // HTTP/1.1: "Internal Server Error" + rv = NS_ERROR_UNKNOWN_HOST; + break; + case 401: + rv = NS_ERROR_PROXY_UNAUTHORIZED; + break; + case 402: + rv = NS_ERROR_PROXY_PAYMENT_REQUIRED; + break; + case 403: + rv = NS_ERROR_PROXY_FORBIDDEN; + break; + case 405: + rv = NS_ERROR_PROXY_METHOD_NOT_ALLOWED; + break; + case 406: + rv = NS_ERROR_PROXY_NOT_ACCEPTABLE; + break; + case 407: // ProcessAuthentication() failed (e.g. no header) + rv = NS_ERROR_PROXY_AUTHENTICATION_FAILED; + break; + case 408: + rv = NS_ERROR_PROXY_REQUEST_TIMEOUT; + break; + case 409: + rv = NS_ERROR_PROXY_CONFLICT; + break; + case 410: + rv = NS_ERROR_PROXY_GONE; + break; + case 411: + rv = NS_ERROR_PROXY_LENGTH_REQUIRED; + break; + case 412: + rv = NS_ERROR_PROXY_PRECONDITION_FAILED; + break; + case 413: + rv = NS_ERROR_PROXY_REQUEST_ENTITY_TOO_LARGE; + break; + case 414: + rv = NS_ERROR_PROXY_REQUEST_URI_TOO_LONG; + break; + case 415: + rv = NS_ERROR_PROXY_UNSUPPORTED_MEDIA_TYPE; + break; + case 416: + rv = NS_ERROR_PROXY_REQUESTED_RANGE_NOT_SATISFIABLE; + break; + case 417: + rv = NS_ERROR_PROXY_EXPECTATION_FAILED; + break; + case 421: + rv = NS_ERROR_PROXY_MISDIRECTED_REQUEST; + break; + case 425: + rv = NS_ERROR_PROXY_TOO_EARLY; + break; + case 426: + rv = NS_ERROR_PROXY_UPGRADE_REQUIRED; + break; + case 428: + rv = NS_ERROR_PROXY_PRECONDITION_REQUIRED; + break; + case 429: + rv = NS_ERROR_PROXY_TOO_MANY_REQUESTS; + break; + case 431: + rv = NS_ERROR_PROXY_REQUEST_HEADER_FIELDS_TOO_LARGE; + break; + case 451: + rv = NS_ERROR_PROXY_UNAVAILABLE_FOR_LEGAL_REASONS; + break; + case 501: + rv = NS_ERROR_PROXY_NOT_IMPLEMENTED; + break; + case 502: + rv = NS_ERROR_PROXY_BAD_GATEWAY; + break; + case 503: + // Squid returns 503 if target request fails for anything but DNS. + /* User sees: "Failed to Connect: + * Firefox can't establish a connection to the server at + * www.foo.com. Though the site seems valid, the browser + * was unable to establish a connection." + */ + rv = NS_ERROR_CONNECTION_REFUSED; + break; + // RFC 2616 uses 504 for both DNS and target timeout, so not clear what to + // do here: picking target timeout, as DNS covered by 400/404/500 + case 504: + rv = NS_ERROR_PROXY_GATEWAY_TIMEOUT; + break; + case 505: + rv = NS_ERROR_PROXY_VERSION_NOT_SUPPORTED; + break; + case 506: + rv = NS_ERROR_PROXY_VARIANT_ALSO_NEGOTIATES; + break; + case 510: + rv = NS_ERROR_PROXY_NOT_EXTENDED; + break; + case 511: + rv = NS_ERROR_PROXY_NETWORK_AUTHENTICATION_REQUIRED; + break; + default: + rv = NS_ERROR_PROXY_CONNECTION_REFUSED; + break; + } + + return rv; +} + +Tuple SelectAlpnFromAlpnList( + const nsTArray& aAlpnList, bool aNoHttp2, bool aNoHttp3) { + nsCString h3Value; + nsCString h2Value; + nsCString h1Value; + for (const auto& npnToken : aAlpnList) { + bool isHttp3 = gHttpHandler->IsHttp3VersionSupported(npnToken); + if (isHttp3 && h3Value.IsEmpty()) { + h3Value.Assign(npnToken); + } + + uint32_t spdyIndex; + SpdyInformation* spdyInfo = gHttpHandler->SpdyInfo(); + if (NS_SUCCEEDED(spdyInfo->GetNPNIndex(npnToken, &spdyIndex)) && + spdyInfo->ProtocolEnabled(spdyIndex) && h2Value.IsEmpty()) { + h2Value.Assign(npnToken); + } + + if (npnToken.LowerCaseEqualsASCII("http/1.1") && h1Value.IsEmpty()) { + h1Value.Assign(npnToken); + } + } + + if (!h3Value.IsEmpty() && gHttpHandler->IsHttp3Enabled() && !aNoHttp3) { + return MakeTuple(h3Value, true); + } + + if (!h2Value.IsEmpty() && gHttpHandler->IsSpdyEnabled() && !aNoHttp2) { + return MakeTuple(h2Value, false); + } + + if (!h1Value.IsEmpty()) { + return MakeTuple(h1Value, false); + } + + // If we are here, there is no supported alpn can be used. + return MakeTuple(EmptyCString(), false); +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/nsHttp.h b/netwerk/protocol/http/nsHttp.h new file mode 100644 index 0000000000..182e8df9d2 --- /dev/null +++ b/netwerk/protocol/http/nsHttp.h @@ -0,0 +1,388 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=4 sw=2 sts=2 et cin: */ +/* 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/. */ + +#ifndef nsHttp_h__ +#define nsHttp_h__ + +#include +#include "prtime.h" +#include "nsString.h" +#include "nsError.h" +#include "nsTArray.h" +#include "mozilla/TimeStamp.h" +#include "mozilla/Tuple.h" +#include "mozilla/UniquePtr.h" + +class nsICacheEntry; + +namespace mozilla { + +class Mutex; + +namespace net { +class nsHttpResponseHead; +class nsHttpRequestHead; +class CacheControlParser; + +enum class HttpVersion { + UNKNOWN = 0, + v0_9 = 9, + v1_0 = 10, + v1_1 = 11, + v2_0 = 20, + v3_0 = 30 +}; + +enum class SpdyVersion { NONE = 0, HTTP_2 = 5 }; + +extern const uint32_t kHttp3VersionCount; +extern const nsCString kHttp3Versions[]; + +//----------------------------------------------------------------------------- +// http connection capabilities +//----------------------------------------------------------------------------- + +#define NS_HTTP_ALLOW_KEEPALIVE (1 << 0) +#define NS_HTTP_LARGE_KEEPALIVE (1 << 1) + +// a transaction with this caps flag will continue to own the connection, +// preventing it from being reclaimed, even after the transaction completes. +#define NS_HTTP_STICKY_CONNECTION (1 << 2) + +// a transaction with this caps flag will, upon opening a new connection, +// bypass the local DNS cache +#define NS_HTTP_REFRESH_DNS (1 << 3) + +// a transaction with this caps flag will not pass SSL client-certificates +// to the server (see bug #466080), but is may also be used for other things +#define NS_HTTP_LOAD_ANONYMOUS (1 << 4) + +// a transaction with this caps flag keeps timing information +#define NS_HTTP_TIMING_ENABLED (1 << 5) + +// a transaction with this flag blocks the initiation of other transactons +// in the same load group until it is complete +#define NS_HTTP_LOAD_AS_BLOCKING (1 << 6) + +// Disallow the use of the SPDY protocol. This is meant for the contexts +// such as HTTP upgrade which are nonsensical for SPDY, it is not the +// SPDY configuration variable. +#define NS_HTTP_DISALLOW_SPDY (1 << 7) + +// a transaction with this flag loads without respect to whether the load +// group is currently blocking on some resources +#define NS_HTTP_LOAD_UNBLOCKED (1 << 8) + +// This flag indicates the transaction should accept associated pushes +#define NS_HTTP_ONPUSH_LISTENER (1 << 9) + +// Transactions with this flag should react to errors without side effects +// First user is to prevent clearing of alt-svc cache on failed probe +#define NS_HTTP_ERROR_SOFTLY (1 << 10) + +// This corresponds to nsIHttpChannelInternal.beConservative +// it disables any cutting edge features that we are worried might result in +// interop problems with critical infrastructure +#define NS_HTTP_BE_CONSERVATIVE (1 << 11) + +// Transactions with this flag should be processed first. +#define NS_HTTP_URGENT_START (1 << 12) + +// A sticky connection of the transaction is explicitly allowed to be restarted +// on ERROR_NET_RESET. +#define NS_HTTP_CONNECTION_RESTARTABLE (1 << 13) + +// Allow re-using a spdy/http2 connection with NS_HTTP_ALLOW_KEEPALIVE not set. +// This is primarily used to allow connection sharing for websockets over http/2 +// without accidentally allowing it for websockets not over http/2 +#define NS_HTTP_ALLOW_SPDY_WITHOUT_KEEPALIVE (1 << 15) + +// Only permit CONNECTing to a proxy. A channel with this flag will not send an +// http request after CONNECT or setup tls. An http upgrade handler MUST be +// set. An ALPN header is set using the upgrade protocol. +#define NS_HTTP_CONNECT_ONLY (1 << 16) + +// The connection should not use IPv4. +#define NS_HTTP_DISABLE_IPV4 (1 << 17) + +// The connection should not use IPv6 +#define NS_HTTP_DISABLE_IPV6 (1 << 18) + +// Encodes the TRR mode. +#define NS_HTTP_TRR_MODE_MASK ((1 << 19) | (1 << 20)) + +// The connection could bring the peeked data for sniffing +#define NS_HTTP_CALL_CONTENT_SNIFFER (1 << 21) + +// Disallow the use of the HTTP3 protocol. This is meant for the contexts +// such as HTTP upgrade which are not supported by HTTP3. +#define NS_HTTP_DISALLOW_HTTP3 (1 << 22) + +// Force a transaction to stay in pending queue until the HTTPSSVC record is +// available. +#define NS_HTTP_WAIT_HTTPSSVC_RESULT (1 << 23) + +#define NS_HTTP_TRR_FLAGS_FROM_MODE(x) ((static_cast(x) & 3) << 19) + +#define NS_HTTP_TRR_MODE_FROM_FLAGS(x) \ + (static_cast((((x)&NS_HTTP_TRR_MODE_MASK) >> 19) & 3)) + +//----------------------------------------------------------------------------- +// some default values +//----------------------------------------------------------------------------- + +#define NS_HTTP_DEFAULT_PORT 80 +#define NS_HTTPS_DEFAULT_PORT 443 + +#define NS_HTTP_HEADER_SEP ',' + +//----------------------------------------------------------------------------- +// http atoms... +//----------------------------------------------------------------------------- + +struct nsHttpAtom { + nsHttpAtom() : _val(nullptr){}; + explicit nsHttpAtom(const char* val) : _val(val) {} + nsHttpAtom(const nsHttpAtom& other) = default; + + operator const char*() const { return _val; } + const char* get() const { return _val; } + + void operator=(const char* v) { _val = v; } + void operator=(const nsHttpAtom& a) { _val = a._val; } + + // private + const char* _val; +}; + +namespace nsHttp { +[[nodiscard]] nsresult CreateAtomTable(); +void DestroyAtomTable(); + +// The mutex is valid any time the Atom Table is valid +// This mutex is used in the unusual case that the network thread and +// main thread might access the same data +Mutex* GetLock(); + +// will dynamically add atoms to the table if they don't already exist +nsHttpAtom ResolveAtom(const char*); +inline nsHttpAtom ResolveAtom(const nsACString& s) { + return ResolveAtom(PromiseFlatCString(s).get()); +} + +// returns true if the specified token [start,end) is valid per RFC 2616 +// section 2.2 +bool IsValidToken(const char* start, const char* end); + +inline bool IsValidToken(const nsACString& s) { + return IsValidToken(s.BeginReading(), s.EndReading()); +} + +// Strip the leading or trailing HTTP whitespace per fetch spec section 2.2. +void TrimHTTPWhitespace(const nsACString& aSource, nsACString& aDest); + +// Returns true if the specified value is reasonable given the defintion +// in RFC 2616 section 4.2. Full strict validation is not performed +// currently as it would require full parsing of the value. +bool IsReasonableHeaderValue(const nsACString& s); + +// find the first instance (case-insensitive comparison) of the given +// |token| in the |input| string. the |token| is bounded by elements of +// |separators| and may appear at the beginning or end of the |input| +// string. null is returned if the |token| is not found. |input| may be +// null, in which case null is returned. +const char* FindToken(const char* input, const char* token, + const char* separators); + +// This function parses a string containing a decimal-valued, non-negative +// 64-bit integer. If the value would exceed INT64_MAX, then false is +// returned. Otherwise, this function returns true and stores the +// parsed value in |result|. The next unparsed character in |input| is +// optionally returned via |next| if |next| is non-null. +// +// TODO(darin): Replace this with something generic. +// +[[nodiscard]] bool ParseInt64(const char* input, const char** next, + int64_t* result); + +// Variant on ParseInt64 that expects the input string to contain nothing +// more than the value being parsed. +[[nodiscard]] inline bool ParseInt64(const char* input, int64_t* result) { + const char* next; + return ParseInt64(input, &next, result) && *next == '\0'; +} + +// Return whether the HTTP status code represents a permanent redirect +bool IsPermanentRedirect(uint32_t httpStatus); + +// Returns the APLN token which represents the used protocol version. +const char* GetProtocolVersion(HttpVersion pv); + +bool ValidationRequired(bool isForcedValid, + nsHttpResponseHead* cachedResponseHead, + uint32_t loadFlags, bool allowStaleCacheContent, + bool isImmutable, bool customConditionalRequest, + nsHttpRequestHead& requestHead, nsICacheEntry* entry, + CacheControlParser& cacheControlRequest, + bool fromPreviousSession, + bool* performBackgroundRevalidation = nullptr); + +nsresult GetHttpResponseHeadFromCacheEntry( + nsICacheEntry* entry, nsHttpResponseHead* cachedResponseHead); + +nsresult CheckPartial(nsICacheEntry* aEntry, int64_t* aSize, + int64_t* aContentLength, + nsHttpResponseHead* responseHead); + +void DetermineFramingAndImmutability(nsICacheEntry* entry, + nsHttpResponseHead* cachedResponseHead, + bool isHttps, bool* weaklyFramed, + bool* isImmutable); + +// Called when an optimization feature affecting active vs background tab load +// took place. Called only on the parent process and only updates +// mLastActiveTabLoadOptimizationHit timestamp to now. +void NotifyActiveTabLoadOptimization(); +TimeStamp const GetLastActiveTabLoadOptimizationHit(); +void SetLastActiveTabLoadOptimizationHit(TimeStamp const& when); +bool IsBeforeLastActiveTabLoadOptimization(TimeStamp const& when); + +// Declare all atoms +// +// The atom names and values are stored in nsHttpAtomList.h and are brought +// to you by the magic of C preprocessing. Add new atoms to nsHttpAtomList +// and all support logic will be auto-generated. +// +#define HTTP_ATOM(_name, _value) extern nsHttpAtom _name; +#include "nsHttpAtomList.h" +#undef HTTP_ATOM + +nsCString ConvertRequestHeadToString(nsHttpRequestHead& aRequestHead, + bool aHasRequestBody, + bool aRequestBodyHasHeaders, + bool aUsingConnect); + +template +using SendFunc = std::function; + +template +bool SendDataInChunks(const nsCString& aData, uint64_t aOffset, uint32_t aCount, + const SendFunc& aSendFunc) { + static uint32_t const kCopyChunkSize = 128 * 1024; + uint32_t toRead = std::min(aCount, kCopyChunkSize); + + uint32_t start = 0; + while (aCount) { + T data(Substring(aData, start, toRead)); + + if (!aSendFunc(data, aOffset, toRead)) { + return false; + } + + aOffset += toRead; + start += toRead; + aCount -= toRead; + toRead = std::min(aCount, kCopyChunkSize); + } + + return true; +} + +} // namespace nsHttp + +//----------------------------------------------------------------------------- +// utilities... +//----------------------------------------------------------------------------- + +static inline uint32_t PRTimeToSeconds(PRTime t_usec) { + return uint32_t(t_usec / PR_USEC_PER_SEC); +} + +#define NowInSeconds() PRTimeToSeconds(PR_Now()) + +// Round q-value to 2 decimal places; return 2 most significant digits as uint. +#define QVAL_TO_UINT(q) ((unsigned int)((q + 0.005) * 100.0)) + +#define HTTP_LWS " \t" +#define HTTP_HEADER_VALUE_SEPS HTTP_LWS "," + +void EnsureBuffer(UniquePtr& buf, uint32_t newSize, uint32_t preserve, + uint32_t& objSize); +void EnsureBuffer(UniquePtr& buf, uint32_t newSize, + uint32_t preserve, uint32_t& objSize); + +// h2=":443"; ma=60; single +// results in 3 mValues = {{h2, :443}, {ma, 60}, {single}} + +class ParsedHeaderPair { + public: + ParsedHeaderPair(const char* name, int32_t nameLen, const char* val, + int32_t valLen, bool isQuotedValue); + + ParsedHeaderPair(ParsedHeaderPair const& copy) + : mName(copy.mName), + mValue(copy.mValue), + mUnquotedValue(copy.mUnquotedValue), + mIsQuotedValue(copy.mIsQuotedValue) { + if (mIsQuotedValue) { + mValue.Rebind(mUnquotedValue.BeginReading(), mUnquotedValue.Length()); + } + } + + nsDependentCSubstring mName; + nsDependentCSubstring mValue; + + private: + nsCString mUnquotedValue; + bool mIsQuotedValue; + + void RemoveQuotedStringEscapes(const char* val, int32_t valLen); +}; + +class ParsedHeaderValueList { + public: + ParsedHeaderValueList(const char* t, uint32_t len, bool allowInvalidValue); + nsTArray mValues; + + private: + void ParseNameAndValue(const char* input, bool allowInvalidValue); +}; + +class ParsedHeaderValueListList { + public: + // RFC 7231 section 3.2.6 defines the syntax of the header field values. + // |allowInvalidValue| indicates whether the rule will be used to check + // the input text. + // Note that ParsedHeaderValueListList is currently used to parse + // Alt-Svc and Server-Timing header. |allowInvalidValue| is set to true + // when parsing Alt-Svc for historical reasons. + explicit ParsedHeaderValueListList(const nsCString& txt, + bool allowInvalidValue = true); + nsTArray mValues; + + private: + nsCString mFull; +}; + +void LogHeaders(const char* lineStart); + +// Convert HTTP response codes returned by a proxy to nsresult. +// This function should be only used when we get a failed response to the +// CONNECT method. +nsresult HttpProxyResponseToErrorCode(uint32_t aStatusCode); + +// Given a list of alpn-id, this function returns a supported alpn-id. If both +// h3 and h2 are enabled, h3 alpn is preferred. This function returns a +// Tuple. The first element is the alpn-id and the second one +// is a boolean to indicate if this alpn-id is for http3. If no supported +// alpn-id is found, the first element would be a n empty string. +Tuple SelectAlpnFromAlpnList( + const nsTArray& aAlpnList, bool aNoHttp2, bool aNoHttp3); + +} // namespace net +} // namespace mozilla + +#endif // nsHttp_h__ diff --git a/netwerk/protocol/http/nsHttpActivityDistributor.cpp b/netwerk/protocol/http/nsHttpActivityDistributor.cpp new file mode 100644 index 0000000000..702ecfa713 --- /dev/null +++ b/netwerk/protocol/http/nsHttpActivityDistributor.cpp @@ -0,0 +1,198 @@ +/* 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/. */ + +// HttpLog.h should generally be included first +#include "HttpLog.h" + +#include "mozilla/net/SocketProcessChild.h" +#include "mozilla/net/SocketProcessParent.h" +#include "nsHttpActivityDistributor.h" +#include "nsHttpHandler.h" +#include "nsCOMPtr.h" +#include "nsIOService.h" +#include "nsNetUtil.h" +#include "nsQueryObject.h" +#include "nsThreadUtils.h" +#include "NullHttpChannel.h" + +namespace mozilla { +namespace net { + +typedef nsMainThreadPtrHolder ObserverHolder; +typedef nsMainThreadPtrHandle ObserverHandle; + +NS_IMPL_ISUPPORTS(nsHttpActivityDistributor, nsIHttpActivityDistributor, + nsIHttpActivityObserver) + +nsHttpActivityDistributor::nsHttpActivityDistributor() + : mLock("nsHttpActivityDistributor.mLock"), mActivated(false) {} + +NS_IMETHODIMP +nsHttpActivityDistributor::ObserveActivity(nsISupports* aHttpChannel, + uint32_t aActivityType, + uint32_t aActivitySubtype, + PRTime aTimestamp, + uint64_t aExtraSizeData, + const nsACString& aExtraStringData) { + MOZ_ASSERT(XRE_IsParentProcess() && NS_IsMainThread()); + + for (size_t i = 0; i < mObservers.Length(); i++) { + Unused << mObservers[i]->ObserveActivity(aHttpChannel, aActivityType, + aActivitySubtype, aTimestamp, + aExtraSizeData, aExtraStringData); + } + return NS_OK; +} + +NS_IMETHODIMP +nsHttpActivityDistributor::ObserveActivityWithArgs( + const HttpActivityArgs& aArgs, uint32_t aActivityType, + uint32_t aActivitySubtype, PRTime aTimestamp, uint64_t aExtraSizeData, + const nsACString& aExtraStringData) { + HttpActivityArgs args(aArgs); + nsCString extraStringData(aExtraStringData); + if (XRE_IsSocketProcess()) { + auto task = [args{std::move(args)}, aActivityType, aActivitySubtype, + aTimestamp, aExtraSizeData, + extraStringData{std::move(extraStringData)}]() { + SocketProcessChild::GetSingleton()->SendObserveHttpActivity( + args, aActivityType, aActivitySubtype, aTimestamp, aExtraSizeData, + extraStringData); + }; + + if (!NS_IsMainThread()) { + return NS_DispatchToMainThread(NS_NewRunnableFunction( + "net::nsHttpActivityDistributor::ObserveActivityWithArgs", task)); + } + + task(); + return NS_OK; + } + + MOZ_ASSERT(XRE_IsParentProcess()); + + RefPtr self = this; + auto task = [args{std::move(args)}, aActivityType, aActivitySubtype, + aTimestamp, aExtraSizeData, + extraStringData{std::move(extraStringData)}, + self{std::move(self)}]() { + if (args.type() == HttpActivityArgs::Tuint64_t) { + nsWeakPtr weakPtr = gHttpHandler->GetWeakHttpChannel(args.get_uint64_t()); + if (nsCOMPtr channel = do_QueryReferent(weakPtr)) { + Unused << self->ObserveActivity(channel, aActivityType, + aActivitySubtype, aTimestamp, + aExtraSizeData, extraStringData); + } + } else if (args.type() == HttpActivityArgs::THttpActivity) { + nsCOMPtr uri; + nsAutoCString portStr(""_ns); + int32_t port = args.get_HttpActivity().port(); + bool endToEndSSL = args.get_HttpActivity().endToEndSSL(); + if (port != -1 && + ((endToEndSSL && port != 443) || (!endToEndSSL && port != 80))) { + portStr.AppendInt(port); + } + + nsresult rv = NS_NewURI(getter_AddRefs(uri), + (endToEndSSL ? "https://"_ns : "http://"_ns) + + args.get_HttpActivity().host() + portStr); + if (NS_FAILED(rv)) { + return; + } + + RefPtr channel = new NullHttpChannel(); + rv = channel->Init(uri, 0, nullptr, 0, nullptr); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + Unused << self->ObserveActivity( + nsCOMPtr(do_QueryObject(channel)), aActivityType, + aActivitySubtype, aTimestamp, aExtraSizeData, extraStringData); + } + }; + + if (!NS_IsMainThread()) { + return NS_DispatchToMainThread(NS_NewRunnableFunction( + "net::nsHttpActivityDistributor::ObserveActivityWithArgs", task)); + } + + task(); + return NS_OK; +} + +NS_IMETHODIMP +nsHttpActivityDistributor::GetIsActive(bool* isActive) { + NS_ENSURE_ARG_POINTER(isActive); + MutexAutoLock lock(mLock); + if (XRE_IsSocketProcess()) { + *isActive = mActivated; + return NS_OK; + } + + *isActive = !!mObservers.Length(); + return NS_OK; +} + +NS_IMETHODIMP nsHttpActivityDistributor::SetIsActive(bool aActived) { + MOZ_RELEASE_ASSERT(XRE_IsSocketProcess()); + + mActivated = aActived; + return NS_OK; +} + +NS_IMETHODIMP +nsHttpActivityDistributor::AddObserver(nsIHttpActivityObserver* aObserver) { + MOZ_ASSERT(XRE_IsParentProcess()); + + ObserverHandle observer( + new ObserverHolder("nsIHttpActivityObserver", aObserver)); + + bool wasEmpty = false; + { + MutexAutoLock lock(mLock); + wasEmpty = mObservers.IsEmpty(); + // XXX(Bug 1631371) Check if this should use a fallible operation as it + // pretended earlier. + mObservers.AppendElement(observer); + } + + if (nsIOService::UseSocketProcess() && wasEmpty) { + auto task = []() { + SocketProcessParent* parent = SocketProcessParent::GetSingleton(); + if (parent && parent->CanSend()) { + Unused << parent->SendOnHttpActivityDistributorActivated(true); + } + }; + gIOService->CallOrWaitForSocketProcess(task); + } + return NS_OK; +} + +NS_IMETHODIMP +nsHttpActivityDistributor::RemoveObserver(nsIHttpActivityObserver* aObserver) { + MOZ_ASSERT(XRE_IsParentProcess()); + + ObserverHandle observer( + new ObserverHolder("nsIHttpActivityObserver", aObserver)); + + bool isEmpty = false; + { + MutexAutoLock lock(mLock); + if (!mObservers.RemoveElement(observer)) return NS_ERROR_FAILURE; + isEmpty = mObservers.IsEmpty(); + } + + if (nsIOService::UseSocketProcess() && isEmpty) { + auto task = []() { + SocketProcessParent* parent = SocketProcessParent::GetSingleton(); + if (parent && parent->CanSend()) { + Unused << parent->SendOnHttpActivityDistributorActivated(false); + } + }; + gIOService->CallOrWaitForSocketProcess(task); + } + return NS_OK; +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/nsHttpActivityDistributor.h b/netwerk/protocol/http/nsHttpActivityDistributor.h new file mode 100644 index 0000000000..81ff3bf251 --- /dev/null +++ b/netwerk/protocol/http/nsHttpActivityDistributor.h @@ -0,0 +1,37 @@ +/* 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/. */ + +#ifndef nsHttpActivityDistributor_h__ +#define nsHttpActivityDistributor_h__ + +#include "nsIHttpActivityObserver.h" +#include "nsTArray.h" +#include "nsProxyRelease.h" +#include "mozilla/Mutex.h" + +namespace mozilla { +namespace net { + +class nsHttpActivityDistributor : public nsIHttpActivityDistributor { + public: + typedef nsTArray > + ObserverArray; + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIHTTPACTIVITYOBSERVER + NS_DECL_NSIHTTPACTIVITYDISTRIBUTOR + + nsHttpActivityDistributor(); + + protected: + virtual ~nsHttpActivityDistributor() = default; + + ObserverArray mObservers; + Mutex mLock; + bool mActivated; +}; + +} // namespace net +} // namespace mozilla + +#endif // nsHttpActivityDistributor_h__ diff --git a/netwerk/protocol/http/nsHttpAtomList.h b/netwerk/protocol/http/nsHttpAtomList.h new file mode 100644 index 0000000000..1619b7a94d --- /dev/null +++ b/netwerk/protocol/http/nsHttpAtomList.h @@ -0,0 +1,104 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +/****** + This file contains the list of all HTTP atoms + See nsHttp.h for access to the atoms. + + It is designed to be used as inline input to nsHttp.cpp *only* + through the magic of C preprocessing. + + All entries must be enclosed in the macro HTTP_ATOM which will have cruel + and unusual things done to it. + + The first argument to HTTP_ATOM is the C++ name of the atom. + The second argument to HTTP_ATOM is the string value of the atom. + ******/ + +HTTP_ATOM(Accept, "Accept") +HTTP_ATOM(Accept_Encoding, "Accept-Encoding") +HTTP_ATOM(Accept_Language, "Accept-Language") +HTTP_ATOM(Accept_Ranges, "Accept-Ranges") +HTTP_ATOM(Access_Control_Allow_Origin, "Access-Control-Allow-Origin") +HTTP_ATOM(Age, "Age") +HTTP_ATOM(Allow, "Allow") +HTTP_ATOM(Alternate_Service, "Alt-Svc") +HTTP_ATOM(Alternate_Service_Used, "Alt-Used") +HTTP_ATOM(Assoc_Req, "Assoc-Req") +HTTP_ATOM(Authentication, "Authentication") +HTTP_ATOM(Authorization, "Authorization") +HTTP_ATOM(Cache_Control, "Cache-Control") +HTTP_ATOM(Connection, "Connection") +HTTP_ATOM(Content_Disposition, "Content-Disposition") +HTTP_ATOM(Content_Encoding, "Content-Encoding") +HTTP_ATOM(Content_Language, "Content-Language") +HTTP_ATOM(Content_Length, "Content-Length") +HTTP_ATOM(Content_Location, "Content-Location") +HTTP_ATOM(Content_MD5, "Content-MD5") +HTTP_ATOM(Content_Range, "Content-Range") +HTTP_ATOM(Content_Type, "Content-Type") +HTTP_ATOM(Cookie, "Cookie") +HTTP_ATOM(Cross_Origin_Embedder_Policy, "Cross-Origin-Embedder-Policy") +HTTP_ATOM(Cross_Origin_Opener_Policy, "Cross-Origin-Opener-Policy") +HTTP_ATOM(Cross_Origin_Resource_Policy, "Cross-Origin-Resource-Policy") +HTTP_ATOM(Date, "Date") +HTTP_ATOM(DAV, "DAV") +HTTP_ATOM(Depth, "Depth") +HTTP_ATOM(Destination, "Destination") +HTTP_ATOM(DoNotTrack, "DNT") +HTTP_ATOM(ETag, "Etag") +HTTP_ATOM(Expect, "Expect") +HTTP_ATOM(Expires, "Expires") +HTTP_ATOM(From, "From") +HTTP_ATOM(Host, "Host") +HTTP_ATOM(If, "If") +HTTP_ATOM(If_Match, "If-Match") +HTTP_ATOM(If_Modified_Since, "If-Modified-Since") +HTTP_ATOM(If_None_Match, "If-None-Match") +HTTP_ATOM(If_None_Match_Any, "If-None-Match-Any") +HTTP_ATOM(If_Range, "If-Range") +HTTP_ATOM(If_Unmodified_Since, "If-Unmodified-Since") +HTTP_ATOM(Keep_Alive, "Keep-Alive") +HTTP_ATOM(Last_Modified, "Last-Modified") +HTTP_ATOM(Lock_Token, "Lock-Token") +HTTP_ATOM(Link, "Link") +HTTP_ATOM(Location, "Location") +HTTP_ATOM(Max_Forwards, "Max-Forwards") +HTTP_ATOM(Origin, "Origin") +HTTP_ATOM(Overwrite, "Overwrite") +HTTP_ATOM(Pragma, "Pragma") +HTTP_ATOM(Prefer, "Prefer") +HTTP_ATOM(Proxy_Authenticate, "Proxy-Authenticate") +HTTP_ATOM(Proxy_Authorization, "Proxy-Authorization") +HTTP_ATOM(Proxy_Connection, "Proxy-Connection") +HTTP_ATOM(Range, "Range") +HTTP_ATOM(Referer, "Referer") +HTTP_ATOM(Retry_After, "Retry-After") +HTTP_ATOM(Server, "Server") +HTTP_ATOM(Server_Timing, "Server-Timing") +HTTP_ATOM(Service_Worker_Allowed, "Service-Worker-Allowed") +HTTP_ATOM(Set_Cookie, "Set-Cookie") +HTTP_ATOM(Status_URI, "Status-URI") +HTTP_ATOM(Strict_Transport_Security, "Strict-Transport-Security") +HTTP_ATOM(TE, "TE") +HTTP_ATOM(Title, "Title") +HTTP_ATOM(Timeout, "Timeout") +HTTP_ATOM(Trailer, "Trailer") +HTTP_ATOM(Transfer_Encoding, "Transfer-Encoding") +HTTP_ATOM(URI, "URI") +HTTP_ATOM(Upgrade, "Upgrade") +HTTP_ATOM(User_Agent, "User-Agent") +HTTP_ATOM(Vary, "Vary") +HTTP_ATOM(Version, "Version") +HTTP_ATOM(WWW_Authenticate, "WWW-Authenticate") +HTTP_ATOM(Warning, "Warning") +HTTP_ATOM(X_Content_Type_Options, "X-Content-Type-Options") +HTTP_ATOM(X_Firefox_Spdy, "X-Firefox-Spdy") +HTTP_ATOM(X_Firefox_Spdy_Proxy, "X-Firefox-Spdy-Proxy") +HTTP_ATOM(X_Firefox_Early_Data, "X-Firefox-Early-Data") +HTTP_ATOM(X_Firefox_TCP_Fast_Open, "X-Firefox-TCP-Fast-Open") +HTTP_ATOM(X_Firefox_Http3, "X-Firefox-Http3") + +// methods are case sensitive and do not use atom table diff --git a/netwerk/protocol/http/nsHttpAuthCache.cpp b/netwerk/protocol/http/nsHttpAuthCache.cpp new file mode 100644 index 0000000000..6022a2ee48 --- /dev/null +++ b/netwerk/protocol/http/nsHttpAuthCache.cpp @@ -0,0 +1,462 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +// HttpLog.h should generally be included first +#include "HttpLog.h" + +#include "nsHttpAuthCache.h" + +#include +#include + +#include "mozilla/Attributes.h" +#include "nsString.h" +#include "nsCRT.h" +#include "nsIObserverService.h" +#include "mozilla/Services.h" +#include "mozilla/DebugOnly.h" +#include "nsNetUtil.h" + +namespace mozilla { +namespace net { + +static inline void GetAuthKey(const char* scheme, const char* host, + int32_t port, nsACString const& originSuffix, + nsCString& key) { + key.Truncate(); + key.Append(originSuffix); + key.Append(':'); + key.Append(scheme); + key.AppendLiteral("://"); + key.Append(host); + key.Append(':'); + key.AppendInt(port); +} + +// return true if the two strings are equal or both empty. an empty string +// is either null or zero length. +static bool StrEquivalent(const char16_t* a, const char16_t* b) { + static const char16_t emptyStr[] = {0}; + + if (!a) a = emptyStr; + if (!b) b = emptyStr; + + return nsCRT::strcmp(a, b) == 0; +} + +//----------------------------------------------------------------------------- +// nsHttpAuthCache +//----------------------------------------------------------------------------- + +nsHttpAuthCache::nsHttpAuthCache() + : mDB(128), mObserver(new OriginClearObserver(this)) { + LOG(("nsHttpAuthCache::nsHttpAuthCache %p", this)); + + nsCOMPtr obsSvc = services::GetObserverService(); + if (obsSvc) { + obsSvc->AddObserver(mObserver, "clear-origin-attributes-data", false); + } +} + +nsHttpAuthCache::~nsHttpAuthCache() { + LOG(("nsHttpAuthCache::~nsHttpAuthCache %p", this)); + + ClearAll(); + nsCOMPtr obsSvc = services::GetObserverService(); + if (obsSvc) { + obsSvc->RemoveObserver(mObserver, "clear-origin-attributes-data"); + mObserver->mOwner = nullptr; + } +} + +nsresult nsHttpAuthCache::GetAuthEntryForPath(const char* scheme, + const char* host, int32_t port, + const char* path, + nsACString const& originSuffix, + nsHttpAuthEntry** entry) { + LOG(("nsHttpAuthCache::GetAuthEntryForPath %p [path=%s]\n", this, path)); + + nsAutoCString key; + nsHttpAuthNode* node = LookupAuthNode(scheme, host, port, originSuffix, key); + if (!node) return NS_ERROR_NOT_AVAILABLE; + + *entry = node->LookupEntryByPath(path); + LOG((" returning %p", *entry)); + return *entry ? NS_OK : NS_ERROR_NOT_AVAILABLE; +} + +nsresult nsHttpAuthCache::GetAuthEntryForDomain(const char* scheme, + const char* host, int32_t port, + const char* realm, + nsACString const& originSuffix, + nsHttpAuthEntry** entry) + +{ + LOG(("nsHttpAuthCache::GetAuthEntryForDomain %p [realm=%s]\n", this, realm)); + + nsAutoCString key; + nsHttpAuthNode* node = LookupAuthNode(scheme, host, port, originSuffix, key); + if (!node) return NS_ERROR_NOT_AVAILABLE; + + *entry = node->LookupEntryByRealm(realm); + LOG((" returning %p", *entry)); + return *entry ? NS_OK : NS_ERROR_NOT_AVAILABLE; +} + +nsresult nsHttpAuthCache::SetAuthEntry(const char* scheme, const char* host, + int32_t port, const char* path, + const char* realm, const char* creds, + const char* challenge, + nsACString const& originSuffix, + const nsHttpAuthIdentity* ident, + nsISupports* metadata) { + nsresult rv; + + LOG(("nsHttpAuthCache::SetAuthEntry %p [realm=%s path=%s metadata=%p]\n", + this, realm, path, metadata)); + + nsAutoCString key; + nsHttpAuthNode* node = LookupAuthNode(scheme, host, port, originSuffix, key); + + if (!node) { + // create a new entry node and set the given entry + node = new nsHttpAuthNode(); + LOG((" new nsHttpAuthNode %p for key='%s'", node, key.get())); + rv = node->SetAuthEntry(path, realm, creds, challenge, ident, metadata); + if (NS_FAILED(rv)) + delete node; + else + mDB.Put(key, node); + return rv; + } + + return node->SetAuthEntry(path, realm, creds, challenge, ident, metadata); +} + +void nsHttpAuthCache::ClearAuthEntry(const char* scheme, const char* host, + int32_t port, const char* realm, + nsACString const& originSuffix) { + nsAutoCString key; + GetAuthKey(scheme, host, port, originSuffix, key); + LOG(("nsHttpAuthCache::ClearAuthEntry %p key='%s'\n", this, key.get())); + mDB.Remove(key); +} + +void nsHttpAuthCache::ClearAll() { + LOG(("nsHttpAuthCache::ClearAll %p\n", this)); + mDB.Clear(); +} + +//----------------------------------------------------------------------------- +// nsHttpAuthCache +//----------------------------------------------------------------------------- + +nsHttpAuthNode* nsHttpAuthCache::LookupAuthNode(const char* scheme, + const char* host, int32_t port, + nsACString const& originSuffix, + nsCString& key) { + GetAuthKey(scheme, host, port, originSuffix, key); + nsHttpAuthNode* result = mDB.Get(key); + + LOG(("nsHttpAuthCache::LookupAuthNode %p key='%s' found node=%p", this, + key.get(), result)); + return result; +} + +NS_IMPL_ISUPPORTS(nsHttpAuthCache::OriginClearObserver, nsIObserver) + +NS_IMETHODIMP +nsHttpAuthCache::OriginClearObserver::Observe(nsISupports* subject, + const char* topic, + const char16_t* data_unicode) { + NS_ENSURE_TRUE(mOwner, NS_ERROR_NOT_AVAILABLE); + + OriginAttributesPattern pattern; + if (!pattern.Init(nsDependentString(data_unicode))) { + NS_ERROR("Cannot parse origin attributes pattern"); + return NS_ERROR_FAILURE; + } + + mOwner->ClearOriginData(pattern); + return NS_OK; +} + +void nsHttpAuthCache::ClearOriginData(OriginAttributesPattern const& pattern) { + LOG(("nsHttpAuthCache::ClearOriginData %p", this)); + + for (auto iter = mDB.Iter(); !iter.Done(); iter.Next()) { + const nsACString& key = iter.Key(); + + // Extract the origin attributes suffix from the key. + int32_t colon = key.FindChar(':'); + MOZ_ASSERT(colon != kNotFound); + nsDependentCSubstring oaSuffix = StringHead(key, colon); + + // Build the OriginAttributes object of it... + OriginAttributes oa; + DebugOnly rv = oa.PopulateFromSuffix(oaSuffix); + MOZ_ASSERT(rv); + + // ...and match it against the given pattern. + if (pattern.Matches(oa)) { + iter.Remove(); + } + } +} + +void nsHttpAuthCache::CollectKeys(nsTArray& aValue) { + for (auto iter = mDB.Iter(); !iter.Done(); iter.Next()) { + aValue.AppendElement(iter.Key()); + } +} + +//----------------------------------------------------------------------------- +// nsHttpAuthIdentity +//----------------------------------------------------------------------------- + +nsresult nsHttpAuthIdentity::Set(const char16_t* domain, const char16_t* user, + const char16_t* pass) { + char16_t *newUser, *newPass, *newDomain; + + int domainLen = domain ? NS_strlen(domain) : 0; + int userLen = user ? NS_strlen(user) : 0; + int passLen = pass ? NS_strlen(pass) : 0; + + int len = userLen + 1 + passLen + 1 + domainLen + 1; + newUser = (char16_t*)malloc(len * sizeof(char16_t)); + if (!newUser) return NS_ERROR_OUT_OF_MEMORY; + + if (user) memcpy(newUser, user, userLen * sizeof(char16_t)); + newUser[userLen] = 0; + + newPass = &newUser[userLen + 1]; + if (pass) memcpy(newPass, pass, passLen * sizeof(char16_t)); + newPass[passLen] = 0; + + newDomain = &newPass[passLen + 1]; + if (domain) memcpy(newDomain, domain, domainLen * sizeof(char16_t)); + newDomain[domainLen] = 0; + + // wait until the end to clear member vars in case input params + // reference our members! + if (mUser) free(mUser); + mUser = newUser; + mPass = newPass; + mDomain = newDomain; + return NS_OK; +} + +void nsHttpAuthIdentity::Clear() { + if (mUser) { + free(mUser); + mUser = nullptr; + mPass = nullptr; + mDomain = nullptr; + } +} + +bool nsHttpAuthIdentity::Equals(const nsHttpAuthIdentity& ident) const { + // we could probably optimize this with a single loop, but why bother? + return StrEquivalent(mUser, ident.mUser) && + StrEquivalent(mPass, ident.mPass) && + StrEquivalent(mDomain, ident.mDomain); +} + +//----------------------------------------------------------------------------- +// nsHttpAuthEntry +//----------------------------------------------------------------------------- + +nsHttpAuthEntry::~nsHttpAuthEntry() { + if (mRealm) free(mRealm); + + while (mRoot) { + nsHttpAuthPath* ap = mRoot; + mRoot = mRoot->mNext; + free(ap); + } +} + +nsresult nsHttpAuthEntry::AddPath(const char* aPath) { + // null path matches empty path + if (!aPath) aPath = ""; + + nsHttpAuthPath* tempPtr = mRoot; + while (tempPtr) { + const char* curpath = tempPtr->mPath; + if (strncmp(aPath, curpath, strlen(curpath)) == 0) + return NS_OK; // subpath already exists in the list + + tempPtr = tempPtr->mNext; + } + + // Append the aPath + nsHttpAuthPath* newAuthPath; + int newpathLen = strlen(aPath); + newAuthPath = (nsHttpAuthPath*)malloc(sizeof(nsHttpAuthPath) + newpathLen); + if (!newAuthPath) return NS_ERROR_OUT_OF_MEMORY; + + memcpy(newAuthPath->mPath, aPath, newpathLen + 1); + newAuthPath->mNext = nullptr; + + if (!mRoot) + mRoot = newAuthPath; // first entry + else + mTail->mNext = newAuthPath; // Append newAuthPath + + // update the tail pointer. + mTail = newAuthPath; + return NS_OK; +} + +nsresult nsHttpAuthEntry::Set(const char* path, const char* realm, + const char* creds, const char* chall, + const nsHttpAuthIdentity* ident, + nsISupports* metadata) { + char *newRealm, *newCreds, *newChall; + + int realmLen = realm ? strlen(realm) : 0; + int credsLen = creds ? strlen(creds) : 0; + int challLen = chall ? strlen(chall) : 0; + + int len = realmLen + 1 + credsLen + 1 + challLen + 1; + newRealm = (char*)malloc(len); + if (!newRealm) return NS_ERROR_OUT_OF_MEMORY; + + if (realm) memcpy(newRealm, realm, realmLen); + newRealm[realmLen] = 0; + + newCreds = &newRealm[realmLen + 1]; + if (creds) memcpy(newCreds, creds, credsLen); + newCreds[credsLen] = 0; + + newChall = &newCreds[credsLen + 1]; + if (chall) memcpy(newChall, chall, challLen); + newChall[challLen] = 0; + + nsresult rv = NS_OK; + if (ident) { + rv = mIdent.Set(*ident); + } else if (mIdent.IsEmpty()) { + // If we are not given an identity and our cached identity has not been + // initialized yet (so is currently empty), initialize it now by + // filling it with nulls. We need to do that because consumers expect + // that mIdent is initialized after this function returns. + rv = mIdent.Set(nullptr, nullptr, nullptr); + } + if (NS_FAILED(rv)) { + free(newRealm); + return rv; + } + + rv = AddPath(path); + if (NS_FAILED(rv)) { + free(newRealm); + return rv; + } + + // wait until the end to clear member vars in case input params + // reference our members! + if (mRealm) free(mRealm); + + mRealm = newRealm; + mCreds = newCreds; + mChallenge = newChall; + mMetaData = metadata; + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsHttpAuthNode +//----------------------------------------------------------------------------- + +nsHttpAuthNode::nsHttpAuthNode() { + LOG(("Creating nsHttpAuthNode @%p\n", this)); +} + +nsHttpAuthNode::~nsHttpAuthNode() { + LOG(("Destroying nsHttpAuthNode @%p\n", this)); + + mList.Clear(); +} + +nsHttpAuthEntry* nsHttpAuthNode::LookupEntryByPath(const char* path) { + // null path matches empty path + if (!path) path = ""; + + // look for an entry that either matches or contains this directory. + // ie. we'll give out credentials if the given directory is a sub- + // directory of an existing entry. + for (uint32_t i = 0; i < mList.Length(); ++i) { + const auto& entry = mList[i]; + nsHttpAuthPath* authPath = entry->RootPath(); + while (authPath) { + const char* entryPath = authPath->mPath; + // proxy auth entries have no path, so require exact match on + // empty path string. + if (entryPath[0] == '\0') { + if (path[0] == '\0') { + return entry.get(); + } + } else if (strncmp(path, entryPath, strlen(entryPath)) == 0) { + return entry.get(); + } + + authPath = authPath->mNext; + } + } + return nullptr; +} + +nsHttpAuthNode::EntryList::const_iterator nsHttpAuthNode::LookupEntryItrByRealm( + const char* realm) const { + // null realm matches empty realm + if (!realm) realm = ""; + + return std::find_if(mList.cbegin(), mList.cend(), [&realm](const auto& val) { + return strcmp(realm, val->Realm()) == 0; + }); +} + +nsHttpAuthEntry* nsHttpAuthNode::LookupEntryByRealm(const char* realm) { + auto itr = LookupEntryItrByRealm(realm); + if (itr != mList.cend()) { + return itr->get(); + } + + return nullptr; +} + +nsresult nsHttpAuthNode::SetAuthEntry(const char* path, const char* realm, + const char* creds, const char* challenge, + const nsHttpAuthIdentity* ident, + nsISupports* metadata) { + // look for an entry with a matching realm + nsHttpAuthEntry* entry = LookupEntryByRealm(realm); + if (!entry) { + // We want the latest identity be at the begining of the list so that + // the newest working credentials are sent first on new requests. + // Changing a realm is sometimes used to "timeout" authrozization. + mList.InsertElementAt( + 0, WrapUnique(new nsHttpAuthEntry(path, realm, creds, challenge, ident, + metadata))); + } else { + // update the entry... + nsresult rv = entry->Set(path, realm, creds, challenge, ident, metadata); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; +} + +void nsHttpAuthNode::ClearAuthEntry(const char* realm) { + auto idx = LookupEntryItrByRealm(realm); + if (idx != mList.cend()) { + mList.RemoveElementAt(idx); + } +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/nsHttpAuthCache.h b/netwerk/protocol/http/nsHttpAuthCache.h new file mode 100644 index 0000000000..3834c08178 --- /dev/null +++ b/netwerk/protocol/http/nsHttpAuthCache.h @@ -0,0 +1,232 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef nsHttpAuthCache_h__ +#define nsHttpAuthCache_h__ + +#include "nsError.h" +#include "nsTArray.h" +#include "nsClassHashtable.h" +#include "nsCOMPtr.h" +#include "nsHashKeys.h" +#include "nsStringFwd.h" +#include "nsIObserver.h" + +namespace mozilla { + +class OriginAttributesPattern; + +namespace net { + +struct nsHttpAuthPath { + struct nsHttpAuthPath* mNext; + char mPath[1]; +}; + +//----------------------------------------------------------------------------- +// nsHttpAuthIdentity +//----------------------------------------------------------------------------- + +class nsHttpAuthIdentity { + public: + nsHttpAuthIdentity() : mUser(nullptr), mPass(nullptr), mDomain(nullptr) {} + nsHttpAuthIdentity(const char16_t* domain, const char16_t* user, + const char16_t* password) + : mUser(nullptr), mPass{nullptr}, mDomain{nullptr} { + DebugOnly rv = Set(domain, user, password); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + ~nsHttpAuthIdentity() { Clear(); } + + const char16_t* Domain() const { return mDomain; } + const char16_t* User() const { return mUser; } + const char16_t* Password() const { return mPass; } + + [[nodiscard]] nsresult Set(const char16_t* domain, const char16_t* user, + const char16_t* password); + [[nodiscard]] nsresult Set(const nsHttpAuthIdentity& other) { + return Set(other.mDomain, other.mUser, other.mPass); + } + void Clear(); + + bool Equals(const nsHttpAuthIdentity& other) const; + bool IsEmpty() const { return !mUser; } + + private: + // allocated as one contiguous blob, starting at mUser. + char16_t* mUser; + char16_t* mPass; + char16_t* mDomain; +}; + +//----------------------------------------------------------------------------- +// nsHttpAuthEntry +//----------------------------------------------------------------------------- + +class nsHttpAuthEntry { + public: + const char* Realm() const { return mRealm; } + const char* Creds() const { return mCreds; } + const char* Challenge() const { return mChallenge; } + const char16_t* Domain() const { return mIdent.Domain(); } + const char16_t* User() const { return mIdent.User(); } + const char16_t* Pass() const { return mIdent.Password(); } + nsHttpAuthPath* RootPath() { return mRoot; } + + const nsHttpAuthIdentity& Identity() const { return mIdent; } + + [[nodiscard]] nsresult AddPath(const char* aPath); + + nsCOMPtr mMetaData; + + private: + nsHttpAuthEntry(const char* path, const char* realm, const char* creds, + const char* challenge, const nsHttpAuthIdentity* ident, + nsISupports* metadata) + : mRoot(nullptr), + mTail(nullptr), + mRealm(nullptr), + mCreds{nullptr}, + mChallenge{nullptr} { + DebugOnly rv = + Set(path, realm, creds, challenge, ident, metadata); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + ~nsHttpAuthEntry(); + + [[nodiscard]] nsresult Set(const char* path, const char* realm, + const char* creds, const char* challenge, + const nsHttpAuthIdentity* ident, + nsISupports* metadata); + + nsHttpAuthIdentity mIdent; + + nsHttpAuthPath* mRoot; // root pointer + nsHttpAuthPath* mTail; // tail pointer + + // allocated together in one blob, starting with mRealm. + char* mRealm; + char* mCreds; + char* mChallenge; + + friend class nsHttpAuthNode; + friend class nsHttpAuthCache; + friend class mozilla::DefaultDelete; // needs to call the + // destructor +}; + +//----------------------------------------------------------------------------- +// nsHttpAuthNode +//----------------------------------------------------------------------------- + +class nsHttpAuthNode { + private: + using EntryList = nsTArray>; + + nsHttpAuthNode(); + ~nsHttpAuthNode(); + + // path can be null, in which case we'll search for an entry + // with a null path. + nsHttpAuthEntry* LookupEntryByPath(const char* path); + + // realm must not be null + nsHttpAuthEntry* LookupEntryByRealm(const char* realm); + EntryList::const_iterator LookupEntryItrByRealm(const char* realm) const; + + // if a matching entry is found, then credentials will be changed. + [[nodiscard]] nsresult SetAuthEntry(const char* path, const char* realm, + const char* credentials, + const char* challenge, + const nsHttpAuthIdentity* ident, + nsISupports* metadata); + + void ClearAuthEntry(const char* realm); + + uint32_t EntryCount() { return mList.Length(); } + + private: + EntryList mList; + + friend class nsHttpAuthCache; + friend class mozilla::DefaultDelete; // needs to call the + // destructor +}; + +//----------------------------------------------------------------------------- +// nsHttpAuthCache +// (holds a hash table from host:port to nsHttpAuthNode) +//----------------------------------------------------------------------------- + +class nsHttpAuthCache { + public: + nsHttpAuthCache(); + ~nsHttpAuthCache(); + + // |scheme|, |host|, and |port| are required + // |path| can be null + // |entry| is either null or a weak reference + [[nodiscard]] nsresult GetAuthEntryForPath(const char* scheme, + const char* host, int32_t port, + const char* path, + nsACString const& originSuffix, + nsHttpAuthEntry** entry); + + // |scheme|, |host|, and |port| are required + // |realm| must not be null + // |entry| is either null or a weak reference + [[nodiscard]] nsresult GetAuthEntryForDomain(const char* scheme, + const char* host, int32_t port, + const char* realm, + nsACString const& originSuffix, + nsHttpAuthEntry** entry); + + // |scheme|, |host|, and |port| are required + // |path| can be null + // |realm| must not be null + // if |credentials|, |user|, |pass|, and |challenge| are each + // null, then the entry is deleted. + [[nodiscard]] nsresult SetAuthEntry( + const char* scheme, const char* host, int32_t port, const char* directory, + const char* realm, const char* credentials, const char* challenge, + nsACString const& originSuffix, const nsHttpAuthIdentity* ident, + nsISupports* metadata); + + void ClearAuthEntry(const char* scheme, const char* host, int32_t port, + const char* realm, nsACString const& originSuffix); + + // expire all existing auth list entries including proxy auths. + void ClearAll(); + + // For testing only. + void CollectKeys(nsTArray& aValue); + + private: + nsHttpAuthNode* LookupAuthNode(const char* scheme, const char* host, + int32_t port, nsACString const& originSuffix, + nsCString& key); + + class OriginClearObserver : public nsIObserver { + virtual ~OriginClearObserver() = default; + + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIOBSERVER + explicit OriginClearObserver(nsHttpAuthCache* aOwner) : mOwner(aOwner) {} + nsHttpAuthCache* mOwner; + }; + + void ClearOriginData(OriginAttributesPattern const& pattern); + + private: + using AuthNodeTable = nsClassHashtable; + AuthNodeTable mDB; // "host:port" --> nsHttpAuthNode + RefPtr mObserver; +}; + +} // namespace net +} // namespace mozilla + +#endif // nsHttpAuthCache_h__ diff --git a/netwerk/protocol/http/nsHttpAuthManager.cpp b/netwerk/protocol/http/nsHttpAuthManager.cpp new file mode 100644 index 0000000000..a08c16139e --- /dev/null +++ b/netwerk/protocol/http/nsHttpAuthManager.cpp @@ -0,0 +1,113 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +// HttpLog.h should generally be included first +#include "HttpLog.h" + +#include "nsHttpHandler.h" +#include "nsHttpAuthManager.h" +#include "nsNetUtil.h" +#include "nsIPrincipal.h" + +namespace mozilla { +namespace net { + +NS_IMPL_ISUPPORTS(nsHttpAuthManager, nsIHttpAuthManager) + +nsHttpAuthManager::nsHttpAuthManager() + : mAuthCache(nullptr), mPrivateAuthCache(nullptr) {} + +nsresult nsHttpAuthManager::Init() { + // get reference to the auth cache. we assume that we will live + // as long as gHttpHandler. instantiate it if necessary. + + if (!gHttpHandler) { + nsresult rv; + nsCOMPtr ios = do_GetIOService(&rv); + if (NS_FAILED(rv)) return rv; + + nsCOMPtr handler; + rv = ios->GetProtocolHandler("http", getter_AddRefs(handler)); + if (NS_FAILED(rv)) return rv; + + // maybe someone is overriding our HTTP handler implementation? + NS_ENSURE_TRUE(gHttpHandler, NS_ERROR_UNEXPECTED); + } + + mAuthCache = gHttpHandler->AuthCache(false); + mPrivateAuthCache = gHttpHandler->AuthCache(true); + NS_ENSURE_TRUE(mAuthCache, NS_ERROR_FAILURE); + NS_ENSURE_TRUE(mPrivateAuthCache, NS_ERROR_FAILURE); + return NS_OK; +} + +NS_IMETHODIMP +nsHttpAuthManager::GetAuthIdentity( + const nsACString& aScheme, const nsACString& aHost, int32_t aPort, + const nsACString& aAuthType, const nsACString& aRealm, + const nsACString& aPath, nsAString& aUserDomain, nsAString& aUserName, + nsAString& aUserPassword, bool aIsPrivate, nsIPrincipal* aPrincipal) { + nsHttpAuthCache* auth_cache = aIsPrivate ? mPrivateAuthCache : mAuthCache; + nsHttpAuthEntry* entry = nullptr; + nsresult rv; + + nsAutoCString originSuffix; + if (aPrincipal) { + aPrincipal->OriginAttributesRef().CreateSuffix(originSuffix); + } + + if (!aPath.IsEmpty()) + rv = auth_cache->GetAuthEntryForPath( + PromiseFlatCString(aScheme).get(), PromiseFlatCString(aHost).get(), + aPort, PromiseFlatCString(aPath).get(), originSuffix, &entry); + else + rv = auth_cache->GetAuthEntryForDomain( + PromiseFlatCString(aScheme).get(), PromiseFlatCString(aHost).get(), + aPort, PromiseFlatCString(aRealm).get(), originSuffix, &entry); + + if (NS_FAILED(rv)) return rv; + if (!entry) return NS_ERROR_UNEXPECTED; + + aUserDomain.Assign(entry->Domain()); + aUserName.Assign(entry->User()); + aUserPassword.Assign(entry->Pass()); + return NS_OK; +} + +NS_IMETHODIMP +nsHttpAuthManager::SetAuthIdentity( + const nsACString& aScheme, const nsACString& aHost, int32_t aPort, + const nsACString& aAuthType, const nsACString& aRealm, + const nsACString& aPath, const nsAString& aUserDomain, + const nsAString& aUserName, const nsAString& aUserPassword, bool aIsPrivate, + nsIPrincipal* aPrincipal) { + nsHttpAuthIdentity ident(PromiseFlatString(aUserDomain).get(), + PromiseFlatString(aUserName).get(), + PromiseFlatString(aUserPassword).get()); + + nsAutoCString originSuffix; + if (aPrincipal) { + aPrincipal->OriginAttributesRef().CreateSuffix(originSuffix); + } + + nsHttpAuthCache* auth_cache = aIsPrivate ? mPrivateAuthCache : mAuthCache; + return auth_cache->SetAuthEntry( + PromiseFlatCString(aScheme).get(), PromiseFlatCString(aHost).get(), aPort, + PromiseFlatCString(aPath).get(), PromiseFlatCString(aRealm).get(), + nullptr, // credentials + nullptr, // challenge + originSuffix, &ident, + nullptr); // metadata +} + +NS_IMETHODIMP +nsHttpAuthManager::ClearAll() { + mAuthCache->ClearAll(); + mPrivateAuthCache->ClearAll(); + return NS_OK; +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/nsHttpAuthManager.h b/netwerk/protocol/http/nsHttpAuthManager.h new file mode 100644 index 0000000000..d28bacbef8 --- /dev/null +++ b/netwerk/protocol/http/nsHttpAuthManager.h @@ -0,0 +1,34 @@ +/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef nsHttpAuthManager_h__ +#define nsHttpAuthManager_h__ + +#include "nsIHttpAuthManager.h" + +namespace mozilla { +namespace net { + +class nsHttpAuthCache; + +class nsHttpAuthManager : public nsIHttpAuthManager { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIHTTPAUTHMANAGER + + nsHttpAuthManager(); + [[nodiscard]] nsresult Init(); + + protected: + virtual ~nsHttpAuthManager() = default; + + nsHttpAuthCache* mAuthCache; + nsHttpAuthCache* mPrivateAuthCache; +}; + +} // namespace net +} // namespace mozilla + +#endif // nsHttpAuthManager_h__ diff --git a/netwerk/protocol/http/nsHttpBasicAuth.cpp b/netwerk/protocol/http/nsHttpBasicAuth.cpp new file mode 100644 index 0000000000..c2e631e6a5 --- /dev/null +++ b/netwerk/protocol/http/nsHttpBasicAuth.cpp @@ -0,0 +1,103 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +// HttpLog.h should generally be included first +#include "HttpLog.h" + +#include "nsHttpBasicAuth.h" +#include "plstr.h" +#include "nsString.h" +#include "mozilla/Base64.h" +#include "mozilla/ClearOnShutdown.h" + +namespace mozilla { +namespace net { + +StaticRefPtr nsHttpBasicAuth::gSingleton; + +already_AddRefed nsHttpBasicAuth::GetOrCreate() { + nsCOMPtr authenticator; + if (gSingleton) { + authenticator = gSingleton; + } else { + gSingleton = new nsHttpBasicAuth(); + ClearOnShutdown(&gSingleton); + authenticator = gSingleton; + } + + return authenticator.forget(); +} + +//----------------------------------------------------------------------------- +// nsHttpBasicAuth::nsISupports +//----------------------------------------------------------------------------- + +NS_IMPL_ISUPPORTS(nsHttpBasicAuth, nsIHttpAuthenticator) + +//----------------------------------------------------------------------------- +// nsHttpBasicAuth::nsIHttpAuthenticator +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsHttpBasicAuth::ChallengeReceived(nsIHttpAuthenticableChannel* authChannel, + const char* challenge, bool isProxyAuth, + nsISupports** sessionState, + nsISupports** continuationState, + bool* identityInvalid) { + // if challenged, then the username:password that was sent must + // have been wrong. + *identityInvalid = true; + return NS_OK; +} +NS_IMETHODIMP +nsHttpBasicAuth::GenerateCredentialsAsync( + nsIHttpAuthenticableChannel* authChannel, + nsIHttpAuthenticatorCallback* aCallback, const char* challenge, + bool isProxyAuth, const char16_t* domain, const char16_t* username, + const char16_t* password, nsISupports* sessionState, + nsISupports* continuationState, nsICancelable** aCancellable) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsHttpBasicAuth::GenerateCredentials( + nsIHttpAuthenticableChannel* authChannel, const char* challenge, + bool isProxyAuth, const char16_t* domain, const char16_t* user, + const char16_t* password, nsISupports** sessionState, + nsISupports** continuationState, uint32_t* aFlags, char** creds) + +{ + LOG(("nsHttpBasicAuth::GenerateCredentials [challenge=%s]\n", challenge)); + + NS_ENSURE_ARG_POINTER(creds); + + *aFlags = 0; + + // we only know how to deal with Basic auth for http. + bool isBasicAuth = !PL_strncasecmp(challenge, "basic", 5); + NS_ENSURE_TRUE(isBasicAuth, NS_ERROR_UNEXPECTED); + + // we work with UTF-8 around here + nsAutoCString userpass; + CopyUTF16toUTF8(mozilla::MakeStringSpan(user), userpass); + userpass.Append(':'); // always send a ':' (see bug 129565) + AppendUTF16toUTF8(mozilla::MakeStringSpan(password), userpass); + + nsAutoCString authString{"Basic "_ns}; + nsresult rv = Base64EncodeAppend(userpass, authString); + NS_ENSURE_SUCCESS(rv, rv); + + *creds = ToNewCString(authString); + return NS_OK; +} + +NS_IMETHODIMP +nsHttpBasicAuth::GetAuthFlags(uint32_t* flags) { + *flags = REQUEST_BASED | REUSABLE_CREDENTIALS | REUSABLE_CHALLENGE; + return NS_OK; +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/nsHttpBasicAuth.h b/netwerk/protocol/http/nsHttpBasicAuth.h new file mode 100644 index 0000000000..cac5db2c49 --- /dev/null +++ b/netwerk/protocol/http/nsHttpBasicAuth.h @@ -0,0 +1,39 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef nsBasicAuth_h__ +#define nsBasicAuth_h__ + +#include "nsIHttpAuthenticator.h" +#include "mozilla/RefPtr.h" +#include "mozilla/StaticPtr.h" + +namespace mozilla { +namespace net { + +//----------------------------------------------------------------------------- +// The nsHttpBasicAuth class produces HTTP Basic-auth responses for a username/ +// (optional)password pair, BASE64("user:pass"). +//----------------------------------------------------------------------------- + +class nsHttpBasicAuth : public nsIHttpAuthenticator { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIHTTPAUTHENTICATOR + + nsHttpBasicAuth() = default; + + static already_AddRefed GetOrCreate(); + + private: + virtual ~nsHttpBasicAuth() = default; + + static StaticRefPtr gSingleton; +}; + +} // namespace net +} // namespace mozilla + +#endif // !nsHttpBasicAuth_h__ diff --git a/netwerk/protocol/http/nsHttpChannel.cpp b/netwerk/protocol/http/nsHttpChannel.cpp new file mode 100644 index 0000000000..775af08373 --- /dev/null +++ b/netwerk/protocol/http/nsHttpChannel.cpp @@ -0,0 +1,10154 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set expandtab ts=4 sw=2 sts=2 cin: */ +/* 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/. */ + +// HttpLog.h should generally be included first +#include "HttpLog.h" + +#include + +#include "DocumentChannelParent.h" +#include "mozilla/MozPromiseInlines.h" // For MozPromise::FromDomPromise +#include "mozilla/ScopeExit.h" +#include "mozilla/Sprintf.h" +#include "mozilla/dom/nsCSPContext.h" +#include "mozilla/dom/nsCSPService.h" +#include "mozilla/StoragePrincipalHelper.h" + +#include "nsHttp.h" +#include "nsHttpChannel.h" +#include "nsHttpChannelAuthProvider.h" +#include "nsHttpHandler.h" +#include "nsString.h" +#include "nsIApplicationCacheContainer.h" +#include "nsICacheStorageService.h" +#include "nsICacheStorage.h" +#include "nsICacheEntry.h" +#include "nsICryptoHash.h" +#include "nsIEffectiveTLDService.h" +#include "nsIHttpHeaderVisitor.h" +#include "nsINetworkInterceptController.h" +#include "nsINSSErrorsService.h" +#include "nsIStringBundle.h" +#include "nsIStreamListenerTee.h" +#include "nsISeekableStream.h" +#include "nsIProtocolProxyService2.h" +#include "nsMimeTypes.h" +#include "nsNetCID.h" +#include "nsNetUtil.h" +#include "nsIURL.h" +#include "nsIStreamTransportService.h" +#include "prnetdb.h" +#include "nsEscape.h" +#include "nsStreamUtils.h" +#include "nsIOService.h" +#include "nsDNSPrefetch.h" +#include "nsChannelClassifier.h" +#include "nsIRedirectResultListener.h" +#include "mozilla/TimeStamp.h" +#include "nsError.h" +#include "nsPrintfCString.h" +#include "nsAlgorithm.h" +#include "nsQueryObject.h" +#include "nsThreadUtils.h" +#include "GeckoProfiler.h" +#include "nsIConsoleService.h" +#include "mozilla/AntiTrackingUtils.h" +#include "mozilla/Attributes.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/ContentBlocking.h" +#include "mozilla/DebugOnly.h" +#include "mozilla/Preferences.h" +#include "mozilla/Services.h" +#include "mozilla/StaticPrefs_browser.h" +#include "mozilla/StaticPrefs_fission.h" +#include "mozilla/StaticPrefs_network.h" +#include "mozilla/StaticPrefs_privacy.h" +#include "mozilla/StaticPrefs_security.h" +#include "sslt.h" +#include "nsCharSeparatedTokenizer.h" +#include "nsContentUtils.h" +#include "nsContentSecurityManager.h" +#include "nsIClassOfService.h" +#include "nsIPrincipal.h" +#include "nsIScriptError.h" +#include "nsIScriptSecurityManager.h" +#include "nsITransportSecurityInfo.h" +#include "nsIWebProgressListener.h" +#include "LoadContextInfo.h" +#include "netCore.h" +#include "nsHttpTransaction.h" +#include "nsICancelable.h" +#include "nsIHttpChannelInternal.h" +#include "nsIPrompt.h" +#include "nsInputStreamPump.h" +#include "nsURLHelper.h" +#include "nsISocketTransport.h" +#include "nsIStreamConverterService.h" +#include "nsISiteSecurityService.h" +#include "nsString.h" +#include "CacheObserver.h" +#include "mozilla/dom/PerformanceStorage.h" +#include "mozilla/Telemetry.h" +#include "AlternateServices.h" +#include "InterceptedChannel.h" +#include "nsIHttpPushListener.h" +#include "nsIX509Cert.h" +#include "ScopedNSSTypes.h" +#include "nsIDeprecationWarner.h" +#include "nsIDNSRecord.h" +#include "mozilla/dom/Document.h" +#include "nsICompressConvStats.h" +#include "nsCORSListenerProxy.h" +#include "nsISocketProvider.h" +#include "mozilla/extensions/StreamFilterParent.h" +#include "mozilla/net/Predictor.h" +#include "mozilla/MathAlgorithms.h" +#include "mozilla/NullPrincipal.h" +#include "CacheControlParser.h" +#include "nsMixedContentBlocker.h" +#include "CacheStorageService.h" +#include "HttpChannelParent.h" +#include "HttpTransactionParent.h" +#include "ParentChannelListener.h" +#include "InterceptedHttpChannel.h" +#include "../../cache2/CacheFileUtils.h" +#include "nsIMultiplexInputStream.h" +#include "nsINetworkLinkService.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/ServiceWorkerUtils.h" +#include "mozilla/dom/nsHTTPSOnlyStreamListener.h" +#include "mozilla/net/AsyncUrlChannelClassifier.h" +#include "mozilla/net/CookieJarSettings.h" +#include "mozilla/net/NeckoChannelParams.h" +#include "mozilla/net/UrlClassifierFeatureFactory.h" +#include "HttpTrafficAnalyzer.h" +#include "mozilla/net/SocketProcessParent.h" +#include "js/Conversions.h" +#include "mozilla/dom/SecFetch.h" +#include "mozilla/net/TRRService.h" + +#ifdef MOZ_TASK_TRACER +# include "GeckoTaskTracer.h" +#endif + +namespace mozilla { + +using namespace dom; + +namespace net { + +namespace { + +// True if the local cache should be bypassed when processing a request. +#define BYPASS_LOCAL_CACHE(loadFlags, isPreferCacheLoadOverBypass) \ + (loadFlags & (nsIRequest::LOAD_BYPASS_CACHE | \ + nsICachingChannel::LOAD_BYPASS_LOCAL_CACHE) && \ + !((loadFlags & nsIRequest::LOAD_FROM_CACHE) && \ + isPreferCacheLoadOverBypass)) + +#define RECOVER_FROM_CACHE_FILE_ERROR(result) \ + ((result) == NS_ERROR_FILE_NOT_FOUND || \ + (result) == NS_ERROR_FILE_CORRUPTED || (result) == NS_ERROR_OUT_OF_MEMORY) + +#define WRONG_RACING_RESPONSE_SOURCE(req) \ + (mRaceCacheWithNetwork && \ + (((mFirstResponseSource == RESPONSE_FROM_CACHE) && (req != mCachePump)) || \ + ((mFirstResponseSource == RESPONSE_FROM_NETWORK) && \ + (req != mTransactionPump)))) + +static NS_DEFINE_CID(kStreamListenerTeeCID, NS_STREAMLISTENERTEE_CID); + +void AccumulateCacheHitTelemetry(CacheDisposition hitOrMiss, + nsIChannel* aChannel) { + nsCString key("UNKNOWN"); + + nsCOMPtr loadInfo = aChannel->LoadInfo(); + + nsAutoCString contentType; + if (NS_SUCCEEDED(aChannel->GetContentType(contentType))) { + if (nsContentUtils::IsJavascriptMIMEType( + NS_ConvertUTF8toUTF16(contentType))) { + key.AssignLiteral("JAVASCRIPT"); + } else if (StringBeginsWith(contentType, "text/css"_ns) || + (loadInfo && loadInfo->GetExternalContentPolicyType() == + ExtContentPolicy::TYPE_STYLESHEET)) { + key.AssignLiteral("STYLESHEET"); + } else if (StringBeginsWith(contentType, "application/wasm"_ns)) { + key.AssignLiteral("WASM"); + } else if (StringBeginsWith(contentType, "image/"_ns)) { + key.AssignLiteral("IMAGE"); + } else if (StringBeginsWith(contentType, "video/"_ns)) { + key.AssignLiteral("MEDIA"); + } else if (StringBeginsWith(contentType, "audio/"_ns)) { + key.AssignLiteral("MEDIA"); + } else if (!StringBeginsWith(contentType, + nsLiteralCString(UNKNOWN_CONTENT_TYPE))) { + key.AssignLiteral("OTHER"); + } + } + + Telemetry::LABELS_HTTP_CACHE_DISPOSITION_3 label = + Telemetry::LABELS_HTTP_CACHE_DISPOSITION_3::Unresolved; + switch (hitOrMiss) { + case kCacheUnresolved: + label = Telemetry::LABELS_HTTP_CACHE_DISPOSITION_3::Unresolved; + break; + case kCacheHit: + label = Telemetry::LABELS_HTTP_CACHE_DISPOSITION_3::Hit; + break; + case kCacheHitViaReval: + label = Telemetry::LABELS_HTTP_CACHE_DISPOSITION_3::HitViaReval; + break; + case kCacheMissedViaReval: + label = Telemetry::LABELS_HTTP_CACHE_DISPOSITION_3::MissedViaReval; + break; + case kCacheMissed: + label = Telemetry::LABELS_HTTP_CACHE_DISPOSITION_3::Missed; + break; + case kCacheUnknown: + label = Telemetry::LABELS_HTTP_CACHE_DISPOSITION_3::Unknown; + break; + } + + Telemetry::AccumulateCategoricalKeyed(key, label); + Telemetry::AccumulateCategoricalKeyed("ALL"_ns, label); +} + +// Computes and returns a SHA1 hash of the input buffer. The input buffer +// must be a null-terminated string. +nsresult Hash(const char* buf, nsACString& hash) { + nsresult rv; + + nsCOMPtr hasher = + do_CreateInstance(NS_CRYPTO_HASH_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = hasher->Init(nsICryptoHash::SHA1); + NS_ENSURE_SUCCESS(rv, rv); + + rv = hasher->Update(reinterpret_cast(buf), strlen(buf)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = hasher->Finish(true, hash); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +bool IsInSubpathOfAppCacheManifest(nsIApplicationCache* cache, + nsACString const& uriSpec) { + MOZ_ASSERT(cache); + + nsresult rv; + + nsCOMPtr uri; + rv = NS_NewURI(getter_AddRefs(uri), uriSpec); + if (NS_FAILED(rv)) { + return false; + } + + nsCOMPtr url(do_QueryInterface(uri, &rv)); + if (NS_FAILED(rv)) { + return false; + } + + nsAutoCString directory; + rv = url->GetDirectory(directory); + if (NS_FAILED(rv)) { + return false; + } + + nsCOMPtr manifestURI; + rv = cache->GetManifestURI(getter_AddRefs(manifestURI)); + if (NS_FAILED(rv)) { + return false; + } + + nsCOMPtr manifestURL(do_QueryInterface(manifestURI, &rv)); + if (NS_FAILED(rv)) { + return false; + } + + nsAutoCString manifestDirectory; + rv = manifestURL->GetDirectory(manifestDirectory); + if (NS_FAILED(rv)) { + return false; + } + + return StringBeginsWith(directory, manifestDirectory); +} + +} // unnamed namespace + +// We only treat 3xx responses as redirects if they have a Location header and +// the status code is in a whitelist. +bool nsHttpChannel::WillRedirect(const nsHttpResponseHead& response) { + return IsRedirectStatus(response.Status()) && + response.HasHeader(nsHttp::Location); +} + +nsresult StoreAuthorizationMetaData(nsICacheEntry* entry, + nsHttpRequestHead* requestHead); + +class AutoRedirectVetoNotifier { + public: + explicit AutoRedirectVetoNotifier(nsHttpChannel* channel) + : mChannel(channel) { + if (mChannel->LoadHasAutoRedirectVetoNotifier()) { + MOZ_CRASH("Nested AutoRedirectVetoNotifier on the stack"); + mChannel = nullptr; + return; + } + + mChannel->StoreHasAutoRedirectVetoNotifier(true); + } + ~AutoRedirectVetoNotifier() { ReportRedirectResult(false); } + void RedirectSucceeded() { ReportRedirectResult(true); } + + private: + nsHttpChannel* mChannel; + void ReportRedirectResult(bool succeeded); +}; + +void AutoRedirectVetoNotifier::ReportRedirectResult(bool succeeded) { + if (!mChannel) return; + + mChannel->mRedirectChannel = nullptr; + + if (succeeded) { + mChannel->RemoveAsNonTailRequest(); + } + + nsCOMPtr vetoHook; + NS_QueryNotificationCallbacks(mChannel, NS_GET_IID(nsIRedirectResultListener), + getter_AddRefs(vetoHook)); + + nsHttpChannel* channel = mChannel; + mChannel = nullptr; + + if (vetoHook) vetoHook->OnRedirectResult(succeeded); + + // Drop after the notification + channel->StoreHasAutoRedirectVetoNotifier(false); +} + +//----------------------------------------------------------------------------- +// nsHttpChannel +//----------------------------------------------------------------------------- + +nsHttpChannel::nsHttpChannel() + : HttpAsyncAborter(this), + mCacheDisposition(kCacheUnresolved), + mLogicalOffset(0), + mPostID(0), + mRequestTime(0), + mOfflineCacheLastModifiedTime(0), + mSuspendTotalTime(0), + mRedirectType(0), + mCacheOpenWithPriority(false), + mCacheQueueSizeWhenOpen(0), + mCachedContentIsValid(false), + mIsAuthChannel(false), + mAuthRetryPending(false), + mPushedStreamId(0), + mLocalBlocklist(false), + mOnTailUnblock(nullptr), + mWarningReporter(nullptr), + mIsReadingFromCache(false), + mFirstResponseSource(RESPONSE_PENDING), + mRaceCacheWithNetwork(false), + mRaceDelay(0), + mIgnoreCacheEntry(false), + mRCWNLock("nsHttpChannel.mRCWNLock"), + mProxyConnectResponseCode(0), + mDidReval(false) { + LOG(("Creating nsHttpChannel [this=%p]\n", this)); + mChannelCreationTime = PR_Now(); + mChannelCreationTimestamp = TimeStamp::Now(); +} + +nsHttpChannel::~nsHttpChannel() { + LOG(("Destroying nsHttpChannel [this=%p]\n", this)); + + if (mAuthProvider) { + DebugOnly rv = mAuthProvider->Disconnect(NS_ERROR_ABORT); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + + ReleaseMainThreadOnlyReferences(); + if (gHttpHandler) { + gHttpHandler->RemoveHttpChannel(mChannelId); + } +} + +void nsHttpChannel::ReleaseMainThreadOnlyReferences() { + if (NS_IsMainThread()) { + // Already on main thread, let dtor to + // take care of releasing references + return; + } + + nsTArray> arrayToRelease; + arrayToRelease.AppendElement(mApplicationCacheForWrite.forget()); + arrayToRelease.AppendElement(mAuthProvider.forget()); + arrayToRelease.AppendElement(mRedirectChannel.forget()); + arrayToRelease.AppendElement(mPreflightChannel.forget()); + arrayToRelease.AppendElement(mDNSPrefetch.forget()); + + NS_DispatchToMainThread(new ProxyReleaseRunnable(std::move(arrayToRelease))); +} + +nsresult nsHttpChannel::Init(nsIURI* uri, uint32_t caps, nsProxyInfo* proxyInfo, + uint32_t proxyResolveFlags, nsIURI* proxyURI, + uint64_t channelId, + ExtContentPolicyType aContentPolicyType) { + nsresult rv = HttpBaseChannel::Init(uri, caps, proxyInfo, proxyResolveFlags, + proxyURI, channelId, aContentPolicyType); + if (NS_FAILED(rv)) return rv; + + LOG1(("nsHttpChannel::Init [this=%p]\n", this)); + + return rv; +} + +nsresult nsHttpChannel::AddSecurityMessage(const nsAString& aMessageTag, + const nsAString& aMessageCategory) { + if (mWarningReporter) { + return mWarningReporter->ReportSecurityMessage(aMessageTag, + aMessageCategory); + } + return HttpBaseChannel::AddSecurityMessage(aMessageTag, aMessageCategory); +} + +NS_IMETHODIMP +nsHttpChannel::LogBlockedCORSRequest(const nsAString& aMessage, + const nsACString& aCategory) { + if (mWarningReporter) { + return mWarningReporter->LogBlockedCORSRequest(aMessage, aCategory); + } + return NS_ERROR_UNEXPECTED; +} + +NS_IMETHODIMP +nsHttpChannel::LogMimeTypeMismatch(const nsACString& aMessageName, + bool aWarning, const nsAString& aURL, + const nsAString& aContentType) { + if (mWarningReporter) { + return mWarningReporter->LogMimeTypeMismatch(aMessageName, aWarning, aURL, + aContentType); + } + return NS_ERROR_UNEXPECTED; +} + +//----------------------------------------------------------------------------- +// nsHttpChannel +//----------------------------------------------------------------------------- + +nsresult nsHttpChannel::PrepareToConnect() { + LOG(("nsHttpChannel::PrepareToConnect [this=%p]\n", this)); + + AddCookiesToRequest(); + + // notify "http-on-modify-request" observers + CallOnModifyRequestObservers(); + + if (mCanceled) { + return mStatus; + } + + if (mSuspendCount) { + // We abandon the connection here if there was one. + LOG(("Waiting until resume OnBeforeConnect [this=%p]\n", this)); + MOZ_ASSERT(!mCallOnResume); + mCallOnResume = [](nsHttpChannel* self) { + self->HandleOnBeforeConnect(); + return NS_OK; + }; + return NS_OK; + } + + return OnBeforeConnect(); +} + +void nsHttpChannel::HandleContinueCancellingByURLClassifier( + nsresult aErrorCode) { + MOZ_ASSERT( + UrlClassifierFeatureFactory::IsClassifierBlockingErrorCode(aErrorCode)); + MOZ_ASSERT(!mCallOnResume, "How did that happen?"); + + if (mSuspendCount) { + LOG( + ("Waiting until resume HandleContinueCancellingByURLClassifier " + "[this=%p]\n", + this)); + mCallOnResume = [aErrorCode](nsHttpChannel* self) { + self->HandleContinueCancellingByURLClassifier(aErrorCode); + return NS_OK; + }; + return; + } + + LOG(("nsHttpChannel::HandleContinueCancellingByURLClassifier [this=%p]\n", + this)); + ContinueCancellingByURLClassifier(aErrorCode); +} + +void nsHttpChannel::HandleOnBeforeConnect() { + MOZ_ASSERT(!mCallOnResume, "How did that happen?"); + nsresult rv; + + if (mSuspendCount) { + LOG(("Waiting until resume OnBeforeConnect [this=%p]\n", this)); + mCallOnResume = [](nsHttpChannel* self) { + self->HandleOnBeforeConnect(); + return NS_OK; + }; + return; + } + + LOG(("nsHttpChannel::HandleOnBeforeConnect [this=%p]\n", this)); + rv = OnBeforeConnect(); + if (NS_FAILED(rv)) { + CloseCacheEntry(false); + Unused << AsyncAbort(rv); + } +} + +nsresult nsHttpChannel::OnBeforeConnect() { + nsresult rv; + + // Check if request was cancelled during suspend AFTER on-modify-request + if (mCanceled) { + return mStatus; + } + + // Check to see if we should redirect this channel elsewhere by + // nsIHttpChannel.redirectTo API request + if (mAPIRedirectToURI) { + return AsyncCall(&nsHttpChannel::HandleAsyncAPIRedirect); + } + + // Note that we are only setting the "Upgrade-Insecure-Requests" request + // header for *all* navigational requests instead of all requests as + // defined in the spec, see: + // https://www.w3.org/TR/upgrade-insecure-requests/#preference + ExtContentPolicyType type = mLoadInfo->GetExternalContentPolicyType(); + + if (type == ExtContentPolicy::TYPE_DOCUMENT || + type == ExtContentPolicy::TYPE_SUBDOCUMENT) { + rv = SetRequestHeader("Upgrade-Insecure-Requests"_ns, "1"_ns, false); + NS_ENSURE_SUCCESS(rv, rv); + } + + SecFetch::AddSecFetchHeader(this); + + nsCOMPtr resultPrincipal; + if (!mURI->SchemeIs("https")) { + nsContentUtils::GetSecurityManager()->GetChannelResultPrincipal( + this, getter_AddRefs(resultPrincipal)); + } + + // At this point it is no longer possible to call + // HttpBaseChannel::UpgradeToSecure. + StoreUpgradableToSecure(false); + bool shouldUpgrade = LoadUpgradeToSecure(); + if (mURI->SchemeIs("http")) { + OriginAttributes originAttributes; + if (!StoragePrincipalHelper::GetOriginAttributesForHSTS(this, + originAttributes)) { + return NS_ERROR_FAILURE; + } + + if (!shouldUpgrade) { + // Make sure http channel is released on main thread. + // See bug 1539148 for details. + nsMainThreadPtrHandle self( + new nsMainThreadPtrHolder( + "nsHttpChannel::OnBeforeConnect::self", this)); + auto resultCallback = [self(self)](bool aResult, nsresult aStatus) { + MOZ_ASSERT(NS_IsMainThread()); + + nsresult rv = self->MaybeUseHTTPSRRForUpgrade(aResult, aStatus); + if (NS_FAILED(rv)) { + self->CloseCacheEntry(false); + Unused << self->AsyncAbort(rv); + } + }; + + bool willCallback = false; + rv = NS_ShouldSecureUpgrade(mURI, mLoadInfo, resultPrincipal, + mPrivateBrowsing, LoadAllowSTS(), + originAttributes, shouldUpgrade, + std::move(resultCallback), willCallback); + // If the request gets upgraded because of the HTTPS-Only mode, but no + // event listener has been registered so far, we want to do that here. + uint32_t httpOnlyStatus = mLoadInfo->GetHttpsOnlyStatus(); + if (httpOnlyStatus & + nsILoadInfo::HTTPS_ONLY_UPGRADED_LISTENER_NOT_REGISTERED) { + RefPtr httpsOnlyListener = + new nsHTTPSOnlyStreamListener(mListener, mLoadInfo); + mListener = httpsOnlyListener; + + httpOnlyStatus ^= + nsILoadInfo::HTTPS_ONLY_UPGRADED_LISTENER_NOT_REGISTERED; + httpOnlyStatus |= nsILoadInfo::HTTPS_ONLY_UPGRADED_LISTENER_REGISTERED; + mLoadInfo->SetHttpsOnlyStatus(httpOnlyStatus); + } + LOG( + ("nsHttpChannel::OnBeforeConnect " + "[this=%p willCallback=%d rv=%" PRIx32 "]\n", + this, willCallback, static_cast(rv))); + + if (NS_FAILED(rv) || MOZ_UNLIKELY(willCallback)) { + return rv; + } + } + } + + return MaybeUseHTTPSRRForUpgrade(shouldUpgrade, NS_OK); +} + +nsresult nsHttpChannel::MaybeUseHTTPSRRForUpgrade(bool aShouldUpgrade, + nsresult aStatus) { + if (NS_FAILED(aStatus)) { + return aStatus; + } + + if (mURI->SchemeIs("https") || aShouldUpgrade || !LoadUseHTTPSSVC()) { + return ContinueOnBeforeConnect(aShouldUpgrade, aStatus); + } + + if (mHTTPSSVCRecord.isSome()) { + LOG(( + "nsHttpChannel::MaybeUseHTTPSRRForUpgrade [%p] mHTTPSSVCRecord is some", + this)); + StoreWaitHTTPSSVCRecord(false); + bool hasHTTPSRR = (mHTTPSSVCRecord.ref() != nullptr); + return ContinueOnBeforeConnect(hasHTTPSRR, aStatus); + } + + LOG(("nsHttpChannel::MaybeUseHTTPSRRForUpgrade [%p] wait for HTTPS RR", + this)); + StoreWaitHTTPSSVCRecord(true); + return NS_OK; +} + +nsresult nsHttpChannel::ContinueOnBeforeConnect(bool aShouldUpgrade, + nsresult aStatus) { + LOG( + ("nsHttpChannel::ContinueOnBeforeConnect " + "[this=%p aShouldUpgrade=%d rv=%" PRIx32 "]\n", + this, aShouldUpgrade, static_cast(aStatus))); + + MOZ_ASSERT(!LoadWaitHTTPSSVCRecord()); + + if (NS_FAILED(aStatus)) { + return aStatus; + } + + if (aShouldUpgrade) { + return AsyncCall(&nsHttpChannel::HandleAsyncRedirectChannelToHttps); + } + + // ensure that we are using a valid hostname + if (!net_IsValidHostName(nsDependentCString(mConnectionInfo->Origin()))) + return NS_ERROR_UNKNOWN_HOST; + + if (mUpgradeProtocolCallback) { + // Websockets can run over HTTP/2, but other upgrades can't. + if (mUpgradeProtocol.EqualsLiteral("websocket") && + gHttpHandler->IsH2WebsocketsEnabled()) { + // Need to tell the conn manager that we're ok with http/2 even with + // the allow keepalive bit not set. That bit needs to stay off, + // though, in case we end up having to fallback to http/1.1 (where + // we absolutely do want to disable keepalive). + mCaps |= NS_HTTP_ALLOW_SPDY_WITHOUT_KEEPALIVE; + } else { + mCaps |= NS_HTTP_DISALLOW_SPDY; + } + // Upgrades cannot use HTTP/3. + mCaps |= NS_HTTP_DISALLOW_HTTP3; + } + + if (LoadIsTRRServiceChannel()) { + mCaps |= NS_HTTP_LARGE_KEEPALIVE; + } + + mCaps |= NS_HTTP_TRR_FLAGS_FROM_MODE(nsIRequest::GetTRRMode()); + + // Finalize ConnectionInfo flags before SpeculativeConnect + mConnectionInfo->SetAnonymous((mLoadFlags & LOAD_ANONYMOUS) != 0); + mConnectionInfo->SetPrivate(mPrivateBrowsing); + mConnectionInfo->SetIsolated(IsIsolated()); + mConnectionInfo->SetNoSpdy(mCaps & NS_HTTP_DISALLOW_SPDY); + mConnectionInfo->SetBeConservative((mCaps & NS_HTTP_BE_CONSERVATIVE) || + LoadBeConservative()); + mConnectionInfo->SetTlsFlags(mTlsFlags); + mConnectionInfo->SetIsTrrServiceChannel(LoadIsTRRServiceChannel()); + mConnectionInfo->SetTRRMode(nsIRequest::GetTRRMode()); + mConnectionInfo->SetIPv4Disabled(mCaps & NS_HTTP_DISABLE_IPV4); + mConnectionInfo->SetIPv6Disabled(mCaps & NS_HTTP_DISABLE_IPV6); + + // notify "http-on-before-connect" observers + gHttpHandler->OnBeforeConnect(this); + + // Check if request was cancelled during http-on-before-connect. + if (mCanceled) { + return mStatus; + } + + if (mSuspendCount) { + // We abandon the connection here if there was one. + LOG(("Waiting until resume OnBeforeConnect [this=%p]\n", this)); + MOZ_ASSERT(!mCallOnResume); + mCallOnResume = [](nsHttpChannel* self) { + self->OnBeforeConnectContinue(); + return NS_OK; + }; + return NS_OK; + } + + return Connect(); +} + +void nsHttpChannel::OnBeforeConnectContinue() { + MOZ_ASSERT(!mCallOnResume, "How did that happen?"); + nsresult rv; + + if (mSuspendCount) { + LOG(("Waiting until resume OnBeforeConnect [this=%p]\n", this)); + mCallOnResume = [](nsHttpChannel* self) { + self->OnBeforeConnectContinue(); + return NS_OK; + }; + return; + } + + LOG(("nsHttpChannel::OnBeforeConnectContinue [this=%p]\n", this)); + rv = Connect(); + if (NS_FAILED(rv)) { + CloseCacheEntry(false); + Unused << AsyncAbort(rv); + } +} + +nsresult nsHttpChannel::Connect() { + LOG(("nsHttpChannel::Connect [this=%p]\n", this)); + + // Don't allow resuming when cache must be used + if (LoadResuming() && (mLoadFlags & LOAD_ONLY_FROM_CACHE)) { + LOG(("Resuming from cache is not supported yet")); + return NS_ERROR_DOCUMENT_NOT_CACHED; + } + + if (ShouldIntercept()) { + return RedirectToInterceptedChannel(); + } + + bool isTrackingResource = IsThirdPartyTrackingResource(); + LOG(("nsHttpChannel %p tracking resource=%d, cos=%u", this, + isTrackingResource, mClassOfService)); + + if (isTrackingResource) { + AddClassFlags(nsIClassOfService::Tail); + } + + if (WaitingForTailUnblock()) { + MOZ_DIAGNOSTIC_ASSERT(!mOnTailUnblock); + mOnTailUnblock = &nsHttpChannel::ConnectOnTailUnblock; + return NS_OK; + } + + return ConnectOnTailUnblock(); +} + +nsresult nsHttpChannel::ConnectOnTailUnblock() { + nsresult rv; + + LOG(("nsHttpChannel::ConnectOnTailUnblock [this=%p]\n", this)); + + // Consider opening a TCP connection right away. + SpeculativeConnect(); + + // open a cache entry for this channel... + rv = OpenCacheEntry(mURI->SchemeIs("https")); + + // do not continue if asyncOpenCacheEntry is in progress + if (AwaitingCacheCallbacks()) { + LOG(("nsHttpChannel::Connect %p AwaitingCacheCallbacks forces async\n", + this)); + MOZ_ASSERT(NS_SUCCEEDED(rv), "Unexpected state"); + + if (mNetworkTriggered && mWaitingForProxy) { + // Someone has called TriggerNetwork(), meaning we are racing the + // network with the cache. + mWaitingForProxy = false; + return ContinueConnect(); + } + + return NS_OK; + } + + if (NS_FAILED(rv)) { + LOG(("OpenCacheEntry failed [rv=%" PRIx32 "]\n", + static_cast(rv))); + // if this channel is only allowed to pull from the cache, then + // we must fail if we were unable to open a cache entry. + if (mLoadFlags & LOAD_ONLY_FROM_CACHE) { + // If we have a fallback URI (and we're not already + // falling back), process the fallback asynchronously. + if (!LoadFallbackChannel() && !mFallbackKey.IsEmpty()) { + return AsyncCall(&nsHttpChannel::HandleAsyncFallback); + } + return NS_ERROR_DOCUMENT_NOT_CACHED; + } + // otherwise, let's just proceed without using the cache. + } + + if (mRaceCacheWithNetwork && ((mCacheEntry && !mCachedContentIsValid && + (mDidReval || LoadCachedContentIsPartial())) || + mIgnoreCacheEntry)) { + // We won't send the conditional request because the unconditional + // request was already sent (see bug 1377223). + AccumulateCategorical( + Telemetry::LABELS_NETWORK_RACE_CACHE_VALIDATION::NotSent); + } + + // When racing, if OnCacheEntryAvailable is called before AsyncOpenURI + // returns, then we may not have started reading from the cache. + // If the content is valid, we should attempt to do so, as technically the + // cache has won the race. + if (mRaceCacheWithNetwork && mCachedContentIsValid) { + Unused << ReadFromCache(true); + } + + return TriggerNetwork(); +} + +nsresult nsHttpChannel::ContinueConnect() { + // If we need to start a CORS preflight, do it now! + // Note that it is important to do this before the early returns below. + if (!LoadIsCorsPreflightDone() && LoadRequireCORSPreflight()) { + MOZ_ASSERT(!mPreflightChannel); + nsresult rv = nsCORSListenerProxy::StartCORSPreflight( + this, this, mUnsafeHeaders, getter_AddRefs(mPreflightChannel)); + return rv; + } + + MOZ_RELEASE_ASSERT(!LoadRequireCORSPreflight() || LoadIsCorsPreflightDone(), + "CORS preflight must have been finished by the time we " + "do the rest of ContinueConnect"); + + // we may or may not have a cache entry at this point + if (mCacheEntry) { + // read straight from the cache if possible... + if (mCachedContentIsValid) { + nsRunnableMethod* event = nullptr; + nsresult rv; + if (!LoadCachedContentIsPartial()) { + rv = AsyncCall(&nsHttpChannel::AsyncOnExamineCachedResponse, &event); + if (NS_FAILED(rv)) { + LOG((" AsyncCall failed (%08x)", static_cast(rv))); + } + } + rv = ReadFromCache(true); + if (NS_FAILED(rv) && event) { + event->Revoke(); + } + + AccumulateCacheHitTelemetry(kCacheHit, this); + mCacheDisposition = kCacheHit; + + return rv; + } + if (mLoadFlags & LOAD_ONLY_FROM_CACHE) { + // the cache contains the requested resource, but it must be + // validated before we can reuse it. since we are not allowed + // to hit the net, there's nothing more to do. the document + // is effectively not in the cache. + LOG((" !mCachedContentIsValid && mLoadFlags & LOAD_ONLY_FROM_CACHE")); + return NS_ERROR_DOCUMENT_NOT_CACHED; + } + } else if (mLoadFlags & LOAD_ONLY_FROM_CACHE) { + // If we have a fallback URI (and we're not already + // falling back), process the fallback asynchronously. + if (!LoadFallbackChannel() && !mFallbackKey.IsEmpty()) { + return AsyncCall(&nsHttpChannel::HandleAsyncFallback); + } + LOG((" !mCacheEntry && mLoadFlags & LOAD_ONLY_FROM_CACHE")); + return NS_ERROR_DOCUMENT_NOT_CACHED; + } + + if (mLoadFlags & LOAD_NO_NETWORK_IO) { + LOG((" mLoadFlags & LOAD_NO_NETWORK_IO")); + return NS_ERROR_DOCUMENT_NOT_CACHED; + } + + // hit the net... + return DoConnect(); +} + +nsresult nsHttpChannel::DoConnect(HttpTransactionShell* aTransWithStickyConn) { + LOG(("nsHttpChannel::DoConnect [this=%p]\n", this)); + + if (!mDNSBlockingPromise.IsEmpty()) { + LOG((" waiting for DNS prefetch")); + + // Transaction is passed only from auth retry for which we will definitely + // not block on DNS to alter the origin server name for IP; it has already + // been done. + MOZ_ASSERT(!aTransWithStickyConn); + MOZ_ASSERT(mDNSBlockingThenable); + + nsCOMPtr target(do_GetMainThread()); + RefPtr self(this); + mDNSBlockingThenable->Then( + target, __func__, + [self](const nsCOMPtr& aRec) { + nsresult rv = self->DoConnectActual(nullptr); + if (NS_FAILED(rv)) { + self->CloseCacheEntry(false); + Unused << self->AsyncAbort(rv); + } + }, + [self](nsresult err) { + self->CloseCacheEntry(false); + Unused << self->AsyncAbort(err); + }); + + // The connection will continue when the promise is resolved in + // OnLookupComplete. + return NS_OK; + } + + return DoConnectActual(aTransWithStickyConn); +} + +nsresult nsHttpChannel::DoConnectActual( + HttpTransactionShell* aTransWithStickyConn) { + LOG(("nsHttpChannel::DoConnectActual [this=%p, aTransWithStickyConn=%p]\n", + this, aTransWithStickyConn)); + + nsresult rv = SetupTransaction(); + if (NS_FAILED(rv)) { + return rv; + } + + if (aTransWithStickyConn) { + rv = gHttpHandler->InitiateTransactionWithStickyConn( + mTransaction, mPriority, aTransWithStickyConn); + } else { + rv = gHttpHandler->InitiateTransaction(mTransaction, mPriority); + } + + if (NS_FAILED(rv)) { + return rv; + } + + rv = mTransaction->AsyncRead(this, getter_AddRefs(mTransactionPump)); + if (NS_FAILED(rv)) { + return rv; + } + + uint32_t suspendCount = mSuspendCount; + if (LoadAsyncResumePending()) { + LOG( + (" Suspend()'ing transaction pump once because of async resume pending" + ", sc=%u, pump=%p, this=%p", + suspendCount, mTransactionPump.get(), this)); + ++suspendCount; + } + while (suspendCount--) { + mTransactionPump->Suspend(); + } + + return NS_OK; +} + +void nsHttpChannel::SpeculativeConnect() { + // Before we take the latency hit of dealing with the cache, try and + // get the TCP (and SSL) handshakes going so they can overlap. + + // don't speculate if we are on uses of the offline application cache, + // if we are offline, when doing http upgrade (i.e. + // websockets bootstrap), or if we can't do keep-alive (because then we + // couldn't reuse the speculative connection anyhow). + if (mApplicationCache || gIOService->IsOffline() || + mUpgradeProtocolCallback || !(mCaps & NS_HTTP_ALLOW_KEEPALIVE)) + return; + + // LOAD_ONLY_FROM_CACHE and LOAD_NO_NETWORK_IO must not hit network. + // LOAD_FROM_CACHE and LOAD_CHECK_OFFLINE_CACHE are unlikely to hit network, + // so skip preconnects for them. + if (mLoadFlags & (LOAD_ONLY_FROM_CACHE | LOAD_FROM_CACHE | + LOAD_NO_NETWORK_IO | LOAD_CHECK_OFFLINE_CACHE)) + return; + + if (LoadAllowStaleCacheContent()) { + return; + } + + nsCOMPtr callbacks; + NS_NewNotificationCallbacksAggregation(mCallbacks, mLoadGroup, + getter_AddRefs(callbacks)); + if (!callbacks) return; + + Unused << gHttpHandler->SpeculativeConnect( + mConnectionInfo, callbacks, + mCaps & (NS_HTTP_DISALLOW_SPDY | NS_HTTP_TRR_MODE_MASK | + NS_HTTP_DISABLE_IPV4 | NS_HTTP_DISABLE_IPV6 | + NS_HTTP_DISALLOW_HTTP3), + gHttpHandler->UseHTTPSRRForSpeculativeConnection()); +} + +void nsHttpChannel::DoNotifyListenerCleanup() { + // We don't need this info anymore + CleanRedirectCacheChainIfNecessary(); +} + +void nsHttpChannel::ReleaseListeners() { + HttpBaseChannel::ReleaseListeners(); + mChannelClassifier = nullptr; + mWarningReporter = nullptr; + + for (StreamFilterRequest& request : mStreamFilterRequests) { + request.mPromise->Reject(false, __func__); + } + mStreamFilterRequests.Clear(); +} + +void nsHttpChannel::DoAsyncAbort(nsresult aStatus) { + Unused << AsyncAbort(aStatus); +} + +void nsHttpChannel::HandleAsyncRedirect() { + MOZ_ASSERT(!mCallOnResume, "How did that happen?"); + + if (mSuspendCount) { + LOG(("Waiting until resume to do async redirect [this=%p]\n", this)); + mCallOnResume = [](nsHttpChannel* self) { + self->HandleAsyncRedirect(); + return NS_OK; + }; + return; + } + + nsresult rv = NS_OK; + + LOG(("nsHttpChannel::HandleAsyncRedirect [this=%p]\n", this)); + + // since this event is handled asynchronously, it is possible that this + // channel could have been canceled, in which case there would be no point + // in processing the redirect. + if (NS_SUCCEEDED(mStatus)) { + PushRedirectAsyncFunc(&nsHttpChannel::ContinueHandleAsyncRedirect); + rv = AsyncProcessRedirection(mResponseHead->Status()); + if (NS_FAILED(rv)) { + PopRedirectAsyncFunc(&nsHttpChannel::ContinueHandleAsyncRedirect); + // TODO: if !DoNotRender3xxBody(), render redirect body instead. + // But first we need to cache 3xx bodies (bug 748510) + rv = ContinueHandleAsyncRedirect(rv); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + } else { + rv = ContinueHandleAsyncRedirect(mStatus); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } +} + +nsresult nsHttpChannel::ContinueHandleAsyncRedirect(nsresult rv) { + if (NS_FAILED(rv)) { + // If AsyncProcessRedirection fails, then we have to send out the + // OnStart/OnStop notifications. + LOG(("ContinueHandleAsyncRedirect got failure result [rv=%" PRIx32 "]\n", + static_cast(rv))); + + bool redirectsEnabled = !mLoadInfo->GetDontFollowRedirects(); + + if (redirectsEnabled) { + // TODO: stop failing original channel if redirect vetoed? + mStatus = rv; + + DoNotifyListener(); + + // Blow away cache entry if we couldn't process the redirect + // for some reason (the cache entry might be corrupt). + if (mCacheEntry) { + mCacheEntry->AsyncDoom(nullptr); + } + } else { + DoNotifyListener(); + } + } + + CloseCacheEntry(true); + + StoreIsPending(false); + + if (mLoadGroup) mLoadGroup->RemoveRequest(this, nullptr, mStatus); + + return NS_OK; +} + +void nsHttpChannel::HandleAsyncNotModified() { + MOZ_ASSERT(!mCallOnResume, "How did that happen?"); + + if (mSuspendCount) { + LOG(("Waiting until resume to do async not-modified [this=%p]\n", this)); + mCallOnResume = [](nsHttpChannel* self) { + self->HandleAsyncNotModified(); + return NS_OK; + }; + return; + } + + LOG(("nsHttpChannel::HandleAsyncNotModified [this=%p]\n", this)); + + DoNotifyListener(); + + CloseCacheEntry(false); + + StoreIsPending(false); + + if (mLoadGroup) mLoadGroup->RemoveRequest(this, nullptr, mStatus); +} + +void nsHttpChannel::HandleAsyncFallback() { + MOZ_ASSERT(!mCallOnResume, "How did that happen?"); + + if (mSuspendCount) { + LOG(("Waiting until resume to do async fallback [this=%p]\n", this)); + mCallOnResume = [](nsHttpChannel* self) { + self->HandleAsyncFallback(); + return NS_OK; + }; + return; + } + + nsresult rv = NS_OK; + + LOG(("nsHttpChannel::HandleAsyncFallback [this=%p]\n", this)); + + // since this event is handled asynchronously, it is possible that this + // channel could have been canceled, in which case there would be no point + // in processing the fallback. + if (!mCanceled) { + PushRedirectAsyncFunc(&nsHttpChannel::ContinueHandleAsyncFallback); + bool waitingForRedirectCallback; + rv = ProcessFallback(&waitingForRedirectCallback); + if (waitingForRedirectCallback) return; + PopRedirectAsyncFunc(&nsHttpChannel::ContinueHandleAsyncFallback); + } + + rv = ContinueHandleAsyncFallback(rv); + MOZ_ASSERT(NS_SUCCEEDED(rv)); +} + +nsresult nsHttpChannel::ContinueHandleAsyncFallback(nsresult rv) { + if (!mCanceled && (NS_FAILED(rv) || !LoadFallingBack())) { + // If ProcessFallback fails, then we have to send out the + // OnStart/OnStop notifications. + LOG(("ProcessFallback failed [rv=%" PRIx32 ", %d]\n", + static_cast(rv), LoadFallingBack())); + mStatus = NS_FAILED(rv) ? rv : NS_ERROR_DOCUMENT_NOT_CACHED; + DoNotifyListener(); + } + + StoreIsPending(false); + + if (mLoadGroup) mLoadGroup->RemoveRequest(this, nullptr, mStatus); + + return rv; +} + +nsresult nsHttpChannel::SetupTransaction() { + LOG(("nsHttpChannel::SetupTransaction [this=%p, cos=%u, prio=%d]\n", this, + mClassOfService, mPriority)); + + NS_ENSURE_TRUE(!mTransaction, NS_ERROR_ALREADY_INITIALIZED); + + nsresult rv; + + mozilla::MutexAutoLock lock(mRCWNLock); + + // If we're racing cache with network, conditional or byte range header + // could be added in OnCacheEntryCheck. We cannot send conditional request + // without having the entry, so we need to remove the headers here and + // ignore the cache entry in OnCacheEntryAvailable. + if (mRaceCacheWithNetwork && AwaitingCacheCallbacks()) { + if (mDidReval) { + LOG((" Removing conditional request headers")); + UntieValidationRequest(); + mDidReval = false; + mIgnoreCacheEntry = true; + } + + if (LoadCachedContentIsPartial()) { + LOG((" Removing byte range request headers")); + UntieByteRangeRequest(); + StoreCachedContentIsPartial(false); + mIgnoreCacheEntry = true; + } + + if (mIgnoreCacheEntry) { + mAvailableCachedAltDataType.Truncate(); + StoreDeliveringAltData(false); + mAltDataLength = -1; + mCacheInputStream.CloseAndRelease(); + } + } + + StoreUsedNetwork(1); + + if (!LoadAllowSpdy()) { + mCaps |= NS_HTTP_DISALLOW_SPDY; + } + if (!LoadAllowHttp3()) { + mCaps |= NS_HTTP_DISALLOW_HTTP3; + } + if (LoadBeConservative()) { + mCaps |= NS_HTTP_BE_CONSERVATIVE; + } + + // Use the URI path if not proxying (transparent proxying such as proxy + // CONNECT does not count here). Also figure out what HTTP version to use. + nsAutoCString buf, path; + nsCString* requestURI; + + // This is the normal e2e H1 path syntax "/index.html" + rv = mURI->GetPathQueryRef(path); + if (NS_FAILED(rv)) { + return rv; + } + + // path may contain UTF-8 characters, so ensure that they're escaped. + if (NS_EscapeURL(path.get(), path.Length(), esc_OnlyNonASCII | esc_Spaces, + buf)) { + requestURI = &buf; + } else { + requestURI = &path; + } + + // trim off the #ref portion if any... + int32_t ref1 = requestURI->FindChar('#'); + if (ref1 != kNotFound) { + requestURI->SetLength(ref1); + } + + if (mConnectionInfo->UsingConnect() || !mConnectionInfo->UsingHttpProxy()) { + mRequestHead.SetVersion(gHttpHandler->HttpVersion()); + } else { + mRequestHead.SetPath(*requestURI); + + // RequestURI should be the absolute uri H1 proxy syntax + // "http://foo/index.html" so we will overwrite the relative version in + // requestURI + rv = mURI->GetUserPass(buf); + if (NS_FAILED(rv)) return rv; + if (!buf.IsEmpty() && ((strncmp(mSpec.get(), "http:", 5) == 0) || + strncmp(mSpec.get(), "https:", 6) == 0)) { + nsCOMPtr tempURI = nsIOService::CreateExposableURI(mURI); + rv = tempURI->GetAsciiSpec(path); + if (NS_FAILED(rv)) return rv; + requestURI = &path; + } else { + requestURI = &mSpec; + } + + // trim off the #ref portion if any... + int32_t ref2 = requestURI->FindChar('#'); + if (ref2 != kNotFound) { + requestURI->SetLength(ref2); + } + + mRequestHead.SetVersion(gHttpHandler->ProxyHttpVersion()); + } + + mRequestHead.SetRequestURI(*requestURI); + + // set the request time for cache expiration calculations + mRequestTime = NowInSeconds(); + StoreRequestTimeInitialized(true); + + // if doing a reload, force end-to-end + if (mLoadFlags & LOAD_BYPASS_CACHE) { + // We need to send 'Pragma:no-cache' to inhibit proxy caching even if + // no proxy is configured since we might be talking with a transparent + // proxy, i.e. one that operates at the network level. See bug #14772. + rv = mRequestHead.SetHeaderOnce(nsHttp::Pragma, "no-cache", true); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + // If we're configured to speak HTTP/1.1 then also send 'Cache-control: + // no-cache' + if (mRequestHead.Version() >= HttpVersion::v1_1) { + rv = mRequestHead.SetHeaderOnce(nsHttp::Cache_Control, "no-cache", true); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + } else if ((mLoadFlags & VALIDATE_ALWAYS) && !LoadCacheEntryIsWriteOnly()) { + // We need to send 'Cache-Control: max-age=0' to force each cache along + // the path to the origin server to revalidate its own entry, if any, + // with the next cache or server. See bug #84847. + // + // If we're configured to speak HTTP/1.0 then just send 'Pragma: no-cache' + if (mRequestHead.Version() >= HttpVersion::v1_1) + rv = mRequestHead.SetHeaderOnce(nsHttp::Cache_Control, "max-age=0", true); + else + rv = mRequestHead.SetHeaderOnce(nsHttp::Pragma, "no-cache", true); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + + if (LoadResuming()) { + char byteRange[32]; + SprintfLiteral(byteRange, "bytes=%" PRIu64 "-", mStartPos); + rv = mRequestHead.SetHeader(nsHttp::Range, nsDependentCString(byteRange)); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + if (!mEntityID.IsEmpty()) { + // Also, we want an error if this resource changed in the meantime + // Format of the entity id is: escaped_etag/size/lastmod + nsCString::const_iterator start, end, slash; + mEntityID.BeginReading(start); + mEntityID.EndReading(end); + mEntityID.BeginReading(slash); + + if (FindCharInReadable('/', slash, end)) { + nsAutoCString ifMatch; + rv = mRequestHead.SetHeader( + nsHttp::If_Match, + NS_UnescapeURL(Substring(start, slash), 0, ifMatch)); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + ++slash; // Incrementing, so that searching for '/' won't find + // the same slash again + } + + if (FindCharInReadable('/', slash, end)) { + rv = mRequestHead.SetHeader(nsHttp::If_Unmodified_Since, + Substring(++slash, end)); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + } + } + + // create wrapper for this channel's notification callbacks + nsCOMPtr callbacks; + NS_NewNotificationCallbacksAggregation(mCallbacks, mLoadGroup, + getter_AddRefs(callbacks)); + + // create the transaction object + if (nsIOService::UseSocketProcess()) { + MOZ_ASSERT(gIOService->SocketProcessReady(), + "Socket process should be ready."); + + nsCOMPtr parentChannel; + NS_QueryNotificationCallbacks(this, parentChannel); + RefPtr documentChannelParent = + do_QueryObject(parentChannel); + // See HttpTransactionChild::CanSendODAToContentProcessDirectly() and + // nsHttpChannel::CallOnStartRequest() for the reason why we need to know if + // this is a document load. We only send ODA directly to child process for + // non document loads. + RefPtr transParent = + new HttpTransactionParent(!!documentChannelParent); + LOG1(("nsHttpChannel %p created HttpTransactionParent %p\n", this, + transParent.get())); + + // Since OnStopRequest could be sent to child process from socket process + // directly, we need to store these two values in HttpTransactionChild and + // forward to child process until HttpTransactionChild::OnStopRequest is + // called. + transParent->SetRedirectTimestamp(mRedirectStartTimeStamp, + mRedirectEndTimeStamp); + + SocketProcessParent* socketProcess = SocketProcessParent::GetSingleton(); + if (socketProcess) { + Unused << socketProcess->SendPHttpTransactionConstructor(transParent); + } + + mTransaction = transParent; + } else { + mTransaction = new nsHttpTransaction(); + LOG1(("nsHttpChannel %p created nsHttpTransaction %p\n", this, + mTransaction.get())); + } + + // Save the mapping of channel id and the channel. We need this mapping for + // nsIHttpActivityObserver. + gHttpHandler->AddHttpChannel(mChannelId, ToSupports(this)); + + // See bug #466080. Transfer LOAD_ANONYMOUS flag to socket-layer. + if (mLoadFlags & LOAD_ANONYMOUS) mCaps |= NS_HTTP_LOAD_ANONYMOUS; + + if (mLoadFlags & LOAD_CALL_CONTENT_SNIFFERS) { + mCaps |= NS_HTTP_CALL_CONTENT_SNIFFER; + } + + if (LoadTimingEnabled()) mCaps |= NS_HTTP_TIMING_ENABLED; + + if (mUpgradeProtocolCallback) { + rv = mRequestHead.SetHeader(nsHttp::Upgrade, mUpgradeProtocol, false); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + rv = mRequestHead.SetHeaderOnce(nsHttp::Connection, nsHttp::Upgrade.get(), + true); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + mCaps |= NS_HTTP_STICKY_CONNECTION; + mCaps &= ~NS_HTTP_ALLOW_KEEPALIVE; + } + + nsCOMPtr pushListener; + NS_QueryNotificationCallbacks(mCallbacks, mLoadGroup, + NS_GET_IID(nsIHttpPushListener), + getter_AddRefs(pushListener)); + HttpTransactionShell::OnPushCallback pushCallback = nullptr; + if (pushListener) { + mCaps |= NS_HTTP_ONPUSH_LISTENER; + nsWeakPtr weakPtrThis( + do_GetWeakReference(static_cast(this))); + pushCallback = [weakPtrThis](uint32_t aPushedStreamId, + const nsACString& aUrl, + const nsACString& aRequestString, + HttpTransactionShell* aTransaction) { + if (nsCOMPtr channel = do_QueryReferent(weakPtrThis)) { + return static_cast(channel.get()) + ->OnPush(aPushedStreamId, aUrl, aRequestString, aTransaction); + } + return NS_ERROR_NOT_AVAILABLE; + }; + } + + EnsureTopLevelOuterContentWindowId(); + EnsureRequestContext(); + + HttpTrafficCategory category = CreateTrafficCategory(); + std::function observer; + if (mTransactionObserver) { + observer = [transactionObserver{std::move(mTransactionObserver)}]( + TransactionObserverResult&& aResult) { + transactionObserver->Complete(aResult.versionOk(), aResult.authOk(), + aResult.closeReason()); + }; + } + rv = mTransaction->Init( + mCaps, mConnectionInfo, &mRequestHead, mUploadStream, mReqContentLength, + LoadUploadStreamHasHeaders(), GetCurrentEventTarget(), callbacks, this, + mTopLevelOuterContentWindowId, category, mRequestContext, mClassOfService, + mInitialRwin, LoadResponseTimeoutEnabled(), mChannelId, + std::move(observer), std::move(pushCallback), mTransWithPushedStream, + mPushedStreamId); + if (NS_FAILED(rv)) { + mTransaction = nullptr; + return rv; + } + + return rv; +} + +HttpTrafficCategory nsHttpChannel::CreateTrafficCategory() { + MOZ_ASSERT(!mFirstPartyClassificationFlags || + !mThirdPartyClassificationFlags); + + if (!StaticPrefs::network_traffic_analyzer_enabled()) { + return HttpTrafficCategory::eInvalid; + } + + HttpTrafficAnalyzer::ClassOfService cos; + { + if ((mClassOfService & nsIClassOfService::Leader) && + mLoadInfo->GetExternalContentPolicyType() == + ExtContentPolicy::TYPE_SCRIPT) { + cos = HttpTrafficAnalyzer::ClassOfService::eLeader; + } else if (mLoadFlags & nsIRequest::LOAD_BACKGROUND) { + cos = HttpTrafficAnalyzer::ClassOfService::eBackground; + } else { + cos = HttpTrafficAnalyzer::ClassOfService::eOther; + } + } + + bool isThirdParty = + nsContentUtils::IsThirdPartyWindowOrChannel(nullptr, this, mURI); + HttpTrafficAnalyzer::TrackingClassification tc; + { + uint32_t flags = isThirdParty ? mThirdPartyClassificationFlags + : mFirstPartyClassificationFlags; + + using CF = nsIClassifiedChannel::ClassificationFlags; + using TC = HttpTrafficAnalyzer::TrackingClassification; + + if (flags & CF::CLASSIFIED_TRACKING_CONTENT) { + tc = TC::eContent; + } else if (flags & CF::CLASSIFIED_FINGERPRINTING_CONTENT) { + tc = TC::eFingerprinting; + } else if (flags & CF::CLASSIFIED_ANY_BASIC_TRACKING) { + tc = TC::eBasic; + } else { + tc = TC::eNone; + } + } + + bool isSystemPrincipal = + mLoadInfo->GetLoadingPrincipal() && + mLoadInfo->GetLoadingPrincipal()->IsSystemPrincipal(); + return HttpTrafficAnalyzer::CreateTrafficCategory( + NS_UsePrivateBrowsing(this), isSystemPrincipal, isThirdParty, cos, tc); +} + +void nsHttpChannel::SetCachedContentType() { + if (!mResponseHead) { + return; + } + + nsAutoCString contentTypeStr; + mResponseHead->ContentType(contentTypeStr); + + uint8_t contentType = nsICacheEntry::CONTENT_TYPE_OTHER; + if (nsContentUtils::IsJavascriptMIMEType( + NS_ConvertUTF8toUTF16(contentTypeStr))) { + contentType = nsICacheEntry::CONTENT_TYPE_JAVASCRIPT; + } else if (StringBeginsWith(contentTypeStr, "text/css"_ns) || + (mLoadInfo->GetExternalContentPolicyType() == + ExtContentPolicy::TYPE_STYLESHEET)) { + contentType = nsICacheEntry::CONTENT_TYPE_STYLESHEET; + } else if (StringBeginsWith(contentTypeStr, "application/wasm"_ns)) { + contentType = nsICacheEntry::CONTENT_TYPE_WASM; + } else if (StringBeginsWith(contentTypeStr, "image/"_ns)) { + contentType = nsICacheEntry::CONTENT_TYPE_IMAGE; + } else if (StringBeginsWith(contentTypeStr, "video/"_ns)) { + contentType = nsICacheEntry::CONTENT_TYPE_MEDIA; + } else if (StringBeginsWith(contentTypeStr, "audio/"_ns)) { + contentType = nsICacheEntry::CONTENT_TYPE_MEDIA; + } + + mCacheEntry->SetContentType(contentType); +} + +nsresult nsHttpChannel::CallOnStartRequest() { + LOG(("nsHttpChannel::CallOnStartRequest [this=%p]", this)); + + MOZ_RELEASE_ASSERT(!LoadRequireCORSPreflight() || LoadIsCorsPreflightDone(), + "CORS preflight must have been finished by the time we " + "call OnStartRequest"); + + if (LoadOnStartRequestCalled()) { + // This can only happen when a range request loading rest of the data + // after interrupted concurrent cache read asynchronously failed, e.g. + // the response range bytes are not as expected or this channel has + // been externally canceled. + // + // It's legal to bypass CallOnStartRequest for that case since we've + // already called OnStartRequest on our listener and also added all + // content converters before. + MOZ_ASSERT(LoadConcurrentCacheAccess()); + LOG(("CallOnStartRequest already invoked before")); + return mStatus; + } + + // Ensure mListener->OnStartRequest will be invoked before exiting + // this function. + auto onStartGuard = MakeScopeExit([&] { + LOG( + (" calling mListener->OnStartRequest by ScopeExit [this=%p, " + "listener=%p]\n", + this, mListener.get())); + MOZ_ASSERT(!LoadOnStartRequestCalled()); + + if (mListener) { + nsCOMPtr deleteProtector(mListener); + StoreOnStartRequestCalled(true); + deleteProtector->OnStartRequest(this); + } + StoreOnStartRequestCalled(true); + }); + + nsresult rv = ValidateMIMEType(); + // Since ODA and OnStopRequest could be sent from socket process directly, we + // need to update the channel status before calling mListener->OnStartRequest. + // This is the only way to let child process discard the already received ODA + // messages. + if (NS_FAILED(rv)) { + mStatus = rv; + return mStatus; + } + + // Allow consumers to override our content type + if (mLoadFlags & LOAD_CALL_CONTENT_SNIFFERS) { + // NOTE: We can have both a txn pump and a cache pump when the cache + // content is partial. In that case, we need to read from the cache, + // because that's the one that has the initial contents. If that fails + // then give the transaction pump a shot. + + nsIChannel* thisChannel = static_cast(this); + + bool typeSniffersCalled = false; + if (mCachePump) { + typeSniffersCalled = + NS_SUCCEEDED(mCachePump->PeekStream(CallTypeSniffers, thisChannel)); + } + + if (!typeSniffersCalled && mTransactionPump) { + RefPtr pump = do_QueryObject(mTransactionPump); + if (pump) { + pump->PeekStream(CallTypeSniffers, thisChannel); + } else { + MOZ_ASSERT(nsIOService::UseSocketProcess()); + RefPtr trans = do_QueryObject(mTransactionPump); + MOZ_ASSERT(trans); + trans->SetSniffedTypeToChannel(CallTypeSniffers, thisChannel); + } + } + } + + // Note that the code below should be synced with the code in + // HttpTransactionChild::CanSendODAToContentProcessDirectly(). We MUST make + // sure HttpTransactionChild::CanSendODAToContentProcessDirectly() returns + // false when a stream converter is applied. + bool unknownDecoderStarted = false; + if (mResponseHead && !mResponseHead->HasContentType()) { + MOZ_ASSERT(mConnectionInfo, "Should have connection info here"); + if (!mContentTypeHint.IsEmpty()) + mResponseHead->SetContentType(mContentTypeHint); + else if (mResponseHead->Version() == HttpVersion::v0_9 && + mConnectionInfo->OriginPort() != mConnectionInfo->DefaultPort()) + mResponseHead->SetContentType(nsLiteralCString(TEXT_PLAIN)); + else { + // Uh-oh. We had better find out what type we are! + nsCOMPtr serv; + rv = gHttpHandler->GetStreamConverterService(getter_AddRefs(serv)); + // If we failed, we just fall through to the "normal" case + if (NS_SUCCEEDED(rv)) { + nsCOMPtr converter; + rv = serv->AsyncConvertData(UNKNOWN_CONTENT_TYPE, "*/*", mListener, + nullptr, getter_AddRefs(converter)); + if (NS_SUCCEEDED(rv)) { + mListener = converter; + unknownDecoderStarted = true; + } + } + } + } + + // If the content is multipart/x-mixed-replace, we'll insert a MIME decoder + // in the pipeline to handle the content and pass it along to our + // original listener. nsUnknownDecoder doesn't support detecting this type, + // so we only need to insert this using the response header's mime type. + // + // We only do this for unwrapped document loads, since we might want to send + // parts to the external protocol handler without leaving the parent process. + bool mustRunStreamFilterInParent = false; + nsCOMPtr parentChannel; + NS_QueryNotificationCallbacks(this, parentChannel); + RefPtr docListener = do_QueryObject(parentChannel); + if (mResponseHead && docListener && docListener->GetChannel() == this) { + nsAutoCString contentType; + mResponseHead->ContentType(contentType); + + if (contentType.Equals("multipart/x-mixed-replace"_ns)) { + nsCOMPtr convServ( + do_GetService("@mozilla.org/streamConverters;1", &rv)); + if (NS_SUCCEEDED(rv)) { + nsCOMPtr toListener(mListener); + nsCOMPtr fromListener; + + rv = convServ->AsyncConvertData("multipart/x-mixed-replace", "*/*", + toListener, nullptr, + getter_AddRefs(fromListener)); + if (NS_SUCCEEDED(rv)) { + mListener = fromListener; + mustRunStreamFilterInParent = true; + } + } + } + } + + // If we installed a multipart converter, then we need to add StreamFilter + // object before it, so that extensions see the un-parsed original stream. + // We may want to add an option for extensions to opt-in to proper multipart + // handling. + // If not, then pass the StreamFilter promise on to DocumentLoadListener, + // where it'll be added in the content process. + for (StreamFilterRequest& request : mStreamFilterRequests) { + if (mustRunStreamFilterInParent) { + mozilla::ipc::Endpoint parent; + mozilla::ipc::Endpoint child; + nsresult rv = extensions::PStreamFilter::CreateEndpoints( + base::GetCurrentProcId(), request.mChildProcessId, &parent, &child); + if (NS_FAILED(rv)) { + request.mPromise->Reject(false, __func__); + } else { + extensions::StreamFilterParent::Attach(this, std::move(parent)); + request.mPromise->Resolve(std::move(child), __func__); + } + } else { + if (docListener) { + docListener->AttachStreamFilter(request.mChildProcessId) + ->ChainTo(request.mPromise.forget(), __func__); + } else { + request.mPromise->Reject(false, __func__); + } + } + request.mPromise = nullptr; + } + mStreamFilterRequests.Clear(); + StoreTracingEnabled(false); + + if (mResponseHead && !mResponseHead->HasContentCharset()) + mResponseHead->SetContentCharset(mContentCharsetHint); + + if (mCacheEntry && LoadCacheEntryIsWriteOnly()) { + SetCachedContentType(); + } + + LOG((" calling mListener->OnStartRequest [this=%p, listener=%p]\n", this, + mListener.get())); + + // About to call OnStartRequest, dismiss the guard object. + onStartGuard.release(); + + if (mListener) { + MOZ_ASSERT(!LoadOnStartRequestCalled(), + "We should not call OsStartRequest twice"); + nsCOMPtr deleteProtector(mListener); + StoreOnStartRequestCalled(true); + rv = deleteProtector->OnStartRequest(this); + if (NS_FAILED(rv)) return rv; + } else { + NS_WARNING("OnStartRequest skipped because of null listener"); + StoreOnStartRequestCalled(true); + } + + // Install stream converter if required. + // Normally, we expect the listener to disable content conversion during + // OnStartRequest if it wants to handle it itself (which is common case with + // HttpChannelParent, disabling so that it can be done in the content + // process). If we've installed an nsUnknownDecoder, then we won't yet have + // called OnStartRequest on the final listener (that happens after we send + // OnDataAvailable to the nsUnknownDecoder), so it can't yet have disabled + // content conversion. + // In that case, assume that the listener will disable content conversion, + // unless it's specifically told us that it won't. + if (!unknownDecoderStarted || LoadListenerRequiresContentConversion()) { + nsCOMPtr listener; + rv = + DoApplyContentConversions(mListener, getter_AddRefs(listener), nullptr); + if (NS_FAILED(rv)) { + return rv; + } + if (listener) { + MOZ_ASSERT(!LoadDataSentToChildProcess(), + "DataSentToChildProcess being true means ODAs are sent to " + "the child process directly. We MUST NOT apply content " + "converter in this case."); + mListener = listener; + mCompressListener = listener; + StoreHasAppliedConversion(true); + } + } + + // if this channel is for a download, close off access to the cache. + if (mCacheEntry && LoadChannelIsForDownload()) { + mCacheEntry->AsyncDoom(nullptr); + + // We must keep the cache entry in case of partial request. + // Concurrent access is the same, we need the entry in + // OnStopRequest. + // We also need the cache entry when racing cache with network to find + // out what is the source of the data. + if (!LoadCachedContentIsPartial() && !LoadConcurrentCacheAccess() && + !(mRaceCacheWithNetwork && + mFirstResponseSource == RESPONSE_FROM_CACHE)) { + CloseCacheEntry(false); + } + } + + if (!mCanceled) { + // create offline cache entry if offline caching was requested + if (ShouldUpdateOfflineCacheEntry()) { + LOG(("writing to the offline cache")); + rv = InitOfflineCacheEntry(); + if (NS_FAILED(rv)) return rv; + + // InitOfflineCacheEntry may have closed mOfflineCacheEntry + if (mOfflineCacheEntry) { + rv = InstallOfflineCacheListener(); + if (NS_FAILED(rv)) return rv; + } + } else if (mApplicationCacheForWrite) { + LOG(("offline cache is up to date, not updating")); + CloseOfflineCacheEntry(); + } + } + + return NS_OK; +} + +NS_IMETHODIMP nsHttpChannel::GetHttpProxyConnectResponseCode( + int32_t* aResponseCode) { + NS_ENSURE_ARG_POINTER(aResponseCode); + + if (mConnectionInfo && mConnectionInfo->UsingConnect()) { + *aResponseCode = mProxyConnectResponseCode; + } else { + *aResponseCode = -1; + } + return NS_OK; +} + +nsresult nsHttpChannel::ProcessFailedProxyConnect(uint32_t httpStatus) { + // Failure to set up a proxy tunnel via CONNECT means one of the following: + // 1) Proxy wants authorization, or forbids. + // 2) DNS at proxy couldn't resolve target URL. + // 3) Proxy connection to target failed or timed out. + // 4) Eve intercepted our CONNECT, and is replying with malicious HTML. + // + // Our current architecture would parse the proxy's response content with + // the permission of the target URL. Given #4, we must avoid rendering the + // body of the reply, and instead give the user a (hopefully helpful) + // boilerplate error page, based on just the HTTP status of the reply. + + MOZ_ASSERT(mConnectionInfo->UsingConnect(), + "proxy connect failed but not using CONNECT?"); + nsresult rv = HttpProxyResponseToErrorCode(httpStatus); + LOG(("Cancelling failed proxy CONNECT [this=%p httpStatus=%u]\n", this, + httpStatus)); + + // Make sure the connection is thrown away as it can be in a bad state + // and the proxy may just hang on the next request. + MOZ_ASSERT(mTransaction); + mTransaction->DontReuseConnection(); + + Cancel(rv); + { + nsresult rv = CallOnStartRequest(); + if (NS_FAILED(rv)) { + LOG(("CallOnStartRequest failed [this=%p httpStatus=%u rv=%08x]\n", this, + httpStatus, static_cast(rv))); + } + } + return rv; +} + +static void GetSTSConsoleErrorTag(uint32_t failureResult, + nsAString& consoleErrorTag) { + switch (failureResult) { + case nsISiteSecurityService::ERROR_UNTRUSTWORTHY_CONNECTION: + consoleErrorTag = u"STSUntrustworthyConnection"_ns; + break; + case nsISiteSecurityService::ERROR_COULD_NOT_PARSE_HEADER: + consoleErrorTag = u"STSCouldNotParseHeader"_ns; + break; + case nsISiteSecurityService::ERROR_NO_MAX_AGE: + consoleErrorTag = u"STSNoMaxAge"_ns; + break; + case nsISiteSecurityService::ERROR_MULTIPLE_MAX_AGES: + consoleErrorTag = u"STSMultipleMaxAges"_ns; + break; + case nsISiteSecurityService::ERROR_INVALID_MAX_AGE: + consoleErrorTag = u"STSInvalidMaxAge"_ns; + break; + case nsISiteSecurityService::ERROR_MULTIPLE_INCLUDE_SUBDOMAINS: + consoleErrorTag = u"STSMultipleIncludeSubdomains"_ns; + break; + case nsISiteSecurityService::ERROR_INVALID_INCLUDE_SUBDOMAINS: + consoleErrorTag = u"STSInvalidIncludeSubdomains"_ns; + break; + case nsISiteSecurityService::ERROR_COULD_NOT_SAVE_STATE: + consoleErrorTag = u"STSCouldNotSaveState"_ns; + break; + default: + consoleErrorTag = u"STSUnknownError"_ns; + break; + } +} + +/** + * Process a single security header. Only one type is supported: HSTS + */ +nsresult nsHttpChannel::ProcessSingleSecurityHeader( + uint32_t aType, nsITransportSecurityInfo* aSecInfo, uint32_t aFlags) { + nsHttpAtom atom; + switch (aType) { + case nsISiteSecurityService::HEADER_HSTS: + atom = nsHttp::ResolveAtom("Strict-Transport-Security"); + break; + default: + MOZ_ASSERT_UNREACHABLE("Invalid security header type"); + return NS_ERROR_FAILURE; + } + + nsAutoCString securityHeader; + nsresult rv = mResponseHead->GetHeader(atom, securityHeader); + if (NS_SUCCEEDED(rv)) { + nsISiteSecurityService* sss = gHttpHandler->GetSSService(); + NS_ENSURE_TRUE(sss, NS_ERROR_OUT_OF_MEMORY); + // Process header will now discard the headers itself if the channel + // wasn't secure (whereas before it had to be checked manually) + OriginAttributes originAttributes; + if (NS_WARN_IF(!StoragePrincipalHelper::GetOriginAttributesForHSTS( + this, originAttributes))) { + return NS_ERROR_FAILURE; + } + + uint32_t failureResult; + uint32_t headerSource = nsISiteSecurityService::SOURCE_ORGANIC_REQUEST; + rv = sss->ProcessHeader(aType, mURI, securityHeader, aSecInfo, aFlags, + headerSource, originAttributes, nullptr, nullptr, + &failureResult); + if (NS_FAILED(rv)) { + nsAutoString consoleErrorCategory; + nsAutoString consoleErrorTag; + switch (aType) { + case nsISiteSecurityService::HEADER_HSTS: + GetSTSConsoleErrorTag(failureResult, consoleErrorTag); + consoleErrorCategory = u"Invalid HSTS Headers"_ns; + break; + default: + return NS_ERROR_FAILURE; + } + Unused << AddSecurityMessage(consoleErrorTag, consoleErrorCategory); + LOG(("nsHttpChannel: Failed to parse %s header, continuing load.\n", + atom.get())); + } + } else { + if (rv != NS_ERROR_NOT_AVAILABLE) { + // All other errors are fatal + NS_ENSURE_SUCCESS(rv, rv); + } + LOG(("nsHttpChannel: No %s header, continuing load.\n", atom.get())); + } + return NS_OK; +} + +/** + * Decide whether or not to remember Strict-Transport-Security, and whether + * or not to enforce channel integrity. + * + * @return NS_ERROR_FAILURE if there's security information missing even though + * it's an HTTPS connection. + */ +nsresult nsHttpChannel::ProcessSecurityHeaders() { + // If this channel is not loading securely, STS or PKP doesn't do anything. + // In the case of HSTS, the upgrade to HTTPS takes place earlier in the + // channel load process. + if (!mURI->SchemeIs("https")) { + return NS_OK; + } + + if (IsBrowsingContextDiscarded()) { + return NS_OK; + } + + nsAutoCString asciiHost; + nsresult rv = mURI->GetAsciiHost(asciiHost); + NS_ENSURE_SUCCESS(rv, NS_OK); + + // If the channel is not a hostname, but rather an IP, do not process STS + // or PKP headers + PRNetAddr hostAddr; + if (PR_SUCCESS == PR_StringToNetAddr(asciiHost.get(), &hostAddr)) + return NS_OK; + + // mSecurityInfo may not always be present, and if it's not then it is okay + // to just disregard any security headers since we know nothing about the + // security of the connection. + NS_ENSURE_TRUE(mSecurityInfo, NS_OK); + + uint32_t flags = + NS_UsePrivateBrowsing(this) ? nsISocketProvider::NO_PERMANENT_STORAGE : 0; + + // Get the TransportSecurityInfo + nsCOMPtr transSecInfo = + do_QueryInterface(mSecurityInfo); + NS_ENSURE_TRUE(transSecInfo, NS_ERROR_FAILURE); + + rv = ProcessSingleSecurityHeader(nsISiteSecurityService::HEADER_HSTS, + transSecInfo, flags); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +bool nsHttpChannel::IsHTTPS() { return mURI->SchemeIs("https"); } + +void nsHttpChannel::ProcessSSLInformation() { + // If this is HTTPS, record any use of RSA so that Key Exchange Algorithm + // can be whitelisted for TLS False Start in future sessions. We could + // do the same for DH but its rarity doesn't justify the lookup. + + if (mCanceled || NS_FAILED(mStatus) || !mSecurityInfo || !IsHTTPS() || + mPrivateBrowsing) + return; + + nsCOMPtr securityInfo = + do_QueryInterface(mSecurityInfo); + if (!securityInfo) return; + + uint32_t state; + if (securityInfo && NS_SUCCEEDED(securityInfo->GetSecurityState(&state)) && + (state & nsIWebProgressListener::STATE_IS_BROKEN)) { + // Send weak crypto warnings to the web console + if (state & nsIWebProgressListener::STATE_USES_WEAK_CRYPTO) { + nsString consoleErrorTag = u"WeakCipherSuiteWarning"_ns; + nsString consoleErrorCategory = u"SSL"_ns; + Unused << AddSecurityMessage(consoleErrorTag, consoleErrorCategory); + } + } + + // Send (SHA-1) signature algorithm errors to the web console + nsCOMPtr cert; + securityInfo->GetServerCert(getter_AddRefs(cert)); + if (cert) { + UniqueCERTCertificate nssCert(cert->GetCert()); + if (nssCert) { + SECOidTag tag = SECOID_GetAlgorithmTag(&nssCert->signature); + LOG(("Checking certificate signature: The OID tag is %i [this=%p]\n", tag, + this)); + // Check to see if the signature is sha-1 based. + // Not including checks for SEC_OID_ISO_SHA1_WITH_RSA_SIGNATURE + // from http://tools.ietf.org/html/rfc2437#section-8 since I + // can't see reference to it outside this spec + if (tag == SEC_OID_PKCS1_SHA1_WITH_RSA_ENCRYPTION || + tag == SEC_OID_ANSIX9_DSA_SIGNATURE_WITH_SHA1_DIGEST || + tag == SEC_OID_ANSIX962_ECDSA_SHA1_SIGNATURE) { + nsString consoleErrorTag = u"SHA1Sig"_ns; + nsString consoleErrorMessage = u"SHA-1 Signature"_ns; + Unused << AddSecurityMessage(consoleErrorTag, consoleErrorMessage); + } + } + } + + uint16_t tlsVersion; + nsresult rv = securityInfo->GetProtocolVersion(&tlsVersion); + if (NS_SUCCEEDED(rv) && + tlsVersion != nsITransportSecurityInfo::TLS_VERSION_1_2 && + tlsVersion != nsITransportSecurityInfo::TLS_VERSION_1_3) { + nsString consoleErrorTag = u"DeprecatedTLSVersion2"_ns; + nsString consoleErrorCategory = u"TLS"_ns; + Unused << AddSecurityMessage(consoleErrorTag, consoleErrorCategory); + } +} + +void nsHttpChannel::ProcessAltService() { + // e.g. Alt-Svc: h2=":443"; ma=60 + // e.g. Alt-Svc: h2="otherhost:443" + // Alt-Svc = 1#( alternative *( OWS ";" OWS parameter ) ) + // alternative = protocol-id "=" alt-authority + // protocol-id = token ; percent-encoded ALPN protocol identifier + // alt-authority = quoted-string ; containing [ uri-host ] ":" port + + if (!LoadAllowAltSvc()) { // per channel opt out + return; + } + + if (!gHttpHandler->AllowAltSvc() || (mCaps & NS_HTTP_DISALLOW_SPDY)) { + return; + } + + if (IsBrowsingContextDiscarded()) { + return; + } + + nsAutoCString scheme; + mURI->GetScheme(scheme); + bool isHttp = scheme.EqualsLiteral("http"); + if (!isHttp && !scheme.EqualsLiteral("https")) { + return; + } + + nsAutoCString altSvc; + Unused << mResponseHead->GetHeader(nsHttp::Alternate_Service, altSvc); + if (altSvc.IsEmpty()) { + return; + } + + if (!nsHttp::IsReasonableHeaderValue(altSvc)) { + LOG(("Alt-Svc Response Header seems unreasonable - skipping\n")); + return; + } + + nsAutoCString originHost; + int32_t originPort = 80; + mURI->GetPort(&originPort); + if (NS_FAILED(mURI->GetAsciiHost(originHost))) { + return; + } + + nsCOMPtr callbacks; + nsCOMPtr proxyInfo; + NS_NewNotificationCallbacksAggregation(mCallbacks, mLoadGroup, + getter_AddRefs(callbacks)); + + if (mProxyInfo) { + proxyInfo = do_QueryInterface(mProxyInfo); + } + + OriginAttributes originAttributes; + // Regular principal in case we have a proxy. + if (proxyInfo && + !StaticPrefs::privacy_partition_network_state_connection_with_proxy()) { + StoragePrincipalHelper::GetOriginAttributes( + this, originAttributes, StoragePrincipalHelper::eRegularPrincipal); + } else { + StoragePrincipalHelper::GetOriginAttributesForNetworkState( + this, originAttributes); + } + + AltSvcMapping::ProcessHeader( + altSvc, scheme, originHost, originPort, mUsername, GetTopWindowOrigin(), + mPrivateBrowsing, IsIsolated(), callbacks, proxyInfo, + mCaps & NS_HTTP_DISALLOW_SPDY, originAttributes); +} + +nsresult nsHttpChannel::ProcessResponse() { + uint32_t httpStatus = mResponseHead->Status(); + + LOG(("nsHttpChannel::ProcessResponse [this=%p httpStatus=%u]\n", this, + httpStatus)); + + // Gather data on whether the transaction and page (if this is + // the initial page load) is being loaded with SSL. + Telemetry::Accumulate(Telemetry::HTTP_TRANSACTION_IS_SSL, + mConnectionInfo->EndToEndSSL()); + if (mLoadFlags & LOAD_INITIAL_DOCUMENT_URI) { + Telemetry::Accumulate(Telemetry::HTTP_PAGELOAD_IS_SSL, + mConnectionInfo->EndToEndSSL()); + } + + if (Telemetry::CanRecordPrereleaseData()) { + // how often do we see something like Alt-Svc: "443:quic,p=1" + // and Alt-Svc: "h3-****" + nsAutoCString alt_service; + Unused << mResponseHead->GetHeader(nsHttp::Alternate_Service, alt_service); + uint32_t saw_quic = 0; + if (!alt_service.IsEmpty()) { + if (PL_strstr(alt_service.get(), "h3-")) { + saw_quic = 1; + } else if (PL_strstr(alt_service.get(), "quic")) { + saw_quic = 2; + } + } + Telemetry::Accumulate(Telemetry::HTTP_SAW_QUIC_ALT_PROTOCOL_2, saw_quic); + + // Gather data on how many URLS get redirected + switch (httpStatus) { + case 200: + Telemetry::Accumulate(Telemetry::HTTP_RESPONSE_STATUS_CODE, 0); + break; + case 301: + Telemetry::Accumulate(Telemetry::HTTP_RESPONSE_STATUS_CODE, 1); + break; + case 302: + Telemetry::Accumulate(Telemetry::HTTP_RESPONSE_STATUS_CODE, 2); + break; + case 304: + Telemetry::Accumulate(Telemetry::HTTP_RESPONSE_STATUS_CODE, 3); + break; + case 307: + Telemetry::Accumulate(Telemetry::HTTP_RESPONSE_STATUS_CODE, 4); + break; + case 308: + Telemetry::Accumulate(Telemetry::HTTP_RESPONSE_STATUS_CODE, 5); + break; + case 400: + Telemetry::Accumulate(Telemetry::HTTP_RESPONSE_STATUS_CODE, 6); + break; + case 401: + Telemetry::Accumulate(Telemetry::HTTP_RESPONSE_STATUS_CODE, 7); + break; + case 403: + Telemetry::Accumulate(Telemetry::HTTP_RESPONSE_STATUS_CODE, 8); + break; + case 404: + Telemetry::Accumulate(Telemetry::HTTP_RESPONSE_STATUS_CODE, 9); + break; + case 500: + Telemetry::Accumulate(Telemetry::HTTP_RESPONSE_STATUS_CODE, 10); + break; + default: + Telemetry::Accumulate(Telemetry::HTTP_RESPONSE_STATUS_CODE, 11); + break; + } + } + + // Let the predictor know whether this was a cacheable response or not so + // that it knows whether or not to possibly prefetch this resource in the + // future. + // We use GetReferringPage because mReferrerInfo may not be set at all(this is + // especially useful in xpcshell tests, where we don't have an actual pageload + // to get a referrer from). + nsCOMPtr referrer = GetReferringPage(); + if (!referrer && mReferrerInfo) { + referrer = mReferrerInfo->GetOriginalReferrer(); + } + + if (referrer) { + nsCOMPtr lci = GetLoadContextInfo(this); + mozilla::net::Predictor::UpdateCacheability( + referrer, mURI, httpStatus, mRequestHead, mResponseHead.get(), lci, + IsThirdPartyTrackingResource()); + } + + // Only allow 407 (authentication required) to continue + if (mTransaction && mTransaction->ProxyConnectFailed() && httpStatus != 407) { + return ProcessFailedProxyConnect(httpStatus); + } + + MOZ_ASSERT(!mCachedContentIsValid || mRaceCacheWithNetwork, + "We should not be hitting the network if we have valid cached " + "content unless we are racing the network and cache"); + + ProcessSSLInformation(); + + // notify "http-on-examine-response" observers + gHttpHandler->OnExamineResponse(this); + + return ContinueProcessResponse1(); +} + +void nsHttpChannel::AsyncContinueProcessResponse() { + nsresult rv; + rv = ContinueProcessResponse1(); + if (NS_FAILED(rv)) { + // A synchronous failure here would normally be passed as the return + // value from OnStartRequest, which would in turn cancel the request. + // If we're continuing asynchronously, we need to cancel the request + // ourselves. + Unused << Cancel(rv); + } +} + +nsresult nsHttpChannel::ContinueProcessResponse1() { + MOZ_ASSERT(!mCallOnResume, "How did that happen?"); + nsresult rv; + + if (mSuspendCount) { + LOG(("Waiting until resume to finish processing response [this=%p]\n", + this)); + mCallOnResume = [](nsHttpChannel* self) { + self->AsyncContinueProcessResponse(); + return NS_OK; + }; + return NS_OK; + } + + rv = ProcessCrossOriginResourcePolicyHeader(); + if (NS_FAILED(rv)) { + mStatus = NS_ERROR_DOM_CORP_FAILED; + HandleAsyncAbort(); + return NS_OK; + } + + rv = ComputeCrossOriginOpenerPolicyMismatch(); + if (rv == NS_ERROR_BLOCKED_BY_POLICY) { + // this navigates the doc's browsing context to a network error. + mStatus = NS_ERROR_BLOCKED_BY_POLICY; + HandleAsyncAbort(); + return NS_OK; + } + + // Check if request was cancelled during http-on-examine-response. + if (mCanceled) { + return CallOnStartRequest(); + } + + uint32_t httpStatus = mResponseHead->Status(); + + // STS, Cookies and Alt-Service should not be handled on proxy failure. + // If proxy CONNECT response needs to complete, wait to process connection + // for Strict-Transport-Security. + if (!(mTransaction && mTransaction->ProxyConnectFailed()) && + (httpStatus != 407)) { + if (nsAutoCString cookie; + NS_SUCCEEDED(mResponseHead->GetHeader(nsHttp::Set_Cookie, cookie))) { + SetCookie(cookie); + nsCOMPtr parentChannel; + NS_QueryNotificationCallbacks(this, parentChannel); + if (RefPtr httpParent = + do_QueryObject(parentChannel)) { + httpParent->SetCookie(std::move(cookie)); + } + } + + // Given a successful connection, process any STS or PKP data that's + // relevant. + DebugOnly rv = ProcessSecurityHeaders(); + MOZ_ASSERT(NS_SUCCEEDED(rv), "ProcessSTSHeader failed, continuing load."); + + if ((httpStatus < 500) && (httpStatus != 421)) { + ProcessAltService(); + } + } + + if (LoadConcurrentCacheAccess() && LoadCachedContentIsPartial() && + httpStatus != 206) { + LOG( + (" only expecting 206 when doing partial request during " + "interrupted cache concurrent read")); + return NS_ERROR_CORRUPTED_CONTENT; + } + + // handle unused username and password in url (see bug 232567) + if (httpStatus != 401 && httpStatus != 407) { + if (!mAuthRetryPending) { + rv = mAuthProvider->CheckForSuperfluousAuth(); + if (NS_FAILED(rv)) { + LOG((" CheckForSuperfluousAuth failed (%08x)", + static_cast(rv))); + } + } + if (mCanceled) return CallOnStartRequest(); + + // reset the authentication's current continuation state because ourvr + // last authentication attempt has been completed successfully + rv = mAuthProvider->Disconnect(NS_ERROR_ABORT); + if (NS_FAILED(rv)) { + LOG((" Disconnect failed (%08x)", static_cast(rv))); + } + mAuthProvider = nullptr; + LOG((" continuation state has been reset")); + } + + rv = ProcessCrossOriginEmbedderPolicyHeader(); + if (NS_FAILED(rv)) { + mStatus = NS_ERROR_BLOCKED_BY_POLICY; + HandleAsyncAbort(); + return NS_OK; + } + + // No process switch needed, continue as normal. + return ContinueProcessResponse2(rv); +} + +nsresult nsHttpChannel::ContinueProcessResponse2(nsresult rv) { + if (NS_FAILED(rv) && !mCanceled) { + // The process switch failed, cancel this channel. + Cancel(rv); + return CallOnStartRequest(); + } + + if (mAPIRedirectToURI && !mCanceled) { + MOZ_ASSERT(!LoadOnStartRequestCalled()); + nsCOMPtr redirectTo; + mAPIRedirectToURI.swap(redirectTo); + + PushRedirectAsyncFunc(&nsHttpChannel::ContinueProcessResponse3); + rv = StartRedirectChannelToURI(redirectTo, + nsIChannelEventSink::REDIRECT_TEMPORARY); + if (NS_SUCCEEDED(rv)) { + return NS_OK; + } + PopRedirectAsyncFunc(&nsHttpChannel::ContinueProcessResponse3); + } + + // Hack: ContinueProcessResponse3 uses NS_OK to detect successful + // redirects, so we distinguish this codepath (a non-redirect that's + // processing normally) by passing in a bogus error code. + return ContinueProcessResponse3(NS_BINDING_FAILED); +} + +nsresult nsHttpChannel::ContinueProcessResponse3(nsresult rv) { + LOG(("nsHttpChannel::ContinueProcessResponse3 [this=%p, rv=%" PRIx32 "]", + this, static_cast(rv))); + + if (NS_SUCCEEDED(rv)) { + // redirectTo() has passed through, we don't want to go on with + // this channel. It will now be canceled by the redirect handling + // code that called this function. + return NS_OK; + } + + rv = NS_OK; + + uint32_t httpStatus = mResponseHead->Status(); + bool transactionRestarted = mTransaction->TakeRestartedState(); + + // handle different server response categories. Note that we handle + // caching or not caching of error pages in + // nsHttpResponseHead::MustValidate; if you change this switch, update that + // one + switch (httpStatus) { + case 200: + case 203: + // Per RFC 2616, 14.35.2, "A server MAY ignore the Range header". + // So if a server does that and sends 200 instead of 206 that we + // expect, notify our caller. + // However, if we wanted to start from the beginning, let it go through + if (LoadResuming() && mStartPos != 0) { + LOG(("Server ignored our Range header, cancelling [this=%p]\n", this)); + Cancel(NS_ERROR_NOT_RESUMABLE); + rv = CallOnStartRequest(); + break; + } + // these can normally be cached + rv = ProcessNormal(); + MaybeInvalidateCacheEntryForSubsequentGet(); + break; + case 206: + if (LoadCachedContentIsPartial()) { // an internal byte range request... + auto func = [](auto* self, nsresult aRv) { + return self->ContinueProcessResponseAfterPartialContent(aRv); + }; + rv = ProcessPartialContent(func); + // Directly call ContinueProcessResponseAfterPartialContent if channel + // is not suspended or ProcessPartialContent throws. + if (!mSuspendCount || NS_FAILED(rv)) { + return ContinueProcessResponseAfterPartialContent(rv); + } + return NS_OK; + } else { + mCacheInputStream.CloseAndRelease(); + rv = ProcessNormal(); + } + break; + case 300: + case 301: + case 302: + case 307: + case 308: + case 303: +#if 0 + case 305: // disabled as a security measure (see bug 187996). +#endif + // don't store the response body for redirects + MaybeInvalidateCacheEntryForSubsequentGet(); + PushRedirectAsyncFunc(&nsHttpChannel::ContinueProcessResponse4); + rv = AsyncProcessRedirection(httpStatus); + if (NS_FAILED(rv)) { + PopRedirectAsyncFunc(&nsHttpChannel::ContinueProcessResponse4); + LOG(("AsyncProcessRedirection failed [rv=%" PRIx32 "]\n", + static_cast(rv))); + // don't cache failed redirect responses. + if (mCacheEntry) mCacheEntry->AsyncDoom(nullptr); + if (DoNotRender3xxBody(rv)) { + mStatus = rv; + DoNotifyListener(); + } else { + rv = ContinueProcessResponse4(rv); + } + } + break; + case 304: + if (!ShouldBypassProcessNotModified()) { + auto func = [](auto* self, nsresult aRv) { + return self->ContinueProcessResponseAfterNotModified(aRv); + }; + rv = ProcessNotModified(func); + // Directly call ContinueProcessResponseAfterNotModified if channel + // is not suspended or ProcessNotModified throws. + if (!mSuspendCount || NS_FAILED(rv)) { + return ContinueProcessResponseAfterNotModified(rv); + } + return NS_OK; + } + + // Don't cache uninformative 304 + if (LoadCustomConditionalRequest()) { + CloseCacheEntry(false); + } + + if (ShouldBypassProcessNotModified() || NS_FAILED(rv)) { + rv = ProcessNormal(); + } + break; + case 401: + case 407: + if (MOZ_UNLIKELY(httpStatus == 407 && transactionRestarted)) { + // The transaction has been internally restarted. We want to + // authenticate to the proxy again, so reuse either cached credentials + // or use default credentials for NTLM/Negotiate. This prevents + // considering the previously used creadentials as invalid. + mAuthProvider->ClearProxyIdent(); + } + if (MOZ_UNLIKELY(LoadCustomAuthHeader()) && httpStatus == 401) { + // When a custom auth header fails, we don't want to try + // any cached credentials, nor we want to ask the user. + // It's up to the consumer to re-try w/o setting a custom + // auth header if cached credentials should be attempted. + rv = NS_ERROR_FAILURE; + } else { + rv = mAuthProvider->ProcessAuthentication( + httpStatus, mConnectionInfo->EndToEndSSL() && mTransaction && + mTransaction->ProxyConnectFailed()); + } + if (rv == NS_ERROR_IN_PROGRESS) { + // authentication prompt has been invoked and result + // is expected asynchronously + mIsAuthChannel = true; + mAuthRetryPending = true; + if (httpStatus == 407 || + (mTransaction && mTransaction->ProxyConnectFailed())) + StoreProxyAuthPending(true); + + // suspend the transaction pump to stop receiving the + // unauthenticated content data. We will throw that data + // away when user provides credentials or resume the pump + // when user refuses to authenticate. + LOG( + ("Suspending the transaction, asynchronously prompting for " + "credentials")); + mTransactionPump->Suspend(); + +#ifdef DEBUG + // This is for test purposes only. See bug 1683176 for details. + gHttpHandler->OnTransactionSuspendedDueToAuthentication(this); +#endif + rv = NS_OK; + } else if (NS_FAILED(rv)) { + LOG(("ProcessAuthentication failed [rv=%" PRIx32 "]\n", + static_cast(rv))); + if (mTransaction && mTransaction->ProxyConnectFailed()) { + return ProcessFailedProxyConnect(httpStatus); + } + if (!mAuthRetryPending) { + rv = mAuthProvider->CheckForSuperfluousAuth(); + if (NS_FAILED(rv)) { + LOG(("CheckForSuperfluousAuth failed [rv=%x]\n", + static_cast(rv))); + } + } + rv = ProcessNormal(); + } else { + mIsAuthChannel = true; + mAuthRetryPending = true; // see DoAuthRetry + } + break; + + case 425: + case 429: + // Do not cache 425 and 429. + CloseCacheEntry(false); + [[fallthrough]]; // process normally + default: + rv = ProcessNormal(); + MaybeInvalidateCacheEntryForSubsequentGet(); + break; + } + + UpdateCacheDisposition(false, false); + return rv; +} + +nsresult nsHttpChannel::ContinueProcessResponseAfterPartialContent( + nsresult aRv) { + LOG( + ("nsHttpChannel::ContinueProcessResponseAfterPartialContent " + "[this=%p, rv=%" PRIx32 "]", + this, static_cast(aRv))); + + UpdateCacheDisposition(false, NS_SUCCEEDED(aRv)); + return aRv; +} + +nsresult nsHttpChannel::ContinueProcessResponseAfterNotModified(nsresult aRv) { + LOG( + ("nsHttpChannel::ContinueProcessResponseAfterNotModified " + "[this=%p, rv=%" PRIx32 "]", + this, static_cast(aRv))); + + if (NS_SUCCEEDED(aRv)) { + StoreTransactionReplaced(true); + UpdateCacheDisposition(true, false); + return NS_OK; + } + + LOG(("ProcessNotModified failed [rv=%" PRIx32 "]\n", + static_cast(aRv))); + + // We cannot read from the cache entry, it might be in an + // incosistent state. Doom it and redirect the channel + // to the same URI to reload from the network. + mCacheInputStream.CloseAndRelease(); + if (mCacheEntry) { + mCacheEntry->AsyncDoom(nullptr); + mCacheEntry = nullptr; + } + + nsresult rv = + StartRedirectChannelToURI(mURI, nsIChannelEventSink::REDIRECT_INTERNAL); + if (NS_SUCCEEDED(rv)) { + return NS_OK; + } + + // Don't cache uninformative 304 + if (LoadCustomConditionalRequest()) { + CloseCacheEntry(false); + } + + if (ShouldBypassProcessNotModified() || NS_FAILED(rv)) { + rv = ProcessNormal(); + } + + UpdateCacheDisposition(false, false); + return rv; +} + +void nsHttpChannel::UpdateCacheDisposition(bool aSuccessfulReval, + bool aPartialContentUsed) { + if (mRaceDelay && !mRaceCacheWithNetwork && + (LoadCachedContentIsPartial() || mDidReval)) { + if (aSuccessfulReval || aPartialContentUsed) { + AccumulateCategorical( + Telemetry::LABELS_NETWORK_RACE_CACHE_VALIDATION::CachedContentUsed); + } else { + AccumulateCategorical(Telemetry::LABELS_NETWORK_RACE_CACHE_VALIDATION:: + CachedContentNotUsed); + } + } + + if (Telemetry::CanRecordPrereleaseData()) { + CacheDisposition cacheDisposition; + if (!mDidReval) { + cacheDisposition = kCacheMissed; + } else if (aSuccessfulReval) { + cacheDisposition = kCacheHitViaReval; + } else { + cacheDisposition = kCacheMissedViaReval; + } + AccumulateCacheHitTelemetry(cacheDisposition, this); + mCacheDisposition = cacheDisposition; + + Telemetry::Accumulate(Telemetry::HTTP_RESPONSE_VERSION, + static_cast(mResponseHead->Version())); + + if (mResponseHead->Version() == HttpVersion::v0_9) { + // DefaultPortTopLevel = 0, DefaultPortSubResource = 1, + // NonDefaultPortTopLevel = 2, NonDefaultPortSubResource = 3 + uint32_t v09Info = 0; + if (!(mLoadFlags & LOAD_INITIAL_DOCUMENT_URI)) { + v09Info += 1; + } + if (mConnectionInfo->OriginPort() != mConnectionInfo->DefaultPort()) { + v09Info += 2; + } + Telemetry::Accumulate(Telemetry::HTTP_09_INFO, v09Info); + } + } +} + +nsresult nsHttpChannel::ContinueProcessResponse4(nsresult rv) { + bool doNotRender = DoNotRender3xxBody(rv); + + if (rv == NS_ERROR_DOM_BAD_URI && mRedirectURI) { + bool isHTTP = + mRedirectURI->SchemeIs("http") || mRedirectURI->SchemeIs("https"); + if (!isHTTP) { + // This was a blocked attempt to redirect and subvert the system by + // redirecting to another protocol (perhaps javascript:) + // In that case we want to throw an error instead of displaying the + // non-redirected response body. + LOG(("ContinueProcessResponse4 detected rejected Non-HTTP Redirection")); + doNotRender = true; + rv = NS_ERROR_CORRUPTED_CONTENT; + } + } + + if (doNotRender) { + Cancel(rv); + DoNotifyListener(); + return rv; + } + + if (NS_SUCCEEDED(rv)) { + UpdateInhibitPersistentCachingFlag(); + + MaybeCreateCacheEntryWhenRCWN(); + + rv = InitCacheEntry(); + if (NS_FAILED(rv)) { + LOG( + ("ContinueProcessResponse4 " + "failed to init cache entry [rv=%x]\n", + static_cast(rv))); + } + CloseCacheEntry(false); + + if (mApplicationCacheForWrite) { + // Store response in the offline cache + Unused << InitOfflineCacheEntry(); + CloseOfflineCacheEntry(); + } + return NS_OK; + } + + LOG(("ContinueProcessResponse4 got failure result [rv=%" PRIx32 "]\n", + static_cast(rv))); + if (mTransaction && mTransaction->ProxyConnectFailed()) { + return ProcessFailedProxyConnect(mRedirectType); + } + return ProcessNormal(); +} + +nsresult nsHttpChannel::ProcessNormal() { + nsresult rv; + + LOG(("nsHttpChannel::ProcessNormal [this=%p]\n", this)); + + bool succeeded; + rv = GetRequestSucceeded(&succeeded); + if (NS_SUCCEEDED(rv) && !succeeded) { + PushRedirectAsyncFunc(&nsHttpChannel::ContinueProcessNormal); + bool waitingForRedirectCallback; + Unused << ProcessFallback(&waitingForRedirectCallback); + if (waitingForRedirectCallback) { + // The transaction has been suspended by ProcessFallback. + return NS_OK; + } + PopRedirectAsyncFunc(&nsHttpChannel::ContinueProcessNormal); + } + + return ContinueProcessNormal(NS_OK); +} + +nsresult nsHttpChannel::ContinueProcessNormal(nsresult rv) { + LOG(("nsHttpChannel::ContinueProcessNormal [this=%p]", this)); + + if (NS_FAILED(rv)) { + // Fill the failure status here, we have failed to fall back, thus we + // have to report our status as failed. + mStatus = rv; + DoNotifyListener(); + return rv; + } + + if (LoadFallingBack()) { + // Do not continue with normal processing, fallback is in + // progress now. + return NS_OK; + } + + // if we're here, then any byte-range requests failed to result in a partial + // response. we must clear this flag to prevent BufferPartialContent from + // being called inside our OnDataAvailable (see bug 136678). + StoreCachedContentIsPartial(false); + + ClearBogusContentEncodingIfNeeded(); + + UpdateInhibitPersistentCachingFlag(); + + MaybeCreateCacheEntryWhenRCWN(); + + // this must be called before firing OnStartRequest, since http clients, + // such as imagelib, expect our cache entry to already have the correct + // expiration time (bug 87710). + if (mCacheEntry) { + rv = InitCacheEntry(); + if (NS_FAILED(rv)) CloseCacheEntry(true); + } + + // Check that the server sent us what we were asking for + if (LoadResuming()) { + // Create an entity id from the response + nsAutoCString id; + rv = GetEntityID(id); + if (NS_FAILED(rv)) { + // If creating an entity id is not possible -> error + Cancel(NS_ERROR_NOT_RESUMABLE); + } else if (mResponseHead->Status() != 206 && + mResponseHead->Status() != 200) { + // Probably 404 Not Found, 412 Precondition Failed or + // 416 Invalid Range -> error + LOG(("Unexpected response status while resuming, aborting [this=%p]\n", + this)); + Cancel(NS_ERROR_ENTITY_CHANGED); + } + // If we were passed an entity id, verify it's equal to the server's + else if (!mEntityID.IsEmpty()) { + if (!mEntityID.Equals(id)) { + LOG(("Entity mismatch, expected '%s', got '%s', aborting [this=%p]", + mEntityID.get(), id.get(), this)); + Cancel(NS_ERROR_ENTITY_CHANGED); + } + } + } + + rv = CallOnStartRequest(); + if (NS_FAILED(rv)) return rv; + + // install cache listener if we still have a cache entry open + if (mCacheEntry && !LoadCacheEntryIsReadOnly()) { + rv = InstallCacheListener(); + if (NS_FAILED(rv)) return rv; + } + + return NS_OK; +} + +nsresult nsHttpChannel::PromptTempRedirect() { + if (!gHttpHandler->PromptTempRedirect()) { + return NS_OK; + } + nsresult rv; + nsCOMPtr bundleService = + do_GetService(NS_STRINGBUNDLE_CONTRACTID, &rv); + if (NS_FAILED(rv)) return rv; + + nsCOMPtr stringBundle; + rv = + bundleService->CreateBundle(NECKO_MSGS_URL, getter_AddRefs(stringBundle)); + if (NS_FAILED(rv)) return rv; + + nsAutoString messageString; + rv = stringBundle->GetStringFromName("RepostFormData", messageString); + if (NS_SUCCEEDED(rv)) { + bool repost = false; + + nsCOMPtr prompt; + GetCallback(prompt); + if (!prompt) return NS_ERROR_NO_INTERFACE; + + prompt->Confirm(nullptr, messageString.get(), &repost); + if (!repost) return NS_ERROR_FAILURE; + } + + return rv; +} + +nsresult nsHttpChannel::ProxyFailover() { + LOG(("nsHttpChannel::ProxyFailover [this=%p]\n", this)); + + nsresult rv; + + nsCOMPtr pps = + do_GetService(NS_PROTOCOLPROXYSERVICE_CONTRACTID, &rv); + if (NS_FAILED(rv)) return rv; + + nsCOMPtr pi; + rv = pps->GetFailoverForProxy(mConnectionInfo->ProxyInfo(), mURI, mStatus, + getter_AddRefs(pi)); + if (NS_FAILED(rv)) return rv; + + // XXXbz so where does this codepath remove us from the loadgroup, + // exactly? + return AsyncDoReplaceWithProxy(pi); +} + +void nsHttpChannel::SetHTTPSSVCRecord( + already_AddRefed&& aRecord) { + LOG(("nsHttpChannel::SetHTTPSSVCRecord [this=%p]\n", this)); + nsCOMPtr record = aRecord; + MOZ_ASSERT(!mHTTPSSVCRecord); + mHTTPSSVCRecord.emplace(std::move(record)); +} + +void nsHttpChannel::HandleAsyncRedirectChannelToHttps() { + MOZ_ASSERT(!mCallOnResume, "How did that happen?"); + + if (mSuspendCount) { + LOG(("Waiting until resume to do async redirect to https [this=%p]\n", + this)); + mCallOnResume = [](nsHttpChannel* self) { + self->HandleAsyncRedirectChannelToHttps(); + return NS_OK; + }; + return; + } + + nsresult rv = StartRedirectChannelToHttps(); + if (NS_FAILED(rv)) { + rv = ContinueAsyncRedirectChannelToURI(rv); + if (NS_FAILED(rv)) { + LOG(("ContinueAsyncRedirectChannelToURI failed (%08x) [this=%p]\n", + static_cast(rv), this)); + } + } +} + +nsresult nsHttpChannel::StartRedirectChannelToHttps() { + LOG(("nsHttpChannel::HandleAsyncRedirectChannelToHttps() [STS]\n")); + + nsCOMPtr upgradedURI; + nsresult rv = NS_GetSecureUpgradedURI(mURI, getter_AddRefs(upgradedURI)); + NS_ENSURE_SUCCESS(rv, rv); + + return StartRedirectChannelToURI( + upgradedURI, nsIChannelEventSink::REDIRECT_PERMANENT | + nsIChannelEventSink::REDIRECT_STS_UPGRADE); +} + +void nsHttpChannel::HandleAsyncAPIRedirect() { + MOZ_ASSERT(!mCallOnResume, "How did that happen?"); + MOZ_ASSERT(mAPIRedirectToURI, "How did that happen?"); + + if (mSuspendCount) { + LOG(("Waiting until resume to do async API redirect [this=%p]\n", this)); + mCallOnResume = [](nsHttpChannel* self) { + self->HandleAsyncAPIRedirect(); + return NS_OK; + }; + return; + } + + nsresult rv = StartRedirectChannelToURI( + mAPIRedirectToURI, nsIChannelEventSink::REDIRECT_PERMANENT); + if (NS_FAILED(rv)) { + rv = ContinueAsyncRedirectChannelToURI(rv); + if (NS_FAILED(rv)) { + LOG(("ContinueAsyncRedirectChannelToURI failed (%08x) [this=%p]\n", + static_cast(rv), this)); + } + } +} + +nsresult nsHttpChannel::StartRedirectChannelToURI(nsIURI* upgradedURI, + uint32_t flags) { + nsresult rv = NS_OK; + LOG(("nsHttpChannel::StartRedirectChannelToURI()\n")); + + nsCOMPtr newChannel; + nsCOMPtr redirectLoadInfo = + CloneLoadInfoForRedirect(upgradedURI, flags); + + nsCOMPtr ioService; + rv = gHttpHandler->GetIOService(getter_AddRefs(ioService)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = NS_NewChannelInternal(getter_AddRefs(newChannel), upgradedURI, + redirectLoadInfo, + nullptr, // PerformanceStorage + nullptr, // aLoadGroup + nullptr, // aCallbacks + nsIRequest::LOAD_NORMAL, ioService); + NS_ENSURE_SUCCESS(rv, rv); + + rv = SetupReplacementChannel(upgradedURI, newChannel, true, flags); + NS_ENSURE_SUCCESS(rv, rv); + + if (mHTTPSSVCRecord) { + RefPtr httpChan = do_QueryObject(newChannel); + nsCOMPtr rec = mHTTPSSVCRecord.ref(); + if (httpChan && rec) { + httpChan->SetHTTPSSVCRecord(rec.forget()); + } + } + + // Inform consumers about this fake redirect + mRedirectChannel = newChannel; + + PushRedirectAsyncFunc(&nsHttpChannel::ContinueAsyncRedirectChannelToURI); + rv = gHttpHandler->AsyncOnChannelRedirect(this, newChannel, flags); + + if (NS_SUCCEEDED(rv)) rv = WaitForRedirectCallback(); + + if (NS_FAILED(rv)) { + AutoRedirectVetoNotifier notifier(this); + + /* Remove the async call to ContinueAsyncRedirectChannelToURI(). + * It is called directly by our callers upon return (to clean up + * the failed redirect). */ + PopRedirectAsyncFunc(&nsHttpChannel::ContinueAsyncRedirectChannelToURI); + } + + return rv; +} + +nsresult nsHttpChannel::ContinueAsyncRedirectChannelToURI(nsresult rv) { + LOG(("nsHttpChannel::ContinueAsyncRedirectChannelToURI [this=%p]", this)); + + // Since we handle mAPIRedirectToURI also after on-examine-response handler + // rather drop it here to avoid any redirect loops, even just hypothetical. + mAPIRedirectToURI = nullptr; + + if (NS_SUCCEEDED(rv)) { + rv = OpenRedirectChannel(rv); + } + + if (NS_FAILED(rv)) { + // Cancel the channel here, the update to https had been vetoed + // but from the security reasons we have to discard the whole channel + // load. + Cancel(rv); + } + + if (mLoadGroup) { + mLoadGroup->RemoveRequest(this, nullptr, mStatus); + } + + if (NS_FAILED(rv) && !mCachePump && !mTransactionPump) { + // We have to manually notify the listener because there is not any pump + // that would call our OnStart/StopRequest after resume from waiting for + // the redirect callback. + DoNotifyListener(); + } + + return rv; +} + +nsresult nsHttpChannel::OpenRedirectChannel(nsresult rv) { + AutoRedirectVetoNotifier notifier(this); + + // Make sure to do this after we received redirect veto answer, + // i.e. after all sinks had been notified + mRedirectChannel->SetOriginalURI(mOriginalURI); + + // open new channel + rv = mRedirectChannel->AsyncOpen(mListener); + + NS_ENSURE_SUCCESS(rv, rv); + + mStatus = NS_BINDING_REDIRECTED; + + notifier.RedirectSucceeded(); + + ReleaseListeners(); + + return NS_OK; +} + +nsresult nsHttpChannel::AsyncDoReplaceWithProxy(nsIProxyInfo* pi) { + LOG(("nsHttpChannel::AsyncDoReplaceWithProxy [this=%p pi=%p]", this, pi)); + nsresult rv; + + nsCOMPtr newChannel; + rv = gHttpHandler->NewProxiedChannel(mURI, pi, mProxyResolveFlags, mProxyURI, + mLoadInfo, getter_AddRefs(newChannel)); + if (NS_FAILED(rv)) return rv; + + uint32_t flags = nsIChannelEventSink::REDIRECT_INTERNAL; + + rv = SetupReplacementChannel(mURI, newChannel, true, flags); + if (NS_FAILED(rv)) return rv; + + // Inform consumers about this fake redirect + mRedirectChannel = newChannel; + + PushRedirectAsyncFunc(&nsHttpChannel::ContinueDoReplaceWithProxy); + rv = gHttpHandler->AsyncOnChannelRedirect(this, newChannel, flags); + + if (NS_SUCCEEDED(rv)) rv = WaitForRedirectCallback(); + + if (NS_FAILED(rv)) { + AutoRedirectVetoNotifier notifier(this); + PopRedirectAsyncFunc(&nsHttpChannel::ContinueDoReplaceWithProxy); + } + + return rv; +} + +nsresult nsHttpChannel::ContinueDoReplaceWithProxy(nsresult rv) { + AutoRedirectVetoNotifier notifier(this); + + if (NS_FAILED(rv)) return rv; + + MOZ_ASSERT(mRedirectChannel, "No redirect channel?"); + + // Make sure to do this after we received redirect veto answer, + // i.e. after all sinks had been notified + mRedirectChannel->SetOriginalURI(mOriginalURI); + + // open new channel + rv = mRedirectChannel->AsyncOpen(mListener); + NS_ENSURE_SUCCESS(rv, rv); + + mStatus = NS_BINDING_REDIRECTED; + + notifier.RedirectSucceeded(); + + ReleaseListeners(); + + return rv; +} + +nsresult nsHttpChannel::ResolveProxy() { + LOG(("nsHttpChannel::ResolveProxy [this=%p]\n", this)); + + nsresult rv; + + nsCOMPtr pps = + do_GetService(NS_PROTOCOLPROXYSERVICE_CONTRACTID, &rv); + if (NS_FAILED(rv)) return rv; + + // using the nsIProtocolProxyService2 allows a minor performance + // optimization, but if an add-on has only provided the original interface + // then it is ok to use that version. + nsCOMPtr pps2 = do_QueryInterface(pps); + if (pps2) { + rv = pps2->AsyncResolve2(this, mProxyResolveFlags, this, nullptr, + getter_AddRefs(mProxyRequest)); + } else { + rv = pps->AsyncResolve(static_cast(this), mProxyResolveFlags, + this, nullptr, getter_AddRefs(mProxyRequest)); + } + + return rv; +} + +bool nsHttpChannel::ResponseWouldVary(nsICacheEntry* entry) { + nsresult rv; + nsAutoCString buf, metaKey; + Unused << mCachedResponseHead->GetHeader(nsHttp::Vary, buf); + + constexpr auto prefix = "request-"_ns; + + // enumerate the elements of the Vary header... + for (const nsACString& token : + nsCCharSeparatedTokenizer(buf, NS_HTTP_HEADER_SEP).ToRange()) { + LOG( + ("nsHttpChannel::ResponseWouldVary [channel=%p] " + "processing %s\n", + this, nsPromiseFlatCString(token).get())); + // + // if "*", then assume response would vary. technically speaking, + // "Vary: header, *" is not permitted, but we allow it anyways. + // + // We hash values of cookie-headers for the following reasons: + // + // 1- cookies can be very large in size + // + // 2- cookies may contain sensitive information. (for parity with + // out policy of not storing Set-cookie headers in the cache + // meta data, we likewise do not want to store cookie headers + // here.) + // + if (token.EqualsLiteral("*")) { + return true; // if we encounter this, just get out of here + } + + // build cache meta data key... + metaKey = prefix + token; + + // check the last value of the given request header to see if it has + // since changed. if so, then indeed the cached response is invalid. + nsCString lastVal; + entry->GetMetaDataElement(metaKey.get(), getter_Copies(lastVal)); + LOG( + ("nsHttpChannel::ResponseWouldVary [channel=%p] " + "stored value = \"%s\"\n", + this, lastVal.get())); + + // Look for value of "Cookie" in the request headers + nsHttpAtom atom = nsHttp::ResolveAtom(token); + nsAutoCString newVal; + bool hasHeader = NS_SUCCEEDED(mRequestHead.GetHeader(atom, newVal)); + if (!lastVal.IsEmpty()) { + // value for this header in cache, but no value in request + if (!hasHeader) { + return true; // yes - response would vary + } + + // If this is a cookie-header, stored metadata is not + // the value itself but the hash. So we also hash the + // outgoing value here in order to compare the hashes + nsAutoCString hash; + if (atom == nsHttp::Cookie) { + rv = Hash(newVal.get(), hash); + // If hash failed, be conservative (the cached hash + // exists at this point) and claim response would vary + if (NS_FAILED(rv)) return true; + newVal = hash; + + LOG( + ("nsHttpChannel::ResponseWouldVary [this=%p] " + "set-cookie value hashed to %s\n", + this, newVal.get())); + } + + if (!newVal.Equals(lastVal)) { + return true; // yes, response would vary + } + + } else if (hasHeader) { // old value is empty, but newVal is set + return true; + } + } + + return false; +} + +// We need to have an implementation of this function just so that we can keep +// all references to mCallOnResume of type nsHttpChannel: it's not OK in C++ +// to set a member function ptr to a base class function. +void nsHttpChannel::HandleAsyncAbort() { + HttpAsyncAborter::HandleAsyncAbort(); +} + +//----------------------------------------------------------------------------- +// nsHttpChannel +//----------------------------------------------------------------------------- + +bool nsHttpChannel::IsResumable(int64_t partialLen, int64_t contentLength, + bool ignoreMissingPartialLen) const { + bool hasContentEncoding = + mCachedResponseHead->HasHeader(nsHttp::Content_Encoding); + + nsAutoCString etag; + Unused << mCachedResponseHead->GetHeader(nsHttp::ETag, etag); + bool hasWeakEtag = !etag.IsEmpty() && StringBeginsWith(etag, "W/"_ns); + + return (partialLen < contentLength) && + (partialLen > 0 || ignoreMissingPartialLen) && !hasContentEncoding && + !hasWeakEtag && mCachedResponseHead->IsResumable() && + !LoadCustomConditionalRequest() && !mCachedResponseHead->NoStore(); +} + +nsresult nsHttpChannel::MaybeSetupByteRangeRequest( + int64_t partialLen, int64_t contentLength, bool ignoreMissingPartialLen) { + // Be pesimistic + StoreIsPartialRequest(false); + + if (!IsResumable(partialLen, contentLength, ignoreMissingPartialLen)) + return NS_ERROR_NOT_RESUMABLE; + + // looks like a partial entry we can reuse; add If-Range + // and Range headers. + nsresult rv = SetupByteRangeRequest(partialLen); + if (NS_FAILED(rv)) { + // Make the request unconditional again. + UntieByteRangeRequest(); + } + + return rv; +} + +nsresult nsHttpChannel::SetupByteRangeRequest(int64_t partialLen) { + // cached content has been found to be partial, add necessary request + // headers to complete cache entry. + + // use strongest validator available... + nsAutoCString val; + Unused << mCachedResponseHead->GetHeader(nsHttp::ETag, val); + if (val.IsEmpty()) + Unused << mCachedResponseHead->GetHeader(nsHttp::Last_Modified, val); + if (val.IsEmpty()) { + // if we hit this code it means mCachedResponseHead->IsResumable() is + // either broken or not being called. + MOZ_ASSERT_UNREACHABLE("no cache validator"); + StoreIsPartialRequest(false); + return NS_ERROR_FAILURE; + } + + char buf[64]; + SprintfLiteral(buf, "bytes=%" PRId64 "-", partialLen); + + DebugOnly rv; + rv = mRequestHead.SetHeader(nsHttp::Range, nsDependentCString(buf)); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + rv = mRequestHead.SetHeader(nsHttp::If_Range, val); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + StoreIsPartialRequest(true); + + return NS_OK; +} + +void nsHttpChannel::UntieByteRangeRequest() { + DebugOnly rv; + rv = mRequestHead.ClearHeader(nsHttp::Range); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + rv = mRequestHead.ClearHeader(nsHttp::If_Range); + MOZ_ASSERT(NS_SUCCEEDED(rv)); +} + +nsresult nsHttpChannel::ProcessPartialContent( + const std::function& + aContinueProcessResponseFunc) { + // ok, we've just received a 206 + // + // we need to stream whatever data is in the cache out first, and then + // pick up whatever data is on the wire, writing it into the cache. + + LOG(("nsHttpChannel::ProcessPartialContent [this=%p]\n", this)); + + NS_ENSURE_TRUE(mCachedResponseHead, NS_ERROR_NOT_INITIALIZED); + NS_ENSURE_TRUE(mCacheEntry, NS_ERROR_NOT_INITIALIZED); + + // Make sure to clear bogus content-encodings before looking at the header + ClearBogusContentEncodingIfNeeded(); + + // Check if the content-encoding we now got is different from the one we + // got before + nsAutoCString contentEncoding, cachedContentEncoding; + // It is possible that there is not such headers + Unused << mResponseHead->GetHeader(nsHttp::Content_Encoding, contentEncoding); + Unused << mCachedResponseHead->GetHeader(nsHttp::Content_Encoding, + cachedContentEncoding); + if (PL_strcasecmp(contentEncoding.get(), cachedContentEncoding.get()) != 0) { + Cancel(NS_ERROR_INVALID_CONTENT_ENCODING); + return CallOnStartRequest(); + } + + nsresult rv; + + int64_t cachedContentLength = mCachedResponseHead->ContentLength(); + int64_t entitySize = mResponseHead->TotalEntitySize(); + + nsAutoCString contentRange; + Unused << mResponseHead->GetHeader(nsHttp::Content_Range, contentRange); + LOG( + ("nsHttpChannel::ProcessPartialContent [this=%p trans=%p] " + "original content-length %" PRId64 ", entity-size %" PRId64 + ", content-range %s\n", + this, mTransaction.get(), cachedContentLength, entitySize, + contentRange.get())); + + if ((entitySize >= 0) && (cachedContentLength >= 0) && + (entitySize != cachedContentLength)) { + LOG( + ("nsHttpChannel::ProcessPartialContent [this=%p] " + "206 has different total entity size than the content length " + "of the original partially cached entity.\n", + this)); + + mCacheEntry->AsyncDoom(nullptr); + Cancel(NS_ERROR_CORRUPTED_CONTENT); + return CallOnStartRequest(); + } + + if (LoadConcurrentCacheAccess()) { + // We started to read cached data sooner than its write has been done. + // But the concurrent write has not finished completely, so we had to + // do a range request. Now let the content coming from the network + // be presented to consumers and also stored to the cache entry. + + rv = InstallCacheListener(mLogicalOffset); + if (NS_FAILED(rv)) return rv; + + if (mOfflineCacheEntry) { + rv = InstallOfflineCacheListener(mLogicalOffset); + if (NS_FAILED(rv)) return rv; + } + } else { + // suspend the current transaction + rv = mTransactionPump->Suspend(); + if (NS_FAILED(rv)) return rv; + } + + // merge any new headers with the cached response headers + mCachedResponseHead->UpdateHeaders(mResponseHead.get()); + + // update the cached response head + nsAutoCString head; + mCachedResponseHead->Flatten(head, true); + rv = mCacheEntry->SetMetaDataElement("response-head", head.get()); + if (NS_FAILED(rv)) return rv; + + // make the cached response be the current response + mResponseHead = std::move(mCachedResponseHead); + + UpdateInhibitPersistentCachingFlag(); + + rv = UpdateExpirationTime(); + if (NS_FAILED(rv)) return rv; + + // notify observers interested in looking at a response that has been + // merged with any cached headers (http-on-examine-merged-response). + gHttpHandler->OnExamineMergedResponse(this); + + if (LoadConcurrentCacheAccess()) { + StoreCachedContentIsPartial(false); + // Leave the ConcurrentCacheAccess flag set, we want to use it + // to prevent duplicate OnStartRequest call on the target listener + // in case this channel is canceled before it gets its OnStartRequest + // from the http transaction. + return rv; + } + + // Now we continue reading the network response. + // the cached content is valid, although incomplete. + mCachedContentIsValid = true; + return CallOrWaitForResume([aContinueProcessResponseFunc](auto* self) { + nsresult rv = self->ReadFromCache(false); + return aContinueProcessResponseFunc(self, rv); + }); +} + +nsresult nsHttpChannel::OnDoneReadingPartialCacheEntry(bool* streamDone) { + nsresult rv; + + LOG(("nsHttpChannel::OnDoneReadingPartialCacheEntry [this=%p]", this)); + + // by default, assume we would have streamed all data or failed... + *streamDone = true; + + // setup cache listener to append to cache entry + int64_t size; + rv = mCacheEntry->GetDataSize(&size); + if (NS_FAILED(rv)) return rv; + + rv = InstallCacheListener(size); + if (NS_FAILED(rv)) return rv; + + // Entry is valid, do it now, after the output stream has been opened, + // otherwise when done earlier, pending readers would consider the cache + // entry still as partial (CacheEntry::GetDataSize would return the partial + // data size) and consumers would do the conditional request again. + rv = mCacheEntry->SetValid(); + if (NS_FAILED(rv)) return rv; + + // need to track the logical offset of the data being sent to our listener + mLogicalOffset = size; + + // we're now completing the cached content, so we can clear this flag. + // this puts us in the state of a regular download. + StoreCachedContentIsPartial(false); + // The cache input stream pump is finished, we do not need it any more. + // (see bug 1313923) + mCachePump = nullptr; + + // resume the transaction if it exists, otherwise the pipe contained the + // remaining part of the document and we've now streamed all of the data. + if (mTransactionPump) { + rv = mTransactionPump->Resume(); + if (NS_SUCCEEDED(rv)) *streamDone = false; + } else + MOZ_ASSERT_UNREACHABLE("no transaction"); + return rv; +} + +//----------------------------------------------------------------------------- +// nsHttpChannel +//----------------------------------------------------------------------------- + +bool nsHttpChannel::ShouldBypassProcessNotModified() { + if (LoadCustomConditionalRequest()) { + LOG(("Bypassing ProcessNotModified due to custom conditional headers")); + return true; + } + + if (!mDidReval) { + LOG( + ("Server returned a 304 response even though we did not send a " + "conditional request")); + return true; + } + + return false; +} + +nsresult nsHttpChannel::ProcessNotModified( + const std::function& + aContinueProcessResponseFunc) { + nsresult rv; + + LOG(("nsHttpChannel::ProcessNotModified [this=%p]\n", this)); + + // Assert ShouldBypassProcessNotModified() has been checked before call to + // ProcessNotModified(). + MOZ_ASSERT(!ShouldBypassProcessNotModified()); + + MOZ_ASSERT(mCachedResponseHead); + MOZ_ASSERT(mCacheEntry); + NS_ENSURE_TRUE(mCachedResponseHead && mCacheEntry, NS_ERROR_UNEXPECTED); + + // If the 304 response contains a Last-Modified different than the + // one in our cache that is pretty suspicious and is, in at least the + // case of bug 716840, a sign of the server having previously corrupted + // our cache with a bad response. Take the minor step here of just dooming + // that cache entry so there is a fighting chance of getting things on the + // right track. + + nsAutoCString lastModifiedCached; + nsAutoCString lastModified304; + + rv = + mCachedResponseHead->GetHeader(nsHttp::Last_Modified, lastModifiedCached); + if (NS_SUCCEEDED(rv)) { + rv = mResponseHead->GetHeader(nsHttp::Last_Modified, lastModified304); + } + + if (NS_SUCCEEDED(rv) && !lastModified304.Equals(lastModifiedCached)) { + LOG( + ("Cache Entry and 304 Last-Modified Headers Do Not Match " + "[%s] and [%s]\n", + lastModifiedCached.get(), lastModified304.get())); + + mCacheEntry->AsyncDoom(nullptr); + Telemetry::Accumulate(Telemetry::CACHE_LM_INCONSISTENT, true); + } + + // merge any new headers with the cached response headers + mCachedResponseHead->UpdateHeaders(mResponseHead.get()); + + // update the cached response head + nsAutoCString head; + mCachedResponseHead->Flatten(head, true); + rv = mCacheEntry->SetMetaDataElement("response-head", head.get()); + if (NS_FAILED(rv)) return rv; + + // make the cached response be the current response + mResponseHead = std::move(mCachedResponseHead); + + UpdateInhibitPersistentCachingFlag(); + + rv = UpdateExpirationTime(); + if (NS_FAILED(rv)) return rv; + + rv = AddCacheEntryHeaders(mCacheEntry); + if (NS_FAILED(rv)) return rv; + + // notify observers interested in looking at a reponse that has been + // merged with any cached headers + gHttpHandler->OnExamineMergedResponse(this); + + mCachedContentIsValid = true; + + // Tell other consumers the entry is OK to use + rv = mCacheEntry->SetValid(); + if (NS_FAILED(rv)) return rv; + + return CallOrWaitForResume([aContinueProcessResponseFunc](auto* self) { + nsresult rv = self->ReadFromCache(false); + return aContinueProcessResponseFunc(self, rv); + }); +} + +nsresult nsHttpChannel::ProcessFallback(bool* waitingForRedirectCallback) { + LOG(("nsHttpChannel::ProcessFallback [this=%p]\n", this)); + nsresult rv; + + *waitingForRedirectCallback = false; + StoreFallingBack(false); + + // At this point a load has failed (either due to network problems + // or an error returned on the server). Perform an application + // cache fallback if we have a URI to fall back to. + if (!mApplicationCache || mFallbackKey.IsEmpty() || LoadFallbackChannel()) { + LOG((" choosing not to fallback [%p,%s,%d]", mApplicationCache.get(), + mFallbackKey.get(), LoadFallbackChannel())); + return NS_OK; + } + + // Make sure the fallback entry hasn't been marked as a foreign + // entry. + uint32_t fallbackEntryType; + rv = mApplicationCache->GetTypes(mFallbackKey, &fallbackEntryType); + NS_ENSURE_SUCCESS(rv, rv); + + if (fallbackEntryType & nsIApplicationCache::ITEM_FOREIGN) { + // This cache points to a fallback that refers to a different + // manifest. Refuse to fall back. + return NS_OK; + } + + if (!IsInSubpathOfAppCacheManifest(mApplicationCache, mFallbackKey)) { + // Refuse to fallback if the fallback key is not contained in the same + // path as the cache manifest. + return NS_OK; + } + + MOZ_ASSERT(fallbackEntryType & nsIApplicationCache::ITEM_FALLBACK, + "Fallback entry not marked correctly!"); + + // Kill any offline cache entry, and disable offline caching for the + // fallback. + if (mOfflineCacheEntry) { + mOfflineCacheEntry->AsyncDoom(nullptr); + mOfflineCacheEntry = nullptr; + } + + mApplicationCacheForWrite = nullptr; + mOfflineCacheEntry = nullptr; + + // Close the current cache entry. + CloseCacheEntry(true); + + // Create a new channel to load the fallback entry. + RefPtr newChannel; + rv = gHttpHandler->NewChannel(mURI, mLoadInfo, getter_AddRefs(newChannel)); + NS_ENSURE_SUCCESS(rv, rv); + + uint32_t redirectFlags = nsIChannelEventSink::REDIRECT_INTERNAL; + rv = SetupReplacementChannel(mURI, newChannel, true, redirectFlags); + NS_ENSURE_SUCCESS(rv, rv); + + // Make sure the new channel loads from the fallback key. + nsCOMPtr httpInternal = + do_QueryInterface(newChannel, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = httpInternal->SetupFallbackChannel(mFallbackKey.get()); + NS_ENSURE_SUCCESS(rv, rv); + + // ... and fallbacks should only load from the cache. + uint32_t newLoadFlags = mLoadFlags | LOAD_REPLACE | LOAD_ONLY_FROM_CACHE; + rv = newChannel->SetLoadFlags(newLoadFlags); + + // Inform consumers about this fake redirect + mRedirectChannel = newChannel; + + PushRedirectAsyncFunc(&nsHttpChannel::ContinueProcessFallback); + rv = gHttpHandler->AsyncOnChannelRedirect(this, newChannel, redirectFlags); + + if (NS_SUCCEEDED(rv)) rv = WaitForRedirectCallback(); + + if (NS_FAILED(rv)) { + AutoRedirectVetoNotifier notifier(this); + PopRedirectAsyncFunc(&nsHttpChannel::ContinueProcessFallback); + return rv; + } + + // Indicate we are now waiting for the asynchronous redirect callback + // if all went OK. + *waitingForRedirectCallback = true; + return NS_OK; +} + +nsresult nsHttpChannel::ContinueProcessFallback(nsresult rv) { + AutoRedirectVetoNotifier notifier(this); + + if (NS_FAILED(rv)) return rv; + + MOZ_ASSERT(mRedirectChannel, "No redirect channel?"); + + // Make sure to do this after we received redirect veto answer, + // i.e. after all sinks had been notified + mRedirectChannel->SetOriginalURI(mOriginalURI); + + rv = mRedirectChannel->AsyncOpen(mListener); + NS_ENSURE_SUCCESS(rv, rv); + + if (mLoadFlags & LOAD_INITIAL_DOCUMENT_URI) { + MaybeWarnAboutAppCache(); + } + + // close down this channel + Cancel(NS_BINDING_REDIRECTED); + + notifier.RedirectSucceeded(); + + ReleaseListeners(); + + StoreFallingBack(true); + + return NS_OK; +} + +// Determines if a request is a byte range request for a subrange, +// i.e. is a byte range request, but not a 0- byte range request. +static bool IsSubRangeRequest(nsHttpRequestHead& aRequestHead) { + nsAutoCString byteRange; + if (NS_FAILED(aRequestHead.GetHeader(nsHttp::Range, byteRange))) { + return false; + } + return !byteRange.EqualsLiteral("bytes=0-"); +} + +nsresult nsHttpChannel::OpenCacheEntry(bool isHttps) { + // Drop this flag here + StoreConcurrentCacheAccess(0); + + StoreLoadedFromApplicationCache(false); + + LOG(("nsHttpChannel::OpenCacheEntry [this=%p]", this)); + + // make sure we're not abusing this function + MOZ_ASSERT(!mCacheEntry, "cache entry already open"); + + if (mRequestHead.IsPost()) { + // If the post id is already set then this is an attempt to replay + // a post transaction via the cache. Otherwise, we need a unique + // post id for this transaction. + if (mPostID == 0) mPostID = gHttpHandler->GenerateUniqueID(); + } else if (!mRequestHead.IsGet() && !mRequestHead.IsHead()) { + // don't use the cache for other types of requests + return NS_OK; + } + + // Pick up an application cache from the notification + // callbacks if available and if we are not an intercepted channel. + if (!mApplicationCache && LoadInheritApplicationCache()) { + nsCOMPtr appCacheContainer; + GetCallback(appCacheContainer); + + if (appCacheContainer) { + appCacheContainer->GetApplicationCache(getter_AddRefs(mApplicationCache)); + } + } + + return OpenCacheEntryInternal(isHttps, mApplicationCache, true); +} + +bool nsHttpChannel::IsIsolated() { + if (LoadHasBeenIsolatedChecked()) { + return LoadIsIsolated(); + } + StoreIsIsolated( + StaticPrefs::browser_cache_cache_isolation() || + (IsThirdPartyTrackingResource() && + !ContentBlocking::ShouldAllowAccessFor(this, mURI, nullptr))); + StoreHasBeenIsolatedChecked(true); + return LoadIsIsolated(); +} + +const nsCString& nsHttpChannel::GetTopWindowOrigin() { + if (LoadTopWindowOriginComputed()) { + return mTopWindowOrigin; + } + + nsCOMPtr topWindowURI; + nsresult rv = GetTopWindowURI(getter_AddRefs(topWindowURI)); + bool isDocument = false; + if (NS_FAILED(rv) && NS_SUCCEEDED(GetIsMainDocumentChannel(&isDocument)) && + isDocument) { + // For top-level documents, use the document channel's origin to compute + // the unique storage space identifier instead of the top Window URI. + rv = NS_GetFinalChannelURI(this, getter_AddRefs(topWindowURI)); + NS_ENSURE_SUCCESS(rv, mTopWindowOrigin); + } + + rv = nsContentUtils::GetASCIIOrigin(topWindowURI ? topWindowURI : mURI, + mTopWindowOrigin); + NS_ENSURE_SUCCESS(rv, mTopWindowOrigin); + + StoreTopWindowOriginComputed(true); + + return mTopWindowOrigin; +} + +nsresult nsHttpChannel::OpenCacheEntryInternal( + bool isHttps, nsIApplicationCache* applicationCache, + bool allowApplicationCache) { + MOZ_ASSERT_IF(!allowApplicationCache, !applicationCache); + + nsresult rv; + + if (LoadResuming()) { + // We don't support caching for requests initiated + // via nsIResumableChannel. + return NS_OK; + } + + // Don't cache byte range requests which are subranges, only cache 0- + // byte range requests. + if (IsSubRangeRequest(mRequestHead)) { + return NS_OK; + } + + // Handle correctly CacheEntriesToWaitFor + AutoCacheWaitFlags waitFlags(this); + + nsAutoCString cacheKey; + + nsCOMPtr cacheStorageService( + services::GetCacheStorageService()); + if (!cacheStorageService) { + return NS_ERROR_NOT_AVAILABLE; + } + + nsCOMPtr cacheStorage; + if (!mFallbackKey.IsEmpty() && LoadFallbackChannel()) { + // This is a fallback channel, open fallback URI instead + rv = NS_NewURI(getter_AddRefs(mCacheEntryURI), mFallbackKey); + NS_ENSURE_SUCCESS(rv, rv); + } else { + mCacheEntryURI = mURI; + } + + RefPtr info = GetLoadContextInfo(this); + if (!info) { + return NS_ERROR_FAILURE; + } + + uint32_t cacheEntryOpenFlags; + bool offline = gIOService->IsOffline(); + + bool maybeRCWN = false; + + nsAutoCString cacheControlRequestHeader; + Unused << mRequestHead.GetHeader(nsHttp::Cache_Control, + cacheControlRequestHeader); + CacheControlParser cacheControlRequest(cacheControlRequestHeader); + if (cacheControlRequest.NoStore()) { + goto bypassCacheEntryOpen; + } + + if (offline || (mLoadFlags & INHIBIT_CACHING)) { + if (BYPASS_LOCAL_CACHE(mLoadFlags, LoadPreferCacheLoadOverBypass()) && + !offline) { + goto bypassCacheEntryOpen; + } + cacheEntryOpenFlags = nsICacheStorage::OPEN_READONLY; + StoreCacheEntryIsReadOnly(true); + } else if (BYPASS_LOCAL_CACHE(mLoadFlags, LoadPreferCacheLoadOverBypass()) && + !applicationCache) { + cacheEntryOpenFlags = nsICacheStorage::OPEN_TRUNCATE; + } else { + cacheEntryOpenFlags = + nsICacheStorage::OPEN_NORMALLY | nsICacheStorage::CHECK_MULTITHREADED; + } + + // Remember the request is a custom conditional request so that we can + // process any 304 response correctly. + StoreCustomConditionalRequest( + mRequestHead.HasHeader(nsHttp::If_Modified_Since) || + mRequestHead.HasHeader(nsHttp::If_None_Match) || + mRequestHead.HasHeader(nsHttp::If_Unmodified_Since) || + mRequestHead.HasHeader(nsHttp::If_Match) || + mRequestHead.HasHeader(nsHttp::If_Range)); + + if (!mPostID && applicationCache) { + rv = cacheStorageService->AppCacheStorage(info, applicationCache, + getter_AddRefs(cacheStorage)); + } else if (mLoadFlags & INHIBIT_PERSISTENT_CACHING) { + rv = cacheStorageService->MemoryCacheStorage( + info, // ? choose app cache as well... + getter_AddRefs(cacheStorage)); + } else if (LoadPinCacheContent()) { + rv = cacheStorageService->PinningCacheStorage(info, + getter_AddRefs(cacheStorage)); + } else { + bool lookupAppCache = (LoadChooseApplicationCache() || + (mLoadFlags & LOAD_CHECK_OFFLINE_CACHE)) && + !mPostID && MOZ_LIKELY(allowApplicationCache); + // Try to race only if we use disk cache storage and we don't lookup + // app cache first + maybeRCWN = (!lookupAppCache) && mRequestHead.IsSafeMethod(); + rv = cacheStorageService->DiskCacheStorage(info, lookupAppCache, + getter_AddRefs(cacheStorage)); + } + NS_ENSURE_SUCCESS(rv, rv); + + if ((mClassOfService & nsIClassOfService::Leader) || + (mLoadFlags & LOAD_INITIAL_DOCUMENT_URI)) + cacheEntryOpenFlags |= nsICacheStorage::OPEN_PRIORITY; + + // Only for backward compatibility with the old cache back end. + // When removed, remove the flags and related code snippets. + if (mLoadFlags & LOAD_BYPASS_LOCAL_CACHE_IF_BUSY) + cacheEntryOpenFlags |= nsICacheStorage::OPEN_BYPASS_IF_BUSY; + + if (mPostID) { + mCacheIdExtension.Append(nsPrintfCString("%d", mPostID)); + } + if (LoadIsTRRServiceChannel()) { + mCacheIdExtension.Append("TRR"); + } + if (mRequestHead.IsHead()) { + mCacheIdExtension.Append("HEAD"); + } + + if (IsIsolated()) { + auto& topWindowOrigin = GetTopWindowOrigin(); + if (topWindowOrigin.IsEmpty()) { + return NS_ERROR_FAILURE; + } + + mCacheIdExtension.Append("-unique:"); + mCacheIdExtension.Append(topWindowOrigin); + } + + mCacheOpenWithPriority = cacheEntryOpenFlags & nsICacheStorage::OPEN_PRIORITY; + mCacheQueueSizeWhenOpen = + CacheStorageService::CacheQueueSize(mCacheOpenWithPriority); + + if (StaticPrefs::network_http_rcwn_enabled() && maybeRCWN && + !mApplicationCacheForWrite) { + bool hasAltData = false; + uint32_t sizeInKb = 0; + rv = cacheStorage->GetCacheIndexEntryAttrs( + mCacheEntryURI, mCacheIdExtension, &hasAltData, &sizeInKb); + + // We will attempt to race the network vs the cache if we've found + // this entry in the cache index, and it has appropriate attributes + // (doesn't have alt-data, and has a small size) + if (NS_SUCCEEDED(rv) && !hasAltData && + sizeInKb < StaticPrefs::network_http_rcwn_small_resource_size_kb()) { + MaybeRaceCacheWithNetwork(); + } + } + + if (!mCacheOpenDelay) { + MOZ_ASSERT(NS_IsMainThread(), "Should be called on the main thread"); + if (mNetworkTriggered) { + mRaceCacheWithNetwork = StaticPrefs::network_http_rcwn_enabled(); + } + rv = cacheStorage->AsyncOpenURI(mCacheEntryURI, mCacheIdExtension, + cacheEntryOpenFlags, this); + } else { + // We pass `this` explicitly as a parameter due to the raw pointer + // to refcounted object in lambda analysis. + mCacheOpenFunc = [cacheEntryOpenFlags, + cacheStorage](nsHttpChannel* self) -> void { + MOZ_ASSERT(NS_IsMainThread(), "Should be called on the main thread"); + cacheStorage->AsyncOpenURI(self->mCacheEntryURI, self->mCacheIdExtension, + cacheEntryOpenFlags, self); + }; + + // calls nsHttpChannel::Notify after `mCacheOpenDelay` milliseconds + NS_NewTimerWithCallback(getter_AddRefs(mCacheOpenTimer), this, + mCacheOpenDelay, nsITimer::TYPE_ONE_SHOT); + } + NS_ENSURE_SUCCESS(rv, rv); + + waitFlags.Keep(WAIT_FOR_CACHE_ENTRY); + +bypassCacheEntryOpen: + if (!mApplicationCacheForWrite || !allowApplicationCache) return NS_OK; + + // If there is an app cache to write to, open the entry right now in parallel. + + // make sure we're not abusing this function + MOZ_ASSERT(!mOfflineCacheEntry, "cache entry already open"); + + if (offline) { + // only put things in the offline cache while online + return NS_OK; + } + + if (mLoadFlags & INHIBIT_CACHING) { + // respect demand not to cache + return NS_OK; + } + + if (!mRequestHead.IsGet()) { + // only cache complete documents offline + return NS_OK; + } + + rv = cacheStorageService->AppCacheStorage(info, mApplicationCacheForWrite, + getter_AddRefs(cacheStorage)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = cacheStorage->AsyncOpenURI(mURI, ""_ns, nsICacheStorage::OPEN_TRUNCATE, + this); + NS_ENSURE_SUCCESS(rv, rv); + + waitFlags.Keep(WAIT_FOR_OFFLINE_CACHE_ENTRY); + + return NS_OK; +} + +nsresult nsHttpChannel::CheckPartial(nsICacheEntry* aEntry, int64_t* aSize, + int64_t* aContentLength) { + return nsHttp::CheckPartial( + aEntry, aSize, aContentLength, + mCachedResponseHead ? mCachedResponseHead.get() : mResponseHead.get()); +} + +void nsHttpChannel::UntieValidationRequest() { + DebugOnly rv; + // Make the request unconditional again. + rv = mRequestHead.ClearHeader(nsHttp::If_Modified_Since); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + rv = mRequestHead.ClearHeader(nsHttp::If_None_Match); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + rv = mRequestHead.ClearHeader(nsHttp::ETag); + MOZ_ASSERT(NS_SUCCEEDED(rv)); +} + +NS_IMETHODIMP +nsHttpChannel::OnCacheEntryCheck(nsICacheEntry* entry, + nsIApplicationCache* appCache, + uint32_t* aResult) { + nsresult rv = NS_OK; + + LOG(("nsHttpChannel::OnCacheEntryCheck enter [channel=%p entry=%p]", this, + entry)); + + mozilla::MutexAutoLock lock(mRCWNLock); + + if (mRaceCacheWithNetwork && mFirstResponseSource == RESPONSE_FROM_NETWORK) { + LOG( + ("Not using cached response because we've already got one from the " + "network\n")); + *aResult = ENTRY_NOT_WANTED; + + // Net-win indicates that mOnStartRequestTimestamp is from net. + int64_t savedTime = + (TimeStamp::Now() - mOnStartRequestTimestamp).ToMilliseconds(); + Telemetry::Accumulate(Telemetry::NETWORK_RACE_CACHE_WITH_NETWORK_SAVED_TIME, + savedTime); + return NS_OK; + } else if (mRaceCacheWithNetwork && + mFirstResponseSource == RESPONSE_PENDING) { + mOnCacheEntryCheckTimestamp = TimeStamp::Now(); + } + + nsAutoCString cacheControlRequestHeader; + Unused << mRequestHead.GetHeader(nsHttp::Cache_Control, + cacheControlRequestHeader); + CacheControlParser cacheControlRequest(cacheControlRequestHeader); + + if (cacheControlRequest.NoStore()) { + LOG( + ("Not using cached response based on no-store request cache " + "directive\n")); + *aResult = ENTRY_NOT_WANTED; + return NS_OK; + } + + // Be pessimistic: assume the cache entry has no useful data. + *aResult = ENTRY_WANTED; + mCachedContentIsValid = false; + + nsCString buf; + + // Get the method that was used to generate the cached response + rv = entry->GetMetaDataElement("request-method", getter_Copies(buf)); + NS_ENSURE_SUCCESS(rv, rv); + + bool methodWasHead = buf.EqualsLiteral("HEAD"); + bool methodWasGet = buf.EqualsLiteral("GET"); + + if (methodWasHead) { + // The cached response does not contain an entity. We can only reuse + // the response if the current request is also HEAD. + if (!mRequestHead.IsHead()) { + *aResult = ENTRY_NOT_WANTED; + return NS_OK; + } + } + buf.Adopt(nullptr); + + // We'll need this value in later computations... + uint32_t lastModifiedTime; + rv = entry->GetLastModified(&lastModifiedTime); + NS_ENSURE_SUCCESS(rv, rv); + + // Determine if this is the first time that this cache entry + // has been accessed during this session. + bool fromPreviousSession = + (gHttpHandler->SessionStartTime() > lastModifiedTime); + + // Get the cached HTTP response headers + mCachedResponseHead = MakeUnique(); + + rv = nsHttp::GetHttpResponseHeadFromCacheEntry(entry, + mCachedResponseHead.get()); + NS_ENSURE_SUCCESS(rv, rv); + + bool isCachedRedirect = WillRedirect(*mCachedResponseHead); + + // Do not return 304 responses from the cache, and also do not return + // any other non-redirect 3xx responses from the cache (see bug 759043). + NS_ENSURE_TRUE((mCachedResponseHead->Status() / 100 != 3) || isCachedRedirect, + NS_ERROR_ABORT); + + if (mCachedResponseHead->NoStore() && LoadCacheEntryIsReadOnly()) { + // This prevents loading no-store responses when navigating back + // while the browser is set to work offline. + LOG((" entry loading as read-only but is no-store, set INHIBIT_CACHING")); + mLoadFlags |= nsIRequest::INHIBIT_CACHING; + } + + // Don't bother to validate items that are read-only, + // unless they are read-only because of INHIBIT_CACHING or because + // we're updating the offline cache. + // Don't bother to validate if this is a fallback entry. + if (!mApplicationCacheForWrite && + (appCache || (LoadCacheEntryIsReadOnly() && + !(mLoadFlags & nsIRequest::INHIBIT_CACHING)))) { + if (!appCache) { + int64_t size, contentLength; + rv = CheckPartial(entry, &size, &contentLength); + NS_ENSURE_SUCCESS(rv, rv); + + if (contentLength != int64_t(-1) && contentLength != size) { + *aResult = ENTRY_NOT_WANTED; + return NS_OK; + } + } + + rv = OpenCacheInputStream(entry, true, !!appCache); + if (NS_SUCCEEDED(rv)) { + mCachedContentIsValid = true; + entry->MaybeMarkValid(); + } + return rv; + } + + bool wantCompleteEntry = false; + + if (!methodWasHead && !isCachedRedirect) { + // If the cached content-length is set and it does not match the data + // size of the cached content, then the cached response is partial... + // either we need to issue a byte range request or we need to refetch + // the entire document. + // + // We exclude redirects from this check because we (usually) strip the + // entity when we store the cache entry, and even if we didn't, we + // always ignore a cached redirect's entity anyway. See bug 759043. + int64_t size, contentLength; + rv = CheckPartial(entry, &size, &contentLength); + NS_ENSURE_SUCCESS(rv, rv); + + if (size == int64_t(-1)) { + LOG((" write is in progress")); + if (mLoadFlags & LOAD_BYPASS_LOCAL_CACHE_IF_BUSY) { + LOG( + (" not interested in the entry, " + "LOAD_BYPASS_LOCAL_CACHE_IF_BUSY specified")); + + *aResult = ENTRY_NOT_WANTED; + return NS_OK; + } + + // Ignore !(size > 0) from the resumability condition + if (!IsResumable(size, contentLength, true)) { + if (IsNavigation()) { + LOG( + (" bypassing wait for the entry, " + "this is a navigational load")); + *aResult = ENTRY_NOT_WANTED; + return NS_OK; + } + + LOG( + (" wait for entry completion, " + "response is not resumable")); + + wantCompleteEntry = true; + } else { + StoreConcurrentCacheAccess(1); + } + } else if (contentLength != int64_t(-1) && contentLength != size) { + LOG( + ("Cached data size does not match the Content-Length header " + "[content-length=%" PRId64 " size=%" PRId64 "]\n", + contentLength, size)); + + rv = MaybeSetupByteRangeRequest(size, contentLength); + StoreCachedContentIsPartial(NS_SUCCEEDED(rv) && LoadIsPartialRequest()); + if (LoadCachedContentIsPartial()) { + rv = OpenCacheInputStream(entry, false, !!appCache); + if (NS_FAILED(rv)) { + UntieByteRangeRequest(); + return rv; + } + + *aResult = ENTRY_NEEDS_REVALIDATION; + return NS_OK; + } + + if (size == 0 && LoadCacheOnlyMetadata()) { + // Don't break cache entry load when the entry's data size + // is 0 and CacheOnlyMetadata flag is set. In that case we + // want to proceed since the LOAD_ONLY_IF_MODIFIED flag is + // also set. + MOZ_ASSERT(mLoadFlags & LOAD_ONLY_IF_MODIFIED); + } else { + return rv; + } + } + } + + bool isHttps = mURI->SchemeIs("https"); + + bool doValidation = false; + bool doBackgroundValidation = false; + bool canAddImsHeader = true; + + bool isForcedValid = false; + entry->GetIsForcedValid(&isForcedValid); + + bool weaklyFramed, isImmutable; + nsHttp::DetermineFramingAndImmutability(entry, mCachedResponseHead.get(), + isHttps, &weaklyFramed, &isImmutable); + + // Cached entry is not the entity we request (see bug #633743) + if (ResponseWouldVary(entry)) { + LOG(("Validating based on Vary headers returning TRUE\n")); + canAddImsHeader = false; + doValidation = true; + } else { + doValidation = nsHttp::ValidationRequired( + isForcedValid, mCachedResponseHead.get(), mLoadFlags, + LoadAllowStaleCacheContent(), isImmutable, + LoadCustomConditionalRequest(), mRequestHead, entry, + cacheControlRequest, fromPreviousSession, &doBackgroundValidation); + } + + nsAutoCString requestedETag; + if (!doValidation && + NS_SUCCEEDED(mRequestHead.GetHeader(nsHttp::If_Match, requestedETag)) && + (methodWasGet || methodWasHead)) { + nsAutoCString cachedETag; + Unused << mCachedResponseHead->GetHeader(nsHttp::ETag, cachedETag); + if (!cachedETag.IsEmpty() && (StringBeginsWith(cachedETag, "W/"_ns) || + !requestedETag.Equals(cachedETag))) { + // User has defined If-Match header, if the cached entry is not + // matching the provided header value or the cached ETag is weak, + // force validation. + doValidation = true; + } + } + + // Previous error should not be propagated. + rv = NS_OK; + + if (!doValidation) { + // + // Check the authorization headers used to generate the cache entry. + // We must validate the cache entry if: + // + // 1) the cache entry was generated prior to this session w/ + // credentials (see bug 103402). + // 2) the cache entry was generated w/o credentials, but would now + // require credentials (see bug 96705). + // + // NOTE: this does not apply to proxy authentication. + // + entry->GetMetaDataElement("auth", getter_Copies(buf)); + doValidation = + (fromPreviousSession && !buf.IsEmpty()) || + (buf.IsEmpty() && mRequestHead.HasHeader(nsHttp::Authorization)); + } + + // Bug #561276: We maintain a chain of cache-keys which returns cached + // 3xx-responses (redirects) in order to detect cycles. If a cycle is + // found, ignore the cached response and hit the net. Otherwise, use + // the cached response and add the cache-key to the chain. Note that + // a limited number of redirects (cached or not) is allowed and is + // enforced independently of this mechanism + if (!doValidation && isCachedRedirect) { + nsAutoCString cacheKey; + rv = GenerateCacheKey(mPostID, cacheKey); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + if (!mRedirectedCachekeys) + mRedirectedCachekeys = MakeUnique>(); + else if (mRedirectedCachekeys->Contains(cacheKey)) + doValidation = true; + + LOG(("Redirection-chain %s key %s\n", + doValidation ? "contains" : "does not contain", cacheKey.get())); + + // Append cacheKey if not in the chain already + if (!doValidation) mRedirectedCachekeys->AppendElement(cacheKey); + } + + mCachedContentIsValid = !doValidation; + + if (doValidation) { + // + // now, we are definitely going to issue a HTTP request to the server. + // make it conditional if possible. + // + // do not attempt to validate no-store content, since servers will not + // expect it to be cached. (we only keep it in our cache for the + // purposes of back/forward, etc.) + // + // the request method MUST be either GET or HEAD (see bug 175641) and + // the cached response code must be < 400 + // + // the cached content must not be weakly framed or marked immutable + // + // do not override conditional headers when consumer has defined its own + if (!mCachedResponseHead->NoStore() && + (mRequestHead.IsGet() || mRequestHead.IsHead()) && + !LoadCustomConditionalRequest() && !weaklyFramed && !isImmutable && + (mCachedResponseHead->Status() < 400)) { + if (LoadConcurrentCacheAccess()) { + // In case of concurrent read and also validation request we + // must wait for the current writer to close the output stream + // first. Otherwise, when the writer's job would have been interrupted + // before all the data were downloaded, we'd have to do a range request + // which would be a second request in line during this channel's + // life-time. nsHttpChannel is not designed to do that, so rather + // turn off concurrent read and wait for entry's completion. + // Then only re-validation or range-re-validation request will go out. + StoreConcurrentCacheAccess(0); + // This will cause that OnCacheEntryCheck is called again with the same + // entry after the writer is done. + wantCompleteEntry = true; + } else { + nsAutoCString val; + // Add If-Modified-Since header if a Last-Modified was given + // and we are allowed to do this (see bugs 510359 and 269303) + if (canAddImsHeader) { + Unused << mCachedResponseHead->GetHeader(nsHttp::Last_Modified, val); + if (!val.IsEmpty()) { + rv = mRequestHead.SetHeader(nsHttp::If_Modified_Since, val); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + } + // Add If-None-Match header if an ETag was given in the response + Unused << mCachedResponseHead->GetHeader(nsHttp::ETag, val); + if (!val.IsEmpty()) { + rv = mRequestHead.SetHeader(nsHttp::If_None_Match, val); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + mDidReval = true; + } + } + } + + if (mCachedContentIsValid || mDidReval) { + rv = OpenCacheInputStream(entry, mCachedContentIsValid, !!appCache); + if (NS_FAILED(rv)) { + // If we can't get the entity then we have to act as though we + // don't have the cache entry. + if (mDidReval) { + UntieValidationRequest(); + mDidReval = false; + } + mCachedContentIsValid = false; + } + } + + if (mDidReval) + *aResult = ENTRY_NEEDS_REVALIDATION; + else if (wantCompleteEntry) + *aResult = RECHECK_AFTER_WRITE_FINISHED; + else { + *aResult = ENTRY_WANTED; + + if (doBackgroundValidation) { + PerformBackgroundCacheRevalidation(); + } + } + + if (mCachedContentIsValid) { + entry->MaybeMarkValid(); + } + + LOG( + ("nsHTTPChannel::OnCacheEntryCheck exit [this=%p doValidation=%d " + "result=%d]\n", + this, doValidation, *aResult)); + return rv; +} + +NS_IMETHODIMP +nsHttpChannel::OnCacheEntryAvailable(nsICacheEntry* entry, bool aNew, + nsIApplicationCache* aAppCache, + nsresult status) { + MOZ_ASSERT(NS_IsMainThread()); + + nsresult rv; + + LOG( + ("nsHttpChannel::OnCacheEntryAvailable [this=%p entry=%p " + "new=%d appcache=%p status=%" PRIx32 + " mAppCache=%p mAppCacheForWrite=%p]\n", + this, entry, aNew, aAppCache, static_cast(status), + mApplicationCache.get(), mApplicationCacheForWrite.get())); + + // if the channel's already fired onStopRequest, then we should ignore + // this event. + if (!LoadIsPending()) { + mCacheInputStream.CloseAndRelease(); + return NS_OK; + } + + rv = OnCacheEntryAvailableInternal(entry, aNew, aAppCache, status); + if (NS_FAILED(rv)) { + CloseCacheEntry(false); + if (mRaceCacheWithNetwork && mNetworkTriggered && + mFirstResponseSource != RESPONSE_FROM_CACHE) { + // Ignore the error if we're racing cache with network and the cache + // didn't win, The network part will handle cancelation or any other + // error. Otherwise we could end up calling the listener twice, see + // bug 1397593. + LOG( + (" not calling AsyncAbort() because we're racing cache with " + "network")); + } else { + Unused << AsyncAbort(rv); + } + } + + return NS_OK; +} + +nsresult nsHttpChannel::OnCacheEntryAvailableInternal( + nsICacheEntry* entry, bool aNew, nsIApplicationCache* aAppCache, + nsresult status) { + nsresult rv; + + if (mCanceled) { + LOG(("channel was canceled [this=%p status=%" PRIx32 "]\n", this, + static_cast(static_cast(mStatus)))); + return mStatus; + } + + if (mIgnoreCacheEntry) { + if (!entry || aNew) { + // We use this flag later to decide whether to report + // LABELS_NETWORK_RACE_CACHE_VALIDATION::NotSent. We didn't have + // an usable entry, so drop the flag. + mIgnoreCacheEntry = false; + } + entry = nullptr; + status = NS_ERROR_NOT_AVAILABLE; + } + + if (aAppCache) { + if (mApplicationCache == aAppCache && !mCacheEntry) { + rv = OnOfflineCacheEntryAvailable(entry, aNew, aAppCache, status); + } else if (mApplicationCacheForWrite == aAppCache && aNew && + !mOfflineCacheEntry) { + rv = OnOfflineCacheEntryForWritingAvailable(entry, aAppCache, status); + } else { + rv = OnOfflineCacheEntryAvailable(entry, aNew, aAppCache, status); + } + } else { + rv = OnNormalCacheEntryAvailable(entry, aNew, status); + } + + if (NS_FAILED(rv) && (mLoadFlags & LOAD_ONLY_FROM_CACHE)) { + // If we have a fallback URI (and we're not already + // falling back), process the fallback asynchronously. + if (!LoadFallbackChannel() && !mFallbackKey.IsEmpty()) { + return AsyncCall(&nsHttpChannel::HandleAsyncFallback); + } + + return NS_ERROR_DOCUMENT_NOT_CACHED; + } + + if (NS_FAILED(rv)) { + return rv; + } + + // We may be waiting for more callbacks... + if (AwaitingCacheCallbacks()) { + return NS_OK; + } + + if (mRaceCacheWithNetwork && ((mCacheEntry && !mCachedContentIsValid && + (mDidReval || LoadCachedContentIsPartial())) || + mIgnoreCacheEntry)) { + // We won't send the conditional request because the unconditional + // request was already sent (see bug 1377223). + AccumulateCategorical( + Telemetry::LABELS_NETWORK_RACE_CACHE_VALIDATION::NotSent); + } + + if (mRaceCacheWithNetwork && mCachedContentIsValid) { + Unused << ReadFromCache(true); + } + + return TriggerNetwork(); +} + +nsresult nsHttpChannel::OnNormalCacheEntryAvailable(nsICacheEntry* aEntry, + bool aNew, + nsresult aEntryStatus) { + StoreCacheEntriesToWaitFor(LoadCacheEntriesToWaitFor() & + ~WAIT_FOR_CACHE_ENTRY); + + if (NS_FAILED(aEntryStatus) || aNew) { + // Make sure this flag is dropped. It may happen the entry is doomed + // between OnCacheEntryCheck and OnCacheEntryAvailable. + mCachedContentIsValid = false; + + // From the same reason remove any conditional headers added + // in OnCacheEntryCheck. + if (mDidReval) { + LOG((" Removing conditional request headers")); + UntieValidationRequest(); + mDidReval = false; + } + + if (LoadCachedContentIsPartial()) { + LOG((" Removing byte range request headers")); + UntieByteRangeRequest(); + StoreCachedContentIsPartial(false); + } + + if (mLoadFlags & LOAD_ONLY_FROM_CACHE) { + // if this channel is only allowed to pull from the cache, then + // we must fail if we were unable to open a cache entry for read. + return NS_ERROR_DOCUMENT_NOT_CACHED; + } + } + + if (NS_SUCCEEDED(aEntryStatus)) { + mCacheEntry = aEntry; + StoreCacheEntryIsWriteOnly(aNew); + + if (!aNew && !mAsyncOpenTime.IsNull()) { + // We use microseconds for IO operations. For consistency let's use + // microseconds here too. + uint32_t duration = (TimeStamp::Now() - mAsyncOpenTime).ToMicroseconds(); + bool isSlow = false; + if ((mCacheOpenWithPriority && + mCacheQueueSizeWhenOpen >= + StaticPrefs:: + network_http_rcwn_cache_queue_priority_threshold()) || + (!mCacheOpenWithPriority && + mCacheQueueSizeWhenOpen >= + StaticPrefs::network_http_rcwn_cache_queue_normal_threshold())) { + isSlow = true; + } + CacheFileUtils::CachePerfStats::AddValue( + CacheFileUtils::CachePerfStats::ENTRY_OPEN, duration, isSlow); + } + + if (mLoadFlags & LOAD_INITIAL_DOCUMENT_URI) { + Telemetry::Accumulate(Telemetry::HTTP_OFFLINE_CACHE_DOCUMENT_LOAD, false); + } + } + + return NS_OK; +} + +nsresult nsHttpChannel::OnOfflineCacheEntryAvailable( + nsICacheEntry* aEntry, bool aNew, nsIApplicationCache* aAppCache, + nsresult aEntryStatus) { + MOZ_ASSERT(!mApplicationCache || aAppCache == mApplicationCache); + MOZ_ASSERT(!aNew || !aEntry || mApplicationCacheForWrite); + + StoreCacheEntriesToWaitFor(LoadCacheEntriesToWaitFor() & + ~WAIT_FOR_CACHE_ENTRY); + + nsresult rv; + + if (NS_SUCCEEDED(aEntryStatus)) { + if (!mApplicationCache) { + mApplicationCache = aAppCache; + } + + // We successfully opened an offline cache session and the entry, + // so indicate we will load from the offline cache. + StoreLoadedFromApplicationCache(true); + StoreCacheEntryIsReadOnly(true); + mCacheEntry = aEntry; + StoreCacheEntryIsWriteOnly(false); + + if (mLoadFlags & LOAD_INITIAL_DOCUMENT_URI && !mApplicationCacheForWrite) { + MaybeWarnAboutAppCache(); + } + + return NS_OK; + } + + if (!mApplicationCacheForWrite && !LoadFallbackChannel()) { + if (!mApplicationCache) { + mApplicationCache = aAppCache; + } + + // Check for namespace match. + nsCOMPtr namespaceEntry; + rv = mApplicationCache->GetMatchingNamespace( + mSpec, getter_AddRefs(namespaceEntry)); + NS_ENSURE_SUCCESS(rv, rv); + + uint32_t namespaceType = 0; + if (!namespaceEntry || + NS_FAILED(namespaceEntry->GetItemType(&namespaceType)) || + (namespaceType & (nsIApplicationCacheNamespace::NAMESPACE_FALLBACK | + nsIApplicationCacheNamespace::NAMESPACE_BYPASS)) == + 0) { + // When loading from an application cache, only items + // on the whitelist or matching a + // fallback namespace should hit the network... + mLoadFlags |= LOAD_ONLY_FROM_CACHE; + + // ... and if there were an application cache entry, + // we would have found it earlier. + return NS_ERROR_CACHE_KEY_NOT_FOUND; + } + + if (namespaceType & nsIApplicationCacheNamespace::NAMESPACE_FALLBACK) { + nsAutoCString namespaceSpec; + rv = namespaceEntry->GetNamespaceSpec(namespaceSpec); + NS_ENSURE_SUCCESS(rv, rv); + + // This prevents fallback attacks injected by an insecure subdirectory + // for the whole origin (or a parent directory). + if (!IsInSubpathOfAppCacheManifest(mApplicationCache, namespaceSpec)) { + return NS_OK; + } + + rv = namespaceEntry->GetData(mFallbackKey); + NS_ENSURE_SUCCESS(rv, rv); + } + + if (namespaceType & nsIApplicationCacheNamespace::NAMESPACE_BYPASS) { + LOG( + ("nsHttpChannel::OnOfflineCacheEntryAvailable this=%p, URL matches " + "NETWORK," + " looking for a regular cache entry", + this)); + + bool isHttps = mURI->SchemeIs("https"); + rv = OpenCacheEntryInternal(isHttps, nullptr, + false /* don't allow appcache lookups */); + if (NS_FAILED(rv)) { + // Don't let this fail when cache entry can't be synchronously open. + // We want to go forward even without a regular cache entry. + return NS_OK; + } + } + } + + return NS_OK; +} + +nsresult nsHttpChannel::OnOfflineCacheEntryForWritingAvailable( + nsICacheEntry* aEntry, nsIApplicationCache* aAppCache, + nsresult aEntryStatus) { + MOZ_ASSERT(mApplicationCacheForWrite && + aAppCache == mApplicationCacheForWrite); + + StoreCacheEntriesToWaitFor(LoadCacheEntriesToWaitFor() & + ~WAIT_FOR_OFFLINE_CACHE_ENTRY); + + if (NS_SUCCEEDED(aEntryStatus)) { + mOfflineCacheEntry = aEntry; + if (NS_FAILED(aEntry->GetLastModified(&mOfflineCacheLastModifiedTime))) { + mOfflineCacheLastModifiedTime = 0; + } + } + + return aEntryStatus; +} + +// Generates the proper cache-key for this instance of nsHttpChannel +nsresult nsHttpChannel::GenerateCacheKey(uint32_t postID, + nsACString& cacheKey) { + AssembleCacheKey(LoadFallbackChannel() ? mFallbackKey.get() : mSpec.get(), + postID, cacheKey); + return NS_OK; +} + +// Assembles a cache-key from the given pieces of information and |mLoadFlags| +void nsHttpChannel::AssembleCacheKey(const char* spec, uint32_t postID, + nsACString& cacheKey) { + cacheKey.Truncate(); + + if (mLoadFlags & LOAD_ANONYMOUS) { + cacheKey.AssignLiteral("anon&"); + } + + if (postID) { + char buf[32]; + SprintfLiteral(buf, "id=%x&", postID); + cacheKey.Append(buf); + } + + if (!cacheKey.IsEmpty()) { + cacheKey.AppendLiteral("uri="); + } + + // Strip any trailing #ref from the URL before using it as the key + const char* p = strchr(spec, '#'); + if (p) + cacheKey.Append(spec, p - spec); + else + cacheKey.Append(spec); +} + +nsresult DoUpdateExpirationTime(nsHttpChannel* aSelf, + nsICacheEntry* aCacheEntry, + nsHttpResponseHead* aResponseHead, + uint32_t& aExpirationTime) { + MOZ_ASSERT(aExpirationTime == 0); + NS_ENSURE_TRUE(aResponseHead, NS_ERROR_FAILURE); + + nsresult rv; + + if (!aResponseHead->MustValidate()) { + // For stale-while-revalidate we use expiration time as the absolute base + // for calculation of the stale window absolute end time. Hence, when the + // entry may be served w/o revalidation, we need a non-zero value for the + // expiration time. Let's set it to |now|, which basicly means "expired", + // same as when set to 0. + uint32_t now = NowInSeconds(); + aExpirationTime = now; + + uint32_t freshnessLifetime = 0; + + rv = aResponseHead->ComputeFreshnessLifetime(&freshnessLifetime); + if (NS_FAILED(rv)) return rv; + + if (freshnessLifetime > 0) { + uint32_t currentAge = 0; + + rv = aResponseHead->ComputeCurrentAge(now, aSelf->GetRequestTime(), + ¤tAge); + if (NS_FAILED(rv)) return rv; + + LOG(("freshnessLifetime = %u, currentAge = %u\n", freshnessLifetime, + currentAge)); + + if (freshnessLifetime > currentAge) { + uint32_t timeRemaining = freshnessLifetime - currentAge; + // be careful... now + timeRemaining may overflow + if (now + timeRemaining < now) { + aExpirationTime = uint32_t(-1); + } else { + aExpirationTime = now + timeRemaining; + } + } + } + } + + rv = aCacheEntry->SetExpirationTime(aExpirationTime); + NS_ENSURE_SUCCESS(rv, rv); + + return rv; +} + +// UpdateExpirationTime is called when a new response comes in from the server. +// It updates the stored response-time and sets the expiration time on the +// cache entry. +// +// From section 13.2.4 of RFC2616, we compute expiration time as follows: +// +// timeRemaining = freshnessLifetime - currentAge +// expirationTime = now + timeRemaining +// +nsresult nsHttpChannel::UpdateExpirationTime() { + uint32_t expirationTime = 0; + nsresult rv = DoUpdateExpirationTime(this, mCacheEntry, mResponseHead.get(), + expirationTime); + NS_ENSURE_SUCCESS(rv, rv); + + if (mOfflineCacheEntry) { + rv = mOfflineCacheEntry->SetExpirationTime(expirationTime); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; +} + +bool nsHttpChannel::ShouldUpdateOfflineCacheEntry() { + if (!mApplicationCacheForWrite || !mOfflineCacheEntry) { + return false; + } + + // if we're updating the cache entry, update the offline cache entry too + if (mCacheEntry && LoadCacheEntryIsWriteOnly()) { + return true; + } + + // if there's nothing in the offline cache, add it + if (mOfflineCacheEntry) { + return true; + } + + // if the document is newer than the offline entry, update it + uint32_t docLastModifiedTime; + nsresult rv = mResponseHead->GetLastModifiedValue(&docLastModifiedTime); + if (NS_FAILED(rv)) { + return true; + } + + if (mOfflineCacheLastModifiedTime == 0) { + return false; + } + + if (docLastModifiedTime > mOfflineCacheLastModifiedTime) { + return true; + } + + return false; +} + +nsresult nsHttpChannel::OpenCacheInputStream(nsICacheEntry* cacheEntry, + bool startBuffering, + bool checkingAppCacheEntry) { + nsresult rv; + + if (mURI->SchemeIs("https")) { + rv = cacheEntry->GetSecurityInfo(getter_AddRefs(mCachedSecurityInfo)); + if (NS_FAILED(rv)) { + LOG(("failed to parse security-info [channel=%p, entry=%p]", this, + cacheEntry)); + NS_WARNING("failed to parse security-info"); + cacheEntry->AsyncDoom(nullptr); + return rv; + } + + // XXX: We should not be skilling this check in the offline cache + // case, but we have to do so now to work around bug 794507. + bool mustHaveSecurityInfo = + !LoadLoadedFromApplicationCache() && !checkingAppCacheEntry; + MOZ_ASSERT(mCachedSecurityInfo || !mustHaveSecurityInfo); + if (!mCachedSecurityInfo && mustHaveSecurityInfo) { + LOG( + ("mCacheEntry->GetSecurityInfo returned success but did not " + "return the security info [channel=%p, entry=%p]", + this, cacheEntry)); + cacheEntry->AsyncDoom(nullptr); + return NS_ERROR_UNEXPECTED; // XXX error code + } + } + + // Keep the conditions below in sync with the conditions in ReadFromCache. + + rv = NS_OK; + + if (WillRedirect(*mCachedResponseHead)) { + // Do not even try to read the entity for a redirect because we do not + // return an entity to the application when we process redirects. + LOG(("Will skip read of cached redirect entity\n")); + return NS_OK; + } + + if ((mLoadFlags & nsICachingChannel::LOAD_ONLY_IF_MODIFIED) && + !LoadCachedContentIsPartial()) { + // For LOAD_ONLY_IF_MODIFIED, we usually don't have to deal with the + // cached entity. + if (!mApplicationCacheForWrite) { + LOG( + ("Will skip read from cache based on LOAD_ONLY_IF_MODIFIED " + "load flag\n")); + return NS_OK; + } + + // If offline caching has been requested and the offline cache needs + // updating, we must complete the call even if the main cache entry + // is up to date. We don't know yet for sure whether the offline + // cache needs updating because at this point we haven't opened it + // for writing yet, so we have to start reading the cached entity now + // just in case. + LOG( + ("May skip read from cache based on LOAD_ONLY_IF_MODIFIED " + "load flag\n")); + } + + // Open an input stream for the entity, so that the call to OpenInputStream + // happens off the main thread. + nsCOMPtr stream; + + // If an alternate representation was requested, try to open the alt + // input stream. + // If the entry has a "is-from-child" metadata, then only open the altdata + // stream if the consumer is also from child. + bool altDataFromChild = false; + { + nsCString value; + rv = cacheEntry->GetMetaDataElement("alt-data-from-child", + getter_Copies(value)); + altDataFromChild = !value.IsEmpty(); + } + + nsAutoCString altDataType; + Unused << cacheEntry->GetAltDataType(altDataType); + + nsAutoCString contentType; + mCachedResponseHead->ContentType(contentType); + + bool foundAltData = false; + bool deliverAltData = true; + if (!LoadDisableAltDataCache() && !altDataType.IsEmpty() && + !mPreferredCachedAltDataTypes.IsEmpty() && + altDataFromChild == LoadAltDataForChild()) { + for (auto& pref : mPreferredCachedAltDataTypes) { + if (pref.type() == altDataType && + (pref.contentType().IsEmpty() || pref.contentType() == contentType)) { + foundAltData = true; + deliverAltData = pref.deliverAltData(); + break; + } + } + } + + nsCOMPtr altData; + int64_t altDataSize = -1; + if (foundAltData) { + rv = cacheEntry->OpenAlternativeInputStream(altDataType, + getter_AddRefs(altData)); + if (NS_SUCCEEDED(rv)) { + // We have succeeded. + mAvailableCachedAltDataType = altDataType; + StoreDeliveringAltData(deliverAltData); + + // Set the correct data size on the channel. + Unused << cacheEntry->GetAltDataSize(&altDataSize); + mAltDataLength = altDataSize; + + LOG(("Opened alt-data input stream [type=%s, size=%" PRId64 + ", deliverAltData=%d]", + altDataType.get(), mAltDataLength, deliverAltData)); + + if (deliverAltData) { + stream = altData; + } + } + } + + if (!stream) { + rv = cacheEntry->OpenInputStream(0, getter_AddRefs(stream)); + } + + if (NS_FAILED(rv)) { + LOG( + ("Failed to open cache input stream [channel=%p, " + "mCacheEntry=%p]", + this, cacheEntry)); + return rv; + } + + if (startBuffering) { + bool nonBlocking; + rv = stream->IsNonBlocking(&nonBlocking); + if (NS_SUCCEEDED(rv) && nonBlocking) startBuffering = false; + } + + if (!startBuffering) { + // Bypass wrapping the input stream for the new cache back-end since + // nsIStreamTransportService expects a blocking stream. Preloading of + // the data must be done on the level of the cache backend, internally. + // + // We do not connect the stream to the stream transport service if we + // have to validate the entry with the server. If we did, we would get + // into a race condition between the stream transport service reading + // the existing contents and the opening of the cache entry's output + // stream to write the new contents in the case where we get a non-304 + // response. + LOG( + ("Opened cache input stream without buffering [channel=%p, " + "mCacheEntry=%p, stream=%p]", + this, cacheEntry, stream.get())); + mCacheInputStream.takeOver(stream); + return rv; + } + + // Have the stream transport service start reading the entity on one of its + // background threads. + + nsCOMPtr transport; + nsCOMPtr wrapper; + + nsCOMPtr sts( + services::GetStreamTransportService()); + rv = sts ? NS_OK : NS_ERROR_NOT_AVAILABLE; + if (NS_SUCCEEDED(rv)) { + rv = sts->CreateInputTransport(stream, true, getter_AddRefs(transport)); + } + if (NS_SUCCEEDED(rv)) { + rv = transport->OpenInputStream(0, 0, 0, getter_AddRefs(wrapper)); + } + if (NS_SUCCEEDED(rv)) { + LOG( + ("Opened cache input stream [channel=%p, wrapper=%p, " + "transport=%p, stream=%p]", + this, wrapper.get(), transport.get(), stream.get())); + } else { + LOG( + ("Failed to open cache input stream [channel=%p, " + "wrapper=%p, transport=%p, stream=%p]", + this, wrapper.get(), transport.get(), stream.get())); + + stream->Close(); + return rv; + } + + mCacheInputStream.takeOver(wrapper); + + return NS_OK; +} + +// Actually process the cached response that we started to handle in CheckCache +// and/or StartBufferingCachedEntity. +nsresult nsHttpChannel::ReadFromCache(bool alreadyMarkedValid) { + NS_ENSURE_TRUE(mCacheEntry, NS_ERROR_FAILURE); + NS_ENSURE_TRUE(mCachedContentIsValid, NS_ERROR_FAILURE); + NS_ENSURE_TRUE(!mCachePump, NS_OK); // already opened + + LOG( + ("nsHttpChannel::ReadFromCache [this=%p] " + "Using cached copy of: %s\n", + this, mSpec.get())); + + // When racing the cache with the network with a timer, and we get data from + // the cache, we should prevent the timer from triggering a network request. + if (mNetworkTriggerTimer) { + mNetworkTriggerTimer->Cancel(); + mNetworkTriggerTimer = nullptr; + } + + if (mRaceCacheWithNetwork) { + MOZ_ASSERT(mFirstResponseSource != RESPONSE_FROM_CACHE); + if (mFirstResponseSource == RESPONSE_PENDING) { + LOG(("First response from cache\n")); + mFirstResponseSource = RESPONSE_FROM_CACHE; + + // Cancel the transaction because we will serve the request from the cache + CancelNetworkRequest(NS_BINDING_ABORTED); + if (mTransactionPump && mSuspendCount) { + uint32_t suspendCount = mSuspendCount; + while (suspendCount--) { + mTransactionPump->Resume(); + } + } + mTransaction = nullptr; + mTransactionPump = nullptr; + } else { + MOZ_ASSERT(mFirstResponseSource == RESPONSE_FROM_NETWORK); + LOG( + ("Skipping read from cache because first response was from " + "network\n")); + + if (!mOnCacheEntryCheckTimestamp.IsNull()) { + TimeStamp currentTime = TimeStamp::Now(); + int64_t savedTime = + (currentTime - mOnStartRequestTimestamp).ToMilliseconds(); + Telemetry::Accumulate( + Telemetry::NETWORK_RACE_CACHE_WITH_NETWORK_SAVED_TIME, savedTime); + + int64_t diffTime = + (currentTime - mOnCacheEntryCheckTimestamp).ToMilliseconds(); + Telemetry::Accumulate( + Telemetry::NETWORK_RACE_CACHE_WITH_NETWORK_OCEC_ON_START_DIFF, + diffTime); + } + return NS_OK; + } + } + + if (mCachedResponseHead) mResponseHead = std::move(mCachedResponseHead); + + UpdateInhibitPersistentCachingFlag(); + + // if we don't already have security info, try to get it from the cache + // entry. there are two cases to consider here: 1) we are just reading + // from the cache, or 2) this may be due to a 304 not modified response, + // in which case we could have security info from a socket transport. + if (!mSecurityInfo) mSecurityInfo = mCachedSecurityInfo; + + if (!alreadyMarkedValid && !LoadCachedContentIsPartial()) { + // We validated the entry, and we have write access to the cache, so + // mark the cache entry as valid in order to allow others access to + // this cache entry. + // + // TODO: This should be done asynchronously so we don't take the cache + // service lock on the main thread. + mCacheEntry->MaybeMarkValid(); + } + + nsresult rv; + + // Keep the conditions below in sync with the conditions in + // StartBufferingCachedEntity. + + if (WillRedirect(*mResponseHead)) { + // TODO: Bug 759040 - We should call HandleAsyncRedirect directly here, + // to avoid event dispatching latency. + MOZ_ASSERT(!mCacheInputStream); + LOG(("Skipping skip read of cached redirect entity\n")); + return AsyncCall(&nsHttpChannel::HandleAsyncRedirect); + } + + if ((mLoadFlags & LOAD_ONLY_IF_MODIFIED) && !LoadCachedContentIsPartial()) { + if (!mApplicationCacheForWrite) { + LOG( + ("Skipping read from cache based on LOAD_ONLY_IF_MODIFIED " + "load flag\n")); + MOZ_ASSERT(!mCacheInputStream); + // TODO: Bug 759040 - We should call HandleAsyncNotModified directly + // here, to avoid event dispatching latency. + return AsyncCall(&nsHttpChannel::HandleAsyncNotModified); + } + + if (!ShouldUpdateOfflineCacheEntry()) { + LOG( + ("Skipping read from cache based on LOAD_ONLY_IF_MODIFIED " + "load flag (mApplicationCacheForWrite not null case)\n")); + mCacheInputStream.CloseAndRelease(); + // TODO: Bug 759040 - We should call HandleAsyncNotModified directly + // here, to avoid event dispatching latency. + return AsyncCall(&nsHttpChannel::HandleAsyncNotModified); + } + } + + MOZ_ASSERT(mCacheInputStream); + if (!mCacheInputStream) { + NS_ERROR( + "mCacheInputStream is null but we're expecting to " + "be able to read from it."); + return NS_ERROR_UNEXPECTED; + } + + nsCOMPtr inputStream = mCacheInputStream.forget(); + + rv = nsInputStreamPump::Create(getter_AddRefs(mCachePump), inputStream, 0, 0, + true); + if (NS_FAILED(rv)) { + inputStream->Close(); + return rv; + } + + rv = mCachePump->AsyncRead(this); + if (NS_FAILED(rv)) return rv; + + if (LoadTimingEnabled()) mCacheReadStart = TimeStamp::Now(); + + uint32_t suspendCount = mSuspendCount; + if (LoadAsyncResumePending()) { + LOG( + (" Suspend()'ing cache pump once because of async resume pending" + ", sc=%u, pump=%p, this=%p", + suspendCount, mCachePump.get(), this)); + ++suspendCount; + } + while (suspendCount--) { + mCachePump->Suspend(); + } + + return NS_OK; +} + +void nsHttpChannel::CloseCacheEntry(bool doomOnFailure) { + mCacheInputStream.CloseAndRelease(); + + if (!mCacheEntry) return; + + LOG(("nsHttpChannel::CloseCacheEntry [this=%p] mStatus=%" PRIx32 + " CacheEntryIsWriteOnly=%x", + this, static_cast(static_cast(mStatus)), + LoadCacheEntryIsWriteOnly())); + + // If we have begun to create or replace a cache entry, and that cache + // entry is not complete and not resumable, then it needs to be doomed. + // Otherwise, CheckCache will make the mistake of thinking that the + // partial cache entry is complete. + + bool doom = false; + if (LoadInitedCacheEntry()) { + MOZ_ASSERT(mResponseHead, "oops"); + if (NS_FAILED(mStatus) && doomOnFailure && LoadCacheEntryIsWriteOnly() && + !mResponseHead->IsResumable()) + doom = true; + } else if (LoadCacheEntryIsWriteOnly()) + doom = true; + + if (doom) { + LOG((" dooming cache entry!!")); + mCacheEntry->AsyncDoom(nullptr); + } else { + // Store updated security info, makes cached EV status race less likely + // (see bug 1040086) + if (mSecurityInfo) mCacheEntry->SetSecurityInfo(mSecurityInfo); + } + + mCachedResponseHead = nullptr; + + mCachePump = nullptr; + // This releases the entry for other consumers to use. + // We call Dismiss() in case someone still keeps a reference + // to this entry handle. + mCacheEntry->Dismiss(); + mCacheEntry = nullptr; + StoreCacheEntryIsWriteOnly(false); + StoreInitedCacheEntry(false); +} + +void nsHttpChannel::CloseOfflineCacheEntry() { + if (!mOfflineCacheEntry) return; + + LOG(("nsHttpChannel::CloseOfflineCacheEntry [this=%p]", this)); + + if (NS_FAILED(mStatus)) { + mOfflineCacheEntry->AsyncDoom(nullptr); + } else { + bool succeeded; + if (NS_SUCCEEDED(GetRequestSucceeded(&succeeded)) && !succeeded) + mOfflineCacheEntry->AsyncDoom(nullptr); + } + + mOfflineCacheEntry = nullptr; +} + +void nsHttpChannel::MaybeCreateCacheEntryWhenRCWN() { + mozilla::MutexAutoLock lock(mRCWNLock); + + // Create cache entry for writing only when we're racing cache with network + // and we don't have the entry because network won. + if (mCacheEntry || !mRaceCacheWithNetwork || + mFirstResponseSource != RESPONSE_FROM_NETWORK || + LoadCacheEntryIsReadOnly()) { + return; + } + + LOG(("nsHttpChannel::MaybeCreateCacheEntryWhenRCWN [this=%p]", this)); + + nsCOMPtr cacheStorageService( + services::GetCacheStorageService()); + if (!cacheStorageService) { + return; + } + + nsCOMPtr cacheStorage; + RefPtr info = GetLoadContextInfo(this); + Unused << cacheStorageService->DiskCacheStorage(info, false, + getter_AddRefs(cacheStorage)); + if (!cacheStorage) { + return; + } + + Unused << cacheStorage->OpenTruncate(mCacheEntryURI, mCacheIdExtension, + getter_AddRefs(mCacheEntry)); + + LOG((" created entry %p", mCacheEntry.get())); + + if (AwaitingCacheCallbacks()) { + // Setting mIgnoreCacheEntry to true ensures that we won't close this + // write-only entry in OnCacheEntryAvailable() if this method was called + // after OnCacheEntryCheck(). + mIgnoreCacheEntry = true; + } + + mAvailableCachedAltDataType.Truncate(); + StoreDeliveringAltData(false); + mAltDataLength = -1; + mCacheInputStream.CloseAndRelease(); + mCachedContentIsValid = false; +} + +// Initialize the cache entry for writing. +// - finalize storage policy +// - store security info +// - update expiration time +// - store headers and other meta data +nsresult nsHttpChannel::InitCacheEntry() { + nsresult rv; + + NS_ENSURE_TRUE(mCacheEntry, NS_ERROR_UNEXPECTED); + // if only reading, nothing to be done here. + if (LoadCacheEntryIsReadOnly()) return NS_OK; + + // Don't cache the response again if already cached... + if (mCachedContentIsValid) return NS_OK; + + LOG(("nsHttpChannel::InitCacheEntry [this=%p entry=%p]\n", this, + mCacheEntry.get())); + + bool recreate = !LoadCacheEntryIsWriteOnly(); + bool dontPersist = mLoadFlags & INHIBIT_PERSISTENT_CACHING; + + if (!recreate && dontPersist) { + // If the current entry is persistent but we inhibit peristence + // then force recreation of the entry as memory/only. + rv = mCacheEntry->GetPersistent(&recreate); + if (NS_FAILED(rv)) return rv; + } + + if (recreate) { + LOG( + (" we have a ready entry, but reading it again from the server -> " + "recreating cache entry\n")); + // clean the altData cache and reset this to avoid wrong content length + mAvailableCachedAltDataType.Truncate(); + StoreDeliveringAltData(false); + + nsCOMPtr currentEntry; + currentEntry.swap(mCacheEntry); + rv = currentEntry->Recreate(dontPersist, getter_AddRefs(mCacheEntry)); + if (NS_FAILED(rv)) { + LOG((" recreation failed, the response will not be cached")); + return NS_OK; + } + + StoreCacheEntryIsWriteOnly(true); + } + + // Set the expiration time for this cache entry + rv = UpdateExpirationTime(); + if (NS_FAILED(rv)) return rv; + + // mark this weakly framed until a response body is seen + mCacheEntry->SetMetaDataElement("strongly-framed", "0"); + + rv = AddCacheEntryHeaders(mCacheEntry); + if (NS_FAILED(rv)) return rv; + + StoreInitedCacheEntry(true); + + // Don't perform the check when writing (doesn't make sense) + StoreConcurrentCacheAccess(0); + + return NS_OK; +} + +void nsHttpChannel::UpdateInhibitPersistentCachingFlag() { + // The no-store directive within the 'Cache-Control:' header indicates + // that we must not store the response in a persistent cache. + if (mResponseHead->NoStore()) mLoadFlags |= INHIBIT_PERSISTENT_CACHING; + + // Only cache SSL content on disk if the pref is set + if (!gHttpHandler->IsPersistentHttpsCachingEnabled() && + mURI->SchemeIs("https")) { + mLoadFlags |= INHIBIT_PERSISTENT_CACHING; + } +} + +nsresult nsHttpChannel::InitOfflineCacheEntry() { + // This function can be called even when we fail to connect (bug 551990) + + if (!mOfflineCacheEntry) { + return NS_OK; + } + + if (!mResponseHead || mResponseHead->NoStore()) { + if (mResponseHead && mResponseHead->NoStore()) { + mOfflineCacheEntry->AsyncDoom(nullptr); + } + + CloseOfflineCacheEntry(); + + if (mResponseHead && mResponseHead->NoStore()) { + return NS_ERROR_NOT_AVAILABLE; + } + + return NS_OK; + } + + // This entry's expiration time should match the main entry's expiration + // time. UpdateExpirationTime() will keep it in sync once the offline + // cache entry has been created. + if (mCacheEntry) { + uint32_t expirationTime; + nsresult rv = mCacheEntry->GetExpirationTime(&expirationTime); + NS_ENSURE_SUCCESS(rv, rv); + + mOfflineCacheEntry->SetExpirationTime(expirationTime); + } + + return AddCacheEntryHeaders(mOfflineCacheEntry); +} + +nsresult DoAddCacheEntryHeaders(nsHttpChannel* self, nsICacheEntry* entry, + nsHttpRequestHead* requestHead, + nsHttpResponseHead* responseHead, + nsISupports* securityInfo) { + nsresult rv; + + LOG(("nsHttpChannel::AddCacheEntryHeaders [this=%p] begin", self)); + // Store secure data in memory only + if (securityInfo) entry->SetSecurityInfo(securityInfo); + + // Store the HTTP request method with the cache entry so we can distinguish + // for example GET and HEAD responses. + nsAutoCString method; + requestHead->Method(method); + rv = entry->SetMetaDataElement("request-method", method.get()); + if (NS_FAILED(rv)) return rv; + + // Store the HTTP authorization scheme used if any... + rv = StoreAuthorizationMetaData(entry, requestHead); + if (NS_FAILED(rv)) return rv; + + // Iterate over the headers listed in the Vary response header, and + // store the value of the corresponding request header so we can verify + // that it has not varied when we try to re-use the cached response at + // a later time. Take care to store "Cookie" headers only as hashes + // due to security considerations and the fact that they can be pretty + // large (bug 468426). We take care of "Vary: cookie" in ResponseWouldVary. + // + // NOTE: if "Vary: accept, cookie", then we will store the "accept" header + // in the cache. we could try to avoid needlessly storing the "accept" + // header in this case, but it doesn't seem worth the extra code to perform + // the check. + { + nsAutoCString buf, metaKey; + Unused << responseHead->GetHeader(nsHttp::Vary, buf); + + constexpr auto prefix = "request-"_ns; + + for (const nsACString& token : + nsCCharSeparatedTokenizer(buf, NS_HTTP_HEADER_SEP).ToRange()) { + LOG( + ("nsHttpChannel::AddCacheEntryHeaders [this=%p] " + "processing %s", + self, nsPromiseFlatCString(token).get())); + if (!token.EqualsLiteral("*")) { + nsHttpAtom atom = nsHttp::ResolveAtom(token); + nsAutoCString val; + nsAutoCString hash; + if (NS_SUCCEEDED(requestHead->GetHeader(atom, val))) { + // If cookie-header, store a hash of the value + if (atom == nsHttp::Cookie) { + LOG( + ("nsHttpChannel::AddCacheEntryHeaders [this=%p] " + "cookie-value %s", + self, val.get())); + rv = Hash(val.get(), hash); + // If hash failed, store a string not very likely + // to be the result of subsequent hashes + if (NS_FAILED(rv)) { + val = ""_ns; + } else { + val = hash; + } + + LOG((" hashed to %s\n", val.get())); + } + + // build cache meta data key and set meta data element... + metaKey = prefix + token; + entry->SetMetaDataElement(metaKey.get(), val.get()); + } else { + LOG( + ("nsHttpChannel::AddCacheEntryHeaders [this=%p] " + "clearing metadata for %s", + self, nsPromiseFlatCString(token).get())); + metaKey = prefix + token; + entry->SetMetaDataElement(metaKey.get(), nullptr); + } + } + } + } + + // Store the received HTTP head with the cache entry as an element of + // the meta data. + nsAutoCString head; + responseHead->Flatten(head, true); + rv = entry->SetMetaDataElement("response-head", head.get()); + if (NS_FAILED(rv)) return rv; + head.Truncate(); + responseHead->FlattenNetworkOriginalHeaders(head); + rv = entry->SetMetaDataElement("original-response-headers", head.get()); + if (NS_FAILED(rv)) return rv; + + // Indicate we have successfully finished setting metadata on the cache entry. + rv = entry->MetaDataReady(); + + return rv; +} + +nsresult nsHttpChannel::AddCacheEntryHeaders(nsICacheEntry* entry) { + return DoAddCacheEntryHeaders(this, entry, &mRequestHead, mResponseHead.get(), + mSecurityInfo); +} + +inline void GetAuthType(const char* challenge, nsCString& authType) { + const char* p; + + // get the challenge type + if ((p = strchr(challenge, ' ')) != nullptr) + authType.Assign(challenge, p - challenge); + else + authType.Assign(challenge); +} + +nsresult StoreAuthorizationMetaData(nsICacheEntry* entry, + nsHttpRequestHead* requestHead) { + // Not applicable to proxy authorization... + nsAutoCString val; + if (NS_FAILED(requestHead->GetHeader(nsHttp::Authorization, val))) { + return NS_OK; + } + + // eg. [Basic realm="wally world"] + nsAutoCString buf; + GetAuthType(val.get(), buf); + return entry->SetMetaDataElement("auth", buf.get()); +} + +// Finalize the cache entry +// - may need to rewrite response headers if any headers changed +// - may need to recalculate the expiration time if any headers changed +// - called only for freshly written cache entries +nsresult nsHttpChannel::FinalizeCacheEntry() { + LOG(("nsHttpChannel::FinalizeCacheEntry [this=%p]\n", this)); + + // Don't update this meta-data on 304 + if (LoadStronglyFramed() && !mCachedContentIsValid && mCacheEntry) { + LOG(("nsHttpChannel::FinalizeCacheEntry [this=%p] Is Strongly Framed\n", + this)); + mCacheEntry->SetMetaDataElement("strongly-framed", "1"); + } + + if (mResponseHead && LoadResponseHeadersModified()) { + // Set the expiration time for this cache entry + nsresult rv = UpdateExpirationTime(); + if (NS_FAILED(rv)) return rv; + } + return NS_OK; +} + +// Open an output stream to the cache entry and insert a listener tee into +// the chain of response listeners. +nsresult nsHttpChannel::InstallCacheListener(int64_t offset) { + nsresult rv; + + LOG(("Preparing to write data into the cache [uri=%s]\n", mSpec.get())); + + MOZ_ASSERT(mCacheEntry); + MOZ_ASSERT(LoadCacheEntryIsWriteOnly() || LoadCachedContentIsPartial() || + mRaceCacheWithNetwork); + MOZ_ASSERT(mListener); + + nsAutoCString contentEncoding, contentType; + Unused << mResponseHead->GetHeader(nsHttp::Content_Encoding, contentEncoding); + mResponseHead->ContentType(contentType); + // If the content is compressible and the server has not compressed it, + // mark the cache entry for compression. + if (contentEncoding.IsEmpty() && + (contentType.EqualsLiteral(TEXT_HTML) || + contentType.EqualsLiteral(TEXT_PLAIN) || + contentType.EqualsLiteral(TEXT_CSS) || + contentType.EqualsLiteral(TEXT_JAVASCRIPT) || + contentType.EqualsLiteral(TEXT_ECMASCRIPT) || + contentType.EqualsLiteral(TEXT_XML) || + contentType.EqualsLiteral(APPLICATION_JAVASCRIPT) || + contentType.EqualsLiteral(APPLICATION_ECMASCRIPT) || + contentType.EqualsLiteral(APPLICATION_XJAVASCRIPT) || + contentType.EqualsLiteral(APPLICATION_XHTML_XML))) { + rv = mCacheEntry->SetMetaDataElement("uncompressed-len", "0"); + if (NS_FAILED(rv)) { + LOG(("unable to mark cache entry for compression")); + } + } + + LOG(("Trading cache input stream for output stream [channel=%p]", this)); + + // We must close the input stream first because cache entries do not + // correctly handle having an output stream and input streams open at + // the same time. + mCacheInputStream.CloseAndRelease(); + + int64_t predictedSize = mResponseHead->TotalEntitySize(); + if (predictedSize != -1) { + predictedSize -= offset; + } + + nsCOMPtr out; + rv = + mCacheEntry->OpenOutputStream(offset, predictedSize, getter_AddRefs(out)); + if (rv == NS_ERROR_NOT_AVAILABLE) { + LOG((" entry doomed, not writing it [channel=%p]", this)); + // Entry is already doomed. + // This may happen when expiration time is set to past and the entry + // has been removed by the background eviction logic. + return NS_OK; + } + if (rv == NS_ERROR_FILE_TOO_BIG) { + LOG((" entry would exceed max allowed size, not writing it [channel=%p]", + this)); + mCacheEntry->AsyncDoom(nullptr); + return NS_OK; + } + if (NS_FAILED(rv)) return rv; + + if (LoadCacheOnlyMetadata()) { + LOG(("Not storing content, cacheOnlyMetadata set")); + // We must open and then close the output stream of the cache entry. + // This way we indicate the content has been written (despite with zero + // length) and the entry is now in the ready state with "having data". + + out->Close(); + return NS_OK; + } + + // XXX disk cache does not support overlapped i/o yet +#if 0 + // Mark entry valid inorder to allow simultaneous reading... + rv = mCacheEntry->MarkValid(); + if (NS_FAILED(rv)) return rv; +#endif + + nsCOMPtr tee = + do_CreateInstance(kStreamListenerTeeCID, &rv); + if (NS_FAILED(rv)) return rv; + + LOG(("nsHttpChannel::InstallCacheListener sync tee %p rv=%" PRIx32, tee.get(), + static_cast(rv))); + rv = tee->Init(mListener, out, nullptr); + if (NS_FAILED(rv)) return rv; + + mListener = tee; + return NS_OK; +} + +nsresult nsHttpChannel::InstallOfflineCacheListener(int64_t offset) { + nsresult rv; + + LOG(("Preparing to write data into the offline cache [uri=%s]\n", + mSpec.get())); + + MOZ_ASSERT(mOfflineCacheEntry); + MOZ_ASSERT(mListener); + + nsCOMPtr out; + rv = mOfflineCacheEntry->OpenOutputStream(offset, -1, getter_AddRefs(out)); + if (NS_FAILED(rv)) return rv; + + nsCOMPtr tee = + do_CreateInstance(kStreamListenerTeeCID, &rv); + if (NS_FAILED(rv)) return rv; + + rv = tee->Init(mListener, out, nullptr); + if (NS_FAILED(rv)) return rv; + + mListener = tee; + + return NS_OK; +} + +void nsHttpChannel::ClearBogusContentEncodingIfNeeded() { + // For .gz files, apache sends both a Content-Type: application/x-gzip + // as well as Content-Encoding: gzip, which is completely wrong. In + // this case, we choose to ignore the rogue Content-Encoding header. We + // must do this early on so as to prevent it from being seen up stream. + // The same problem exists for Content-Encoding: compress in default + // Apache installs. + nsAutoCString contentType; + mResponseHead->ContentType(contentType); + if (mResponseHead->HasHeaderValue(nsHttp::Content_Encoding, "gzip") && + (contentType.EqualsLiteral(APPLICATION_GZIP) || + contentType.EqualsLiteral(APPLICATION_GZIP2) || + contentType.EqualsLiteral(APPLICATION_GZIP3))) { + // clear the Content-Encoding header + mResponseHead->ClearHeader(nsHttp::Content_Encoding); + } else if (mResponseHead->HasHeaderValue(nsHttp::Content_Encoding, + "compress") && + (contentType.EqualsLiteral(APPLICATION_COMPRESS) || + contentType.EqualsLiteral(APPLICATION_COMPRESS2))) { + // clear the Content-Encoding header + mResponseHead->ClearHeader(nsHttp::Content_Encoding); + } +} + +//----------------------------------------------------------------------------- +// nsHttpChannel +//----------------------------------------------------------------------------- + +nsresult nsHttpChannel::SetupReplacementChannel(nsIURI* newURI, + nsIChannel* newChannel, + bool preserveMethod, + uint32_t redirectFlags) { + LOG( + ("nsHttpChannel::SetupReplacementChannel " + "[this=%p newChannel=%p preserveMethod=%d]", + this, newChannel, preserveMethod)); + + nsresult rv = HttpBaseChannel::SetupReplacementChannel( + newURI, newChannel, preserveMethod, redirectFlags); + if (NS_FAILED(rv)) return rv; + + rv = CheckRedirectLimit(redirectFlags); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr httpChannel = do_QueryInterface(newChannel); + if (!httpChannel) return NS_OK; // no other options to set + + // convey the ApplyConversion flag (bug 91862) + nsCOMPtr encodedChannel = do_QueryInterface(httpChannel); + if (encodedChannel) encodedChannel->SetApplyConversion(LoadApplyConversion()); + + // transfer the resume information + if (LoadResuming()) { + nsCOMPtr resumableChannel( + do_QueryInterface(newChannel)); + if (!resumableChannel) { + NS_WARNING( + "Got asked to resume, but redirected to non-resumable channel!"); + return NS_ERROR_NOT_RESUMABLE; + } + resumableChannel->ResumeAt(mStartPos, mEntityID); + } + + nsCOMPtr internalChannel = + do_QueryInterface(newChannel, &rv); + if (NS_SUCCEEDED(rv)) { + TimeStamp timestamp; + rv = GetNavigationStartTimeStamp(×tamp); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + if (timestamp) { + Unused << internalChannel->SetNavigationStartTimeStamp(timestamp); + } + } + + return NS_OK; +} + +nsresult nsHttpChannel::AsyncProcessRedirection(uint32_t redirectType) { + LOG(("nsHttpChannel::AsyncProcessRedirection [this=%p type=%u]\n", this, + redirectType)); + + nsAutoCString location; + + // if a location header was not given, then we can't perform the redirect, + // so just carry on as though this were a normal response. + if (NS_FAILED(mResponseHead->GetHeader(nsHttp::Location, location))) + return NS_ERROR_FAILURE; + + // If we were told to not follow redirects automatically, then again + // carry on as though this were a normal response. + if (mLoadInfo->GetDontFollowRedirects()) { + return NS_ERROR_FAILURE; + } + + // make sure non-ASCII characters in the location header are escaped. + nsAutoCString locationBuf; + if (NS_EscapeURL(location.get(), -1, esc_OnlyNonASCII | esc_Spaces, + locationBuf)) + location = locationBuf; + + mRedirectType = redirectType; + + LOG(("redirecting to: %s [redirection-limit=%u]\n", location.get(), + uint32_t(mRedirectionLimit))); + + nsresult rv = CreateNewURI(location.get(), getter_AddRefs(mRedirectURI)); + + if (NS_FAILED(rv)) { + LOG(("Invalid URI for redirect: Location: %s\n", location.get())); + return NS_ERROR_CORRUPTED_CONTENT; + } + + if (mApplicationCache) { + // if we are redirected to a different origin check if there is a fallback + // cache entry to fall back to. we don't care about file strict + // checking, at least mURI is not a file URI. + if (!NS_SecurityCompareURIs(mURI, mRedirectURI, false)) { + PushRedirectAsyncFunc( + &nsHttpChannel::ContinueProcessRedirectionAfterFallback); + bool waitingForRedirectCallback; + Unused << ProcessFallback(&waitingForRedirectCallback); + if (waitingForRedirectCallback) return NS_OK; + PopRedirectAsyncFunc( + &nsHttpChannel::ContinueProcessRedirectionAfterFallback); + } + } + + return ContinueProcessRedirectionAfterFallback(NS_OK); +} + +nsresult nsHttpChannel::ContinueProcessRedirectionAfterFallback(nsresult rv) { + if (NS_SUCCEEDED(rv) && LoadFallingBack()) { + // do not continue with redirect processing, fallback is in + // progress now. + return NS_OK; + } + + // Kill the current cache entry if we are redirecting + // back to ourself. + bool redirectingBackToSameURI = false; + if (mCacheEntry && LoadCacheEntryIsWriteOnly() && + NS_SUCCEEDED(mURI->Equals(mRedirectURI, &redirectingBackToSameURI)) && + redirectingBackToSameURI) + mCacheEntry->AsyncDoom(nullptr); + + // move the reference of the old location to the new one if the new + // one has none. + PropagateReferenceIfNeeded(mURI, mRedirectURI); + + bool rewriteToGET = + ShouldRewriteRedirectToGET(mRedirectType, mRequestHead.ParsedMethod()); + + // prompt if the method is not safe (such as POST, PUT, DELETE, ...) + if (!rewriteToGET && !mRequestHead.IsSafeMethod()) { + rv = PromptTempRedirect(); + if (NS_FAILED(rv)) return rv; + } + +#ifdef MOZ_GECKO_PROFILER + if (profiler_can_accept_markers()) { + nsAutoCString requestMethod; + GetRequestMethod(requestMethod); + + int32_t priority = PRIORITY_NORMAL; + GetPriority(&priority); + + TimingStruct timings; + if (mTransaction) { + timings = mTransaction->Timings(); + } + + nsAutoCString contentType; + if (mResponseHead) { + mResponseHead->ContentType(contentType); + } + + profiler_add_network_marker( + mURI, requestMethod, priority, mChannelId, + NetworkLoadType::LOAD_REDIRECT, mLastStatusReported, TimeStamp::Now(), + mLogicalOffset, mCacheDisposition, mLoadInfo->GetInnerWindowID(), + &timings, mRedirectURI, std::move(mSource), + Some(nsDependentCString(contentType.get()))); + } +#endif + + nsCOMPtr ioService; + rv = gHttpHandler->GetIOService(getter_AddRefs(ioService)); + if (NS_FAILED(rv)) return rv; + + uint32_t redirectFlags; + if (nsHttp::IsPermanentRedirect(mRedirectType)) + redirectFlags = nsIChannelEventSink::REDIRECT_PERMANENT; + else + redirectFlags = nsIChannelEventSink::REDIRECT_TEMPORARY; + + nsCOMPtr newChannel; + nsCOMPtr redirectLoadInfo = + CloneLoadInfoForRedirect(mRedirectURI, redirectFlags); + rv = NS_NewChannelInternal(getter_AddRefs(newChannel), mRedirectURI, + redirectLoadInfo, + nullptr, // PerformanceStorage + nullptr, // aLoadGroup + nullptr, // aCallbacks + nsIRequest::LOAD_NORMAL, ioService); + NS_ENSURE_SUCCESS(rv, rv); + + rv = SetupReplacementChannel(mRedirectURI, newChannel, !rewriteToGET, + redirectFlags); + if (NS_FAILED(rv)) return rv; + + // verify that this is a legal redirect + mRedirectChannel = newChannel; + + PushRedirectAsyncFunc(&nsHttpChannel::ContinueProcessRedirection); + rv = gHttpHandler->AsyncOnChannelRedirect(this, newChannel, redirectFlags); + + if (NS_SUCCEEDED(rv)) rv = WaitForRedirectCallback(); + + if (NS_FAILED(rv)) { + AutoRedirectVetoNotifier notifier(this); + PopRedirectAsyncFunc(&nsHttpChannel::ContinueProcessRedirection); + } + + return rv; +} + +nsresult nsHttpChannel::ContinueProcessRedirection(nsresult rv) { + AutoRedirectVetoNotifier notifier(this); + + LOG(("nsHttpChannel::ContinueProcessRedirection [rv=%" PRIx32 ",this=%p]\n", + static_cast(rv), this)); + if (NS_FAILED(rv)) return rv; + + MOZ_ASSERT(mRedirectChannel, "No redirect channel?"); + + // Make sure to do this after we received redirect veto answer, + // i.e. after all sinks had been notified + mRedirectChannel->SetOriginalURI(mOriginalURI); + + // XXX we used to talk directly with the script security manager, but that + // should really be handled by the event sink implementation. + + // begin loading the new channel + rv = mRedirectChannel->AsyncOpen(mListener); + LOG((" new channel AsyncOpen returned %" PRIX32, static_cast(rv))); + NS_ENSURE_SUCCESS(rv, rv); + + // close down this channel + Cancel(NS_BINDING_REDIRECTED); + + notifier.RedirectSucceeded(); + + ReleaseListeners(); + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsHttpChannel +//----------------------------------------------------------------------------- + +NS_IMETHODIMP nsHttpChannel::OnAuthAvailable() { + LOG(("nsHttpChannel::OnAuthAvailable [this=%p]", this)); + + // setting mAuthRetryPending flag and resuming the transaction + // triggers process of throwing away the unauthenticated data already + // coming from the network + mIsAuthChannel = true; + mAuthRetryPending = true; + StoreProxyAuthPending(false); + LOG(("Resuming the transaction, we got credentials from user")); + if (mTransactionPump) { + mTransactionPump->Resume(); + } + + return NS_OK; +} + +NS_IMETHODIMP nsHttpChannel::OnAuthCancelled(bool userCancel) { + LOG(("nsHttpChannel::OnAuthCancelled [this=%p]", this)); + + if (mTransactionPump) { + // If the channel is trying to authenticate to a proxy and + // that was canceled we cannot show the http response body + // from the 40x as that might mislead the user into thinking + // it was a end host response instead of a proxy reponse. + // This must check explicitly whether a proxy auth was being done + // because we do want to show the content if this is an error from + // the origin server. + if (LoadProxyAuthPending()) Cancel(NS_ERROR_PROXY_CONNECTION_REFUSED); + + // ensure call of OnStartRequest of the current listener here, + // it would not be called otherwise at all + nsresult rv = CallOnStartRequest(); + + // drop mAuthRetryPending flag and resume the transaction + // this resumes load of the unauthenticated content data (which + // may have been canceled if we don't want to show it) + mAuthRetryPending = false; + LOG(("Resuming the transaction, user cancelled the auth dialog")); + mTransactionPump->Resume(); + + if (NS_FAILED(rv)) mTransactionPump->Cancel(rv); + } + + StoreProxyAuthPending(false); + return NS_OK; +} + +NS_IMETHODIMP nsHttpChannel::CloseStickyConnection() { + LOG(("nsHttpChannel::CloseStickyConnection this=%p", this)); + + // Require we are between OnStartRequest and OnStopRequest, because + // what we do here takes effect in OnStopRequest (not reusing the + // connection for next authentication round). + if (!LoadIsPending()) { + LOG((" channel not pending")); + NS_ERROR( + "CloseStickyConnection not called before OnStopRequest, won't have any " + "effect"); + return NS_ERROR_UNEXPECTED; + } + + MOZ_ASSERT(mTransaction); + if (!mTransaction) { + return NS_ERROR_UNEXPECTED; + } + + if (!(mCaps & NS_HTTP_STICKY_CONNECTION || + mTransaction->HasStickyConnection())) { + LOG((" not sticky")); + return NS_OK; + } + + mTransaction->DontReuseConnection(); + return NS_OK; +} + +NS_IMETHODIMP nsHttpChannel::ConnectionRestartable(bool aRestartable) { + LOG(("nsHttpChannel::ConnectionRestartable this=%p, restartable=%d", this, + aRestartable)); + StoreAuthConnectionRestartable(aRestartable); + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsHttpChannel::nsISupports +//----------------------------------------------------------------------------- + +NS_IMPL_ADDREF_INHERITED(nsHttpChannel, HttpBaseChannel) +NS_IMPL_RELEASE_INHERITED(nsHttpChannel, HttpBaseChannel) + +NS_INTERFACE_MAP_BEGIN(nsHttpChannel) + NS_INTERFACE_MAP_ENTRY(nsIRequest) + NS_INTERFACE_MAP_ENTRY(nsIChannel) + NS_INTERFACE_MAP_ENTRY(nsIRequestObserver) + NS_INTERFACE_MAP_ENTRY(nsIStreamListener) + NS_INTERFACE_MAP_ENTRY(nsIHttpChannel) + NS_INTERFACE_MAP_ENTRY(nsICacheInfoChannel) + NS_INTERFACE_MAP_ENTRY(nsICachingChannel) + NS_INTERFACE_MAP_ENTRY(nsIClassOfService) + NS_INTERFACE_MAP_ENTRY(nsIUploadChannel) + NS_INTERFACE_MAP_ENTRY(nsIFormPOSTActionChannel) + NS_INTERFACE_MAP_ENTRY(nsIUploadChannel2) + NS_INTERFACE_MAP_ENTRY(nsICacheEntryOpenCallback) + NS_INTERFACE_MAP_ENTRY(nsIHttpChannelInternal) + NS_INTERFACE_MAP_ENTRY(nsIResumableChannel) + NS_INTERFACE_MAP_ENTRY(nsITransportEventSink) + NS_INTERFACE_MAP_ENTRY(nsISupportsPriority) + NS_INTERFACE_MAP_ENTRY(nsIProtocolProxyCallback) + NS_INTERFACE_MAP_ENTRY(nsIProxiedChannel) + NS_INTERFACE_MAP_ENTRY(nsIHttpAuthenticableChannel) + NS_INTERFACE_MAP_ENTRY(nsIApplicationCacheContainer) + NS_INTERFACE_MAP_ENTRY(nsIApplicationCacheChannel) + NS_INTERFACE_MAP_ENTRY(nsIAsyncVerifyRedirectCallback) + NS_INTERFACE_MAP_ENTRY(nsIThreadRetargetableRequest) + NS_INTERFACE_MAP_ENTRY(nsIThreadRetargetableStreamListener) + NS_INTERFACE_MAP_ENTRY(nsIDNSListener) + NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference) + NS_INTERFACE_MAP_ENTRY(nsICorsPreflightCallback) + NS_INTERFACE_MAP_ENTRY(nsIRaceCacheWithNetwork) + NS_INTERFACE_MAP_ENTRY(nsITimerCallback) + NS_INTERFACE_MAP_ENTRY(nsIRequestTailUnblockCallback) + NS_INTERFACE_MAP_ENTRY_CONCRETE(nsHttpChannel) +NS_INTERFACE_MAP_END_INHERITING(HttpBaseChannel) + +//----------------------------------------------------------------------------- +// nsHttpChannel::nsIRequest +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsHttpChannel::Cancel(nsresult status) { + MOZ_ASSERT(NS_IsMainThread()); + // We should never have a pump open while a CORS preflight is in progress. + MOZ_ASSERT_IF(mPreflightChannel, !mCachePump); +#ifdef DEBUG + // We want to perform this check only when the chanel is being cancelled the + // first time with a URL classifier blocking error code. If mStatus is + // already set to such an error code then Cancel() may be called for some + // other reason, for example because we've received notification about our + // parent process side channel being canceled, in which case we cannot expect + // that CancelByURLClassifier() would have handled this case. + if (UrlClassifierFeatureFactory::IsClassifierBlockingErrorCode(status) && + !UrlClassifierFeatureFactory::IsClassifierBlockingErrorCode(mStatus)) { + MOZ_CRASH_UNSAFE_PRINTF("Blocking classifier error %" PRIx32 + " need to be handled by CancelByURLClassifier()", + static_cast(status)); + } +#endif + + LOG(("nsHttpChannel::Cancel [this=%p status=%" PRIx32 "]\n", this, + static_cast(status))); + if (mCanceled) { + LOG((" ignoring; already canceled\n")); + return NS_OK; + } + + LogCallingScriptLocation(this); + + if (LoadWaitingForRedirectCallback()) { + LOG(("channel canceled during wait for redirect callback")); + } + + return CancelInternal(status); +} + +NS_IMETHODIMP +nsHttpChannel::CancelByURLClassifier(nsresult aErrorCode) { + MOZ_ASSERT( + UrlClassifierFeatureFactory::IsClassifierBlockingErrorCode(aErrorCode)); + MOZ_ASSERT(NS_IsMainThread()); + // We should never have a pump open while a CORS preflight is in progress. + MOZ_ASSERT_IF(mPreflightChannel, !mCachePump); + + LOG(("nsHttpChannel::CancelByURLClassifier [this=%p]\n", this)); + + if (mCanceled) { + LOG((" ignoring; already canceled\n")); + return NS_OK; + } + + // We are being canceled by the channel classifier because of tracking + // protection, but we haven't yet had a chance to dispatch the + // "http-on-modify-request" notifications yet (this would normally be + // done in PrepareToConnect()). So do that now, before proceeding to + // cancel. + // + // Note that running these observers can itself result in the channel + // being canceled. In that case, we accept that cancelation code as + // the cause of the cancelation, as if the classification of the channel + // would have occurred past this point! + + // notify "http-on-modify-request" observers + CallOnModifyRequestObservers(); + + // Check if request was cancelled during on-modify-request + if (mCanceled) { + return mStatus; + } + + if (mSuspendCount) { + LOG(("Waiting until resume in Cancel [this=%p]\n", this)); + MOZ_ASSERT(!mCallOnResume); + StoreChannelClassifierCancellationPending(1); + mCallOnResume = [aErrorCode](nsHttpChannel* self) { + self->HandleContinueCancellingByURLClassifier(aErrorCode); + return NS_OK; + }; + return NS_OK; + } + + // Check to see if we should redirect this channel elsewhere by + // nsIHttpChannel.redirectTo API request + if (mAPIRedirectToURI) { + StoreChannelClassifierCancellationPending(1); + return AsyncCall(&nsHttpChannel::HandleAsyncAPIRedirect); + } + + return CancelInternal(aErrorCode); +} + +void nsHttpChannel::ContinueCancellingByURLClassifier(nsresult aErrorCode) { + MOZ_ASSERT( + UrlClassifierFeatureFactory::IsClassifierBlockingErrorCode(aErrorCode)); + MOZ_ASSERT(NS_IsMainThread()); + // We should never have a pump open while a CORS preflight is in progress. + MOZ_ASSERT_IF(mPreflightChannel, !mCachePump); + + LOG(("nsHttpChannel::ContinueCancellingByURLClassifier [this=%p]\n", this)); + if (mCanceled) { + LOG((" ignoring; already canceled\n")); + return; + } + + // Check to see if we should redirect this channel elsewhere by + // nsIHttpChannel.redirectTo API request + if (mAPIRedirectToURI) { + Unused << AsyncCall(&nsHttpChannel::HandleAsyncAPIRedirect); + return; + } + + Unused << CancelInternal(aErrorCode); +} + +nsresult nsHttpChannel::CancelInternal(nsresult status) { + bool channelClassifierCancellationPending = + !!LoadChannelClassifierCancellationPending(); + if (UrlClassifierFeatureFactory::IsClassifierBlockingErrorCode(status)) { + StoreChannelClassifierCancellationPending(0); + } + + mCanceled = true; + mStatus = status; + if (mProxyRequest) mProxyRequest->Cancel(status); + CancelNetworkRequest(status); + mCacheInputStream.CloseAndRelease(); + if (mCachePump) mCachePump->Cancel(status); + if (mAuthProvider) mAuthProvider->Cancel(status); + if (mPreflightChannel) mPreflightChannel->Cancel(status); + if (mRequestContext && mOnTailUnblock) { + mOnTailUnblock = nullptr; + mRequestContext->CancelTailedRequest(this); + CloseCacheEntry(false); + Unused << AsyncAbort(status); + } else if (channelClassifierCancellationPending) { + // If we're coming from an asynchronous path when canceling a channel due + // to safe-browsing protection, we need to AsyncAbort the channel now. + Unused << AsyncAbort(status); + } + return NS_OK; +} + +void nsHttpChannel::CancelNetworkRequest(nsresult aStatus) { + if (mTransaction) { + nsresult rv = gHttpHandler->CancelTransaction(mTransaction, aStatus); + if (NS_FAILED(rv)) { + LOG(("failed to cancel the transaction\n")); + } + } + if (mTransactionPump) mTransactionPump->Cancel(aStatus); +} + +NS_IMETHODIMP +nsHttpChannel::Suspend() { + NS_ENSURE_TRUE(LoadIsPending(), NS_ERROR_NOT_AVAILABLE); + + LOG(("nsHttpChannel::SuspendInternal [this=%p]\n", this)); + LogCallingScriptLocation(this); + + ++mSuspendCount; + + if (mSuspendCount == 1) { + mSuspendTimestamp = TimeStamp::NowLoRes(); + } + + nsresult rvTransaction = NS_OK; + if (mTransactionPump) { + rvTransaction = mTransactionPump->Suspend(); + } + nsresult rvCache = NS_OK; + if (mCachePump) { + rvCache = mCachePump->Suspend(); + } + + return NS_FAILED(rvTransaction) ? rvTransaction : rvCache; +} + +NS_IMETHODIMP +nsHttpChannel::Resume() { + NS_ENSURE_TRUE(mSuspendCount > 0, NS_ERROR_UNEXPECTED); + + LOG(("nsHttpChannel::ResumeInternal [this=%p]\n", this)); + LogCallingScriptLocation(this); + + if (--mSuspendCount == 0) { + mSuspendTotalTime += + (TimeStamp::NowLoRes() - mSuspendTimestamp).ToMilliseconds(); + + if (mCallOnResume) { + // Resume the interrupted procedure first, then resume + // the pump to continue process the input stream. + // Any newly created pump MUST be suspended to prevent calling + // its OnStartRequest before OnStopRequest of any pre-existing + // pump. AsyncResumePending ensures that. + MOZ_ASSERT(!LoadAsyncResumePending()); + StoreAsyncResumePending(1); + + std::function callOnResume = nullptr; + std::swap(callOnResume, mCallOnResume); + + RefPtr self(this); + nsCOMPtr transactionPump = mTransactionPump; + RefPtr cachePump = mCachePump; + + nsresult rv = NS_DispatchToCurrentThread(NS_NewRunnableFunction( + "nsHttpChannel::CallOnResume", + [callOnResume{std::move(callOnResume)}, self{std::move(self)}, + transactionPump{std::move(transactionPump)}, + cachePump{std::move(cachePump)}]() { + MOZ_ASSERT(self->LoadAsyncResumePending()); + nsresult rv = self->CallOrWaitForResume(callOnResume); + if (NS_FAILED(rv)) { + self->CloseCacheEntry(false); + Unused << self->AsyncAbort(rv); + } + MOZ_ASSERT(self->LoadAsyncResumePending()); + + self->StoreAsyncResumePending(0); + + // And now actually resume the previously existing pumps. + if (transactionPump) { + LOG( + ("nsHttpChannel::CallOnResume resuming previous transaction " + "pump %p, this=%p", + transactionPump.get(), self.get())); + transactionPump->Resume(); + } + if (cachePump) { + LOG( + ("nsHttpChannel::CallOnResume resuming previous cache pump " + "%p, this=%p", + cachePump.get(), self.get())); + cachePump->Resume(); + } + + // Any newly created pumps were suspended once because of + // AsyncResumePending. Problem is that the stream listener + // notification is already pending in the queue right now, because + // AsyncRead doesn't (regardless if called after Suspend) respect + // the suspend coutner and the right order would not be preserved. + // Hence, we do another dispatch round to actually Resume after + // the notification from the original pump. + if (transactionPump != self->mTransactionPump && + self->mTransactionPump) { + LOG( + ("nsHttpChannel::CallOnResume async-resuming new " + "transaction " + "pump %p, this=%p", + self->mTransactionPump.get(), self.get())); + + nsCOMPtr pump = self->mTransactionPump; + NS_DispatchToCurrentThread(NS_NewRunnableFunction( + "nsHttpChannel::CallOnResume new transaction", + [pump{std::move(pump)}]() { pump->Resume(); })); + } + if (cachePump != self->mCachePump && self->mCachePump) { + LOG( + ("nsHttpChannel::CallOnResume async-resuming new cache pump " + "%p, this=%p", + self->mCachePump.get(), self.get())); + + RefPtr pump = self->mCachePump; + NS_DispatchToCurrentThread(NS_NewRunnableFunction( + "nsHttpChannel::CallOnResume new pump", + [pump{std::move(pump)}]() { pump->Resume(); })); + } + })); + NS_ENSURE_SUCCESS(rv, rv); + return rv; + } + } + + nsresult rvTransaction = NS_OK; + if (mTransactionPump) { + rvTransaction = mTransactionPump->Resume(); + } + + nsresult rvCache = NS_OK; + if (mCachePump) { + rvCache = mCachePump->Resume(); + } + + return NS_FAILED(rvTransaction) ? rvTransaction : rvCache; +} + +//----------------------------------------------------------------------------- +// nsHttpChannel::nsIChannel +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsHttpChannel::GetSecurityInfo(nsISupports** securityInfo) { + NS_ENSURE_ARG_POINTER(securityInfo); + *securityInfo = mSecurityInfo; + NS_IF_ADDREF(*securityInfo); + return NS_OK; +} + +// If any of the functions that AsyncOpen calls returns immediately an error +// AsyncAbort(which calls onStart/onStopRequest) does not need to be call. +// To be sure that they are not call ReleaseListeners() is called. +// If AsyncOpen returns NS_OK, after that point AsyncAbort must be called on +// any error. +NS_IMETHODIMP +nsHttpChannel::AsyncOpen(nsIStreamListener* aListener) { + nsCOMPtr listener = aListener; + nsresult rv = + nsContentSecurityManager::doContentSecurityCheck(this, listener); + if (NS_WARN_IF(NS_FAILED(rv))) { + ReleaseListeners(); + return rv; + } + MOZ_ASSERT( + mLoadInfo->GetSecurityMode() == 0 || + mLoadInfo->GetInitialSecurityCheckDone() || + (mLoadInfo->GetSecurityMode() == + nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL && + mLoadInfo->GetLoadingPrincipal() && + mLoadInfo->GetLoadingPrincipal()->IsSystemPrincipal()), + "security flags in loadInfo but doContentSecurityCheck() not called"); + + LOG(("nsHttpChannel::AsyncOpen [this=%p]\n", this)); + LogCallingScriptLocation(this); + +#ifdef MOZ_TASK_TRACER + if (tasktracer::IsStartLogging()) { + uint64_t sourceEventId, parentTaskId; + tasktracer::SourceEventType sourceEventType; + GetCurTraceInfo(&sourceEventId, &parentTaskId, &sourceEventType); + nsAutoCString urispec; + mURI->GetSpec(urispec); + tasktracer::AddLabel("nsHttpChannel::AsyncOpen %s", urispec.get()); + } +#endif + +#ifdef MOZ_GECKO_PROFILER + mLastStatusReported = + TimeStamp::Now(); // in case we enable the profiler after AsyncOpen() + if (profiler_can_accept_markers()) { + nsAutoCString requestMethod; + GetRequestMethod(requestMethod); + + profiler_add_network_marker( + mURI, requestMethod, mPriority, mChannelId, NetworkLoadType::LOAD_START, + mChannelCreationTimestamp, mLastStatusReported, 0, mCacheDisposition, + mLoadInfo->GetInnerWindowID(), nullptr, nullptr); + } +#endif + + NS_CompareLoadInfoAndLoadContext(this); + +#ifdef DEBUG + AssertPrivateBrowsingId(); +#endif + + NS_ENSURE_ARG_POINTER(listener); + NS_ENSURE_TRUE(!LoadIsPending(), NS_ERROR_IN_PROGRESS); + NS_ENSURE_TRUE(!LoadWasOpened(), NS_ERROR_ALREADY_OPENED); + + if (mCanceled) { + ReleaseListeners(); + return mStatus; + } + + if (MaybeWaitForUploadStreamLength(listener, nullptr)) { + return NS_OK; + } + + MOZ_ASSERT(NS_IsMainThread()); + + if (!gHttpHandler->Active()) { + LOG((" after HTTP shutdown...")); + ReleaseListeners(); + return NS_ERROR_NOT_AVAILABLE; + } + + rv = NS_CheckPortSafety(mURI); + if (NS_FAILED(rv)) { + ReleaseListeners(); + return rv; + } + + if (!mLoadGroup && !mCallbacks) { + // If no one called SetLoadGroup or SetNotificationCallbacks, the private + // state has not been updated on PrivateBrowsingChannel (which we derive + // from) Hence, we have to call UpdatePrivateBrowsing() here + UpdatePrivateBrowsing(); + } + + AntiTrackingUtils::UpdateAntiTrackingInfoForChannel(this); + + if (WaitingForTailUnblock()) { + // This channel is marked as Tail and is part of a request context + // that has positive number of non-tailed requestst, hence this channel + // has been put to a queue. + // When tail is unblocked, OnTailUnblock on this channel will be called + // to continue AsyncOpen. + mListener = listener; + MOZ_DIAGNOSTIC_ASSERT(!mOnTailUnblock); + mOnTailUnblock = &nsHttpChannel::AsyncOpenOnTailUnblock; + + LOG((" put on hold until tail is unblocked")); + return NS_OK; + } + + // Remember the cookie header that was set, if any + nsAutoCString cookieHeader; + if (NS_SUCCEEDED(mRequestHead.GetHeader(nsHttp::Cookie, cookieHeader))) { + mUserSetCookieHeader = cookieHeader; + } + + // Set user agent override, do so before OnOpeningRequest notification + // since we want to allow consumers of that notification change or remove + // the User-Agent request header. + HttpBaseChannel::SetDocshellUserAgentOverride(); + + // After we notify any observers (on-opening-request, loadGroup, etc) we + // must return NS_OK and return any errors asynchronously via + // OnStart/OnStopRequest. Observers may add a reference to the channel + // and expect to get OnStopRequest so they know when to drop the reference, + // etc. + + // notify "http-on-opening-request" observers, but not if this is a redirect + if (!(mLoadFlags & LOAD_REPLACE)) { + gHttpHandler->OnOpeningRequest(this); + } + + StoreIsPending(true); + StoreWasOpened(true); + + mListener = listener; + + if (nsIOService::UseSocketProcess() && + !gIOService->IsSocketProcessLaunchComplete()) { + RefPtr self = this; + gIOService->CallOrWaitForSocketProcess( + [self]() { self->AsyncOpenFinal(TimeStamp::Now()); }); + return NS_OK; + } + + // PauseTask/DelayHttpChannel queuing + if (!DelayHttpChannelQueue::AttemptQueueChannel(this)) { + // If fuzzyfox is disabled; or adding to the queue failed, the channel must + // continue. + AsyncOpenFinal(TimeStamp::Now()); + } + + return NS_OK; +} + +nsresult nsHttpChannel::AsyncOpenFinal(TimeStamp aTimeStamp) { + // Added due to PauseTask/DelayHttpChannel + if (mLoadGroup) mLoadGroup->AddRequest(this, nullptr); + + // record asyncopen time unconditionally and clear it if we + // don't want it after OnModifyRequest() weighs in. But waiting for + // that to complete would mean we don't include proxy resolution in the + // timing. + if (!LoadAsyncOpenTimeOverriden()) { + mAsyncOpenTime = aTimeStamp; + } + + // Remember we have Authorization header set here. We need to check on it + // just once and early, AsyncOpen is the best place. + StoreCustomAuthHeader(mRequestHead.HasHeader(nsHttp::Authorization)); + + if (!NS_ShouldClassifyChannel(this)) { + return MaybeResolveProxyAndBeginConnect(); + } + + // We are about to do an async lookup to check if the URI is a tracker. If + // yes, this channel will be canceled by channel classifier. Chances are the + // lookup is not needed so CheckIsTrackerWithLocalTable() will return an + // error and then we can MaybeResolveProxyAndBeginConnect() right away. + RefPtr self = this; + bool willCallback = NS_SUCCEEDED( + AsyncUrlChannelClassifier::CheckChannel(this, [self]() -> void { + nsresult rv = self->MaybeResolveProxyAndBeginConnect(); + if (NS_FAILED(rv)) { + // Since this error is thrown asynchronously so that the caller + // of BeginConnect() will not do clean up for us. We have to do + // it on our own. + self->CloseCacheEntry(false); + Unused << self->AsyncAbort(rv); + } + })); + + if (!willCallback) { + // We can do MaybeResolveProxyAndBeginConnect immediately if + // CheckIsTrackerWithLocalTable is failed. Note that we don't need to + // handle the failure because BeginConnect() will return synchronously and + // the caller will be responsible for handling it. + return MaybeResolveProxyAndBeginConnect(); + } + + return NS_OK; +} + +nsresult nsHttpChannel::MaybeResolveProxyAndBeginConnect() { + nsresult rv; + + // The common case for HTTP channels is to begin proxy resolution and return + // at this point. The only time we know mProxyInfo already is if we're + // proxying a non-http protocol like ftp. We don't need to discover proxy + // settings if we are never going to make a network connection. + if (!mProxyInfo && + !(mLoadFlags & (LOAD_ONLY_FROM_CACHE | LOAD_NO_NETWORK_IO)) && + NS_SUCCEEDED(ResolveProxy())) { + return NS_OK; + } + + rv = BeginConnect(); + if (NS_FAILED(rv)) { + CloseCacheEntry(false); + Unused << AsyncAbort(rv); + } + + return NS_OK; +} + +nsresult nsHttpChannel::AsyncOpenOnTailUnblock() { + return AsyncOpen(mListener); +} + +already_AddRefed +nsHttpChannel::GetOrCreateChannelClassifier() { + if (!mChannelClassifier) { + mChannelClassifier = new nsChannelClassifier(this); + LOG(("nsHttpChannel [%p] created nsChannelClassifier [%p]\n", this, + mChannelClassifier.get())); + } + + RefPtr classifier = mChannelClassifier; + return classifier.forget(); +} + +uint16_t nsHttpChannel::GetProxyDNSStrategy() { + // This function currently only supports returning DNS_PREFETCH_ORIGIN. + // Support for the rest of the DNS_* flags will be added later. + + if (!mProxyInfo) { + return DNS_PREFETCH_ORIGIN; + } + + nsAutoCString type; + mProxyInfo->GetType(type); + + if (!StaticPrefs::network_proxy_socks_remote_dns()) { + if (type.EqualsLiteral("socks")) { + return DNS_PREFETCH_ORIGIN; + } + } + + return 0; +} + +// BeginConnect() SHOULD NOT call AsyncAbort(). AsyncAbort will be called by +// functions that called BeginConnect if needed. Only AsyncOpenFinal, +// MaybeResolveProxyAndBeginConnect and OnProxyAvailable ever call +// BeginConnect. +nsresult nsHttpChannel::BeginConnect() { + LOG(("nsHttpChannel::BeginConnect [this=%p]\n", this)); + nsresult rv; + + // Construct connection info object + nsAutoCString host; + nsAutoCString scheme; + int32_t port = -1; + bool isHttps = mURI->SchemeIs("https"); + + rv = mURI->GetScheme(scheme); + if (NS_SUCCEEDED(rv)) rv = mURI->GetAsciiHost(host); + if (NS_SUCCEEDED(rv)) rv = mURI->GetPort(&port); + if (NS_SUCCEEDED(rv)) rv = mURI->GetAsciiSpec(mSpec); + if (NS_FAILED(rv)) { + return rv; + } + + // Just a warning here because some nsIURIs do not implement this method. + Unused << NS_WARN_IF(NS_FAILED(mURI->GetUsername(mUsername))); + + // Reject the URL if it doesn't specify a host + if (host.IsEmpty()) { + rv = NS_ERROR_MALFORMED_URI; + return rv; + } + LOG(("host=%s port=%d\n", host.get(), port)); + LOG(("uri=%s\n", mSpec.get())); + + nsCOMPtr proxyInfo; + if (mProxyInfo) proxyInfo = do_QueryInterface(mProxyInfo); + + if (mCaps & NS_HTTP_CONNECT_ONLY) { + if (!proxyInfo) { + LOG(("return failure: no proxy for connect-only channel\n")); + return NS_ERROR_FAILURE; + } + + if (!proxyInfo->IsHTTP() && !proxyInfo->IsHTTPS()) { + LOG(("return failure: non-http proxy for connect-only channel\n")); + return NS_ERROR_FAILURE; + } + } + + mRequestHead.SetHTTPS(isHttps); + mRequestHead.SetOrigin(scheme, host, port); + + SetOriginHeader(); + SetDoNotTrack(); + + OriginAttributes originAttributes; + // Regular principal in case we have a proxy. + if (proxyInfo && + !StaticPrefs::privacy_partition_network_state_connection_with_proxy()) { + StoragePrincipalHelper::GetOriginAttributes( + this, originAttributes, StoragePrincipalHelper::eRegularPrincipal); + } else { + StoragePrincipalHelper::GetOriginAttributesForNetworkState( + this, originAttributes); + } + + // Adjust mCaps according to our request headers: + // - If "Connection: close" is set as a request header, then do not bother + // trying to establish a keep-alive connection. + if (mRequestHead.HasHeaderValue(nsHttp::Connection, "close")) { + mCaps &= ~(NS_HTTP_ALLOW_KEEPALIVE); + StoreAllowHttp3(false); + } + + gHttpHandler->MaybeAddAltSvcForTesting(mURI, mUsername, GetTopWindowOrigin(), + mPrivateBrowsing, IsIsolated(), + mCallbacks, originAttributes); + + RefPtr connInfo = new nsHttpConnectionInfo( + host, port, ""_ns, mUsername, GetTopWindowOrigin(), proxyInfo, + originAttributes, isHttps); + bool http2Allowed = !gHttpHandler->IsHttp2Excluded(connInfo); + if (!LoadAllowHttp3()) { + mCaps |= NS_HTTP_DISALLOW_HTTP3; + } + bool http3Allowed = !mUpgradeProtocolCallback && !mProxyInfo && + !(mCaps & NS_HTTP_BE_CONSERVATIVE) && + !LoadBeConservative() && LoadAllowHttp3(); + + // No need to lookup HTTPSSVC record if mHTTPSSVCRecord already contains a + // value. + StoreUseHTTPSSVC(StaticPrefs::network_dns_upgrade_with_https_rr() && + mHTTPSSVCRecord.isNothing()); + + RefPtr mapping; + if (!mConnectionInfo && LoadAllowAltSvc() && // per channel + (http2Allowed || http3Allowed) && !(mLoadFlags & LOAD_FRESH_CONNECTION) && + AltSvcMapping::AcceptableProxy(proxyInfo) && + (scheme.EqualsLiteral("http") || scheme.EqualsLiteral("https")) && + (mapping = gHttpHandler->GetAltServiceMapping( + scheme, host, port, mPrivateBrowsing, IsIsolated(), + GetTopWindowOrigin(), originAttributes, http2Allowed, + http3Allowed))) { + LOG(("nsHttpChannel %p Alt Service Mapping Found %s://%s:%d [%s]\n", this, + scheme.get(), mapping->AlternateHost().get(), mapping->AlternatePort(), + mapping->HashKey().get())); + + if (!(mLoadFlags & LOAD_ANONYMOUS) && !mPrivateBrowsing) { + nsAutoCString altUsedLine(mapping->AlternateHost()); + bool defaultPort = + mapping->AlternatePort() == + (isHttps ? NS_HTTPS_DEFAULT_PORT : NS_HTTP_DEFAULT_PORT); + if (!defaultPort) { + altUsedLine.AppendLiteral(":"); + altUsedLine.AppendInt(mapping->AlternatePort()); + } + rv = mRequestHead.SetHeader(nsHttp::Alternate_Service_Used, altUsedLine); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + + nsCOMPtr consoleService = + do_GetService(NS_CONSOLESERVICE_CONTRACTID); + if (consoleService) { + nsAutoString message(u"Alternate Service Mapping found: "_ns); + AppendASCIItoUTF16(scheme, message); + message.AppendLiteral(u"://"); + AppendASCIItoUTF16(host, message); + message.AppendLiteral(u":"); + message.AppendInt(port); + message.AppendLiteral(u" to "); + AppendASCIItoUTF16(scheme, message); + message.AppendLiteral(u"://"); + AppendASCIItoUTF16(mapping->AlternateHost(), message); + message.AppendLiteral(u":"); + message.AppendInt(mapping->AlternatePort()); + consoleService->LogStringMessage(message.get()); + } + + LOG(("nsHttpChannel %p Using connection info from altsvc mapping", this)); + mapping->GetConnectionInfo(getter_AddRefs(mConnectionInfo), proxyInfo, + originAttributes); + Telemetry::Accumulate(Telemetry::HTTP_TRANSACTION_USE_ALTSVC, true); + Telemetry::Accumulate(Telemetry::HTTP_TRANSACTION_USE_ALTSVC_OE, !isHttps); + + // Don't use HTTPSSVC record if we found altsvc mapping. + StoreUseHTTPSSVC(false); + } else if (mConnectionInfo) { + LOG(("nsHttpChannel %p Using channel supplied connection info", this)); + Telemetry::Accumulate(Telemetry::HTTP_TRANSACTION_USE_ALTSVC, false); + } else { + LOG(("nsHttpChannel %p Using default connection info", this)); + + mConnectionInfo = connInfo; + Telemetry::Accumulate(Telemetry::HTTP_TRANSACTION_USE_ALTSVC, false); + } + + // Need to re-ask the handler, since mConnectionInfo may not be the connInfo + // we used earlier + if (gHttpHandler->IsHttp2Excluded(mConnectionInfo)) { + StoreAllowSpdy(0); + mCaps |= NS_HTTP_DISALLOW_SPDY; + mConnectionInfo->SetNoSpdy(true); + } + + mAuthProvider = new nsHttpChannelAuthProvider(); + rv = mAuthProvider->Init(this); + if (NS_FAILED(rv)) { + return rv; + } + + // check to see if authorization headers should be included + // CustomAuthHeader is set in AsyncOpen if we find Authorization header + rv = mAuthProvider->AddAuthorizationHeaders(LoadCustomAuthHeader()); + if (NS_FAILED(rv)) { + LOG(("nsHttpChannel %p AddAuthorizationHeaders failed (%08x)", this, + static_cast(rv))); + } + + // If TimingEnabled flag is not set after OnModifyRequest() then + // clear the already recorded AsyncOpen value for consistency. + if (!LoadTimingEnabled()) mAsyncOpenTime = TimeStamp(); + + // if this somehow fails we can go on without it + Unused << gHttpHandler->AddConnectionHeader(&mRequestHead, mCaps); + + if (!LoadIsTRRServiceChannel() && + (mLoadFlags & VALIDATE_ALWAYS || + BYPASS_LOCAL_CACHE(mLoadFlags, LoadPreferCacheLoadOverBypass()))) + mCaps |= NS_HTTP_REFRESH_DNS; + + if (gHttpHandler->CriticalRequestPrioritization()) { + if (mClassOfService & nsIClassOfService::Leader) { + mCaps |= NS_HTTP_LOAD_AS_BLOCKING; + } + if (mClassOfService & nsIClassOfService::Unblocked) { + mCaps |= NS_HTTP_LOAD_UNBLOCKED; + } + if (mClassOfService & nsIClassOfService::UrgentStart && + gHttpHandler->IsUrgentStartEnabled()) { + mCaps |= NS_HTTP_URGENT_START; + SetPriority(nsISupportsPriority::PRIORITY_HIGHEST); + } + } + + // Force-Reload should reset the persistent connection pool for this host + if (mLoadFlags & LOAD_FRESH_CONNECTION) { + // just the initial document resets the whole pool + if (mLoadFlags & LOAD_INITIAL_DOCUMENT_URI) { + gHttpHandler->AltServiceCache()->ClearAltServiceMappings(); + rv = gHttpHandler->DoShiftReloadConnectionCleanupWithConnInfo( + mConnectionInfo); + if (NS_FAILED(rv)) { + LOG(( + "nsHttpChannel::BeginConnect " + "DoShiftReloadConnectionCleanupWithConnInfo failed: %08x [this=%p]", + static_cast(rv), this)); + } + } + } + + // We may have been cancelled already, either by on-modify-request + // listeners or load group observers; in that case, we should not send the + // request to the server + if (mCanceled) { + return mStatus; + } + + bool shouldBeClassified = NS_ShouldClassifyChannel(this); + + if (shouldBeClassified) { + if (LoadChannelClassifierCancellationPending()) { + LOG( + ("Waiting for safe-browsing protection cancellation in BeginConnect " + "[this=%p]\n", + this)); + return NS_OK; + } + + ReEvaluateReferrerAfterTrackingStatusIsKnown(); + } + + rv = MaybeStartDNSPrefetch(); + if (NS_FAILED(rv)) { + auto dnsStrategy = GetProxyDNSStrategy(); + if (dnsStrategy & DNS_BLOCK_ON_ORIGIN_RESOLVE) { + // TODO: Should this be fatal? + return rv; + } + // Otherwise this shouldn't be fatal. + return NS_OK; + } + + rv = ContinueBeginConnectWithResult(); + if (NS_FAILED(rv)) { + return rv; + } + + if (shouldBeClassified) { + // Start nsChannelClassifier to catch phishing and malware URIs. + RefPtr channelClassifier = + GetOrCreateChannelClassifier(); + LOG(("nsHttpChannel::Starting nsChannelClassifier %p [this=%p]", + channelClassifier.get(), this)); + channelClassifier->Start(); + } + + return NS_OK; +} + +nsresult nsHttpChannel::MaybeStartDNSPrefetch() { + bool httpssvcQueried = false; + // If https rr is not queried sucessfully, we have to reset mUseHTTPSSVC to + // false. Otherwise, this channel may wait https rr forever. + auto resetUsHTTPSSVC = + MakeScopeExit([&] { StoreUseHTTPSSVC(httpssvcQueried); }); + + // Start a DNS lookup very early in case the real open is queued the DNS can + // happen in parallel. Do not do so in the presence of an HTTP proxy as + // all lookups other than for the proxy itself are done by the proxy. + // Also we don't do a lookup if the LOAD_NO_NETWORK_IO or + // LOAD_ONLY_FROM_CACHE flags are set. + // + // We keep the DNS prefetch object around so that we can retrieve + // timing information from it. There is no guarantee that we actually + // use the DNS prefetch data for the real connection, but as we keep + // this data around for 3 minutes by default, this should almost always + // be correct, and even when it isn't, the timing still represents _a_ + // valid DNS lookup timing for the site, even if it is not _the_ + // timing we used. + if (mLoadFlags & (LOAD_NO_NETWORK_IO | LOAD_ONLY_FROM_CACHE)) { + return NS_OK; + } + + auto dnsStrategy = GetProxyDNSStrategy(); + + LOG( + ("nsHttpChannel::MaybeStartDNSPrefetch [this=%p, strategy=%u] " + "prefetching%s\n", + this, dnsStrategy, + mCaps & NS_HTTP_REFRESH_DNS ? ", refresh requested" : "")); + + if (dnsStrategy & DNS_PREFETCH_ORIGIN) { + OriginAttributes originAttributes; + StoragePrincipalHelper::GetOriginAttributesForNetworkState( + this, originAttributes); + + mDNSPrefetch = + new nsDNSPrefetch(mURI, originAttributes, nsIRequest::GetTRRMode(), + this, LoadTimingEnabled()); + nsresult rv = mDNSPrefetch->PrefetchHigh(mCaps & NS_HTTP_REFRESH_DNS); + + if (dnsStrategy & DNS_BLOCK_ON_ORIGIN_RESOLVE) { + LOG((" blocking on prefetching origin")); + + if (NS_WARN_IF(NS_FAILED(rv))) { + LOG((" lookup failed with 0x%08" PRIx32 ", aborting request", + static_cast(rv))); + return rv; + } + + // Resolved in OnLookupComplete. + mDNSBlockingThenable = mDNSBlockingPromise.Ensure(__func__); + } + + // When LoadUseHTTPSSVC() is true, we should really "fetch" the HTTPS RR, + // not "prefetch", since DNS prefetch can be disabled by the pref. + if (LoadUseHTTPSSVC() || + gHttpHandler->UseHTTPSRRForSpeculativeConnection()) { + OriginAttributes originAttributes; + StoragePrincipalHelper::GetOriginAttributesForHTTPSRR(this, + originAttributes); + + RefPtr resolver = + new nsDNSPrefetch(mURI, originAttributes, nsIRequest::GetTRRMode()); + nsWeakPtr weakPtrThis( + do_GetWeakReference(static_cast(this))); + nsresult rv = resolver->FetchHTTPSSVC( + mCaps & NS_HTTP_REFRESH_DNS, !LoadUseHTTPSSVC(), + [weakPtrThis](nsIDNSHTTPSSVCRecord* aRecord) { + nsCOMPtr channel = do_QueryReferent(weakPtrThis); + RefPtr httpChannelImpl = do_QueryObject(channel); + if (httpChannelImpl) { + httpChannelImpl->OnHTTPSRRAvailable(aRecord); + } + }); + if (NS_FAILED(rv)) { + LOG((" FetchHTTPSSVC failed with 0x%08" PRIx32, + static_cast(rv))); + } else { + httpssvcQueried = true; + } + } + } + + return NS_OK; +} + +NS_IMETHODIMP +nsHttpChannel::GetEncodedBodySize(uint64_t* aEncodedBodySize) { + if (mCacheEntry && !LoadCacheEntryIsWriteOnly()) { + int64_t dataSize = 0; + mCacheEntry->GetDataSize(&dataSize); + *aEncodedBodySize = dataSize; + } else { + *aEncodedBodySize = mLogicalOffset; + } + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsHttpChannel::nsIHttpChannelInternal +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsHttpChannel::SetupFallbackChannel(const char* aFallbackKey) { + ENSURE_CALLED_BEFORE_CONNECT(); + + LOG(("nsHttpChannel::SetupFallbackChannel [this=%p, key=%s]\n", this, + aFallbackKey)); + StoreFallbackChannel(true); + mFallbackKey = aFallbackKey; + + return NS_OK; +} + +NS_IMETHODIMP +nsHttpChannel::GetIsAuthChannel(bool* aIsAuthChannel) { + *aIsAuthChannel = mIsAuthChannel; + return NS_OK; +} + +NS_IMETHODIMP +nsHttpChannel::SetChannelIsForDownload(bool aChannelIsForDownload) { + if (aChannelIsForDownload) { + AddClassFlags(nsIClassOfService::Throttleable); + } else { + ClearClassFlags(nsIClassOfService::Throttleable); + } + + return HttpBaseChannel::SetChannelIsForDownload(aChannelIsForDownload); +} + +base::ProcessId nsHttpChannel::ProcessId() { + nsCOMPtr parentChannel; + NS_QueryNotificationCallbacks(this, parentChannel); + if (RefPtr httpParent = do_QueryObject(parentChannel)) { + return httpParent->OtherPid(); + } + if (RefPtr docParent = do_QueryObject(parentChannel)) { + return docParent->OtherPid(); + } + return base::GetCurrentProcId(); +} + +auto nsHttpChannel::AttachStreamFilter(base::ProcessId aChildProcessId) + -> RefPtr { + LOG(("nsHttpChannel::AttachStreamFilter [this=%p]", this)); + MOZ_ASSERT(!LoadOnStartRequestCalled()); + + if (!ProcessId()) { + return ChildEndpointPromise::CreateAndReject(false, __func__); + } + + nsCOMPtr parentChannel; + NS_QueryNotificationCallbacks(this, parentChannel); + + // If our listener is a DocumentLoadListener, then we might handle + // multi-part responses here in the parent process. The current extension + // API doesn't understand the parsed multipart format, so we defer responding + // here until CallOnStartRequest, and attach the StreamFilter before the + // multipart handler (in the parent process!) if applicable. + if (RefPtr docParent = do_QueryObject(parentChannel)) { + StreamFilterRequest* request = mStreamFilterRequests.AppendElement(); + request->mPromise = new ChildEndpointPromise::Private(__func__); + request->mChildProcessId = aChildProcessId; + return request->mPromise; + } + + mozilla::ipc::Endpoint parent; + mozilla::ipc::Endpoint child; + nsresult rv = extensions::PStreamFilter::CreateEndpoints( + ProcessId(), aChildProcessId, &parent, &child); + if (NS_FAILED(rv)) { + return ChildEndpointPromise::CreateAndReject(false, __func__); + } + + if (RefPtr httpParent = do_QueryObject(parentChannel)) { + return httpParent->AttachStreamFilter(std::move(parent), std::move(child)); + } + + extensions::StreamFilterParent::Attach(this, std::move(parent)); + return ChildEndpointPromise::CreateAndResolve(std::move(child), __func__); +} + +NS_IMETHODIMP +nsHttpChannel::GetNavigationStartTimeStamp(TimeStamp* aTimeStamp) { + LOG(("nsHttpChannel::GetNavigationStartTimeStamp [this=%p]", this)); + MOZ_ASSERT(aTimeStamp); + *aTimeStamp = mNavigationStartTimeStamp; + return NS_OK; +} + +NS_IMETHODIMP +nsHttpChannel::SetNavigationStartTimeStamp(TimeStamp aTimeStamp) { + LOG(("nsHttpChannel::SetNavigationStartTimeStamp [this=%p]", this)); + mNavigationStartTimeStamp = aTimeStamp; + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsHttpChannel::nsISupportsPriority +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsHttpChannel::SetPriority(int32_t value) { + int16_t newValue = clamped(value, INT16_MIN, INT16_MAX); + if (mPriority == newValue) return NS_OK; + + LOG(("nsHttpChannel::SetPriority %p p=%d", this, newValue)); + + mPriority = newValue; + if (mTransaction) { + nsresult rv = gHttpHandler->RescheduleTransaction(mTransaction, mPriority); + if (NS_FAILED(rv)) { + LOG( + ("nsHttpChannel::SetPriority [this=%p] " + "RescheduleTransaction failed (%08x)", + this, static_cast(rv))); + } + } + + // If this channel is the real channel for an e10s channel, notify the + // child side about the priority change as well. + nsCOMPtr parentChannel; + NS_QueryNotificationCallbacks(this, parentChannel); + RefPtr httpParent = do_QueryObject(parentChannel); + if (httpParent) { + httpParent->DoSendSetPriority(newValue); + } + + return NS_OK; +} + +nsresult nsHttpChannel::ContinueBeginConnectWithResult() { + LOG(("nsHttpChannel::ContinueBeginConnectWithResult [this=%p]", this)); + MOZ_ASSERT(!mCallOnResume, "How did that happen?"); + + nsresult rv; + + if (mSuspendCount) { + LOG(("Waiting until resume to do async connect [this=%p]\n", this)); + mCallOnResume = [](nsHttpChannel* self) { + self->ContinueBeginConnect(); + return NS_OK; + }; + rv = NS_OK; + } else if (mCanceled) { + // We may have been cancelled already, by nsChannelClassifier in that + // case, we should not send the request to the server + rv = mStatus; + } else { + rv = PrepareToConnect(); + } + + LOG( + ("nsHttpChannel::ContinueBeginConnectWithResult result [this=%p " + "rv=%" PRIx32 " mCanceled=%u]\n", + this, static_cast(rv), static_cast(mCanceled))); + return rv; +} + +void nsHttpChannel::ContinueBeginConnect() { + LOG(("nsHttpChannel::ContinueBeginConnect this=%p", this)); + + nsresult rv = ContinueBeginConnectWithResult(); + if (NS_FAILED(rv)) { + CloseCacheEntry(false); + Unused << AsyncAbort(rv); + } +} + +//----------------------------------------------------------------------------- +// HttpChannel::nsIClassOfService +//----------------------------------------------------------------------------- + +void nsHttpChannel::OnClassOfServiceUpdated() { + LOG(("nsHttpChannel::OnClassOfServiceUpdated this=%p, cos=%u", this, + mClassOfService)); + + if (mTransaction) { + gHttpHandler->UpdateClassOfServiceOnTransaction(mTransaction, + mClassOfService); + } + if (EligibleForTailing()) { + RemoveAsNonTailRequest(); + } else { + AddAsNonTailRequest(); + } +} + +NS_IMETHODIMP +nsHttpChannel::SetClassFlags(uint32_t inFlags) { + uint32_t previous = mClassOfService; + mClassOfService = inFlags; + if (previous != mClassOfService) { + OnClassOfServiceUpdated(); + } + return NS_OK; +} + +NS_IMETHODIMP +nsHttpChannel::AddClassFlags(uint32_t inFlags) { + uint32_t previous = mClassOfService; + mClassOfService |= inFlags; + if (previous != mClassOfService) { + OnClassOfServiceUpdated(); + } + return NS_OK; +} + +NS_IMETHODIMP +nsHttpChannel::ClearClassFlags(uint32_t inFlags) { + uint32_t previous = mClassOfService; + mClassOfService &= ~inFlags; + if (previous != mClassOfService) { + OnClassOfServiceUpdated(); + } + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsHttpChannel::nsIProtocolProxyCallback +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsHttpChannel::OnProxyAvailable(nsICancelable* request, nsIChannel* channel, + nsIProxyInfo* pi, nsresult status) { + LOG(("nsHttpChannel::OnProxyAvailable [this=%p pi=%p status=%" PRIx32 + " mStatus=%" PRIx32 "]\n", + this, pi, static_cast(status), + static_cast(static_cast(mStatus)))); + mProxyRequest = nullptr; + + nsresult rv; + + // If status is a failure code, then it means that we failed to resolve + // proxy info. That is a non-fatal error assuming it wasn't because the + // request was canceled. We just failover to DIRECT when proxy resolution + // fails (failure can mean that the PAC URL could not be loaded). + + if (NS_SUCCEEDED(status)) mProxyInfo = pi; + + if (!gHttpHandler->Active()) { + LOG( + ("nsHttpChannel::OnProxyAvailable [this=%p] " + "Handler no longer active.\n", + this)); + rv = NS_ERROR_NOT_AVAILABLE; + } else { + rv = BeginConnect(); + } + + if (NS_FAILED(rv)) { + CloseCacheEntry(false); + Unused << AsyncAbort(rv); + } + return rv; +} + +//----------------------------------------------------------------------------- +// nsHttpChannel::nsIProxiedChannel +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsHttpChannel::GetProxyInfo(nsIProxyInfo** result) { + if (!mConnectionInfo) + *result = mProxyInfo; + else + *result = mConnectionInfo->ProxyInfo(); + NS_IF_ADDREF(*result); + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsHttpChannel::nsITimedChannel +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsHttpChannel::GetDomainLookupStart(TimeStamp* _retval) { + if (mTransaction) + *_retval = mTransaction->GetDomainLookupStart(); + else + *_retval = mTransactionTimings.domainLookupStart; + return NS_OK; +} + +NS_IMETHODIMP +nsHttpChannel::GetDomainLookupEnd(TimeStamp* _retval) { + if (mTransaction) + *_retval = mTransaction->GetDomainLookupEnd(); + else + *_retval = mTransactionTimings.domainLookupEnd; + return NS_OK; +} + +NS_IMETHODIMP +nsHttpChannel::GetConnectStart(TimeStamp* _retval) { + if (mTransaction) + *_retval = mTransaction->GetConnectStart(); + else + *_retval = mTransactionTimings.connectStart; + return NS_OK; +} + +NS_IMETHODIMP +nsHttpChannel::GetTcpConnectEnd(TimeStamp* _retval) { + if (mTransaction) + *_retval = mTransaction->GetTcpConnectEnd(); + else + *_retval = mTransactionTimings.tcpConnectEnd; + return NS_OK; +} + +NS_IMETHODIMP +nsHttpChannel::GetSecureConnectionStart(TimeStamp* _retval) { + if (mTransaction) + *_retval = mTransaction->GetSecureConnectionStart(); + else + *_retval = mTransactionTimings.secureConnectionStart; + return NS_OK; +} + +NS_IMETHODIMP +nsHttpChannel::GetConnectEnd(TimeStamp* _retval) { + if (mTransaction) + *_retval = mTransaction->GetConnectEnd(); + else + *_retval = mTransactionTimings.connectEnd; + return NS_OK; +} + +NS_IMETHODIMP +nsHttpChannel::GetRequestStart(TimeStamp* _retval) { + if (mTransaction) + *_retval = mTransaction->GetRequestStart(); + else + *_retval = mTransactionTimings.requestStart; + return NS_OK; +} + +NS_IMETHODIMP +nsHttpChannel::GetResponseStart(TimeStamp* _retval) { + if (mTransaction) + *_retval = mTransaction->GetResponseStart(); + else + *_retval = mTransactionTimings.responseStart; + return NS_OK; +} + +NS_IMETHODIMP +nsHttpChannel::GetResponseEnd(TimeStamp* _retval) { + if (mTransaction) + *_retval = mTransaction->GetResponseEnd(); + else + *_retval = mTransactionTimings.responseEnd; + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsHttpChannel::nsIHttpAuthenticableChannel +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsHttpChannel::GetIsSSL(bool* aIsSSL) { + // this attribute is really misnamed - it wants to know if + // https:// is being used. SSL might be used to cover http:// + // in some circumstances (proxies, http/2, etc..) + return mURI->SchemeIs("https", aIsSSL); +} + +NS_IMETHODIMP +nsHttpChannel::GetProxyMethodIsConnect(bool* aProxyMethodIsConnect) { + *aProxyMethodIsConnect = mConnectionInfo->UsingConnect(); + return NS_OK; +} + +NS_IMETHODIMP +nsHttpChannel::GetServerResponseHeader(nsACString& value) { + if (!mResponseHead) return NS_ERROR_NOT_AVAILABLE; + return mResponseHead->GetHeader(nsHttp::Server, value); +} + +NS_IMETHODIMP +nsHttpChannel::GetProxyChallenges(nsACString& value) { + if (!mResponseHead) return NS_ERROR_UNEXPECTED; + return mResponseHead->GetHeader(nsHttp::Proxy_Authenticate, value); +} + +NS_IMETHODIMP +nsHttpChannel::GetWWWChallenges(nsACString& value) { + if (!mResponseHead) return NS_ERROR_UNEXPECTED; + return mResponseHead->GetHeader(nsHttp::WWW_Authenticate, value); +} + +NS_IMETHODIMP +nsHttpChannel::SetProxyCredentials(const nsACString& value) { + return mRequestHead.SetHeader(nsHttp::Proxy_Authorization, value); +} + +NS_IMETHODIMP +nsHttpChannel::SetWWWCredentials(const nsACString& value) { + // This method is called when various browser initiated authorization + // code sets the credentials. We need to flag this header as the + // "browser default" so it does not show up in the ServiceWorker + // FetchEvent. This may actually get called more than once, though, + // so we clear the header first since "default" headers are not + // allowed to overwrite normally. + Unused << mRequestHead.ClearHeader(nsHttp::Authorization); + return mRequestHead.SetHeader(nsHttp::Authorization, value, false, + nsHttpHeaderArray::eVarietyRequestDefault); +} + +//----------------------------------------------------------------------------- +// Methods that nsIHttpAuthenticableChannel dupes from other IDLs, which we +// get from HttpBaseChannel, must be explicitly forwarded, because C++ sucks. +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsHttpChannel::GetLoadFlags(nsLoadFlags* aLoadFlags) { + return HttpBaseChannel::GetLoadFlags(aLoadFlags); +} + +NS_IMETHODIMP +nsHttpChannel::GetURI(nsIURI** aURI) { return HttpBaseChannel::GetURI(aURI); } + +NS_IMETHODIMP +nsHttpChannel::GetNotificationCallbacks(nsIInterfaceRequestor** aCallbacks) { + return HttpBaseChannel::GetNotificationCallbacks(aCallbacks); +} + +NS_IMETHODIMP +nsHttpChannel::GetLoadGroup(nsILoadGroup** aLoadGroup) { + return HttpBaseChannel::GetLoadGroup(aLoadGroup); +} + +NS_IMETHODIMP +nsHttpChannel::GetRequestMethod(nsACString& aMethod) { + return HttpBaseChannel::GetRequestMethod(aMethod); +} + +//----------------------------------------------------------------------------- +// nsHttpChannel::nsIRequestObserver +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsHttpChannel::OnStartRequest(nsIRequest* request) { + nsresult rv; + + MOZ_ASSERT(LoadRequestObserversCalled()); + + AUTO_PROFILER_LABEL("nsHttpChannel::OnStartRequest", NETWORK); + + if (!(mCanceled || NS_FAILED(mStatus)) && + !WRONG_RACING_RESPONSE_SOURCE(request)) { + // capture the request's status, so our consumers will know ASAP of any + // connection failures, etc - bug 93581 + nsresult status; + request->GetStatus(&status); + mStatus = status; + } + + LOG(("nsHttpChannel::OnStartRequest [this=%p request=%p status=%" PRIx32 + "]\n", + this, request, static_cast(static_cast(mStatus)))); + + Telemetry::Accumulate(Telemetry::HTTP_CHANNEL_ONSTART_SUCCESS, + NS_SUCCEEDED(mStatus)); + + if (mTransaction) { + Telemetry::Accumulate( + Telemetry::HTTP3_CHANNEL_ONSTART_SUCCESS, + (mTransaction->IsHttp3Used()) ? "http3"_ns : "no_http3"_ns, + NS_SUCCEEDED(mStatus)); + } + + if (gTRRService && gTRRService->IsConfirmed()) { + Telemetry::Accumulate(Telemetry::HTTP_CHANNEL_ONSTART_SUCCESS_TRR, + TRRService::AutoDetectedKey(), NS_SUCCEEDED(mStatus)); + } + + if (mRaceCacheWithNetwork) { + LOG( + (" racingNetAndCache - mFirstResponseSource:%d fromCache:%d " + "fromNet:%d\n", + static_cast(mFirstResponseSource), request == mCachePump, + request == mTransactionPump)); + if (mFirstResponseSource == RESPONSE_PENDING) { + // When the cache wins mFirstResponseSource is set to + // RESPONSE_FROM_CACHE earlier in ReadFromCache, so this must be a + // response from the network. + MOZ_ASSERT(request == mTransactionPump); + LOG((" First response from network\n")); + { + // Race condition with OnCacheEntryCheck, which is not limited + // to main thread. + mozilla::MutexAutoLock lock(mRCWNLock); + mFirstResponseSource = RESPONSE_FROM_NETWORK; + mOnStartRequestTimestamp = TimeStamp::Now(); + + // Conditional or byte range header could be added in + // OnCacheEntryCheck. We need to remove them because the + // request might be sent again due to auth retry and we must + // not send these headers without having the entry. + if (mDidReval) { + LOG((" Removing conditional request headers")); + UntieValidationRequest(); + mDidReval = false; + } + if (LoadCachedContentIsPartial()) { + LOG((" Removing byte range request headers")); + UntieByteRangeRequest(); + StoreCachedContentIsPartial(false); + } + } + mAvailableCachedAltDataType.Truncate(); + StoreDeliveringAltData(false); + } else if (WRONG_RACING_RESPONSE_SOURCE(request)) { + LOG((" Early return when racing. This response not needed.")); + return NS_OK; + } + } + + // Make sure things are what we expect them to be... + MOZ_ASSERT(request == mCachePump || request == mTransactionPump, + "Unexpected request"); + + MOZ_ASSERT(mRaceCacheWithNetwork || !(mTransactionPump && mCachePump) || + LoadCachedContentIsPartial() || LoadTransactionReplaced(), + "If we have both pumps, we're racing cache with network, the cache" + " content is partial, or the cache entry was revalidated and " + "OnStopRequest was not called yet for the transaction pump."); + + StoreAfterOnStartRequestBegun(true); + if (mOnStartRequestTimestamp.IsNull()) { + mOnStartRequestTimestamp = TimeStamp::Now(); + } + + Telemetry::Accumulate(Telemetry::HTTP_ONSTART_SUSPEND_TOTAL_TIME, + mSuspendTotalTime); + + if (mTransaction) { + mProxyConnectResponseCode = mTransaction->GetProxyConnectResponseCode(); + if (request == mTransactionPump) { + StoreDataSentToChildProcess(mTransaction->DataSentToChildProcess()); + } + + if (!mSecurityInfo && !mCachePump) { + // grab the security info from the connection object; the transaction + // is guaranteed to own a reference to the connection. + mSecurityInfo = mTransaction->SecurityInfo(); + } + + uint32_t stage = mTransaction->HTTPSSVCReceivedStage(); + if (!LoadHTTPSSVCTelemetryReported()) { + if (stage != HTTPSSVC_NOT_USED) { + Telemetry::Accumulate(Telemetry::DNS_HTTPSSVC_RECORD_RECEIVING_STAGE, + stage); + } + } + + if (HTTPS_RR_IS_USED(stage)) { + Telemetry::Accumulate( + Telemetry::HTTP_CHANNEL_ONSTART_SUCCESS_HTTPS_RR, + LoadEchConfigUsed() ? "echConfig-used"_ns : "echConfig-not-used"_ns, + NS_SUCCEEDED(mStatus)); + } + } + + // don't enter this block if we're reading from the cache... + if (NS_SUCCEEDED(mStatus) && !mCachePump && mTransaction) { + // mTransactionPump doesn't hit OnInputStreamReady and call this until + // all of the response headers have been acquired, so we can take + // ownership of them from the transaction. + mResponseHead = mTransaction->TakeResponseHead(); + mSupportsHTTP3 = mTransaction->GetSupportsHTTP3(); + // the response head may be null if the transaction was cancelled. in + // which case we just need to call OnStartRequest/OnStopRequest. + if (mResponseHead) return ProcessResponse(); + + NS_WARNING("No response head in OnStartRequest"); + } + + // cache file could be deleted on our behalf, it could contain errors or + // it failed to allocate memory, reload from network here. + if (mCacheEntry && mCachePump && RECOVER_FROM_CACHE_FILE_ERROR(mStatus)) { + LOG((" cache file error, reloading from server")); + mCacheEntry->AsyncDoom(nullptr); + rv = + StartRedirectChannelToURI(mURI, nsIChannelEventSink::REDIRECT_INTERNAL); + if (NS_SUCCEEDED(rv)) return NS_OK; + } + + // avoid crashing if mListener happens to be null... + if (!mListener) { + MOZ_ASSERT_UNREACHABLE("mListener is null"); + return NS_OK; + } + + rv = ProcessCrossOriginEmbedderPolicyHeader(); + if (NS_FAILED(rv)) { + mStatus = NS_ERROR_BLOCKED_BY_POLICY; + HandleAsyncAbort(); + return NS_OK; + } + + rv = ProcessCrossOriginResourcePolicyHeader(); + if (NS_FAILED(rv)) { + mStatus = NS_ERROR_DOM_CORP_FAILED; + HandleAsyncAbort(); + return NS_OK; + } + + // before we check for redirects, check if the load should be shifted into a + // new process. + rv = ComputeCrossOriginOpenerPolicyMismatch(); + + if (rv == NS_ERROR_BLOCKED_BY_POLICY) { + // this navigates the doc's browsing context to a network error. + mStatus = NS_ERROR_BLOCKED_BY_POLICY; + HandleAsyncAbort(); + return NS_OK; + } + + // No process change is needed, so continue on to ContinueOnStartRequest1. + return ContinueOnStartRequest1(rv); +} + +nsresult nsHttpChannel::ContinueOnStartRequest1(nsresult result) { + nsresult rv; + + // if process selection failed, cancel this load. + if (NS_FAILED(result) && !mCanceled) { + Cancel(result); + return CallOnStartRequest(); + } + + // before we start any content load, check for redirectTo being called + // this code is executed mainly before we start load from the cache + if (mAPIRedirectToURI && !mCanceled) { + nsAutoCString redirectToSpec; + mAPIRedirectToURI->GetAsciiSpec(redirectToSpec); + LOG((" redirectTo called with uri=%s", redirectToSpec.BeginReading())); + + MOZ_ASSERT(!LoadOnStartRequestCalled()); + + nsCOMPtr redirectTo; + mAPIRedirectToURI.swap(redirectTo); + + PushRedirectAsyncFunc(&nsHttpChannel::ContinueOnStartRequest2); + rv = StartRedirectChannelToURI(redirectTo, + nsIChannelEventSink::REDIRECT_TEMPORARY); + if (NS_SUCCEEDED(rv)) { + return NS_OK; + } + PopRedirectAsyncFunc(&nsHttpChannel::ContinueOnStartRequest2); + } + + // Hack: ContinueOnStartRequest2 uses NS_OK to detect successful redirects, + // so we distinguish this codepath (a non-redirect that's processing + // normally) by passing in a bogus error code. + return ContinueOnStartRequest2(NS_BINDING_FAILED); +} + +nsresult nsHttpChannel::ContinueOnStartRequest2(nsresult result) { + if (NS_SUCCEEDED(result)) { + // Redirect has passed through, we don't want to go on with this + // channel. It will now be canceled by the redirect handling code + // that called this function. + return NS_OK; + } + + // on proxy errors, try to failover + if (mConnectionInfo->ProxyInfo() && + (mStatus == NS_ERROR_PROXY_CONNECTION_REFUSED || + mStatus == NS_ERROR_UNKNOWN_PROXY_HOST || + mStatus == NS_ERROR_NET_TIMEOUT)) { + PushRedirectAsyncFunc(&nsHttpChannel::ContinueOnStartRequest3); + if (NS_SUCCEEDED(ProxyFailover())) { + mProxyConnectResponseCode = 0; + return NS_OK; + } + PopRedirectAsyncFunc(&nsHttpChannel::ContinueOnStartRequest3); + } + + // Hack: ContinueOnStartRequest3 uses NS_OK to detect successful redirects, + // so we distinguish this codepath (a non-redirect that's processing + // normally) by passing in a bogus error code. + return ContinueOnStartRequest3(NS_BINDING_FAILED); +} + +nsresult nsHttpChannel::ContinueOnStartRequest3(nsresult result) { + if (NS_SUCCEEDED(result)) { + // Redirect has passed through, we don't want to go on with this + // channel. It will now be canceled by the redirect handling code + // that called this function. + return NS_OK; + } + + // on other request errors, try to fall back + if (NS_FAILED(mStatus)) { + PushRedirectAsyncFunc(&nsHttpChannel::ContinueOnStartRequest4); + bool waitingForRedirectCallback; + Unused << ProcessFallback(&waitingForRedirectCallback); + if (waitingForRedirectCallback) return NS_OK; + PopRedirectAsyncFunc(&nsHttpChannel::ContinueOnStartRequest4); + } + + return ContinueOnStartRequest4(NS_OK); +} + +nsresult nsHttpChannel::ContinueOnStartRequest4(nsresult result) { + LOG(("nsHttpChannel::ContinueOnStartRequest4 [this=%p]", this)); + + if (LoadFallingBack()) return NS_OK; + + if (NS_SUCCEEDED(mStatus) && mResponseHead && mAuthProvider) { + uint32_t httpStatus = mResponseHead->Status(); + if (httpStatus != 401 && httpStatus != 407) { + nsresult rv = mAuthProvider->CheckForSuperfluousAuth(); + if (NS_FAILED(rv)) { + LOG((" CheckForSuperfluousAuth failed (%08x)", + static_cast(rv))); + } + } + } + + return CallOnStartRequest(); +} + +NS_IMETHODIMP +nsHttpChannel::OnStopRequest(nsIRequest* request, nsresult status) { + AUTO_PROFILER_LABEL("nsHttpChannel::OnStopRequest", NETWORK); + + LOG(("nsHttpChannel::OnStopRequest [this=%p request=%p status=%" PRIx32 "]\n", + this, request, static_cast(status))); + + LOG(("OnStopRequest %p requestFromCache: %d mFirstResponseSource: %d\n", this, + request == mCachePump, static_cast(mFirstResponseSource))); + + MOZ_ASSERT(NS_IsMainThread(), + "OnStopRequest should only be called from the main thread"); + + if (WRONG_RACING_RESPONSE_SOURCE(request)) { + return NS_OK; + } + + // If this load failed because of a security error, it may be because we + // are in a captive portal - trigger an async check to make sure. + int32_t nsprError = -1 * NS_ERROR_GET_CODE(status); + if (mozilla::psm::IsNSSErrorCode(nsprError) && IsHTTPS()) { + gIOService->RecheckCaptivePortal(); + } + + if (LoadTimingEnabled() && request == mCachePump) { + mCacheReadEnd = TimeStamp::Now(); + + ReportNetVSCacheTelemetry(); + } + + // allow content to be cached if it was loaded successfully (bug #482935) + bool contentComplete = NS_SUCCEEDED(status); + + // honor the cancelation status even if the underlying transaction + // completed. + if (mCanceled || NS_FAILED(mStatus)) status = mStatus; + + if (LoadCachedContentIsPartial()) { + if (NS_SUCCEEDED(status)) { + // mTransactionPump should be suspended + MOZ_ASSERT(request != mTransactionPump, + "byte-range transaction finished prematurely"); + + if (request == mCachePump) { + bool streamDone; + status = OnDoneReadingPartialCacheEntry(&streamDone); + if (NS_SUCCEEDED(status) && !streamDone) return status; + // otherwise, fall through and fire OnStopRequest... + } else if (request == mTransactionPump) { + MOZ_ASSERT(LoadConcurrentCacheAccess()); + } else + MOZ_ASSERT_UNREACHABLE("unexpected request"); + } + // Do not to leave the transaction in a suspended state in error cases. + if (NS_FAILED(status) && mTransaction) { + nsresult rv = gHttpHandler->CancelTransaction(mTransaction, status); + if (NS_FAILED(rv)) { + LOG((" CancelTransaction failed (%08x)", static_cast(rv))); + } + } + } + + nsCOMPtr conv = do_QueryInterface(mCompressListener); + if (conv) { + conv->GetDecodedDataLength(&mDecodedBodySize); + } + + bool isFromNet = request == mTransactionPump; + + if (mTransaction) { + // determine if we should call DoAuthRetry + bool authRetry = mAuthRetryPending && NS_SUCCEEDED(status); + StoreStronglyFramed(mTransaction->ResponseIsComplete()); + LOG(("nsHttpChannel %p has a strongly framed transaction: %d", this, + LoadStronglyFramed())); + + // Save the reference of |mTransaction| to |transactionWithStickyConn| + // when it has a sticky connection. + // In the case we need to retry an authentication request, we need to + // reuse the connection of |transactionWithStickyConn|. + RefPtr transactionWithStickyConn; + if (mCaps & NS_HTTP_STICKY_CONNECTION || + mTransaction->HasStickyConnection()) { + transactionWithStickyConn = mTransaction; + // Make sure we use the updated caps and connection info from transaction. + // We read these values when the transaction is already closed, so there + // should be no race. + if (mTransaction->Http2Disabled()) { + mCaps |= NS_HTTP_DISALLOW_SPDY; + } + if (mTransaction->Http3Disabled()) { + mCaps |= NS_HTTP_DISALLOW_HTTP3; + } + mConnectionInfo = mTransaction->GetConnInfo(); + LOG((" transaction %p has sticky connection", + transactionWithStickyConn.get())); + } + + // this code relies on the code in nsHttpTransaction::Close, which + // tests for NS_HTTP_STICKY_CONNECTION to determine whether or not to + // keep the connection around after the transaction is finished. + // + LOG((" mAuthRetryPending=%d, status=%" PRIx32 ", sticky conn cap=%d", + static_cast(mAuthRetryPending), static_cast(status), + mCaps & NS_HTTP_STICKY_CONNECTION)); + // We must check caps for stickinness also on the transaction because it + // might have been updated by the transaction itself during inspection of + // the reposnse headers yet on the socket thread (found connection based + // auth schema). + + if ((mAuthRetryPending || NS_FAILED(status)) && transactionWithStickyConn) { + if (NS_FAILED(status)) { + // Close (don't reuse) the sticky connection if it's in the middle + // of an NTLM negotiation and this channel has been cancelled. + // There are proxy servers known to get confused when we send + // a new request over such a half-stated connection. + if (!LoadAuthConnectionRestartable()) { + LOG((" not reusing a half-authenticated sticky connection")); + transactionWithStickyConn->DontReuseConnection(); + } + } + } + + if (mCaps & NS_HTTP_STICKY_CONNECTION) { + mTransaction->SetH2WSConnRefTaken(); + } + + mTransferSize = mTransaction->GetTransferSize(); + mRequestSize = mTransaction->GetRequestSize(); + + // If we are using the transaction to serve content, we also save the + // time since async open in the cache entry so we can compare telemetry + // between cache and net response. + // Do not store the time of conditional requests because even if we + // fetch the data from the server, the time includes loading of the old + // cache entry which would skew the network load time. + if (request == mTransactionPump && mCacheEntry && !mDidReval && + !LoadCustomConditionalRequest() && !mAsyncOpenTime.IsNull() && + !mOnStartRequestTimestamp.IsNull()) { + uint64_t onStartTime = + (mOnStartRequestTimestamp - mAsyncOpenTime).ToMilliseconds(); + uint64_t onStopTime = + (TimeStamp::Now() - mAsyncOpenTime).ToMilliseconds(); + Unused << mCacheEntry->SetNetworkTimes(onStartTime, onStopTime); + } + + mResponseTrailers = mTransaction->TakeResponseTrailers(); + + // at this point, we're done with the transaction + mTransactionTimings = mTransaction->Timings(); + mTransaction = nullptr; + mTransactionPump = nullptr; + + // We no longer need the dns prefetch object + if (mDNSPrefetch && mDNSPrefetch->TimingsValid() && + !mTransactionTimings.requestStart.IsNull() && + !mTransactionTimings.connectStart.IsNull() && + mDNSPrefetch->EndTimestamp() <= mTransactionTimings.connectStart) { + // We only need the domainLookup timestamps when not using a + // persistent connection, meaning if the endTimestamp < connectStart + mTransactionTimings.domainLookupStart = mDNSPrefetch->StartTimestamp(); + mTransactionTimings.domainLookupEnd = mDNSPrefetch->EndTimestamp(); + } + mDNSPrefetch = nullptr; + + // handle auth retry... + if (authRetry) { + mAuthRetryPending = false; + auto continueOSR = [authRetry, isFromNet, contentComplete, + transactionWithStickyConn](auto* self, + nsresult aStatus) { + return self->ContinueOnStopRequestAfterAuthRetry( + aStatus, authRetry, isFromNet, contentComplete, + transactionWithStickyConn); + }; + status = DoAuthRetry(transactionWithStickyConn, continueOSR); + if (NS_SUCCEEDED(status)) { + return NS_OK; + } + } + return ContinueOnStopRequestAfterAuthRetry(status, authRetry, isFromNet, + contentComplete, + transactionWithStickyConn); + } + + return ContinueOnStopRequest(status, isFromNet, contentComplete); +} + +nsresult nsHttpChannel::ContinueOnStopRequestAfterAuthRetry( + nsresult aStatus, bool aAuthRetry, bool aIsFromNet, bool aContentComplete, + HttpTransactionShell* aTransWithStickyConn) { + LOG( + ("nsHttpChannel::ContinueOnStopRequestAfterAuthRetry " + "[this=%p, aStatus=%" PRIx32 + " aAuthRetry=%d, aIsFromNet=%d, aTransWithStickyConn=%p]\n", + this, static_cast(aStatus), aAuthRetry, aIsFromNet, + aTransWithStickyConn)); + + if (aAuthRetry && NS_SUCCEEDED(aStatus)) { + return NS_OK; + } + + // If DoAuthRetry failed, or if we have been cancelled since showing + // the auth. dialog, then we need to send OnStartRequest now + if (aAuthRetry || (mAuthRetryPending && NS_FAILED(aStatus))) { + MOZ_ASSERT(NS_FAILED(aStatus), "should have a failure code here"); + // NOTE: since we have a failure status, we can ignore the return + // value from onStartRequest. + LOG((" calling mListener->OnStartRequest [this=%p, listener=%p]\n", this, + mListener.get())); + if (mListener) { + MOZ_ASSERT(!LoadOnStartRequestCalled(), + "We should not call OnStartRequest twice."); + nsCOMPtr listener(mListener); + StoreOnStartRequestCalled(true); + listener->OnStartRequest(this); + } else { + StoreOnStartRequestCalled(true); + NS_WARNING("OnStartRequest skipped because of null listener"); + } + } + + // if this transaction has been replaced, then bail. + if (LoadTransactionReplaced()) { + LOG(("Transaction replaced\n")); + // This was just the network check for a 304 response. + mFirstResponseSource = RESPONSE_PENDING; + return NS_OK; + } + + bool upgradeWebsocket = mUpgradeProtocolCallback && aTransWithStickyConn && + mResponseHead && + ((mResponseHead->Status() == 101 && + mResponseHead->Version() == HttpVersion::v1_1) || + (mResponseHead->Status() == 200 && + mResponseHead->Version() == HttpVersion::v2_0)); + + bool upgradeConnect = mUpgradeProtocolCallback && aTransWithStickyConn && + (mCaps & NS_HTTP_CONNECT_ONLY) && mResponseHead && + mResponseHead->Status() == 200; + + if (upgradeWebsocket || upgradeConnect) { + nsresult rv = gHttpHandler->CompleteUpgrade(aTransWithStickyConn, + mUpgradeProtocolCallback); + if (NS_FAILED(rv)) { + LOG((" CompleteUpgrade failed with %" PRIx32, + static_cast(rv))); + + // This ensures that WebSocketChannel::OnStopRequest will be + // called with an error so the session is properly aborted. + aStatus = rv; + } + } + + return ContinueOnStopRequest(aStatus, aIsFromNet, aContentComplete); +} + +nsresult nsHttpChannel::ContinueOnStopRequest(nsresult aStatus, bool aIsFromNet, + bool aContentComplete) { + LOG( + ("nsHttpChannel::ContinueOnStopRequest " + "[this=%p aStatus=%" PRIx32 ", aIsFromNet=%d]\n", + this, static_cast(aStatus), aIsFromNet)); + + // HTTP_CHANNEL_DISPOSITION TELEMETRY + enum ChannelDisposition { + kHttpCanceled = 0, + kHttpDisk = 1, + kHttpNetOK = 2, + kHttpNetEarlyFail = 3, + kHttpNetLateFail = 4, + kHttpsCanceled = 8, + kHttpsDisk = 9, + kHttpsNetOK = 10, + kHttpsNetEarlyFail = 11, + kHttpsNetLateFail = 12 + } chanDisposition = kHttpCanceled; + // HTTP_CHANNEL_DISPOSITION_UPGRADE TELEMETRY + Telemetry::LABELS_HTTP_CHANNEL_DISPOSITION_UPGRADE upgradeChanDisposition = + Telemetry::LABELS_HTTP_CHANNEL_DISPOSITION_UPGRADE::cancel; + + // HTTP 0.9 is more likely to be an error than really 0.9, so count it that + // way + if (mCanceled) { + chanDisposition = kHttpCanceled; + upgradeChanDisposition = + Telemetry::LABELS_HTTP_CHANNEL_DISPOSITION_UPGRADE::cancel; + } else if (!LoadUsedNetwork() || + (mRaceCacheWithNetwork && + mFirstResponseSource == RESPONSE_FROM_CACHE)) { + chanDisposition = kHttpDisk; + upgradeChanDisposition = + Telemetry::LABELS_HTTP_CHANNEL_DISPOSITION_UPGRADE::disk; + } else if (NS_SUCCEEDED(aStatus) && mResponseHead && + mResponseHead->Version() != HttpVersion::v0_9) { + chanDisposition = kHttpNetOK; + upgradeChanDisposition = + Telemetry::LABELS_HTTP_CHANNEL_DISPOSITION_UPGRADE::netOk; + } else if (!mTransferSize) { + chanDisposition = kHttpNetEarlyFail; + upgradeChanDisposition = + Telemetry::LABELS_HTTP_CHANNEL_DISPOSITION_UPGRADE::netEarlyFail; + } else { + chanDisposition = kHttpNetLateFail; + upgradeChanDisposition = + Telemetry::LABELS_HTTP_CHANNEL_DISPOSITION_UPGRADE::netLateFail; + } + // Browser upgrading only happens on HTTPS pages for mixed passive content + // when upgrading is enabled. + nsCString upgradeKey; + if (IsHTTPS()) { + // Browser upgrading is disabled and the content is already HTTPS + upgradeKey = "disabledNoReason"_ns; + // Checks "security.mixed_content.upgrade_display_content" is true + if (StaticPrefs::security_mixed_content_upgrade_display_content()) { + if (mLoadInfo->GetBrowserUpgradeInsecureRequests()) { + // HTTP content the browser has upgraded to HTTPS + upgradeKey = "enabledUpgrade"_ns; + } else { + // Content wasn't upgraded but is already HTTPS + upgradeKey = "enabledNoReason"_ns; + } + } + // shift http to https disposition enums + chanDisposition = + static_cast(chanDisposition + kHttpsCanceled); + } else if (mLoadInfo->GetBrowserWouldUpgradeInsecureRequests()) { + // HTTP content the browser would upgrade to HTTPS if upgrading was + // enabled + upgradeKey = "disabledUpgrade"_ns; + } else { + // HTTP content that wouldn't upgrade + upgradeKey = StaticPrefs::security_mixed_content_upgrade_display_content() + ? "enabledWont"_ns + : "disabledWont"_ns; + } + Telemetry::AccumulateCategoricalKeyed(upgradeKey, upgradeChanDisposition); + LOG((" nsHttpChannel::OnStopRequest ChannelDisposition %d\n", + chanDisposition)); + Telemetry::Accumulate(Telemetry::HTTP_CHANNEL_DISPOSITION, chanDisposition); + + // If we upgraded because of 'security.mixed_content.upgrade_display_content', + // we collect telemetry if the upgrade was a success. + if (mLoadInfo->GetBrowserDidUpgradeInsecureRequests()) { + bool success = NS_SUCCEEDED(aStatus); + nsContentPolicyType type; + mLoadInfo->GetInternalContentPolicyType(&type); + + switch (type) { + case nsIContentPolicy::TYPE_INTERNAL_IMAGE: + case nsIContentPolicy::TYPE_INTERNAL_IMAGE_PRELOAD: + case nsIContentPolicy::TYPE_INTERNAL_IMAGE_FAVICON: + Telemetry::AccumulateCategorical( + success + ? Telemetry::LABELS_MIXED_CONTENT_UPGRADE_SUCCESS::ImageSuccess + : Telemetry::LABELS_MIXED_CONTENT_UPGRADE_SUCCESS::ImageFailed); + break; + + case nsIContentPolicy::TYPE_INTERNAL_VIDEO: + Telemetry::AccumulateCategorical( + success + ? Telemetry::LABELS_MIXED_CONTENT_UPGRADE_SUCCESS::VideoSuccess + : Telemetry::LABELS_MIXED_CONTENT_UPGRADE_SUCCESS::VideoFailed); + break; + + case nsIContentPolicy::TYPE_INTERNAL_AUDIO: + case nsIContentPolicy::TYPE_INTERNAL_TRACK: + Telemetry::AccumulateCategorical( + success + ? Telemetry::LABELS_MIXED_CONTENT_UPGRADE_SUCCESS::AudioSuccess + : Telemetry::LABELS_MIXED_CONTENT_UPGRADE_SUCCESS::AudioFailed); + break; + + default: + // upgrade_display_content only upgrades + // audio, video and images. + MOZ_ASSERT(false, "Unexpected content type."); + } + } + + // if needed, check cache entry has all data we expect + if (mCacheEntry && mCachePump && LoadConcurrentCacheAccess() && + aContentComplete) { + int64_t size, contentLength; + nsresult rv = CheckPartial(mCacheEntry, &size, &contentLength); + if (NS_SUCCEEDED(rv)) { + if (size == int64_t(-1)) { + // mayhemer TODO - we have to restart read from cache here at the size + // offset + MOZ_ASSERT(false); + LOG( + (" cache entry write is still in progress, but we just " + "finished reading the cache entry")); + } else if (contentLength != int64_t(-1) && contentLength != size) { + LOG((" concurrent cache entry write has been interrupted")); + mCachedResponseHead = std::move(mResponseHead); + // Ignore zero partial length because we also want to resume when + // no data at all has been read from the cache. + rv = MaybeSetupByteRangeRequest(size, contentLength, true); + if (NS_SUCCEEDED(rv) && LoadIsPartialRequest()) { + // Prevent read from cache again + mCachedContentIsValid = false; + StoreCachedContentIsPartial(1); + + // Perform the range request + rv = ContinueConnect(); + if (NS_SUCCEEDED(rv)) { + LOG((" performing range request")); + mCachePump = nullptr; + return NS_OK; + } + LOG((" but range request perform failed 0x%08" PRIx32, + static_cast(rv))); + aStatus = NS_ERROR_NET_INTERRUPT; + } else { + LOG((" but range request setup failed rv=0x%08" PRIx32 + ", failing load", + static_cast(rv))); + } + } + } + } + + StoreIsPending(false); + mStatus = aStatus; + + // perform any final cache operations before we close the cache entry. + if (mCacheEntry && LoadRequestTimeInitialized()) { + bool writeAccess; + // New implementation just returns value of the !LoadCacheEntryIsReadOnly() + // flag passed in. Old implementation checks on nsICache::ACCESS_WRITE + // flag. + mCacheEntry->HasWriteAccess(!LoadCacheEntryIsReadOnly(), &writeAccess); + if (writeAccess) { + nsresult rv = FinalizeCacheEntry(); + if (NS_FAILED(rv)) { + LOG(("FinalizeCacheEntry failed (%08x)", static_cast(rv))); + } + } + } + + ReportRcwnStats(aIsFromNet); + + // Register entry to the PerformanceStorage resource timing + MaybeReportTimingData(); + + MaybeFlushConsoleReports(); + +#ifdef MOZ_GECKO_PROFILER + if (profiler_can_accept_markers() && !mRedirectURI) { + // Don't include this if we already redirected + // These do allocations/frees/etc; avoid if not active + nsAutoCString requestMethod; + GetRequestMethod(requestMethod); + + nsCOMPtr uri; + GetURI(getter_AddRefs(uri)); + int32_t priority = PRIORITY_NORMAL; + GetPriority(&priority); + + nsAutoCString contentType; + if (mResponseHead) { + mResponseHead->ContentType(contentType); + } + profiler_add_network_marker( + uri, requestMethod, priority, mChannelId, NetworkLoadType::LOAD_STOP, + mLastStatusReported, TimeStamp::Now(), mLogicalOffset, + mCacheDisposition, mLoadInfo->GetInnerWindowID(), &mTransactionTimings, + nullptr, std::move(mSource), + Some(nsDependentCString(contentType.get()))); + } +#endif + + if (mListener) { + LOG(("nsHttpChannel %p calling OnStopRequest\n", this)); + MOZ_ASSERT(LoadOnStartRequestCalled(), + "OnStartRequest should be called before OnStopRequest"); + MOZ_ASSERT(!LoadOnStopRequestCalled(), + "We should not call OnStopRequest twice"); + StoreOnStopRequestCalled(true); + mListener->OnStopRequest(this, aStatus); + } + StoreOnStopRequestCalled(true); + + // The prefetch needs to be released on the main thread + mDNSPrefetch = nullptr; + + // notify "http-on-stop-connect" observers + gHttpHandler->OnStopRequest(this); + + RemoveAsNonTailRequest(); + + // If a preferred alt-data type was set, this signals the consumer is + // interested in reading and/or writing the alt-data representation. + // We need to hold a reference to the cache entry in case the listener calls + // openAlternativeOutputStream() after CloseCacheEntry() clears mCacheEntry. + if (!mPreferredCachedAltDataTypes.IsEmpty()) { + mAltDataCacheEntry = mCacheEntry; + } + + CloseCacheEntry(!aContentComplete); + + if (mOfflineCacheEntry) CloseOfflineCacheEntry(); + + if (mLoadGroup) { + mLoadGroup->RemoveRequest(this, nullptr, aStatus); + } + + // We don't need this info anymore + CleanRedirectCacheChainIfNecessary(); + + ReleaseListeners(); + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsHttpChannel::nsIStreamListener +//----------------------------------------------------------------------------- + +class OnTransportStatusAsyncEvent : public Runnable { + public: + OnTransportStatusAsyncEvent(nsITransportEventSink* aEventSink, + nsresult aTransportStatus, int64_t aProgress, + int64_t aProgressMax) + : Runnable("net::OnTransportStatusAsyncEvent"), + mEventSink(aEventSink), + mTransportStatus(aTransportStatus), + mProgress(aProgress), + mProgressMax(aProgressMax) { + MOZ_ASSERT(!NS_IsMainThread(), "Shouldn't be created on main thread"); + } + + NS_IMETHOD Run() override { + MOZ_ASSERT(NS_IsMainThread(), "Should run on main thread"); + if (mEventSink) { + mEventSink->OnTransportStatus(nullptr, mTransportStatus, mProgress, + mProgressMax); + } + return NS_OK; + } + + private: + nsCOMPtr mEventSink; + nsresult mTransportStatus; + int64_t mProgress; + int64_t mProgressMax; +}; + +NS_IMETHODIMP +nsHttpChannel::OnDataAvailable(nsIRequest* request, nsIInputStream* input, + uint64_t offset, uint32_t count) { + nsresult rv; + AUTO_PROFILER_LABEL("nsHttpChannel::OnDataAvailable", NETWORK); + + LOG(("nsHttpChannel::OnDataAvailable [this=%p request=%p offset=%" PRIu64 + " count=%" PRIu32 "]\n", + this, request, offset, count)); + + LOG((" requestFromCache: %d mFirstResponseSource: %d\n", + request == mCachePump, static_cast(mFirstResponseSource))); + + // don't send out OnDataAvailable notifications if we've been canceled. + if (mCanceled) return mStatus; + + if (mAuthRetryPending || WRONG_RACING_RESPONSE_SOURCE(request) || + (request == mTransactionPump && LoadTransactionReplaced())) { + uint32_t n; + return input->ReadSegments(NS_DiscardSegment, nullptr, count, &n); + } + + MOZ_ASSERT(mResponseHead, "No response head in ODA!!"); + + MOZ_ASSERT(!(LoadCachedContentIsPartial() && (request == mTransactionPump)), + "transaction pump not suspended"); + + mIsReadingFromCache = (request == mCachePump); + + if (mListener) { + // + // synthesize transport progress event. we do this here since we want + // to delay OnProgress events until we start streaming data. this is + // crucially important since it impacts the lock icon (see bug 240053). + // + nsresult transportStatus; + if (request == mCachePump) + transportStatus = NS_NET_STATUS_READING; + else + transportStatus = NS_NET_STATUS_RECEIVING_FROM; + + // mResponseHead may reference new or cached headers, but either way it + // holds our best estimate of the total content length. Even in the case + // of a byte range request, the content length stored in the cached + // response headers is what we want to use here. + + int64_t progressMax = -1; + rv = GetContentLength(&progressMax); + if (NS_FAILED(rv)) { + NS_WARNING("GetContentLength failed"); + } + int64_t progress = mLogicalOffset + count; + + if ((progress > progressMax) && (progressMax != -1)) { + NS_WARNING( + "unexpected progress values - " + "is server exceeding content length?"); + } + + // make sure params are in range for js + if (!InScriptableRange(progressMax)) { + progressMax = -1; + } + + if (!InScriptableRange(progress)) { + progress = -1; + } + + if (NS_IsMainThread()) { + OnTransportStatus(nullptr, transportStatus, progress, progressMax); + } else { + rv = NS_DispatchToMainThread(new OnTransportStatusAsyncEvent( + this, transportStatus, progress, progressMax)); + NS_ENSURE_SUCCESS(rv, rv); + } + + // + // we have to manually keep the logical offset of the stream up-to-date. + // we cannot depend solely on the offset provided, since we may have + // already streamed some data from another source (see, for example, + // OnDoneReadingPartialCacheEntry). + // + int64_t offsetBefore = 0; + nsCOMPtr seekable = do_QueryInterface(input); + if (seekable && NS_FAILED(seekable->Tell(&offsetBefore))) { + seekable = nullptr; + } + + nsresult rv = + mListener->OnDataAvailable(this, input, mLogicalOffset, count); + if (NS_SUCCEEDED(rv)) { + // by contract mListener must read all of "count" bytes, but + // nsInputStreamPump is tolerant to seekable streams that violate that + // and it will redeliver incompletely read data. So we need to do + // the same thing when updating the progress counter to stay in sync. + int64_t offsetAfter, delta; + if (seekable && NS_SUCCEEDED(seekable->Tell(&offsetAfter))) { + delta = offsetAfter - offsetBefore; + if (delta != count) { + count = delta; + + NS_WARNING("Listener OnDataAvailable contract violation"); + nsCOMPtr consoleService = + do_GetService(NS_CONSOLESERVICE_CONTRACTID); + nsAutoString message(nsLiteralString( + u"http channel Listener OnDataAvailable contract violation")); + if (consoleService) { + consoleService->LogStringMessage(message.get()); + } + } + } + mLogicalOffset += count; + } + + return rv; + } + + return NS_ERROR_ABORT; +} + +//----------------------------------------------------------------------------- +// nsHttpChannel::nsIThreadRetargetableRequest +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsHttpChannel::RetargetDeliveryTo(nsIEventTarget* aNewTarget) { + MOZ_ASSERT(NS_IsMainThread(), "Should be called on main thread only"); + + NS_ENSURE_ARG(aNewTarget); + if (aNewTarget->IsOnCurrentThread()) { + NS_WARNING("Retargeting delivery to same thread"); + return NS_OK; + } + if (!mTransactionPump && !mCachePump) { + LOG(("nsHttpChannel::RetargetDeliveryTo %p %p no pump available\n", this, + aNewTarget)); + return NS_ERROR_NOT_AVAILABLE; + } + + nsresult rv = NS_OK; + // If both cache pump and transaction pump exist, we're probably dealing + // with partially cached content. So, we must be able to retarget both. + nsCOMPtr retargetableCachePump; + nsCOMPtr retargetableTransactionPump; + if (mCachePump) { + retargetableCachePump = do_QueryObject(mCachePump); + // nsInputStreamPump should implement this interface. + MOZ_ASSERT(retargetableCachePump); + rv = retargetableCachePump->RetargetDeliveryTo(aNewTarget); + } + if (NS_SUCCEEDED(rv) && mTransactionPump) { + retargetableTransactionPump = do_QueryObject(mTransactionPump); + // nsInputStreamPump should implement this interface. + MOZ_ASSERT(retargetableTransactionPump); + rv = retargetableTransactionPump->RetargetDeliveryTo(aNewTarget); + + // If retarget fails for transaction pump, we must restore mCachePump. + if (NS_FAILED(rv) && retargetableCachePump) { + nsCOMPtr main = GetMainThreadEventTarget(); + NS_ENSURE_TRUE(main, NS_ERROR_UNEXPECTED); + rv = retargetableCachePump->RetargetDeliveryTo(main); + } + } + return rv; +} + +NS_IMETHODIMP +nsHttpChannel::GetDeliveryTarget(nsIEventTarget** aEventTarget) { + if (mCachePump) { + return mCachePump->GetDeliveryTarget(aEventTarget); + } + if (mTransactionPump) { + nsCOMPtr request = + do_QueryInterface(mTransactionPump); + return request->GetDeliveryTarget(aEventTarget); + } + return NS_ERROR_NOT_AVAILABLE; +} + +//----------------------------------------------------------------------------- +// nsHttpChannel::nsThreadRetargetableStreamListener +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsHttpChannel::CheckListenerChain() { + NS_ASSERTION(NS_IsMainThread(), "Should be on main thread!"); + nsresult rv = NS_OK; + nsCOMPtr retargetableListener = + do_QueryInterface(mListener, &rv); + if (retargetableListener) { + rv = retargetableListener->CheckListenerChain(); + } + return rv; +} + +//----------------------------------------------------------------------------- +// nsHttpChannel::nsITransportEventSink +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsHttpChannel::OnTransportStatus(nsITransport* trans, nsresult status, + int64_t progress, int64_t progressMax) { + MOZ_ASSERT(NS_IsMainThread(), "Should be on main thread only"); + // cache the progress sink so we don't have to query for it each time. + if (!mProgressSink) GetCallback(mProgressSink); + + if (status == NS_NET_STATUS_CONNECTED_TO || + status == NS_NET_STATUS_WAITING_FOR) { + bool isTrr = false; + bool echConfigUsed = false; + if (mTransaction) { + mTransaction->GetNetworkAddresses(mSelfAddr, mPeerAddr, isTrr, + echConfigUsed); + } else { + nsCOMPtr socketTransport = do_QueryInterface(trans); + if (socketTransport) { + socketTransport->GetSelfAddr(&mSelfAddr); + socketTransport->GetPeerAddr(&mPeerAddr); + socketTransport->ResolvedByTRR(&isTrr); + socketTransport->GetEchConfigUsed(&echConfigUsed); + } + } + StoreResolvedByTRR(isTrr); + StoreEchConfigUsed(echConfigUsed); + } + + // block socket status event after Cancel or OnStopRequest has been called. + if (mProgressSink && NS_SUCCEEDED(mStatus) && LoadIsPending()) { + LOG(("sending progress%s notification [this=%p status=%" PRIx32 + " progress=%" PRId64 "/%" PRId64 "]\n", + (mLoadFlags & LOAD_BACKGROUND) ? "" : " and status", this, + static_cast(status), progress, progressMax)); + + nsAutoCString host; + mURI->GetHost(host); + if (!(mLoadFlags & LOAD_BACKGROUND)) { + mProgressSink->OnStatus(this, status, NS_ConvertUTF8toUTF16(host).get()); + } else { + nsCOMPtr parentChannel; + NS_QueryNotificationCallbacks(this, parentChannel); + // If the event sink is |HttpChannelParent|, we have to send status + // events to it even if LOAD_BACKGROUND is set. |HttpChannelParent| + // needs to be aware of whether the status is + // |NS_NET_STATUS_RECEIVING_FROM| or |NS_NET_STATUS_READING|. + // LOAD_BACKGROUND is checked again in |HttpChannelChild|, so the final + // consumer won't get this event. + if (SameCOMIdentity(parentChannel, mProgressSink)) { + mProgressSink->OnStatus(this, status, + NS_ConvertUTF8toUTF16(host).get()); + } + } + + if (progress > 0) { + if ((progress > progressMax) && (progressMax != -1)) { + NS_WARNING("unexpected progress values"); + } + + // Try to get mProgressSink if it was nulled out during OnStatus. + if (!mProgressSink) { + GetCallback(mProgressSink); + } + if (mProgressSink) { + mProgressSink->OnProgress(this, progress, progressMax); + } + } + } + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsHttpChannel::nsICacheInfoChannel +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsHttpChannel::IsFromCache(bool* value) { + if (!LoadIsPending()) return NS_ERROR_NOT_AVAILABLE; + + if (!mRaceCacheWithNetwork) { + // return false if reading a partial cache entry; the data isn't + // entirely from the cache! + *value = (mCachePump || (mLoadFlags & LOAD_ONLY_IF_MODIFIED)) && + mCachedContentIsValid && !LoadCachedContentIsPartial(); + return NS_OK; + } + + // If we are racing network and cache (or skipping the cache) + // we just return the first response source. + *value = mFirstResponseSource == RESPONSE_FROM_CACHE; + + return NS_OK; +} + +NS_IMETHODIMP +nsHttpChannel::GetCacheEntryId(uint64_t* aCacheEntryId) { + bool fromCache = false; + if (NS_FAILED(IsFromCache(&fromCache)) || !fromCache || !mCacheEntry || + NS_FAILED(mCacheEntry->GetCacheEntryId(aCacheEntryId))) { + return NS_ERROR_NOT_AVAILABLE; + } + + return NS_OK; +} + +NS_IMETHODIMP +nsHttpChannel::GetCacheTokenFetchCount(int32_t* _retval) { + NS_ENSURE_ARG_POINTER(_retval); + nsCOMPtr cacheEntry = + mCacheEntry ? mCacheEntry : mAltDataCacheEntry; + if (!cacheEntry) { + return NS_ERROR_NOT_AVAILABLE; + } + + return cacheEntry->GetFetchCount(_retval); +} + +NS_IMETHODIMP +nsHttpChannel::GetCacheTokenExpirationTime(uint32_t* _retval) { + NS_ENSURE_ARG_POINTER(_retval); + if (!mCacheEntry) return NS_ERROR_NOT_AVAILABLE; + + return mCacheEntry->GetExpirationTime(_retval); +} + +NS_IMETHODIMP +nsHttpChannel::SetAllowStaleCacheContent(bool aAllowStaleCacheContent) { + LOG(("nsHttpChannel::SetAllowStaleCacheContent [this=%p, allow=%d]", this, + aAllowStaleCacheContent)); + StoreAllowStaleCacheContent(aAllowStaleCacheContent); + return NS_OK; +} +NS_IMETHODIMP +nsHttpChannel::GetAllowStaleCacheContent(bool* aAllowStaleCacheContent) { + NS_ENSURE_ARG(aAllowStaleCacheContent); + *aAllowStaleCacheContent = LoadAllowStaleCacheContent(); + return NS_OK; +} + +NS_IMETHODIMP +nsHttpChannel::SetPreferCacheLoadOverBypass(bool aPreferCacheLoadOverBypass) { + StorePreferCacheLoadOverBypass(aPreferCacheLoadOverBypass); + return NS_OK; +} +NS_IMETHODIMP +nsHttpChannel::GetPreferCacheLoadOverBypass(bool* aPreferCacheLoadOverBypass) { + NS_ENSURE_ARG(aPreferCacheLoadOverBypass); + *aPreferCacheLoadOverBypass = LoadPreferCacheLoadOverBypass(); + return NS_OK; +} + +NS_IMETHODIMP +nsHttpChannel::PreferAlternativeDataType(const nsACString& aType, + const nsACString& aContentType, + bool aDeliverAltData) { + ENSURE_CALLED_BEFORE_ASYNC_OPEN(); + mPreferredCachedAltDataTypes.AppendElement(PreferredAlternativeDataTypeParams( + nsCString(aType), nsCString(aContentType), aDeliverAltData)); + return NS_OK; +} + +const nsTArray& +nsHttpChannel::PreferredAlternativeDataTypes() { + return mPreferredCachedAltDataTypes; +} + +NS_IMETHODIMP +nsHttpChannel::GetAlternativeDataType(nsACString& aType) { + // must be called during or after OnStartRequest + if (!LoadAfterOnStartRequestBegun()) { + return NS_ERROR_NOT_AVAILABLE; + } + aType = mAvailableCachedAltDataType; + return NS_OK; +} + +NS_IMETHODIMP +nsHttpChannel::OpenAlternativeOutputStream(const nsACString& type, + int64_t predictedSize, + nsIAsyncOutputStream** _retval) { + // OnStopRequest will clear mCacheEntry, but we may use mAltDataCacheEntry + // if the consumer called PreferAlternativeDataType() + nsCOMPtr cacheEntry = + mCacheEntry ? mCacheEntry : mAltDataCacheEntry; + if (!cacheEntry) { + return NS_ERROR_NOT_AVAILABLE; + } + nsresult rv = + cacheEntry->OpenAlternativeOutputStream(type, predictedSize, _retval); + if (NS_SUCCEEDED(rv)) { + // Clear this metadata flag in case it exists. + // The caller of this method may set it again. + cacheEntry->SetMetaDataElement("alt-data-from-child", nullptr); + } + return rv; +} + +NS_IMETHODIMP +nsHttpChannel::GetOriginalInputStream(nsIInputStreamReceiver* aReceiver) { + if (aReceiver == nullptr) { + return NS_ERROR_INVALID_ARG; + } + nsCOMPtr inputStream; + + nsCOMPtr cacheEntry = + mCacheEntry ? mCacheEntry : mAltDataCacheEntry; + if (cacheEntry) { + cacheEntry->OpenInputStream(0, getter_AddRefs(inputStream)); + } + aReceiver->OnInputStreamReady(inputStream); + return NS_OK; +} + +NS_IMETHODIMP +nsHttpChannel::GetAltDataInputStream(const nsACString& aType, + nsIInputStreamReceiver* aReceiver) { + if (aReceiver == nullptr) { + return NS_ERROR_INVALID_ARG; + } + nsCOMPtr inputStream; + + nsCOMPtr cacheEntry = + mCacheEntry ? mCacheEntry : mAltDataCacheEntry; + if (cacheEntry) { + nsresult rv = cacheEntry->OpenAlternativeInputStream( + aType, getter_AddRefs(inputStream)); + NS_ENSURE_SUCCESS(rv, rv); + } + + aReceiver->OnInputStreamReady(inputStream); + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsHttpChannel::nsICachingChannel +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsHttpChannel::IsRacing(bool* aIsRacing) { + if (!LoadAfterOnStartRequestBegun()) { + return NS_ERROR_NOT_AVAILABLE; + } + *aIsRacing = mRaceCacheWithNetwork; + return NS_OK; +} + +NS_IMETHODIMP +nsHttpChannel::GetCacheToken(nsISupports** token) { + NS_ENSURE_ARG_POINTER(token); + if (!mCacheEntry) return NS_ERROR_NOT_AVAILABLE; + return CallQueryInterface(mCacheEntry, token); +} + +NS_IMETHODIMP +nsHttpChannel::SetCacheToken(nsISupports* token) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsHttpChannel::GetOfflineCacheToken(nsISupports** token) { + NS_ENSURE_ARG_POINTER(token); + if (!mOfflineCacheEntry) return NS_ERROR_NOT_AVAILABLE; + return CallQueryInterface(mOfflineCacheEntry, token); +} + +NS_IMETHODIMP +nsHttpChannel::SetOfflineCacheToken(nsISupports* token) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsHttpChannel::GetCacheKey(uint32_t* key) { + NS_ENSURE_ARG_POINTER(key); + + LOG(("nsHttpChannel::GetCacheKey [this=%p]\n", this)); + + *key = mPostID; + return NS_OK; +} + +NS_IMETHODIMP +nsHttpChannel::SetCacheKey(uint32_t key) { + LOG(("nsHttpChannel::SetCacheKey [this=%p key=%u]\n", this, key)); + + ENSURE_CALLED_BEFORE_CONNECT(); + + mPostID = key; + return NS_OK; +} + +NS_IMETHODIMP +nsHttpChannel::GetCacheOnlyMetadata(bool* aOnlyMetadata) { + NS_ENSURE_ARG(aOnlyMetadata); + *aOnlyMetadata = LoadCacheOnlyMetadata(); + return NS_OK; +} + +NS_IMETHODIMP +nsHttpChannel::SetCacheOnlyMetadata(bool aOnlyMetadata) { + LOG(("nsHttpChannel::SetCacheOnlyMetadata [this=%p only-metadata=%d]\n", this, + aOnlyMetadata)); + + ENSURE_CALLED_BEFORE_ASYNC_OPEN(); + + StoreCacheOnlyMetadata(aOnlyMetadata); + if (aOnlyMetadata) { + mLoadFlags |= LOAD_ONLY_IF_MODIFIED; + } + + return NS_OK; +} + +NS_IMETHODIMP +nsHttpChannel::GetPin(bool* aPin) { + NS_ENSURE_ARG(aPin); + *aPin = LoadPinCacheContent(); + return NS_OK; +} + +NS_IMETHODIMP +nsHttpChannel::SetPin(bool aPin) { + LOG(("nsHttpChannel::SetPin [this=%p pin=%d]\n", this, aPin)); + + ENSURE_CALLED_BEFORE_CONNECT(); + + StorePinCacheContent(aPin); + return NS_OK; +} + +NS_IMETHODIMP +nsHttpChannel::ForceCacheEntryValidFor(uint32_t aSecondsToTheFuture) { + if (!mCacheEntry) { + LOG( + ("nsHttpChannel::ForceCacheEntryValidFor found no cache entry " + "for this channel [this=%p].", + this)); + } else { + mCacheEntry->ForceValidFor(aSecondsToTheFuture); + + nsAutoCString key; + mCacheEntry->GetKey(key); + + LOG( + ("nsHttpChannel::ForceCacheEntryValidFor successfully forced valid " + "entry with key %s for %d seconds. [this=%p]", + key.get(), aSecondsToTheFuture, this)); + } + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsHttpChannel::nsIResumableChannel +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsHttpChannel::ResumeAt(uint64_t aStartPos, const nsACString& aEntityID) { + LOG(("nsHttpChannel::ResumeAt [this=%p startPos=%" PRIu64 " id='%s']\n", this, + aStartPos, PromiseFlatCString(aEntityID).get())); + mEntityID = aEntityID; + mStartPos = aStartPos; + StoreResuming(true); + return NS_OK; +} + +nsresult nsHttpChannel::DoAuthRetry( + HttpTransactionShell* aTransWithStickyConn, + const std::function& + aContinueOnStopRequestFunc) { + LOG(("nsHttpChannel::DoAuthRetry [this=%p, aTransWithStickyConn=%p]\n", this, + aTransWithStickyConn)); + + MOZ_ASSERT(!mTransaction, "should not have a transaction"); + + // Note that we don't have to toggle |IsPending| anymore. See the reasons + // below. + // 1. We can't suspend the channel during "http-on-modify-request" + // when |IsPending| is false. + // 2. We don't check |IsPending| in SetRequestHeader now. + + // Reset RequestObserversCalled because we've probably called the request + // observers once already. + StoreRequestObserversCalled(false); + + // fetch cookies, and add them to the request header. + // the server response could have included cookies that must be sent with + // this authentication attempt (bug 84794). + // TODO: save cookies from auth response and send them here (bug 572151). + AddCookiesToRequest(); + + // notify "http-on-modify-request" observers + CallOnModifyRequestObservers(); + + RefPtr trans(aTransWithStickyConn); + return CallOrWaitForResume( + [trans{std::move(trans)}, aContinueOnStopRequestFunc](auto* self) { + return self->ContinueDoAuthRetry(trans, aContinueOnStopRequestFunc); + }); +} + +nsresult nsHttpChannel::ContinueDoAuthRetry( + HttpTransactionShell* aTransWithStickyConn, + const std::function& + aContinueOnStopRequestFunc) { + LOG(("nsHttpChannel::ContinueDoAuthRetry [this=%p]\n", this)); + + StoreIsPending(true); + + // get rid of the old response headers + mResponseHead = nullptr; + + // rewind the upload stream + if (mUploadStream) { + nsCOMPtr seekable = do_QueryInterface(mUploadStream); + if (seekable) { + seekable->Seek(nsISeekableStream::NS_SEEK_SET, 0); + } + } + + // always set sticky connection flag + mCaps |= NS_HTTP_STICKY_CONNECTION; + // and when needed, allow restart regardless the sticky flag + if (LoadAuthConnectionRestartable()) { + LOG((" connection made restartable")); + mCaps |= NS_HTTP_CONNECTION_RESTARTABLE; + StoreAuthConnectionRestartable(false); + } else { + LOG((" connection made non-restartable")); + mCaps &= ~NS_HTTP_CONNECTION_RESTARTABLE; + } + + // notify "http-on-before-connect" observers + gHttpHandler->OnBeforeConnect(this); + + RefPtr trans(aTransWithStickyConn); + return CallOrWaitForResume( + [trans{std::move(trans)}, aContinueOnStopRequestFunc](auto* self) { + nsresult rv = self->DoConnect(trans); + return aContinueOnStopRequestFunc(self, rv); + }); +} + +//----------------------------------------------------------------------------- +// nsHttpChannel::nsIApplicationCacheChannel +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsHttpChannel::GetApplicationCache(nsIApplicationCache** out) { + NS_IF_ADDREF(*out = mApplicationCache); + return NS_OK; +} + +NS_IMETHODIMP +nsHttpChannel::SetApplicationCache(nsIApplicationCache* appCache) { + ENSURE_CALLED_BEFORE_CONNECT(); + + mApplicationCache = appCache; + return NS_OK; +} + +NS_IMETHODIMP +nsHttpChannel::GetApplicationCacheForWrite(nsIApplicationCache** out) { + NS_IF_ADDREF(*out = mApplicationCacheForWrite); + return NS_OK; +} + +NS_IMETHODIMP +nsHttpChannel::SetApplicationCacheForWrite(nsIApplicationCache* appCache) { + ENSURE_CALLED_BEFORE_CONNECT(); + + mApplicationCacheForWrite = appCache; + return NS_OK; +} + +NS_IMETHODIMP +nsHttpChannel::GetLoadedFromApplicationCache( + bool* aLoadedFromApplicationCache) { + *aLoadedFromApplicationCache = LoadLoadedFromApplicationCache(); + return NS_OK; +} + +NS_IMETHODIMP +nsHttpChannel::GetInheritApplicationCache(bool* aInherit) { + *aInherit = LoadInheritApplicationCache(); + return NS_OK; +} + +NS_IMETHODIMP +nsHttpChannel::SetInheritApplicationCache(bool aInherit) { + ENSURE_CALLED_BEFORE_CONNECT(); + + StoreInheritApplicationCache(aInherit); + return NS_OK; +} + +NS_IMETHODIMP +nsHttpChannel::GetChooseApplicationCache(bool* aChoose) { + *aChoose = LoadChooseApplicationCache(); + return NS_OK; +} + +NS_IMETHODIMP +nsHttpChannel::SetChooseApplicationCache(bool aChoose) { + ENSURE_CALLED_BEFORE_CONNECT(); + + StoreChooseApplicationCache(aChoose); + return NS_OK; +} + +nsHttpChannel::OfflineCacheEntryAsForeignMarker* +nsHttpChannel::GetOfflineCacheEntryAsForeignMarker() { + if (!mApplicationCache) return nullptr; + + return new OfflineCacheEntryAsForeignMarker(mApplicationCache, mURI); +} + +nsresult nsHttpChannel::OfflineCacheEntryAsForeignMarker::MarkAsForeign() { + nsresult rv; + + nsCOMPtr noRefURI; + rv = NS_GetURIWithoutRef(mCacheURI, getter_AddRefs(noRefURI)); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString spec; + rv = noRefURI->GetAsciiSpec(spec); + NS_ENSURE_SUCCESS(rv, rv); + + return mApplicationCache->MarkEntry(spec, nsIApplicationCache::ITEM_FOREIGN); +} + +NS_IMETHODIMP +nsHttpChannel::MarkOfflineCacheEntryAsForeign() { + nsresult rv; + + UniquePtr marker( + GetOfflineCacheEntryAsForeignMarker()); + + if (!marker) return NS_ERROR_NOT_AVAILABLE; + + rv = marker->MarkAsForeign(); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsHttpChannel::nsIAsyncVerifyRedirectCallback +//----------------------------------------------------------------------------- + +nsresult nsHttpChannel::WaitForRedirectCallback() { + nsresult rv; + LOG(("nsHttpChannel::WaitForRedirectCallback [this=%p]\n", this)); + + if (mTransactionPump) { + rv = mTransactionPump->Suspend(); + NS_ENSURE_SUCCESS(rv, rv); + } + if (mCachePump) { + rv = mCachePump->Suspend(); + if (NS_FAILED(rv) && mTransactionPump) { +#ifdef DEBUG + nsresult resume = +#endif + mTransactionPump->Resume(); + MOZ_ASSERT(NS_SUCCEEDED(resume), "Failed to resume transaction pump"); + } + NS_ENSURE_SUCCESS(rv, rv); + } + + StoreWaitingForRedirectCallback(true); + return NS_OK; +} + +NS_IMETHODIMP +nsHttpChannel::OnRedirectVerifyCallback(nsresult result) { + LOG( + ("nsHttpChannel::OnRedirectVerifyCallback [this=%p] " + "result=%" PRIx32 " stack=%zu WaitingForRedirectCallback=%u\n", + this, static_cast(result), mRedirectFuncStack.Length(), + LoadWaitingForRedirectCallback())); + MOZ_ASSERT(LoadWaitingForRedirectCallback(), + "Someone forgot to call WaitForRedirectCallback() ?!"); + StoreWaitingForRedirectCallback(false); + + if (mCanceled && NS_SUCCEEDED(result)) result = NS_BINDING_ABORTED; + + for (uint32_t i = mRedirectFuncStack.Length(); i > 0;) { + --i; + // Pop the last function pushed to the stack + nsContinueRedirectionFunc func = mRedirectFuncStack.PopLastElement(); + + // Call it with the result we got from the callback or the deeper + // function call. + result = (this->*func)(result); + + // If a new function has been pushed to the stack and placed us in the + // waiting state, we need to break the chain and wait for the callback + // again. + if (LoadWaitingForRedirectCallback()) break; + } + + if (NS_FAILED(result) && !mCanceled) { + // First, cancel this channel if we are in failure state to set mStatus + // and let it be propagated to pumps. + Cancel(result); + } + + if (!LoadWaitingForRedirectCallback()) { + // We are not waiting for the callback. At this moment we must release + // reference to the redirect target channel, otherwise we may leak. + mRedirectChannel = nullptr; + } + + // We always resume the pumps here. If all functions on stack have been + // called we need OnStopRequest to be triggered, and if we broke out of the + // loop above (and are thus waiting for a new callback) the suspension + // count must be balanced in the pumps. + if (mTransactionPump) mTransactionPump->Resume(); + if (mCachePump) mCachePump->Resume(); + + return result; +} + +void nsHttpChannel::PushRedirectAsyncFunc(nsContinueRedirectionFunc func) { + mRedirectFuncStack.AppendElement(func); +} + +void nsHttpChannel::PopRedirectAsyncFunc(nsContinueRedirectionFunc func) { + MOZ_ASSERT(func == mRedirectFuncStack.LastElement(), + "Trying to pop wrong method from redirect async stack!"); + + mRedirectFuncStack.RemoveLastElement(); +} + +//----------------------------------------------------------------------------- +// nsIDNSListener functions +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsHttpChannel::OnLookupComplete(nsICancelable* request, nsIDNSRecord* rec, + nsresult status) { + MOZ_ASSERT(NS_IsMainThread(), "Expecting DNS callback on main thread."); + + LOG( + ("nsHttpChannel::OnLookupComplete [this=%p] prefetch complete%s: " + "%s status[0x%" PRIx32 "]\n", + this, mCaps & NS_HTTP_REFRESH_DNS ? ", refresh requested" : "", + NS_SUCCEEDED(status) ? "success" : "failure", + static_cast(status))); + + // Unset DNS cache refresh if it was requested, + if (mCaps & NS_HTTP_REFRESH_DNS) { + mCaps &= ~NS_HTTP_REFRESH_DNS; + if (mTransaction) { + mTransaction->SetDNSWasRefreshed(); + } + } + + if (!mDNSBlockingPromise.IsEmpty()) { + if (NS_SUCCEEDED(status)) { + nsCOMPtr record(rec); + mDNSBlockingPromise.Resolve(record, __func__); + } else { + mDNSBlockingPromise.Reject(status, __func__); + } + } + + return NS_OK; +} + +void nsHttpChannel::OnHTTPSRRAvailable(nsIDNSHTTPSSVCRecord* aRecord) { + MOZ_ASSERT(NS_IsMainThread(), "Expecting DNS callback on main thread."); + + LOG(("nsHttpChannel::OnHTTPSRRAvailable [this=%p, aRecord=%p]\n", this, + aRecord)); + + MOZ_ASSERT(!mHTTPSSVCRecord); + nsCOMPtr record = aRecord; + mHTTPSSVCRecord.emplace(std::move(record)); + const nsCOMPtr& httprr = mHTTPSSVCRecord.ref(); + + if (LoadWaitHTTPSSVCRecord()) { + MOZ_ASSERT(mURI->SchemeIs("http")); + + StoreWaitHTTPSSVCRecord(false); + nsresult rv = ContinueOnBeforeConnect(!!httprr, mStatus); + if (NS_FAILED(rv)) { + CloseCacheEntry(false); + Unused << AsyncAbort(rv); + } + } else { + // This channel is not canceled and the transaction is not created. + if (httprr && NS_SUCCEEDED(mStatus) && !mTransaction && + (mFirstResponseSource != RESPONSE_FROM_CACHE)) { + bool hasIPAddress = false; + Unused << httprr->GetHasIPAddresses(&hasIPAddress); + Telemetry::Accumulate(Telemetry::DNS_HTTPSSVC_RECORD_RECEIVING_STAGE, + hasIPAddress + ? HTTPSSVC_WITH_IPHINT_RECEIVED_STAGE_0 + : HTTPSSVC_WITHOUT_IPHINT_RECEIVED_STAGE_0); + StoreHTTPSSVCTelemetryReported(true); + } + } +} + +//----------------------------------------------------------------------------- +// nsHttpChannel internal functions +//----------------------------------------------------------------------------- + +// Creates an URI to the given location using current URI for base and charset +nsresult nsHttpChannel::CreateNewURI(const char* loc, nsIURI** newURI) { + nsCOMPtr ioService; + nsresult rv = gHttpHandler->GetIOService(getter_AddRefs(ioService)); + if (NS_FAILED(rv)) return rv; + + return ioService->NewURI(nsDependentCString(loc), nullptr, mURI, newURI); +} + +void nsHttpChannel::MaybeInvalidateCacheEntryForSubsequentGet() { + // See RFC 2616 section 5.1.1. These are considered valid + // methods which DO NOT invalidate cache-entries for the + // referred resource. POST, PUT and DELETE as well as any + // other method not listed here will potentially invalidate + // any cached copy of the resource + if (mRequestHead.IsGet() || mRequestHead.IsOptions() || + mRequestHead.IsHead() || mRequestHead.IsTrace() || + mRequestHead.IsConnect()) { + return; + } + + // Invalidate the request-uri. + if (LOG_ENABLED()) { + nsAutoCString key; + mURI->GetAsciiSpec(key); + LOG(("MaybeInvalidateCacheEntryForSubsequentGet [this=%p uri=%s]\n", this, + key.get())); + } + + DoInvalidateCacheEntry(mURI); + + // Invalidate Location-header if set + nsAutoCString location; + Unused << mResponseHead->GetHeader(nsHttp::Location, location); + if (!location.IsEmpty()) { + LOG((" Location-header=%s\n", location.get())); + InvalidateCacheEntryForLocation(location.get()); + } + + // Invalidate Content-Location-header if set + Unused << mResponseHead->GetHeader(nsHttp::Content_Location, location); + if (!location.IsEmpty()) { + LOG((" Content-Location-header=%s\n", location.get())); + InvalidateCacheEntryForLocation(location.get()); + } +} + +void nsHttpChannel::InvalidateCacheEntryForLocation(const char* location) { + nsAutoCString tmpCacheKey, tmpSpec; + nsCOMPtr resultingURI; + nsresult rv = CreateNewURI(location, getter_AddRefs(resultingURI)); + if (NS_SUCCEEDED(rv) && HostPartIsTheSame(resultingURI)) { + DoInvalidateCacheEntry(resultingURI); + } else { + LOG((" hosts not matching\n")); + } +} + +void nsHttpChannel::DoInvalidateCacheEntry(nsIURI* aURI) { + // NOTE: + // Following comments 24,32 and 33 in bug #327765, we only care about + // the cache in the protocol-handler, not the application cache. + // The logic below deviates from the original logic in OpenCacheEntry on + // one point by using only READ_ONLY access-policy. I think this is safe. + + nsresult rv; + + nsAutoCString key; + if (LOG_ENABLED()) { + aURI->GetAsciiSpec(key); + } + + LOG(("DoInvalidateCacheEntry [channel=%p key=%s]", this, key.get())); + + nsCOMPtr cacheStorageService( + services::GetCacheStorageService()); + rv = cacheStorageService ? NS_OK : NS_ERROR_FAILURE; + + nsCOMPtr cacheStorage; + if (NS_SUCCEEDED(rv)) { + RefPtr info = GetLoadContextInfo(this); + rv = cacheStorageService->DiskCacheStorage(info, false, + getter_AddRefs(cacheStorage)); + } + + if (NS_SUCCEEDED(rv)) { + rv = cacheStorage->AsyncDoomURI(aURI, ""_ns, nullptr); + } + + LOG(("DoInvalidateCacheEntry [channel=%p key=%s rv=%d]", this, key.get(), + int(rv))); +} + +void nsHttpChannel::AsyncOnExamineCachedResponse() { + gHttpHandler->OnExamineCachedResponse(this); +} + +void nsHttpChannel::UpdateAggregateCallbacks() { + if (!mTransaction) { + return; + } + nsCOMPtr callbacks; + NS_NewNotificationCallbacksAggregation(mCallbacks, mLoadGroup, + GetCurrentEventTarget(), + getter_AddRefs(callbacks)); + mTransaction->SetSecurityCallbacks(callbacks); +} + +NS_IMETHODIMP +nsHttpChannel::SetLoadGroup(nsILoadGroup* aLoadGroup) { + MOZ_ASSERT(NS_IsMainThread(), "Wrong thread."); + + nsresult rv = HttpBaseChannel::SetLoadGroup(aLoadGroup); + if (NS_SUCCEEDED(rv)) { + UpdateAggregateCallbacks(); + } + return rv; +} + +NS_IMETHODIMP +nsHttpChannel::SetNotificationCallbacks(nsIInterfaceRequestor* aCallbacks) { + MOZ_ASSERT(NS_IsMainThread(), "Wrong thread."); + + nsresult rv = HttpBaseChannel::SetNotificationCallbacks(aCallbacks); + if (NS_SUCCEEDED(rv)) { + UpdateAggregateCallbacks(); + } + return rv; +} + +bool nsHttpChannel::AwaitingCacheCallbacks() { + return LoadCacheEntriesToWaitFor() != 0; +} + +void nsHttpChannel::SetPushedStreamTransactionAndId( + HttpTransactionShell* aTransWithPushedStream, uint32_t aPushedStreamId) { + MOZ_ASSERT(!mTransWithPushedStream); + LOG(("nsHttpChannel::SetPushedStreamTransaction [this=%p] trans=%p", this, + aTransWithPushedStream)); + + mTransWithPushedStream = aTransWithPushedStream; + mPushedStreamId = aPushedStreamId; +} + +nsresult nsHttpChannel::OnPush(uint32_t aPushedStreamId, const nsACString& aUrl, + const nsACString& aRequestString, + HttpTransactionShell* aTransaction) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aTransaction); + LOG(("nsHttpChannel::OnPush [this=%p, trans=%p]\n", this, aTransaction)); + + MOZ_ASSERT(mCaps & NS_HTTP_ONPUSH_LISTENER); + nsCOMPtr pushListener; + NS_QueryNotificationCallbacks(mCallbacks, mLoadGroup, + NS_GET_IID(nsIHttpPushListener), + getter_AddRefs(pushListener)); + + if (!pushListener) { + LOG( + ("nsHttpChannel::OnPush [this=%p] notification callbacks do not " + "implement nsIHttpPushListener\n", + this)); + return NS_ERROR_NOT_AVAILABLE; + } + + nsCOMPtr pushResource; + nsresult rv; + + // Create a Channel for the Push Resource + rv = NS_NewURI(getter_AddRefs(pushResource), aUrl); + if (NS_FAILED(rv)) { + return NS_ERROR_FAILURE; + } + + nsCOMPtr ioService; + rv = gHttpHandler->GetIOService(getter_AddRefs(ioService)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr pushChannel; + rv = NS_NewChannelInternal(getter_AddRefs(pushChannel), pushResource, + mLoadInfo, + nullptr, // PerformanceStorage + nullptr, // aLoadGroup + nullptr, // aCallbacks + nsIRequest::LOAD_NORMAL, ioService); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr pushHttpChannel = do_QueryInterface(pushChannel); + MOZ_ASSERT(pushHttpChannel); + if (!pushHttpChannel) { + return NS_ERROR_UNEXPECTED; + } + + RefPtr channel; + CallQueryInterface(pushHttpChannel, channel.StartAssignment()); + MOZ_ASSERT(channel); + if (!channel) { + return NS_ERROR_UNEXPECTED; + } + + // new channel needs mrqeuesthead and headers from pushedStream + channel->mRequestHead.ParseHeaderSet(aRequestString.BeginReading()); + channel->mLoadGroup = mLoadGroup; + channel->mLoadInfo = mLoadInfo; + channel->mCallbacks = mCallbacks; + + // Link the trans with pushed stream and the new channel and call listener + channel->SetPushedStreamTransactionAndId(aTransaction, aPushedStreamId); + rv = pushListener->OnPush(this, pushHttpChannel); + return rv; +} + +// static +bool nsHttpChannel::IsRedirectStatus(uint32_t status) { + // 305 disabled as a security measure (see bug 187996). + return status == 300 || status == 301 || status == 302 || status == 303 || + status == 307 || status == 308; +} + +void nsHttpChannel::SetCouldBeSynthesized() { + MOZ_ASSERT(!BypassServiceWorker()); + StoreResponseCouldBeSynthesized(true); +} + +void nsHttpChannel::SetConnectionInfo(nsHttpConnectionInfo* aCI) { + mConnectionInfo = aCI ? aCI->Clone() : nullptr; +} + +NS_IMETHODIMP +nsHttpChannel::OnPreflightSucceeded() { + MOZ_ASSERT(LoadRequireCORSPreflight(), "Why did a preflight happen?"); + StoreIsCorsPreflightDone(1); + mPreflightChannel = nullptr; + + return ContinueConnect(); +} + +NS_IMETHODIMP +nsHttpChannel::OnPreflightFailed(nsresult aError) { + MOZ_ASSERT(LoadRequireCORSPreflight(), "Why did a preflight happen?"); + StoreIsCorsPreflightDone(1); + mPreflightChannel = nullptr; + + CloseCacheEntry(false); + Unused << AsyncAbort(aError); + return NS_OK; +} + +nsresult nsHttpChannel::CallOrWaitForResume( + const std::function& aFunc) { + if (mCanceled) { + MOZ_ASSERT(NS_FAILED(mStatus)); + return mStatus; + } + + if (mSuspendCount) { + LOG(("Waiting until resume [this=%p]\n", this)); + MOZ_ASSERT(!mCallOnResume); + mCallOnResume = aFunc; + return NS_OK; + } + + return aFunc(this); +} + +void nsHttpChannel::MaybeWarnAboutAppCache() { + // First, accumulate a telemetry ping about appcache usage. + Telemetry::Accumulate(Telemetry::HTTP_OFFLINE_CACHE_DOCUMENT_LOAD, true); + + // Then, issue a deprecation warning. + nsCOMPtr warner; + GetCallback(warner); + if (warner) { + warner->IssueWarning(static_cast(DeprecatedOperations::eAppCache), + false); + } +} + +// Step 10 of HTTP-network-or-cache fetch +void nsHttpChannel::SetOriginHeader() { + if (mRequestHead.IsGet() || mRequestHead.IsHead()) { + return; + } + if (mLoadInfo->TriggeringPrincipal()->IsSystemPrincipal()) { + // Do not set origin header for system principal contexts: + return; + } + nsresult rv; + + nsAutoCString existingHeader; + Unused << mRequestHead.GetHeader(nsHttp::Origin, existingHeader); + if (!existingHeader.IsEmpty()) { + LOG(("nsHttpChannel::SetOriginHeader Origin header already present")); + // In case we already have an Origin header, check with referrerInfo + // if we should "null" it. + Unused << mRequestHead.GetHeader(nsHttp::Origin, existingHeader); + auto const shouldNullifyOriginHeader = + [&existingHeader](nsHttpChannel* self) { + if (self->LoadTaintedOriginFlag()) { + return true; + } + + nsCOMPtr uri; + nsresult rv = NS_NewURI(getter_AddRefs(uri), existingHeader); + if (NS_FAILED(rv)) { + return false; + } + return ReferrerInfo::ShouldSetNullOriginHeader(self, uri); + }; + + if (shouldNullifyOriginHeader(this)) { + LOG(("nsHttpChannel::SetOriginHeader null Origin by Referrer-Policy")); + rv = mRequestHead.SetHeader(nsHttp::Origin, "null"_ns, false /* merge */); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + return; + } + + nsCOMPtr referrer; + auto* basePrin = BasePrincipal::Cast(mLoadInfo->TriggeringPrincipal()); + rv = basePrin->GetURI(getter_AddRefs(referrer)); + if (NS_FAILED(rv)) { + return; + } + + nsAutoCString origin("null"); + + if (StaticPrefs::network_http_sendOriginHeader() != 0 && referrer && + ReferrerInfo::IsReferrerSchemeAllowed(referrer) && + !ReferrerInfo::ShouldSetNullOriginHeader(this, referrer) && + !LoadTaintedOriginFlag()) { + nsContentUtils::GetASCIIOrigin(referrer, origin); + + // Restrict Origin to same-origin loads if requested by user + if (StaticPrefs::network_http_sendOriginHeader() == 1) { + nsAutoCString currentOrigin; + nsContentUtils::GetASCIIOrigin(mURI, currentOrigin); + if (!origin.EqualsIgnoreCase(currentOrigin.get())) { + // Origin header suppressed by user setting + origin.AssignLiteral("null"); + } + } + } + + rv = mRequestHead.SetHeader(nsHttp::Origin, origin, false /* merge */); + MOZ_ASSERT(NS_SUCCEEDED(rv)); +} + +void nsHttpChannel::SetDoNotTrack() { + /** + * 'DoNotTrack' header should be added if 'privacy.donottrackheader.enabled' + * is true or tracking protection is enabled. See bug 1258033. + */ + nsCOMPtr loadContext; + NS_QueryNotificationCallbacks(this, loadContext); + + if ((loadContext && loadContext->UseTrackingProtection()) || + StaticPrefs::privacy_donottrackheader_enabled()) { + DebugOnly rv = + mRequestHead.SetHeader(nsHttp::DoNotTrack, "1"_ns, false); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } +} + +void nsHttpChannel::ReportRcwnStats(bool isFromNet) { + if (!StaticPrefs::network_http_rcwn_enabled()) { + return; + } + + if (isFromNet) { + if (mRaceCacheWithNetwork) { + gIOService->IncrementNetWonRequestNumber(); + Telemetry::Accumulate( + Telemetry::NETWORK_RACE_CACHE_BANDWIDTH_RACE_NETWORK_WIN, + mTransferSize); + if (mRaceDelay) { + AccumulateCategorical( + Telemetry::LABELS_NETWORK_RACE_CACHE_WITH_NETWORK_USAGE_2:: + NetworkDelayedRace); + } else { + AccumulateCategorical( + Telemetry::LABELS_NETWORK_RACE_CACHE_WITH_NETWORK_USAGE_2:: + NetworkRace); + } + } else { + Telemetry::Accumulate(Telemetry::NETWORK_RACE_CACHE_BANDWIDTH_NOT_RACE, + mTransferSize); + AccumulateCategorical( + Telemetry::LABELS_NETWORK_RACE_CACHE_WITH_NETWORK_USAGE_2:: + NetworkNoRace); + } + } else { + if (mRaceCacheWithNetwork || mRaceDelay) { + gIOService->IncrementCacheWonRequestNumber(); + Telemetry::Accumulate( + Telemetry::NETWORK_RACE_CACHE_BANDWIDTH_RACE_CACHE_WIN, + mTransferSize); + if (mRaceDelay) { + AccumulateCategorical( + Telemetry::LABELS_NETWORK_RACE_CACHE_WITH_NETWORK_USAGE_2:: + CacheDelayedRace); + } else { + AccumulateCategorical( + Telemetry::LABELS_NETWORK_RACE_CACHE_WITH_NETWORK_USAGE_2:: + CacheRace); + } + } else { + Telemetry::Accumulate(Telemetry::NETWORK_RACE_CACHE_BANDWIDTH_NOT_RACE, + mTransferSize); + AccumulateCategorical( + Telemetry::LABELS_NETWORK_RACE_CACHE_WITH_NETWORK_USAGE_2:: + CacheNoRace); + } + } + + gIOService->IncrementRequestNumber(); +} + +static const size_t kPositiveBucketNumbers = 34; +static const int64_t kPositiveBucketLevels[kPositiveBucketNumbers] = { + 0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 200, + 300, 400, 500, 600, 700, 800, 900, 1000, 2000, 3000, 4000, 5000, + 6000, 7000, 8000, 9000, 10000, 20000, 30000, 40000, 50000, 60000}; + +/** + * For space efficiency, we collect finer resolution for small difference + * between net and cache time, coarser for larger. + * Bucket #40 for a tie. + * #41 to #50 indicates cache wins by 1ms to 100ms, split equally. + * #51 to #59 indicates cache wins by 101ms to 1000ms. + * #60 to #68 indicates cache wins by 1s to 10s. + * #69 to #73 indicates cache wins by 11s to 60s. + * #74 indicates cache wins by more than 1 minute. + * + * #39 to #30 indicates network wins by 1ms to 100ms, split equally. + * #29 to #21 indicates network wins by 101ms to 1000ms. + * #20 to #12 indicates network wins by 1s to 10s. + * #11 to #7 indicates network wins by 11s to 60s. + * #6 indicates network wins by more than 1 minute. + * + * Other bucket numbers are reserved. + */ +inline int64_t nsHttpChannel::ComputeTelemetryBucketNumber( + int64_t difftime_ms) { + int64_t absBucketIndex = + std::lower_bound(kPositiveBucketLevels, + kPositiveBucketLevels + kPositiveBucketNumbers, + static_cast(mozilla::Abs(difftime_ms))) - + kPositiveBucketLevels; + + return difftime_ms >= 0 ? 40 + absBucketIndex : 40 - absBucketIndex; +} + +void nsHttpChannel::ReportNetVSCacheTelemetry() { + nsresult rv; + if (!mCacheEntry) { + return; + } + + // We only report telemetry if the entry is persistent (on disk) + bool persistent; + rv = mCacheEntry->GetPersistent(&persistent); + if (NS_FAILED(rv) || !persistent) { + return; + } + + uint64_t onStartNetTime = 0; + if (NS_FAILED(mCacheEntry->GetOnStartTime(&onStartNetTime))) { + return; + } + + uint64_t onStopNetTime = 0; + if (NS_FAILED(mCacheEntry->GetOnStopTime(&onStopNetTime))) { + return; + } + + uint64_t onStartCacheTime = + (mOnStartRequestTimestamp - mAsyncOpenTime).ToMilliseconds(); + int64_t onStartDiff = onStartNetTime - onStartCacheTime; + onStartDiff = ComputeTelemetryBucketNumber(onStartDiff); + + uint64_t onStopCacheTime = (mCacheReadEnd - mAsyncOpenTime).ToMilliseconds(); + int64_t onStopDiff = onStopNetTime - onStopCacheTime; + onStopDiff = ComputeTelemetryBucketNumber(onStopDiff); + + if (mDidReval) { + Telemetry::Accumulate(Telemetry::HTTP_NET_VS_CACHE_ONSTART_REVALIDATED_V2, + onStartDiff); + Telemetry::Accumulate(Telemetry::HTTP_NET_VS_CACHE_ONSTOP_REVALIDATED_V2, + onStopDiff); + } else { + Telemetry::Accumulate( + Telemetry::HTTP_NET_VS_CACHE_ONSTART_NOTREVALIDATED_V2, onStartDiff); + Telemetry::Accumulate(Telemetry::HTTP_NET_VS_CACHE_ONSTOP_NOTREVALIDATED_V2, + onStopDiff); + } + + if (mDidReval) { + // We don't report revalidated probes as the data would be skewed. + return; + } + + if (mCacheOpenWithPriority) { + if (mCacheQueueSizeWhenOpen < 5) { + Telemetry::Accumulate( + Telemetry::HTTP_NET_VS_CACHE_ONSTART_QSMALL_HIGHPRI_V2, onStartDiff); + Telemetry::Accumulate( + Telemetry::HTTP_NET_VS_CACHE_ONSTOP_QSMALL_HIGHPRI_V2, onStopDiff); + } else if (mCacheQueueSizeWhenOpen < 10) { + Telemetry::Accumulate( + Telemetry::HTTP_NET_VS_CACHE_ONSTART_QMED_HIGHPRI_V2, onStartDiff); + Telemetry::Accumulate(Telemetry::HTTP_NET_VS_CACHE_ONSTOP_QMED_HIGHPRI_V2, + onStopDiff); + } else { + Telemetry::Accumulate( + Telemetry::HTTP_NET_VS_CACHE_ONSTART_QBIG_HIGHPRI_V2, onStartDiff); + Telemetry::Accumulate(Telemetry::HTTP_NET_VS_CACHE_ONSTOP_QBIG_HIGHPRI_V2, + onStopDiff); + } + } else { // The limits are higher for normal priority cache queues + if (mCacheQueueSizeWhenOpen < 10) { + Telemetry::Accumulate( + Telemetry::HTTP_NET_VS_CACHE_ONSTART_QSMALL_NORMALPRI_V2, + onStartDiff); + Telemetry::Accumulate( + Telemetry::HTTP_NET_VS_CACHE_ONSTOP_QSMALL_NORMALPRI_V2, onStopDiff); + } else if (mCacheQueueSizeWhenOpen < 50) { + Telemetry::Accumulate( + Telemetry::HTTP_NET_VS_CACHE_ONSTART_QMED_NORMALPRI_V2, onStartDiff); + Telemetry::Accumulate( + Telemetry::HTTP_NET_VS_CACHE_ONSTOP_QMED_NORMALPRI_V2, onStopDiff); + } else { + Telemetry::Accumulate( + Telemetry::HTTP_NET_VS_CACHE_ONSTART_QBIG_NORMALPRI_V2, onStartDiff); + Telemetry::Accumulate( + Telemetry::HTTP_NET_VS_CACHE_ONSTOP_QBIG_NORMALPRI_V2, onStopDiff); + } + } + + uint32_t diskStorageSizeK = 0; + rv = mCacheEntry->GetDiskStorageSizeInKB(&diskStorageSizeK); + if (NS_FAILED(rv)) { + return; + } + + // No significant difference was observed between different sizes for + // |onStartDiff| + if (diskStorageSizeK < 256) { + Telemetry::Accumulate(Telemetry::HTTP_NET_VS_CACHE_ONSTOP_SMALL_V2, + onStopDiff); + } else { + Telemetry::Accumulate(Telemetry::HTTP_NET_VS_CACHE_ONSTOP_LARGE_V2, + onStopDiff); + } +} + +NS_IMETHODIMP +nsHttpChannel::Test_delayCacheEntryOpeningBy(int32_t aTimeout) { + MOZ_ASSERT(NS_IsMainThread(), "Must be called on the main thread"); + mCacheOpenDelay = aTimeout; + return NS_OK; +} + +NS_IMETHODIMP +nsHttpChannel::Test_triggerDelayedOpenCacheEntry() { + MOZ_ASSERT(NS_IsMainThread(), "Must be called on the main thread"); + nsresult rv; + if (!mCacheOpenDelay) { + // No delay was set. + return NS_ERROR_NOT_AVAILABLE; + } + if (!mCacheOpenFunc) { + // There should be a runnable. + return NS_ERROR_FAILURE; + } + if (mCacheOpenTimer) { + rv = mCacheOpenTimer->Cancel(); + if (NS_FAILED(rv)) { + return rv; + } + mCacheOpenTimer = nullptr; + } + mCacheOpenDelay = 0; + // Avoid re-entrancy issues by nulling our mCacheOpenFunc before calling it. + std::function cacheOpenFunc = nullptr; + std::swap(cacheOpenFunc, mCacheOpenFunc); + cacheOpenFunc(this); + + return NS_OK; +} + +nsresult nsHttpChannel::TriggerNetworkWithDelay(uint32_t aDelay) { + MOZ_ASSERT(NS_IsMainThread(), "Must be called on the main thread"); + + LOG(("nsHttpChannel::TriggerNetworkWithDelay [this=%p, delay=%u]\n", this, + aDelay)); + + if (mCanceled) { + LOG((" channel was canceled.\n")); + return mStatus; + } + + // If a network request has already gone out, there is no point in + // doing this again. + if (mNetworkTriggered) { + LOG((" network already triggered. Returning.\n")); + return NS_OK; + } + + if (!aDelay) { + // We cannot call TriggerNetwork() directly here, because it would + // cause performance regression in tp6 tests, see bug 1398847. + return NS_DispatchToMainThread( + NewRunnableMethod("net::nsHttpChannel::TriggerNetworkWithDelay", this, + &nsHttpChannel::TriggerNetwork), + NS_DISPATCH_NORMAL); + } + + if (!mNetworkTriggerTimer) { + mNetworkTriggerTimer = NS_NewTimer(); + } + mNetworkTriggerTimer->InitWithCallback(this, aDelay, nsITimer::TYPE_ONE_SHOT); + return NS_OK; +} + +nsresult nsHttpChannel::TriggerNetwork() { + MOZ_ASSERT(NS_IsMainThread(), "Must be called on the main thread"); + + LOG(("nsHttpChannel::TriggerNetwork [this=%p]\n", this)); + + if (mCanceled) { + LOG((" channel was canceled.\n")); + return mStatus; + } + + // If a network request has already gone out, there is no point in + // doing this again. + if (mNetworkTriggered) { + LOG((" network already triggered. Returning.\n")); + return NS_OK; + } + + mNetworkTriggered = true; + if (mNetworkTriggerTimer) { + mNetworkTriggerTimer->Cancel(); + mNetworkTriggerTimer = nullptr; + } + + // If we are waiting for a proxy request, that means we can't trigger + // the next step just yet. We need for mConnectionInfo to be non-null + // before we call ContinueConnect. OnProxyAvailable will trigger + // BeginConnect, and Connect will call ContinueConnect even if it's + // for the cache callbacks. + if (mProxyRequest) { + LOG((" proxy request in progress. Delaying network trigger.\n")); + mWaitingForProxy = true; + return NS_OK; + } + + // If |mCacheOpenFunc| is assigned, we're delaying opening the entry to + // simulate racing. Although cache entry opening hasn't started yet, we're + // actually racing, so we must set mRaceCacheWithNetwork to true now. + if (mCacheOpenFunc) { + mRaceCacheWithNetwork = true; + } else if (AwaitingCacheCallbacks()) { + mRaceCacheWithNetwork = StaticPrefs::network_http_rcwn_enabled(); + } + + LOG((" triggering network\n")); + return ContinueConnect(); +} + +nsresult nsHttpChannel::MaybeRaceCacheWithNetwork() { + nsresult rv; + + nsCOMPtr netLinkSvc = + do_GetService(NS_NETWORK_LINK_SERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + uint32_t linkType; + rv = netLinkSvc->GetLinkType(&linkType); + NS_ENSURE_SUCCESS(rv, rv); + + if (!(linkType == nsINetworkLinkService::LINK_TYPE_UNKNOWN || + linkType == nsINetworkLinkService::LINK_TYPE_ETHERNET || + linkType == nsINetworkLinkService::LINK_TYPE_USB || + linkType == nsINetworkLinkService::LINK_TYPE_WIFI)) { + return NS_OK; + } + + // Don't trigger the network if the load flags say so. + if (mLoadFlags & (LOAD_ONLY_FROM_CACHE | LOAD_NO_NETWORK_IO)) { + return NS_OK; + } + + // We must not race if the channel has a failure status code. + if (NS_FAILED(mStatus)) { + return NS_OK; + } + + // If a CORS Preflight is required we must not race. + if (LoadRequireCORSPreflight() && !LoadIsCorsPreflightDone()) { + return NS_OK; + } + + if (CacheFileUtils::CachePerfStats::IsCacheSlow()) { + // If the cache is slow, trigger the network request immediately. + mRaceDelay = 0; + } else { + // Give cache a headstart of 3 times the average cache entry open time. + mRaceDelay = CacheFileUtils::CachePerfStats::GetAverage( + CacheFileUtils::CachePerfStats::ENTRY_OPEN, true) * + 3; + // We use microseconds in CachePerfStats but we need milliseconds + // for TriggerNetwork. + mRaceDelay /= 1000; + } + + mRaceDelay = clamped( + mRaceDelay, StaticPrefs::network_http_rcwn_min_wait_before_racing_ms(), + StaticPrefs::network_http_rcwn_max_wait_before_racing_ms()); + + MOZ_ASSERT(StaticPrefs::network_http_rcwn_enabled(), + "The pref must be turned on."); + LOG(("nsHttpChannel::MaybeRaceCacheWithNetwork [this=%p, delay=%u]\n", this, + mRaceDelay)); + + return TriggerNetworkWithDelay(mRaceDelay); +} + +NS_IMETHODIMP +nsHttpChannel::Test_triggerNetwork(int32_t aTimeout) { + MOZ_ASSERT(NS_IsMainThread(), "Must be called on the main thread"); + return TriggerNetworkWithDelay(aTimeout); +} + +NS_IMETHODIMP +nsHttpChannel::Notify(nsITimer* aTimer) { + RefPtr self(this); + if (aTimer == mCacheOpenTimer) { + return Test_triggerDelayedOpenCacheEntry(); + } else if (aTimer == mNetworkTriggerTimer) { + return TriggerNetwork(); + } else { + MOZ_CRASH("Unknown timer"); + } + + return NS_OK; +} + +bool nsHttpChannel::EligibleForTailing() { + if (!(mClassOfService & nsIClassOfService::Tail)) { + return false; + } + + if (mClassOfService & + (nsIClassOfService::UrgentStart | nsIClassOfService::Leader | + nsIClassOfService::TailForbidden)) { + return false; + } + + if (mClassOfService & nsIClassOfService::Unblocked && + !(mClassOfService & nsIClassOfService::TailAllowed)) { + return false; + } + + if (IsNavigation()) { + return false; + } + + return true; +} + +bool nsHttpChannel::WaitingForTailUnblock() { + nsresult rv; + + if (!gHttpHandler->IsTailBlockingEnabled()) { + LOG(("nsHttpChannel %p tail-blocking disabled", this)); + return false; + } + + if (!EligibleForTailing()) { + LOG(("nsHttpChannel %p not eligible for tail-blocking", this)); + AddAsNonTailRequest(); + return false; + } + + if (!EnsureRequestContext()) { + LOG(("nsHttpChannel %p no request context", this)); + return false; + } + + LOG(("nsHttpChannel::WaitingForTailUnblock this=%p, rc=%p", this, + mRequestContext.get())); + + bool blocked; + rv = mRequestContext->IsContextTailBlocked(this, &blocked); + if (NS_FAILED(rv)) { + return false; + } + + LOG((" blocked=%d", blocked)); + + return blocked; +} + +//----------------------------------------------------------------------------- +// nsHttpChannel::nsIRequestTailUnblockCallback +//----------------------------------------------------------------------------- + +// Must be implemented in the leaf class because we don't have +// AsyncAbort in HttpBaseChannel. +NS_IMETHODIMP +nsHttpChannel::OnTailUnblock(nsresult rv) { + LOG(("nsHttpChannel::OnTailUnblock this=%p rv=%" PRIx32 " rc=%p", this, + static_cast(rv), mRequestContext.get())); + + MOZ_RELEASE_ASSERT(mOnTailUnblock); + + if (NS_FAILED(mStatus)) { + rv = mStatus; + } + + if (NS_SUCCEEDED(rv)) { + auto callback = mOnTailUnblock; + mOnTailUnblock = nullptr; + rv = (this->*callback)(); + } + + if (NS_FAILED(rv)) { + CloseCacheEntry(false); + return AsyncAbort(rv); + } + + return NS_OK; +} + +void nsHttpChannel::SetWarningReporter( + HttpChannelSecurityWarningReporter* aReporter) { + LOG(("nsHttpChannel [this=%p] SetWarningReporter [%p]", this, aReporter)); + mWarningReporter = aReporter; +} + +HttpChannelSecurityWarningReporter* nsHttpChannel::GetWarningReporter() { + LOG(("nsHttpChannel [this=%p] GetWarningReporter [%p]", this, + mWarningReporter.get())); + return mWarningReporter.get(); +} + +namespace { + +class CopyNonDefaultHeaderVisitor final : public nsIHttpHeaderVisitor { + nsCOMPtr mTarget; + + ~CopyNonDefaultHeaderVisitor() = default; + + NS_IMETHOD + VisitHeader(const nsACString& aHeader, const nsACString& aValue) override { + if (aValue.IsEmpty()) { + return mTarget->SetEmptyRequestHeader(aHeader); + } else { + return mTarget->SetRequestHeader(aHeader, aValue, false /* merge */); + } + } + + public: + explicit CopyNonDefaultHeaderVisitor(nsIHttpChannel* aTarget) + : mTarget(aTarget) { + MOZ_DIAGNOSTIC_ASSERT(mTarget); + } + + NS_DECL_ISUPPORTS +}; + +NS_IMPL_ISUPPORTS(CopyNonDefaultHeaderVisitor, nsIHttpHeaderVisitor) + +} // anonymous namespace + +nsresult nsHttpChannel::RedirectToInterceptedChannel() { + nsCOMPtr controller; + GetCallback(controller); + + RefPtr intercepted = + InterceptedHttpChannel::CreateForInterception( + mChannelCreationTime, mChannelCreationTimestamp, mAsyncOpenTime); + + ExtContentPolicyType type = mLoadInfo->GetExternalContentPolicyType(); + + nsresult rv = intercepted->Init( + mURI, mCaps, static_cast(mProxyInfo.get()), + mProxyResolveFlags, mProxyURI, mChannelId, type); + + nsCOMPtr redirectLoadInfo = + CloneLoadInfoForRedirect(mURI, nsIChannelEventSink::REDIRECT_INTERNAL); + intercepted->SetLoadInfo(redirectLoadInfo); + + rv = SetupReplacementChannel(mURI, intercepted, true, + nsIChannelEventSink::REDIRECT_INTERNAL); + NS_ENSURE_SUCCESS(rv, rv); + + // Some APIs, like fetch(), allow content to set non-standard headers. + // Normally these APIs are responsible for copying these headers across + // redirects. In the e10s parent-side intercept case, though, we currently + // "hide" the internal redirect to the InterceptedHttpChannel. So the + // fetch() API does not have the opportunity to move headers over. + // Therefore, we do it automatically here. + // + // Once child-side interception is removed and the internal redirect no + // longer needs to be "hidden", then this header copying code can be + // removed. + if (ServiceWorkerParentInterceptEnabled()) { + nsCOMPtr visitor = + new CopyNonDefaultHeaderVisitor(intercepted); + rv = VisitNonDefaultRequestHeaders(visitor); + NS_ENSURE_SUCCESS(rv, rv); + } + + mRedirectChannel = intercepted; + + PushRedirectAsyncFunc(&nsHttpChannel::ContinueAsyncRedirectChannelToURI); + + rv = gHttpHandler->AsyncOnChannelRedirect( + this, intercepted, nsIChannelEventSink::REDIRECT_INTERNAL); + + if (NS_SUCCEEDED(rv)) { + rv = WaitForRedirectCallback(); + } + + if (NS_FAILED(rv)) { + AutoRedirectVetoNotifier notifier(this); + + PopRedirectAsyncFunc(&nsHttpChannel::ContinueAsyncRedirectChannelToURI); + } + + return rv; +} + +void nsHttpChannel::ReEvaluateReferrerAfterTrackingStatusIsKnown() { + nsCOMPtr cjs; + if (mLoadInfo) { + Unused << mLoadInfo->GetCookieJarSettings(getter_AddRefs(cjs)); + } + if (!cjs) { + cjs = net::CookieJarSettings::Create(); + } + if (cjs->GetRejectThirdPartyContexts()) { + bool isPrivate = mLoadInfo->GetOriginAttributes().mPrivateBrowsingId > 0; + // If our referrer has been set before, and our referrer policy is unset + // (default policy) if we thought the channel wasn't a third-party + // tracking channel, we may need to set our referrer with referrer policy + // once again to ensure our defaults properly take effect now. + if (mReferrerInfo) { + ReferrerInfo* referrerInfo = + static_cast(mReferrerInfo.get()); + + if (referrerInfo->IsPolicyOverrided() && + referrerInfo->ReferrerPolicy() == + ReferrerInfo::GetDefaultReferrerPolicy(nullptr, nullptr, + isPrivate)) { + nsCOMPtr newReferrerInfo = + referrerInfo->CloneWithNewPolicy( + ReferrerInfo::GetDefaultReferrerPolicy(this, mURI, isPrivate)); + // The arguments passed to SetReferrerInfoInternal here should mirror + // the arguments passed in + // HttpChannelChild::RecvOverrideReferrerInfoDuringBeginConnect(). + SetReferrerInfoInternal(newReferrerInfo, false, true, true); + + nsCOMPtr parentChannel; + NS_QueryNotificationCallbacks(this, parentChannel); + RefPtr httpParent = do_QueryObject(parentChannel); + if (httpParent) { + httpParent->OverrideReferrerInfoDuringBeginConnect(newReferrerInfo); + } + } + } + } +} + +namespace { + +class BackgroundRevalidatingListener : public nsIStreamListener { + NS_DECL_ISUPPORTS + + NS_DECL_NSISTREAMLISTENER + NS_DECL_NSIREQUESTOBSERVER + + private: + virtual ~BackgroundRevalidatingListener() = default; +}; + +NS_IMETHODIMP +BackgroundRevalidatingListener::OnStartRequest(nsIRequest* request) { + return NS_OK; +} + +NS_IMETHODIMP +BackgroundRevalidatingListener::OnDataAvailable(nsIRequest* request, + nsIInputStream* input, + uint64_t offset, + uint32_t count) { + uint32_t bytesRead = 0; + return input->ReadSegments(NS_DiscardSegment, nullptr, count, &bytesRead); +} + +NS_IMETHODIMP +BackgroundRevalidatingListener::OnStopRequest(nsIRequest* request, + nsresult status) { + if (NS_FAILED(status)) { + return status; + } + + nsCOMPtr channel(do_QueryInterface(request)); + if (gHttpHandler) { + gHttpHandler->OnBackgroundRevalidation(channel); + } + return NS_OK; +} + +NS_IMPL_ISUPPORTS(BackgroundRevalidatingListener, nsIStreamListener, + nsIRequestObserver) + +} // namespace + +void nsHttpChannel::PerformBackgroundCacheRevalidation() { + if (!StaticPrefs::network_http_stale_while_revalidate_enabled()) { + return; + } + + // This is a channel doing a revalidation. It shouldn't do it again. + if (mStaleRevalidation) { + return; + } + + LOG(("nsHttpChannel::PerformBackgroundCacheRevalidation %p", this)); + + Unused << NS_DispatchToMainThreadQueue( + NewIdleRunnableMethod( + "nsHttpChannel::PerformBackgroundCacheRevalidation", this, + &nsHttpChannel::PerformBackgroundCacheRevalidationNow), + EventQueuePriority::Idle); +} + +void nsHttpChannel::PerformBackgroundCacheRevalidationNow() { + LOG(("nsHttpChannel::PerformBackgroundCacheRevalidationNow %p", this)); + + MOZ_ASSERT(NS_IsMainThread()); + + nsresult rv; + + nsLoadFlags loadFlags = mLoadFlags | LOAD_ONLY_IF_MODIFIED | VALIDATE_ALWAYS | + LOAD_BACKGROUND | LOAD_BYPASS_SERVICE_WORKER; + + nsCOMPtr validatingChannel; + rv = NS_NewChannelInternal(getter_AddRefs(validatingChannel), mURI, mLoadInfo, + nullptr /* performance storage */, mLoadGroup, + mCallbacks, loadFlags); + if (NS_FAILED(rv)) { + LOG((" failed to created the channel, rv=0x%08x", + static_cast(rv))); + return; + } + + nsCOMPtr priority(do_QueryInterface(validatingChannel)); + if (priority) { + priority->SetPriority(nsISupportsPriority::PRIORITY_LOWEST); + } + + nsCOMPtr cos(do_QueryInterface(validatingChannel)); + if (cos) { + cos->AddClassFlags(nsIClassOfService::Tail); + } + + RefPtr httpChan = do_QueryObject(validatingChannel); + if (httpChan) { + httpChan->mStaleRevalidation = true; + } + + RefPtr listener = + new BackgroundRevalidatingListener(); + rv = validatingChannel->AsyncOpen(listener); + if (NS_FAILED(rv)) { + LOG((" failed to open the channel, rv=0x%08x", static_cast(rv))); + return; + } + + LOG((" %p is re-validating with a new channel %p", this, + validatingChannel.get())); +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/nsHttpChannel.h b/netwerk/protocol/http/nsHttpChannel.h new file mode 100644 index 0000000000..208dd829af --- /dev/null +++ b/netwerk/protocol/http/nsHttpChannel.h @@ -0,0 +1,888 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set et cin ts=4 sw=2 sts=2: */ +/* 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/. */ + +#ifndef nsHttpChannel_h__ +#define nsHttpChannel_h__ + +#include "DelayHttpChannelQueue.h" +#include "HttpBaseChannel.h" +#include "nsTArray.h" +#include "nsIApplicationCache.h" +#include "nsICachingChannel.h" +#include "nsICacheEntry.h" +#include "nsICacheEntryOpenCallback.h" +#include "nsIDNSListener.h" +#include "nsIApplicationCacheChannel.h" +#include "nsIProtocolProxyCallback.h" +#include "nsIHttpAuthenticableChannel.h" +#include "nsIAsyncVerifyRedirectCallback.h" +#include "nsIThreadRetargetableRequest.h" +#include "nsIThreadRetargetableStreamListener.h" +#include "nsWeakReference.h" +#include "TimingStruct.h" +#include "AutoClose.h" +#include "nsIStreamListener.h" +#include "nsICorsPreflightCallback.h" +#include "AlternateServices.h" +#include "nsIRaceCacheWithNetwork.h" +#include "mozilla/AtomicBitfields.h" +#include "mozilla/Atomics.h" +#include "mozilla/extensions/PStreamFilterParent.h" +#include "mozilla/net/DocumentLoadListener.h" +#include "mozilla/Mutex.h" + +class nsDNSPrefetch; +class nsICancelable; +class nsIDNSRecord; +class nsIDNSHTTPSSVCRecord; +class nsIHttpChannelAuthProvider; +class nsInputStreamPump; +class nsITransportSecurityInfo; + +namespace mozilla { +namespace net { + +class nsChannelClassifier; +class HttpChannelSecurityWarningReporter; + +using DNSPromise = MozPromise, nsresult, false>; + +//----------------------------------------------------------------------------- +// nsHttpChannel +//----------------------------------------------------------------------------- + +// Use to support QI nsIChannel to nsHttpChannel +#define NS_HTTPCHANNEL_IID \ + { \ + 0x301bf95b, 0x7bb3, 0x4ae1, { \ + 0xa9, 0x71, 0x40, 0xbc, 0xfa, 0x81, 0xde, 0x12 \ + } \ + } + +class nsHttpChannel final : public HttpBaseChannel, + public HttpAsyncAborter, + public nsIStreamListener, + public nsICachingChannel, + public nsICacheEntryOpenCallback, + public nsITransportEventSink, + public nsIProtocolProxyCallback, + public nsIHttpAuthenticableChannel, + public nsIApplicationCacheChannel, + public nsIAsyncVerifyRedirectCallback, + public nsIThreadRetargetableRequest, + public nsIThreadRetargetableStreamListener, + public nsIDNSListener, + public nsSupportsWeakReference, + public nsICorsPreflightCallback, + public nsIRaceCacheWithNetwork, + public nsIRequestTailUnblockCallback, + public nsITimerCallback { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMLISTENER + NS_DECL_NSITHREADRETARGETABLESTREAMLISTENER + NS_DECL_NSICACHEINFOCHANNEL + NS_DECL_NSICACHINGCHANNEL + NS_DECL_NSICACHEENTRYOPENCALLBACK + NS_DECL_NSITRANSPORTEVENTSINK + NS_DECL_NSIPROTOCOLPROXYCALLBACK + NS_DECL_NSIPROXIEDCHANNEL + NS_DECL_NSIAPPLICATIONCACHECONTAINER + NS_DECL_NSIAPPLICATIONCACHECHANNEL + NS_DECL_NSIASYNCVERIFYREDIRECTCALLBACK + NS_DECL_NSITHREADRETARGETABLEREQUEST + NS_DECL_NSIDNSLISTENER + NS_DECLARE_STATIC_IID_ACCESSOR(NS_HTTPCHANNEL_IID) + NS_DECL_NSIRACECACHEWITHNETWORK + NS_DECL_NSITIMERCALLBACK + NS_DECL_NSIREQUESTTAILUNBLOCKCALLBACK + + // nsIHttpAuthenticableChannel. We can't use + // NS_DECL_NSIHTTPAUTHENTICABLECHANNEL because it duplicates cancel() and + // others. + NS_IMETHOD GetIsSSL(bool* aIsSSL) override; + NS_IMETHOD GetProxyMethodIsConnect(bool* aProxyMethodIsConnect) override; + NS_IMETHOD GetServerResponseHeader( + nsACString& aServerResponseHeader) override; + NS_IMETHOD GetProxyChallenges(nsACString& aChallenges) override; + NS_IMETHOD GetWWWChallenges(nsACString& aChallenges) override; + NS_IMETHOD SetProxyCredentials(const nsACString& aCredentials) override; + NS_IMETHOD SetWWWCredentials(const nsACString& aCredentials) override; + NS_IMETHOD OnAuthAvailable() override; + NS_IMETHOD OnAuthCancelled(bool userCancel) override; + NS_IMETHOD CloseStickyConnection() override; + NS_IMETHOD ConnectionRestartable(bool) override; + // Functions we implement from nsIHttpAuthenticableChannel but are + // declared in HttpBaseChannel must be implemented in this class. We + // just call the HttpBaseChannel:: impls. + NS_IMETHOD GetLoadFlags(nsLoadFlags* aLoadFlags) override; + NS_IMETHOD GetURI(nsIURI** aURI) override; + NS_IMETHOD GetNotificationCallbacks( + nsIInterfaceRequestor** aCallbacks) override; + NS_IMETHOD GetLoadGroup(nsILoadGroup** aLoadGroup) override; + NS_IMETHOD GetRequestMethod(nsACString& aMethod) override; + + nsHttpChannel(); + + [[nodiscard]] virtual nsresult Init( + nsIURI* aURI, uint32_t aCaps, nsProxyInfo* aProxyInfo, + uint32_t aProxyResolveFlags, nsIURI* aProxyURI, uint64_t aChannelId, + ExtContentPolicyType aContentPolicyType) override; + + [[nodiscard]] nsresult OnPush(uint32_t aPushedStreamId, + const nsACString& aUrl, + const nsACString& aRequestString, + HttpTransactionShell* aTransaction); + + static bool IsRedirectStatus(uint32_t status); + static bool WillRedirect(const nsHttpResponseHead& response); + + // Methods HttpBaseChannel didn't implement for us or that we override. + // + // nsIRequest + NS_IMETHOD Cancel(nsresult status) override; + NS_IMETHOD Suspend() override; + NS_IMETHOD Resume() override; + // nsIChannel + NS_IMETHOD GetSecurityInfo(nsISupports** aSecurityInfo) override; + NS_IMETHOD AsyncOpen(nsIStreamListener* aListener) override; + // nsIHttpChannel + NS_IMETHOD GetEncodedBodySize(uint64_t* aEncodedBodySize) override; + // nsIHttpChannelInternal + NS_IMETHOD SetupFallbackChannel(const char* aFallbackKey) override; + NS_IMETHOD GetIsAuthChannel(bool* aIsAuthChannel) override; + NS_IMETHOD SetChannelIsForDownload(bool aChannelIsForDownload) override; + NS_IMETHOD GetNavigationStartTimeStamp(TimeStamp* aTimeStamp) override; + NS_IMETHOD SetNavigationStartTimeStamp(TimeStamp aTimeStamp) override; + NS_IMETHOD CancelByURLClassifier(nsresult aErrorCode) override; + // nsISupportsPriority + NS_IMETHOD SetPriority(int32_t value) override; + // nsIClassOfService + NS_IMETHOD SetClassFlags(uint32_t inFlags) override; + NS_IMETHOD AddClassFlags(uint32_t inFlags) override; + NS_IMETHOD ClearClassFlags(uint32_t inFlags) override; + + // nsIResumableChannel + NS_IMETHOD ResumeAt(uint64_t startPos, const nsACString& entityID) override; + + NS_IMETHOD SetNotificationCallbacks( + nsIInterfaceRequestor* aCallbacks) override; + NS_IMETHOD SetLoadGroup(nsILoadGroup* aLoadGroup) override; + // nsITimedChannel + NS_IMETHOD GetDomainLookupStart( + mozilla::TimeStamp* aDomainLookupStart) override; + NS_IMETHOD GetDomainLookupEnd(mozilla::TimeStamp* aDomainLookupEnd) override; + NS_IMETHOD GetConnectStart(mozilla::TimeStamp* aConnectStart) override; + NS_IMETHOD GetTcpConnectEnd(mozilla::TimeStamp* aTcpConnectEnd) override; + NS_IMETHOD GetSecureConnectionStart( + mozilla::TimeStamp* aSecureConnectionStart) override; + NS_IMETHOD GetConnectEnd(mozilla::TimeStamp* aConnectEnd) override; + NS_IMETHOD GetRequestStart(mozilla::TimeStamp* aRequestStart) override; + NS_IMETHOD GetResponseStart(mozilla::TimeStamp* aResponseStart) override; + NS_IMETHOD GetResponseEnd(mozilla::TimeStamp* aResponseEnd) override; + // nsICorsPreflightCallback + NS_IMETHOD OnPreflightSucceeded() override; + NS_IMETHOD OnPreflightFailed(nsresult aError) override; + + [[nodiscard]] nsresult AddSecurityMessage( + const nsAString& aMessageTag, const nsAString& aMessageCategory) override; + NS_IMETHOD LogBlockedCORSRequest(const nsAString& aMessage, + const nsACString& aCategory) override; + NS_IMETHOD LogMimeTypeMismatch(const nsACString& aMessageName, bool aWarning, + const nsAString& aURL, + const nsAString& aContentType) override; + + void SetWarningReporter(HttpChannelSecurityWarningReporter* aReporter); + HttpChannelSecurityWarningReporter* GetWarningReporter(); + + bool DataSentToChildProcess() { return LoadDataSentToChildProcess(); } + + public: /* internal necko use only */ + uint32_t GetRequestTime() const { return mRequestTime; } + + nsresult AsyncOpenFinal(TimeStamp aTimeStamp); + + [[nodiscard]] nsresult OpenCacheEntry(bool usingSSL); + [[nodiscard]] nsresult OpenCacheEntryInternal( + bool isHttps, nsIApplicationCache* applicationCache, bool noAppCache); + [[nodiscard]] nsresult ContinueConnect(); + + [[nodiscard]] nsresult StartRedirectChannelToURI(nsIURI*, uint32_t); + + // This allows cache entry to be marked as foreign even after channel itself + // is gone. Needed for e10s (see + // HttpChannelParent::RecvDocumentChannelCleanup) + class OfflineCacheEntryAsForeignMarker { + nsCOMPtr mApplicationCache; + nsCOMPtr mCacheURI; + + public: + OfflineCacheEntryAsForeignMarker(nsIApplicationCache* appCache, + nsIURI* aURI) + : mApplicationCache(appCache), mCacheURI(aURI) {} + + nsresult MarkAsForeign(); + }; + + OfflineCacheEntryAsForeignMarker* GetOfflineCacheEntryAsForeignMarker(); + + // Helper to keep cache callbacks wait flags consistent + class AutoCacheWaitFlags { + public: + explicit AutoCacheWaitFlags(nsHttpChannel* channel) + : mChannel(channel), mKeep(0) { + // Flags must be set before entering any AsyncOpenCacheEntry call. + mChannel->StoreCacheEntriesToWaitFor( + nsHttpChannel::WAIT_FOR_CACHE_ENTRY | + nsHttpChannel::WAIT_FOR_OFFLINE_CACHE_ENTRY); + } + + void Keep(uint32_t flags) { + // Called after successful call to appropriate AsyncOpenCacheEntry call. + mKeep |= flags; + } + + ~AutoCacheWaitFlags() { + // Keep only flags those are left to be wait for. + mChannel->StoreCacheEntriesToWaitFor( + mChannel->LoadCacheEntriesToWaitFor() & mKeep); + } + + private: + nsHttpChannel* mChannel; + uint32_t mKeep : 2; + }; + + bool AwaitingCacheCallbacks(); + void SetCouldBeSynthesized(); + + // Return true if the latest ODA is invoked by mCachePump. + // Should only be called on the same thread as ODA. + bool IsReadingFromCache() const { return mIsReadingFromCache; } + + base::ProcessId ProcessId(); + + using ChildEndpointPromise = + MozPromise, bool, true>; + [[nodiscard]] RefPtr AttachStreamFilter( + base::ProcessId aChildProcessId); + + private: // used for alternate service validation + RefPtr mTransactionObserver; + + public: + void SetConnectionInfo(nsHttpConnectionInfo*); // clones the argument + void SetTransactionObserver(TransactionObserver* arg) { + mTransactionObserver = arg; + } + TransactionObserver* GetTransactionObserver() { return mTransactionObserver; } + + CacheDisposition mCacheDisposition; + + protected: + virtual ~nsHttpChannel(); + + private: + typedef nsresult (nsHttpChannel::*nsContinueRedirectionFunc)(nsresult result); + + // Directly call |aFunc| if the channel is not canceled and not suspended. + // Otherwise, set |aFunc| to |mCallOnResume| and wait until the channel + // resumes. + nsresult CallOrWaitForResume( + const std::function& aFunc); + + bool RequestIsConditional(); + void HandleContinueCancellingByURLClassifier(nsresult aErrorCode); + nsresult CancelInternal(nsresult status); + void ContinueCancellingByURLClassifier(nsresult aErrorCode); + + // Connections will only be established in this function. + // (including DNS prefetch and speculative connection.) + nsresult MaybeResolveProxyAndBeginConnect(); + nsresult MaybeStartDNSPrefetch(); + + // Tells the channel to resolve the origin of the end server we are connecting + // to. + static uint16_t const DNS_PREFETCH_ORIGIN = 1 << 0; + // Tells the channel to resolve the host name of the proxy. + static uint16_t const DNS_PREFETCH_PROXY = 1 << 1; + // Will be set if the current channel uses an HTTP/HTTPS proxy. + static uint16_t const DNS_PROXY_IS_HTTP = 1 << 2; + // Tells the channel to wait for the result of the origin server resolution + // before any connection attempts are made. + static uint16_t const DNS_BLOCK_ON_ORIGIN_RESOLVE = 1 << 3; + + // Based on the proxy configuration determine the strategy for resolving the + // end server host name. + // Returns a combination of the above flags. + uint16_t GetProxyDNSStrategy(); + + // We might synchronously or asynchronously call BeginConnect, + // which includes DNS prefetch and speculative connection, according to + // whether an async tracker lookup is required. If the tracker lookup + // is required, this funciton will just return NS_OK and BeginConnect() + // will be called when callback. See Bug 1325054 for more information. + nsresult BeginConnect(); + [[nodiscard]] nsresult ContinueBeginConnectWithResult(); + void ContinueBeginConnect(); + [[nodiscard]] nsresult PrepareToConnect(); + void HandleOnBeforeConnect(); + [[nodiscard]] nsresult OnBeforeConnect(); + [[nodiscard]] nsresult ContinueOnBeforeConnect(bool aShouldUpgrade, + nsresult aStatus); + nsresult MaybeUseHTTPSRRForUpgrade(bool aShouldUpgrade, nsresult aStatus); + void OnHTTPSRRAvailable(nsIDNSHTTPSSVCRecord* aRecord); + void OnBeforeConnectContinue(); + [[nodiscard]] nsresult Connect(); + void SpeculativeConnect(); + [[nodiscard]] nsresult SetupTransaction(); + [[nodiscard]] nsresult CallOnStartRequest(); + [[nodiscard]] nsresult ProcessResponse(); + void AsyncContinueProcessResponse(); + [[nodiscard]] nsresult ContinueProcessResponse1(); + [[nodiscard]] nsresult ContinueProcessResponse2(nsresult); + + public: + void UpdateCacheDisposition(bool aSuccessfulReval, bool aPartialContentUsed); + [[nodiscard]] nsresult ContinueProcessResponse3(nsresult); + [[nodiscard]] nsresult ContinueProcessResponse4(nsresult); + [[nodiscard]] nsresult ProcessNormal(); + [[nodiscard]] nsresult ContinueProcessNormal(nsresult); + void ProcessAltService(); + bool ShouldBypassProcessNotModified(); + [[nodiscard]] nsresult ProcessNotModified( + const std::function& + aContinueProcessResponseFunc); + [[nodiscard]] nsresult ContinueProcessResponseAfterNotModified(nsresult aRv); + + [[nodiscard]] nsresult AsyncProcessRedirection(uint32_t httpStatus); + [[nodiscard]] nsresult ContinueProcessRedirection(nsresult); + [[nodiscard]] nsresult ContinueProcessRedirectionAfterFallback(nsresult); + [[nodiscard]] nsresult ProcessFailedProxyConnect(uint32_t httpStatus); + [[nodiscard]] nsresult ProcessFallback(bool* waitingForRedirectCallback); + [[nodiscard]] nsresult ContinueProcessFallback(nsresult); + void HandleAsyncAbort(); + [[nodiscard]] nsresult EnsureAssocReq(); + void ProcessSSLInformation(); + bool IsHTTPS(); + + [[nodiscard]] nsresult ContinueOnStartRequest1(nsresult); + [[nodiscard]] nsresult ContinueOnStartRequest2(nsresult); + [[nodiscard]] nsresult ContinueOnStartRequest3(nsresult); + [[nodiscard]] nsresult ContinueOnStartRequest4(nsresult); + + void OnClassOfServiceUpdated(); + + // redirection specific methods + void HandleAsyncRedirect(); + void HandleAsyncAPIRedirect(); + [[nodiscard]] nsresult ContinueHandleAsyncRedirect(nsresult); + void HandleAsyncNotModified(); + void HandleAsyncFallback(); + [[nodiscard]] nsresult ContinueHandleAsyncFallback(nsresult); + [[nodiscard]] nsresult PromptTempRedirect(); + [[nodiscard]] virtual nsresult SetupReplacementChannel( + nsIURI*, nsIChannel*, bool preserveMethod, + uint32_t redirectFlags) override; + + // proxy specific methods + [[nodiscard]] nsresult ProxyFailover(); + [[nodiscard]] nsresult AsyncDoReplaceWithProxy(nsIProxyInfo*); + [[nodiscard]] nsresult ContinueDoReplaceWithProxy(nsresult); + [[nodiscard]] nsresult ResolveProxy(); + + // cache specific methods + [[nodiscard]] nsresult OnOfflineCacheEntryAvailable( + nsICacheEntry* aEntry, bool aNew, nsIApplicationCache* aAppCache, + nsresult aResult); + [[nodiscard]] nsresult OnNormalCacheEntryAvailable(nsICacheEntry* aEntry, + bool aNew, + nsresult aResult); + [[nodiscard]] nsresult OpenOfflineCacheEntryForWriting(); + [[nodiscard]] nsresult OnOfflineCacheEntryForWritingAvailable( + nsICacheEntry* aEntry, nsIApplicationCache* aAppCache, nsresult aResult); + [[nodiscard]] nsresult OnCacheEntryAvailableInternal( + nsICacheEntry* entry, bool aNew, nsIApplicationCache* aAppCache, + nsresult status); + [[nodiscard]] nsresult GenerateCacheKey(uint32_t postID, nsACString& key); + [[nodiscard]] nsresult UpdateExpirationTime(); + [[nodiscard]] nsresult CheckPartial(nsICacheEntry* aEntry, int64_t* aSize, + int64_t* aContentLength); + bool ShouldUpdateOfflineCacheEntry(); + [[nodiscard]] nsresult ReadFromCache(bool alreadyMarkedValid); + void CloseCacheEntry(bool doomOnFailure); + void CloseOfflineCacheEntry(); + [[nodiscard]] nsresult InitCacheEntry(); + void UpdateInhibitPersistentCachingFlag(); + [[nodiscard]] nsresult InitOfflineCacheEntry(); + [[nodiscard]] nsresult AddCacheEntryHeaders(nsICacheEntry* entry); + [[nodiscard]] nsresult FinalizeCacheEntry(); + [[nodiscard]] nsresult InstallCacheListener(int64_t offset = 0); + [[nodiscard]] nsresult InstallOfflineCacheListener(int64_t offset = 0); + void MaybeInvalidateCacheEntryForSubsequentGet(); + void AsyncOnExamineCachedResponse(); + + // Handle the bogus Content-Encoding Apache sometimes sends + void ClearBogusContentEncodingIfNeeded(); + + // byte range request specific methods + [[nodiscard]] nsresult ProcessPartialContent( + const std::function& + aContinueProcessResponseFunc); + [[nodiscard]] nsresult ContinueProcessResponseAfterPartialContent( + nsresult aRv); + [[nodiscard]] nsresult OnDoneReadingPartialCacheEntry(bool* streamDone); + + [[nodiscard]] nsresult DoAuthRetry( + HttpTransactionShell* aTransWithStickyConn, + const std::function& + aContinueOnStopRequestFunc); + [[nodiscard]] nsresult ContinueDoAuthRetry( + HttpTransactionShell* aTransWithStickyConn, + const std::function& + aContinueOnStopRequestFunc); + [[nodiscard]] MOZ_NEVER_INLINE nsresult + DoConnect(HttpTransactionShell* aTransWithStickyConn = nullptr); + [[nodiscard]] nsresult DoConnectActual( + HttpTransactionShell* aTransWithStickyConn); + [[nodiscard]] nsresult ContinueOnStopRequestAfterAuthRetry( + nsresult aStatus, bool aAuthRetry, bool aIsFromNet, bool aContentComplete, + HttpTransactionShell* aTransWithStickyConn); + [[nodiscard]] nsresult ContinueOnStopRequest(nsresult status, bool aIsFromNet, + bool aContentComplete); + + void HandleAsyncRedirectChannelToHttps(); + [[nodiscard]] nsresult StartRedirectChannelToHttps(); + [[nodiscard]] nsresult ContinueAsyncRedirectChannelToURI(nsresult rv); + [[nodiscard]] nsresult OpenRedirectChannel(nsresult rv); + + HttpTrafficCategory CreateTrafficCategory(); + + /** + * A function that takes care of reading STS and PKP headers and enforcing + * STS and PKP load rules. After a secure channel is erected, STS and PKP + * requires the channel to be trusted or any STS or PKP header data on + * the channel is ignored. This is called from ProcessResponse. + */ + [[nodiscard]] nsresult ProcessSecurityHeaders(); + + /** + * Taking care of the Content-Signature header and fail the channel if + * the signature verification fails or is required but the header is not + * present. + * This sets mListener to ContentVerifier, which buffers the entire response + * before verifying the Content-Signature header. If the verification is + * successful, the load proceeds as usual. If the verification fails, a + * NS_ERROR_INVALID_SIGNATURE is thrown and a fallback loaded in nsDocShell + */ + [[nodiscard]] nsresult ProcessContentSignatureHeader( + nsHttpResponseHead* aResponseHead); + + /** + * A function to process a single security header (STS or PKP), assumes + * some basic sanity checks have been applied to the channel. Called + * from ProcessSecurityHeaders. + */ + [[nodiscard]] nsresult ProcessSingleSecurityHeader( + uint32_t aType, nsITransportSecurityInfo* aSecInfo, uint32_t aFlags); + + void InvalidateCacheEntryForLocation(const char* location); + void AssembleCacheKey(const char* spec, uint32_t postID, nsACString& key); + [[nodiscard]] nsresult CreateNewURI(const char* loc, nsIURI** newURI); + void DoInvalidateCacheEntry(nsIURI* aURI); + + // Ref RFC2616 13.10: "invalidation... MUST only be performed if + // the host part is the same as in the Request-URI" + inline bool HostPartIsTheSame(nsIURI* uri) { + nsAutoCString tmpHost1, tmpHost2; + return (NS_SUCCEEDED(mURI->GetAsciiHost(tmpHost1)) && + NS_SUCCEEDED(uri->GetAsciiHost(tmpHost2)) && + (tmpHost1 == tmpHost2)); + } + + inline static bool DoNotRender3xxBody(nsresult rv) { + return rv == NS_ERROR_REDIRECT_LOOP || rv == NS_ERROR_CORRUPTED_CONTENT || + rv == NS_ERROR_UNKNOWN_PROTOCOL || rv == NS_ERROR_MALFORMED_URI || + rv == NS_ERROR_PORT_ACCESS_NOT_ALLOWED; + } + + // Report net vs cache time telemetry + void ReportNetVSCacheTelemetry(); + int64_t ComputeTelemetryBucketNumber(int64_t difftime_ms); + + // Report telemetry and stats to about:networking + void ReportRcwnStats(bool isFromNet); + + // Create a aggregate set of the current notification callbacks + // and ensure the transaction is updated to use it. + void UpdateAggregateCallbacks(); + + static bool HasQueryString(nsHttpRequestHead::ParsedMethodType method, + nsIURI* uri); + bool ResponseWouldVary(nsICacheEntry* entry); + bool IsResumable(int64_t partialLen, int64_t contentLength, + bool ignoreMissingPartialLen = false) const; + [[nodiscard]] nsresult MaybeSetupByteRangeRequest( + int64_t partialLen, int64_t contentLength, + bool ignoreMissingPartialLen = false); + [[nodiscard]] nsresult SetupByteRangeRequest(int64_t partialLen); + void UntieByteRangeRequest(); + void UntieValidationRequest(); + [[nodiscard]] nsresult OpenCacheInputStream(nsICacheEntry* cacheEntry, + bool startBuffering, + bool checkingAppCacheEntry); + + void SetPushedStreamTransactionAndId( + HttpTransactionShell* aTransWithPushedStream, uint32_t aPushedStreamId); + + void MaybeWarnAboutAppCache(); + + void SetOriginHeader(); + void SetDoNotTrack(); + + bool IsIsolated(); + + const nsCString& GetTopWindowOrigin(); + + already_AddRefed GetOrCreateChannelClassifier(); + + // Start an internal redirect to a new InterceptedHttpChannel which will + // resolve in firing a ServiceWorker FetchEvent. + [[nodiscard]] nsresult RedirectToInterceptedChannel(); + + // Determines and sets content type in the cache entry. It's called when + // writing a new entry. The content type is used in cache internally only. + void SetCachedContentType(); + + private: + // this section is for main-thread-only object + // all the references need to be proxy released on main thread. + nsCOMPtr mApplicationCacheForWrite; + // auth specific data + nsCOMPtr mAuthProvider; + nsCOMPtr mRedirectURI; + nsCOMPtr mRedirectChannel; + nsCOMPtr mPreflightChannel; + + // nsChannelClassifier checks this channel's URI against + // the URI classifier service. + // nsChannelClassifier will be invoked twice in InitLocalBlockList() and + // BeginConnect(), so save the nsChannelClassifier here to keep the + // state of whether tracking protection is enabled or not. + RefPtr mChannelClassifier; + + // Proxy release all members above on main thread. + void ReleaseMainThreadOnlyReferences(); + + // Called after the channel is made aware of its tracking status in order + // to readjust the referrer if needed according to the referrer default + // policy preferences. + void ReEvaluateReferrerAfterTrackingStatusIsKnown(); + + // Create a dummy channel for the same principal, out of the load group + // just to revalidate the cache entry. We don't care if this fails. + // This method can be called on any thread, and creates an idle task + // to perform the revalidation with delay. + void PerformBackgroundCacheRevalidation(); + // This method can only be called on the main thread. + void PerformBackgroundCacheRevalidationNow(); + + private: + nsCOMPtr mProxyRequest; + + nsCOMPtr mTransactionPump; + RefPtr mTransaction; + + uint64_t mLogicalOffset; + + // cache specific data + nsCOMPtr mCacheEntry; + // This will be set during OnStopRequest() before calling CloseCacheEntry(), + // but only if the listener wants to use alt-data (signaled by + // HttpBaseChannel::mPreferredCachedAltDataType being not empty) + // Needed because calling openAlternativeOutputStream needs a reference + // to the cache entry. + nsCOMPtr mAltDataCacheEntry; + + nsCOMPtr mCacheEntryURI; + nsCString mCacheIdExtension; + + // We must close mCacheInputStream explicitly to avoid leaks. + AutoClose mCacheInputStream; + RefPtr mCachePump; + UniquePtr mCachedResponseHead; + nsCOMPtr mCachedSecurityInfo; + uint32_t mPostID; + uint32_t mRequestTime; + + nsCOMPtr mOfflineCacheEntry; + uint32_t mOfflineCacheLastModifiedTime; + + nsTArray mStreamFilterRequests; + + mozilla::TimeStamp mOnStartRequestTimestamp; + // Timestamp of the time the channel was suspended. + mozilla::TimeStamp mSuspendTimestamp; + mozilla::TimeStamp mOnCacheEntryCheckTimestamp; +#ifdef MOZ_GECKO_PROFILER + // For the profiler markers + mozilla::TimeStamp mLastStatusReported; +#endif + // Total time the channel spent suspended. This value is reported to + // telemetry in nsHttpChannel::OnStartRequest(). + uint32_t mSuspendTotalTime; + + // If the channel is associated with a cache, and the URI matched + // a fallback namespace, this will hold the key for the fallback + // cache entry. + nsCString mFallbackKey; + + friend class AutoRedirectVetoNotifier; + friend class HttpAsyncAborter; + + uint32_t mRedirectType; + + static const uint32_t WAIT_FOR_CACHE_ENTRY = 1; + static const uint32_t WAIT_FOR_OFFLINE_CACHE_ENTRY = 2; + + bool mCacheOpenWithPriority; + uint32_t mCacheQueueSizeWhenOpen; + + Atomic mCachedContentIsValid; + Atomic mIsAuthChannel; + Atomic mAuthRetryPending; + + // clang-format off + // state flags + MOZ_ATOMIC_BITFIELDS(mAtomicBitfields5, 32, ( + (uint32_t, CachedContentIsPartial, 1), + (uint32_t, CacheOnlyMetadata, 1), + (uint32_t, TransactionReplaced, 1), + (uint32_t, ProxyAuthPending, 1), + // Set if before the first authentication attempt a custom authorization + // header has been set on the channel. This will make that custom header + // go to the server instead of any cached credentials. + (uint32_t, CustomAuthHeader, 1), + (uint32_t, Resuming, 1), + (uint32_t, InitedCacheEntry, 1), + // True if we are loading a fallback cache entry from the + // application cache. + (uint32_t, FallbackChannel, 1), + // True if consumer added its own If-None-Match or If-Modified-Since + // headers. In such a case we must not override them in the cache code + // and also we want to pass possible 304 code response through. + (uint32_t, CustomConditionalRequest, 1), + (uint32_t, FallingBack, 1), + (uint32_t, WaitingForRedirectCallback, 1), + // True if mRequestTime has been set. In such a case it is safe to update + // the cache entry's expiration time. Otherwise, it is not(see bug 567360). + (uint32_t, RequestTimeInitialized, 1), + (uint32_t, CacheEntryIsReadOnly, 1), + (uint32_t, CacheEntryIsWriteOnly, 1), + // see WAIT_FOR_* constants above + (uint32_t, CacheEntriesToWaitFor, 2), + // whether cache entry data write was in progress during cache entry check + // when true, after we finish read from cache we must check all data + // had been loaded from cache. If not, then an error has to be propagated + // to the consumer. + (uint32_t, ConcurrentCacheAccess, 1), + // whether the request is setup be byte-range + (uint32_t, IsPartialRequest, 1), + // true iff there is AutoRedirectVetoNotifier on the stack + (uint32_t, HasAutoRedirectVetoNotifier, 1), + // consumers set this to true to use cache pinning, this has effect + // only when the channel is in an app context + (uint32_t, PinCacheContent, 1), + // True if CORS preflight has been performed + (uint32_t, IsCorsPreflightDone, 1), + + // if the http transaction was performed (i.e. not cached) and + // the result in OnStopRequest was known to be correctly delimited + // by chunking, content-length, or h2 end-stream framing + (uint32_t, StronglyFramed, 1), + + // true if an HTTP transaction is created for the socket thread + (uint32_t, UsedNetwork, 1), + + // the next authentication request can be sent on a whole new connection + (uint32_t, AuthConnectionRestartable, 1), + + // True if the channel classifier has marked the channel to be cancelled due + // to the safe-browsing classifier rules, but the asynchronous cancellation + // process hasn't finished yet. + (uint32_t, ChannelClassifierCancellationPending, 1), + + // True only when we are between Resume and async fire of mCallOnResume. + // Used to suspend any newly created pumps in mCallOnResume handler. + (uint32_t, AsyncResumePending, 1), + + // True only when we have checked whether this channel has been isolated for + // anti-tracking purposes. + (uint32_t, HasBeenIsolatedChecked, 1), + // True only when we have determined this channel should be isolated for + // anti-tracking purposes. Can never ben true unless HasBeenIsolatedChecked + // is true. + (uint32_t, IsIsolated, 1), + + // True only when we have computed the value of the top window origin. + (uint32_t, TopWindowOriginComputed, 1), + + // True if the data will be sent from the socket process to the + // content process directly. + (uint32_t, DataSentToChildProcess, 1), + + (uint32_t, UseHTTPSSVC, 1), + (uint32_t, WaitHTTPSSVCRecord, 1) + )) + + // Broken up into two bitfields to avoid alignment requirements of uint64_t. + // (Too many bits used for one uint32_t.) + MOZ_ATOMIC_BITFIELDS(mAtomicBitfields6, 32, ( + // Only set to true when we receive an HTTPSSVC record before the + // transaction is created. + (uint32_t, HTTPSSVCTelemetryReported, 1), + (uint32_t, EchConfigUsed, 1) + )) + // clang-format on + + // The origin of the top window, only valid when TopWindowOriginComputed is + // true. + nsCString mTopWindowOrigin; + + nsTArray mRedirectFuncStack; + + // Needed for accurate DNS timing + RefPtr mDNSPrefetch; + + uint32_t mPushedStreamId; + RefPtr mTransWithPushedStream; + + // True if the channel's principal was found on a phishing, malware, or + // tracking (if tracking protection is enabled) blocklist + bool mLocalBlocklist; + + [[nodiscard]] nsresult WaitForRedirectCallback(); + void PushRedirectAsyncFunc(nsContinueRedirectionFunc func); + void PopRedirectAsyncFunc(nsContinueRedirectionFunc func); + + // If this resource is eligible for tailing based on class-of-service flags + // and load flags. We don't tail Leaders/Unblocked/UrgentStart and top-level + // loads. + bool EligibleForTailing(); + + // Called exclusively only from AsyncOpen or after all classification + // callbacks. If this channel is 1) Tail, 2) assigned a request context, 3) + // the context is still in the tail-blocked phase, then the method will queue + // this channel. OnTailUnblock will be called after the context is + // tail-unblocked or canceled. + bool WaitingForTailUnblock(); + + // A function we trigger when untail callback is triggered by our request + // context in case this channel was tail-blocked. + nsresult (nsHttpChannel::*mOnTailUnblock)(); + // Called on untail when tailed during AsyncOpen execution. + nsresult AsyncOpenOnTailUnblock(); + // Called on untail when tailed because of being a tracking resource. + nsresult ConnectOnTailUnblock(); + + nsCString mUsername; + + // If non-null, warnings should be reported to this object. + RefPtr mWarningReporter; + + // True if the channel is reading from cache. + Atomic mIsReadingFromCache; + + // These next members are only used in unit tests to delay the call to + // cache->AsyncOpenURI in order to race the cache with the network. + nsCOMPtr mCacheOpenTimer; + std::function mCacheOpenFunc; + uint32_t mCacheOpenDelay = 0; + + // We need to remember which is the source of the response we are using. + enum ResponseSource { + RESPONSE_PENDING = 0, // response is pending + RESPONSE_FROM_CACHE = 1, // response coming from cache. no network. + RESPONSE_FROM_NETWORK = 2 // response coming from the network + }; + Atomic mFirstResponseSource; + + // Determines if it's possible and advisable to race the network request + // with the cache fetch, and proceeds to do so. + nsresult MaybeRaceCacheWithNetwork(); + + // Creates a new cache entry when network wins the race to ensure we have + // the latest version of the resource in the cache. Otherwise we might return + // an old content when navigating back in history. + void MaybeCreateCacheEntryWhenRCWN(); + + nsresult TriggerNetworkWithDelay(uint32_t aDelay); + nsresult TriggerNetwork(); + void CancelNetworkRequest(nsresult aStatus); + + void SetHTTPSSVCRecord(already_AddRefed&& aRecord); + + // Timer used to delay the network request, or to trigger the network + // request if retrieving the cache entry takes too long. + nsCOMPtr mNetworkTriggerTimer; + // Is true if the network request has been triggered. + bool mNetworkTriggered = false; + bool mWaitingForProxy = false; + bool mStaleRevalidation = false; + // Will be true if the onCacheEntryAvailable callback is not called by the + // time we send the network request + Atomic mRaceCacheWithNetwork; + uint32_t mRaceDelay; + // If true then OnCacheEntryAvailable should ignore the entry, because + // SetupTransaction removed conditional headers and decisions made in + // OnCacheEntryCheck are no longer valid. + bool mIgnoreCacheEntry; + // Lock preventing SetupTransaction/MaybeCreateCacheEntryWhenRCWN and + // OnCacheEntryCheck being called at the same time. + mozilla::Mutex mRCWNLock; + + TimeStamp mNavigationStartTimeStamp; + + // Promise that blocks connection creation when we want to resolve the origin + // host name to be able to give the configured proxy only the resolved IP + // to not leak names. + MozPromiseHolder mDNSBlockingPromise; + // When we hit DoConnect before the resolution is done, Then() will be set + // here to resume DoConnect. + RefPtr mDNSBlockingThenable; + + // We update the value of mProxyConnectResponseCode when OnStartRequest is + // called and reset the value when we switch to another failover proxy. + int32_t mProxyConnectResponseCode; + + // If mHTTPSSVCRecord has value, it means OnHTTPSRRAvailable() is called and + // we got the result of HTTPS RR query. Otherwise, it means we are still + // waiting for the result or the query is not performed. + Maybe> mHTTPSSVCRecord; + + protected: + virtual void DoNotifyListenerCleanup() override; + + // Override ReleaseListeners() because mChannelClassifier only exists + // in nsHttpChannel and it will be released in ReleaseListeners(). + virtual void ReleaseListeners() override; + + virtual void DoAsyncAbort(nsresult aStatus) override; + + private: // cache telemetry + bool mDidReval; +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(nsHttpChannel, NS_HTTPCHANNEL_IID) +} // namespace net +} // namespace mozilla + +inline nsISupports* ToSupports(mozilla::net::nsHttpChannel* aChannel) { + return static_cast(aChannel); +} + +#endif // nsHttpChannel_h__ diff --git a/netwerk/protocol/http/nsHttpChannelAuthProvider.cpp b/netwerk/protocol/http/nsHttpChannelAuthProvider.cpp new file mode 100644 index 0000000000..df3fbbc6a6 --- /dev/null +++ b/netwerk/protocol/http/nsHttpChannelAuthProvider.cpp @@ -0,0 +1,1698 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set expandtab ts=4 sw=2 sts=2 cin: */ +/* 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/. */ + +// HttpLog.h should generally be included first +#include "HttpLog.h" + +#include "mozilla/BasePrincipal.h" +#include "mozilla/Preferences.h" +#include "mozilla/StoragePrincipalHelper.h" +#include "nsHttpChannelAuthProvider.h" +#include "nsNetUtil.h" +#include "nsHttpHandler.h" +#include "nsIHttpAuthenticator.h" +#include "nsIHttpChannelInternal.h" +#include "nsIAuthPrompt2.h" +#include "nsIAuthPromptProvider.h" +#include "nsIInterfaceRequestor.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsEscape.h" +#include "nsAuthInformationHolder.h" +#include "nsIStringBundle.h" +#include "nsIPromptService.h" +#include "netCore.h" +#include "nsIHttpAuthenticableChannel.h" +#include "nsIURI.h" +#include "nsContentUtils.h" +#include "nsHttpBasicAuth.h" +#include "nsHttpDigestAuth.h" +#include "nsHttpNegotiateAuth.h" +#include "nsHttpNTLMAuth.h" +#include "nsServiceManagerUtils.h" +#include "nsIURL.h" +#include "mozilla/StaticPrefs_network.h" +#include "mozilla/StaticPrefs_prompts.h" +#include "mozilla/Telemetry.h" +#include "nsIProxiedChannel.h" +#include "nsIProxyInfo.h" + +namespace mozilla::net { + +#define SUBRESOURCE_AUTH_DIALOG_DISALLOW_ALL 0 +#define SUBRESOURCE_AUTH_DIALOG_DISALLOW_CROSS_ORIGIN 1 +#define SUBRESOURCE_AUTH_DIALOG_ALLOW_ALL 2 + +#define HTTP_AUTH_DIALOG_TOP_LEVEL_DOC 29 +#define HTTP_AUTH_DIALOG_SAME_ORIGIN_SUBRESOURCE 30 +#define HTTP_AUTH_DIALOG_SAME_ORIGIN_XHR 31 +#define HTTP_AUTH_DIALOG_NON_WEB_CONTENT 32 + +#define HTTP_AUTH_BASIC_INSECURE 0 +#define HTTP_AUTH_BASIC_SECURE 1 +#define HTTP_AUTH_DIGEST_INSECURE 2 +#define HTTP_AUTH_DIGEST_SECURE 3 +#define HTTP_AUTH_NTLM_INSECURE 4 +#define HTTP_AUTH_NTLM_SECURE 5 +#define HTTP_AUTH_NEGOTIATE_INSECURE 6 +#define HTTP_AUTH_NEGOTIATE_SECURE 7 + +#define MAX_DISPLAYED_USER_LENGTH 64 +#define MAX_DISPLAYED_HOST_LENGTH 64 + +static void GetOriginAttributesSuffix(nsIChannel* aChan, nsACString& aSuffix) { + OriginAttributes oa; + + // Deliberately ignoring the result and going with defaults + if (aChan) { + StoragePrincipalHelper::GetOriginAttributesForNetworkState(aChan, oa); + } + + oa.CreateSuffix(aSuffix); +} + +nsHttpChannelAuthProvider::nsHttpChannelAuthProvider() + : mAuthChannel(nullptr), + mPort(-1), + mUsingSSL(false), + mProxyUsingSSL(false), + mIsPrivate(false), + mProxyAuthContinuationState(nullptr), + mAuthContinuationState(nullptr), + mProxyAuth(false), + mTriedProxyAuth(false), + mTriedHostAuth(false), + mSuppressDefensiveAuth(false), + mCrossOrigin(false), + mConnectionBased(false), + mHttpHandler(gHttpHandler) {} + +nsHttpChannelAuthProvider::~nsHttpChannelAuthProvider() { + MOZ_ASSERT(!mAuthChannel, "Disconnect wasn't called"); +} + +NS_IMETHODIMP +nsHttpChannelAuthProvider::Init(nsIHttpAuthenticableChannel* channel) { + MOZ_ASSERT(channel, "channel expected!"); + + mAuthChannel = channel; + + nsresult rv = mAuthChannel->GetURI(getter_AddRefs(mURI)); + if (NS_FAILED(rv)) return rv; + + rv = mAuthChannel->GetIsSSL(&mUsingSSL); + if (NS_FAILED(rv)) return rv; + + nsCOMPtr proxied(channel); + if (proxied) { + nsCOMPtr pi; + rv = proxied->GetProxyInfo(getter_AddRefs(pi)); + if (NS_FAILED(rv)) return rv; + + if (pi) { + nsAutoCString proxyType; + rv = pi->GetType(proxyType); + if (NS_FAILED(rv)) return rv; + + mProxyUsingSSL = proxyType.EqualsLiteral("https"); + } + } + + rv = mURI->GetAsciiHost(mHost); + if (NS_FAILED(rv)) return rv; + + // reject the URL if it doesn't specify a host + if (mHost.IsEmpty()) return NS_ERROR_MALFORMED_URI; + + rv = mURI->GetPort(&mPort); + if (NS_FAILED(rv)) return rv; + + nsCOMPtr bareChannel = do_QueryInterface(channel); + mIsPrivate = NS_UsePrivateBrowsing(bareChannel); + + return NS_OK; +} + +NS_IMETHODIMP +nsHttpChannelAuthProvider::ProcessAuthentication(uint32_t httpStatus, + bool SSLConnectFailed) { + LOG( + ("nsHttpChannelAuthProvider::ProcessAuthentication " + "[this=%p channel=%p code=%u SSLConnectFailed=%d]\n", + this, mAuthChannel, httpStatus, SSLConnectFailed)); + + MOZ_ASSERT(mAuthChannel, "Channel not initialized"); + + nsCOMPtr proxyInfo; + nsresult rv = mAuthChannel->GetProxyInfo(getter_AddRefs(proxyInfo)); + if (NS_FAILED(rv)) return rv; + if (proxyInfo) { + mProxyInfo = do_QueryInterface(proxyInfo); + if (!mProxyInfo) return NS_ERROR_NO_INTERFACE; + } + + nsAutoCString challenges; + mProxyAuth = (httpStatus == 407); + + rv = PrepareForAuthentication(mProxyAuth); + if (NS_FAILED(rv)) return rv; + + if (mProxyAuth) { + // only allow a proxy challenge if we have a proxy server configured. + // otherwise, we could inadvertently expose the user's proxy + // credentials to an origin server. We could attempt to proceed as + // if we had received a 401 from the server, but why risk flirting + // with trouble? IE similarly rejects 407s when a proxy server is + // not configured, so there's no reason not to do the same. + if (!UsingHttpProxy()) { + LOG(("rejecting 407 when proxy server not configured!\n")); + return NS_ERROR_UNEXPECTED; + } + if (UsingSSL() && !SSLConnectFailed) { + // we need to verify that this challenge came from the proxy + // server itself, and not some server on the other side of the + // SSL tunnel. + LOG(("rejecting 407 from origin server!\n")); + return NS_ERROR_UNEXPECTED; + } + rv = mAuthChannel->GetProxyChallenges(challenges); + } else + rv = mAuthChannel->GetWWWChallenges(challenges); + if (NS_FAILED(rv)) return rv; + + nsAutoCString creds; + rv = GetCredentials(challenges.get(), mProxyAuth, creds); + if (rv == NS_ERROR_IN_PROGRESS) return rv; + if (NS_FAILED(rv)) + LOG(("unable to authenticate\n")); + else { + // set the authentication credentials + if (mProxyAuth) + rv = mAuthChannel->SetProxyCredentials(creds); + else + rv = mAuthChannel->SetWWWCredentials(creds); + } + return rv; +} + +NS_IMETHODIMP +nsHttpChannelAuthProvider::AddAuthorizationHeaders( + bool aDontUseCachedWWWCreds) { + LOG( + ("nsHttpChannelAuthProvider::AddAuthorizationHeaders? " + "[this=%p channel=%p]\n", + this, mAuthChannel)); + + MOZ_ASSERT(mAuthChannel, "Channel not initialized"); + + nsCOMPtr proxyInfo; + nsresult rv = mAuthChannel->GetProxyInfo(getter_AddRefs(proxyInfo)); + if (NS_FAILED(rv)) return rv; + if (proxyInfo) { + mProxyInfo = do_QueryInterface(proxyInfo); + if (!mProxyInfo) return NS_ERROR_NO_INTERFACE; + } + + uint32_t loadFlags; + rv = mAuthChannel->GetLoadFlags(&loadFlags); + if (NS_FAILED(rv)) return rv; + + // this getter never fails + nsHttpAuthCache* authCache = gHttpHandler->AuthCache(mIsPrivate); + + // check if proxy credentials should be sent + const char* proxyHost = ProxyHost(); + if (proxyHost && UsingHttpProxy()) { + SetAuthorizationHeader(authCache, nsHttp::Proxy_Authorization, "http", + proxyHost, ProxyPort(), + nullptr, // proxy has no path + mProxyIdent); + } + + if (loadFlags & nsIRequest::LOAD_ANONYMOUS) { + LOG(("Skipping Authorization header for anonymous load\n")); + return NS_OK; + } + + if (aDontUseCachedWWWCreds) { + LOG( + ("Authorization header already present:" + " skipping adding auth header from cache\n")); + return NS_OK; + } + + // check if server credentials should be sent + nsAutoCString path, scheme; + if (NS_SUCCEEDED(GetCurrentPath(path)) && + NS_SUCCEEDED(mURI->GetScheme(scheme))) { + SetAuthorizationHeader(authCache, nsHttp::Authorization, scheme.get(), + Host(), Port(), path.get(), mIdent); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsHttpChannelAuthProvider::CheckForSuperfluousAuth() { + LOG( + ("nsHttpChannelAuthProvider::CheckForSuperfluousAuth? " + "[this=%p channel=%p]\n", + this, mAuthChannel)); + + MOZ_ASSERT(mAuthChannel, "Channel not initialized"); + + // we've been called because it has been determined that this channel is + // getting loaded without taking the userpass from the URL. if the URL + // contained a userpass, then (provided some other conditions are true), + // we'll give the user an opportunity to abort the channel as this might be + // an attempt to spoof a different site (see bug 232567). + if (!ConfirmAuth("SuperfluousAuth", true)) { + // calling cancel here sets our mStatus and aborts the HTTP + // transaction, which prevents OnDataAvailable events. + Unused << mAuthChannel->Cancel(NS_ERROR_ABORT); + return NS_ERROR_ABORT; + } + return NS_OK; +} + +NS_IMETHODIMP +nsHttpChannelAuthProvider::Cancel(nsresult status) { + MOZ_ASSERT(mAuthChannel, "Channel not initialized"); + + if (mAsyncPromptAuthCancelable) { + mAsyncPromptAuthCancelable->Cancel(status); + mAsyncPromptAuthCancelable = nullptr; + } + + if (mGenerateCredentialsCancelable) { + mGenerateCredentialsCancelable->Cancel(status); + mGenerateCredentialsCancelable = nullptr; + } + return NS_OK; +} + +NS_IMETHODIMP +nsHttpChannelAuthProvider::Disconnect(nsresult status) { + mAuthChannel = nullptr; + + if (mAsyncPromptAuthCancelable) { + mAsyncPromptAuthCancelable->Cancel(status); + mAsyncPromptAuthCancelable = nullptr; + } + + if (mGenerateCredentialsCancelable) { + mGenerateCredentialsCancelable->Cancel(status); + mGenerateCredentialsCancelable = nullptr; + } + + NS_IF_RELEASE(mProxyAuthContinuationState); + NS_IF_RELEASE(mAuthContinuationState); + + return NS_OK; +} + +// buf contains "domain\user" +static void ParseUserDomain(char16_t* buf, const char16_t** user, + const char16_t** domain) { + char16_t* p = buf; + while (*p && *p != '\\') ++p; + if (!*p) return; + *p = '\0'; + *domain = buf; + *user = p + 1; +} + +// helper function for setting identity from raw user:pass +static void SetIdent(nsHttpAuthIdentity& ident, uint32_t authFlags, + char16_t* userBuf, char16_t* passBuf) { + const char16_t* user = userBuf; + const char16_t* domain = nullptr; + + if (authFlags & nsIHttpAuthenticator::IDENTITY_INCLUDES_DOMAIN) + ParseUserDomain(userBuf, &user, &domain); + + DebugOnly rv = ident.Set(domain, user, passBuf); + MOZ_ASSERT(NS_SUCCEEDED(rv)); +} + +// helper function for getting an auth prompt from an interface requestor +static void GetAuthPrompt(nsIInterfaceRequestor* ifreq, bool proxyAuth, + nsIAuthPrompt2** result) { + if (!ifreq) return; + + uint32_t promptReason; + if (proxyAuth) + promptReason = nsIAuthPromptProvider::PROMPT_PROXY; + else + promptReason = nsIAuthPromptProvider::PROMPT_NORMAL; + + nsCOMPtr promptProvider = do_GetInterface(ifreq); + if (promptProvider) + promptProvider->GetAuthPrompt(promptReason, NS_GET_IID(nsIAuthPrompt2), + reinterpret_cast(result)); + else + NS_QueryAuthPrompt2(ifreq, result); +} + +// generate credentials for the given challenge, and update the auth cache. +nsresult nsHttpChannelAuthProvider::GenCredsAndSetEntry( + nsIHttpAuthenticator* auth, bool proxyAuth, const char* scheme, + const char* host, int32_t port, const char* directory, const char* realm, + const char* challenge, const nsHttpAuthIdentity& ident, + nsCOMPtr& sessionState, char** result) { + nsresult rv; + nsISupports* ss = sessionState; + + // set informations that depend on whether + // we're authenticating against a proxy + // or a webserver + nsISupports** continuationState; + + if (proxyAuth) { + continuationState = &mProxyAuthContinuationState; + } else { + continuationState = &mAuthContinuationState; + } + + rv = auth->GenerateCredentialsAsync( + mAuthChannel, this, challenge, proxyAuth, ident.Domain(), ident.User(), + ident.Password(), ss, *continuationState, + getter_AddRefs(mGenerateCredentialsCancelable)); + if (NS_SUCCEEDED(rv)) { + // Calling generate credentials async, results will be dispatched to the + // main thread by calling OnCredsGenerated method + return NS_ERROR_IN_PROGRESS; + } + + uint32_t generateFlags; + rv = auth->GenerateCredentials( + mAuthChannel, challenge, proxyAuth, ident.Domain(), ident.User(), + ident.Password(), &ss, &*continuationState, &generateFlags, result); + + sessionState.swap(ss); + if (NS_FAILED(rv)) return rv; + + // don't log this in release build since it could contain sensitive info. +#ifdef DEBUG + LOG(("generated creds: %s\n", *result)); +#endif + + return UpdateCache(auth, scheme, host, port, directory, realm, challenge, + ident, *result, generateFlags, sessionState, proxyAuth); +} + +nsresult nsHttpChannelAuthProvider::UpdateCache( + nsIHttpAuthenticator* auth, const char* scheme, const char* host, + int32_t port, const char* directory, const char* realm, + const char* challenge, const nsHttpAuthIdentity& ident, const char* creds, + uint32_t generateFlags, nsISupports* sessionState, bool aProxyAuth) { + nsresult rv; + + uint32_t authFlags; + rv = auth->GetAuthFlags(&authFlags); + if (NS_FAILED(rv)) return rv; + + // find out if this authenticator allows reuse of credentials and/or + // challenge. + bool saveCreds = + 0 != (authFlags & nsIHttpAuthenticator::REUSABLE_CREDENTIALS); + bool saveChallenge = + 0 != (authFlags & nsIHttpAuthenticator::REUSABLE_CHALLENGE); + + bool saveIdentity = + 0 == (generateFlags & nsIHttpAuthenticator::USING_INTERNAL_IDENTITY); + + // this getter never fails + nsHttpAuthCache* authCache = gHttpHandler->AuthCache(mIsPrivate); + + nsAutoCString suffix; + if (!aProxyAuth) { + // We don't isolate proxy credentials cache entries with the origin suffix + // as it would only annoy users with authentication dialogs popping up. + nsCOMPtr chan = do_QueryInterface(mAuthChannel); + GetOriginAttributesSuffix(chan, suffix); + } + + // create a cache entry. we do this even though we don't yet know that + // these credentials are valid b/c we need to avoid prompting the user + // more than once in case the credentials are valid. + // + // if the credentials are not reusable, then we don't bother sticking + // them in the auth cache. + rv = authCache->SetAuthEntry(scheme, host, port, directory, realm, + saveCreds ? creds : nullptr, + saveChallenge ? challenge : nullptr, suffix, + saveIdentity ? &ident : nullptr, sessionState); + return rv; +} + +NS_IMETHODIMP nsHttpChannelAuthProvider::ClearProxyIdent() { + LOG(("nsHttpChannelAuthProvider::ClearProxyIdent [this=%p]\n", this)); + + mProxyIdent.Clear(); + return NS_OK; +} + +nsresult nsHttpChannelAuthProvider::PrepareForAuthentication(bool proxyAuth) { + LOG( + ("nsHttpChannelAuthProvider::PrepareForAuthentication " + "[this=%p channel=%p]\n", + this, mAuthChannel)); + + if (!proxyAuth) { + // reset the current proxy continuation state because our last + // authentication attempt was completed successfully. + NS_IF_RELEASE(mProxyAuthContinuationState); + LOG((" proxy continuation state has been reset")); + } + + if (!UsingHttpProxy() || mProxyAuthType.IsEmpty()) return NS_OK; + + // We need to remove any Proxy_Authorization header left over from a + // non-request based authentication handshake (e.g., for NTLM auth). + + nsresult rv; + nsCOMPtr precedingAuth; + nsCString proxyAuthType; + rv = GetAuthenticator(mProxyAuthType.get(), proxyAuthType, + getter_AddRefs(precedingAuth)); + if (NS_FAILED(rv)) return rv; + + uint32_t precedingAuthFlags; + rv = precedingAuth->GetAuthFlags(&precedingAuthFlags); + if (NS_FAILED(rv)) return rv; + + if (!(precedingAuthFlags & nsIHttpAuthenticator::REQUEST_BASED)) { + nsAutoCString challenges; + rv = mAuthChannel->GetProxyChallenges(challenges); + if (NS_FAILED(rv)) { + // delete the proxy authorization header because we weren't + // asked to authenticate + rv = mAuthChannel->SetProxyCredentials(""_ns); + if (NS_FAILED(rv)) return rv; + LOG((" cleared proxy authorization header")); + } + } + + return NS_OK; +} + +nsresult nsHttpChannelAuthProvider::GetCredentials(const char* challenges, + bool proxyAuth, + nsCString& creds) { + nsCOMPtr auth; + nsAutoCString challenge; + + nsCString authType; // force heap allocation to enable string sharing since + // we'll be assigning this value into mAuthType. + + // set informations that depend on whether we're authenticating against a + // proxy or a webserver + nsISupports** currentContinuationState; + nsCString* currentAuthType; + + if (proxyAuth) { + currentContinuationState = &mProxyAuthContinuationState; + currentAuthType = &mProxyAuthType; + } else { + currentContinuationState = &mAuthContinuationState; + currentAuthType = &mAuthType; + } + + nsresult rv = NS_ERROR_NOT_AVAILABLE; + bool gotCreds = false; + + // figure out which challenge we can handle and which authenticator to use. + for (const char* eol = challenges - 1; eol;) { + const char* p = eol + 1; + + // get the challenge string (LF separated -- see nsHttpHeaderArray) + if ((eol = strchr(p, '\n')) != nullptr) + challenge.Assign(p, eol - p); + else + challenge.Assign(p); + + rv = GetAuthenticator(challenge.get(), authType, getter_AddRefs(auth)); + if (NS_SUCCEEDED(rv)) { + // + // if we've already selected an auth type from a previous challenge + // received while processing this channel, then skip others until + // we find a challenge corresponding to the previously tried auth + // type. + // + if (!currentAuthType->IsEmpty() && authType != *currentAuthType) continue; + + // + // we allow the routines to run all the way through before we + // decide if they are valid. + // + // we don't worry about the auth cache being altered because that + // would have been the last step, and if the error is from updating + // the authcache it wasn't really altered anyway. -CTN + // + // at this point the code is really only useful for client side + // errors (it will not automatically fail over to do a different + // auth type if the server keeps rejecting what is being sent, even + // if a particular auth method only knows 1 thing, like a + // non-identity based authentication method) + // + rv = GetCredentialsForChallenge(challenge.get(), authType.get(), + proxyAuth, auth, creds); + if (NS_SUCCEEDED(rv)) { + gotCreds = true; + *currentAuthType = authType; + + break; + } + if (rv == NS_ERROR_IN_PROGRESS) { + // authentication prompt has been invoked and result is + // expected asynchronously, save current challenge being + // processed and all remaining challenges to use later in + // OnAuthAvailable and now immediately return + mCurrentChallenge = challenge; + mRemainingChallenges = eol ? eol + 1 : nullptr; + return rv; + } + + // reset the auth type and continuation state + NS_IF_RELEASE(*currentContinuationState); + currentAuthType->Truncate(); + } + } + + if (!gotCreds && !currentAuthType->IsEmpty()) { + // looks like we never found the auth type we were looking for. + // reset the auth type and continuation state, and try again. + currentAuthType->Truncate(); + NS_IF_RELEASE(*currentContinuationState); + + rv = GetCredentials(challenges, proxyAuth, creds); + } + + return rv; +} + +nsresult nsHttpChannelAuthProvider::GetAuthorizationMembers( + bool proxyAuth, nsACString& scheme, const char*& host, int32_t& port, + nsACString& path, nsHttpAuthIdentity*& ident, + nsISupports**& continuationState) { + if (proxyAuth) { + MOZ_ASSERT(UsingHttpProxy(), + "proxyAuth is true, but no HTTP proxy is configured!"); + + host = ProxyHost(); + port = ProxyPort(); + ident = &mProxyIdent; + scheme.AssignLiteral("http"); + + continuationState = &mProxyAuthContinuationState; + } else { + host = Host(); + port = Port(); + ident = &mIdent; + + nsresult rv; + rv = GetCurrentPath(path); + if (NS_FAILED(rv)) return rv; + + rv = mURI->GetScheme(scheme); + if (NS_FAILED(rv)) return rv; + + continuationState = &mAuthContinuationState; + } + + return NS_OK; +} + +nsresult nsHttpChannelAuthProvider::GetCredentialsForChallenge( + const char* challenge, const char* authType, bool proxyAuth, + nsIHttpAuthenticator* auth, nsCString& creds) { + LOG( + ("nsHttpChannelAuthProvider::GetCredentialsForChallenge " + "[this=%p channel=%p proxyAuth=%d challenges=%s]\n", + this, mAuthChannel, proxyAuth, challenge)); + + // this getter never fails + nsHttpAuthCache* authCache = gHttpHandler->AuthCache(mIsPrivate); + + uint32_t authFlags; + nsresult rv = auth->GetAuthFlags(&authFlags); + if (NS_FAILED(rv)) return rv; + + nsAutoCString realm; + ParseRealm(challenge, realm); + + // if no realm, then use the auth type as the realm. ToUpperCase so the + // ficticious realm stands out a bit more. + // XXX this will cause some single signon misses! + // XXX this was meant to be used with NTLM, which supplies no realm. + /* + if (realm.IsEmpty()) { + realm = authType; + ToUpperCase(realm); + } + */ + + // set informations that depend on whether + // we're authenticating against a proxy + // or a webserver + const char* host; + int32_t port; + nsHttpAuthIdentity* ident; + nsAutoCString path, scheme; + bool identFromURI = false; + nsISupports** continuationState; + + rv = GetAuthorizationMembers(proxyAuth, scheme, host, port, path, ident, + continuationState); + if (NS_FAILED(rv)) return rv; + + uint32_t loadFlags; + rv = mAuthChannel->GetLoadFlags(&loadFlags); + if (NS_FAILED(rv)) return rv; + + // Fill only for non-proxy auth, proxy credentials are not OA-isolated. + nsAutoCString suffix; + + if (!proxyAuth) { + nsCOMPtr chan = do_QueryInterface(mAuthChannel); + GetOriginAttributesSuffix(chan, suffix); + + // if this is the first challenge, then try using the identity + // specified in the URL. + if (mIdent.IsEmpty()) { + GetIdentityFromURI(authFlags, mIdent); + identFromURI = !mIdent.IsEmpty(); + } + + if ((loadFlags & nsIRequest::LOAD_ANONYMOUS) && !identFromURI) { + LOG(("Skipping authentication for anonymous non-proxy request\n")); + return NS_ERROR_NOT_AVAILABLE; + } + + // Let explicit URL credentials pass + // regardless of the LOAD_ANONYMOUS flag + } else if ((loadFlags & nsIRequest::LOAD_ANONYMOUS) && !UsingHttpProxy()) { + LOG(("Skipping authentication for anonymous non-proxy request\n")); + return NS_ERROR_NOT_AVAILABLE; + } + + // + // if we already tried some credentials for this transaction, then + // we need to possibly clear them from the cache, unless the credentials + // in the cache have changed, in which case we'd want to give them a + // try instead. + // + nsHttpAuthEntry* entry = nullptr; + Unused << authCache->GetAuthEntryForDomain(scheme.get(), host, port, + realm.get(), suffix, &entry); + + // hold reference to the auth session state (in case we clear our + // reference to the entry). + nsCOMPtr sessionStateGrip; + if (entry) sessionStateGrip = entry->mMetaData; + + // remember if we already had the continuation state. it means we are in + // the middle of the authentication exchange and the connection must be + // kept sticky then (and only then). + bool authAtProgress = !!*continuationState; + + // for digest auth, maybe our cached nonce value simply timed out... + bool identityInvalid; + nsISupports* sessionState = sessionStateGrip; + rv = + auth->ChallengeReceived(mAuthChannel, challenge, proxyAuth, &sessionState, + &*continuationState, &identityInvalid); + sessionStateGrip.swap(sessionState); + if (NS_FAILED(rv)) return rv; + + LOG((" identity invalid = %d\n", identityInvalid)); + + if (mConnectionBased && identityInvalid) { + // If the flag is set and identity is invalid, it means we received the + // first challange for a new negotiation round after negotiating a + // connection based auth failed (invalid password). The mConnectionBased + // flag is set later for the newly received challenge, so here it reflects + // the previous 401/7 response schema. + rv = mAuthChannel->CloseStickyConnection(); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + if (!proxyAuth) { + // We must clear proxy ident in the following scenario + explanation: + // - we are authenticating to an NTLM proxy and an NTLM server + // - we successfully authenticated to the proxy, mProxyIdent keeps + // the user name/domain and password, the identity has also been cached + // - we just threw away the connection because we are now asking for + // creds for the server (WWW auth) + // - hence, we will have to auth to the proxy again as well + // - if we didn't clear the proxy identity, it would be considered + // as non-valid and we would ask the user again ; clearing it forces + // use of the cached identity and not asking the user again + ClearProxyIdent(); + } + } + + mConnectionBased = !!(authFlags & nsIHttpAuthenticator::CONNECTION_BASED); + + // It's legal if the peer closes the connection after the first 401/7. + // Making the connection sticky will prevent its restart giving the user + // a 'network reset' error every time. Hence, we mark the connection + // as restartable. + mAuthChannel->ConnectionRestartable(!authAtProgress); + + if (identityInvalid) { + if (entry) { + if (ident->Equals(entry->Identity())) { + if (!identFromURI) { + LOG((" clearing bad auth cache entry\n")); + // ok, we've already tried this user identity, so clear the + // corresponding entry from the auth cache. + authCache->ClearAuthEntry(scheme.get(), host, port, realm.get(), + suffix); + entry = nullptr; + ident->Clear(); + } + } else if (!identFromURI || + (nsCRT::strcmp(ident->User(), entry->Identity().User()) == 0 && + !(loadFlags & (nsIChannel::LOAD_ANONYMOUS | + nsIChannel::LOAD_EXPLICIT_CREDENTIALS)))) { + LOG((" taking identity from auth cache\n")); + // the password from the auth cache is more likely to be + // correct than the one in the URL. at least, we know that it + // works with the given username. it is possible for a server + // to distinguish logons based on the supplied password alone, + // but that would be quite unusual... and i don't think we need + // to worry about such unorthodox cases. + rv = ident->Set(entry->Identity()); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + identFromURI = false; + if (entry->Creds()[0] != '\0') { + LOG((" using cached credentials!\n")); + creds.Assign(entry->Creds()); + return entry->AddPath(path.get()); + } + } + } else if (!identFromURI) { + // hmm... identity invalid, but no auth entry! the realm probably + // changed (see bug 201986). + ident->Clear(); + } + + if (!entry && ident->IsEmpty()) { + uint32_t level = nsIAuthPrompt2::LEVEL_NONE; + if ((!proxyAuth && mUsingSSL) || (proxyAuth && mProxyUsingSSL)) + level = nsIAuthPrompt2::LEVEL_SECURE; + else if (authFlags & nsIHttpAuthenticator::IDENTITY_ENCRYPTED) + level = nsIAuthPrompt2::LEVEL_PW_ENCRYPTED; + + // Collect statistics on how frequently the various types of HTTP + // authentication are used over SSL and non-SSL connections. + if (Telemetry::CanRecordPrereleaseData()) { + if ("basic"_ns.LowerCaseEqualsASCII(authType)) { + Telemetry::Accumulate( + Telemetry::HTTP_AUTH_TYPE_STATS, + UsingSSL() ? HTTP_AUTH_BASIC_SECURE : HTTP_AUTH_BASIC_INSECURE); + } else if ("digest"_ns.LowerCaseEqualsASCII(authType)) { + Telemetry::Accumulate( + Telemetry::HTTP_AUTH_TYPE_STATS, + UsingSSL() ? HTTP_AUTH_DIGEST_SECURE : HTTP_AUTH_DIGEST_INSECURE); + } else if ("ntlm"_ns.LowerCaseEqualsASCII(authType)) { + Telemetry::Accumulate( + Telemetry::HTTP_AUTH_TYPE_STATS, + UsingSSL() ? HTTP_AUTH_NTLM_SECURE : HTTP_AUTH_NTLM_INSECURE); + } else if ("negotiate"_ns.LowerCaseEqualsASCII(authType)) { + Telemetry::Accumulate(Telemetry::HTTP_AUTH_TYPE_STATS, + UsingSSL() ? HTTP_AUTH_NEGOTIATE_SECURE + : HTTP_AUTH_NEGOTIATE_INSECURE); + } + } + + // Depending on the pref setting, the authentication dialog may be + // blocked for all sub-resources, blocked for cross-origin + // sub-resources, or always allowed for sub-resources. + // For more details look at the bug 647010. + // BlockPrompt will set mCrossOrigin parameter as well. + if (BlockPrompt(proxyAuth)) { + LOG(( + "nsHttpChannelAuthProvider::GetCredentialsForChallenge: " + "Prompt is blocked [this=%p pref=%d img-pref=%d " + "non-web-content-triggered-pref=%d]\n", + this, StaticPrefs::network_auth_subresource_http_auth_allow(), + StaticPrefs:: + network_auth_subresource_img_cross_origin_http_auth_allow(), + StaticPrefs:: + network_auth_non_web_content_triggered_resources_http_auth_allow())); + return NS_ERROR_ABORT; + } + + // at this point we are forced to interact with the user to get + // their username and password for this domain. + rv = PromptForIdentity(level, proxyAuth, realm.get(), authType, authFlags, + *ident); + if (NS_FAILED(rv)) return rv; + identFromURI = false; + } + } + + if (identFromURI) { + // Warn the user before automatically using the identity from the URL + // to automatically log them into a site (see bug 232567). + if (!ConfirmAuth("AutomaticAuth", false)) { + // calling cancel here sets our mStatus and aborts the HTTP + // transaction, which prevents OnDataAvailable events. + rv = mAuthChannel->Cancel(NS_ERROR_ABORT); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + // this return code alone is not equivalent to Cancel, since + // it only instructs our caller that authentication failed. + // without an explicit call to Cancel, our caller would just + // load the page that accompanies the HTTP auth challenge. + return NS_ERROR_ABORT; + } + } + + // + // get credentials for the given user:pass + // + // always store the credentials we're trying now so that they will be used + // on subsequent links. This will potentially remove good credentials from + // the cache. This is ok as we don't want to use cached credentials if the + // user specified something on the URI or in another manner. This is so + // that we don't transparently authenticate as someone they're not + // expecting to authenticate as. + // + nsCString result; + rv = GenCredsAndSetEntry(auth, proxyAuth, scheme.get(), host, port, + path.get(), realm.get(), challenge, *ident, + sessionStateGrip, getter_Copies(result)); + if (NS_SUCCEEDED(rv)) creds = result; + return rv; +} + +bool nsHttpChannelAuthProvider::BlockPrompt(bool proxyAuth) { + // Verify that it's ok to prompt for credentials here, per spec + // http://xhr.spec.whatwg.org/#the-send%28%29-method + + nsCOMPtr chanInternal = + do_QueryInterface(mAuthChannel); + MOZ_ASSERT(chanInternal); + + if (chanInternal->GetBlockAuthPrompt()) { + LOG( + ("nsHttpChannelAuthProvider::BlockPrompt: Prompt is blocked " + "[this=%p channel=%p]\n", + this, mAuthChannel)); + return true; + } + + if (proxyAuth) { + // Do not block auth-dialog if this is a proxy authentication. + return false; + } + + nsCOMPtr chan = do_QueryInterface(mAuthChannel); + nsCOMPtr loadInfo = chan->LoadInfo(); + + // We will treat loads w/o loadInfo as a top level document. + bool topDoc = true; + bool xhr = false; + bool nonWebContent = false; + + if (loadInfo->GetExternalContentPolicyType() != + ExtContentPolicy::TYPE_DOCUMENT) { + topDoc = false; + } + + if (!topDoc) { + nsCOMPtr triggeringPrinc = loadInfo->TriggeringPrincipal(); + if (triggeringPrinc->IsSystemPrincipal()) { + nonWebContent = true; + } + } + + if (loadInfo->GetExternalContentPolicyType() == + ExtContentPolicy::TYPE_XMLHTTPREQUEST) { + xhr = true; + } + + if (!topDoc && !xhr) { + nsCOMPtr topURI; + Unused << chanInternal->GetTopWindowURI(getter_AddRefs(topURI)); + if (topURI) { + mCrossOrigin = !NS_SecurityCompareURIs(topURI, mURI, true); + } else { + nsIPrincipal* loadingPrinc = loadInfo->GetLoadingPrincipal(); + MOZ_ASSERT(loadingPrinc); + bool sameOrigin = false; + loadingPrinc->IsSameOrigin(mURI, false, &sameOrigin); + mCrossOrigin = !sameOrigin; + } + } + + if (Telemetry::CanRecordPrereleaseData()) { + if (topDoc) { + Telemetry::Accumulate(Telemetry::HTTP_AUTH_DIALOG_STATS_3, + HTTP_AUTH_DIALOG_TOP_LEVEL_DOC); + } else if (nonWebContent) { + Telemetry::Accumulate(Telemetry::HTTP_AUTH_DIALOG_STATS_3, + HTTP_AUTH_DIALOG_NON_WEB_CONTENT); + } else if (!mCrossOrigin) { + if (xhr) { + Telemetry::Accumulate(Telemetry::HTTP_AUTH_DIALOG_STATS_3, + HTTP_AUTH_DIALOG_SAME_ORIGIN_XHR); + } else { + Telemetry::Accumulate(Telemetry::HTTP_AUTH_DIALOG_STATS_3, + HTTP_AUTH_DIALOG_SAME_ORIGIN_SUBRESOURCE); + } + } else { + Telemetry::Accumulate( + Telemetry::HTTP_AUTH_DIALOG_STATS_3, + static_cast(loadInfo->GetExternalContentPolicyType())); + } + } + + if (!topDoc && + !StaticPrefs:: + network_auth_non_web_content_triggered_resources_http_auth_allow() && + nonWebContent) { + return true; + } + + switch (StaticPrefs::network_auth_subresource_http_auth_allow()) { + case SUBRESOURCE_AUTH_DIALOG_DISALLOW_ALL: + // Do not open the http-authentication credentials dialog for + // the sub-resources. + return !topDoc && !xhr; + case SUBRESOURCE_AUTH_DIALOG_DISALLOW_CROSS_ORIGIN: + // Open the http-authentication credentials dialog for + // the sub-resources only if they are not cross-origin. + return !topDoc && !xhr && mCrossOrigin; + case SUBRESOURCE_AUTH_DIALOG_ALLOW_ALL: + // Allow the http-authentication dialog for subresources. + // If pref network.auth.subresource-img-cross-origin-http-auth-allow + // is set, http-authentication dialog for image subresources is + // blocked. + if (mCrossOrigin && + !StaticPrefs:: + network_auth_subresource_img_cross_origin_http_auth_allow() && + loadInfo && + ((loadInfo->GetExternalContentPolicyType() == + ExtContentPolicy::TYPE_IMAGE) || + (loadInfo->GetExternalContentPolicyType() == + ExtContentPolicy::TYPE_IMAGESET))) { + return true; + } + return false; + default: + // This is an invalid value. + MOZ_ASSERT(false, "A non valid value!"); + } + return false; +} + +inline void GetAuthType(const char* challenge, nsCString& authType) { + const char* p; + + // get the challenge type + if ((p = strchr(challenge, ' ')) != nullptr) + authType.Assign(challenge, p - challenge); + else + authType.Assign(challenge); +} + +nsresult nsHttpChannelAuthProvider::GetAuthenticator( + const char* challenge, nsCString& authType, nsIHttpAuthenticator** auth) { + LOG(("nsHttpChannelAuthProvider::GetAuthenticator [this=%p channel=%p]\n", + this, mAuthChannel)); + + GetAuthType(challenge, authType); + + // normalize to lowercase + ToLowerCase(authType); + + nsCOMPtr authenticator; + if (authType.EqualsLiteral("negotiate")) { + authenticator = nsHttpNegotiateAuth::GetOrCreate(); + } else if (authType.EqualsLiteral("basic")) { + authenticator = nsHttpBasicAuth::GetOrCreate(); + } else if (authType.EqualsLiteral("digest")) { + authenticator = nsHttpDigestAuth::GetOrCreate(); + } else if (authType.EqualsLiteral("ntlm")) { + authenticator = nsHttpNTLMAuth::GetOrCreate(); + } else { + return NS_ERROR_FACTORY_NOT_REGISTERED; + } + + MOZ_ASSERT(authenticator); + authenticator.forget(auth); + + return NS_OK; +} + +void nsHttpChannelAuthProvider::GetIdentityFromURI(uint32_t authFlags, + nsHttpAuthIdentity& ident) { + LOG(("nsHttpChannelAuthProvider::GetIdentityFromURI [this=%p channel=%p]\n", + this, mAuthChannel)); + + nsAutoString userBuf; + nsAutoString passBuf; + + // XXX i18n + nsAutoCString buf; + mURI->GetUsername(buf); + if (!buf.IsEmpty()) { + NS_UnescapeURL(buf); + CopyUTF8toUTF16(buf, userBuf); + mURI->GetPassword(buf); + if (!buf.IsEmpty()) { + NS_UnescapeURL(buf); + CopyUTF8toUTF16(buf, passBuf); + } + } + + if (!userBuf.IsEmpty()) { + SetIdent(ident, authFlags, (char16_t*)userBuf.get(), + (char16_t*)passBuf.get()); + } +} + +void nsHttpChannelAuthProvider::ParseRealm(const char* challenge, + nsACString& realm) { + // + // From RFC2617 section 1.2, the realm value is defined as such: + // + // realm = "realm" "=" realm-value + // realm-value = quoted-string + // + // but, we'll accept anything after the the "=" up to the first space, or + // end-of-line, if the string is not quoted. + // + + const char* p = PL_strcasestr(challenge, "realm="); + if (p) { + bool has_quote = false; + p += 6; + if (*p == '"') { + has_quote = true; + p++; + } + + const char* end; + if (has_quote) { + end = p; + while (*end) { + if (*end == '\\') { + // escaped character, store that one instead if not zero + if (!*++end) break; + } else if (*end == '\"') + // end of string + break; + + realm.Append(*end); + ++end; + } + } else { + // realm given without quotes + end = strchr(p, ' '); + if (end) + realm.Assign(p, end - p); + else + realm.Assign(p); + } + } +} + +class nsHTTPAuthInformation : public nsAuthInformationHolder { + public: + nsHTTPAuthInformation(uint32_t aFlags, const nsString& aRealm, + const nsCString& aAuthType) + : nsAuthInformationHolder(aFlags, aRealm, aAuthType) {} + + void SetToHttpAuthIdentity(uint32_t authFlags, nsHttpAuthIdentity& identity); +}; + +void nsHTTPAuthInformation::SetToHttpAuthIdentity( + uint32_t authFlags, nsHttpAuthIdentity& identity) { + DebugOnly rv = + identity.Set(Domain().get(), User().get(), Password().get()); + MOZ_ASSERT(NS_SUCCEEDED(rv)); +} + +nsresult nsHttpChannelAuthProvider::PromptForIdentity( + uint32_t level, bool proxyAuth, const char* realm, const char* authType, + uint32_t authFlags, nsHttpAuthIdentity& ident) { + LOG(("nsHttpChannelAuthProvider::PromptForIdentity [this=%p channel=%p]\n", + this, mAuthChannel)); + + nsresult rv; + + nsCOMPtr callbacks; + rv = mAuthChannel->GetNotificationCallbacks(getter_AddRefs(callbacks)); + if (NS_FAILED(rv)) return rv; + + nsCOMPtr loadGroup; + rv = mAuthChannel->GetLoadGroup(getter_AddRefs(loadGroup)); + if (NS_FAILED(rv)) return rv; + + nsCOMPtr authPrompt; + GetAuthPrompt(callbacks, proxyAuth, getter_AddRefs(authPrompt)); + if (!authPrompt && loadGroup) { + nsCOMPtr cbs; + loadGroup->GetNotificationCallbacks(getter_AddRefs(cbs)); + GetAuthPrompt(cbs, proxyAuth, getter_AddRefs(authPrompt)); + } + if (!authPrompt) return NS_ERROR_NO_INTERFACE; + + // XXX i18n: need to support non-ASCII realm strings (see bug 41489) + NS_ConvertASCIItoUTF16 realmU(realm); + + // prompt the user... + uint32_t promptFlags = 0; + if (proxyAuth) { + promptFlags |= nsIAuthInformation::AUTH_PROXY; + if (mTriedProxyAuth) promptFlags |= nsIAuthInformation::PREVIOUS_FAILED; + mTriedProxyAuth = true; + } else { + promptFlags |= nsIAuthInformation::AUTH_HOST; + if (mTriedHostAuth) promptFlags |= nsIAuthInformation::PREVIOUS_FAILED; + mTriedHostAuth = true; + } + + if (authFlags & nsIHttpAuthenticator::IDENTITY_INCLUDES_DOMAIN) + promptFlags |= nsIAuthInformation::NEED_DOMAIN; + + if (mCrossOrigin) { + promptFlags |= nsIAuthInformation::CROSS_ORIGIN_SUB_RESOURCE; + } + + RefPtr holder = new nsHTTPAuthInformation( + promptFlags, realmU, nsDependentCString(authType)); + if (!holder) return NS_ERROR_OUT_OF_MEMORY; + + nsCOMPtr channel(do_QueryInterface(mAuthChannel, &rv)); + if (NS_FAILED(rv)) return rv; + + rv = authPrompt->AsyncPromptAuth(channel, this, nullptr, level, holder, + getter_AddRefs(mAsyncPromptAuthCancelable)); + + if (NS_SUCCEEDED(rv)) { + // indicate using this error code that authentication prompt + // result is expected asynchronously + rv = NS_ERROR_IN_PROGRESS; + } else { + // Fall back to synchronous prompt + bool retval = false; + rv = authPrompt->PromptAuth(channel, level, holder, &retval); + if (NS_FAILED(rv)) return rv; + + if (!retval) + rv = NS_ERROR_ABORT; + else + holder->SetToHttpAuthIdentity(authFlags, ident); + } + + // remember that we successfully showed the user an auth dialog + if (!proxyAuth) mSuppressDefensiveAuth = true; + + if (mConnectionBased) { + // Connection can be reset by the server in the meantime user is entering + // the credentials. Result would be just a "Connection was reset" error. + // Hence, we drop the current regardless if the user would make it on time + // to provide credentials. + // It's OK to send the NTLM type 1 message (response to the plain "NTLM" + // challenge) on a new connection. + { + DebugOnly rv = mAuthChannel->CloseStickyConnection(); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + } + + return rv; +} + +NS_IMETHODIMP nsHttpChannelAuthProvider::OnAuthAvailable( + nsISupports* aContext, nsIAuthInformation* aAuthInfo) { + LOG(("nsHttpChannelAuthProvider::OnAuthAvailable [this=%p channel=%p]", this, + mAuthChannel)); + + mAsyncPromptAuthCancelable = nullptr; + if (!mAuthChannel) return NS_OK; + + nsresult rv; + + const char* host; + int32_t port; + nsHttpAuthIdentity* ident; + nsAutoCString path, scheme; + nsISupports** continuationState; + rv = GetAuthorizationMembers(mProxyAuth, scheme, host, port, path, ident, + continuationState); + if (NS_FAILED(rv)) OnAuthCancelled(aContext, false); + + nsAutoCString realm; + ParseRealm(mCurrentChallenge.get(), realm); + + nsCOMPtr chan = do_QueryInterface(mAuthChannel); + nsAutoCString suffix; + if (!mProxyAuth) { + // Fill only for non-proxy auth, proxy credentials are not OA-isolated. + GetOriginAttributesSuffix(chan, suffix); + } + + nsHttpAuthCache* authCache = gHttpHandler->AuthCache(mIsPrivate); + nsHttpAuthEntry* entry = nullptr; + Unused << authCache->GetAuthEntryForDomain(scheme.get(), host, port, + realm.get(), suffix, &entry); + + nsCOMPtr sessionStateGrip; + if (entry) sessionStateGrip = entry->mMetaData; + + nsAuthInformationHolder* holder = + static_cast(aAuthInfo); + rv = ident->Set(holder->Domain().get(), holder->User().get(), + holder->Password().get()); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + nsAutoCString unused; + nsCOMPtr auth; + rv = GetAuthenticator(mCurrentChallenge.get(), unused, getter_AddRefs(auth)); + if (NS_FAILED(rv)) { + MOZ_ASSERT(false, "GetAuthenticator failed"); + OnAuthCancelled(aContext, true); + return NS_OK; + } + + nsCString creds; + rv = GenCredsAndSetEntry(auth, mProxyAuth, scheme.get(), host, port, + path.get(), realm.get(), mCurrentChallenge.get(), + *ident, sessionStateGrip, getter_Copies(creds)); + + mCurrentChallenge.Truncate(); + if (NS_FAILED(rv)) { + OnAuthCancelled(aContext, true); + return NS_OK; + } + + return ContinueOnAuthAvailable(creds); +} + +NS_IMETHODIMP nsHttpChannelAuthProvider::OnAuthCancelled(nsISupports* aContext, + bool userCancel) { + LOG(("nsHttpChannelAuthProvider::OnAuthCancelled [this=%p channel=%p]", this, + mAuthChannel)); + + mAsyncPromptAuthCancelable = nullptr; + if (!mAuthChannel) return NS_OK; + + // When user cancels or auth fails we want to close the connection for + // connection based schemes like NTLM. Some servers don't like re-negotiation + // on the same connection. + nsresult rv; + if (mConnectionBased) { + rv = mAuthChannel->CloseStickyConnection(); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + mConnectionBased = false; + } + + nsCOMPtr channel = do_QueryInterface(mAuthChannel); + if (channel) { + nsresult status; + Unused << channel->GetStatus(&status); + if (NS_FAILED(status)) { + // If the channel is already cancelled, there is no need to deal with the + // rest challenges. + LOG((" Clear mRemainingChallenges, since mAuthChannel is cancelled")); + mRemainingChallenges.Truncate(); + } + } + + if (userCancel) { + if (!mRemainingChallenges.IsEmpty()) { + // there are still some challenges to process, do so + + // Get rid of current continuationState to avoid reusing it in + // next challenges since it is no longer relevant. + if (mProxyAuth) { + NS_IF_RELEASE(mProxyAuthContinuationState); + } else { + NS_IF_RELEASE(mAuthContinuationState); + } + nsAutoCString creds; + rv = GetCredentials(mRemainingChallenges.get(), mProxyAuth, creds); + if (NS_SUCCEEDED(rv)) { + // GetCredentials loaded the credentials from the cache or + // some other way in a synchronous manner, process those + // credentials now + mRemainingChallenges.Truncate(); + return ContinueOnAuthAvailable(creds); + } + if (rv == NS_ERROR_IN_PROGRESS) { + // GetCredentials successfully queued another authprompt for + // a challenge from the list, we are now waiting for the user + // to provide the credentials + return NS_OK; + } + + // otherwise, we failed... + } + + mRemainingChallenges.Truncate(); + } + + rv = mAuthChannel->OnAuthCancelled(userCancel); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + return NS_OK; +} + +NS_IMETHODIMP nsHttpChannelAuthProvider::OnCredsGenerated( + const char* aGeneratedCreds, uint32_t aFlags, nsresult aResult, + nsISupports* aSessionState, nsISupports* aContinuationState) { + nsresult rv; + + MOZ_ASSERT(NS_IsMainThread()); + + // When channel is closed, do not proceed + if (!mAuthChannel) { + return NS_OK; + } + + mGenerateCredentialsCancelable = nullptr; + + if (NS_FAILED(aResult)) { + return OnAuthCancelled(nullptr, true); + } + + // We want to update m(Proxy)AuthContinuationState in case it was changed by + // nsHttpNegotiateAuth::GenerateCredentials + nsCOMPtr contState(aContinuationState); + if (mProxyAuth) { + contState.swap(mProxyAuthContinuationState); + } else { + contState.swap(mAuthContinuationState); + } + + nsCOMPtr auth; + nsAutoCString unused; + rv = GetAuthenticator(mCurrentChallenge.get(), unused, getter_AddRefs(auth)); + NS_ENSURE_SUCCESS(rv, rv); + + const char* host; + int32_t port; + nsHttpAuthIdentity* ident; + nsAutoCString directory, scheme; + nsISupports** unusedContinuationState; + + // Get realm from challenge + nsAutoCString realm; + ParseRealm(mCurrentChallenge.get(), realm); + + rv = GetAuthorizationMembers(mProxyAuth, scheme, host, port, directory, ident, + unusedContinuationState); + if (NS_FAILED(rv)) return rv; + + rv = UpdateCache(auth, scheme.get(), host, port, directory.get(), realm.get(), + mCurrentChallenge.get(), *ident, aGeneratedCreds, aFlags, + aSessionState, mProxyAuth); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + mCurrentChallenge.Truncate(); + + rv = ContinueOnAuthAvailable(nsDependentCString(aGeneratedCreds)); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + return NS_OK; +} + +nsresult nsHttpChannelAuthProvider::ContinueOnAuthAvailable( + const nsACString& creds) { + nsresult rv; + if (mProxyAuth) + rv = mAuthChannel->SetProxyCredentials(creds); + else + rv = mAuthChannel->SetWWWCredentials(creds); + if (NS_FAILED(rv)) return rv; + + // drop our remaining list of challenges. We don't need them, because we + // have now authenticated against a challenge and will be sending that + // information to the server (or proxy). If it doesn't accept our + // authentication it'll respond with failure and resend the challenge list + mRemainingChallenges.Truncate(); + + Unused << mAuthChannel->OnAuthAvailable(); + + return NS_OK; +} + +bool nsHttpChannelAuthProvider::ConfirmAuth(const char* bundleKey, + bool doYesNoPrompt) { + // skip prompting the user if + // 1) prompts are disabled by pref + // 2) we've already prompted the user + // 3) we're not a toplevel channel + // 4) the userpass length is less than the "phishy" threshold + + if (!StaticPrefs::network_auth_confirmAuth_enabled()) { + return true; + } + + uint32_t loadFlags; + nsresult rv = mAuthChannel->GetLoadFlags(&loadFlags); + if (NS_FAILED(rv)) return true; + + if (mSuppressDefensiveAuth || + !(loadFlags & nsIChannel::LOAD_INITIAL_DOCUMENT_URI)) + return true; + + nsAutoCString userPass; + rv = mURI->GetUserPass(userPass); + if (NS_FAILED(rv) || + (userPass.Length() < gHttpHandler->PhishyUserPassLength())) + return true; + + // we try to confirm by prompting the user. if we cannot do so, then + // assume the user said ok. this is done to keep things working in + // embedded builds, where the string bundle might not be present, etc. + + nsCOMPtr bundleService = + do_GetService(NS_STRINGBUNDLE_CONTRACTID); + if (!bundleService) return true; + + nsCOMPtr bundle; + bundleService->CreateBundle(NECKO_MSGS_URL, getter_AddRefs(bundle)); + if (!bundle) return true; + + nsAutoCString host; + rv = mURI->GetHost(host); + if (NS_FAILED(rv)) return true; + + nsAutoCString user; + rv = mURI->GetUsername(user); + if (NS_FAILED(rv)) return true; + + NS_ConvertUTF8toUTF16 ucsHost(host), ucsUser(user); + + size_t userLength = ucsUser.Length(); + if (userLength > MAX_DISPLAYED_USER_LENGTH) { + size_t desiredLength = MAX_DISPLAYED_USER_LENGTH; + // Don't cut off right before a low surrogate. Just include it. + if (NS_IS_LOW_SURROGATE(ucsUser[desiredLength])) { + desiredLength++; + } + ucsUser.Replace(desiredLength, userLength - desiredLength, + nsContentUtils::GetLocalizedEllipsis()); + } + + size_t hostLen = ucsHost.Length(); + if (hostLen > MAX_DISPLAYED_HOST_LENGTH) { + size_t cutPoint = hostLen - MAX_DISPLAYED_HOST_LENGTH; + // Likewise, don't cut off right before a low surrogate here. + // Keep the low surrogate + if (NS_IS_LOW_SURROGATE(ucsHost[cutPoint])) { + cutPoint--; + } + // It's possible cutPoint was 1 and is now 0. Only insert the ellipsis + // if we're actually removing anything. + if (cutPoint > 0) { + ucsHost.Replace(0, cutPoint, nsContentUtils::GetLocalizedEllipsis()); + } + } + + AutoTArray strs = {ucsHost, ucsUser}; + + nsAutoString msg; + rv = bundle->FormatStringFromName(bundleKey, strs, msg); + if (NS_FAILED(rv)) return true; + + nsCOMPtr callbacks; + rv = mAuthChannel->GetNotificationCallbacks(getter_AddRefs(callbacks)); + if (NS_FAILED(rv)) return true; + + nsCOMPtr loadGroup; + rv = mAuthChannel->GetLoadGroup(getter_AddRefs(loadGroup)); + if (NS_FAILED(rv)) return true; + + nsCOMPtr promptSvc = + do_GetService("@mozilla.org/embedcomp/prompt-service;1", &rv); + if (NS_FAILED(rv) || !promptSvc) { + return true; + } + + // do not prompt again + mSuppressDefensiveAuth = true; + + // Get current browsing context to use as prompt parent + nsCOMPtr chan = do_QueryInterface(mAuthChannel); + if (!chan) { + return true; + } + + nsCOMPtr loadInfo = chan->LoadInfo(); + RefPtr browsingContext; + loadInfo->GetBrowsingContext(getter_AddRefs(browsingContext)); + + bool confirmed; + if (doYesNoPrompt) { + int32_t choice; + bool checkState = false; + rv = promptSvc->ConfirmExBC( + browsingContext, StaticPrefs::prompts_modalType_confirmAuth(), nullptr, + msg.get(), + nsIPromptService::BUTTON_POS_1_DEFAULT + + nsIPromptService::STD_YES_NO_BUTTONS, + nullptr, nullptr, nullptr, nullptr, &checkState, &choice); + if (NS_FAILED(rv)) return true; + + confirmed = choice == 0; + } else { + rv = promptSvc->ConfirmBC(browsingContext, + StaticPrefs::prompts_modalType_confirmAuth(), + nullptr, msg.get(), &confirmed); + if (NS_FAILED(rv)) return true; + } + + return confirmed; +} + +void nsHttpChannelAuthProvider::SetAuthorizationHeader( + nsHttpAuthCache* authCache, nsHttpAtom header, const char* scheme, + const char* host, int32_t port, const char* path, + nsHttpAuthIdentity& ident) { + nsHttpAuthEntry* entry = nullptr; + nsresult rv; + + // set informations that depend on whether + // we're authenticating against a proxy + // or a webserver + nsISupports** continuationState; + + nsAutoCString suffix; + if (header == nsHttp::Proxy_Authorization) { + continuationState = &mProxyAuthContinuationState; + + if (mProxyInfo) { + nsAutoCString type; + mProxyInfo->GetType(type); + if (type.EqualsLiteral("https")) { + // Let this be overriden by anything from the cache. + auto const& pa = mProxyInfo->ProxyAuthorizationHeader(); + if (!pa.IsEmpty()) { + rv = mAuthChannel->SetProxyCredentials(pa); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + } + } + } else { + continuationState = &mAuthContinuationState; + + nsCOMPtr chan = do_QueryInterface(mAuthChannel); + GetOriginAttributesSuffix(chan, suffix); + } + + rv = authCache->GetAuthEntryForPath(scheme, host, port, path, suffix, &entry); + if (NS_SUCCEEDED(rv)) { + // if we are trying to add a header for origin server auth and if the + // URL contains an explicit username, then try the given username first. + // we only want to do this, however, if we know the URL requires auth + // based on the presence of an auth cache entry for this URL (which is + // true since we are here). but, if the username from the URL matches + // the username from the cache, then we should prefer the password + // stored in the cache since that is most likely to be valid. + if (header == nsHttp::Authorization && entry->Domain()[0] == '\0') { + GetIdentityFromURI(0, ident); + // if the usernames match, then clear the ident so we will pick + // up the one from the auth cache instead. + // when this is undesired, specify LOAD_EXPLICIT_CREDENTIALS load + // flag. + if (nsCRT::strcmp(ident.User(), entry->User()) == 0) { + uint32_t loadFlags; + if (NS_SUCCEEDED(mAuthChannel->GetLoadFlags(&loadFlags)) && + !(loadFlags & nsIChannel::LOAD_EXPLICIT_CREDENTIALS)) { + ident.Clear(); + } + } + } + bool identFromURI; + if (ident.IsEmpty()) { + rv = ident.Set(entry->Identity()); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + identFromURI = false; + } else + identFromURI = true; + + nsCString temp; // this must have the same lifetime as creds + const char* creds = entry->Creds(); + const char* challenge = entry->Challenge(); + // we can only send a preemptive Authorization header if we have either + // stored credentials or a stored challenge from which to derive + // credentials. if the identity is from the URI, then we cannot use + // the stored credentials. + if ((!creds[0] || identFromURI) && challenge[0]) { + nsCOMPtr auth; + nsAutoCString unused; + rv = GetAuthenticator(challenge, unused, getter_AddRefs(auth)); + if (NS_SUCCEEDED(rv)) { + bool proxyAuth = (header == nsHttp::Proxy_Authorization); + rv = GenCredsAndSetEntry(auth, proxyAuth, scheme, host, port, path, + entry->Realm(), challenge, ident, + entry->mMetaData, getter_Copies(temp)); + if (NS_SUCCEEDED(rv)) creds = temp.get(); + + // make sure the continuation state is null since we do not + // support mixing preemptive and 'multirequest' authentication. + NS_IF_RELEASE(*continuationState); + } + } + if (creds[0]) { + LOG((" adding \"%s\" request header\n", header.get())); + if (header == nsHttp::Proxy_Authorization) { + rv = mAuthChannel->SetProxyCredentials(nsDependentCString(creds)); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } else { + rv = mAuthChannel->SetWWWCredentials(nsDependentCString(creds)); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + + // suppress defensive auth prompting for this channel since we know + // that we already prompted at least once this session. we only do + // this for non-proxy auth since the URL's userpass is not used for + // proxy auth. + if (header == nsHttp::Authorization) mSuppressDefensiveAuth = true; + } else + ident.Clear(); // don't remember the identity + } +} + +nsresult nsHttpChannelAuthProvider::GetCurrentPath(nsACString& path) { + nsresult rv; + nsCOMPtr url = do_QueryInterface(mURI); + if (url) + rv = url->GetDirectory(path); + else + rv = mURI->GetPathQueryRef(path); + return rv; +} + +NS_IMPL_ISUPPORTS(nsHttpChannelAuthProvider, nsICancelable, + nsIHttpChannelAuthProvider, nsIAuthPromptCallback, + nsIHttpAuthenticatorCallback) + +} // namespace mozilla::net diff --git a/netwerk/protocol/http/nsHttpChannelAuthProvider.h b/netwerk/protocol/http/nsHttpChannelAuthProvider.h new file mode 100644 index 0000000000..4bff58ae96 --- /dev/null +++ b/netwerk/protocol/http/nsHttpChannelAuthProvider.h @@ -0,0 +1,186 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set et cin ts=4 sw=2 sts=2: */ +/* 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/. */ + +#ifndef nsHttpChannelAuthProvider_h__ +#define nsHttpChannelAuthProvider_h__ + +#include "nsIHttpChannelAuthProvider.h" +#include "nsIAuthPromptCallback.h" +#include "nsIHttpAuthenticatorCallback.h" +#include "nsString.h" +#include "nsCOMPtr.h" +#include "nsHttpAuthCache.h" +#include "nsProxyInfo.h" +#include "nsCRT.h" +#include "nsICancelableRunnable.h" + +class nsIHttpAuthenticableChannel; +class nsIHttpAuthenticator; +class nsIURI; + +namespace mozilla { +namespace net { + +class nsHttpHandler; + +class nsHttpChannelAuthProvider final : public nsIHttpChannelAuthProvider, + public nsIAuthPromptCallback, + public nsIHttpAuthenticatorCallback { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSICANCELABLE + NS_DECL_NSIHTTPCHANNELAUTHPROVIDER + NS_DECL_NSIAUTHPROMPTCALLBACK + NS_DECL_NSIHTTPAUTHENTICATORCALLBACK + + nsHttpChannelAuthProvider(); + + private: + virtual ~nsHttpChannelAuthProvider(); + + const char* ProxyHost() const { + return mProxyInfo ? mProxyInfo->Host().get() : nullptr; + } + + int32_t ProxyPort() const { return mProxyInfo ? mProxyInfo->Port() : -1; } + + const char* Host() const { return mHost.get(); } + int32_t Port() const { return mPort; } + bool UsingSSL() const { return mUsingSSL; } + + bool UsingHttpProxy() const { + return mProxyInfo && (mProxyInfo->IsHTTP() || mProxyInfo->IsHTTPS()); + } + + [[nodiscard]] nsresult PrepareForAuthentication(bool proxyAuth); + [[nodiscard]] nsresult GenCredsAndSetEntry( + nsIHttpAuthenticator*, bool proxyAuth, const char* scheme, + const char* host, int32_t port, const char* dir, const char* realm, + const char* challenge, const nsHttpAuthIdentity& ident, + nsCOMPtr& session, char** result); + [[nodiscard]] nsresult GetAuthenticator(const char* challenge, + nsCString& scheme, + nsIHttpAuthenticator** auth); + void ParseRealm(const char* challenge, nsACString& realm); + void GetIdentityFromURI(uint32_t authFlags, nsHttpAuthIdentity&); + + /** + * Following three methods return NS_ERROR_IN_PROGRESS when + * nsIAuthPrompt2.asyncPromptAuth method is called. This result indicates + * the user's decision will be gathered in a callback and is not an actual + * error. + */ + [[nodiscard]] nsresult GetCredentials(const char* challenges, bool proxyAuth, + nsCString& creds); + [[nodiscard]] nsresult GetCredentialsForChallenge(const char* challenge, + const char* scheme, + bool proxyAuth, + nsIHttpAuthenticator* auth, + nsCString& creds); + [[nodiscard]] nsresult PromptForIdentity(uint32_t level, bool proxyAuth, + const char* realm, + const char* authType, + uint32_t authFlags, + nsHttpAuthIdentity&); + + bool ConfirmAuth(const char* bundleKey, bool doYesNoPrompt); + void SetAuthorizationHeader(nsHttpAuthCache*, nsHttpAtom header, + const char* scheme, const char* host, + int32_t port, const char* path, + nsHttpAuthIdentity& ident); + [[nodiscard]] nsresult GetCurrentPath(nsACString&); + /** + * Return all information needed to build authorization information, + * all parameters except proxyAuth are out parameters. proxyAuth specifies + * with what authorization we work (WWW or proxy). + */ + [[nodiscard]] nsresult GetAuthorizationMembers( + bool proxyAuth, nsACString& scheme, const char*& host, int32_t& port, + nsACString& path, nsHttpAuthIdentity*& ident, + nsISupports**& continuationState); + /** + * Method called to resume suspended transaction after we got credentials + * from the user. Called from OnAuthAvailable callback or OnAuthCancelled + * when credentials for next challenge were obtained synchronously. + */ + [[nodiscard]] nsresult ContinueOnAuthAvailable(const nsACString& creds); + + [[nodiscard]] nsresult DoRedirectChannelToHttps(); + + /** + * A function that takes care of reading STS headers and enforcing STS + * load rules. After a secure channel is erected, STS requires the channel + * to be trusted or any STS header data on the channel is ignored. + * This is called from ProcessResponse. + */ + [[nodiscard]] nsresult ProcessSTSHeader(); + + // Depending on the pref setting, the authentication dialog may be blocked + // for all sub-resources, blocked for cross-origin sub-resources, or + // always allowed for sub-resources. + // For more details look at the bug 647010. + bool BlockPrompt(bool proxyAuth); + + // Store credentials to the cache when appropriate aFlags are set. + [[nodiscard]] nsresult UpdateCache( + nsIHttpAuthenticator* aAuth, const char* aScheme, const char* aHost, + int32_t aPort, const char* aDirectory, const char* aRealm, + const char* aChallenge, const nsHttpAuthIdentity& aIdent, + const char* aCreds, uint32_t aGenerateFlags, nsISupports* aSessionState, + bool aProxyAuth); + + private: + nsIHttpAuthenticableChannel* mAuthChannel; // weak ref + + nsCOMPtr mURI; + nsCOMPtr mProxyInfo; + nsCString mHost; + int32_t mPort; + bool mUsingSSL; + bool mProxyUsingSSL; + bool mIsPrivate; + + nsISupports* mProxyAuthContinuationState; + nsCString mProxyAuthType; + nsISupports* mAuthContinuationState; + nsCString mAuthType; + nsHttpAuthIdentity mIdent; + nsHttpAuthIdentity mProxyIdent; + + // Reference to the prompt waiting in prompt queue. The channel is + // responsible to call its cancel method when user in any way cancels + // this request. + nsCOMPtr mAsyncPromptAuthCancelable; + // Saved in GetCredentials when prompt is asynchronous, the first challenge + // we obtained from the server with 401/407 response, will be processed in + // OnAuthAvailable callback. + nsCString mCurrentChallenge; + // Saved in GetCredentials when prompt is asynchronous, remaning challenges + // we have to process when user cancels the auth dialog for the current + // challenge. + nsCString mRemainingChallenges; + + // True when we need to authenticate to proxy, i.e. when we get 407 + // response. Used in OnAuthAvailable and OnAuthCancelled callbacks. + uint32_t mProxyAuth : 1; + uint32_t mTriedProxyAuth : 1; + uint32_t mTriedHostAuth : 1; + uint32_t mSuppressDefensiveAuth : 1; + + // If a cross-origin sub-resource is being loaded, this flag will be set. + // In that case, the prompt text will be different to warn users. + uint32_t mCrossOrigin : 1; + uint32_t mConnectionBased : 1; + + RefPtr mHttpHandler; // keep gHttpHandler alive + + nsCOMPtr mGenerateCredentialsCancelable; +}; + +} // namespace net +} // namespace mozilla + +#endif // nsHttpChannelAuthProvider_h__ diff --git a/netwerk/protocol/http/nsHttpChunkedDecoder.cpp b/netwerk/protocol/http/nsHttpChunkedDecoder.cpp new file mode 100644 index 0000000000..76bbd82805 --- /dev/null +++ b/netwerk/protocol/http/nsHttpChunkedDecoder.cpp @@ -0,0 +1,168 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +// HttpLog.h should generally be included first +#include "HttpLog.h" + +#include +#include "nsHttpChunkedDecoder.h" +#include +#include "plstr.h" + +#include "mozilla/Unused.h" + +namespace mozilla { +namespace net { + +//----------------------------------------------------------------------------- +// nsHttpChunkedDecoder +//----------------------------------------------------------------------------- + +nsresult nsHttpChunkedDecoder::HandleChunkedContent( + char* buf, uint32_t count, uint32_t* contentRead, + uint32_t* contentRemaining) { + LOG(("nsHttpChunkedDecoder::HandleChunkedContent [count=%u]\n", count)); + + *contentRead = 0; + + // from RFC2616 section 3.6.1, the chunked transfer coding is defined as: + // + // Chunked-Body = *chunk + // last-chunk + // trailer + // CRLF + // chunk = chunk-size [ chunk-extension ] CRLF + // chunk-data CRLF + // chunk-size = 1*HEX + // last-chunk = 1*("0") [ chunk-extension ] CRLF + // + // chunk-extension = *( ";" chunk-ext-name [ "=" chunk-ext-val ] ) + // chunk-ext-name = token + // chunk-ext-val = token | quoted-string + // chunk-data = chunk-size(OCTET) + // trailer = *(entity-header CRLF) + // + // the chunk-size field is a string of hex digits indicating the size of the + // chunk. the chunked encoding is ended by any chunk whose size is zero, + // followed by the trailer, which is terminated by an empty line. + + while (count) { + if (mChunkRemaining) { + uint32_t amt = std::min(mChunkRemaining, count); + + count -= amt; + mChunkRemaining -= amt; + + *contentRead += amt; + buf += amt; + } else if (mReachedEOF) + break; // done + else { + uint32_t bytesConsumed = 0; + + nsresult rv = ParseChunkRemaining(buf, count, &bytesConsumed); + if (NS_FAILED(rv)) return rv; + + count -= bytesConsumed; + + if (count) { + // shift buf by bytesConsumed + memmove(buf, buf + bytesConsumed, count); + } + } + } + + *contentRemaining = count; + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsHttpChunkedDecoder +//----------------------------------------------------------------------------- + +nsresult nsHttpChunkedDecoder::ParseChunkRemaining(char* buf, uint32_t count, + uint32_t* bytesConsumed) { + MOZ_ASSERT(mChunkRemaining == 0, "chunk remaining should be zero"); + MOZ_ASSERT(count, "unexpected"); + + *bytesConsumed = 0; + + char* p = static_cast(memchr(buf, '\n', count)); + if (p) { + *p = 0; + count = p - buf; // new length + *bytesConsumed = count + 1; // length + newline + if ((p > buf) && (*(p - 1) == '\r')) { // eliminate a preceding CR + *(p - 1) = 0; + count--; + } + + // make buf point to the full line buffer to parse + if (!mLineBuf.IsEmpty()) { + mLineBuf.Append(buf, count); + buf = (char*)mLineBuf.get(); + count = mLineBuf.Length(); + } + + if (mWaitEOF) { + if (*buf) { + LOG(("got trailer: %s\n", buf)); + // allocate a header array for the trailers on demand + if (!mTrailers) { + mTrailers = MakeUnique(); + } + + nsHttpAtom hdr; + nsAutoCString headerNameOriginal; + nsAutoCString val; + if (NS_SUCCEEDED( + mTrailers->ParseHeaderLine(nsDependentCSubstring(buf, count), + &hdr, &headerNameOriginal, &val))) { + if (hdr == nsHttp::Server_Timing) { + Unused << mTrailers->SetHeaderFromNet(hdr, headerNameOriginal, val, + true); + } + } + } else { + mWaitEOF = false; + mReachedEOF = true; + LOG(("reached end of chunked-body\n")); + } + } else if (*buf) { + char* endptr; + unsigned long parsedval; // could be 64 bit, could be 32 + + // ignore any chunk-extensions + if ((p = PL_strchr(buf, ';')) != nullptr) *p = 0; + + // mChunkRemaining is an uint32_t! + parsedval = strtoul(buf, &endptr, 16); + mChunkRemaining = (uint32_t)parsedval; + + if ((endptr == buf) || ((errno == ERANGE) && (parsedval == ULONG_MAX)) || + (parsedval != mChunkRemaining)) { + LOG(("failed parsing hex on string [%s]\n", buf)); + return NS_ERROR_UNEXPECTED; + } + + // we've discovered the last chunk + if (mChunkRemaining == 0) mWaitEOF = true; + } + + // ensure that the line buffer is clear + mLineBuf.Truncate(); + } else { + // save the partial line; wait for more data + *bytesConsumed = count; + // ignore a trailing CR + if (buf[count - 1] == '\r') count--; + mLineBuf.Append(buf, count); + } + + return NS_OK; +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/nsHttpChunkedDecoder.h b/netwerk/protocol/http/nsHttpChunkedDecoder.h new file mode 100644 index 0000000000..a1fb291df2 --- /dev/null +++ b/netwerk/protocol/http/nsHttpChunkedDecoder.h @@ -0,0 +1,54 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef nsHttpChunkedDecoder_h__ +#define nsHttpChunkedDecoder_h__ + +#include "nsError.h" +#include "nsString.h" +#include "nsHttpHeaderArray.h" + +namespace mozilla { +namespace net { + +class nsHttpChunkedDecoder { + public: + nsHttpChunkedDecoder() + : mTrailers(nullptr), + mChunkRemaining(0), + mReachedEOF(false), + mWaitEOF(false) {} + ~nsHttpChunkedDecoder() = default; + + bool ReachedEOF() { return mReachedEOF; } + + // called by the transaction to handle chunked content. + [[nodiscard]] nsresult HandleChunkedContent(char* buf, uint32_t count, + uint32_t* contentRead, + uint32_t* contentRemaining); + + nsHttpHeaderArray* Trailers() { return mTrailers.get(); } + + UniquePtr TakeTrailers() { return std::move(mTrailers); } + + // How mush data is still missing(needs to arrive) from the current chunk. + uint32_t GetChunkRemaining() { return mChunkRemaining; } + + private: + [[nodiscard]] nsresult ParseChunkRemaining(char* buf, uint32_t count, + uint32_t* countRead); + + private: + UniquePtr mTrailers; + uint32_t mChunkRemaining; + nsCString mLineBuf; // may hold a partial line + bool mReachedEOF; + bool mWaitEOF; +}; + +} // namespace net +} // namespace mozilla + +#endif diff --git a/netwerk/protocol/http/nsHttpConnection.cpp b/netwerk/protocol/http/nsHttpConnection.cpp new file mode 100644 index 0000000000..b01f02394b --- /dev/null +++ b/netwerk/protocol/http/nsHttpConnection.cpp @@ -0,0 +1,2735 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=4 sw=2 sts=2 et cin: */ +/* 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/. */ + +// HttpLog.h should generally be included first +#include "HttpLog.h" + +// Log on level :5, instead of default :4. +#undef LOG +#define LOG(args) LOG5(args) +#undef LOG_ENABLED +#define LOG_ENABLED() LOG5_ENABLED() + +#define TLS_EARLY_DATA_NOT_AVAILABLE 0 +#define TLS_EARLY_DATA_AVAILABLE_BUT_NOT_USED 1 +#define TLS_EARLY_DATA_AVAILABLE_AND_USED 2 + +#include "ASpdySession.h" +#include "mozilla/ChaosMode.h" +#include "mozilla/Telemetry.h" +#include "nsHttpConnection.h" +#include "nsHttpHandler.h" +#include "nsHttpRequestHead.h" +#include "nsHttpResponseHead.h" +#include "nsIClassOfService.h" +#include "nsIOService.h" +#include "nsISocketTransport.h" +#include "nsSocketTransportService2.h" +#include "nsISSLSocketControl.h" +#include "nsISupportsPriority.h" +#include "nsITransportSecurityInfo.h" +#include "nsPreloadedStream.h" +#include "nsProxyRelease.h" +#include "nsSocketTransport2.h" +#include "nsStringStream.h" +#include "nsITransportSecurityInfo.h" +#include "mozpkix/pkixnss.h" +#include "sslt.h" +#include "NSSErrorsService.h" +#include "TunnelUtils.h" +#include "TCPFastOpenLayer.h" + +namespace mozilla { +namespace net { + +//----------------------------------------------------------------------------- +// nsHttpConnection +//----------------------------------------------------------------------------- + +nsHttpConnection::nsHttpConnection() + : mSocketInCondition(NS_ERROR_NOT_INITIALIZED), + mSocketOutCondition(NS_ERROR_NOT_INITIALIZED), + mHttpHandler(gHttpHandler), + mLastReadTime(0), + mLastWriteTime(0), + mMaxHangTime(0), + mConsiderReusedAfterInterval(0), + mConsiderReusedAfterEpoch(0), + mCurrentBytesRead(0), + mMaxBytesRead(0), + mTotalBytesRead(0), + mContentBytesWritten(0), + mUrgentStartPreferred(false), + mUrgentStartPreferredKnown(false), + mConnectedTransport(false), + mKeepAlive(true) // assume to keep-alive by default + , + mKeepAliveMask(true), + mDontReuse(false), + mIsReused(false), + mCompletedProxyConnect(false), + mLastTransactionExpectedNoContent(false), + mIdleMonitoring(false), + mProxyConnectInProgress(false), + mInSpdyTunnel(false), + mForcePlainText(false), + mTrafficCount(0), + mTrafficStamp(false), + mHttp1xTransactionCount(0), + mRemainingConnectionUses(0xffffffff), + mNPNComplete(false), + mSetupSSLCalled(false), + mUsingSpdyVersion(SpdyVersion::NONE), + mPriority(nsISupportsPriority::PRIORITY_NORMAL), + mReportedSpdy(false), + mEverUsedSpdy(false), + mLastHttpResponseVersion(HttpVersion::v1_1), + mDefaultTimeoutFactor(1), + mResponseTimeoutEnabled(false), + mTCPKeepaliveConfig(kTCPKeepaliveDisabled), + mForceSendPending(false), + m0RTTChecked(false), + mWaitingFor0RTTResponse(false), + mContentBytesWritten0RTT(0), + mEarlyDataNegotiated(false), + mDid0RTTSpdy(false), + mFastOpen(false), + mFastOpenStatus(TFO_NOT_SET), + mForceSendDuringFastOpenPending(false), + mReceivedSocketWouldBlockDuringFastOpen(false), + mCheckNetworkStallsWithTFO(false), + mLastRequestBytesSentTime(0) { + LOG(("Creating nsHttpConnection @%p\n", this)); + + // the default timeout is for when this connection has not yet processed a + // transaction + static const PRIntervalTime k5Sec = PR_SecondsToInterval(5); + mIdleTimeout = (k5Sec < gHttpHandler->IdleTimeout()) + ? k5Sec + : gHttpHandler->IdleTimeout(); + + mThroughCaptivePortal = gHttpHandler->GetThroughCaptivePortal(); +} + +nsHttpConnection::~nsHttpConnection() { + LOG(("Destroying nsHttpConnection @%p\n", this)); + + if (!mEverUsedSpdy) { + LOG(("nsHttpConnection %p performed %d HTTP/1.x transactions\n", this, + mHttp1xTransactionCount)); + Telemetry::Accumulate(Telemetry::HTTP_REQUEST_PER_CONN, + mHttp1xTransactionCount); + nsHttpConnectionInfo* ci = nullptr; + if (mTransaction) { + ci = mTransaction->ConnectionInfo(); + } + if (!ci) { + ci = mConnInfo; + } + + MOZ_ASSERT(ci); + if (ci->GetIsTrrServiceChannel()) { + Telemetry::Accumulate(Telemetry::DNS_TRR_REQUEST_PER_CONN, + mHttp1xTransactionCount); + } + } + + if (mTotalBytesRead) { + uint32_t totalKBRead = static_cast(mTotalBytesRead >> 10); + LOG(("nsHttpConnection %p read %dkb on connection spdy=%d\n", this, + totalKBRead, mEverUsedSpdy)); + Telemetry::Accumulate(mEverUsedSpdy ? Telemetry::SPDY_KBREAD_PER_CONN2 + : Telemetry::HTTP_KBREAD_PER_CONN2, + totalKBRead); + } + + if (mThroughCaptivePortal) { + if (mTotalBytesRead || mTotalBytesWritten) { + auto total = + Clamp((mTotalBytesRead >> 10) + (mTotalBytesWritten >> 10), + 0, std::numeric_limits::max()); + Telemetry::ScalarAdd( + Telemetry::ScalarID::NETWORKING_DATA_TRANSFERRED_CAPTIVE_PORTAL, + total); + } + + Telemetry::ScalarAdd( + Telemetry::ScalarID::NETWORKING_HTTP_CONNECTIONS_CAPTIVE_PORTAL, 1); + } + + if (mForceSendTimer) { + mForceSendTimer->Cancel(); + mForceSendTimer = nullptr; + } + + if ((mFastOpenStatus != TFO_FAILED) && (mFastOpenStatus != TFO_HTTP) && + (((mFastOpenStatus > TFO_DISABLED_CONNECT) && + (mFastOpenStatus < TFO_BACKUP_CONN)) || + gHttpHandler->UseFastOpen())) { + // TFO_FAILED will be reported in the replacement connection with more + // details. + // Otherwise report only if TFO is enabled and supported. + // If TFO is disabled, report only connections ha cause it to be disabled, + // e.g. TFO_FAILED_NET_TIMEOUT, etc. + Telemetry::Accumulate(Telemetry::TCP_FAST_OPEN_3, mFastOpenStatus); + } +} + +nsresult nsHttpConnection::Init( + nsHttpConnectionInfo* info, uint16_t maxHangTime, + nsISocketTransport* transport, nsIAsyncInputStream* instream, + nsIAsyncOutputStream* outstream, bool connectedTransport, + nsIInterfaceRequestor* callbacks, PRIntervalTime rtt) { + LOG1(("nsHttpConnection::Init this=%p sockettransport=%p", this, transport)); + NS_ENSURE_ARG_POINTER(info); + NS_ENSURE_TRUE(!mConnInfo, NS_ERROR_ALREADY_INITIALIZED); + + mConnectedTransport = connectedTransport; + mConnInfo = info; + MOZ_ASSERT(mConnInfo); + + mLastWriteTime = mLastReadTime = PR_IntervalNow(); + mRtt = rtt; + mMaxHangTime = PR_SecondsToInterval(maxHangTime); + + mSocketTransport = transport; + mSocketIn = instream; + mSocketOut = outstream; + + // See explanation for non-strictness of this operation in + // SetSecurityCallbacks. + mCallbacks = new nsMainThreadPtrHolder( + "nsHttpConnection::mCallbacks", callbacks, false); + + mSocketTransport->SetEventSink(this, nullptr); + mSocketTransport->SetSecurityCallbacks(this); + + return NS_OK; +} + +nsresult nsHttpConnection::TryTakeSubTransactions( + nsTArray >& list) { + nsresult rv = mTransaction->TakeSubTransactions(list); + + if (rv == NS_ERROR_ALREADY_OPENED) { + // Has the interface for TakeSubTransactions() changed? + LOG( + ("TakeSubTransactions somehow called after " + "nsAHttpTransaction began processing\n")); + MOZ_ASSERT(false, + "TakeSubTransactions somehow called after " + "nsAHttpTransaction began processing"); + mTransaction->Close(NS_ERROR_ABORT); + return rv; + } + + if (NS_FAILED(rv) && rv != NS_ERROR_NOT_IMPLEMENTED) { + // Has the interface for TakeSubTransactions() changed? + LOG(("unexpected rv from nnsAHttpTransaction::TakeSubTransactions()")); + MOZ_ASSERT(false, + "unexpected result from " + "nsAHttpTransaction::TakeSubTransactions()"); + mTransaction->Close(NS_ERROR_ABORT); + return rv; + } + + return rv; +} + +nsresult nsHttpConnection::MoveTransactionsToSpdy( + nsresult status, nsTArray >& list) { + if (NS_FAILED(status)) { // includes NS_ERROR_NOT_IMPLEMENTED + MOZ_ASSERT(list.IsEmpty(), "sub transaction list not empty"); + + // This is ok - treat mTransaction as a single real request. + // Wrap the old http transaction into the new spdy session + // as the first stream. + LOG( + ("nsHttpConnection::MoveTransactionsToSpdy moves single transaction %p " + "into SpdySession %p\n", + mTransaction.get(), mSpdySession.get())); + nsresult rv = AddTransaction(mTransaction, mPriority); + if (NS_FAILED(rv)) { + return rv; + } + } else { + int32_t count = list.Length(); + + LOG( + ("nsHttpConnection::MoveTransactionsToSpdy moving transaction list " + "len=%d " + "into SpdySession %p\n", + count, mSpdySession.get())); + + if (!count) { + mTransaction->Close(NS_ERROR_ABORT); + return NS_ERROR_ABORT; + } + + for (int32_t index = 0; index < count; ++index) { + nsresult rv = AddTransaction(list[index], mPriority); + if (NS_FAILED(rv)) { + return rv; + } + } + } + + return NS_OK; +} + +void nsHttpConnection::Start0RTTSpdy(SpdyVersion spdyVersion) { + LOG(("nsHttpConnection::Start0RTTSpdy [this=%p]", this)); + + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + mDid0RTTSpdy = true; + mUsingSpdyVersion = spdyVersion; + mSpdySession = + ASpdySession::NewSpdySession(spdyVersion, mSocketTransport, true); + + nsTArray > list; + nsresult rv = TryTakeSubTransactions(list); + if (NS_FAILED(rv) && rv != NS_ERROR_NOT_IMPLEMENTED) { + LOG( + ("nsHttpConnection::Start0RTTSpdy [this=%p] failed taking " + "subtransactions rv=%" PRIx32, + this, static_cast(rv))); + return; + } + + rv = MoveTransactionsToSpdy(rv, list); + if (NS_FAILED(rv)) { + LOG( + ("nsHttpConnection::Start0RTTSpdy [this=%p] failed moving " + "transactions rv=%" PRIx32, + this, static_cast(rv))); + return; + } + + mTransaction = mSpdySession; +} + +void nsHttpConnection::StartSpdy(nsISSLSocketControl* sslControl, + SpdyVersion spdyVersion) { + LOG(("nsHttpConnection::StartSpdy [this=%p, mDid0RTTSpdy=%d]\n", this, + mDid0RTTSpdy)); + + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + MOZ_ASSERT(!mSpdySession || mDid0RTTSpdy); + + mUsingSpdyVersion = spdyVersion; + mEverUsedSpdy = true; + if (sslControl) { + sslControl->SetDenyClientCert(true); + } + + if (!mDid0RTTSpdy) { + mSpdySession = + ASpdySession::NewSpdySession(spdyVersion, mSocketTransport, false); + } + + if (!mReportedSpdy) { + mReportedSpdy = true; + gHttpHandler->ConnMgr()->ReportSpdyConnection(this, true); + } + + // Setting the connection as reused allows some transactions that fail + // with NS_ERROR_NET_RESET to be restarted and SPDY uses that code + // to handle clean rejections (such as those that arrived after + // a server goaway was generated). + mIsReused = true; + + // If mTransaction is a muxed object it might represent + // several requests. If so, we need to unpack that and + // pack them all into a new spdy session. + + nsTArray > list; + nsresult status = NS_OK; + if (!mDid0RTTSpdy) { + status = TryTakeSubTransactions(list); + + if (NS_FAILED(status) && status != NS_ERROR_NOT_IMPLEMENTED) { + return; + } + } + + if (NeedSpdyTunnel()) { + LOG3( + ("nsHttpConnection::StartSpdy %p Connecting To a HTTP/2 " + "Proxy and Need Connect", + this)); + MOZ_ASSERT(mProxyConnectStream); + + mProxyConnectStream = nullptr; + mCompletedProxyConnect = true; + mProxyConnectInProgress = false; + } + + nsresult rv = NS_OK; + bool spdyProxy = mConnInfo->UsingHttpsProxy() && !mTLSFilter; + if (spdyProxy) { + RefPtr wildCardProxyCi; + rv = mConnInfo->CreateWildCard(getter_AddRefs(wildCardProxyCi)); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + gHttpHandler->ConnMgr()->MoveToWildCardConnEntry(mConnInfo, wildCardProxyCi, + this); + mConnInfo = wildCardProxyCi; + MOZ_ASSERT(mConnInfo); + } + + if (!mDid0RTTSpdy) { + rv = MoveTransactionsToSpdy(status, list); + if (NS_FAILED(rv)) { + return; + } + } + + // Disable TCP Keepalives - use SPDY ping instead. + rv = DisableTCPKeepalives(); + if (NS_FAILED(rv)) { + LOG( + ("nsHttpConnection::StartSpdy [%p] DisableTCPKeepalives failed " + "rv[0x%" PRIx32 "]", + this, static_cast(rv))); + } + + mIdleTimeout = gHttpHandler->SpdyTimeout() * mDefaultTimeoutFactor; + + if (!mTLSFilter) { + mTransaction = mSpdySession; + } else { + rv = mTLSFilter->SetProxiedTransaction(mSpdySession); + if (NS_FAILED(rv)) { + LOG( + ("nsHttpConnection::StartSpdy [%p] SetProxiedTransaction failed" + " rv[0x%x]", + this, static_cast(rv))); + } + } + if (mDontReuse) { + mSpdySession->DontReuse(); + } +} + +bool nsHttpConnection::EnsureNPNComplete(nsresult& aOut0RTTWriteHandshakeValue, + uint32_t& aOut0RTTBytesWritten) { + // If for some reason the components to check on NPN aren't available, + // this function will just return true to continue on and disable SPDY + + aOut0RTTWriteHandshakeValue = NS_OK; + aOut0RTTBytesWritten = 0; + + MOZ_ASSERT(mSocketTransport); + if (!mSocketTransport) { + // this cannot happen + mNPNComplete = true; + return true; + } + + if (mNPNComplete) { + return true; + } + + nsresult rv = NS_OK; + nsCOMPtr securityInfo; + nsCOMPtr info; + nsCOMPtr ssl; + nsAutoCString negotiatedNPN; + + GetSecurityInfo(getter_AddRefs(securityInfo)); + if (!securityInfo) { + goto npnComplete; + } + + ssl = do_QueryInterface(securityInfo, &rv); + if (NS_FAILED(rv)) goto npnComplete; + + info = do_QueryInterface(securityInfo, &rv); + if (NS_FAILED(rv)) goto npnComplete; + + if (!m0RTTChecked) { + // We reuse m0RTTChecked. We want to send this status only once. + mTransaction->OnTransportStatus(mSocketTransport, + NS_NET_STATUS_TLS_HANDSHAKE_STARTING, 0); + } + + rv = info->GetNegotiatedNPN(negotiatedNPN); + if (!m0RTTChecked && (rv == NS_ERROR_NOT_CONNECTED) && + !mConnInfo->UsingProxy()) { + // There is no ALPN info (yet!). We need to consider doing 0RTT. We + // will do so if there is ALPN information from a previous session + // (AlpnEarlySelection), we are using HTTP/1, and the request data can + // be safely retried. + m0RTTChecked = true; + nsresult rvEarlyAlpn = ssl->GetAlpnEarlySelection(mEarlyNegotiatedALPN); + if (NS_FAILED(rvEarlyAlpn)) { + // if ssl->DriveHandshake() has never been called the value + // for AlpnEarlySelection is still not set. So call it here and + // check again. + LOG1( + ("nsHttpConnection::EnsureNPNComplete %p - " + "early selected alpn not available, we will try one more time.", + this)); + // Let's do DriveHandshake again. + rv = ssl->DriveHandshake(); + if (NS_FAILED(rv) && rv != NS_BASE_STREAM_WOULD_BLOCK) { + goto npnComplete; + } + + // Check NegotiatedNPN first. + rv = info->GetNegotiatedNPN(negotiatedNPN); + if (rv == NS_ERROR_NOT_CONNECTED) { + rvEarlyAlpn = ssl->GetAlpnEarlySelection(mEarlyNegotiatedALPN); + } + } + + if (NS_FAILED(rvEarlyAlpn)) { + LOG1( + ("nsHttpConnection::EnsureNPNComplete %p - " + "early selected alpn not available", + this)); + mEarlyDataNegotiated = false; + } else { + LOG1( + ("nsHttpConnection::EnsureNPNComplete %p -" + "early selected alpn: %s", + this, mEarlyNegotiatedALPN.get())); + uint32_t infoIndex; + const SpdyInformation* info = gHttpHandler->SpdyInfo(); + if (NS_FAILED(info->GetNPNIndex(mEarlyNegotiatedALPN, &infoIndex))) { + // This is the HTTP/1 case. + // Check if early-data is allowed for this transaction. + if (mTransaction->Do0RTT()) { + LOG( + ("nsHttpConnection::EnsureNPNComplete [this=%p] - We " + "can do 0RTT (http/1)!", + this)); + mWaitingFor0RTTResponse = true; + } + } else { + // We have h2, we can at least 0-RTT the preamble and opening + // SETTINGS, etc, and maybe some of the first request + LOG( + ("nsHttpConnection::EnsureNPNComplete [this=%p] - Starting " + "0RTT for h2!", + this)); + mWaitingFor0RTTResponse = true; + Start0RTTSpdy(info->Version[infoIndex]); + } + mEarlyDataNegotiated = true; + } + } + + if (rv == NS_ERROR_NOT_CONNECTED) { + if (mWaitingFor0RTTResponse) { + aOut0RTTWriteHandshakeValue = mTransaction->ReadSegments( + this, nsIOService::gDefaultSegmentSize, &aOut0RTTBytesWritten); + if (NS_FAILED(aOut0RTTWriteHandshakeValue) && + aOut0RTTWriteHandshakeValue != NS_BASE_STREAM_WOULD_BLOCK) { + goto npnComplete; + } + LOG( + ("nsHttpConnection::EnsureNPNComplete [this=%p] - written %d " + "bytes during 0RTT", + this, aOut0RTTBytesWritten)); + mContentBytesWritten0RTT += aOut0RTTBytesWritten; + if (mSocketOutCondition == NS_BASE_STREAM_WOULD_BLOCK) { + mReceivedSocketWouldBlockDuringFastOpen = true; + } + } + + rv = ssl->DriveHandshake(); + if (NS_FAILED(rv) && rv != NS_BASE_STREAM_WOULD_BLOCK) { + goto npnComplete; + } + + return false; + } + + if (NS_SUCCEEDED(rv)) { + LOG1(("nsHttpConnection::EnsureNPNComplete %p [%s] negotiated to '%s'%s\n", + this, mConnInfo->HashKey().get(), negotiatedNPN.get(), + mTLSFilter ? " [Double Tunnel]" : "")); + + int16_t tlsVersion; + ssl->GetSSLVersionUsed(&tlsVersion); + mConnInfo->SetLessThanTls13( + (tlsVersion < nsISSLSocketControl::TLS_VERSION_1_3) && + (tlsVersion != nsISSLSocketControl::SSL_VERSION_UNKNOWN)); + + bool earlyDataAccepted = false; + if (mWaitingFor0RTTResponse) { + // Check if early data has been accepted. + nsresult rvEarlyData = ssl->GetEarlyDataAccepted(&earlyDataAccepted); + LOG( + ("nsHttpConnection::EnsureNPNComplete [this=%p] - early data " + "that was sent during 0RTT %s been accepted [rv=%" PRIx32 "].", + this, earlyDataAccepted ? "has" : "has not", + static_cast(rv))); + + if (NS_FAILED(rvEarlyData) || + NS_FAILED(mTransaction->Finish0RTT( + !earlyDataAccepted, negotiatedNPN != mEarlyNegotiatedALPN))) { + LOG( + ("nsHttpConection::EnsureNPNComplete [this=%p] closing transaction " + "%p", + this, mTransaction.get())); + mTransaction->Close(NS_ERROR_NET_RESET); + goto npnComplete; + } + } + + // Send the 0RTT telemetry only for tls1.3 + if (tlsVersion > nsISSLSocketControl::TLS_VERSION_1_2) { + Telemetry::Accumulate( + Telemetry::TLS_EARLY_DATA_NEGOTIATED, + (!mEarlyDataNegotiated) + ? TLS_EARLY_DATA_NOT_AVAILABLE + : ((mWaitingFor0RTTResponse) + ? TLS_EARLY_DATA_AVAILABLE_AND_USED + : TLS_EARLY_DATA_AVAILABLE_BUT_NOT_USED)); + if (mWaitingFor0RTTResponse) { + Telemetry::Accumulate(Telemetry::TLS_EARLY_DATA_ACCEPTED, + earlyDataAccepted); + } + if (earlyDataAccepted) { + Telemetry::Accumulate(Telemetry::TLS_EARLY_DATA_BYTES_WRITTEN, + mContentBytesWritten0RTT); + } + } + mWaitingFor0RTTResponse = false; + + if (!earlyDataAccepted) { + LOG( + ("nsHttpConnection::EnsureNPNComplete [this=%p] early data not " + "accepted", + this)); + if (mTransaction->QueryNullTransaction() && + (mBootstrappedTimings.secureConnectionStart.IsNull() || + mBootstrappedTimings.tcpConnectEnd.IsNull())) { + // if TFO is used some socket event will be sent after + // mBootstrappedTimings has been set. therefore we should + // update them. + mBootstrappedTimings.secureConnectionStart = + mTransaction->QueryNullTransaction()->GetSecureConnectionStart(); + mBootstrappedTimings.tcpConnectEnd = + mTransaction->QueryNullTransaction()->GetTcpConnectEnd(); + } + uint32_t infoIndex; + const SpdyInformation* info = gHttpHandler->SpdyInfo(); + if (NS_SUCCEEDED(info->GetNPNIndex(negotiatedNPN, &infoIndex))) { + StartSpdy(ssl, info->Version[infoIndex]); + } + } else { + LOG(("nsHttpConnection::EnsureNPNComplete [this=%p] - %" PRId64 " bytes " + "has been sent during 0RTT.", + this, mContentBytesWritten0RTT)); + mContentBytesWritten = mContentBytesWritten0RTT; + if (mSpdySession) { + // We had already started 0RTT-spdy, now we need to fully set up + // spdy, since we know we're sticking with it. + LOG( + ("nsHttpConnection::EnsureNPNComplete [this=%p] - finishing " + "StartSpdy for 0rtt spdy session %p", + this, mSpdySession.get())); + StartSpdy(ssl, mSpdySession->SpdyVersion()); + } + } + + Telemetry::Accumulate(Telemetry::SPDY_NPN_CONNECT, UsingSpdy()); + } + +npnComplete: + LOG(("nsHttpConnection::EnsureNPNComplete [this=%p] setting complete to true", + this)); + mNPNComplete = true; + + mTransaction->OnTransportStatus(mSocketTransport, + NS_NET_STATUS_TLS_HANDSHAKE_ENDED, 0); + + // this is happening after the bootstrap was originally written to. so update + // it. + if (mTransaction->QueryNullTransaction() && + (mBootstrappedTimings.secureConnectionStart.IsNull() || + mBootstrappedTimings.tcpConnectEnd.IsNull())) { + // if TFO is used some socket event will be sent after + // mBootstrappedTimings has been set. therefore we should + // update them. + mBootstrappedTimings.secureConnectionStart = + mTransaction->QueryNullTransaction()->GetSecureConnectionStart(); + mBootstrappedTimings.tcpConnectEnd = + mTransaction->QueryNullTransaction()->GetTcpConnectEnd(); + } + + if (securityInfo) { + mBootstrappedTimings.connectEnd = TimeStamp::Now(); + } + + if (mWaitingFor0RTTResponse) { + // Didn't get 0RTT OK, back out of the "attempting 0RTT" state + mWaitingFor0RTTResponse = false; + LOG(("nsHttpConnection::EnsureNPNComplete [this=%p] 0rtt failed", this)); + if (NS_FAILED(mTransaction->Finish0RTT( + true, negotiatedNPN != mEarlyNegotiatedALPN))) { + mTransaction->Close(NS_ERROR_NET_RESET); + } + mContentBytesWritten0RTT = 0; + } + + if (mDid0RTTSpdy && negotiatedNPN != mEarlyNegotiatedALPN) { + // Reset the work done by Start0RTTSpdy + LOG(( + "nsHttpConnection::EnsureNPNComplete [this=%p] resetting Start0RTTSpdy", + this)); + mUsingSpdyVersion = SpdyVersion::NONE; + mTransaction = nullptr; + mSpdySession = nullptr; + // We have to reset this here, just in case we end up starting spdy again, + // so it can actually do everything it needs to do. + mDid0RTTSpdy = false; + } + + if (rv == psm::GetXPCOMFromNSSError( + mozilla::pkix::MOZILLA_PKIX_ERROR_MITM_DETECTED)) { + gSocketTransportService->SetNotTrustedMitmDetected(); + } + return true; +} + +nsresult nsHttpConnection::OnTunnelNudged(TLSFilterTransaction* trans) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG(("nsHttpConnection::OnTunnelNudged %p\n", this)); + if (trans != mTLSFilter) { + return NS_OK; + } + LOG(("nsHttpConnection::OnTunnelNudged %p Calling OnSocketWritable\n", this)); + return OnSocketWritable(); +} + +// called on the socket thread +nsresult nsHttpConnection::Activate(nsAHttpTransaction* trans, uint32_t caps, + int32_t pri) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG1(("nsHttpConnection::Activate [this=%p trans=%p caps=%x]\n", this, trans, + caps)); + + if (!mExperienced && !trans->IsNullTransaction()) { + // For QUIC and TFO we have nsHttpConneciton before the actual connection + // has been establish so wait fo TFO and TLS handshake to be finished before + // we mark the connection 'experienced'. + if (!mFastOpen && mNPNComplete) { + mExperienced = true; + } + if (mBootstrappedTimingsSet) { + mBootstrappedTimingsSet = false; + nsHttpTransaction* hTrans = trans->QueryHttpTransaction(); + if (hTrans) { + hTrans->BootstrapTimings(mBootstrappedTimings); + SetUrgentStartPreferred(hTrans->ClassOfService() & + nsIClassOfService::UrgentStart); + } + } + mBootstrappedTimings = TimingStruct(); + } + + if (caps & NS_HTTP_LARGE_KEEPALIVE) { + mDefaultTimeoutFactor = 10; // don't ever lower + } + + mTransactionCaps = caps; + mPriority = pri; + if (mTransaction && (mUsingSpdyVersion != SpdyVersion::NONE)) { + return AddTransaction(trans, pri); + } + + NS_ENSURE_ARG_POINTER(trans); + NS_ENSURE_TRUE(!mTransaction, NS_ERROR_IN_PROGRESS); + + // If TCP fast Open has been used and conection was idle for some time + // we will be cautious and watch out for bug 1395494. + if (mNPNComplete && (mFastOpenStatus == TFO_DATA_SENT) && + gHttpHandler + ->CheckIfConnectionIsStalledOnlyIfIdleForThisAmountOfSeconds() && + IdleTime() >= + gHttpHandler + ->CheckIfConnectionIsStalledOnlyIfIdleForThisAmountOfSeconds()) { + // If a connection was using the TCP FastOpen and it was idle for a + // long time we should check for stalls like bug 1395494. + mCheckNetworkStallsWithTFO = true; + // Also reset last write. We should start measuring a stall time only + // after we really write a request to the network. + mLastRequestBytesSentTime = 0; + } + // reset the read timers to wash away any idle time + mLastWriteTime = mLastReadTime = PR_IntervalNow(); + + // Connection failures are Activated() just like regular transacions. + // If we don't have a confirmation of a connected socket then test it + // with a write() to get relevant error code. + if (!mConnectedTransport) { + uint32_t count; + mSocketOutCondition = NS_ERROR_FAILURE; + if (mSocketOut) { + mSocketOutCondition = mSocketOut->Write("", 0, &count); + } + if (NS_FAILED(mSocketOutCondition) && + mSocketOutCondition != NS_BASE_STREAM_WOULD_BLOCK) { + LOG(("nsHttpConnection::Activate [this=%p] Bad Socket %" PRIx32 "\n", + this, static_cast(mSocketOutCondition))); + mSocketOut->AsyncWait(nullptr, 0, 0, nullptr); + mTransaction = trans; + CloseTransaction(mTransaction, mSocketOutCondition); + return mSocketOutCondition; + } + } + + // Update security callbacks + nsCOMPtr callbacks; + trans->GetSecurityCallbacks(getter_AddRefs(callbacks)); + SetSecurityCallbacks(callbacks); + SetupSSL(); + + // take ownership of the transaction + mTransaction = trans; + + MOZ_ASSERT(!mIdleMonitoring, "Activating a connection with an Idle Monitor"); + mIdleMonitoring = false; + + // set mKeepAlive according to what will be requested + mKeepAliveMask = mKeepAlive = (caps & NS_HTTP_ALLOW_KEEPALIVE); + + // need to handle HTTP CONNECT tunnels if this is the first time if + // we are tunneling through a proxy + nsresult rv = NS_OK; + if (mTransaction->ConnectionInfo()->UsingConnect() && + !mCompletedProxyConnect) { + rv = SetupProxyConnect(); + if (NS_FAILED(rv)) goto failed_activation; + mProxyConnectInProgress = true; + } + + // Clear the per activation counter + mCurrentBytesRead = 0; + + // The overflow state is not needed between activations + mInputOverflow = nullptr; + + mResponseTimeoutEnabled = gHttpHandler->ResponseTimeoutEnabled() && + mTransaction->ResponseTimeout() > 0 && + mTransaction->ResponseTimeoutEnabled(); + + rv = StartShortLivedTCPKeepalives(); + if (NS_FAILED(rv)) { + LOG( + ("nsHttpConnection::Activate [%p] " + "StartShortLivedTCPKeepalives failed rv[0x%" PRIx32 "]", + this, static_cast(rv))); + } + + if (mTLSFilter) { + RefPtr baseTrans(do_QueryReferent(mWeakTrans)); + rv = mTLSFilter->SetProxiedTransaction(trans, baseTrans); + NS_ENSURE_SUCCESS(rv, rv); + if (mTransaction->ConnectionInfo()->UsingConnect()) { + SpdyConnectTransaction* trans = + baseTrans ? baseTrans->QuerySpdyConnectTransaction() : nullptr; + if (trans && !trans->IsWebsocket()) { + // If we are here, the tunnel is already established. Let the + // transaction know that proxy connect is successful. + mTransaction->OnProxyConnectComplete(200); + } + } + mTransaction = mTLSFilter; + } + + trans->OnActivated(); + + rv = OnOutputStreamReady(mSocketOut); + +failed_activation: + if (NS_FAILED(rv)) { + mTransaction = nullptr; + } + + return rv; +} + +void nsHttpConnection::SetupSSL() { + LOG1(("nsHttpConnection::SetupSSL %p caps=0x%X %s\n", this, mTransactionCaps, + mConnInfo->HashKey().get())); + + if (mSetupSSLCalled) // do only once + return; + mSetupSSLCalled = true; + + if (mNPNComplete) return; + + // we flip this back to false if SetNPNList succeeds at the end + // of this function + mNPNComplete = true; + + if (!mConnInfo->FirstHopSSL() || mForcePlainText) { + return; + } + + // if we are connected to the proxy with TLS, start the TLS + // flow immediately without waiting for a CONNECT sequence. + DebugOnly rv; + if (mInSpdyTunnel) { + rv = InitSSLParams(false, true); + } else { + bool usingHttpsProxy = mConnInfo->UsingHttpsProxy(); + rv = InitSSLParams(usingHttpsProxy, usingHttpsProxy); + } + MOZ_ASSERT(NS_SUCCEEDED(rv)); +} + +// The naming of NPN is historical - this function creates the basic +// offer list for both NPN and ALPN. ALPN validation callbacks are made +// now before the handshake is complete, and NPN validation callbacks +// are made during the handshake. +nsresult nsHttpConnection::SetupNPNList(nsISSLSocketControl* ssl, + uint32_t caps) { + nsTArray protocolArray; + + nsCString npnToken = mConnInfo->GetNPNToken(); + if (npnToken.IsEmpty()) { + // The first protocol is used as the fallback if none of the + // protocols supported overlap with the server's list. + // When using ALPN the advertised preferences are protocolArray indicies + // {1, .., N, 0} in decreasing order. + // For NPN, In the case of overlap, matching priority is driven by + // the order of the server's advertisement - with index 0 used when + // there is no match. + protocolArray.AppendElement("http/1.1"_ns); + + if (gHttpHandler->IsSpdyEnabled() && !(caps & NS_HTTP_DISALLOW_SPDY)) { + LOG(("nsHttpConnection::SetupSSL Allow SPDY NPN selection")); + const SpdyInformation* info = gHttpHandler->SpdyInfo(); + for (uint32_t index = SpdyInformation::kCount; index > 0; --index) { + if (info->ProtocolEnabled(index - 1) && + info->ALPNCallbacks[index - 1](ssl)) { + protocolArray.AppendElement(info->VersionString[index - 1]); + } + } + } + } else { + LOG(("nsHttpConnection::SetupSSL limiting NPN selection to %s", + npnToken.get())); + protocolArray.AppendElement(npnToken); + } + + nsresult rv = ssl->SetNPNList(protocolArray); + LOG(("nsHttpConnection::SetupNPNList %p %" PRIx32 "\n", this, + static_cast(rv))); + return rv; +} + +nsresult nsHttpConnection::AddTransaction(nsAHttpTransaction* httpTransaction, + int32_t priority) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + MOZ_ASSERT(mSpdySession && (mUsingSpdyVersion != SpdyVersion::NONE), + "AddTransaction to live http connection without spdy/quic"); + + // If this is a wild card nshttpconnection (i.e. a spdy proxy) then + // it is important to start the stream using the specific connection + // info of the transaction to ensure it is routed on the right tunnel + + nsHttpConnectionInfo* transCI = httpTransaction->ConnectionInfo(); + + bool needTunnel = transCI->UsingHttpsProxy(); + needTunnel = needTunnel && !mTLSFilter; + needTunnel = needTunnel && transCI->UsingConnect(); + needTunnel = needTunnel && httpTransaction->QueryHttpTransaction(); + + // Let the transaction know that the tunnel is already established and we + // don't need to setup the tunnel again. + if (transCI->UsingConnect() && mEverUsedSpdy && mTLSFilter) { + httpTransaction->OnProxyConnectComplete(200); + } + + bool isWebsocket = false; + nsHttpTransaction* trans = httpTransaction->QueryHttpTransaction(); + if (trans) { + isWebsocket = trans->IsWebsocketUpgrade(); + MOZ_ASSERT(!isWebsocket || !needTunnel, "Websocket and tunnel?!"); + } + + LOG(("nsHttpConnection::AddTransaction [this=%p] for %s%s", this, + mSpdySession ? "SPDY" : "QUIC", + needTunnel ? " over tunnel" : (isWebsocket ? " websocket" : ""))); + + if (mSpdySession) { + if (!mSpdySession->AddStream(httpTransaction, priority, needTunnel, + isWebsocket, mCallbacks)) { + MOZ_ASSERT(false); // this cannot happen! + httpTransaction->Close(NS_ERROR_ABORT); + return NS_ERROR_FAILURE; + } + } + + Unused << ResumeSend(); + return NS_OK; +} + +void nsHttpConnection::Close(nsresult reason, bool aIsShutdown) { + LOG(("nsHttpConnection::Close [this=%p reason=%" PRIx32 "]\n", this, + static_cast(reason))); + + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + // Ensure TCP keepalive timer is stopped. + if (mTCPKeepaliveTransitionTimer) { + mTCPKeepaliveTransitionTimer->Cancel(); + mTCPKeepaliveTransitionTimer = nullptr; + } + if (mForceSendTimer) { + mForceSendTimer->Cancel(); + mForceSendTimer = nullptr; + } + + if (!mTrafficCategory.IsEmpty()) { + HttpTrafficAnalyzer* hta = gHttpHandler->GetHttpTrafficAnalyzer(); + if (hta) { + hta->IncrementHttpConnection(std::move(mTrafficCategory)); + MOZ_ASSERT(mTrafficCategory.IsEmpty()); + } + } + + if (NS_FAILED(reason)) { + if (mIdleMonitoring) EndIdleMonitoring(); + + mTLSFilter = nullptr; + + // The connection and security errors clear out alt-svc mappings + // in case any previously validated ones are now invalid + if (((reason == NS_ERROR_NET_RESET) || + (NS_ERROR_GET_MODULE(reason) == NS_ERROR_MODULE_SECURITY)) && + mConnInfo && !(mTransactionCaps & NS_HTTP_ERROR_SOFTLY)) { + gHttpHandler->ClearHostMapping(mConnInfo); + } + + if (mSocketTransport) { + mSocketTransport->SetEventSink(nullptr, nullptr); + + // If there are bytes sitting in the input queue then read them + // into a junk buffer to avoid generating a tcp rst by closing a + // socket with data pending. TLS is a classic case of this where + // a Alert record might be superfulous to a clean HTTP/SPDY shutdown. + // Never block to do this and limit it to a small amount of data. + // During shutdown just be fast! + if (mSocketIn && !aIsShutdown) { + char buffer[4000]; + uint32_t count, total = 0; + nsresult rv; + do { + rv = mSocketIn->Read(buffer, 4000, &count); + if (NS_SUCCEEDED(rv)) total += count; + } while (NS_SUCCEEDED(rv) && count > 0 && total < 64000); + LOG(("nsHttpConnection::Close drained %d bytes\n", total)); + } + + mSocketTransport->SetSecurityCallbacks(nullptr); + mSocketTransport->Close(reason); + if (mSocketOut) mSocketOut->AsyncWait(nullptr, 0, 0, nullptr); + } + mKeepAlive = false; + } +} + +// called on the socket thread +nsresult nsHttpConnection::InitSSLParams(bool connectingToProxy, + bool proxyStartSSL) { + LOG(("nsHttpConnection::InitSSLParams [this=%p] connectingToProxy=%d\n", this, + connectingToProxy)); + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + nsresult rv; + nsCOMPtr securityInfo; + GetSecurityInfo(getter_AddRefs(securityInfo)); + if (!securityInfo) { + return NS_ERROR_FAILURE; + } + + nsCOMPtr ssl = do_QueryInterface(securityInfo, &rv); + if (NS_FAILED(rv)) { + return rv; + } + + if (proxyStartSSL) { + rv = ssl->ProxyStartSSL(); + if (NS_FAILED(rv)) { + return rv; + } + } + + if (NS_SUCCEEDED(SetupNPNList(ssl, mTransactionCaps))) { + LOG(("InitSSLParams Setting up SPDY Negotiation OK")); + mNPNComplete = false; + } + + return NS_OK; +} + +void nsHttpConnection::DontReuse() { + LOG(("nsHttpConnection::DontReuse %p spdysession=%p\n", this, + mSpdySession.get())); + mKeepAliveMask = false; + mKeepAlive = false; + mDontReuse = true; + mIdleTimeout = 0; + if (mSpdySession) { + mSpdySession->DontReuse(); + } +} + +bool nsHttpConnection::TestJoinConnection(const nsACString& hostname, + int32_t port) { + if (mSpdySession && CanDirectlyActivate()) { + return mSpdySession->TestJoinConnection(hostname, port); + } + + return false; +} + +bool nsHttpConnection::JoinConnection(const nsACString& hostname, + int32_t port) { + if (mSpdySession && CanDirectlyActivate()) { + return mSpdySession->JoinConnection(hostname, port); + } + + return false; +} + +bool nsHttpConnection::CanReuse() { + if (mDontReuse || !mRemainingConnectionUses) { + return false; + } + + if ((mTransaction ? (mTransaction->IsDone() ? 0U : 1U) : 0U) >= + mRemainingConnectionUses) { + return false; + } + + bool canReuse; + if (mSpdySession) { + canReuse = mSpdySession->CanReuse(); + } else { + canReuse = IsKeepAlive(); + } + + canReuse = canReuse && (IdleTime() < mIdleTimeout) && IsAlive(); + + // An idle persistent connection should not have data waiting to be read + // before a request is sent. Data here is likely a 408 timeout response + // which we would deal with later on through the restart logic, but that + // path is more expensive than just closing the socket now. + + uint64_t dataSize; + if (canReuse && mSocketIn && (mUsingSpdyVersion == SpdyVersion::NONE) && + mHttp1xTransactionCount && + NS_SUCCEEDED(mSocketIn->Available(&dataSize)) && dataSize) { + LOG( + ("nsHttpConnection::CanReuse %p %s" + "Socket not reusable because read data pending (%" PRIu64 ") on it.\n", + this, mConnInfo->Origin(), dataSize)); + canReuse = false; + } + return canReuse; +} + +bool nsHttpConnection::CanDirectlyActivate() { + // return true if a new transaction can be addded to ths connection at any + // time through Activate(). In practice this means this is a healthy SPDY + // connection with room for more concurrent streams. + + return UsingSpdy() && CanReuse() && mSpdySession && + mSpdySession->RoomForMoreStreams(); +} + +PRIntervalTime nsHttpConnection::IdleTime() { + return mSpdySession ? mSpdySession->IdleTime() + : (PR_IntervalNow() - mLastReadTime); +} + +// returns the number of seconds left before the allowable idle period +// expires, or 0 if the period has already expied. +uint32_t nsHttpConnection::TimeToLive() { + LOG(("nsHttpConnection::TTL: %p %s idle %d timeout %d\n", this, + mConnInfo->Origin(), IdleTime(), mIdleTimeout)); + + if (IdleTime() >= mIdleTimeout) { + return 0; + } + + uint32_t timeToLive = PR_IntervalToSeconds(mIdleTimeout - IdleTime()); + + // a positive amount of time can be rounded to 0. Because 0 is used + // as the expiration signal, round all values from 0 to 1 up to 1. + if (!timeToLive) { + timeToLive = 1; + } + return timeToLive; +} + +bool nsHttpConnection::IsAlive() { + if (!mSocketTransport || !mConnectedTransport) return false; + + // SocketTransport::IsAlive can run the SSL state machine, so make sure + // the NPN options are set before that happens. + SetupSSL(); + + bool alive; + nsresult rv = mSocketTransport->IsAlive(&alive); + if (NS_FAILED(rv)) alive = false; + +//#define TEST_RESTART_LOGIC +#ifdef TEST_RESTART_LOGIC + if (!alive) { + LOG(("pretending socket is still alive to test restart logic\n")); + alive = true; + } +#endif + + return alive; +} + +void nsHttpConnection::SetUrgentStartPreferred(bool urgent) { + if (mExperienced && !mUrgentStartPreferredKnown) { + // Set only according the first ever dispatched non-null transaction + mUrgentStartPreferredKnown = true; + mUrgentStartPreferred = urgent; + LOG(("nsHttpConnection::SetUrgentStartPreferred [this=%p urgent=%d]", this, + urgent)); + } +} + +//---------------------------------------------------------------------------- +// nsHttpConnection::nsAHttpConnection compatible methods +//---------------------------------------------------------------------------- + +nsresult nsHttpConnection::OnHeadersAvailable(nsAHttpTransaction* trans, + nsHttpRequestHead* requestHead, + nsHttpResponseHead* responseHead, + bool* reset) { + LOG( + ("nsHttpConnection::OnHeadersAvailable [this=%p trans=%p " + "response-head=%p]\n", + this, trans, responseHead)); + + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + NS_ENSURE_ARG_POINTER(trans); + MOZ_ASSERT(responseHead, "No response head?"); + + if (mInSpdyTunnel) { + DebugOnly rv = + responseHead->SetHeader(nsHttp::X_Firefox_Spdy_Proxy, "true"_ns); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + + // we won't change our keep-alive policy unless the server has explicitly + // told us to do so. + + // inspect the connection headers for keep-alive info provided the + // transaction completed successfully. In the case of a non-sensical close + // and keep-alive favor the close out of conservatism. + + bool explicitKeepAlive = false; + bool explicitClose = + responseHead->HasHeaderValue(nsHttp::Connection, "close") || + responseHead->HasHeaderValue(nsHttp::Proxy_Connection, "close"); + if (!explicitClose) + explicitKeepAlive = + responseHead->HasHeaderValue(nsHttp::Connection, "keep-alive") || + responseHead->HasHeaderValue(nsHttp::Proxy_Connection, "keep-alive"); + + // deal with 408 Server Timeouts + uint16_t responseStatus = responseHead->Status(); + static const PRIntervalTime k1000ms = PR_MillisecondsToInterval(1000); + if (responseStatus == 408) { + // If this error could be due to a persistent connection reuse then + // we pass an error code of NS_ERROR_NET_RESET to + // trigger the transaction 'restart' mechanism. We tell it to reset its + // response headers so that it will be ready to receive the new response. + if (mIsReused && ((PR_IntervalNow() - mLastWriteTime) < k1000ms)) { + Close(NS_ERROR_NET_RESET); + *reset = true; + return NS_OK; + } + + // timeouts that are not caused by persistent connection reuse should + // not be retried for browser compatibility reasons. bug 907800. The + // server driven close is implicit in the 408. + explicitClose = true; + explicitKeepAlive = false; + } + + if ((responseHead->Version() < HttpVersion::v1_1) || + (requestHead->Version() < HttpVersion::v1_1)) { + // HTTP/1.0 connections are by default NOT persistent + if (explicitKeepAlive) + mKeepAlive = true; + else + mKeepAlive = false; + } else { + // HTTP/1.1 connections are by default persistent + mKeepAlive = !explicitClose; + } + mKeepAliveMask = mKeepAlive; + + // if this connection is persistent, then the server may send a "Keep-Alive" + // header specifying the maximum number of times the connection can be + // reused as well as the maximum amount of time the connection can be idle + // before the server will close it. we ignore the max reuse count, because + // a "keep-alive" connection is by definition capable of being reused, and + // we only care about being able to reuse it once. if a timeout is not + // specified then we use our advertized timeout value. + bool foundKeepAliveMax = false; + if (mKeepAlive) { + nsAutoCString keepAlive; + Unused << responseHead->GetHeader(nsHttp::Keep_Alive, keepAlive); + + if (mUsingSpdyVersion == SpdyVersion::NONE) { + const char* cp = PL_strcasestr(keepAlive.get(), "timeout="); + if (cp) + mIdleTimeout = PR_SecondsToInterval((uint32_t)atoi(cp + 8)); + else + mIdleTimeout = gHttpHandler->IdleTimeout() * mDefaultTimeoutFactor; + + cp = PL_strcasestr(keepAlive.get(), "max="); + if (cp) { + int maxUses = atoi(cp + 4); + if (maxUses > 0) { + foundKeepAliveMax = true; + mRemainingConnectionUses = static_cast(maxUses); + } + } + } + + LOG(("Connection can be reused [this=%p idle-timeout=%usec]\n", this, + PR_IntervalToSeconds(mIdleTimeout))); + } + + if (!foundKeepAliveMax && mRemainingConnectionUses && + (mUsingSpdyVersion == SpdyVersion::NONE)) { + --mRemainingConnectionUses; + } + + // If we're doing a proxy connect, we need to check whether or not + // it was successful. If so, we have to reset the transaction and step-up + // the socket connection if using SSL. Finally, we have to wake up the + // socket write request. + bool itWasProxyConnect = !!mProxyConnectStream; + if (mProxyConnectStream) { + MOZ_ASSERT(mUsingSpdyVersion == SpdyVersion::NONE, + "SPDY NPN Complete while using proxy connect stream"); + mProxyConnectStream = nullptr; + bool isHttps = mTransaction ? mTransaction->ConnectionInfo()->EndToEndSSL() + : mConnInfo->EndToEndSSL(); + bool onlyConnect = mTransactionCaps & NS_HTTP_CONNECT_ONLY; + + mTransaction->OnProxyConnectComplete(responseStatus); + if (responseStatus == 200) { + LOG(("proxy CONNECT succeeded! endtoendssl=%d onlyconnect=%d\n", isHttps, + onlyConnect)); + // If we're only connecting, we don't need to reset the transaction + // state. We need to upgrade the socket now without doing the actual + // http request. + if (!onlyConnect) { + *reset = true; + } + nsresult rv; + // CONNECT only flag doesn't do the tls setup. https here only + // ensures a proxy tunnel was used not that tls is setup. + if (isHttps) { + if (!onlyConnect) { + if (mConnInfo->UsingHttpsProxy()) { + LOG(("%p new TLSFilterTransaction %s %d\n", this, + mConnInfo->Origin(), mConnInfo->OriginPort())); + SetupSecondaryTLS(); + } + + rv = InitSSLParams(false, true); + LOG(("InitSSLParams [rv=%" PRIx32 "]\n", static_cast(rv))); + } else { + // We have an https protocol but the CONNECT only flag was + // specified. The consumer only wants a raw socket to the + // proxy. We have to mark this as complete to finish the + // transaction and be upgraded. OnSocketReadable() uses this + // to detect an inactive tunnel and blocks completion. + mNPNComplete = true; + } + } + mCompletedProxyConnect = true; + mProxyConnectInProgress = false; + rv = mSocketOut->AsyncWait(this, 0, 0, nullptr); + // XXX what if this fails -- need to handle this error + MOZ_ASSERT(NS_SUCCEEDED(rv), "mSocketOut->AsyncWait failed"); + } else { + LOG(("proxy CONNECT failed! endtoendssl=%d onlyconnect=%d\n", isHttps, + onlyConnect)); + mTransaction->SetProxyConnectFailed(); + } + } + + nsAutoCString upgradeReq; + bool hasUpgradeReq = + NS_SUCCEEDED(requestHead->GetHeader(nsHttp::Upgrade, upgradeReq)); + // Don't use persistent connection for Upgrade unless there's an auth failure: + // some proxies expect to see auth response on persistent connection. + // Also allow persistent conn for h2, as we don't want to waste connections + // for multiplexed upgrades. + if (!itWasProxyConnect && hasUpgradeReq && responseStatus != 401 && + responseStatus != 407 && !mSpdySession) { + LOG(("HTTP Upgrade in play - disable keepalive for http/1.x\n")); + DontReuse(); + } + + if (responseStatus == 101) { + nsAutoCString upgradeResp; + bool hasUpgradeResp = + NS_SUCCEEDED(responseHead->GetHeader(nsHttp::Upgrade, upgradeResp)); + if (!hasUpgradeReq || !hasUpgradeResp || + !nsHttp::FindToken(upgradeResp.get(), upgradeReq.get(), + HTTP_HEADER_VALUE_SEPS)) { + LOG(("HTTP 101 Upgrade header mismatch req = %s, resp = %s\n", + upgradeReq.get(), + !upgradeResp.IsEmpty() ? upgradeResp.get() + : "RESPONSE's nsHttp::Upgrade is empty")); + Close(NS_ERROR_ABORT); + } else { + LOG(("HTTP Upgrade Response to %s\n", upgradeResp.get())); + } + } + + mLastHttpResponseVersion = responseHead->Version(); + + return NS_OK; +} + +bool nsHttpConnection::IsReused() { + if (mIsReused) return true; + if (!mConsiderReusedAfterInterval) return false; + + // ReusedAfter allows a socket to be consider reused only after a certain + // interval of time has passed + return (PR_IntervalNow() - mConsiderReusedAfterEpoch) >= + mConsiderReusedAfterInterval; +} + +void nsHttpConnection::SetIsReusedAfter(uint32_t afterMilliseconds) { + mConsiderReusedAfterEpoch = PR_IntervalNow(); + mConsiderReusedAfterInterval = PR_MillisecondsToInterval(afterMilliseconds); +} + +nsresult nsHttpConnection::TakeTransport(nsISocketTransport** aTransport, + nsIAsyncInputStream** aInputStream, + nsIAsyncOutputStream** aOutputStream) { + if (mUsingSpdyVersion != SpdyVersion::NONE) return NS_ERROR_FAILURE; + if (mTransaction && !mTransaction->IsDone()) return NS_ERROR_IN_PROGRESS; + if (!(mSocketTransport && mSocketIn && mSocketOut)) + return NS_ERROR_NOT_INITIALIZED; + + if (mInputOverflow) mSocketIn = mInputOverflow.forget(); + + // Change TCP Keepalive frequency to long-lived if currently short-lived. + if (mTCPKeepaliveConfig == kTCPKeepaliveShortLivedConfig) { + if (mTCPKeepaliveTransitionTimer) { + mTCPKeepaliveTransitionTimer->Cancel(); + mTCPKeepaliveTransitionTimer = nullptr; + } + nsresult rv = StartLongLivedTCPKeepalives(); + LOG( + ("nsHttpConnection::TakeTransport [%p] calling " + "StartLongLivedTCPKeepalives", + this)); + if (NS_FAILED(rv)) { + LOG( + ("nsHttpConnection::TakeTransport [%p] " + "StartLongLivedTCPKeepalives failed rv[0x%" PRIx32 "]", + this, static_cast(rv))); + } + } + + mSocketTransport->SetSecurityCallbacks(nullptr); + mSocketTransport->SetEventSink(nullptr, nullptr); + + // The nsHttpConnection will go away soon, so if there is a TLS Filter + // being used (e.g. for wss CONNECT tunnel from a proxy connected to + // via https) that filter needs to take direct control of the + // streams + if (mTLSFilter) { + nsCOMPtr ref1(mSocketIn); + nsCOMPtr ref2(mSocketOut); + mTLSFilter->newIODriver(ref1, ref2, getter_AddRefs(mSocketIn), + getter_AddRefs(mSocketOut)); + mTLSFilter = nullptr; + } + + mSocketTransport.forget(aTransport); + mSocketIn.forget(aInputStream); + mSocketOut.forget(aOutputStream); + + return NS_OK; +} + +uint32_t nsHttpConnection::ReadTimeoutTick(PRIntervalTime now) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + // make sure timer didn't tick before Activate() + if (!mTransaction) return UINT32_MAX; + + // Spdy implements some timeout handling using the SPDY ping frame. + if (mSpdySession) { + return mSpdySession->ReadTimeoutTick(now); + } + + uint32_t nextTickAfter = UINT32_MAX; + // Timeout if the response is taking too long to arrive. + if (mResponseTimeoutEnabled) { + NS_WARNING_ASSERTION( + gHttpHandler->ResponseTimeoutEnabled(), + "Timing out a response, but response timeout is disabled!"); + + PRIntervalTime initialResponseDelta = now - mLastWriteTime; + + if (initialResponseDelta > mTransaction->ResponseTimeout()) { + LOG(("canceling transaction: no response for %ums: timeout is %dms\n", + PR_IntervalToMilliseconds(initialResponseDelta), + PR_IntervalToMilliseconds(mTransaction->ResponseTimeout()))); + + mResponseTimeoutEnabled = false; + + // This will also close the connection + CloseTransaction(mTransaction, NS_ERROR_NET_TIMEOUT); + return UINT32_MAX; + } + nextTickAfter = PR_IntervalToSeconds(mTransaction->ResponseTimeout()) - + PR_IntervalToSeconds(initialResponseDelta); + nextTickAfter = std::max(nextTickAfter, 1U); + } + + // Check for the TCP Fast Open related stalls. + if (mCheckNetworkStallsWithTFO && mLastRequestBytesSentTime) { + PRIntervalTime initialResponseDelta = now - mLastRequestBytesSentTime; + if (initialResponseDelta >= gHttpHandler->FastOpenStallsTimeout()) { + gHttpHandler->IncrementFastOpenStallsCounter(); + mCheckNetworkStallsWithTFO = false; + } else { + uint32_t next = + PR_IntervalToSeconds(gHttpHandler->FastOpenStallsTimeout()) - + PR_IntervalToSeconds(initialResponseDelta); + nextTickAfter = std::min(nextTickAfter, next); + } + } + + if (!mNPNComplete) { + // We can reuse mLastWriteTime here, because it is set when the + // connection is activated and only change when a transaction + // succesfullu write to the socket and this can only happen after + // the TLS handshake is done. + PRIntervalTime initialTLSDelta = now - mLastWriteTime; + if (initialTLSDelta > + PR_MillisecondsToInterval(gHttpHandler->TLSHandshakeTimeout())) { + LOG( + ("canceling transaction: tls handshake takes too long: tls handshake " + "last %ums, timeout is %dms.", + PR_IntervalToMilliseconds(initialTLSDelta), + gHttpHandler->TLSHandshakeTimeout())); + + // This will also close the connection + CloseTransaction(mTransaction, NS_ERROR_NET_TIMEOUT); + return UINT32_MAX; + } + } + + return nextTickAfter; +} + +void nsHttpConnection::UpdateTCPKeepalive(nsITimer* aTimer, void* aClosure) { + MOZ_ASSERT(aTimer); + MOZ_ASSERT(aClosure); + + nsHttpConnection* self = static_cast(aClosure); + + if (NS_WARN_IF(self->mUsingSpdyVersion != SpdyVersion::NONE)) { + return; + } + + // Do not reduce keepalive probe frequency for idle connections. + if (self->mIdleMonitoring) { + return; + } + + nsresult rv = self->StartLongLivedTCPKeepalives(); + if (NS_FAILED(rv)) { + LOG( + ("nsHttpConnection::UpdateTCPKeepalive [%p] " + "StartLongLivedTCPKeepalives failed rv[0x%" PRIx32 "]", + self, static_cast(rv))); + } +} + +void nsHttpConnection::GetSecurityInfo(nsISupports** secinfo) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG(("nsHttpConnection::GetSecurityInfo trans=%p tlsfilter=%p socket=%p\n", + mTransaction.get(), mTLSFilter.get(), mSocketTransport.get())); + + if (mTransaction && + NS_SUCCEEDED(mTransaction->GetTransactionSecurityInfo(secinfo))) { + return; + } + + if (mTLSFilter && + NS_SUCCEEDED(mTLSFilter->GetTransactionSecurityInfo(secinfo))) { + return; + } + + if (mSocketTransport && + NS_SUCCEEDED(mSocketTransport->GetSecurityInfo(secinfo))) { + return; + } + + *secinfo = nullptr; +} + +nsresult nsHttpConnection::PushBack(const char* data, uint32_t length) { + LOG(("nsHttpConnection::PushBack [this=%p, length=%d]\n", this, length)); + + if (mInputOverflow) { + NS_ERROR("nsHttpConnection::PushBack only one buffer supported"); + return NS_ERROR_UNEXPECTED; + } + + mInputOverflow = new nsPreloadedStream(mSocketIn, data, length); + return NS_OK; +} + +class HttpConnectionForceIO : public Runnable { + public: + HttpConnectionForceIO(nsHttpConnection* aConn, bool doRecv, + bool isFastOpenForce) + : Runnable("net::HttpConnectionForceIO"), + mConn(aConn), + mDoRecv(doRecv), + mIsFastOpenForce(isFastOpenForce) {} + + NS_IMETHOD Run() override { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + if (mDoRecv) { + if (!mConn->mSocketIn) return NS_OK; + return mConn->OnInputStreamReady(mConn->mSocketIn); + } + + // This runnable will be called when the ForceIO timer expires + // (mIsFastOpenForce==false) or during the TCP Fast Open to force + // writes (mIsFastOpenForce==true). + if (mIsFastOpenForce && !mConn->mWaitingFor0RTTResponse) { + // If we have exit the TCP Fast Open in the meantime we can skip + // this. + return NS_OK; + } + if (!mIsFastOpenForce) { + MOZ_ASSERT(mConn->mForceSendPending); + mConn->mForceSendPending = false; + } + + if (!mConn->mSocketOut) { + return NS_OK; + } + return mConn->OnOutputStreamReady(mConn->mSocketOut); + } + + private: + RefPtr mConn; + bool mDoRecv; + bool mIsFastOpenForce; +}; + +nsresult nsHttpConnection::ResumeSend() { + LOG(("nsHttpConnection::ResumeSend [this=%p]\n", this)); + + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + if (mSocketOut) { + nsresult rv = mSocketOut->AsyncWait(this, 0, 0, nullptr); + LOG( + ("nsHttpConnection::ResumeSend [this=%p] " + "mWaitingFor0RTTResponse=%d mForceSendDuringFastOpenPending=%d " + "mReceivedSocketWouldBlockDuringFastOpen=%d\n", + this, mWaitingFor0RTTResponse, mForceSendDuringFastOpenPending, + mReceivedSocketWouldBlockDuringFastOpen)); + if (mWaitingFor0RTTResponse && !mForceSendDuringFastOpenPending && + !mReceivedSocketWouldBlockDuringFastOpen && NS_SUCCEEDED(rv)) { + // During TCP Fast Open, poll does not work properly so we will + // trigger writes manually. + mForceSendDuringFastOpenPending = true; + NS_DispatchToCurrentThread(new HttpConnectionForceIO(this, false, true)); + } + return rv; + } + + MOZ_ASSERT_UNREACHABLE("no socket output stream"); + return NS_ERROR_UNEXPECTED; +} + +nsresult nsHttpConnection::ResumeRecv() { + LOG(("nsHttpConnection::ResumeRecv [this=%p]\n", this)); + + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + if (mFastOpen) { + LOG( + ("nsHttpConnection::ResumeRecv - do not waiting for read during " + "fast open! [this=%p]\n", + this)); + return NS_OK; + } + + // the mLastReadTime timestamp is used for finding slowish readers + // and can be pretty sensitive. For that reason we actually reset it + // when we ask to read (resume recv()) so that when we get called back + // with actual read data in OnSocketReadable() we are only measuring + // the latency between those two acts and not all the processing that + // may get done before the ResumeRecv() call + mLastReadTime = PR_IntervalNow(); + + if (mSocketIn) { + if (!mTLSFilter || !mTLSFilter->HasDataToRecv() || NS_FAILED(ForceRecv())) { + return mSocketIn->AsyncWait(this, 0, 0, nullptr); + } + return NS_OK; + } + + MOZ_ASSERT_UNREACHABLE("no socket input stream"); + return NS_ERROR_UNEXPECTED; +} + +void nsHttpConnection::ForceSendIO(nsITimer* aTimer, void* aClosure) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + nsHttpConnection* self = static_cast(aClosure); + MOZ_ASSERT(aTimer == self->mForceSendTimer); + self->mForceSendTimer = nullptr; + NS_DispatchToCurrentThread(new HttpConnectionForceIO(self, false, false)); +} + +nsresult nsHttpConnection::MaybeForceSendIO() { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + // due to bug 1213084 sometimes real I/O events do not get serviced when + // NSPR derived I/O events are ready and this can cause a deadlock with + // https over https proxying. Normally we would expect the write callback to + // be invoked before this timer goes off, but set it at the old windows + // tick interval (kForceDelay) as a backup for those circumstances. + static const uint32_t kForceDelay = 17; // ms + + if (mForceSendPending) { + return NS_OK; + } + MOZ_ASSERT(!mForceSendTimer); + mForceSendPending = true; + return NS_NewTimerWithFuncCallback(getter_AddRefs(mForceSendTimer), + nsHttpConnection::ForceSendIO, this, + kForceDelay, nsITimer::TYPE_ONE_SHOT, + "net::nsHttpConnection::MaybeForceSendIO"); +} + +// trigger an asynchronous read +nsresult nsHttpConnection::ForceRecv() { + LOG(("nsHttpConnection::ForceRecv [this=%p]\n", this)); + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + return NS_DispatchToCurrentThread( + new HttpConnectionForceIO(this, true, false)); +} + +// trigger an asynchronous write +nsresult nsHttpConnection::ForceSend() { + LOG(("nsHttpConnection::ForceSend [this=%p]\n", this)); + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + if (mTLSFilter) { + return mTLSFilter->NudgeTunnel(this); + } + return MaybeForceSendIO(); +} + +void nsHttpConnection::BeginIdleMonitoring() { + LOG(("nsHttpConnection::BeginIdleMonitoring [this=%p]\n", this)); + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + MOZ_ASSERT(!mTransaction, "BeginIdleMonitoring() while active"); + MOZ_ASSERT(mUsingSpdyVersion == SpdyVersion::NONE, + "Idle monitoring of spdy not allowed"); + + LOG(("Entering Idle Monitoring Mode [this=%p]", this)); + mIdleMonitoring = true; + if (mSocketIn) mSocketIn->AsyncWait(this, 0, 0, nullptr); +} + +void nsHttpConnection::EndIdleMonitoring() { + LOG(("nsHttpConnection::EndIdleMonitoring [this=%p]\n", this)); + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + MOZ_ASSERT(!mTransaction, "EndIdleMonitoring() while active"); + + if (mIdleMonitoring) { + LOG(("Leaving Idle Monitoring Mode [this=%p]", this)); + mIdleMonitoring = false; + if (mSocketIn) mSocketIn->AsyncWait(nullptr, 0, 0, nullptr); + } +} + +HttpVersion nsHttpConnection::Version() { + if (mUsingSpdyVersion != SpdyVersion::NONE) { + return HttpVersion::v2_0; + } + return mLastHttpResponseVersion; +} + +//----------------------------------------------------------------------------- +// nsHttpConnection +//----------------------------------------------------------------------------- + +void nsHttpConnection::CloseTransaction(nsAHttpTransaction* trans, + nsresult reason, bool aIsShutdown) { + LOG(("nsHttpConnection::CloseTransaction[this=%p trans=%p reason=%" PRIx32 + "]\n", + this, trans, static_cast(reason))); + + MOZ_ASSERT((trans == mTransaction) || + (mTLSFilter && !mTLSFilter->Transaction()) || + (mTLSFilter && mTLSFilter->Transaction() == trans)); + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + if (mCurrentBytesRead > mMaxBytesRead) mMaxBytesRead = mCurrentBytesRead; + + // mask this error code because its not a real error. + if (reason == NS_BASE_STREAM_CLOSED) reason = NS_OK; + + if (mUsingSpdyVersion != SpdyVersion::NONE) { + DontReuse(); + // if !mSpdySession then mUsingSpdyVersion must be false for canreuse() + mSpdySession->SetCleanShutdown(aIsShutdown); + mUsingSpdyVersion = SpdyVersion::NONE; + mSpdySession = nullptr; + } + + if (!mTransaction && mTLSFilter) { + // In case of a race when the transaction is being closed before the tunnel + // is established we need to carry closing status on the proxied + // transaction. + // Not doing this leads to use of this closed connection to activate the + // not closed transaction what will likely lead to a use of a closed ssl + // socket and may cause a crash because of an unexpected use. + // + // There can possibly be two states: the actual transaction is still hanging + // of off the filter, or has not even been assigned on it yet. In the + // latter case we simply must close the transaction given to us via the + // argument. + if (!mTLSFilter->Transaction()) { + if (trans) { + LOG((" closing transaction directly")); + trans->Close(reason); + } + } else { + LOG((" closing transactin hanging of off mTLSFilter")); + mTLSFilter->Close(reason); + } + } + + if (mTransaction) { + LOG((" closing associated mTransaction")); + mHttp1xTransactionCount += mTransaction->Http1xTransactionCount(); + + mTransaction->Close(reason); + mTransaction = nullptr; + } + + { + MutexAutoLock lock(mCallbacksLock); + mCallbacks = nullptr; + } + + if (NS_FAILED(reason) && (reason != NS_BINDING_RETARGETED)) { + Close(reason, aIsShutdown); + } + + // flag the connection as reused here for convenience sake. certainly + // it might be going away instead ;-) + mIsReused = true; +} + +nsresult nsHttpConnection::ReadFromStream(nsIInputStream* input, void* closure, + const char* buf, uint32_t offset, + uint32_t count, uint32_t* countRead) { + // thunk for nsIInputStream instance + nsHttpConnection* conn = (nsHttpConnection*)closure; + return conn->OnReadSegment(buf, count, countRead); +} + +nsresult nsHttpConnection::OnReadSegment(const char* buf, uint32_t count, + uint32_t* countRead) { + LOG(("nsHttpConnection::OnReadSegment [this=%p]\n", this)); + if (count == 0) { + // some ReadSegments implementations will erroneously call the writer + // to consume 0 bytes worth of data. we must protect against this case + // or else we'd end up closing the socket prematurely. + NS_ERROR("bad ReadSegments implementation"); + return NS_ERROR_FAILURE; // stop iterating + } + + nsresult rv = mSocketOut->Write(buf, count, countRead); + if (NS_FAILED(rv)) + mSocketOutCondition = rv; + else if (*countRead == 0) + mSocketOutCondition = NS_BASE_STREAM_CLOSED; + else { + mLastWriteTime = PR_IntervalNow(); + mSocketOutCondition = NS_OK; // reset condition + if (!mProxyConnectInProgress) mTotalBytesWritten += *countRead; + } + + return mSocketOutCondition; +} + +nsresult nsHttpConnection::OnSocketWritable() { + LOG(("nsHttpConnection::OnSocketWritable [this=%p] host=%s\n", this, + mConnInfo->Origin())); + + nsresult rv; + uint32_t transactionBytes; + bool again = true; + + // Prevent STS thread from being blocked by single OnOutputStreamReady + // callback. + const uint32_t maxWriteAttempts = 128; + uint32_t writeAttempts = 0; + + mForceSendDuringFastOpenPending = false; + + if (mTransactionCaps & NS_HTTP_CONNECT_ONLY) { + if (!mCompletedProxyConnect && !mProxyConnectStream) { + // A CONNECT has been requested for this connection but will never + // be performed. This should never happen. + MOZ_ASSERT(false, "proxy connect will never happen"); + LOG(("return failure because proxy connect will never happen\n")); + return NS_ERROR_FAILURE; + } + + if (mCompletedProxyConnect) { + // Don't need to check this each write attempt since it is only + // updated after OnSocketWritable completes. + // We've already done primary tls (if needed) and sent our CONNECT. + // If we're doing a CONNECT only request there's no need to write + // the http transaction or do the SSL handshake here. + LOG(("return ok because proxy connect successful\n")); + return NS_OK; + } + } + + do { + ++writeAttempts; + rv = mSocketOutCondition = NS_OK; + transactionBytes = 0; + + // The SSL handshake must be completed before the + // transaction->readsegments() processing can proceed because we need to + // know how to format the request differently for http/1, http/2, spdy, + // etc.. and that is negotiated with NPN/ALPN in the SSL handshake. + + if (mConnInfo->UsingHttpsProxy() && + !EnsureNPNComplete(rv, transactionBytes)) { + MOZ_ASSERT(!transactionBytes); + mSocketOutCondition = NS_BASE_STREAM_WOULD_BLOCK; + } else if (mProxyConnectStream) { + // If we're need an HTTP/1 CONNECT tunnel through a proxy + // send it before doing the SSL handshake + LOG((" writing CONNECT request stream\n")); + rv = mProxyConnectStream->ReadSegments(ReadFromStream, this, + nsIOService::gDefaultSegmentSize, + &transactionBytes); + } else if (!EnsureNPNComplete(rv, transactionBytes)) { + if (NS_SUCCEEDED(rv) && !transactionBytes && + NS_SUCCEEDED(mSocketOutCondition)) { + mSocketOutCondition = NS_BASE_STREAM_WOULD_BLOCK; + } + } else if (!mTransaction) { + rv = NS_ERROR_FAILURE; + LOG((" No Transaction In OnSocketWritable\n")); + } else if (NS_SUCCEEDED(rv)) { + // for non spdy sessions let the connection manager know + if (!mReportedSpdy) { + mReportedSpdy = true; + MOZ_ASSERT(!mEverUsedSpdy); + gHttpHandler->ConnMgr()->ReportSpdyConnection(this, false); + } + + LOG((" writing transaction request stream\n")); + mProxyConnectInProgress = false; + rv = mTransaction->ReadSegmentsAgain( + this, nsIOService::gDefaultSegmentSize, &transactionBytes, &again); + mContentBytesWritten += transactionBytes; + } + + LOG( + ("nsHttpConnection::OnSocketWritable %p " + "ReadSegments returned [rv=%" PRIx32 " read=%u " + "sock-cond=%" PRIx32 " again=%d]\n", + this, static_cast(rv), transactionBytes, + static_cast(mSocketOutCondition), again)); + + // XXX some streams return NS_BASE_STREAM_CLOSED to indicate EOF. + if (rv == NS_BASE_STREAM_CLOSED && !mTransaction->IsDone()) { + rv = NS_OK; + transactionBytes = 0; + } + + if (!again && (mFastOpen || mWaitingFor0RTTResponse)) { + // Continue waiting; + rv = mSocketOut->AsyncWait(this, 0, 0, nullptr); + } + if (NS_FAILED(rv)) { + // if the transaction didn't want to write any more data, then + // wait for the transaction to call ResumeSend. + if (rv == NS_BASE_STREAM_WOULD_BLOCK) { + rv = NS_OK; + if (mFastOpen || mWaitingFor0RTTResponse) { + // Continue waiting; + rv = mSocketOut->AsyncWait(this, 0, 0, nullptr); + } + } + again = false; + } else if (NS_FAILED(mSocketOutCondition)) { + if (mSocketOutCondition == NS_BASE_STREAM_WOULD_BLOCK) { + if (mTLSFilter) { + LOG((" blocked tunnel (handshake?)\n")); + rv = mTLSFilter->NudgeTunnel(this); + } else { + rv = mSocketOut->AsyncWait(this, 0, 0, nullptr); // continue writing + } + } else { + rv = mSocketOutCondition; + } + again = false; + } else if (!transactionBytes) { + rv = NS_OK; + + if (mWaitingFor0RTTResponse || mFastOpen) { + // Wait for tls handshake to finish or waiting for connect. + rv = mSocketOut->AsyncWait(this, 0, 0, nullptr); + } else if (mTransaction) { // in case the ReadSegments stack called + // CloseTransaction() + // + // at this point we've written out the entire transaction, and now we + // must wait for the server's response. we manufacture a status message + // here to reflect the fact that we are waiting. this message will be + // trumped (overwritten) if the server responds quickly. + // + mTransaction->OnTransportStatus(mSocketTransport, + NS_NET_STATUS_WAITING_FOR, 0); + if (mCheckNetworkStallsWithTFO) { + mLastRequestBytesSentTime = PR_IntervalNow(); + } + + rv = ResumeRecv(); // start reading + } + again = false; + } else if (writeAttempts >= maxWriteAttempts) { + LOG((" yield for other transactions\n")); + rv = mSocketOut->AsyncWait(this, 0, 0, nullptr); // continue writing + again = false; + } + // write more to the socket until error or end-of-request... + } while (again && gHttpHandler->Active()); + + return rv; +} + +nsresult nsHttpConnection::OnWriteSegment(char* buf, uint32_t count, + uint32_t* countWritten) { + if (count == 0) { + // some WriteSegments implementations will erroneously call the reader + // to provide 0 bytes worth of data. we must protect against this case + // or else we'd end up closing the socket prematurely. + NS_ERROR("bad WriteSegments implementation"); + return NS_ERROR_FAILURE; // stop iterating + } + + if (ChaosMode::isActive(ChaosFeature::IOAmounts) && + ChaosMode::randomUint32LessThan(2)) { + // read 1...count bytes + count = ChaosMode::randomUint32LessThan(count) + 1; + } + + nsresult rv = mSocketIn->Read(buf, count, countWritten); + if (NS_FAILED(rv)) + mSocketInCondition = rv; + else if (*countWritten == 0) + mSocketInCondition = NS_BASE_STREAM_CLOSED; + else + mSocketInCondition = NS_OK; // reset condition + + mCheckNetworkStallsWithTFO = false; + + return mSocketInCondition; +} + +nsresult nsHttpConnection::OnSocketReadable() { + LOG(("nsHttpConnection::OnSocketReadable [this=%p]\n", this)); + + PRIntervalTime now = PR_IntervalNow(); + PRIntervalTime delta = now - mLastReadTime; + + // Reset mResponseTimeoutEnabled to stop response timeout checks. + mResponseTimeoutEnabled = false; + + if ((mTransactionCaps & NS_HTTP_CONNECT_ONLY) && !mCompletedProxyConnect && + !mProxyConnectStream) { + // A CONNECT has been requested for this connection but will never + // be performed. This should never happen. + MOZ_ASSERT(false, "proxy connect will never happen"); + LOG(("return failure because proxy connect will never happen\n")); + return NS_ERROR_FAILURE; + } + + if (mKeepAliveMask && (delta >= mMaxHangTime)) { + LOG(("max hang time exceeded!\n")); + // give the handler a chance to create a new persistent connection to + // this host if we've been busy for too long. + mKeepAliveMask = false; + Unused << gHttpHandler->ProcessPendingQ(mConnInfo); + } + + // Reduce the estimate of the time since last read by up to 1 RTT to + // accommodate exhausted sender TCP congestion windows or minor I/O delays. + mLastReadTime = now; + + nsresult rv; + uint32_t n; + bool again = true; + + do { + if (!mProxyConnectInProgress && !mNPNComplete) { + // Unless we are setting up a tunnel via CONNECT, prevent reading + // from the socket until the results of NPN + // negotiation are known (which is determined from the write path). + // If the server speaks SPDY it is likely the readable data here is + // a spdy settings frame and without NPN it would be misinterpreted + // as HTTP/* + + LOG( + ("nsHttpConnection::OnSocketReadable %p return due to inactive " + "tunnel setup but incomplete NPN state\n", + this)); + rv = NS_OK; + break; + } + + mSocketInCondition = NS_OK; + rv = mTransaction->WriteSegmentsAgain( + this, nsIOService::gDefaultSegmentSize, &n, &again); + LOG(("nsHttpConnection::OnSocketReadable %p trans->ws rv=%" PRIx32 + " n=%d socketin=%" PRIx32 "\n", + this, static_cast(rv), n, + static_cast(mSocketInCondition))); + if (NS_FAILED(rv)) { + // if the transaction didn't want to take any more data, then + // wait for the transaction to call ResumeRecv. + if (rv == NS_BASE_STREAM_WOULD_BLOCK) { + rv = NS_OK; + } + again = false; + } else { + mCurrentBytesRead += n; + mTotalBytesRead += n; + if (NS_FAILED(mSocketInCondition)) { + // continue waiting for the socket if necessary... + if (mSocketInCondition == NS_BASE_STREAM_WOULD_BLOCK) { + rv = ResumeRecv(); + } else { + rv = mSocketInCondition; + } + again = false; + } + } + // read more from the socket until error... + } while (again && gHttpHandler->Active()); + + return rv; +} + +void nsHttpConnection::SetupSecondaryTLS( + nsAHttpTransaction* aSpdyConnectTransaction) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + MOZ_ASSERT(!mTLSFilter); + LOG( + ("nsHttpConnection %p SetupSecondaryTLS %s %d " + "aSpdyConnectTransaction=%p\n", + this, mConnInfo->Origin(), mConnInfo->OriginPort(), + aSpdyConnectTransaction)); + + nsHttpConnectionInfo* ci = nullptr; + if (mTransaction) { + ci = mTransaction->ConnectionInfo(); + } + if (!ci) { + ci = mConnInfo; + } + MOZ_ASSERT(ci); + + mTLSFilter = new TLSFilterTransaction(mTransaction, ci->Origin(), + ci->OriginPort(), this, this); + + if (mTransaction) { + mTransaction = mTLSFilter; + } + mWeakTrans = do_GetWeakReference(aSpdyConnectTransaction); +} + +void nsHttpConnection::SetInSpdyTunnel(bool arg) { + MOZ_ASSERT(mTLSFilter); + mInSpdyTunnel = arg; + + // don't setup another tunnel :) + mProxyConnectStream = nullptr; + mCompletedProxyConnect = true; + mProxyConnectInProgress = false; +} + +// static +nsresult nsHttpConnection::MakeConnectString(nsAHttpTransaction* trans, + nsHttpRequestHead* request, + nsACString& result, bool h2ws) { + result.Truncate(); + if (!trans->ConnectionInfo()) { + return NS_ERROR_NOT_INITIALIZED; + } + + DebugOnly rv; + + rv = nsHttpHandler::GenerateHostPort( + nsDependentCString(trans->ConnectionInfo()->Origin()), + trans->ConnectionInfo()->OriginPort(), result); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + // CONNECT host:port HTTP/1.1 + request->SetMethod("CONNECT"_ns); + request->SetVersion(gHttpHandler->HttpVersion()); + if (h2ws) { + // HTTP/2 websocket CONNECT forms need the full request URI + nsAutoCString requestURI; + trans->RequestHead()->RequestURI(requestURI); + request->SetRequestURI(requestURI); + + request->SetHTTPS(trans->RequestHead()->IsHTTPS()); + } else { + request->SetRequestURI(result); + } + rv = request->SetHeader(nsHttp::User_Agent, gHttpHandler->UserAgent()); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + // a CONNECT is always persistent + rv = request->SetHeader(nsHttp::Proxy_Connection, "keep-alive"_ns); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + rv = request->SetHeader(nsHttp::Connection, "keep-alive"_ns); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + // all HTTP/1.1 requests must include a Host header (even though it + // may seem redundant in this case; see bug 82388). + rv = request->SetHeader(nsHttp::Host, result); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + nsAutoCString val; + if (NS_SUCCEEDED( + trans->RequestHead()->GetHeader(nsHttp::Proxy_Authorization, val))) { + // we don't know for sure if this authorization is intended for the + // SSL proxy, so we add it just in case. + rv = request->SetHeader(nsHttp::Proxy_Authorization, val); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + + if ((trans->Caps() & NS_HTTP_CONNECT_ONLY) && + NS_SUCCEEDED(trans->RequestHead()->GetHeader(nsHttp::Upgrade, val))) { + // rfc7639 proposes using the ALPN header to indicate the protocol used + // in CONNECT when not used for TLS. The protocol is stored in Upgrade. + // We have to copy this header here since a new HEAD request is created + // for the CONNECT. + rv = request->SetHeader("ALPN"_ns, val); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + + result.Truncate(); + request->Flatten(result, false); + + if (LOG1_ENABLED()) { + LOG(("nsHttpConnection::MakeConnectString for transaction=%p [", + trans->QueryHttpTransaction())); + LogHeaders(result.BeginReading()); + LOG(("]")); + } + + result.AppendLiteral("\r\n"); + return NS_OK; +} + +nsresult nsHttpConnection::SetupProxyConnect() { + LOG(("nsHttpConnection::SetupProxyConnect [this=%p]\n", this)); + NS_ENSURE_TRUE(!mProxyConnectStream, NS_ERROR_ALREADY_INITIALIZED); + MOZ_ASSERT(mUsingSpdyVersion == SpdyVersion::NONE, + "SPDY NPN Complete while using proxy connect stream"); + + nsAutoCString buf; + nsHttpRequestHead request; + nsresult rv = MakeConnectString(mTransaction, &request, buf, false); + if (NS_FAILED(rv)) { + return rv; + } + return NS_NewCStringInputStream(getter_AddRefs(mProxyConnectStream), + std::move(buf)); +} + +nsresult nsHttpConnection::StartShortLivedTCPKeepalives() { + if (mUsingSpdyVersion != SpdyVersion::NONE) { + return NS_OK; + } + MOZ_ASSERT(mSocketTransport); + if (!mSocketTransport) { + return NS_ERROR_NOT_INITIALIZED; + } + + nsresult rv = NS_OK; + int32_t idleTimeS = -1; + int32_t retryIntervalS = -1; + if (gHttpHandler->TCPKeepaliveEnabledForShortLivedConns()) { + // Set the idle time. + idleTimeS = gHttpHandler->GetTCPKeepaliveShortLivedIdleTime(); + LOG( + ("nsHttpConnection::StartShortLivedTCPKeepalives[%p] " + "idle time[%ds].", + this, idleTimeS)); + + retryIntervalS = std::max((int32_t)PR_IntervalToSeconds(mRtt), 1); + rv = mSocketTransport->SetKeepaliveVals(idleTimeS, retryIntervalS); + if (NS_FAILED(rv)) { + return rv; + } + rv = mSocketTransport->SetKeepaliveEnabled(true); + mTCPKeepaliveConfig = kTCPKeepaliveShortLivedConfig; + } else { + rv = mSocketTransport->SetKeepaliveEnabled(false); + mTCPKeepaliveConfig = kTCPKeepaliveDisabled; + } + if (NS_FAILED(rv)) { + return rv; + } + + // Start a timer to move to long-lived keepalive config. + if (!mTCPKeepaliveTransitionTimer) { + mTCPKeepaliveTransitionTimer = NS_NewTimer(); + } + + if (mTCPKeepaliveTransitionTimer) { + int32_t time = gHttpHandler->GetTCPKeepaliveShortLivedTime(); + + // Adjust |time| to ensure a full set of keepalive probes can be sent + // at the end of the short-lived phase. + if (gHttpHandler->TCPKeepaliveEnabledForShortLivedConns()) { + if (NS_WARN_IF(!gSocketTransportService)) { + return NS_ERROR_NOT_INITIALIZED; + } + int32_t probeCount = -1; + rv = gSocketTransportService->GetKeepaliveProbeCount(&probeCount); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + if (NS_WARN_IF(probeCount <= 0)) { + return NS_ERROR_UNEXPECTED; + } + // Add time for final keepalive probes, and 2 seconds for a buffer. + time += ((probeCount)*retryIntervalS) - (time % idleTimeS) + 2; + } + mTCPKeepaliveTransitionTimer->InitWithNamedFuncCallback( + nsHttpConnection::UpdateTCPKeepalive, this, (uint32_t)time * 1000, + nsITimer::TYPE_ONE_SHOT, + "net::nsHttpConnection::StartShortLivedTCPKeepalives"); + } else { + NS_WARNING( + "nsHttpConnection::StartShortLivedTCPKeepalives failed to " + "create timer."); + } + + return NS_OK; +} + +nsresult nsHttpConnection::StartLongLivedTCPKeepalives() { + MOZ_ASSERT(mUsingSpdyVersion == SpdyVersion::NONE, + "Don't use TCP Keepalive with SPDY!"); + if (NS_WARN_IF(mUsingSpdyVersion != SpdyVersion::NONE)) { + return NS_OK; + } + MOZ_ASSERT(mSocketTransport); + if (!mSocketTransport) { + return NS_ERROR_NOT_INITIALIZED; + } + + nsresult rv = NS_OK; + if (gHttpHandler->TCPKeepaliveEnabledForLongLivedConns()) { + // Increase the idle time. + int32_t idleTimeS = gHttpHandler->GetTCPKeepaliveLongLivedIdleTime(); + LOG(("nsHttpConnection::StartLongLivedTCPKeepalives[%p] idle time[%ds]", + this, idleTimeS)); + + int32_t retryIntervalS = + std::max((int32_t)PR_IntervalToSeconds(mRtt), 1); + rv = mSocketTransport->SetKeepaliveVals(idleTimeS, retryIntervalS); + if (NS_FAILED(rv)) { + return rv; + } + + // Ensure keepalive is enabled, if current status is disabled. + if (mTCPKeepaliveConfig == kTCPKeepaliveDisabled) { + rv = mSocketTransport->SetKeepaliveEnabled(true); + if (NS_FAILED(rv)) { + return rv; + } + } + mTCPKeepaliveConfig = kTCPKeepaliveLongLivedConfig; + } else { + rv = mSocketTransport->SetKeepaliveEnabled(false); + mTCPKeepaliveConfig = kTCPKeepaliveDisabled; + } + + if (NS_FAILED(rv)) { + return rv; + } + return NS_OK; +} + +nsresult nsHttpConnection::DisableTCPKeepalives() { + MOZ_ASSERT(mSocketTransport); + if (!mSocketTransport) { + return NS_ERROR_NOT_INITIALIZED; + } + + LOG(("nsHttpConnection::DisableTCPKeepalives [%p]", this)); + if (mTCPKeepaliveConfig != kTCPKeepaliveDisabled) { + nsresult rv = mSocketTransport->SetKeepaliveEnabled(false); + if (NS_FAILED(rv)) { + return rv; + } + mTCPKeepaliveConfig = kTCPKeepaliveDisabled; + } + if (mTCPKeepaliveTransitionTimer) { + mTCPKeepaliveTransitionTimer->Cancel(); + mTCPKeepaliveTransitionTimer = nullptr; + } + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsHttpConnection::nsISupports +//----------------------------------------------------------------------------- + +NS_IMPL_ADDREF(nsHttpConnection) +NS_IMPL_RELEASE(nsHttpConnection) + +NS_INTERFACE_MAP_BEGIN(nsHttpConnection) + NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference) + NS_INTERFACE_MAP_ENTRY(nsIInputStreamCallback) + NS_INTERFACE_MAP_ENTRY(nsIOutputStreamCallback) + NS_INTERFACE_MAP_ENTRY(nsITransportEventSink) + NS_INTERFACE_MAP_ENTRY(nsIInterfaceRequestor) + NS_INTERFACE_MAP_ENTRY(HttpConnectionBase) + NS_INTERFACE_MAP_ENTRY_CONCRETE(nsHttpConnection) +NS_INTERFACE_MAP_END + +//----------------------------------------------------------------------------- +// nsHttpConnection::nsIInputStreamCallback +//----------------------------------------------------------------------------- + +// called on the socket transport thread +NS_IMETHODIMP +nsHttpConnection::OnInputStreamReady(nsIAsyncInputStream* in) { + MOZ_ASSERT(in == mSocketIn, "unexpected stream"); + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + if (mIdleMonitoring) { + MOZ_ASSERT(!mTransaction, "Idle Input Event While Active"); + + // The only read event that is protocol compliant for an idle connection + // is an EOF, which we check for with CanReuse(). If the data is + // something else then just ignore it and suspend checking for EOF - + // our normal timers or protocol stack are the place to deal with + // any exception logic. + + if (!CanReuse()) { + LOG(("Server initiated close of idle conn %p\n", this)); + Unused << gHttpHandler->ConnMgr()->CloseIdleConnection(this); + return NS_OK; + } + + LOG(("Input data on idle conn %p, but not closing yet\n", this)); + return NS_OK; + } + + // if the transaction was dropped... + if (!mTransaction) { + LOG((" no transaction; ignoring event\n")); + return NS_OK; + } + + nsresult rv = OnSocketReadable(); + if (NS_FAILED(rv)) CloseTransaction(mTransaction, rv); + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsHttpConnection::nsIOutputStreamCallback +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsHttpConnection::OnOutputStreamReady(nsIAsyncOutputStream* out) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + MOZ_ASSERT(out == mSocketOut, "unexpected socket"); + // if the transaction was dropped... + if (!mTransaction) { + LOG((" no transaction; ignoring event\n")); + return NS_OK; + } + + nsresult rv = OnSocketWritable(); + if (NS_FAILED(rv)) CloseTransaction(mTransaction, rv); + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsHttpConnection::nsITransportEventSink +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsHttpConnection::OnTransportStatus(nsITransport* trans, nsresult status, + int64_t progress, int64_t progressMax) { + if (mTransaction) mTransaction->OnTransportStatus(trans, status, progress); + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsHttpConnection::nsIInterfaceRequestor +//----------------------------------------------------------------------------- + +// not called on the socket transport thread +NS_IMETHODIMP +nsHttpConnection::GetInterface(const nsIID& iid, void** result) { + // NOTE: This function is only called on the UI thread via sync proxy from + // the socket transport thread. If that weren't the case, then we'd + // have to worry about the possibility of mTransaction going away + // part-way through this function call. See CloseTransaction. + + // NOTE - there is a bug here, the call to getinterface is proxied off the + // nss thread, not the ui thread as the above comment says. So there is + // indeed a chance of mTransaction going away. bug 615342 + + MOZ_ASSERT(!OnSocketThread(), "on socket thread"); + + nsCOMPtr callbacks; + { + MutexAutoLock lock(mCallbacksLock); + callbacks = mCallbacks; + } + if (callbacks) return callbacks->GetInterface(iid, result); + return NS_ERROR_NO_INTERFACE; +} + +void nsHttpConnection::CheckForTraffic(bool check) { + if (check) { + LOG((" CheckForTraffic conn %p\n", this)); + if (mSpdySession) { + if (PR_IntervalToMilliseconds(IdleTime()) >= 500) { + // Send a ping to verify it is still alive if it has been idle + // more than half a second, the network changed events are + // rate-limited to one per 1000 ms. + LOG((" SendPing\n")); + mSpdySession->SendPing(); + } else { + LOG((" SendPing skipped due to network activity\n")); + } + } else { + // If not SPDY, Store snapshot amount of data right now + mTrafficCount = mTotalBytesWritten + mTotalBytesRead; + mTrafficStamp = true; + } + } else { + // mark it as not checked + mTrafficStamp = false; + } +} + +nsAHttpTransaction* +nsHttpConnection::CloseConnectionFastOpenTakesTooLongOrError( + bool aCloseSocketTransport) { + MOZ_ASSERT(!mCurrentBytesRead); + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + mFastOpenStatus = TFO_FAILED; + RefPtr trans; + + DontReuse(); + + if (mUsingSpdyVersion != SpdyVersion::NONE) { + // If we have a http2 connection just restart it as if 0rtt failed. + // For http2 we do not need to do similar thing as for http1 because + // backup connection will pick immediately all this transaction anyway. + mUsingSpdyVersion = SpdyVersion::NONE; + if (mSpdySession) { + mTransaction->SetFastOpenStatus(TFO_FAILED); + Unused << mSpdySession->Finish0RTT(true, true); + } + mSpdySession = nullptr; + } else { + // For http1 we want to make this transaction an absolute priority to + // get the backup connection so we will return it from here. + if (NS_SUCCEEDED(mTransaction->RestartOnFastOpenError())) { + trans = mTransaction; + } + mTransaction->SetConnection(nullptr); + } + + { + MutexAutoLock lock(mCallbacksLock); + mCallbacks = nullptr; + } + + if (mSocketIn) { + mSocketIn->AsyncWait(nullptr, 0, 0, nullptr); + } + + mTransaction = nullptr; + if (!aCloseSocketTransport) { + if (mSocketOut) { + mSocketOut->AsyncWait(nullptr, 0, 0, nullptr); + } + mSocketTransport->SetEventSink(nullptr, nullptr); + mSocketTransport->SetSecurityCallbacks(nullptr); + mSocketTransport = nullptr; + } + Close(NS_ERROR_NET_RESET); + return trans; +} + +void nsHttpConnection::SetFastOpen(bool aFastOpen) { + mFastOpen = aFastOpen; + if (!mFastOpen && mTransaction && !mTransaction->IsNullTransaction()) { + mExperienced = true; + + nsHttpTransaction* hTrans = mTransaction->QueryHttpTransaction(); + if (hTrans) { + SetUrgentStartPreferred(hTrans->ClassOfService() & + nsIClassOfService::UrgentStart); + } + } +} + +void nsHttpConnection::SetFastOpenStatus(uint8_t tfoStatus) { + mFastOpenStatus = tfoStatus; + if ((mFastOpenStatus >= TFO_FAILED_CONNECTION_REFUSED) && + (mFastOpenStatus <= + TFO_FAILED_BACKUP_CONNECTION_TFO_DATA_COOKIE_NOT_ACCEPTED) && + mSocketTransport) { + nsresult firstRetryError; + if (NS_SUCCEEDED(mSocketTransport->GetFirstRetryError(&firstRetryError)) && + (NS_FAILED(firstRetryError))) { + if ((mFastOpenStatus >= TFO_FAILED_BACKUP_CONNECTION_TFO_NOT_TRIED) && + (mFastOpenStatus <= + TFO_FAILED_BACKUP_CONNECTION_TFO_DATA_COOKIE_NOT_ACCEPTED)) { + mFastOpenStatus = TFO_FAILED_BACKUP_CONNECTION_NO_TFO_FAILED_TOO; + } else { + // We add +7 to tranform TFO_FAILED_CONNECTION_REFUSED into + // TFO_FAILED_CONNECTION_REFUSED_NO_TFO_FAILED_TOO, etc. + // If the list in TCPFastOpenLayer.h changes please addapt +7. + mFastOpenStatus = tfoStatus + 7; + } + } + } +} + +void nsHttpConnection::SetEvent(nsresult aStatus) { + switch (aStatus) { + case NS_NET_STATUS_RESOLVING_HOST: + mBootstrappedTimings.domainLookupStart = TimeStamp::Now(); + break; + case NS_NET_STATUS_RESOLVED_HOST: + mBootstrappedTimings.domainLookupEnd = TimeStamp::Now(); + break; + case NS_NET_STATUS_CONNECTING_TO: + mBootstrappedTimings.connectStart = TimeStamp::Now(); + break; + case NS_NET_STATUS_CONNECTED_TO: { + TimeStamp tnow = TimeStamp::Now(); + mBootstrappedTimings.tcpConnectEnd = tnow; + mBootstrappedTimings.connectEnd = tnow; + if ((mFastOpenStatus != TFO_DATA_SENT) && + !mBootstrappedTimings.secureConnectionStart.IsNull()) { + mBootstrappedTimings.secureConnectionStart = tnow; + } + break; + } + case NS_NET_STATUS_TLS_HANDSHAKE_STARTING: + mBootstrappedTimings.secureConnectionStart = TimeStamp::Now(); + break; + case NS_NET_STATUS_TLS_HANDSHAKE_ENDED: + mBootstrappedTimings.connectEnd = TimeStamp::Now(); + break; + default: + break; + } +} + +bool nsHttpConnection::NoClientCertAuth() const { + if (!mSocketTransport) { + return false; + } + + nsCOMPtr secInfo; + mSocketTransport->GetSecurityInfo(getter_AddRefs(secInfo)); + if (!secInfo) { + return false; + } + + nsCOMPtr ssc(do_QueryInterface(secInfo)); + if (!ssc) { + return false; + } + + return !ssc->GetClientCertSent(); +} + +bool nsHttpConnection::CanAcceptWebsocket() { + if (!UsingSpdy()) { + return true; + } + + return mSpdySession->CanAcceptWebsocket(); +} + +bool nsHttpConnection::IsProxyConnectInProgress() { + return mProxyConnectInProgress; +} + +bool nsHttpConnection::LastTransactionExpectedNoContent() { + return mLastTransactionExpectedNoContent; +} + +void nsHttpConnection::SetLastTransactionExpectedNoContent(bool val) { + mLastTransactionExpectedNoContent = val; +} + +bool nsHttpConnection::IsPersistent() { return IsKeepAlive() && !mDontReuse; } + +nsAHttpTransaction* nsHttpConnection::Transaction() { return mTransaction; } + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/nsHttpConnection.h b/netwerk/protocol/http/nsHttpConnection.h new file mode 100644 index 0000000000..7616daba62 --- /dev/null +++ b/netwerk/protocol/http/nsHttpConnection.h @@ -0,0 +1,359 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef nsHttpConnection_h__ +#define nsHttpConnection_h__ + +#include "HttpConnectionBase.h" +#include "nsHttpConnectionInfo.h" +#include "nsHttpResponseHead.h" +#include "nsAHttpTransaction.h" +#include "nsCOMPtr.h" +#include "nsProxyRelease.h" +#include "prinrval.h" +#include "TunnelUtils.h" +#include "mozilla/Mutex.h" +#include "ARefBase.h" +#include "TimingStruct.h" +#include "HttpTrafficAnalyzer.h" + +#include "nsIAsyncInputStream.h" +#include "nsIAsyncOutputStream.h" +#include "nsIInterfaceRequestor.h" +#include "nsITimer.h" + +class nsISocketTransport; +class nsISSLSocketControl; + +namespace mozilla { +namespace net { + +class nsHttpHandler; +class ASpdySession; + +// 1dcc863e-db90-4652-a1fe-13fea0b54e46 +#define NS_HTTPCONNECTION_IID \ + { \ + 0x1dcc863e, 0xdb90, 0x4652, { \ + 0xa1, 0xfe, 0x13, 0xfe, 0xa0, 0xb5, 0x4e, 0x46 \ + } \ + } + +//----------------------------------------------------------------------------- +// nsHttpConnection - represents a connection to a HTTP server (or proxy) +// +// NOTE: this objects lives on the socket thread only. it should not be +// accessed from any other thread. +//----------------------------------------------------------------------------- + +class nsHttpConnection final : public HttpConnectionBase, + public nsAHttpSegmentReader, + public nsAHttpSegmentWriter, + public nsIInputStreamCallback, + public nsIOutputStreamCallback, + public nsITransportEventSink, + public nsIInterfaceRequestor, + public NudgeTunnelCallback { + private: + virtual ~nsHttpConnection(); + + public: + NS_DECLARE_STATIC_IID_ACCESSOR(NS_HTTPCONNECTION_IID) + NS_DECL_HTTPCONNECTIONBASE + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSAHTTPSEGMENTREADER + NS_DECL_NSAHTTPSEGMENTWRITER + NS_DECL_NSIINPUTSTREAMCALLBACK + NS_DECL_NSIOUTPUTSTREAMCALLBACK + NS_DECL_NSITRANSPORTEVENTSINK + NS_DECL_NSIINTERFACEREQUESTOR + NS_DECL_NUDGETUNNELCALLBACK + + nsHttpConnection(); + + void SetFastOpen(bool aFastOpen); + // Close this connection and return the transaction. The transaction is + // restarted as well. This will only happened before connection is + // connected. + nsAHttpTransaction* CloseConnectionFastOpenTakesTooLongOrError( + bool aCloseocketTransport); + + //------------------------------------------------------------------------- + // XXX document when these are ok to call + + bool IsKeepAlive() { + return (mUsingSpdyVersion != SpdyVersion::NONE) || + (mKeepAliveMask && mKeepAlive); + } + + // Returns time in seconds for how long connection can be reused. + uint32_t TimeToLive(); + + bool NeedSpdyTunnel() { + return mConnInfo->UsingHttpsProxy() && !mTLSFilter && + mConnInfo->UsingConnect(); + } + + // A connection is forced into plaintext when it is intended to be used as a + // CONNECT tunnel but the setup fails. The plaintext only carries the CONNECT + // error. + void ForcePlainText() { mForcePlainText = true; } + + bool IsUrgentStartPreferred() const { + return mUrgentStartPreferredKnown && mUrgentStartPreferred; + } + void SetUrgentStartPreferred(bool urgent); + + void SetIsReusedAfter(uint32_t afterMilliseconds); + + int64_t MaxBytesRead() { return mMaxBytesRead; } + HttpVersion GetLastHttpResponseVersion() { return mLastHttpResponseVersion; } + + friend class HttpConnectionForceIO; + + [[nodiscard]] static nsresult ReadFromStream(nsIInputStream*, void*, + const char*, uint32_t, uint32_t, + uint32_t*); + + // When a persistent connection is in the connection manager idle + // connection pool, the nsHttpConnection still reads errors and hangups + // on the socket so that it can be proactively released if the server + // initiates a termination. Only call on socket thread. + void BeginIdleMonitoring(); + void EndIdleMonitoring(); + + bool UsingSpdy() override { return (mUsingSpdyVersion != SpdyVersion::NONE); } + SpdyVersion GetSpdyVersion() { return mUsingSpdyVersion; } + bool EverUsedSpdy() { return mEverUsedSpdy; } + bool UsingHttp3() override { return false; } + + // true when connection SSL NPN phase is complete and we know + // authoritatively whether UsingSpdy() or not. + bool ReportedNPN() { return mReportedSpdy; } + + // When the connection is active this is called up to once every 1 second + // return the interval (in seconds) that the connection next wants to + // have this invoked. It might happen sooner depending on the needs of + // other connections. + uint32_t ReadTimeoutTick(PRIntervalTime now); + + // For Active and Idle connections, this will be called when + // mTCPKeepaliveTransitionTimer fires, to check if the TCP keepalive config + // should move from short-lived (fast-detect) to long-lived. + static void UpdateTCPKeepalive(nsITimer* aTimer, void* aClosure); + + // When the connection is active this is called every second + void ReadTimeoutTick(); + + int64_t ContentBytesWritten() { return mContentBytesWritten; } + + [[nodiscard]] static nsresult MakeConnectString(nsAHttpTransaction* trans, + nsHttpRequestHead* request, + nsACString& result, + bool h2ws); + void SetupSecondaryTLS(nsAHttpTransaction* aSpdyConnectTransaction = nullptr); + void SetInSpdyTunnel(bool arg); + + // Check active connections for traffic (or not). SPDY connections send a + // ping, ordinary HTTP connections get some time to get traffic to be + // considered alive. + void CheckForTraffic(bool check); + + // NoTraffic() returns true if there's been no traffic on the (non-spdy) + // connection since CheckForTraffic() was called. + bool NoTraffic() { + return mTrafficStamp && + (mTrafficCount == (mTotalBytesWritten + mTotalBytesRead)) && + !mFastOpen; + } + + void SetFastOpenStatus(uint8_t tfoStatus); + uint8_t GetFastOpenStatus() { return mFastOpenStatus; } + + // Return true when the socket this connection is using has not been + // authenticated using a client certificate. Before SSL negotiation + // has finished this returns false. + bool NoClientCertAuth() const override; + + bool CanAcceptWebsocket() override; + + private: + // Value (set in mTCPKeepaliveConfig) indicates which set of prefs to use. + enum TCPKeepaliveConfig { + kTCPKeepaliveDisabled = 0, + kTCPKeepaliveShortLivedConfig, + kTCPKeepaliveLongLivedConfig + }; + + // called to cause the underlying socket to start speaking SSL + [[nodiscard]] nsresult InitSSLParams(bool connectingToProxy, + bool ProxyStartSSL); + [[nodiscard]] nsresult SetupNPNList(nsISSLSocketControl* ssl, uint32_t caps); + + [[nodiscard]] nsresult OnTransactionDone(nsresult reason); + [[nodiscard]] nsresult OnSocketWritable(); + [[nodiscard]] nsresult OnSocketReadable(); + + [[nodiscard]] nsresult SetupProxyConnect(); + + PRIntervalTime IdleTime(); + bool IsAlive(); + + // Makes certain the SSL handshake is complete and NPN negotiation + // has had a chance to happen + [[nodiscard]] bool EnsureNPNComplete(nsresult& aOut0RTTWriteHandshakeValue, + uint32_t& aOut0RTTBytesWritten); + + void SetupSSL(); + + // Start the Spdy transaction handler when NPN indicates spdy/* + void StartSpdy(nsISSLSocketControl* ssl, SpdyVersion versionLevel); + // Like the above, but do the bare minimum to do 0RTT data, so we can back + // it out, if necessary + void Start0RTTSpdy(SpdyVersion versionLevel); + + // Helpers for Start*Spdy + nsresult TryTakeSubTransactions(nsTArray >& list); + nsresult MoveTransactionsToSpdy(nsresult status, + nsTArray >& list); + + // Directly Add a transaction to an active connection for SPDY + [[nodiscard]] nsresult AddTransaction(nsAHttpTransaction*, int32_t); + + // Used to set TCP keepalives for fast detection of dead connections during + // an initial period, and slower detection for long-lived connections. + [[nodiscard]] nsresult StartShortLivedTCPKeepalives(); + [[nodiscard]] nsresult StartLongLivedTCPKeepalives(); + [[nodiscard]] nsresult DisableTCPKeepalives(); + + private: + // mTransaction only points to the HTTP Transaction callbacks if the + // transaction is open, otherwise it is null. + RefPtr mTransaction; + + nsCOMPtr mSocketIn; + nsCOMPtr mSocketOut; + + nsresult mSocketInCondition; + nsresult mSocketOutCondition; + + nsCOMPtr mProxyConnectStream; + nsCOMPtr mRequestStream; + + RefPtr mTLSFilter; + nsWeakPtr mWeakTrans; // SpdyConnectTransaction * + + RefPtr mHttpHandler; // keep gHttpHandler alive + + PRIntervalTime mLastReadTime; + PRIntervalTime mLastWriteTime; + PRIntervalTime + mMaxHangTime; // max download time before dropping keep-alive status + PRIntervalTime mIdleTimeout; // value of keep-alive: timeout= + PRIntervalTime mConsiderReusedAfterInterval; + PRIntervalTime mConsiderReusedAfterEpoch; + int64_t mCurrentBytesRead; // data read per activation + int64_t mMaxBytesRead; // max read in 1 activation + int64_t mTotalBytesRead; // total data read + int64_t mContentBytesWritten; // does not include CONNECT tunnel or TLS + + RefPtr mInputOverflow; + + // Whether the first non-null transaction dispatched on this connection was + // urgent-start or not + bool mUrgentStartPreferred; + // A flag to prevent reset of mUrgentStartPreferred by subsequent transactions + bool mUrgentStartPreferredKnown; + bool mConnectedTransport; + bool mKeepAlive; + bool mKeepAliveMask; + bool mDontReuse; + bool mIsReused; + bool mCompletedProxyConnect; + bool mLastTransactionExpectedNoContent; + bool mIdleMonitoring; + bool mProxyConnectInProgress; + bool mInSpdyTunnel; + bool mForcePlainText; + + // A snapshot of current number of transfered bytes + int64_t mTrafficCount; + bool mTrafficStamp; // true then the above is set + + // The number of <= HTTP/1.1 transactions performed on this connection. This + // excludes spdy transactions. + uint32_t mHttp1xTransactionCount; + + // Keep-Alive: max="mRemainingConnectionUses" provides the number of future + // transactions (including the current one) that the server expects to allow + // on this persistent connection. + uint32_t mRemainingConnectionUses; + + // SPDY related + bool mNPNComplete; + bool mSetupSSLCalled; + + // version level in use, 0 if unused + SpdyVersion mUsingSpdyVersion; + + RefPtr mSpdySession; + int32_t mPriority; + bool mReportedSpdy; + + // mUsingSpdyVersion is cleared when mSpdySession is freed, this is permanent + bool mEverUsedSpdy; + + // mLastHttpResponseVersion stores the last response's http version seen. + HttpVersion mLastHttpResponseVersion; + + // If a large keepalive has been requested for any trans, + // scale the default by this factor + uint32_t mDefaultTimeoutFactor; + + bool mResponseTimeoutEnabled; + + // Flag to indicate connection is in inital keepalive period (fast detect). + uint32_t mTCPKeepaliveConfig; + nsCOMPtr mTCPKeepaliveTransitionTimer; + + private: + // For ForceSend() + static void ForceSendIO(nsITimer* aTimer, void* aClosure); + [[nodiscard]] nsresult MaybeForceSendIO(); + bool mForceSendPending; + nsCOMPtr mForceSendTimer; + + // Helper variable for 0RTT handshake; + bool m0RTTChecked; // Possible 0RTT has been + // checked. + bool mWaitingFor0RTTResponse; // We have are + // sending 0RTT + // data and we + // are waiting + // for the end of + // the handsake. + int64_t mContentBytesWritten0RTT; + bool mEarlyDataNegotiated; // Only used for telemetry + nsCString mEarlyNegotiatedALPN; + bool mDid0RTTSpdy; + + bool mFastOpen; + uint8_t mFastOpenStatus; + + bool mForceSendDuringFastOpenPending; + bool mReceivedSocketWouldBlockDuringFastOpen; + bool mCheckNetworkStallsWithTFO; + PRIntervalTime mLastRequestBytesSentTime; + + private: + bool mThroughCaptivePortal; +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(nsHttpConnection, NS_HTTPCONNECTION_IID) + +} // namespace net +} // namespace mozilla + +#endif // nsHttpConnection_h__ diff --git a/netwerk/protocol/http/nsHttpConnectionInfo.cpp b/netwerk/protocol/http/nsHttpConnectionInfo.cpp new file mode 100644 index 0000000000..e2c256580e --- /dev/null +++ b/netwerk/protocol/http/nsHttpConnectionInfo.cpp @@ -0,0 +1,580 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et 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/. */ + +// HttpLog.h should generally be included first +#include "HttpLog.h" + +// Log on level :5, instead of default :4. +#undef LOG +#define LOG(args) LOG5(args) +#undef LOG_ENABLED +#define LOG_ENABLED() LOG5_ENABLED() + +#include "nsHttpConnectionInfo.h" + +#include "mozilla/net/DNS.h" +#include "mozilla/net/NeckoChannelParams.h" +#include "nsComponentManagerUtils.h" +#include "nsICryptoHash.h" +#include "nsIDNSByTypeRecord.h" +#include "nsIProtocolProxyService.h" +#include "nsHttpHandler.h" +#include "nsNetCID.h" +#include "nsProxyInfo.h" +#include "prnetdb.h" + +static nsresult SHA256(const char* aPlainText, nsAutoCString& aResult) { + nsresult rv; + nsCOMPtr hasher = + do_CreateInstance(NS_CRYPTO_HASH_CONTRACTID, &rv); + if (NS_FAILED(rv)) { + LOG(("nsHttpDigestAuth: no crypto hash!\n")); + return rv; + } + rv = hasher->Init(nsICryptoHash::SHA256); + NS_ENSURE_SUCCESS(rv, rv); + rv = hasher->Update((unsigned char*)aPlainText, strlen(aPlainText)); + NS_ENSURE_SUCCESS(rv, rv); + return hasher->Finish(false, aResult); +} + +namespace mozilla { +namespace net { + +nsHttpConnectionInfo::nsHttpConnectionInfo( + const nsACString& originHost, int32_t originPort, + const nsACString& npnToken, const nsACString& username, + const nsACString& topWindowOrigin, nsProxyInfo* proxyInfo, + const OriginAttributes& originAttributes, bool endToEndSSL, bool isolated, + bool aIsHttp3) + : mRoutedPort(443), mIsolated(isolated), mLessThanTls13(false) { + Init(originHost, originPort, npnToken, username, topWindowOrigin, proxyInfo, + originAttributes, endToEndSSL, aIsHttp3); +} + +nsHttpConnectionInfo::nsHttpConnectionInfo( + const nsACString& originHost, int32_t originPort, + const nsACString& npnToken, const nsACString& username, + const nsACString& topWindowOrigin, nsProxyInfo* proxyInfo, + const OriginAttributes& originAttributes, bool endToEndSSL, bool aIsHttp3) + : nsHttpConnectionInfo(originHost, originPort, npnToken, username, + topWindowOrigin, proxyInfo, originAttributes, + endToEndSSL, false, aIsHttp3) {} + +nsHttpConnectionInfo::nsHttpConnectionInfo( + const nsACString& originHost, int32_t originPort, + const nsACString& npnToken, const nsACString& username, + const nsACString& topWindowOrigin, nsProxyInfo* proxyInfo, + const OriginAttributes& originAttributes, const nsACString& routedHost, + int32_t routedPort, bool isolated, bool aIsHttp3) + : mIsolated(isolated), mLessThanTls13(false) { + mEndToEndSSL = true; // so DefaultPort() works + mRoutedPort = routedPort == -1 ? DefaultPort() : routedPort; + + if (!originHost.Equals(routedHost) || (originPort != routedPort)) { + mRoutedHost = routedHost; + } + Init(originHost, originPort, npnToken, username, topWindowOrigin, proxyInfo, + originAttributes, true, aIsHttp3); +} + +nsHttpConnectionInfo::nsHttpConnectionInfo( + const nsACString& originHost, int32_t originPort, + const nsACString& npnToken, const nsACString& username, + const nsACString& topWindowOrigin, nsProxyInfo* proxyInfo, + const OriginAttributes& originAttributes, const nsACString& routedHost, + int32_t routedPort, bool aIsHttp3) + : nsHttpConnectionInfo(originHost, originPort, npnToken, username, + topWindowOrigin, proxyInfo, originAttributes, + routedHost, routedPort, false, aIsHttp3) {} + +void nsHttpConnectionInfo::Init(const nsACString& host, int32_t port, + const nsACString& npnToken, + const nsACString& username, + const nsACString& topWindowOrigin, + nsProxyInfo* proxyInfo, + const OriginAttributes& originAttributes, + bool e2eSSL, bool aIsHttp3) { + LOG(("Init nsHttpConnectionInfo @%p\n", this)); + + mUsername = username; + mTopWindowOrigin = topWindowOrigin; + mProxyInfo = proxyInfo; + mEndToEndSSL = e2eSSL; + mUsingConnect = false; + mNPNToken = npnToken; + mIsHttp3 = aIsHttp3; + mOriginAttributes = originAttributes; + mTlsFlags = 0x0; + mIsTrrServiceChannel = false; + mTRRMode = nsIRequest::TRR_DEFAULT_MODE; + mIPv4Disabled = false; + mIPv6Disabled = false; + mHasIPHintAddress = false; + + mUsingHttpsProxy = (proxyInfo && proxyInfo->IsHTTPS()); + mUsingHttpProxy = mUsingHttpsProxy || (proxyInfo && proxyInfo->IsHTTP()); + + if (mUsingHttpProxy) { + mUsingConnect = mEndToEndSSL; // SSL always uses CONNECT + uint32_t resolveFlags = 0; + if (NS_SUCCEEDED(mProxyInfo->GetResolveFlags(&resolveFlags)) && + resolveFlags & nsIProtocolProxyService::RESOLVE_ALWAYS_TUNNEL) { + mUsingConnect = true; + } + } + + SetOriginServer(host, port); +} + +void nsHttpConnectionInfo::BuildHashKey() { + // + // build hash key: + // + // the hash key uniquely identifies the connection type. two connections + // are "equal" if they end up talking the same protocol to the same server + // and are both used for anonymous or non-anonymous connection only; + // anonymity of the connection is setup later from nsHttpChannel::AsyncOpen + // where we know we use anonymous connection (LOAD_ANONYMOUS load flag) + // + + const char* keyHost; + int32_t keyPort; + + if (mUsingHttpProxy && !mUsingConnect) { + keyHost = ProxyHost(); + keyPort = ProxyPort(); + } else { + keyHost = Origin(); + keyPort = OriginPort(); + } + + // The hashkey has 4 fields followed by host connection info + // byte 0 is P/T/. {P,T} for Plaintext/TLS Proxy over HTTP + // byte 1 is S/. S is for end to end ssl such as https:// uris + // byte 2 is A/. A is for an anonymous channel (no cookies, etc..) + // byte 3 is P/. P is for a private browising channel + // byte 4 is I/. I is for insecure scheme on TLS for http:// uris + // byte 5 is X/. X is for disallow_spdy flag + // byte 6 is C/. C is for be Conservative + // byte 7 is i/. i is for isolated + // Note: when adding/removing fields from this list which do not have + // corresponding data fields on the object itself, you may also need to + // modify RebuildHashKey. + + mHashKey.AssignLiteral("........[tlsflags0x00000000]"); + if (mIsolated) { + mHashKey.SetCharAt('i', 7); + } + + mHashKey.Append(keyHost); + mHashKey.Append(':'); + mHashKey.AppendInt(keyPort); + if (!mUsername.IsEmpty()) { + mHashKey.Append('['); + mHashKey.Append(mUsername); + mHashKey.Append(']'); + } + + if (mUsingHttpsProxy) { + mHashKey.SetCharAt('T', 0); + } else if (mUsingHttpProxy) { + mHashKey.SetCharAt('P', 0); + } + if (mEndToEndSSL) { + mHashKey.SetCharAt('S', 1); + } + + // NOTE: for transparent proxies (e.g., SOCKS) we need to encode the proxy + // info in the hash key (this ensures that we will continue to speak the + // right protocol even if our proxy preferences change). + // + // NOTE: for SSL tunnels add the proxy information to the cache key. + // We cannot use the proxy as the host parameter (as we do for non SSL) + // because this is a single host tunnel, but we need to include the proxy + // information so that a change in proxy config will mean this connection + // is not reused + + // NOTE: Adding the username and the password provides a means to isolate + // keep-alive to the URL bar domain as well: If the username is the URL bar + // domain, keep-alive connections are not reused by resources bound to + // different URL bar domains as the respective hash keys are not matching. + + if ((!mUsingHttpProxy && ProxyHost()) || (mUsingHttpProxy && mUsingConnect)) { + mHashKey.AppendLiteral(" ("); + mHashKey.Append(ProxyType()); + mHashKey.Append(':'); + mHashKey.Append(ProxyHost()); + mHashKey.Append(':'); + mHashKey.AppendInt(ProxyPort()); + mHashKey.Append(')'); + mHashKey.Append('['); + mHashKey.Append(ProxyUsername()); + mHashKey.Append(':'); + const char* password = ProxyPassword(); + if (strlen(password) > 0) { + nsAutoCString digestedPassword; + nsresult rv = SHA256(password, digestedPassword); + if (rv == NS_OK) { + mHashKey.Append(digestedPassword); + } + } + mHashKey.Append(']'); + } + + if (!mRoutedHost.IsEmpty()) { + mHashKey.AppendLiteral(" '); + } + + if (!mNPNToken.IsEmpty()) { + mHashKey.AppendLiteral(" {NPN-TOKEN "); + mHashKey.Append(mNPNToken); + mHashKey.AppendLiteral("}"); + } + + if (GetTRRMode() != nsIRequest::TRR_DEFAULT_MODE) { + // When connecting with another TRR mode, we enforce a separate connection + // hashkey so that we also can trigger a fresh DNS resolver that then + // doesn't use TRR as the previous connection might have. + mHashKey.AppendLiteral("[TRR:"); + mHashKey.AppendInt(GetTRRMode()); + mHashKey.AppendLiteral("]"); + } + + if (GetIPv4Disabled()) { + mHashKey.AppendLiteral("[!v4]"); + } + + if (GetIPv6Disabled()) { + mHashKey.AppendLiteral("[!v6]"); + } + + if (mIsolated && !mTopWindowOrigin.IsEmpty()) { + mHashKey.Append('{'); + mHashKey.Append('{'); + mHashKey.Append(mTopWindowOrigin); + mHashKey.Append('}'); + mHashKey.Append('}'); + } + + if (mProxyInfo) { + const nsCString& connectionIsolationKey = + mProxyInfo->ConnectionIsolationKey(); + if (!connectionIsolationKey.IsEmpty()) { + mHashKey.AppendLiteral("{CIK "); + mHashKey.Append(connectionIsolationKey); + mHashKey.AppendLiteral("}"); + } + if (mProxyInfo->Flags() & nsIProxyInfo::TRANSPARENT_PROXY_RESOLVES_HOST) { + mHashKey.AppendLiteral("{TPRH}"); + } + } + + nsAutoCString originAttributes; + mOriginAttributes.CreateSuffix(originAttributes); + mHashKey.Append(originAttributes); +} + +void nsHttpConnectionInfo::RebuildHashKey() { + // Create copies of all properties stored in our hash key. + bool isAnonymous = GetAnonymous(); + bool isPrivate = GetPrivate(); + bool isInsecureScheme = GetInsecureScheme(); + bool isNoSpdy = GetNoSpdy(); + bool isBeConservative = GetBeConservative(); + + BuildHashKey(); + + // Restore all of those properties. + SetAnonymous(isAnonymous); + SetPrivate(isPrivate); + SetInsecureScheme(isInsecureScheme); + SetNoSpdy(isNoSpdy); + SetBeConservative(isBeConservative); +} + +void nsHttpConnectionInfo::SetOriginServer(const nsACString& host, + int32_t port) { + mOrigin = host; + mOriginPort = port == -1 ? DefaultPort() : port; + // Use BuildHashKey() since this can only be called when constructing an + // nsHttpConnectionInfo object. + MOZ_DIAGNOSTIC_ASSERT(mHashKey.IsEmpty()); + BuildHashKey(); +} + +// Note that this function needs to be synced with +// nsHttpConnectionInfo::DeserializeHttpConnectionInfoCloneArgs to make sure +// nsHttpConnectionInfo can be serialized/deserialized. +already_AddRefed nsHttpConnectionInfo::Clone() const { + RefPtr clone; + if (mRoutedHost.IsEmpty()) { + clone = new nsHttpConnectionInfo( + mOrigin, mOriginPort, mNPNToken, mUsername, mTopWindowOrigin, + mProxyInfo, mOriginAttributes, mEndToEndSSL, mIsolated, mIsHttp3); + } else { + MOZ_ASSERT(mEndToEndSSL); + clone = new nsHttpConnectionInfo(mOrigin, mOriginPort, mNPNToken, mUsername, + mTopWindowOrigin, mProxyInfo, + mOriginAttributes, mRoutedHost, + mRoutedPort, mIsolated, mIsHttp3); + } + + // Make sure the anonymous, insecure-scheme, and private flags are transferred + clone->SetAnonymous(GetAnonymous()); + clone->SetPrivate(GetPrivate()); + clone->SetInsecureScheme(GetInsecureScheme()); + clone->SetNoSpdy(GetNoSpdy()); + clone->SetBeConservative(GetBeConservative()); + clone->SetTlsFlags(GetTlsFlags()); + clone->SetIsTrrServiceChannel(GetIsTrrServiceChannel()); + clone->SetTRRMode(GetTRRMode()); + clone->SetIPv4Disabled(GetIPv4Disabled()); + clone->SetIPv6Disabled(GetIPv6Disabled()); + clone->SetHasIPHintAddress(HasIPHintAddress()); + clone->SetEchConfig(GetEchConfig()); + MOZ_ASSERT(clone->Equals(this)); + + return clone.forget(); +} + +already_AddRefed +nsHttpConnectionInfo::CloneAndAdoptHTTPSSVCRecord( + nsISVCBRecord* aRecord) const { + MOZ_ASSERT(aRecord); + + // Get the domain name of this HTTPS RR. This name will be assigned to + // mRoutedHost in the new connection info. + nsAutoCString name; + aRecord->GetName(name); + + // Try to get the port and Alpn. If this record has SvcParamKeyPort defined, + // the new port will be used as mRoutedPort. + Maybe port = aRecord->GetPort(); + Maybe> alpn = aRecord->GetAlpn(); + + // Let the new conn info learn h3 will be used. + bool isHttp3 = alpn ? Get<1>(*alpn) : false; + + LOG(("HTTPSSVC: use new routed host (%s) and new npnToken (%s)", name.get(), + alpn ? Get<0>(*alpn).get() : "None")); + + RefPtr clone; + if (name.IsEmpty()) { + clone = new nsHttpConnectionInfo( + mOrigin, mOriginPort, alpn ? Get<0>(*alpn) : EmptyCString(), mUsername, + mTopWindowOrigin, mProxyInfo, mOriginAttributes, mEndToEndSSL, + mIsolated, isHttp3); + } else { + MOZ_ASSERT(mEndToEndSSL); + clone = new nsHttpConnectionInfo( + mOrigin, mOriginPort, alpn ? Get<0>(*alpn) : EmptyCString(), mUsername, + mTopWindowOrigin, mProxyInfo, mOriginAttributes, name, + port ? *port : mOriginPort, mIsolated, isHttp3); + } + + // Make sure the anonymous, insecure-scheme, and private flags are transferred + clone->SetAnonymous(GetAnonymous()); + clone->SetPrivate(GetPrivate()); + clone->SetInsecureScheme(GetInsecureScheme()); + clone->SetNoSpdy(GetNoSpdy()); + clone->SetBeConservative(GetBeConservative()); + clone->SetTlsFlags(GetTlsFlags()); + clone->SetIsTrrServiceChannel(GetIsTrrServiceChannel()); + clone->SetTRRMode(GetTRRMode()); + clone->SetIPv4Disabled(GetIPv4Disabled()); + clone->SetIPv6Disabled(GetIPv6Disabled()); + + bool hasIPHint = false; + Unused << aRecord->GetHasIPHintAddress(&hasIPHint); + if (hasIPHint) { + clone->SetHasIPHintAddress(hasIPHint); + } + + nsAutoCString echConfig; + Unused << aRecord->GetEchConfig(echConfig); + clone->SetEchConfig(echConfig); + + return clone.forget(); +} + +/* static */ +void nsHttpConnectionInfo::SerializeHttpConnectionInfo( + nsHttpConnectionInfo* aInfo, HttpConnectionInfoCloneArgs& aArgs) { + aArgs.host() = aInfo->GetOrigin(); + aArgs.port() = aInfo->OriginPort(); + aArgs.npnToken() = aInfo->GetNPNToken(); + aArgs.username() = aInfo->GetUsername(); + aArgs.originAttributes() = aInfo->GetOriginAttributes(); + aArgs.endToEndSSL() = aInfo->EndToEndSSL(); + aArgs.routedHost() = aInfo->GetRoutedHost(); + aArgs.routedPort() = aInfo->RoutedPort(); + aArgs.anonymous() = aInfo->GetAnonymous(); + aArgs.aPrivate() = aInfo->GetPrivate(); + aArgs.insecureScheme() = aInfo->GetInsecureScheme(); + aArgs.noSpdy() = aInfo->GetNoSpdy(); + aArgs.beConservative() = aInfo->GetBeConservative(); + aArgs.tlsFlags() = aInfo->GetTlsFlags(); + aArgs.isolated() = aInfo->GetIsolated(); + aArgs.isTrrServiceChannel() = aInfo->GetTRRMode(); + aArgs.trrMode() = aInfo->GetTRRMode(); + aArgs.isIPv4Disabled() = aInfo->GetIPv4Disabled(); + aArgs.isIPv6Disabled() = aInfo->GetIPv6Disabled(); + aArgs.topWindowOrigin() = aInfo->GetTopWindowOrigin(); + aArgs.isHttp3() = aInfo->IsHttp3(); + aArgs.hasIPHintAddress() = aInfo->HasIPHintAddress(); + aArgs.echConfig() = aInfo->GetEchConfig(); + + if (!aInfo->ProxyInfo()) { + return; + } + + nsTArray proxyInfoArray; + nsProxyInfo::SerializeProxyInfo(aInfo->ProxyInfo(), proxyInfoArray); + aArgs.proxyInfo() = std::move(proxyInfoArray); +} + +// This function needs to be synced with nsHttpConnectionInfo::Clone. +/* static */ +already_AddRefed +nsHttpConnectionInfo::DeserializeHttpConnectionInfoCloneArgs( + const HttpConnectionInfoCloneArgs& aInfoArgs) { + nsProxyInfo* pi = nsProxyInfo::DeserializeProxyInfo(aInfoArgs.proxyInfo()); + RefPtr cinfo; + if (aInfoArgs.routedHost().IsEmpty()) { + cinfo = new nsHttpConnectionInfo( + aInfoArgs.host(), aInfoArgs.port(), aInfoArgs.npnToken(), + aInfoArgs.username(), aInfoArgs.topWindowOrigin(), pi, + aInfoArgs.originAttributes(), aInfoArgs.endToEndSSL(), + aInfoArgs.isolated(), aInfoArgs.isHttp3()); + } else { + MOZ_ASSERT(aInfoArgs.endToEndSSL()); + cinfo = new nsHttpConnectionInfo( + aInfoArgs.host(), aInfoArgs.port(), aInfoArgs.npnToken(), + aInfoArgs.username(), aInfoArgs.topWindowOrigin(), pi, + aInfoArgs.originAttributes(), aInfoArgs.routedHost(), + aInfoArgs.routedPort(), aInfoArgs.isolated(), aInfoArgs.isHttp3()); + } + + // Make sure the anonymous, insecure-scheme, and private flags are transferred + cinfo->SetAnonymous(aInfoArgs.anonymous()); + cinfo->SetPrivate(aInfoArgs.aPrivate()); + cinfo->SetInsecureScheme(aInfoArgs.insecureScheme()); + cinfo->SetNoSpdy(aInfoArgs.noSpdy()); + cinfo->SetBeConservative(aInfoArgs.beConservative()); + cinfo->SetTlsFlags(aInfoArgs.tlsFlags()); + cinfo->SetIsTrrServiceChannel(aInfoArgs.isTrrServiceChannel()); + cinfo->SetTRRMode(static_cast(aInfoArgs.trrMode())); + cinfo->SetIPv4Disabled(aInfoArgs.isIPv4Disabled()); + cinfo->SetIPv6Disabled(aInfoArgs.isIPv6Disabled()); + cinfo->SetHasIPHintAddress(aInfoArgs.hasIPHintAddress()); + cinfo->SetEchConfig(aInfoArgs.echConfig()); + + return cinfo.forget(); +} + +void nsHttpConnectionInfo::CloneAsDirectRoute(nsHttpConnectionInfo** outCI) { + if (mRoutedHost.IsEmpty()) { + RefPtr clone = Clone(); + // Explicitly set mIsHttp3 to false, since CloneAsDirectRoute() is used to + // create a non-http3 connection info. + clone->mIsHttp3 = false; + clone.forget(outCI); + return; + } + + RefPtr clone = new nsHttpConnectionInfo( + mOrigin, mOriginPort, ""_ns, mUsername, mTopWindowOrigin, mProxyInfo, + mOriginAttributes, mEndToEndSSL, mIsolated); + // Make sure the anonymous, insecure-scheme, and private flags are transferred + clone->SetAnonymous(GetAnonymous()); + clone->SetPrivate(GetPrivate()); + clone->SetInsecureScheme(GetInsecureScheme()); + clone->SetNoSpdy(GetNoSpdy()); + clone->SetBeConservative(GetBeConservative()); + clone->SetTlsFlags(GetTlsFlags()); + clone->SetIsTrrServiceChannel(GetIsTrrServiceChannel()); + clone->SetTRRMode(GetTRRMode()); + clone->SetIPv4Disabled(GetIPv4Disabled()); + clone->SetIPv6Disabled(GetIPv6Disabled()); + clone->SetHasIPHintAddress(HasIPHintAddress()); + clone->SetEchConfig(GetEchConfig()); + + clone.forget(outCI); +} + +nsresult nsHttpConnectionInfo::CreateWildCard(nsHttpConnectionInfo** outParam) { + // T???mozilla.org:443 (https:proxy.ducksong.com:3128) [specifc form] + // TS??*:0 (https:proxy.ducksong.com:3128) [wildcard form] + + if (!mUsingHttpsProxy) { + MOZ_ASSERT(false); + return NS_ERROR_NOT_IMPLEMENTED; + } + + RefPtr clone; + clone = new nsHttpConnectionInfo("*"_ns, 0, mNPNToken, mUsername, + mTopWindowOrigin, mProxyInfo, + mOriginAttributes, true, mIsHttp3); + // Make sure the anonymous and private flags are transferred! + clone->SetAnonymous(GetAnonymous()); + clone->SetPrivate(GetPrivate()); + clone.forget(outParam); + return NS_OK; +} + +void nsHttpConnectionInfo::SetTRRMode(nsIRequest::TRRMode aTRRMode) { + if (mTRRMode != aTRRMode) { + mTRRMode = aTRRMode; + RebuildHashKey(); + } +} + +void nsHttpConnectionInfo::SetIPv4Disabled(bool aNoIPv4) { + if (mIPv4Disabled != aNoIPv4) { + mIPv4Disabled = aNoIPv4; + RebuildHashKey(); + } +} + +void nsHttpConnectionInfo::SetIPv6Disabled(bool aNoIPv6) { + if (mIPv6Disabled != aNoIPv6) { + mIPv6Disabled = aNoIPv6; + RebuildHashKey(); + } +} + +void nsHttpConnectionInfo::SetTlsFlags(uint32_t aTlsFlags) { + mTlsFlags = aTlsFlags; + + mHashKey.Replace(19, 8, nsPrintfCString("%08x", mTlsFlags)); +} + +bool nsHttpConnectionInfo::UsingProxy() { + if (!mProxyInfo) return false; + return !mProxyInfo->IsDirect(); +} + +bool nsHttpConnectionInfo::HostIsLocalIPLiteral() const { + PRNetAddr prAddr; + // If the host/proxy host is not an IP address literal, return false. + if (ProxyHost()) { + if (PR_StringToNetAddr(ProxyHost(), &prAddr) != PR_SUCCESS) { + return false; + } + } else if (PR_StringToNetAddr(Origin(), &prAddr) != PR_SUCCESS) { + return false; + } + NetAddr netAddr(&prAddr); + return netAddr.IsIPAddrLocal(); +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/nsHttpConnectionInfo.h b/netwerk/protocol/http/nsHttpConnectionInfo.h new file mode 100644 index 0000000000..c1b22f48eb --- /dev/null +++ b/netwerk/protocol/http/nsHttpConnectionInfo.h @@ -0,0 +1,280 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et 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/. */ + +#ifndef nsHttpConnectionInfo_h__ +#define nsHttpConnectionInfo_h__ + +#include "nsHttp.h" +#include "nsProxyInfo.h" +#include "nsCOMPtr.h" +#include "nsStringFwd.h" +#include "mozilla/Logging.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/AlreadyAddRefed.h" +#include "ARefBase.h" + +//----------------------------------------------------------------------------- +// nsHttpConnectionInfo - holds the properties of a connection +//----------------------------------------------------------------------------- + +// http:// uris through a proxy will all share the same CI, because they can +// all use the same connection. (modulo pb and anonymous flags). They just use +// the proxy as the origin host name. +// however, https:// uris tunnel through the proxy so they will have different +// CIs - the CI reflects both the proxy and the origin. +// however, proxy conenctions made with http/2 (or spdy) can tunnel to the +// origin and multiplex non tunneled transactions at the same time, so they have +// a special wildcard CI that accepts all origins through that proxy. + +class nsISVCBRecord; + +namespace mozilla { +namespace net { + +extern LazyLogModule gHttpLog; +class HttpConnectionInfoCloneArgs; +class nsHttpTransaction; + +class nsHttpConnectionInfo final : public ARefBase { + public: + nsHttpConnectionInfo(const nsACString& originHost, int32_t originPort, + const nsACString& npnToken, const nsACString& username, + const nsACString& topWindowOrigin, + nsProxyInfo* proxyInfo, + const OriginAttributes& originAttributes, + bool endToEndSSL = false, bool aIsHttp3 = false); + + // this version must use TLS and you may supply separate + // connection (aka routing) information than the authenticated + // origin information + nsHttpConnectionInfo(const nsACString& originHost, int32_t originPort, + const nsACString& npnToken, const nsACString& username, + const nsACString& topWindowOrigin, + nsProxyInfo* proxyInfo, + const OriginAttributes& originAttributes, + const nsACString& routedHost, int32_t routedPort, + bool aIsHttp3); + + static void SerializeHttpConnectionInfo(nsHttpConnectionInfo* aInfo, + HttpConnectionInfoCloneArgs& aArgs); + static already_AddRefed + DeserializeHttpConnectionInfoCloneArgs( + const HttpConnectionInfoCloneArgs& aInfoArgs); + + private: + virtual ~nsHttpConnectionInfo() { + MOZ_LOG(gHttpLog, LogLevel::Debug, + ("Destroying nsHttpConnectionInfo @%p\n", this)); + } + + void BuildHashKey(); + void RebuildHashKey(); + + public: + const nsCString& HashKey() const { return mHashKey; } + + const nsCString& GetOrigin() const { return mOrigin; } + const char* Origin() const { return mOrigin.get(); } + int32_t OriginPort() const { return mOriginPort; } + + const nsCString& GetRoutedHost() const { return mRoutedHost; } + const char* RoutedHost() const { return mRoutedHost.get(); } + int32_t RoutedPort() const { return mRoutedPort; } + + // OK to treat these as an infalible allocation + already_AddRefed Clone() const; + // This main prupose of this function is to clone this connection info, but + // replace mRoutedHost with SvcDomainName in the given SVCB record. Note that + // if SvcParamKeyPort and SvcParamKeyAlpn are presented in the SVCB record, + // mRoutedPort and mNPNToken will be replaced as well. + already_AddRefed CloneAndAdoptHTTPSSVCRecord( + nsISVCBRecord* aRecord) const; + void CloneAsDirectRoute(nsHttpConnectionInfo** outParam); + [[nodiscard]] nsresult CreateWildCard(nsHttpConnectionInfo** outParam); + + const char* ProxyHost() const { + return mProxyInfo ? mProxyInfo->Host().get() : nullptr; + } + int32_t ProxyPort() const { return mProxyInfo ? mProxyInfo->Port() : -1; } + const char* ProxyType() const { + return mProxyInfo ? mProxyInfo->Type() : nullptr; + } + const char* ProxyUsername() const { + return mProxyInfo ? mProxyInfo->Username().get() : nullptr; + } + const char* ProxyPassword() const { + return mProxyInfo ? mProxyInfo->Password().get() : nullptr; + } + + const nsCString& ProxyAuthorizationHeader() const { + return mProxyInfo ? mProxyInfo->ProxyAuthorizationHeader() : EmptyCString(); + } + const nsCString& ConnectionIsolationKey() const { + return mProxyInfo ? mProxyInfo->ConnectionIsolationKey() : EmptyCString(); + } + + // Compare this connection info to another... + // Two connections are 'equal' if they end up talking the same + // protocol to the same server. This is needed to properly manage + // persistent connections to proxies + // Note that we don't care about transparent proxies - + // it doesn't matter if we're talking via socks or not, since + // a request will end up at the same host. + bool Equals(const nsHttpConnectionInfo* info) { + return mHashKey.Equals(info->HashKey()); + } + + const char* Username() const { return mUsername.get(); } + nsProxyInfo* ProxyInfo() const { return mProxyInfo; } + int32_t DefaultPort() const { + return mEndToEndSSL ? NS_HTTPS_DEFAULT_PORT : NS_HTTP_DEFAULT_PORT; + } + void SetAnonymous(bool anon) { mHashKey.SetCharAt(anon ? 'A' : '.', 2); } + bool GetAnonymous() const { return mHashKey.CharAt(2) == 'A'; } + void SetPrivate(bool priv) { mHashKey.SetCharAt(priv ? 'P' : '.', 3); } + bool GetPrivate() const { return mHashKey.CharAt(3) == 'P'; } + void SetInsecureScheme(bool insecureScheme) { + mHashKey.SetCharAt(insecureScheme ? 'I' : '.', 4); + } + bool GetInsecureScheme() const { return mHashKey.CharAt(4) == 'I'; } + + void SetNoSpdy(bool aNoSpdy) { mHashKey.SetCharAt(aNoSpdy ? 'X' : '.', 5); } + bool GetNoSpdy() const { return mHashKey.CharAt(5) == 'X'; } + + void SetBeConservative(bool aBeConservative) { + mHashKey.SetCharAt(aBeConservative ? 'C' : '.', 6); + } + bool GetBeConservative() const { return mHashKey.CharAt(6) == 'C'; } + + void SetIsolated(bool aIsolated) { + mIsolated = aIsolated; + RebuildHashKey(); + } + bool GetIsolated() const { return mIsolated; } + + void SetTlsFlags(uint32_t aTlsFlags); + uint32_t GetTlsFlags() const { return mTlsFlags; } + + // IsTrrServiceChannel means that this connection is used to send TRR requests + // over + void SetIsTrrServiceChannel(bool aIsTRRChannel) { + mIsTrrServiceChannel = aIsTRRChannel; + } + bool GetIsTrrServiceChannel() const { return mIsTrrServiceChannel; } + + void SetTRRMode(nsIRequest::TRRMode aTRRMode); + nsIRequest::TRRMode GetTRRMode() const { return mTRRMode; } + + void SetIPv4Disabled(bool aNoIPv4); + bool GetIPv4Disabled() const { return mIPv4Disabled; } + + void SetIPv6Disabled(bool aNoIPv6); + bool GetIPv6Disabled() const { return mIPv6Disabled; } + + const nsCString& GetNPNToken() { return mNPNToken; } + const nsCString& GetUsername() { return mUsername; } + const nsCString& GetTopWindowOrigin() { return mTopWindowOrigin; } + + const OriginAttributes& GetOriginAttributes() { return mOriginAttributes; } + + // Returns true for any kind of proxy (http, socks, https, etc..) + bool UsingProxy(); + + // Returns true when proxying over HTTP or HTTPS + bool UsingHttpProxy() const { return mUsingHttpProxy || mUsingHttpsProxy; } + + // Returns true when proxying over HTTPS + bool UsingHttpsProxy() const { return mUsingHttpsProxy; } + + // Returns true when a resource is in SSL end to end (e.g. https:// uri) + bool EndToEndSSL() const { return mEndToEndSSL; } + + // Returns true when at least first hop is SSL (e.g. proxy over https or https + // uri) + bool FirstHopSSL() const { return mEndToEndSSL || mUsingHttpsProxy; } + + // Returns true when CONNECT is used to tunnel through the proxy (e.g. + // https:// or ws://) + bool UsingConnect() const { return mUsingConnect; } + + // Returns true when origin/proxy is an RFC1918 literal. + bool HostIsLocalIPLiteral() const; + + bool GetLessThanTls13() const { return mLessThanTls13; } + void SetLessThanTls13(bool aLessThanTls13) { + mLessThanTls13 = aLessThanTls13; + } + + bool IsHttp3() const { return mIsHttp3; } + + void SetHasIPHintAddress(bool aHasIPHint) { mHasIPHintAddress = aHasIPHint; } + bool HasIPHintAddress() const { return mHasIPHintAddress; } + + void SetEchConfig(const nsACString& aEchConfig) { mEchConfig = aEchConfig; } + const nsCString& GetEchConfig() const { return mEchConfig; } + + private: + // These constructor versions are intended to be used from Clone() and + // DeserializeHttpConnectionInfoCloneArgs(). + nsHttpConnectionInfo(const nsACString& originHost, int32_t originPort, + const nsACString& npnToken, const nsACString& username, + const nsACString& topWindowOrigin, + nsProxyInfo* proxyInfo, + const OriginAttributes& originAttributes, + bool endToEndSSL, bool isolated, bool aIsHttp3); + nsHttpConnectionInfo(const nsACString& originHost, int32_t originPort, + const nsACString& npnToken, const nsACString& username, + const nsACString& topWindowOrigin, + nsProxyInfo* proxyInfo, + const OriginAttributes& originAttributes, + const nsACString& routedHost, int32_t routedPort, + bool isolated, bool aIsHttp3); + + void Init(const nsACString& host, int32_t port, const nsACString& npnToken, + const nsACString& username, const nsACString& topWindowOrigin, + nsProxyInfo* proxyInfo, const OriginAttributes& originAttributes, + bool EndToEndSSL, bool aIsHttp3); + void SetOriginServer(const nsACString& host, int32_t port); + + nsCString mOrigin; + int32_t mOriginPort; + nsCString mRoutedHost; + int32_t mRoutedPort; + + nsCString mHashKey; + nsCString mUsername; + nsCString mTopWindowOrigin; + nsCOMPtr mProxyInfo; + bool mUsingHttpProxy; + bool mUsingHttpsProxy; + bool mEndToEndSSL; + bool mUsingConnect; // if will use CONNECT with http proxy + nsCString mNPNToken; + OriginAttributes mOriginAttributes; + nsIRequest::TRRMode mTRRMode; + + uint32_t mTlsFlags; + uint16_t mIsolated : 1; + uint16_t mIsTrrServiceChannel : 1; + uint16_t mIPv4Disabled : 1; + uint16_t mIPv6Disabled : 1; + + bool mLessThanTls13; // This will be set to true if we negotiate less than + // tls1.3. If the tls version is till not know or it + // is 1.3 or greater the value will be false. + bool mIsHttp3; + + bool mHasIPHintAddress = false; + nsCString mEchConfig; + + // for RefPtr + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(nsHttpConnectionInfo, override) +}; + +} // namespace net +} // namespace mozilla + +#endif // nsHttpConnectionInfo_h__ diff --git a/netwerk/protocol/http/nsHttpConnectionMgr.cpp b/netwerk/protocol/http/nsHttpConnectionMgr.cpp new file mode 100644 index 0000000000..1cecce5724 --- /dev/null +++ b/netwerk/protocol/http/nsHttpConnectionMgr.cpp @@ -0,0 +1,3527 @@ +/* vim:set ts=4 sw=2 sts=2 et cin: */ +/* 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/. */ + +// HttpLog.h should generally be included first +#include "HttpLog.h" + +// Log on level :5, instead of default :4. +#undef LOG +#define LOG(args) LOG5(args) +#undef LOG_ENABLED +#define LOG_ENABLED() LOG5_ENABLED() + +#include +#include + +#include "NullHttpTransaction.h" +#include "mozilla/Services.h" +#include "mozilla/SpinEventLoopUntil.h" +#include "mozilla/Telemetry.h" +#include "mozilla/Unused.h" +#include "mozilla/net/DNS.h" +#include "mozilla/net/DashboardTypes.h" +#include "nsCOMPtr.h" +#include "nsHttpConnectionMgr.h" +#include "nsHttpHandler.h" +#include "nsIClassOfService.h" +#include "nsIDNSByTypeRecord.h" +#include "nsIDNSRecord.h" +#include "nsIDNSListener.h" +#include "nsIDNSService.h" +#include "nsIHttpChannelInternal.h" +#include "nsIRequestContext.h" +#include "nsISocketTransport.h" +#include "nsISocketTransportService.h" +#include "nsITransport.h" +#include "nsIXPConnect.h" +#include "nsInterfaceRequestorAgg.h" +#include "nsNetCID.h" +#include "nsNetUtil.h" +#include "nsQueryObject.h" +#include "ConnectionHandle.h" +#include "HttpConnectionUDP.h" +#include "SpeculativeTransaction.h" +#include "TCPFastOpenLayer.h" + +namespace mozilla { +namespace net { + +//----------------------------------------------------------------------------- + +NS_IMPL_ISUPPORTS(nsHttpConnectionMgr, nsIObserver) + +//----------------------------------------------------------------------------- + +nsHttpConnectionMgr::nsHttpConnectionMgr() + : mReentrantMonitor("nsHttpConnectionMgr.mReentrantMonitor"), + mMaxUrgentExcessiveConns(0), + mMaxConns(0), + mMaxPersistConnsPerHost(0), + mMaxPersistConnsPerProxy(0), + mMaxRequestDelay(0), + mThrottleEnabled(false), + mThrottleVersion(2), + mThrottleSuspendFor(0), + mThrottleResumeFor(0), + mThrottleReadLimit(0), + mThrottleReadInterval(0), + mThrottleHoldTime(0), + mThrottleMaxTime(0), + mBeConservativeForProxy(true), + mIsShuttingDown(false), + mNumActiveConns(0), + mNumIdleConns(0), + mNumSpdyHttp3ActiveConns(0), + mNumHalfOpenConns(0), + mTimeOfNextWakeUp(UINT64_MAX), + mPruningNoTraffic(false), + mTimeoutTickArmed(false), + mTimeoutTickNext(1), + mCurrentTopLevelOuterContentWindowId(0), + mThrottlingInhibitsReading(false), + mActiveTabTransactionsExist(false), + mActiveTabUnthrottledTransactionsExist(false) { + LOG(("Creating nsHttpConnectionMgr @%p\n", this)); +} + +nsHttpConnectionMgr::~nsHttpConnectionMgr() { + LOG(("Destroying nsHttpConnectionMgr @%p\n", this)); + MOZ_ASSERT(mCoalescingHash.Count() == 0); + if (mTimeoutTick) mTimeoutTick->Cancel(); +} + +nsresult nsHttpConnectionMgr::EnsureSocketThreadTarget() { + nsCOMPtr sts; + nsCOMPtr ioService = services::GetIOService(); + if (ioService) { + nsCOMPtr realSTS = + services::GetSocketTransportService(); + sts = do_QueryInterface(realSTS); + } + + ReentrantMonitorAutoEnter mon(mReentrantMonitor); + + // do nothing if already initialized or if we've shut down + if (mSocketThreadTarget || mIsShuttingDown) return NS_OK; + + mSocketThreadTarget = sts; + + return sts ? NS_OK : NS_ERROR_NOT_AVAILABLE; +} + +nsresult nsHttpConnectionMgr::Init( + uint16_t maxUrgentExcessiveConns, uint16_t maxConns, + uint16_t maxPersistConnsPerHost, uint16_t maxPersistConnsPerProxy, + uint16_t maxRequestDelay, bool throttleEnabled, uint32_t throttleVersion, + uint32_t throttleSuspendFor, uint32_t throttleResumeFor, + uint32_t throttleReadLimit, uint32_t throttleReadInterval, + uint32_t throttleHoldTime, uint32_t throttleMaxTime, + bool beConservativeForProxy) { + LOG(("nsHttpConnectionMgr::Init\n")); + + { + ReentrantMonitorAutoEnter mon(mReentrantMonitor); + + mMaxUrgentExcessiveConns = maxUrgentExcessiveConns; + mMaxConns = maxConns; + mMaxPersistConnsPerHost = maxPersistConnsPerHost; + mMaxPersistConnsPerProxy = maxPersistConnsPerProxy; + mMaxRequestDelay = maxRequestDelay; + + mThrottleEnabled = throttleEnabled; + mThrottleVersion = throttleVersion; + mThrottleSuspendFor = throttleSuspendFor; + mThrottleResumeFor = throttleResumeFor; + mThrottleReadLimit = throttleReadLimit; + mThrottleReadInterval = throttleReadInterval; + mThrottleHoldTime = throttleHoldTime; + mThrottleMaxTime = TimeDuration::FromMilliseconds(throttleMaxTime); + + mBeConservativeForProxy = beConservativeForProxy; + + mIsShuttingDown = false; + } + + return EnsureSocketThreadTarget(); +} + +class BoolWrapper : public ARefBase { + public: + BoolWrapper() : mBool(false) {} + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(BoolWrapper, override) + + public: // intentional! + bool mBool; + + private: + virtual ~BoolWrapper() = default; +}; + +nsresult nsHttpConnectionMgr::Shutdown() { + LOG(("nsHttpConnectionMgr::Shutdown\n")); + + RefPtr shutdownWrapper = new BoolWrapper(); + { + ReentrantMonitorAutoEnter mon(mReentrantMonitor); + + // do nothing if already shutdown + if (!mSocketThreadTarget) return NS_OK; + + nsresult rv = + PostEvent(&nsHttpConnectionMgr::OnMsgShutdown, 0, shutdownWrapper); + + // release our reference to the STS to prevent further events + // from being posted. this is how we indicate that we are + // shutting down. + mIsShuttingDown = true; + mSocketThreadTarget = nullptr; + + if (NS_FAILED(rv)) { + NS_WARNING("unable to post SHUTDOWN message"); + return rv; + } + } + + // wait for shutdown event to complete + SpinEventLoopUntil([&, shutdownWrapper]() { return shutdownWrapper->mBool; }); + + return NS_OK; +} + +class ConnEvent : public Runnable { + public: + ConnEvent(nsHttpConnectionMgr* mgr, nsConnEventHandler handler, + int32_t iparam, ARefBase* vparam) + : Runnable("net::ConnEvent"), + mMgr(mgr), + mHandler(handler), + mIParam(iparam), + mVParam(vparam) {} + + NS_IMETHOD Run() override { + (mMgr->*mHandler)(mIParam, mVParam); + return NS_OK; + } + + private: + virtual ~ConnEvent() = default; + + RefPtr mMgr; + nsConnEventHandler mHandler; + int32_t mIParam; + RefPtr mVParam; +}; + +nsresult nsHttpConnectionMgr::PostEvent(nsConnEventHandler handler, + int32_t iparam, ARefBase* vparam) { + Unused << EnsureSocketThreadTarget(); + + ReentrantMonitorAutoEnter mon(mReentrantMonitor); + + nsresult rv; + if (!mSocketThreadTarget) { + NS_WARNING("cannot post event if not initialized"); + rv = NS_ERROR_NOT_INITIALIZED; + } else { + nsCOMPtr event = new ConnEvent(this, handler, iparam, vparam); + rv = mSocketThreadTarget->Dispatch(event, NS_DISPATCH_NORMAL); + } + return rv; +} + +void nsHttpConnectionMgr::PruneDeadConnectionsAfter(uint32_t timeInSeconds) { + LOG(("nsHttpConnectionMgr::PruneDeadConnectionsAfter\n")); + + if (!mTimer) mTimer = NS_NewTimer(); + + // failure to create a timer is not a fatal error, but idle connections + // will not be cleaned up until we try to use them. + if (mTimer) { + mTimeOfNextWakeUp = timeInSeconds + NowInSeconds(); + mTimer->Init(this, timeInSeconds * 1000, nsITimer::TYPE_ONE_SHOT); + } else { + NS_WARNING("failed to create: timer for pruning the dead connections!"); + } +} + +void nsHttpConnectionMgr::ConditionallyStopPruneDeadConnectionsTimer() { + // Leave the timer in place if there are connections that potentially + // need management + if (mNumIdleConns || (mNumActiveConns && gHttpHandler->IsSpdyEnabled())) + return; + + LOG(("nsHttpConnectionMgr::StopPruneDeadConnectionsTimer\n")); + + // Reset mTimeOfNextWakeUp so that we can find a new shortest value. + mTimeOfNextWakeUp = UINT64_MAX; + if (mTimer) { + mTimer->Cancel(); + mTimer = nullptr; + } +} + +void nsHttpConnectionMgr::ConditionallyStopTimeoutTick() { + LOG( + ("nsHttpConnectionMgr::ConditionallyStopTimeoutTick " + "armed=%d active=%d\n", + mTimeoutTickArmed, mNumActiveConns)); + + if (!mTimeoutTickArmed) return; + + if (mNumActiveConns) return; + + LOG(("nsHttpConnectionMgr::ConditionallyStopTimeoutTick stop==true\n")); + + mTimeoutTick->Cancel(); + mTimeoutTickArmed = false; +} + +//----------------------------------------------------------------------------- +// nsHttpConnectionMgr::nsIObserver +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsHttpConnectionMgr::Observe(nsISupports* subject, const char* topic, + const char16_t* data) { + LOG(("nsHttpConnectionMgr::Observe [topic=\"%s\"]\n", topic)); + + if (0 == strcmp(topic, NS_TIMER_CALLBACK_TOPIC)) { + nsCOMPtr timer = do_QueryInterface(subject); + if (timer == mTimer) { + Unused << PruneDeadConnections(); + } else if (timer == mTimeoutTick) { + TimeoutTick(); + } else if (timer == mTrafficTimer) { + Unused << PruneNoTraffic(); + } else if (timer == mThrottleTicker) { + ThrottlerTick(); + } else if (timer == mDelayedResumeReadTimer) { + ResumeBackgroundThrottledTransactions(); + } else { + MOZ_ASSERT(false, "unexpected timer-callback"); + LOG(("Unexpected timer object\n")); + return NS_ERROR_UNEXPECTED; + } + } + + return NS_OK; +} + +//----------------------------------------------------------------------------- + +nsresult nsHttpConnectionMgr::AddTransaction(HttpTransactionShell* trans, + int32_t priority) { + LOG(("nsHttpConnectionMgr::AddTransaction [trans=%p %d]\n", trans, priority)); + return PostEvent(&nsHttpConnectionMgr::OnMsgNewTransaction, priority, + trans->AsHttpTransaction()); +} + +class NewTransactionData : public ARefBase { + public: + NewTransactionData(nsHttpTransaction* trans, int32_t priority, + nsHttpTransaction* transWithStickyConn) + : mTrans(trans), + mPriority(priority), + mTransWithStickyConn(transWithStickyConn) {} + + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(NewTransactionData, override) + + RefPtr mTrans; + int32_t mPriority; + RefPtr mTransWithStickyConn; + + private: + virtual ~NewTransactionData() = default; +}; + +nsresult nsHttpConnectionMgr::AddTransactionWithStickyConn( + HttpTransactionShell* trans, int32_t priority, + HttpTransactionShell* transWithStickyConn) { + LOG( + ("nsHttpConnectionMgr::AddTransactionWithStickyConn " + "[trans=%p %d transWithStickyConn=%p]\n", + trans, priority, transWithStickyConn)); + RefPtr data = + new NewTransactionData(trans->AsHttpTransaction(), priority, + transWithStickyConn->AsHttpTransaction()); + return PostEvent(&nsHttpConnectionMgr::OnMsgNewTransactionWithStickyConn, 0, + data); +} + +nsresult nsHttpConnectionMgr::RescheduleTransaction(HttpTransactionShell* trans, + int32_t priority) { + LOG(("nsHttpConnectionMgr::RescheduleTransaction [trans=%p %d]\n", trans, + priority)); + return PostEvent(&nsHttpConnectionMgr::OnMsgReschedTransaction, priority, + trans->AsHttpTransaction()); +} + +void nsHttpConnectionMgr::UpdateClassOfServiceOnTransaction( + HttpTransactionShell* trans, uint32_t classOfService) { + LOG( + ("nsHttpConnectionMgr::UpdateClassOfServiceOnTransaction [trans=%p " + "classOfService=%" PRIu32 "]\n", + trans, static_cast(classOfService))); + Unused << PostEvent( + &nsHttpConnectionMgr::OnMsgUpdateClassOfServiceOnTransaction, + static_cast(classOfService), trans->AsHttpTransaction()); +} + +nsresult nsHttpConnectionMgr::CancelTransaction(HttpTransactionShell* trans, + nsresult reason) { + LOG(("nsHttpConnectionMgr::CancelTransaction [trans=%p reason=%" PRIx32 "]\n", + trans, static_cast(reason))); + return PostEvent(&nsHttpConnectionMgr::OnMsgCancelTransaction, + static_cast(reason), trans->AsHttpTransaction()); +} + +nsresult nsHttpConnectionMgr::PruneDeadConnections() { + return PostEvent(&nsHttpConnectionMgr::OnMsgPruneDeadConnections); +} + +// +// Called after a timeout. Check for active connections that have had no +// traffic since they were "marked" and nuke them. +nsresult nsHttpConnectionMgr::PruneNoTraffic() { + LOG(("nsHttpConnectionMgr::PruneNoTraffic\n")); + mPruningNoTraffic = true; + return PostEvent(&nsHttpConnectionMgr::OnMsgPruneNoTraffic); +} + +nsresult nsHttpConnectionMgr::VerifyTraffic() { + LOG(("nsHttpConnectionMgr::VerifyTraffic\n")); + return PostEvent(&nsHttpConnectionMgr::OnMsgVerifyTraffic); +} + +nsresult nsHttpConnectionMgr::DoShiftReloadConnectionCleanup() { + return PostEvent(&nsHttpConnectionMgr::OnMsgDoShiftReloadConnectionCleanup, 0, + nullptr); +} + +nsresult nsHttpConnectionMgr::DoShiftReloadConnectionCleanupWithConnInfo( + nsHttpConnectionInfo* aCI) { + if (!aCI) { + return NS_ERROR_INVALID_ARG; + } + + RefPtr ci = aCI->Clone(); + return PostEvent(&nsHttpConnectionMgr::OnMsgDoShiftReloadConnectionCleanup, 0, + ci); +} + +class SpeculativeConnectArgs : public ARefBase { + public: + SpeculativeConnectArgs() : mFetchHTTPSRR(false) {} + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(SpeculativeConnectArgs, override) + + public: // intentional! + RefPtr mTrans; + + bool mFetchHTTPSRR; + + private: + virtual ~SpeculativeConnectArgs() = default; + NS_DECL_OWNINGTHREAD +}; + +nsresult nsHttpConnectionMgr::SpeculativeConnect( + nsHttpConnectionInfo* ci, nsIInterfaceRequestor* callbacks, uint32_t caps, + SpeculativeTransaction* aTransaction, bool aFetchHTTPSRR) { + if (!IsNeckoChild() && NS_IsMainThread()) { + // HACK: make sure PSM gets initialized on the main thread. + net_EnsurePSMInit(); + } + + LOG(("nsHttpConnectionMgr::SpeculativeConnect [ci=%s]\n", + ci->HashKey().get())); + + nsCOMPtr overrider = + do_GetInterface(callbacks); + + bool allow1918 = overrider ? overrider->GetAllow1918() : false; + + // Hosts that are Local IP Literals should not be speculatively + // connected - Bug 853423. + if ((!allow1918) && ci && ci->HostIsLocalIPLiteral()) { + LOG( + ("nsHttpConnectionMgr::SpeculativeConnect skipping RFC1918 " + "address [%s]", + ci->Origin())); + return NS_OK; + } + + RefPtr args = new SpeculativeConnectArgs(); + + // Wrap up the callbacks and the target to ensure they're released on the + // target thread properly. + nsCOMPtr wrappedCallbacks; + NS_NewInterfaceRequestorAggregation(callbacks, nullptr, + getter_AddRefs(wrappedCallbacks)); + + caps |= ci->GetAnonymous() ? NS_HTTP_LOAD_ANONYMOUS : 0; + caps |= NS_HTTP_ERROR_SOFTLY; + args->mTrans = aTransaction + ? aTransaction + : new SpeculativeTransaction(ci, wrappedCallbacks, caps); + args->mFetchHTTPSRR = aFetchHTTPSRR; + + if (overrider) { + args->mTrans->SetParallelSpeculativeConnectLimit( + overrider->GetParallelSpeculativeConnectLimit()); + args->mTrans->SetIgnoreIdle(overrider->GetIgnoreIdle()); + args->mTrans->SetIsFromPredictor(overrider->GetIsFromPredictor()); + args->mTrans->SetAllow1918(overrider->GetAllow1918()); + } + + return PostEvent(&nsHttpConnectionMgr::OnMsgSpeculativeConnect, 0, args); +} + +nsresult nsHttpConnectionMgr::GetSocketThreadTarget(nsIEventTarget** target) { + Unused << EnsureSocketThreadTarget(); + + ReentrantMonitorAutoEnter mon(mReentrantMonitor); + nsCOMPtr temp(mSocketThreadTarget); + temp.forget(target); + return NS_OK; +} + +nsresult nsHttpConnectionMgr::ReclaimConnection(HttpConnectionBase* conn) { + LOG(("nsHttpConnectionMgr::ReclaimConnection [conn=%p]\n", conn)); + + Unused << EnsureSocketThreadTarget(); + + ReentrantMonitorAutoEnter mon(mReentrantMonitor); + + if (!mSocketThreadTarget) { + NS_WARNING("cannot post event if not initialized"); + return NS_ERROR_NOT_INITIALIZED; + } + + RefPtr connRef(conn); + RefPtr self(this); + return mSocketThreadTarget->Dispatch(NS_NewRunnableFunction( + "nsHttpConnectionMgr::CallReclaimConnection", + [conn{std::move(connRef)}, self{std::move(self)}]() { + self->OnMsgReclaimConnection(conn); + })); +} + +// A structure used to marshall 6 pointers across the various necessary +// threads to complete an HTTP upgrade. +class nsCompleteUpgradeData : public ARefBase { + public: + nsCompleteUpgradeData(nsHttpTransaction* aTrans, + nsIHttpUpgradeListener* aListener, bool aJsWrapped) + : mTrans(aTrans), mUpgradeListener(aListener), mJsWrapped(aJsWrapped) {} + + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(nsCompleteUpgradeData, override) + + RefPtr mTrans; + nsCOMPtr mUpgradeListener; + + nsCOMPtr mSocketTransport; + nsCOMPtr mSocketIn; + nsCOMPtr mSocketOut; + + bool mJsWrapped; + + private: + virtual ~nsCompleteUpgradeData() { + NS_ReleaseOnMainThread("nsCompleteUpgradeData.mUpgradeListener", + mUpgradeListener.forget()); + } +}; + +nsresult nsHttpConnectionMgr::CompleteUpgrade( + HttpTransactionShell* aTrans, nsIHttpUpgradeListener* aUpgradeListener) { + // test if aUpgradeListener is a wrapped JsObject + nsCOMPtr wrapper = do_QueryInterface(aUpgradeListener); + + bool wrapped = !!wrapper; + + RefPtr data = new nsCompleteUpgradeData( + aTrans->AsHttpTransaction(), aUpgradeListener, wrapped); + return PostEvent(&nsHttpConnectionMgr::OnMsgCompleteUpgrade, 0, data); +} + +nsresult nsHttpConnectionMgr::UpdateParam(nsParamName name, uint16_t value) { + uint32_t param = (uint32_t(name) << 16) | uint32_t(value); + return PostEvent(&nsHttpConnectionMgr::OnMsgUpdateParam, + static_cast(param), nullptr); +} + +nsresult nsHttpConnectionMgr::ProcessPendingQ(nsHttpConnectionInfo* aCI) { + LOG(("nsHttpConnectionMgr::ProcessPendingQ [ci=%s]\n", aCI->HashKey().get())); + RefPtr ci; + if (aCI) { + ci = aCI->Clone(); + } + return PostEvent(&nsHttpConnectionMgr::OnMsgProcessPendingQ, 0, ci); +} + +nsresult nsHttpConnectionMgr::ProcessPendingQ() { + LOG(("nsHttpConnectionMgr::ProcessPendingQ [All CI]\n")); + return PostEvent(&nsHttpConnectionMgr::OnMsgProcessPendingQ, 0, nullptr); +} + +void nsHttpConnectionMgr::OnMsgUpdateRequestTokenBucket(int32_t, + ARefBase* param) { + EventTokenBucket* tokenBucket = static_cast(param); + gHttpHandler->SetRequestTokenBucket(tokenBucket); +} + +nsresult nsHttpConnectionMgr::UpdateRequestTokenBucket( + EventTokenBucket* aBucket) { + // Call From main thread when a new EventTokenBucket has been made in order + // to post the new value to the socket thread. + return PostEvent(&nsHttpConnectionMgr::OnMsgUpdateRequestTokenBucket, 0, + aBucket); +} + +nsresult nsHttpConnectionMgr::ClearConnectionHistory() { + return PostEvent(&nsHttpConnectionMgr::OnMsgClearConnectionHistory, 0, + nullptr); +} + +void nsHttpConnectionMgr::OnMsgClearConnectionHistory(int32_t, + ARefBase* param) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + LOG(("nsHttpConnectionMgr::OnMsgClearConnectionHistory")); + + for (auto iter = mCT.Iter(); !iter.Done(); iter.Next()) { + RefPtr ent = iter.Data(); + if (ent->IdleConnectionsLength() == 0 && ent->ActiveConnsLength() == 0 && + ent->HalfOpensLength() == 0 && ent->UrgentStartQueueLength() == 0 && + ent->PendingQueueLength() == 0 && + ent->HalfOpenFastOpenBackupsLength() == 0 && !ent->mDoNotDestroy) { + iter.Remove(); + } + } +} + +nsresult nsHttpConnectionMgr::CloseIdleConnection(nsHttpConnection* conn) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG(("nsHttpConnectionMgr::CloseIdleConnection %p conn=%p", this, conn)); + + if (!conn->ConnectionInfo()) { + return NS_ERROR_UNEXPECTED; + } + + ConnectionEntry* ent = mCT.GetWeak(conn->ConnectionInfo()->HashKey()); + + if (!ent || NS_FAILED(ent->CloseIdleConnection(conn))) { + return NS_ERROR_UNEXPECTED; + } + + return NS_OK; +} + +nsresult nsHttpConnectionMgr::RemoveIdleConnection(nsHttpConnection* conn) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + LOG(("nsHttpConnectionMgr::RemoveIdleConnection %p conn=%p", this, conn)); + + if (!conn->ConnectionInfo()) { + return NS_ERROR_UNEXPECTED; + } + + ConnectionEntry* ent = mCT.GetWeak(conn->ConnectionInfo()->HashKey()); + + if (!ent || NS_FAILED(ent->RemoveIdleConnection(conn))) { + return NS_ERROR_UNEXPECTED; + } + + return NS_OK; +} + +HttpConnectionBase* nsHttpConnectionMgr::FindCoalescableConnectionByHashKey( + ConnectionEntry* ent, const nsCString& key, bool justKidding, bool aNoHttp2, + bool aNoHttp3) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + MOZ_ASSERT(!aNoHttp2 || !aNoHttp3); + MOZ_ASSERT(ent->mConnInfo); + nsHttpConnectionInfo* ci = ent->mConnInfo; + + nsTArray* listOfWeakConns = mCoalescingHash.Get(key); + if (!listOfWeakConns) { + return nullptr; + } + + uint32_t listLen = listOfWeakConns->Length(); + for (uint32_t j = 0; j < listLen;) { + RefPtr potentialMatch = + do_QueryReferent(listOfWeakConns->ElementAt(j)); + if (!potentialMatch) { + // This is a connection that needs to be removed from the list + LOG( + ("FindCoalescableConnectionByHashKey() found old conn %p that has " + "null weak ptr - removing\n", + listOfWeakConns->ElementAt(j).get())); + if (j != listLen - 1) { + listOfWeakConns->Elements()[j] = + listOfWeakConns->Elements()[listLen - 1]; + } + listOfWeakConns->RemoveLastElement(); + MOZ_ASSERT(listOfWeakConns->Length() == listLen - 1); + listLen--; + continue; // without adjusting iterator + } + + if (aNoHttp3 && potentialMatch->UsingHttp3()) { + j++; + continue; + } + if (aNoHttp2 && potentialMatch->UsingSpdy()) { + j++; + continue; + } + bool couldJoin; + if (justKidding) { + couldJoin = + potentialMatch->TestJoinConnection(ci->GetOrigin(), ci->OriginPort()); + } else { + couldJoin = + potentialMatch->JoinConnection(ci->GetOrigin(), ci->OriginPort()); + } + if (couldJoin) { + LOG( + ("FindCoalescableConnectionByHashKey() found match conn=%p key=%s " + "newCI=%s matchedCI=%s join ok\n", + potentialMatch.get(), key.get(), ci->HashKey().get(), + potentialMatch->ConnectionInfo()->HashKey().get())); + return potentialMatch.get(); + } + LOG( + ("FindCoalescableConnectionByHashKey() found match conn=%p key=%s " + "newCI=%s matchedCI=%s join failed\n", + potentialMatch.get(), key.get(), ci->HashKey().get(), + potentialMatch->ConnectionInfo()->HashKey().get())); + + ++j; // bypassed by continue when weakptr fails + } + + if (!listLen) { // shrunk to 0 while iterating + LOG(("FindCoalescableConnectionByHashKey() removing empty list element\n")); + mCoalescingHash.Remove(key); + } + return nullptr; +} + +static void BuildOriginFrameHashKey(nsACString& newKey, + nsHttpConnectionInfo* ci, + const nsACString& host, int32_t port) { + newKey.Assign(host); + if (ci->GetAnonymous()) { + newKey.AppendLiteral("~A:"); + } else { + newKey.AppendLiteral("~.:"); + } + newKey.AppendInt(port); + newKey.AppendLiteral("/["); + nsAutoCString suffix; + ci->GetOriginAttributes().CreateSuffix(suffix); + newKey.Append(suffix); + newKey.AppendLiteral("]viaORIGIN.FRAME"); +} + +HttpConnectionBase* nsHttpConnectionMgr::FindCoalescableConnection( + ConnectionEntry* ent, bool justKidding, bool aNoHttp2, bool aNoHttp3) { + MOZ_ASSERT(!aNoHttp2 || !aNoHttp3); + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + MOZ_ASSERT(ent->mConnInfo); + nsHttpConnectionInfo* ci = ent->mConnInfo; + LOG(("FindCoalescableConnection %s\n", ci->HashKey().get())); + // First try and look it up by origin frame + nsCString newKey; + BuildOriginFrameHashKey(newKey, ci, ci->GetOrigin(), ci->OriginPort()); + HttpConnectionBase* conn = FindCoalescableConnectionByHashKey( + ent, newKey, justKidding, aNoHttp2, aNoHttp3); + if (conn) { + LOG(("FindCoalescableConnection(%s) match conn %p on frame key %s\n", + ci->HashKey().get(), conn, newKey.get())); + return conn; + } + + // now check for DNS based keys + // deleted conns (null weak pointers) are removed from list + uint32_t keyLen = ent->mCoalescingKeys.Length(); + for (uint32_t i = 0; i < keyLen; ++i) { + conn = FindCoalescableConnectionByHashKey(ent, ent->mCoalescingKeys[i], + justKidding, aNoHttp2, aNoHttp3); + if (conn) { + LOG(("FindCoalescableConnection(%s) match conn %p on dns key %s\n", + ci->HashKey().get(), conn, ent->mCoalescingKeys[i].get())); + return conn; + } + } + + LOG(("FindCoalescableConnection(%s) no matching conn\n", + ci->HashKey().get())); + return nullptr; +} + +void nsHttpConnectionMgr::UpdateCoalescingForNewConn( + HttpConnectionBase* newConn, ConnectionEntry* ent) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + MOZ_ASSERT(newConn); + MOZ_ASSERT(newConn->ConnectionInfo()); + MOZ_ASSERT(ent); + MOZ_ASSERT(mCT.GetWeak(newConn->ConnectionInfo()->HashKey()) == ent); + + HttpConnectionBase* existingConn = + FindCoalescableConnection(ent, true, false, false); + if (existingConn) { + // Prefer http3 connection. + if (newConn->UsingHttp3() && existingConn->UsingSpdy()) { + LOG( + ("UpdateCoalescingForNewConn() found existing active H2 conn that " + "could have served newConn, but new connection is H3, therefore " + "close the H2 conncetion")); + existingConn->DontReuse(); + } else { + LOG( + ("UpdateCoalescingForNewConn() found existing active conn that could " + "have served newConn " + "graceful close of newConn=%p to migrate to existingConn %p\n", + newConn, existingConn)); + newConn->DontReuse(); + return; + } + } + + // This connection might go into the mCoalescingHash for new transactions to + // be coalesced onto if it can accept new transactions + if (!newConn->CanDirectlyActivate()) { + return; + } + + uint32_t keyLen = ent->mCoalescingKeys.Length(); + for (uint32_t i = 0; i < keyLen; ++i) { + LOG(( + "UpdateCoalescingForNewConn() registering newConn %p %s under key %s\n", + newConn, newConn->ConnectionInfo()->HashKey().get(), + ent->mCoalescingKeys[i].get())); + nsTArray* listOfWeakConns = + mCoalescingHash.Get(ent->mCoalescingKeys[i]); + if (!listOfWeakConns) { + LOG(("UpdateCoalescingForNewConn() need new list element\n")); + listOfWeakConns = new nsTArray(1); + mCoalescingHash.Put(ent->mCoalescingKeys[i], listOfWeakConns); + } + listOfWeakConns->AppendElement( + do_GetWeakReference(static_cast(newConn))); + } + + // this is a new connection that can be coalesced onto. hooray! + // if there are other connection to this entry (e.g. + // some could still be handshaking, shutting down, etc..) then close + // them down after any transactions that are on them are complete. + // This probably happened due to the parallel connection algorithm + // that is used only before the host is known to speak h2. + ent->MakeAllDontReuseExcept(newConn); +} + +// This function lets a connection, after completing the NPN phase, +// report whether or not it is using spdy through the usingSpdy +// argument. It would not be necessary if NPN were driven out of +// the connection manager. The connection entry associated with the +// connection is then updated to indicate whether or not we want to use +// spdy with that host and update the coalescing hash +// entries used for de-sharding hostsnames. +void nsHttpConnectionMgr::ReportSpdyConnection(nsHttpConnection* conn, + bool usingSpdy) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + if (!conn->ConnectionInfo()) { + return; + } + ConnectionEntry* ent = mCT.GetWeak(conn->ConnectionInfo()->HashKey()); + if (!ent || !usingSpdy) { + return; + } + + ent->mUsingSpdy = true; + mNumSpdyHttp3ActiveConns++; + + // adjust timeout timer + uint32_t ttl = conn->TimeToLive(); + uint64_t timeOfExpire = NowInSeconds() + ttl; + if (!mTimer || timeOfExpire < mTimeOfNextWakeUp) { + PruneDeadConnectionsAfter(ttl); + } + + UpdateCoalescingForNewConn(conn, ent); + + nsresult rv = ProcessPendingQ(ent->mConnInfo); + if (NS_FAILED(rv)) { + LOG( + ("ReportSpdyConnection conn=%p ent=%p " + "failed to process pending queue (%08x)\n", + conn, ent, static_cast(rv))); + } + rv = PostEvent(&nsHttpConnectionMgr::OnMsgProcessAllSpdyPendingQ); + if (NS_FAILED(rv)) { + LOG( + ("ReportSpdyConnection conn=%p ent=%p " + "failed to post event (%08x)\n", + conn, ent, static_cast(rv))); + } +} + +void nsHttpConnectionMgr::ReportHttp3Connection(HttpConnectionBase* conn) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + if (!conn->ConnectionInfo()) { + return; + } + ConnectionEntry* ent = mCT.GetWeak(conn->ConnectionInfo()->HashKey()); + if (!ent) { + return; + } + + mNumSpdyHttp3ActiveConns++; + + UpdateCoalescingForNewConn(conn, ent); + nsresult rv = ProcessPendingQ(ent->mConnInfo); + if (NS_FAILED(rv)) { + LOG( + ("ReportHttp3Connection conn=%p ent=%p " + "failed to process pending queue (%08x)\n", + conn, ent, static_cast(rv))); + } + rv = PostEvent(&nsHttpConnectionMgr::OnMsgProcessAllSpdyPendingQ); + if (NS_FAILED(rv)) { + LOG( + ("ReportHttp3Connection conn=%p ent=%p " + "failed to post event (%08x)\n", + conn, ent, static_cast(rv))); + } +} + +//----------------------------------------------------------------------------- +bool nsHttpConnectionMgr::DispatchPendingQ( + nsTArray>& pendingQ, ConnectionEntry* ent, + bool considerAll) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + PendingTransactionInfo* pendingTransInfo = nullptr; + nsresult rv; + bool dispatchedSuccessfully = false; + + // if !considerAll iterate the pending list until one is dispatched + // successfully. Keep iterating afterwards only until a transaction fails to + // dispatch. if considerAll == true then try and dispatch all items. + for (uint32_t i = 0; i < pendingQ.Length();) { + pendingTransInfo = pendingQ[i]; + + bool alreadyHalfOpenOrWaitingForTLS = + pendingTransInfo->IsAlreadyClaimedInitializingConn(); + + rv = TryDispatchTransaction( + ent, + alreadyHalfOpenOrWaitingForTLS || + !!pendingTransInfo->Transaction()->TunnelProvider(), + pendingTransInfo); + if (NS_SUCCEEDED(rv) || (rv != NS_ERROR_NOT_AVAILABLE)) { + if (NS_SUCCEEDED(rv)) { + LOG((" dispatching pending transaction...\n")); + } else { + LOG( + (" removing pending transaction based on " + "TryDispatchTransaction returning hard error %" PRIx32 "\n", + static_cast(rv))); + } + if (pendingQ.RemoveElement(pendingTransInfo)) { + // pendingTransInfo is now potentially destroyed + dispatchedSuccessfully = true; + continue; // dont ++i as we just made the array shorter + } + + LOG((" transaction not found in pending queue\n")); + } + + if (dispatchedSuccessfully && !considerAll) break; + + ++i; + } + return dispatchedSuccessfully; +} + +uint32_t nsHttpConnectionMgr::MaxPersistConnections( + ConnectionEntry* ent) const { + if (ent->mConnInfo->UsingHttpProxy() && !ent->mConnInfo->UsingConnect()) { + return static_cast(mMaxPersistConnsPerProxy); + } + + return static_cast(mMaxPersistConnsPerHost); +} + +void nsHttpConnectionMgr::PreparePendingQForDispatching( + ConnectionEntry* ent, nsTArray>& pendingQ, + bool considerAll) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + pendingQ.Clear(); + + uint32_t totalCount = ent->TotalActiveConnections(); + uint32_t maxPersistConns = MaxPersistConnections(ent); + uint32_t availableConnections = + maxPersistConns > totalCount ? maxPersistConns - totalCount : 0; + + // No need to try dispatching if we reach the active connection limit. + if (!availableConnections) { + return; + } + + // Only have to get transactions from the queue whose window id is 0. + if (!gHttpHandler->ActiveTabPriority()) { + ent->AppendPendingQForFocusedWindow(0, pendingQ, availableConnections); + return; + } + + uint32_t maxFocusedWindowConnections = + availableConnections * gHttpHandler->FocusedWindowTransactionRatio(); + MOZ_ASSERT(maxFocusedWindowConnections < availableConnections); + + if (!maxFocusedWindowConnections) { + maxFocusedWindowConnections = 1; + } + + // Only need to dispatch transactions for either focused or + // non-focused window because considerAll is false. + if (!considerAll) { + ent->AppendPendingQForFocusedWindow(mCurrentTopLevelOuterContentWindowId, + pendingQ, maxFocusedWindowConnections); + + if (pendingQ.IsEmpty()) { + ent->AppendPendingQForNonFocusedWindows( + mCurrentTopLevelOuterContentWindowId, pendingQ, availableConnections); + } + return; + } + + uint32_t maxNonFocusedWindowConnections = + availableConnections - maxFocusedWindowConnections; + nsTArray> remainingPendingQ; + + ent->AppendPendingQForFocusedWindow(mCurrentTopLevelOuterContentWindowId, + pendingQ, maxFocusedWindowConnections); + + if (maxNonFocusedWindowConnections) { + ent->AppendPendingQForNonFocusedWindows( + mCurrentTopLevelOuterContentWindowId, remainingPendingQ, + maxNonFocusedWindowConnections); + } + + // If the slots for either focused or non-focused window are not filled up + // to the availability, try to use the remaining available connections + // for the other slot (with preference for the focused window). + if (remainingPendingQ.Length() < maxNonFocusedWindowConnections) { + ent->AppendPendingQForFocusedWindow( + mCurrentTopLevelOuterContentWindowId, pendingQ, + maxNonFocusedWindowConnections - remainingPendingQ.Length()); + } else if (pendingQ.Length() < maxFocusedWindowConnections) { + ent->AppendPendingQForNonFocusedWindows( + mCurrentTopLevelOuterContentWindowId, remainingPendingQ, + maxFocusedWindowConnections - pendingQ.Length()); + } + + MOZ_ASSERT(pendingQ.Length() + remainingPendingQ.Length() <= + availableConnections); + + LOG( + ("nsHttpConnectionMgr::PreparePendingQForDispatching " + "focused window pendingQ.Length()=%zu" + ", remainingPendingQ.Length()=%zu\n", + pendingQ.Length(), remainingPendingQ.Length())); + + // Append elements in |remainingPendingQ| to |pendingQ|. The order in + // |pendingQ| is like: [focusedWindowTrans...nonFocusedWindowTrans]. + pendingQ.AppendElements(std::move(remainingPendingQ)); +} + +bool nsHttpConnectionMgr::ProcessPendingQForEntry(ConnectionEntry* ent, + bool considerAll) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + LOG( + ("nsHttpConnectionMgr::ProcessPendingQForEntry " + "[ci=%s ent=%p active=%zu idle=%zu urgent-start-queue=%zu" + " queued=%zu]\n", + ent->mConnInfo->HashKey().get(), ent, ent->ActiveConnsLength(), + ent->IdleConnectionsLength(), ent->UrgentStartQueueLength(), + ent->PendingQueueLength())); + + if (LOG_ENABLED()) { + ent->PrintPendingQ(); + ent->LogConnections(); + } + + if (!ent->PendingQueueLength() && !ent->UrgentStartQueueLength()) { + return false; + } + ProcessSpdyPendingQ(ent); + + bool dispatchedSuccessfully = false; + + if (ent->UrgentStartQueueLength()) { + nsTArray> pendingQ; + ent->AppendPendingUrgentStartQ(pendingQ); + dispatchedSuccessfully = DispatchPendingQ(pendingQ, ent, considerAll); + for (const auto& transactionInfo : Reversed(pendingQ)) { + ent->InsertTransaction(transactionInfo); + } + } + + if (dispatchedSuccessfully && !considerAll) { + return dispatchedSuccessfully; + } + + nsTArray> pendingQ; + PreparePendingQForDispatching(ent, pendingQ, considerAll); + + // The only case that |pendingQ| is empty is when there is no + // connection available for dispatching. + if (pendingQ.IsEmpty()) { + return dispatchedSuccessfully; + } + + dispatchedSuccessfully |= DispatchPendingQ(pendingQ, ent, considerAll); + + // Put the leftovers into connection entry, in the same order as they + // were before to keep the natural ordering. + for (const auto& transactionInfo : Reversed(pendingQ)) { + ent->InsertTransaction(transactionInfo, true); + } + + // Only remove empty pendingQ when considerAll is true. + if (considerAll) { + ent->RemoveEmptyPendingQ(); + } + + return dispatchedSuccessfully; +} + +bool nsHttpConnectionMgr::ProcessPendingQForEntry(nsHttpConnectionInfo* ci) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + ConnectionEntry* ent = mCT.GetWeak(ci->HashKey()); + if (ent) return ProcessPendingQForEntry(ent, false); + return false; +} + +// we're at the active connection limit if any one of the following conditions +// is true: +// (1) at max-connections +// (2) keep-alive enabled and at max-persistent-connections-per-server/proxy +// (3) keep-alive disabled and at max-connections-per-server +bool nsHttpConnectionMgr::AtActiveConnectionLimit(ConnectionEntry* ent, + uint32_t caps) { + nsHttpConnectionInfo* ci = ent->mConnInfo; + uint32_t totalCount = ent->TotalActiveConnections(); + + if (ci->IsHttp3()) { + return totalCount > 0; + } + + uint32_t maxPersistConns = MaxPersistConnections(ent); + + LOG( + ("nsHttpConnectionMgr::AtActiveConnectionLimit [ci=%s caps=%x," + "totalCount=%u, maxPersistConns=%u]\n", + ci->HashKey().get(), caps, totalCount, maxPersistConns)); + + if (caps & NS_HTTP_URGENT_START) { + if (totalCount >= (mMaxUrgentExcessiveConns + maxPersistConns)) { + LOG(( + "The number of total connections are greater than or equal to sum of " + "max urgent-start queue length and the number of max persistent " + "connections.\n")); + return true; + } + return false; + } + + // update maxconns if potentially limited by the max socket count + // this requires a dynamic reduction in the max socket count to a point + // lower than the max-connections pref. + uint32_t maxSocketCount = gHttpHandler->MaxSocketCount(); + if (mMaxConns > maxSocketCount) { + mMaxConns = maxSocketCount; + LOG(("nsHttpConnectionMgr %p mMaxConns dynamically reduced to %u", this, + mMaxConns)); + } + + // If there are more active connections than the global limit, then we're + // done. Purging idle connections won't get us below it. + if (mNumActiveConns >= mMaxConns) { + LOG((" num active conns == max conns\n")); + return true; + } + + bool result = (totalCount >= maxPersistConns); + LOG(("AtActiveConnectionLimit result: %s", result ? "true" : "false")); + return result; +} + +// returns NS_OK if a connection was started +// return NS_ERROR_NOT_AVAILABLE if a new connection cannot be made due to +// ephemeral limits +// returns other NS_ERROR on hard failure conditions +nsresult nsHttpConnectionMgr::MakeNewConnection( + ConnectionEntry* ent, PendingTransactionInfo* pendingTransInfo) { + LOG(("nsHttpConnectionMgr::MakeNewConnection %p ent=%p trans=%p", this, ent, + pendingTransInfo->Transaction())); + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + if (ent->FindConnToClaim(pendingTransInfo)) { + return NS_OK; + } + + nsHttpTransaction* trans = pendingTransInfo->Transaction(); + + // If this host is trying to negotiate a SPDY session right now, + // don't create any new connections until the result of the + // negotiation is known. + if (!(trans->Caps() & NS_HTTP_DISALLOW_SPDY) && + (trans->Caps() & NS_HTTP_ALLOW_KEEPALIVE) && ent->RestrictConnections()) { + LOG( + ("nsHttpConnectionMgr::MakeNewConnection [ci = %s] " + "Not Available Due to RestrictConnections()\n", + ent->mConnInfo->HashKey().get())); + return NS_ERROR_NOT_AVAILABLE; + } + + // We need to make a new connection. If that is going to exceed the + // global connection limit then try and free up some room by closing + // an idle connection to another host. We know it won't select "ent" + // because we have already determined there are no idle connections + // to our destination + + if ((mNumIdleConns + mNumActiveConns + 1 >= mMaxConns) && mNumIdleConns) { + // If the global number of connections is preventing the opening of new + // connections to a host without idle connections, then close them + // regardless of their TTL. + auto iter = mCT.Iter(); + while (mNumIdleConns + mNumActiveConns + 1 >= mMaxConns && !iter.Done()) { + RefPtr entry = iter.Data(); + entry->CloseIdleConnections((mNumIdleConns + mNumActiveConns + 1) - + mMaxConns); + iter.Next(); + } + } + + if ((mNumIdleConns + mNumActiveConns + 1 >= mMaxConns) && mNumActiveConns && + gHttpHandler->IsSpdyEnabled()) { + // If the global number of connections is preventing the opening of new + // connections to a host without idle connections, then close any spdy + // ASAP. + for (auto iter = mCT.Iter(); !iter.Done(); iter.Next()) { + RefPtr entry = iter.Data(); + while (entry->MakeFirstActiveSpdyConnDontReuse()) { + // Stop on <= (particularly =) because this dontreuse + // causes async close. + if (mNumIdleConns + mNumActiveConns + 1 <= mMaxConns) { + goto outerLoopEnd; + } + } + } + outerLoopEnd:; + } + + if (AtActiveConnectionLimit(ent, trans->Caps())) + return NS_ERROR_NOT_AVAILABLE; + + nsresult rv = + CreateTransport(ent, trans, trans->Caps(), false, false, + trans->ClassOfService() & nsIClassOfService::UrgentStart, + true, pendingTransInfo); + if (NS_FAILED(rv)) { + /* hard failure */ + LOG( + ("nsHttpConnectionMgr::MakeNewConnection [ci = %s trans = %p] " + "CreateTransport() hard failure.\n", + ent->mConnInfo->HashKey().get(), trans)); + trans->Close(rv); + if (rv == NS_ERROR_NOT_AVAILABLE) rv = NS_ERROR_FAILURE; + return rv; + } + + return NS_OK; +} + +// returns OK if a connection is found for the transaction +// and the transaction is started. +// returns ERROR_NOT_AVAILABLE if no connection can be found and it +// should be queued until circumstances change +// returns other ERROR when transaction has a hard failure and should +// not remain in the pending queue +nsresult nsHttpConnectionMgr::TryDispatchTransaction( + ConnectionEntry* ent, bool onlyReusedConnection, + PendingTransactionInfo* pendingTransInfo) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + nsHttpTransaction* trans = pendingTransInfo->Transaction(); + + LOG( + ("nsHttpConnectionMgr::TryDispatchTransaction without conn " + "[trans=%p ci=%p ci=%s caps=%x tunnelprovider=%p " + "onlyreused=%d active=%zu idle=%zu]\n", + trans, ent->mConnInfo.get(), ent->mConnInfo->HashKey().get(), + uint32_t(trans->Caps()), trans->TunnelProvider(), onlyReusedConnection, + ent->ActiveConnsLength(), ent->IdleConnectionsLength())); + + uint32_t caps = trans->Caps(); + + // 0 - If this should use spdy then dispatch it post haste. + // 1 - If there is connection pressure then see if we can pipeline this on + // a connection of a matching type instead of using a new conn + // 2 - If there is an idle connection, use it! + // 3 - if class == reval or script and there is an open conn of that type + // then pipeline onto shortest pipeline of that class if limits allow + // 4 - If we aren't up against our connection limit, + // then open a new one + // 5 - Try a pipeline if we haven't already - this will be unusual because + // it implies a low connection pressure situation where + // MakeNewConnection() failed.. that is possible, but unlikely, due to + // global limits + // 6 - no connection is available - queue it + + RefPtr unusedSpdyPersistentConnection; + + // step 0 + // look for existing spdy connection - that's always best because it is + // essentially pipelining without head of line blocking + + RefPtr conn = GetH2orH3ActiveConn( + ent, (!gHttpHandler->IsSpdyEnabled() || (caps & NS_HTTP_DISALLOW_SPDY)), + (!gHttpHandler->IsHttp3Enabled() || (caps & NS_HTTP_DISALLOW_HTTP3))); + if (conn) { + if (trans->IsWebsocketUpgrade() && !conn->CanAcceptWebsocket()) { + // This is a websocket transaction and we already have a h2 connection + // that do not support websockets, we should disable h2 for this + // transaction. + trans->DisableSpdy(); + caps &= NS_HTTP_DISALLOW_SPDY; + } else { + if ((caps & NS_HTTP_ALLOW_KEEPALIVE) || + (caps & NS_HTTP_ALLOW_SPDY_WITHOUT_KEEPALIVE) || + !conn->IsExperienced()) { + LOG((" dispatch to spdy: [conn=%p]\n", conn.get())); + trans->RemoveDispatchedAsBlocking(); /* just in case */ + nsresult rv = DispatchTransaction(ent, trans, conn); + NS_ENSURE_SUCCESS(rv, rv); + return NS_OK; + } + unusedSpdyPersistentConnection = conn; + } + } + + // If this is not a blocking transaction and the request context for it is + // currently processing one or more blocking transactions then we + // need to just leave it in the queue until those are complete unless it is + // explicitly marked as unblocked. + if (!(caps & NS_HTTP_LOAD_AS_BLOCKING)) { + if (!(caps & NS_HTTP_LOAD_UNBLOCKED)) { + nsIRequestContext* requestContext = trans->RequestContext(); + if (requestContext) { + uint32_t blockers = 0; + if (NS_SUCCEEDED( + requestContext->GetBlockingTransactionCount(&blockers)) && + blockers) { + // need to wait for blockers to clear + LOG((" blocked by request context: [rc=%p trans=%p blockers=%d]\n", + requestContext, trans, blockers)); + return NS_ERROR_NOT_AVAILABLE; + } + } + } + } else { + // Mark the transaction and its load group as blocking right now to prevent + // other transactions from being reordered in the queue due to slow syns. + trans->DispatchedAsBlocking(); + } + + // step 1 + // If connection pressure, then we want to favor pipelining of any kind + // h1 pipelining has been removed + + // Subject most transactions at high parallelism to rate pacing. + // It will only be actually submitted to the + // token bucket once, and if possible it is granted admission synchronously. + // It is important to leave a transaction in the pending queue when blocked by + // pacing so it can be found on cancel if necessary. + // Transactions that cause blocking or bypass it (e.g. js/css) are not rate + // limited. + if (gHttpHandler->UseRequestTokenBucket()) { + // submit even whitelisted transactions to the token bucket though they will + // not be slowed by it + bool runNow = trans->TryToRunPacedRequest(); + if (!runNow) { + if ((mNumActiveConns - mNumSpdyHttp3ActiveConns) <= + gHttpHandler->RequestTokenBucketMinParallelism()) { + runNow = true; // white list it + } else if (caps & (NS_HTTP_LOAD_AS_BLOCKING | NS_HTTP_LOAD_UNBLOCKED)) { + runNow = true; // white list it + } + } + if (!runNow) { + LOG((" blocked due to rate pacing trans=%p\n", trans)); + return NS_ERROR_NOT_AVAILABLE; + } + } + + // step 2 + // consider an idle persistent connection + bool idleConnsAllUrgent = false; + if (caps & NS_HTTP_ALLOW_KEEPALIVE) { + nsresult rv = TryDispatchTransactionOnIdleConn(ent, pendingTransInfo, true, + &idleConnsAllUrgent); + if (NS_SUCCEEDED(rv)) { + LOG((" dispatched step 2 (idle) trans=%p\n", trans)); + return NS_OK; + } + } + + // step 3 + // consider pipelining scripts and revalidations + // h1 pipelining has been removed + + // Don't dispatch if this transaction is waiting for HTTPS RR. + // Note that this is only used in test currently. + if (caps & NS_HTTP_WAIT_HTTPSSVC_RESULT) { + return NS_ERROR_NOT_AVAILABLE; + } + + // step 4 + if (!onlyReusedConnection) { + nsresult rv = MakeNewConnection(ent, pendingTransInfo); + if (NS_SUCCEEDED(rv)) { + // this function returns NOT_AVAILABLE for asynchronous connects + LOG((" dispatched step 4 (async new conn) trans=%p\n", trans)); + return NS_ERROR_NOT_AVAILABLE; + } + + if (rv != NS_ERROR_NOT_AVAILABLE) { + // not available return codes should try next step as they are + // not hard errors. Other codes should stop now + LOG((" failed step 4 (%" PRIx32 ") trans=%p\n", + static_cast(rv), trans)); + return rv; + } + + // repeat step 2 when there are only idle connections and all are urgent, + // don't respect urgency so that non-urgent transaction will be allowed + // to dispatch on an urgent-start-only marked connection to avoid + // dispatch deadlocks + if (!(trans->ClassOfService() & nsIClassOfService::UrgentStart) && + idleConnsAllUrgent && + ent->ActiveConnsLength() < MaxPersistConnections(ent)) { + rv = TryDispatchTransactionOnIdleConn(ent, pendingTransInfo, false); + if (NS_SUCCEEDED(rv)) { + LOG((" dispatched step 2a (idle, reuse urgent) trans=%p\n", trans)); + return NS_OK; + } + } + } else if (trans->TunnelProvider() && + trans->TunnelProvider()->MaybeReTunnel(trans)) { + LOG((" sort of dispatched step 4a tunnel requeue trans=%p\n", trans)); + // the tunnel provider took responsibility for making a new tunnel + return NS_OK; + } + + // step 5 + // previously pipelined anything here if allowed but h1 pipelining has been + // removed + + // step 6 + if (unusedSpdyPersistentConnection) { + // to avoid deadlocks, we need to throw away this perfectly valid SPDY + // connection to make room for a new one that can service a no KEEPALIVE + // request + unusedSpdyPersistentConnection->DontReuse(); + } + + LOG((" not dispatched (queued) trans=%p\n", trans)); + return NS_ERROR_NOT_AVAILABLE; /* queue it */ +} + +nsresult nsHttpConnectionMgr::TryDispatchTransactionOnIdleConn( + ConnectionEntry* ent, PendingTransactionInfo* pendingTransInfo, + bool respectUrgency, bool* allUrgent) { + bool onlyUrgent = !!ent->IdleConnectionsLength(); + + nsHttpTransaction* trans = pendingTransInfo->Transaction(); + bool urgentTrans = trans->ClassOfService() & nsIClassOfService::UrgentStart; + + LOG( + ("nsHttpConnectionMgr::TryDispatchTransactionOnIdleConn, ent=%p, " + "trans=%p, urgent=%d", + ent, trans, urgentTrans)); + + RefPtr conn = + ent->GetIdleConnection(respectUrgency, urgentTrans, &onlyUrgent); + + if (allUrgent) { + *allUrgent = onlyUrgent; + } + + if (conn) { + // This will update the class of the connection to be the class of + // the transaction dispatched on it. + ent->InsertIntoActiveConns(conn); + nsresult rv = DispatchTransaction(ent, trans, conn); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; + } + + return NS_ERROR_NOT_AVAILABLE; +} + +nsresult nsHttpConnectionMgr::DispatchTransaction(ConnectionEntry* ent, + nsHttpTransaction* trans, + HttpConnectionBase* conn) { + uint32_t caps = trans->Caps(); + int32_t priority = trans->Priority(); + nsresult rv; + + LOG( + ("nsHttpConnectionMgr::DispatchTransaction " + "[ent-ci=%s %p trans=%p caps=%x conn=%p priority=%d isHttp2=%d " + "isHttp3=%d]\n", + ent->mConnInfo->HashKey().get(), ent, trans, caps, conn, priority, + conn->UsingSpdy(), conn->UsingHttp3())); + + // It is possible for a rate-paced transaction to be dispatched independent + // of the token bucket when the amount of parallelization has changed or + // when a muxed connection (e.g. h2) becomes available. + trans->CancelPacing(NS_OK); + + if (conn->UsingSpdy() || conn->UsingHttp3()) { + LOG( + ("Spdy Dispatch Transaction via Activate(). Transaction host = %s, " + "Connection host = %s\n", + trans->ConnectionInfo()->Origin(), conn->ConnectionInfo()->Origin())); + rv = conn->Activate(trans, caps, priority); + MOZ_ASSERT(NS_SUCCEEDED(rv), "SPDY Cannot Fail Dispatch"); + if (NS_SUCCEEDED(rv) && !trans->GetPendingTime().IsNull()) { + if (conn->UsingSpdy()) { + AccumulateTimeDelta(Telemetry::TRANSACTION_WAIT_TIME_SPDY, + trans->GetPendingTime(), TimeStamp::Now()); + } else { + AccumulateTimeDelta(Telemetry::TRANSACTION_WAIT_TIME_HTTP3, + trans->GetPendingTime(), TimeStamp::Now()); + } + trans->SetPendingTime(false); + } + return rv; + } + + MOZ_ASSERT(conn && !conn->Transaction(), + "DispatchTranaction() on non spdy active connection"); + + rv = DispatchAbstractTransaction(ent, trans, caps, conn, priority); + + if (NS_SUCCEEDED(rv) && !trans->GetPendingTime().IsNull()) { + AccumulateTimeDelta(Telemetry::TRANSACTION_WAIT_TIME_HTTP, + trans->GetPendingTime(), TimeStamp::Now()); + trans->SetPendingTime(false); + } + return rv; +} + +nsAHttpConnection* nsHttpConnectionMgr::MakeConnectionHandle( + HttpConnectionBase* aWrapped) { + return new ConnectionHandle(aWrapped); +} + +// Use this method for dispatching nsAHttpTransction's. It can only safely be +// used upon first use of a connection when NPN has not negotiated SPDY vs +// HTTP/1 yet as multiplexing onto an existing SPDY session requires a +// concrete nsHttpTransaction +nsresult nsHttpConnectionMgr::DispatchAbstractTransaction( + ConnectionEntry* ent, nsAHttpTransaction* aTrans, uint32_t caps, + HttpConnectionBase* conn, int32_t priority) { + MOZ_ASSERT(ent); + + nsresult rv; + MOZ_ASSERT(!conn->UsingSpdy(), + "Spdy Must Not Use DispatchAbstractTransaction"); + LOG( + ("nsHttpConnectionMgr::DispatchAbstractTransaction " + "[ci=%s trans=%p caps=%x conn=%p]\n", + ent->mConnInfo->HashKey().get(), aTrans, caps, conn)); + + RefPtr transaction(aTrans); + RefPtr handle = new ConnectionHandle(conn); + + // give the transaction the indirect reference to the connection. + transaction->SetConnection(handle); + + rv = conn->Activate(transaction, caps, priority); + if (NS_FAILED(rv)) { + LOG((" conn->Activate failed [rv=%" PRIx32 "]\n", + static_cast(rv))); + DebugOnly rv_remove = ent->RemoveActiveConnection(conn); + MOZ_ASSERT(NS_SUCCEEDED(rv_remove)); + + // sever back references to connection, and do so without triggering + // a call to ReclaimConnection ;-) + transaction->SetConnection(nullptr); + handle->Reset(); // destroy the connection + } + + return rv; +} + +void nsHttpConnectionMgr::ReportProxyTelemetry(ConnectionEntry* ent) { + enum { PROXY_NONE = 1, PROXY_HTTP = 2, PROXY_SOCKS = 3, PROXY_HTTPS = 4 }; + + if (!ent->mConnInfo->UsingProxy()) + Telemetry::Accumulate(Telemetry::HTTP_PROXY_TYPE, PROXY_NONE); + else if (ent->mConnInfo->UsingHttpsProxy()) + Telemetry::Accumulate(Telemetry::HTTP_PROXY_TYPE, PROXY_HTTPS); + else if (ent->mConnInfo->UsingHttpProxy()) + Telemetry::Accumulate(Telemetry::HTTP_PROXY_TYPE, PROXY_HTTP); + else + Telemetry::Accumulate(Telemetry::HTTP_PROXY_TYPE, PROXY_SOCKS); +} + +nsresult nsHttpConnectionMgr::ProcessNewTransaction(nsHttpTransaction* trans) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + // since "adds" and "cancels" are processed asynchronously and because + // various events might trigger an "add" directly on the socket thread, + // we must take care to avoid dispatching a transaction that has already + // been canceled (see bug 190001). + if (NS_FAILED(trans->Status())) { + LOG((" transaction was canceled... dropping event!\n")); + return NS_OK; + } + + trans->SetPendingTime(); + + RefPtr pushedStreamWrapper = + trans->GetPushedStream(); + if (pushedStreamWrapper) { + Http2PushedStream* pushedStream = pushedStreamWrapper->GetStream(); + if (pushedStream) { + LOG((" ProcessNewTransaction %p tied to h2 session push %p\n", trans, + pushedStream->Session())); + return pushedStream->Session()->AddStream(trans, trans->Priority(), false, + false, nullptr) + ? NS_OK + : NS_ERROR_UNEXPECTED; + } + } + + nsresult rv = NS_OK; + nsHttpConnectionInfo* ci = trans->ConnectionInfo(); + MOZ_ASSERT(ci); + MOZ_ASSERT(!ci->IsHttp3() || !(trans->Caps() & NS_HTTP_DISALLOW_HTTP3)); + + ConnectionEntry* ent = GetOrCreateConnectionEntry( + ci, !!trans->TunnelProvider(), trans->Caps() & NS_HTTP_DISALLOW_SPDY, + trans->Caps() & NS_HTTP_DISALLOW_HTTP3); + MOZ_ASSERT(ent); + + if (gHttpHandler->EchConfigEnabled()) { + ent->MaybeUpdateEchConfig(ci); + } + + ReportProxyTelemetry(ent); + + // Check if the transaction already has a sticky reference to a connection. + // If so, then we can just use it directly by transferring its reference + // to the new connection variable instead of searching for a new one + + nsAHttpConnection* wrappedConnection = trans->Connection(); + RefPtr conn; + RefPtr pendingTransInfo; + if (wrappedConnection) conn = wrappedConnection->TakeHttpConnection(); + + if (conn) { + MOZ_ASSERT(trans->Caps() & NS_HTTP_STICKY_CONNECTION); + LOG( + ("nsHttpConnectionMgr::ProcessNewTransaction trans=%p " + "sticky connection=%p\n", + trans, conn.get())); + + if (!ent->IsInActiveConns(conn)) { + LOG( + ("nsHttpConnectionMgr::ProcessNewTransaction trans=%p " + "sticky connection=%p needs to go on the active list\n", + trans, conn.get())); + + // make sure it isn't on the idle list - we expect this to be an + // unknown fresh connection + MOZ_ASSERT(!ent->IsInIdleConnections(conn)); + MOZ_ASSERT(!conn->IsExperienced()); + + ent->InsertIntoActiveConns(conn); // make it active + } + + trans->SetConnection(nullptr); + rv = DispatchTransaction(ent, trans, conn); + } else { + if (!ent->AllowHttp2()) { + trans->DisableSpdy(); + } + pendingTransInfo = new PendingTransactionInfo(trans); + rv = TryDispatchTransaction(ent, !!trans->TunnelProvider(), + pendingTransInfo); + } + + if (NS_SUCCEEDED(rv)) { + LOG((" ProcessNewTransaction Dispatch Immediately trans=%p\n", trans)); + return rv; + } + + if (rv == NS_ERROR_NOT_AVAILABLE) { + if (!pendingTransInfo) { + pendingTransInfo = new PendingTransactionInfo(trans); + } + + ent->InsertTransaction(pendingTransInfo); + return NS_OK; + } + + LOG((" ProcessNewTransaction Hard Error trans=%p rv=%" PRIx32 "\n", trans, + static_cast(rv))); + return rv; +} + +void nsHttpConnectionMgr::IncrementActiveConnCount() { + mNumActiveConns++; + ActivateTimeoutTick(); +} + +void nsHttpConnectionMgr::DecrementActiveConnCount(HttpConnectionBase* conn) { + mNumActiveConns--; + + RefPtr connTCP = do_QueryObject(conn); + if (!connTCP || connTCP->EverUsedSpdy()) mNumSpdyHttp3ActiveConns--; + ConditionallyStopTimeoutTick(); +} + +void nsHttpConnectionMgr::StartedConnect() { + mNumActiveConns++; + ActivateTimeoutTick(); // likely disabled by RecvdConnect() +} + +void nsHttpConnectionMgr::RecvdConnect() { + mNumActiveConns--; + ConditionallyStopTimeoutTick(); +} + +nsresult nsHttpConnectionMgr::CreateTransport( + ConnectionEntry* ent, nsAHttpTransaction* trans, uint32_t caps, + bool speculative, bool isFromPredictor, bool urgentStart, bool allow1918, + PendingTransactionInfo* pendingTransInfo) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + MOZ_ASSERT((speculative && !pendingTransInfo) || + (!speculative && pendingTransInfo)); + + RefPtr sock = new HalfOpenSocket( + ent, trans, caps, speculative, isFromPredictor, urgentStart); + + if (speculative) { + sock->SetAllow1918(allow1918); + } + // The socket stream holds the reference to the half open + // socket - so if the stream fails to init the half open + // will go away. + nsresult rv = sock->SetupPrimaryStreams(); + NS_ENSURE_SUCCESS(rv, rv); + + if (pendingTransInfo) { + DebugOnly claimed = pendingTransInfo->TryClaimingHalfOpen(sock); + MOZ_ASSERT(claimed); + } + + ent->InsertIntoHalfOpens(sock); + return NS_OK; +} + +void nsHttpConnectionMgr::DispatchSpdyPendingQ( + nsTArray>& pendingQ, ConnectionEntry* ent, + HttpConnectionBase* connH2, HttpConnectionBase* connH3) { + if (pendingQ.Length() == 0) { + return; + } + + nsTArray> leftovers; + uint32_t index; + // Dispatch all the transactions we can + for (index = 0; index < pendingQ.Length() && + ((connH3 && connH3->CanDirectlyActivate()) || + (connH2 && connH2->CanDirectlyActivate())); + ++index) { + PendingTransactionInfo* pendingTransInfo = pendingQ[index]; + + // We can not dispatch NS_HTTP_ALLOW_KEEPALIVE transactions. + if (!(pendingTransInfo->Transaction()->Caps() & NS_HTTP_ALLOW_KEEPALIVE)) { + leftovers.AppendElement(pendingTransInfo); + continue; + } + + // Try dispatching on HTTP3 first + HttpConnectionBase* conn = nullptr; + if (!(pendingTransInfo->Transaction()->Caps() & NS_HTTP_DISALLOW_HTTP3) && + connH3 && connH3->CanDirectlyActivate()) { + conn = connH3; + } else if (!(pendingTransInfo->Transaction()->Caps() & + NS_HTTP_DISALLOW_SPDY) && + connH2 && connH2->CanDirectlyActivate()) { + conn = connH2; + } else { + leftovers.AppendElement(pendingTransInfo); + continue; + } + + nsresult rv = + DispatchTransaction(ent, pendingTransInfo->Transaction(), conn); + if (NS_FAILED(rv)) { + // this cannot happen, but if due to some bug it does then + // close the transaction + MOZ_ASSERT(false, "Dispatch SPDY Transaction"); + LOG(("ProcessSpdyPendingQ Dispatch Transaction failed trans=%p\n", + pendingTransInfo->Transaction())); + pendingTransInfo->Transaction()->Close(rv); + } + } + + // Slurp up the rest of the pending queue into our leftovers bucket (we + // might have some left if conn->CanDirectlyActivate returned false) + for (; index < pendingQ.Length(); ++index) { + PendingTransactionInfo* pendingTransInfo = pendingQ[index]; + leftovers.AppendElement(pendingTransInfo); + } + + // Put the leftovers back in the pending queue and get rid of the + // transactions we dispatched + pendingQ = std::move(leftovers); +} + +// This function tries to dispatch the pending h2 or h3 transactions on +// the connection entry sent in as an argument. It will do so on the +// active h2 or h3 connection either in that same entry or from the +// coalescing hash table +void nsHttpConnectionMgr::ProcessSpdyPendingQ(ConnectionEntry* ent) { + // Look for one HTTP2 and one HTTP3 connection. + // We may have transactions that have NS_HTTP_DISALLOW_SPDY/HTTP3 set + // and we may need an alternative. + HttpConnectionBase* connH3 = GetH2orH3ActiveConn(ent, true, false); + HttpConnectionBase* connH2 = GetH2orH3ActiveConn(ent, false, true); + if ((!connH3 || !connH3->CanDirectlyActivate()) && + (!connH2 || !connH2->CanDirectlyActivate())) { + return; + } + + nsTArray> urgentQ; + ent->AppendPendingUrgentStartQ(urgentQ); + DispatchSpdyPendingQ(urgentQ, ent, connH2, connH3); + for (const auto& transactionInfo : Reversed(urgentQ)) { + ent->InsertTransaction(transactionInfo); + } + + if ((!connH3 || !connH3->CanDirectlyActivate()) && + (!connH2 || !connH2->CanDirectlyActivate())) { + return; + } + + nsTArray> pendingQ; + // XXX Get all transactions for SPDY currently. + ent->AppendPendingQForNonFocusedWindows(0, pendingQ); + DispatchSpdyPendingQ(pendingQ, ent, connH2, connH3); + + // Put the leftovers back in the pending queue. + for (const auto& transactionInfo : pendingQ) { + ent->InsertTransaction(transactionInfo); + } +} + +void nsHttpConnectionMgr::OnMsgProcessAllSpdyPendingQ(int32_t, ARefBase*) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG(("nsHttpConnectionMgr::OnMsgProcessAllSpdyPendingQ\n")); + for (auto iter = mCT.Iter(); !iter.Done(); iter.Next()) { + ProcessSpdyPendingQ(iter.Data().get()); + } +} + +// Given a connection entry, return an active h2 or h3 connection +// that can be directly activated or null. +HttpConnectionBase* nsHttpConnectionMgr::GetH2orH3ActiveConn( + ConnectionEntry* ent, bool aNoHttp2, bool aNoHttp3) { + if (aNoHttp2 && aNoHttp3) { + return nullptr; + } + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + MOZ_ASSERT(ent); + + // First look at ent. If protocol that ent provides is no forbidden, + // i.e. ent use HTTP3 and !aNoHttp3 or en uses HTTP over TCP and !aNoHttp2. + if ((!aNoHttp3 && ent->IsHttp3()) || (!aNoHttp2 && !ent->IsHttp3())) { + HttpConnectionBase* conn = ent->GetH2orH3ActiveConn(); + if (conn) { + return conn; + } + } + + nsHttpConnectionInfo* ci = ent->mConnInfo; + + // there was no active HTTP2/3 connection in the connection entry, but + // there might be one in the hash table for coalescing + HttpConnectionBase* existingConn = + FindCoalescableConnection(ent, false, aNoHttp2, aNoHttp3); + if (existingConn) { + LOG( + ("GetH2orH3ActiveConn() request for ent %p %s " + "found an active connection %p in the coalescing hashtable\n", + ent, ci->HashKey().get(), existingConn)); + return existingConn; + } + + LOG( + ("GetH2orH3ActiveConn() request for ent %p %s " + "did not find an active connection\n", + ent, ci->HashKey().get())); + return nullptr; +} + +//----------------------------------------------------------------------------- + +void nsHttpConnectionMgr::AbortAndCloseAllConnections(int32_t, ARefBase*) { + if (!OnSocketThread()) { + Unused << PostEvent(&nsHttpConnectionMgr::AbortAndCloseAllConnections); + return; + } + + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG(("nsHttpConnectionMgr::AbortAndCloseAllConnections\n")); + for (auto iter = mCT.Iter(); !iter.Done(); iter.Next()) { + RefPtr ent = iter.Data(); + + // Close all active connections. + ent->CloseActiveConnections(); + + // Close all idle connections. + ent->CloseIdleConnections(); + + // Close all pending transactions. + ent->CancelAllTransactions(NS_ERROR_ABORT); + + // Close all half open tcp connections. + ent->CloseAllHalfOpens(); + + MOZ_ASSERT(ent->HalfOpenFastOpenBackupsLength() == 0 && + !ent->mDoNotDestroy); + iter.Remove(); + } + + mActiveTransactions[false].Clear(); + mActiveTransactions[true].Clear(); +} + +void nsHttpConnectionMgr::OnMsgShutdown(int32_t, ARefBase* param) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG(("nsHttpConnectionMgr::OnMsgShutdown\n")); + + gHttpHandler->StopRequestTokenBucket(); + AbortAndCloseAllConnections(0, nullptr); + + // If all idle connections are removed we can stop pruning dead + // connections. + ConditionallyStopPruneDeadConnectionsTimer(); + + if (mTimeoutTick) { + mTimeoutTick->Cancel(); + mTimeoutTick = nullptr; + mTimeoutTickArmed = false; + } + if (mTimer) { + mTimer->Cancel(); + mTimer = nullptr; + } + if (mTrafficTimer) { + mTrafficTimer->Cancel(); + mTrafficTimer = nullptr; + } + DestroyThrottleTicker(); + + mCoalescingHash.Clear(); + + // signal shutdown complete + nsCOMPtr runnable = + new ConnEvent(this, &nsHttpConnectionMgr::OnMsgShutdownConfirm, 0, param); + NS_DispatchToMainThread(runnable); +} + +void nsHttpConnectionMgr::OnMsgShutdownConfirm(int32_t priority, + ARefBase* param) { + MOZ_ASSERT(NS_IsMainThread()); + LOG(("nsHttpConnectionMgr::OnMsgShutdownConfirm\n")); + + BoolWrapper* shutdown = static_cast(param); + shutdown->mBool = true; +} + +void nsHttpConnectionMgr::OnMsgNewTransaction(int32_t priority, + ARefBase* param) { + nsHttpTransaction* trans = static_cast(param); + + LOG(("nsHttpConnectionMgr::OnMsgNewTransaction [trans=%p]\n", trans)); + trans->SetPriority(priority); + nsresult rv = ProcessNewTransaction(trans); + if (NS_FAILED(rv)) trans->Close(rv); // for whatever its worth +} + +void nsHttpConnectionMgr::OnMsgNewTransactionWithStickyConn(int32_t priority, + ARefBase* param) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + NewTransactionData* data = static_cast(param); + LOG( + ("nsHttpConnectionMgr::OnMsgNewTransactionWithStickyConn " + "[trans=%p, transWithStickyConn=%p, conn=%p]\n", + data->mTrans.get(), data->mTransWithStickyConn.get(), + data->mTransWithStickyConn->Connection())); + + MOZ_ASSERT(data->mTransWithStickyConn && + data->mTransWithStickyConn->Caps() & NS_HTTP_STICKY_CONNECTION); + + data->mTrans->SetPriority(data->mPriority); + + RefPtr conn = data->mTransWithStickyConn->Connection(); + if (conn && conn->IsPersistent()) { + // This is so far a workaround to only reuse persistent + // connection for authentication retry. See bug 459620 comment 4 + // for details. + LOG((" Reuse connection [%p] for transaction [%p]", conn.get(), + data->mTrans.get())); + data->mTrans->SetConnection(conn); + } + + nsresult rv = ProcessNewTransaction(data->mTrans); + if (NS_FAILED(rv)) { + data->mTrans->Close(rv); // for whatever its worth + } +} + +void nsHttpConnectionMgr::OnMsgReschedTransaction(int32_t priority, + ARefBase* param) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG(("nsHttpConnectionMgr::OnMsgReschedTransaction [trans=%p]\n", param)); + + RefPtr trans = static_cast(param); + trans->SetPriority(priority); + + if (!trans->ConnectionInfo()) { + return; + } + ConnectionEntry* ent = mCT.GetWeak(trans->ConnectionInfo()->HashKey()); + + if (ent) { + ent->ReschedTransaction(trans); + } +} + +void nsHttpConnectionMgr::OnMsgUpdateClassOfServiceOnTransaction( + int32_t arg, ARefBase* param) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG( + ("nsHttpConnectionMgr::OnMsgUpdateClassOfServiceOnTransaction " + "[trans=%p]\n", + param)); + + uint32_t cos = static_cast(arg); + nsHttpTransaction* trans = static_cast(param); + + uint32_t previous = trans->ClassOfService(); + trans->SetClassOfService(cos); + + if ((previous ^ cos) & (NS_HTTP_LOAD_AS_BLOCKING | NS_HTTP_LOAD_UNBLOCKED)) { + Unused << RescheduleTransaction(trans, trans->Priority()); + } +} + +void nsHttpConnectionMgr::OnMsgCancelTransaction(int32_t reason, + ARefBase* param) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG(("nsHttpConnectionMgr::OnMsgCancelTransaction [trans=%p]\n", param)); + + nsresult closeCode = static_cast(reason); + + // caller holds a ref to param/trans on stack + nsHttpTransaction* trans = static_cast(param); + + // + // if the transaction owns a connection and the transaction is not done, + // then ask the connection to close the transaction. otherwise, close the + // transaction directly (removing it from the pending queue first). + // + RefPtr conn(trans->Connection()); + if (conn && !trans->IsDone()) { + conn->CloseTransaction(trans, closeCode); + } else { + ConnectionEntry* ent = nullptr; + if (trans->ConnectionInfo()) { + ent = mCT.GetWeak(trans->ConnectionInfo()->HashKey()); + } + if (ent && ent->RemoveTransFromPendingQ(trans)) { + LOG( + ("nsHttpConnectionMgr::OnMsgCancelTransaction [trans=%p]" + " removed from pending queue\n", + trans)); + } + + trans->Close(closeCode); + + // Cancel is a pretty strong signal that things might be hanging + // so we want to cancel any null transactions related to this connection + // entry. They are just optimizations, but they aren't hooked up to + // anything that might get canceled from the rest of gecko, so best + // to assume that's what was meant by the cancel we did receive if + // it only applied to something in the queue. + if (ent) { + ent->CloseAllActiveConnsWithNullTransactcion(closeCode); + } + } +} + +void nsHttpConnectionMgr::OnMsgProcessPendingQ(int32_t, ARefBase* param) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + nsHttpConnectionInfo* ci = static_cast(param); + + if (!ci) { + LOG(("nsHttpConnectionMgr::OnMsgProcessPendingQ [ci=nullptr]\n")); + // Try and dispatch everything + for (auto iter = mCT.Iter(); !iter.Done(); iter.Next()) { + Unused << ProcessPendingQForEntry(iter.Data().get(), true); + } + return; + } + + LOG(("nsHttpConnectionMgr::OnMsgProcessPendingQ [ci=%s]\n", + ci->HashKey().get())); + + // start by processing the queue identified by the given connection info. + ConnectionEntry* ent = mCT.GetWeak(ci->HashKey()); + if (!(ent && ProcessPendingQForEntry(ent, false))) { + // if we reach here, it means that we couldn't dispatch a transaction + // for the specified connection info. walk the connection table... + for (auto iter = mCT.Iter(); !iter.Done(); iter.Next()) { + if (ProcessPendingQForEntry(iter.Data().get(), false)) { + break; + } + } + } +} + +nsresult nsHttpConnectionMgr::CancelTransactions(nsHttpConnectionInfo* ci, + nsresult code) { + LOG(("nsHttpConnectionMgr::CancelTransactions %s\n", ci->HashKey().get())); + + int32_t intReason = static_cast(code); + return PostEvent(&nsHttpConnectionMgr::OnMsgCancelTransactions, intReason, + ci); +} + +void nsHttpConnectionMgr::OnMsgCancelTransactions(int32_t code, + ARefBase* param) { + nsresult reason = static_cast(code); + nsHttpConnectionInfo* ci = static_cast(param); + ConnectionEntry* ent = mCT.GetWeak(ci->HashKey()); + LOG(("nsHttpConnectionMgr::OnMsgCancelTransactions %s %p\n", + ci->HashKey().get(), ent)); + if (ent) { + ent->CancelAllTransactions(reason); + } +} + +void nsHttpConnectionMgr::OnMsgPruneDeadConnections(int32_t, ARefBase*) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG(("nsHttpConnectionMgr::OnMsgPruneDeadConnections\n")); + + // Reset mTimeOfNextWakeUp so that we can find a new shortest value. + mTimeOfNextWakeUp = UINT64_MAX; + + // check canreuse() for all idle connections plus any active connections on + // connection entries that are using spdy. + if (mNumIdleConns || (mNumActiveConns && gHttpHandler->IsSpdyEnabled())) { + for (auto iter = mCT.Iter(); !iter.Done(); iter.Next()) { + RefPtr ent = iter.Data(); + + LOG((" pruning [ci=%s]\n", ent->mConnInfo->HashKey().get())); + + // Find out how long it will take for next idle connection to not + // be reusable anymore. + uint32_t timeToNextExpire = ent->PruneDeadConnections(); + + // If time to next expire found is shorter than time to next + // wake-up, we need to change the time for next wake-up. + if (timeToNextExpire != UINT32_MAX) { + uint32_t now = NowInSeconds(); + uint64_t timeOfNextExpire = now + timeToNextExpire; + // If pruning of dead connections is not already scheduled to + // happen or time found for next connection to expire is is + // before mTimeOfNextWakeUp, we need to schedule the pruning to + // happen after timeToNextExpire. + if (!mTimer || timeOfNextExpire < mTimeOfNextWakeUp) { + PruneDeadConnectionsAfter(timeToNextExpire); + } + } else { + ConditionallyStopPruneDeadConnectionsTimer(); + } + + ent->RemoveEmptyPendingQ(); + + // If this entry is empty, we have too many entries busy then + // we can clean it up and restart + if (mCT.Count() > 125 && ent->IdleConnectionsLength() == 0 && + ent->ActiveConnsLength() == 0 && ent->HalfOpensLength() == 0 && + ent->PendingQueueLength() == 0 && + ent->UrgentStartQueueLength() == 0 && + ent->HalfOpenFastOpenBackupsLength() == 0 && !ent->mDoNotDestroy && + (!ent->mUsingSpdy || mCT.Count() > 300)) { + LOG((" removing empty connection entry\n")); + iter.Remove(); + continue; + } + + // Otherwise use this opportunity to compact our arrays... + ent->Compact(); + } + } +} + +void nsHttpConnectionMgr::OnMsgPruneNoTraffic(int32_t, ARefBase*) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG(("nsHttpConnectionMgr::OnMsgPruneNoTraffic\n")); + + // Prune connections without traffic + for (auto iter = mCT.Iter(); !iter.Done(); iter.Next()) { + // Close the connections with no registered traffic. + RefPtr ent = iter.Data(); + ent->PruneNoTraffic(); + } + + mPruningNoTraffic = false; // not pruning anymore +} + +void nsHttpConnectionMgr::OnMsgVerifyTraffic(int32_t, ARefBase*) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG(("nsHttpConnectionMgr::OnMsgVerifyTraffic\n")); + + if (mPruningNoTraffic) { + // Called in the time gap when the timeout to prune notraffic + // connections has triggered but the pruning hasn't happened yet. + return; + } + + // Mark connections for traffic verification + for (auto iter = mCT.Iter(); !iter.Done(); iter.Next()) { + iter.Data()->VerifyTraffic(); + } + + // If the timer is already there. we just re-init it + if (!mTrafficTimer) { + mTrafficTimer = NS_NewTimer(); + } + + // failure to create a timer is not a fatal error, but dead + // connections will not be cleaned up as nicely + if (mTrafficTimer) { + // Give active connections time to get more traffic before killing + // them off. Default: 5000 milliseconds + mTrafficTimer->Init(this, gHttpHandler->NetworkChangedTimeout(), + nsITimer::TYPE_ONE_SHOT); + } else { + NS_WARNING("failed to create timer for VerifyTraffic!"); + } +} + +void nsHttpConnectionMgr::OnMsgDoShiftReloadConnectionCleanup(int32_t, + ARefBase* param) { + LOG(("nsHttpConnectionMgr::OnMsgDoShiftReloadConnectionCleanup\n")); + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + nsHttpConnectionInfo* ci = static_cast(param); + + for (auto iter = mCT.Iter(); !iter.Done(); iter.Next()) { + iter.Data()->ClosePersistentConnections(); + } + + if (ci) ResetIPFamilyPreference(ci); +} + +void nsHttpConnectionMgr::OnMsgReclaimConnection(HttpConnectionBase* conn) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + // + // 1) remove the connection from the active list + // 2) if keep-alive, add connection to idle list + // 3) post event to process the pending transaction queue + // + + MOZ_ASSERT(conn); + ConnectionEntry* ent = conn->ConnectionInfo() + ? mCT.GetWeak(conn->ConnectionInfo()->HashKey()) + : nullptr; + + if (!ent) { + // this can happen if the connection is made outside of the + // connection manager and is being "reclaimed" for use with + // future transactions. HTTP/2 tunnels work like this. + ent = + GetOrCreateConnectionEntry(conn->ConnectionInfo(), true, false, false); + LOG( + ("nsHttpConnectionMgr::OnMsgReclaimConnection conn %p " + "forced new hash entry %s\n", + conn, conn->ConnectionInfo()->HashKey().get())); + } + + MOZ_ASSERT(ent); + RefPtr ci(ent->mConnInfo); + + LOG(("nsHttpConnectionMgr::OnMsgReclaimConnection [ent=%p conn=%p]\n", ent, + conn)); + + // If the connection is in the active list, remove that entry + // and the reference held by the mActiveConns list. + // This is never the final reference on conn as the event context + // is also holding one that is released at the end of this function. + + RefPtr connTCP = do_QueryObject(conn); + if (!connTCP || connTCP->EverUsedSpdy()) { + // Spdyand Http3 connections aren't reused in the traditional HTTP way in + // the idleconns list, they are actively multplexed as active + // conns. Even when they have 0 transactions on them they are + // considered active connections. So when one is reclaimed it + // is really complete and is meant to be shut down and not + // reused. + conn->DontReuse(); + } + + // a connection that still holds a reference to a transaction was + // not closed naturally (i.e. it was reset or aborted) and is + // therefore not something that should be reused. + if (conn->Transaction()) { + conn->DontReuse(); + } + + if (NS_SUCCEEDED(ent->RemoveActiveConnection(conn))) { + } else if (!connTCP || connTCP->EverUsedSpdy()) { + LOG(("HttpConnectionBase %p not found in its connection entry, try ^anon", + conn)); + // repeat for flipped anon flag as we share connection entries for spdy + // connections. + RefPtr anonInvertedCI(ci->Clone()); + anonInvertedCI->SetAnonymous(!ci->GetAnonymous()); + + ConnectionEntry* ent = mCT.GetWeak(anonInvertedCI->HashKey()); + if (ent) { + if (NS_SUCCEEDED(ent->RemoveActiveConnection(conn))) { + } else { + LOG( + ("HttpConnectionBase %p could not be removed from its entry's " + "active list", + conn)); + } + } + } + + if (connTCP && connTCP->CanReuse()) { + LOG((" adding connection to idle list\n")); + // Keep The idle connection list sorted with the connections that + // have moved the largest data pipelines at the front because these + // connections have the largest cwnds on the server. + + // The linear search is ok here because the number of idleconns + // in a single entry is generally limited to a small number (i.e. 6) + + ent->InsertIntoIdleConnections(connTCP); + } else { + LOG((" connection cannot be reused; closing connection\n")); + conn->Close(NS_ERROR_ABORT); + } + + OnMsgProcessPendingQ(0, ci); +} + +void nsHttpConnectionMgr::OnMsgCompleteUpgrade(int32_t, ARefBase* param) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + nsresult rv = NS_OK; + nsCompleteUpgradeData* data = static_cast(param); + MOZ_ASSERT(data->mTrans && data->mTrans->Caps() & NS_HTTP_STICKY_CONNECTION); + + RefPtr conn(data->mTrans->Connection()); + LOG( + ("nsHttpConnectionMgr::OnMsgCompleteUpgrade " + "conn=%p listener=%p wrapped=%d\n", + conn.get(), data->mUpgradeListener.get(), data->mJsWrapped)); + + if (!conn) { + // Delay any error reporting to happen in transportAvailableFunc + rv = NS_ERROR_UNEXPECTED; + } else { + MOZ_ASSERT(!data->mSocketTransport); + rv = conn->TakeTransport(getter_AddRefs(data->mSocketTransport), + getter_AddRefs(data->mSocketIn), + getter_AddRefs(data->mSocketOut)); + + if (NS_FAILED(rv)) { + LOG((" conn->TakeTransport failed with %" PRIx32, + static_cast(rv))); + } + } + + RefPtr upgradeData(data); + auto transportAvailableFunc = [upgradeData{std::move(upgradeData)}, + aRv(rv)]() { + // Handle any potential previous errors first + // and call OnUpgradeFailed if necessary. + nsresult rv = aRv; + + if (NS_FAILED(rv)) { + rv = upgradeData->mUpgradeListener->OnUpgradeFailed(rv); + if (NS_FAILED(rv)) { + LOG( + ("nsHttpConnectionMgr::OnMsgCompleteUpgrade OnUpgradeFailed failed." + " listener=%p\n", + upgradeData->mUpgradeListener.get())); + } + return; + } + + rv = upgradeData->mUpgradeListener->OnTransportAvailable( + upgradeData->mSocketTransport, upgradeData->mSocketIn, + upgradeData->mSocketOut); + if (NS_FAILED(rv)) { + LOG( + ("nsHttpConnectionMgr::OnMsgCompleteUpgrade OnTransportAvailable " + "failed. listener=%p\n", + upgradeData->mUpgradeListener.get())); + } + }; + + if (data->mJsWrapped) { + LOG( + ("nsHttpConnectionMgr::OnMsgCompleteUpgrade " + "conn=%p listener=%p wrapped=%d pass to main thread\n", + conn.get(), data->mUpgradeListener.get(), data->mJsWrapped)); + NS_DispatchToMainThread( + NS_NewRunnableFunction("net::nsHttpConnectionMgr::OnMsgCompleteUpgrade", + transportAvailableFunc)); + } else { + transportAvailableFunc(); + } +} + +void nsHttpConnectionMgr::OnMsgUpdateParam(int32_t inParam, ARefBase*) { + uint32_t param = static_cast(inParam); + uint16_t name = ((param)&0xFFFF0000) >> 16; + uint16_t value = param & 0x0000FFFF; + + switch (name) { + case MAX_CONNECTIONS: + mMaxConns = value; + break; + case MAX_URGENT_START_Q: + mMaxUrgentExcessiveConns = value; + break; + case MAX_PERSISTENT_CONNECTIONS_PER_HOST: + mMaxPersistConnsPerHost = value; + break; + case MAX_PERSISTENT_CONNECTIONS_PER_PROXY: + mMaxPersistConnsPerProxy = value; + break; + case MAX_REQUEST_DELAY: + mMaxRequestDelay = value; + break; + case THROTTLING_ENABLED: + SetThrottlingEnabled(!!value); + break; + case THROTTLING_SUSPEND_FOR: + mThrottleSuspendFor = value; + break; + case THROTTLING_RESUME_FOR: + mThrottleResumeFor = value; + break; + case THROTTLING_READ_LIMIT: + mThrottleReadLimit = value; + break; + case THROTTLING_READ_INTERVAL: + mThrottleReadInterval = value; + break; + case THROTTLING_HOLD_TIME: + mThrottleHoldTime = value; + break; + case THROTTLING_MAX_TIME: + mThrottleMaxTime = TimeDuration::FromMilliseconds(value); + break; + case PROXY_BE_CONSERVATIVE: + mBeConservativeForProxy = !!value; + break; + default: + MOZ_ASSERT_UNREACHABLE("unexpected parameter name"); + } +} + +// Read Timeout Tick handlers + +void nsHttpConnectionMgr::ActivateTimeoutTick() { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG( + ("nsHttpConnectionMgr::ActivateTimeoutTick() " + "this=%p mTimeoutTick=%p\n", + this, mTimeoutTick.get())); + + // The timer tick should be enabled if it is not already pending. + // Upon running the tick will rearm itself if there are active + // connections available. + + if (mTimeoutTick && mTimeoutTickArmed) { + // make sure we get one iteration on a quick tick + if (mTimeoutTickNext > 1) { + mTimeoutTickNext = 1; + mTimeoutTick->SetDelay(1000); + } + return; + } + + if (!mTimeoutTick) { + mTimeoutTick = NS_NewTimer(); + if (!mTimeoutTick) { + NS_WARNING("failed to create timer for http timeout management"); + return; + } + mTimeoutTick->SetTarget(mSocketThreadTarget); + } + + MOZ_ASSERT(!mTimeoutTickArmed, "timer tick armed"); + mTimeoutTickArmed = true; + mTimeoutTick->Init(this, 1000, nsITimer::TYPE_REPEATING_SLACK); +} + +class UINT64Wrapper : public ARefBase { + public: + explicit UINT64Wrapper(uint64_t aUint64) : mUint64(aUint64) {} + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(UINT64Wrapper, override) + + uint64_t GetValue() { return mUint64; } + + private: + uint64_t mUint64; + virtual ~UINT64Wrapper() = default; +}; + +nsresult nsHttpConnectionMgr::UpdateCurrentTopLevelOuterContentWindowId( + uint64_t aWindowId) { + RefPtr windowIdWrapper = new UINT64Wrapper(aWindowId); + return PostEvent( + &nsHttpConnectionMgr::OnMsgUpdateCurrentTopLevelOuterContentWindowId, 0, + windowIdWrapper); +} + +void nsHttpConnectionMgr::SetThrottlingEnabled(bool aEnable) { + LOG(("nsHttpConnectionMgr::SetThrottlingEnabled enable=%d", aEnable)); + + mThrottleEnabled = aEnable; + + if (mThrottleEnabled) { + EnsureThrottleTickerIfNeeded(); + } else { + DestroyThrottleTicker(); + ResumeReadOf(mActiveTransactions[false]); + ResumeReadOf(mActiveTransactions[true]); + } +} + +bool nsHttpConnectionMgr::InThrottlingTimeWindow() { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + if (mThrottlingWindowEndsAt.IsNull()) { + return true; + } + return TimeStamp::NowLoRes() <= mThrottlingWindowEndsAt; +} + +void nsHttpConnectionMgr::TouchThrottlingTimeWindow(bool aEnsureTicker) { + LOG(("nsHttpConnectionMgr::TouchThrottlingTimeWindow")); + + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + mThrottlingWindowEndsAt = TimeStamp::NowLoRes() + mThrottleMaxTime; + + if (!mThrottleTicker && MOZ_LIKELY(aEnsureTicker) && + MOZ_LIKELY(mThrottleEnabled)) { + EnsureThrottleTickerIfNeeded(); + } +} + +void nsHttpConnectionMgr::LogActiveTransactions(char operation) { + if (!LOG_ENABLED()) { + return; + } + + nsTArray>* trs = nullptr; + uint32_t au, at, bu = 0, bt = 0; + + trs = mActiveTransactions[false].Get(mCurrentTopLevelOuterContentWindowId); + au = trs ? trs->Length() : 0; + trs = mActiveTransactions[true].Get(mCurrentTopLevelOuterContentWindowId); + at = trs ? trs->Length() : 0; + + for (auto iter = mActiveTransactions[false].Iter(); !iter.Done(); + iter.Next()) { + bu += iter.UserData()->Length(); + } + bu -= au; + for (auto iter = mActiveTransactions[true].Iter(); !iter.Done(); + iter.Next()) { + bt += iter.UserData()->Length(); + } + bt -= at; + + // Shows counts of: + // - unthrottled transaction for the active tab + // - throttled transaction for the active tab + // - unthrottled transaction for background tabs + // - throttled transaction for background tabs + LOG(("Active transactions %c[%u,%u,%u,%u]", operation, au, at, bu, bt)); +} + +void nsHttpConnectionMgr::AddActiveTransaction(nsHttpTransaction* aTrans) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + uint64_t tabId = aTrans->TopLevelOuterContentWindowId(); + bool throttled = aTrans->EligibleForThrottling(); + + nsTArray>* transactions = + mActiveTransactions[throttled].LookupOrAdd(tabId); + + MOZ_ASSERT(!transactions->Contains(aTrans)); + + transactions->AppendElement(aTrans); + + LOG(("nsHttpConnectionMgr::AddActiveTransaction t=%p tabid=%" PRIx64 + "(%d) thr=%d", + aTrans, tabId, tabId == mCurrentTopLevelOuterContentWindowId, + throttled)); + LogActiveTransactions('+'); + + if (tabId == mCurrentTopLevelOuterContentWindowId) { + mActiveTabTransactionsExist = true; + if (!throttled) { + mActiveTabUnthrottledTransactionsExist = true; + } + } + + // Shift the throttling window to the future (actually, makes sure + // that throttling will engage when there is anything to throttle.) + // The |false| argument means we don't need this call to ensure + // the ticker, since we do it just below. Calling + // EnsureThrottleTickerIfNeeded directly does a bit more than call + // from inside of TouchThrottlingTimeWindow. + TouchThrottlingTimeWindow(false); + + if (!mThrottleEnabled) { + return; + } + + EnsureThrottleTickerIfNeeded(); +} + +void nsHttpConnectionMgr::RemoveActiveTransaction( + nsHttpTransaction* aTrans, Maybe const& aOverride) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + uint64_t tabId = aTrans->TopLevelOuterContentWindowId(); + bool forActiveTab = tabId == mCurrentTopLevelOuterContentWindowId; + bool throttled = aOverride.valueOr(aTrans->EligibleForThrottling()); + + nsTArray>* transactions = + mActiveTransactions[throttled].Get(tabId); + + if (!transactions || !transactions->RemoveElement(aTrans)) { + // Was not tracked as active, probably just ignore. + return; + } + + LOG(("nsHttpConnectionMgr::RemoveActiveTransaction t=%p tabid=%" PRIx64 + "(%d) thr=%d", + aTrans, tabId, forActiveTab, throttled)); + + if (!transactions->IsEmpty()) { + // There are still transactions of the type, hence nothing in the throttling + // conditions has changed and we don't need to update "Exists" caches nor we + // need to wake any now throttled transactions. + LogActiveTransactions('-'); + return; + } + + // To optimize the following logic, always remove the entry when the array is + // empty. + mActiveTransactions[throttled].Remove(tabId); + LogActiveTransactions('-'); + + if (forActiveTab) { + // Update caches of the active tab transaction existence, since it's now + // affected + if (!throttled) { + mActiveTabUnthrottledTransactionsExist = false; + } + if (mActiveTabTransactionsExist) { + mActiveTabTransactionsExist = + mActiveTransactions[!throttled].Contains(tabId); + } + } + + if (!mThrottleEnabled) { + return; + } + + bool unthrottledExist = !mActiveTransactions[false].IsEmpty(); + bool throttledExist = !mActiveTransactions[true].IsEmpty(); + + if (!unthrottledExist && !throttledExist) { + // Nothing active globally, just get rid of the timer completely and we are + // done. + MOZ_ASSERT(!mActiveTabUnthrottledTransactionsExist); + MOZ_ASSERT(!mActiveTabTransactionsExist); + + DestroyThrottleTicker(); + return; + } + + if (mThrottleVersion == 1) { + if (!mThrottlingInhibitsReading) { + // There is then nothing to wake up. Affected transactions will not be + // put to sleep automatically on next tick. + LOG((" reading not currently inhibited")); + return; + } + } + + if (mActiveTabUnthrottledTransactionsExist) { + // There are still unthrottled transactions for the active tab, hence the + // state is unaffected and we don't need to do anything (nothing to wake). + LOG((" there are unthrottled for the active tab")); + return; + } + + if (mActiveTabTransactionsExist) { + // There are only trottled transactions for the active tab. + // If the last transaction we just removed was a non-throttled for the + // active tab we can wake the throttled transactions for the active tab. + if (forActiveTab && !throttled) { + LOG((" resuming throttled for active tab")); + ResumeReadOf( + mActiveTransactions[true].Get(mCurrentTopLevelOuterContentWindowId)); + } + return; + } + + if (!unthrottledExist) { + // There are no unthrottled transactions for any tab. Resume all throttled, + // all are only for background tabs. + LOG((" delay resuming throttled for background tabs")); + DelayedResumeBackgroundThrottledTransactions(); + return; + } + + if (forActiveTab) { + // Removing the last transaction for the active tab frees up the unthrottled + // background tabs transactions. + LOG((" delay resuming unthrottled for background tabs")); + DelayedResumeBackgroundThrottledTransactions(); + return; + } + + LOG((" not resuming anything")); +} + +void nsHttpConnectionMgr::UpdateActiveTransaction(nsHttpTransaction* aTrans) { + LOG(("nsHttpConnectionMgr::UpdateActiveTransaction ENTER t=%p", aTrans)); + + // First remove then add. In case of a download that is the only active + // transaction and has just been marked as download (goes unthrottled to + // throttled), adding first would cause it to be throttled for first few + // milliseconds - becuause it would appear as if there were both throttled + // and unthrottled transactions at the time. + + Maybe reversed; + reversed.emplace(!aTrans->EligibleForThrottling()); + RemoveActiveTransaction(aTrans, reversed); + + AddActiveTransaction(aTrans); + + LOG(("nsHttpConnectionMgr::UpdateActiveTransaction EXIT t=%p", aTrans)); +} + +bool nsHttpConnectionMgr::ShouldThrottle(nsHttpTransaction* aTrans) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + LOG(("nsHttpConnectionMgr::ShouldThrottle trans=%p", aTrans)); + + if (mThrottleVersion == 1) { + if (!mThrottlingInhibitsReading || !mThrottleEnabled) { + return false; + } + } else { + if (!mThrottleEnabled) { + return false; + } + } + + uint64_t tabId = aTrans->TopLevelOuterContentWindowId(); + bool forActiveTab = tabId == mCurrentTopLevelOuterContentWindowId; + bool throttled = aTrans->EligibleForThrottling(); + + bool stop = [=]() { + if (mActiveTabTransactionsExist) { + if (!tabId) { + // Chrome initiated and unidentified transactions just respect + // their throttle flag, when something for the active tab is happening. + // This also includes downloads. + LOG((" active tab loads, trans is tab-less, throttled=%d", throttled)); + return throttled; + } + if (!forActiveTab) { + // This is a background tab request, we want them to always throttle + // when there are transactions running for the ative tab. + LOG((" active tab loads, trans not of the active tab")); + return true; + } + + if (mActiveTabUnthrottledTransactionsExist) { + // Unthrottled transactions for the active tab take precedence + LOG((" active tab loads unthrottled, trans throttled=%d", throttled)); + return throttled; + } + + LOG((" trans for active tab, don't throttle")); + return false; + } + + MOZ_ASSERT(!forActiveTab); + + if (!mActiveTransactions[false].IsEmpty()) { + // This means there are unthrottled active transactions for background + // tabs. If we are here, there can't be any transactions for the active + // tab. (If there is no transaction for a tab id, there is no entry for it + // in the hashtable.) + LOG((" backround tab(s) load unthrottled, trans throttled=%d", + throttled)); + return throttled; + } + + // There are only unthrottled transactions for background tabs: don't + // throttle. + LOG((" backround tab(s) load throttled, don't throttle")); + return false; + }(); + + if (forActiveTab && !stop) { + // This is an active-tab transaction and is allowed to read. Hence, + // prolong the throttle time window to make sure all 'lower-decks' + // transactions will actually throttle. + TouchThrottlingTimeWindow(); + return false; + } + + // Only stop reading when in the configured throttle max-time (aka time + // window). This window is prolonged (restarted) by a call to + // TouchThrottlingTimeWindow called on new transaction activation or on + // receive of response bytes of an active tab transaction. + bool inWindow = InThrottlingTimeWindow(); + + LOG((" stop=%d, in-window=%d, delayed-bck-timer=%d", stop, inWindow, + !!mDelayedResumeReadTimer)); + + if (!forActiveTab) { + // If the delayed background resume timer exists, background transactions + // are scheduled to be woken after a delay, hence leave them throttled. + inWindow = inWindow || mDelayedResumeReadTimer; + } + + return stop && inWindow; +} + +bool nsHttpConnectionMgr::IsConnEntryUnderPressure( + nsHttpConnectionInfo* connInfo) { + ConnectionEntry* ent = mCT.GetWeak(connInfo->HashKey()); + if (!ent) { + // No entry, no pressure. + return false; + } + + return ent->PendingQueueLengthForWindow( + mCurrentTopLevelOuterContentWindowId) > 0; +} + +bool nsHttpConnectionMgr::IsThrottleTickerNeeded() { + LOG(("nsHttpConnectionMgr::IsThrottleTickerNeeded")); + + if (mActiveTabUnthrottledTransactionsExist && + mActiveTransactions[false].Count() > 1) { + LOG((" there are unthrottled transactions for both active and bck")); + return true; + } + + if (mActiveTabTransactionsExist && mActiveTransactions[true].Count() > 1) { + LOG((" there are throttled transactions for both active and bck")); + return true; + } + + if (!mActiveTransactions[true].IsEmpty() && + !mActiveTransactions[false].IsEmpty()) { + LOG((" there are both throttled and unthrottled transactions")); + return true; + } + + LOG((" nothing to throttle")); + return false; +} + +void nsHttpConnectionMgr::EnsureThrottleTickerIfNeeded() { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + LOG(("nsHttpConnectionMgr::EnsureThrottleTickerIfNeeded")); + if (!IsThrottleTickerNeeded()) { + return; + } + + // There is a new demand to throttle, hence unschedule delayed resume + // of background throttled transastions. + CancelDelayedResumeBackgroundThrottledTransactions(); + + if (mThrottleTicker) { + return; + } + + mThrottleTicker = NS_NewTimer(); + if (mThrottleTicker) { + if (mThrottleVersion == 1) { + MOZ_ASSERT(!mThrottlingInhibitsReading); + + mThrottleTicker->Init(this, mThrottleSuspendFor, nsITimer::TYPE_ONE_SHOT); + mThrottlingInhibitsReading = true; + } else { + mThrottleTicker->Init(this, mThrottleReadInterval, + nsITimer::TYPE_ONE_SHOT); + } + } + + LogActiveTransactions('^'); +} + +void nsHttpConnectionMgr::DestroyThrottleTicker() { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + // Nothing to throttle, hence no need for this timer anymore. + CancelDelayedResumeBackgroundThrottledTransactions(); + + MOZ_ASSERT(!mThrottleEnabled || !IsThrottleTickerNeeded()); + + if (!mThrottleTicker) { + return; + } + + LOG(("nsHttpConnectionMgr::DestroyThrottleTicker")); + mThrottleTicker->Cancel(); + mThrottleTicker = nullptr; + + if (mThrottleVersion == 1) { + mThrottlingInhibitsReading = false; + } + + LogActiveTransactions('v'); +} + +void nsHttpConnectionMgr::ThrottlerTick() { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + if (mThrottleVersion == 1) { + mThrottlingInhibitsReading = !mThrottlingInhibitsReading; + + LOG(("nsHttpConnectionMgr::ThrottlerTick inhibit=%d", + mThrottlingInhibitsReading)); + + // If there are only background transactions to be woken after a delay, keep + // the ticker so that we woke them only for the resume-for interval and then + // throttle them again until the background-resume delay passes. + if (!mThrottlingInhibitsReading && !mDelayedResumeReadTimer && + (!IsThrottleTickerNeeded() || !InThrottlingTimeWindow())) { + LOG((" last tick")); + mThrottleTicker = nullptr; + } + + if (mThrottlingInhibitsReading) { + if (mThrottleTicker) { + mThrottleTicker->Init(this, mThrottleSuspendFor, + nsITimer::TYPE_ONE_SHOT); + } + } else { + if (mThrottleTicker) { + mThrottleTicker->Init(this, mThrottleResumeFor, + nsITimer::TYPE_ONE_SHOT); + } + + ResumeReadOf(mActiveTransactions[false], true); + ResumeReadOf(mActiveTransactions[true]); + } + } else { + LOG(("nsHttpConnectionMgr::ThrottlerTick")); + + // If there are only background transactions to be woken after a delay, keep + // the ticker so that we still keep the low read limit for that time. + if (!mDelayedResumeReadTimer && + (!IsThrottleTickerNeeded() || !InThrottlingTimeWindow())) { + LOG((" last tick")); + mThrottleTicker = nullptr; + } + + if (mThrottleTicker) { + mThrottleTicker->Init(this, mThrottleReadInterval, + nsITimer::TYPE_ONE_SHOT); + } + + ResumeReadOf(mActiveTransactions[false], true); + ResumeReadOf(mActiveTransactions[true]); + } +} + +void nsHttpConnectionMgr::DelayedResumeBackgroundThrottledTransactions() { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + if (mThrottleVersion == 1) { + if (mDelayedResumeReadTimer) { + return; + } + } else { + // If the mThrottleTicker doesn't exist, there is nothing currently + // being throttled. Hence, don't invoke the hold time interval. + // This is called also when a single download transaction becomes + // marked as throttleable. We would otherwise block it unnecessarily. + if (mDelayedResumeReadTimer || !mThrottleTicker) { + return; + } + } + + LOG(("nsHttpConnectionMgr::DelayedResumeBackgroundThrottledTransactions")); + NS_NewTimerWithObserver(getter_AddRefs(mDelayedResumeReadTimer), this, + mThrottleHoldTime, nsITimer::TYPE_ONE_SHOT); +} + +void nsHttpConnectionMgr::CancelDelayedResumeBackgroundThrottledTransactions() { + if (!mDelayedResumeReadTimer) { + return; + } + + LOG( + ("nsHttpConnectionMgr::" + "CancelDelayedResumeBackgroundThrottledTransactions")); + mDelayedResumeReadTimer->Cancel(); + mDelayedResumeReadTimer = nullptr; +} + +void nsHttpConnectionMgr::ResumeBackgroundThrottledTransactions() { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + LOG(("nsHttpConnectionMgr::ResumeBackgroundThrottledTransactions")); + mDelayedResumeReadTimer = nullptr; + + if (!IsThrottleTickerNeeded()) { + DestroyThrottleTicker(); + } + + if (!mActiveTransactions[false].IsEmpty()) { + ResumeReadOf(mActiveTransactions[false], true); + } else { + ResumeReadOf(mActiveTransactions[true], true); + } +} + +void nsHttpConnectionMgr::ResumeReadOf( + nsClassHashtable>>& + hashtable, + bool excludeForActiveTab) { + for (auto iter = hashtable.Iter(); !iter.Done(); iter.Next()) { + if (excludeForActiveTab && + iter.Key() == mCurrentTopLevelOuterContentWindowId) { + // These have never been throttled (never stopped reading) + continue; + } + ResumeReadOf(iter.UserData()); + } +} + +void nsHttpConnectionMgr::ResumeReadOf( + nsTArray>* transactions) { + MOZ_ASSERT(transactions); + + for (const auto& trans : *transactions) { + trans->ResumeReading(); + } +} + +void nsHttpConnectionMgr::NotifyConnectionOfWindowIdChange( + uint64_t previousWindowId) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + nsTArray>* transactions = nullptr; + nsTArray> connections; + + auto addConnectionHelper = + [&connections](nsTArray>* trans) { + if (!trans) { + return; + } + + for (const auto& t : *trans) { + RefPtr conn = t->Connection(); + if (conn && !connections.Contains(conn)) { + connections.AppendElement(conn); + } + } + }; + + // Get unthrottled transactions with the previous and current window id. + transactions = mActiveTransactions[false].Get(previousWindowId); + addConnectionHelper(transactions); + transactions = + mActiveTransactions[false].Get(mCurrentTopLevelOuterContentWindowId); + addConnectionHelper(transactions); + + // Get throttled transactions with the previous and current window id. + transactions = mActiveTransactions[true].Get(previousWindowId); + addConnectionHelper(transactions); + transactions = + mActiveTransactions[true].Get(mCurrentTopLevelOuterContentWindowId); + addConnectionHelper(transactions); + + for (const auto& conn : connections) { + conn->TopLevelOuterContentWindowIdChanged( + mCurrentTopLevelOuterContentWindowId); + } +} + +void nsHttpConnectionMgr::OnMsgUpdateCurrentTopLevelOuterContentWindowId( + int32_t aLoading, ARefBase* param) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + uint64_t winId = static_cast(param)->GetValue(); + + if (mCurrentTopLevelOuterContentWindowId == winId) { + // duplicate notification + return; + } + + bool activeTabWasLoading = mActiveTabTransactionsExist; + + uint64_t previousWindowId = mCurrentTopLevelOuterContentWindowId; + mCurrentTopLevelOuterContentWindowId = winId; + + if (gHttpHandler->ActiveTabPriority()) { + NotifyConnectionOfWindowIdChange(previousWindowId); + } + + LOG( + ("nsHttpConnectionMgr::OnMsgUpdateCurrentTopLevelOuterContentWindowId" + " id=%" PRIx64 "\n", + mCurrentTopLevelOuterContentWindowId)); + + nsTArray>* transactions = nullptr; + + // Update the "Exists" caches and resume any transactions that now deserve it, + // changing the active tab changes the conditions for throttling. + transactions = + mActiveTransactions[false].Get(mCurrentTopLevelOuterContentWindowId); + mActiveTabUnthrottledTransactionsExist = !!transactions; + + if (!mActiveTabUnthrottledTransactionsExist) { + transactions = + mActiveTransactions[true].Get(mCurrentTopLevelOuterContentWindowId); + } + mActiveTabTransactionsExist = !!transactions; + + if (transactions) { + // This means there are some transactions for this newly activated tab, + // resume them but anything else. + LOG((" resuming newly activated tab transactions")); + ResumeReadOf(transactions); + return; + } + + if (!activeTabWasLoading) { + // There were no transactions for the previously active tab, hence + // all remaning transactions, if there were, were all unthrottled, + // no need to wake them. + return; + } + + if (!mActiveTransactions[false].IsEmpty()) { + LOG((" resuming unthrottled background transactions")); + ResumeReadOf(mActiveTransactions[false]); + return; + } + + if (!mActiveTransactions[true].IsEmpty()) { + LOG((" resuming throttled background transactions")); + ResumeReadOf(mActiveTransactions[true]); + return; + } + + DestroyThrottleTicker(); +} + +void nsHttpConnectionMgr::TimeoutTick() { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + MOZ_ASSERT(mTimeoutTick, "no readtimeout tick"); + + LOG(("nsHttpConnectionMgr::TimeoutTick active=%d\n", mNumActiveConns)); + // The next tick will be between 1 second and 1 hr + // Set it to the max value here, and the TimeoutTick()s can + // reduce it to their local needs. + mTimeoutTickNext = 3600; // 1hr + + for (auto iter = mCT.Iter(); !iter.Done(); iter.Next()) { + RefPtr ent = iter.Data(); + + uint32_t timeoutTickNext = ent->TimeoutTick(); + mTimeoutTickNext = std::min(mTimeoutTickNext, timeoutTickNext); + } + + if (mTimeoutTick) { + mTimeoutTickNext = std::max(mTimeoutTickNext, 1U); + mTimeoutTick->SetDelay(mTimeoutTickNext * 1000); + } +} + +// GetOrCreateConnectionEntry finds a ent for a particular CI for use in +// dispatching a transaction according to these rules +// 1] use an ent that matches the ci that can be dispatched immediately +// 2] otherwise use an ent of wildcard(ci) than can be dispatched immediately +// 3] otherwise create an ent that matches ci and make new conn on it + +ConnectionEntry* nsHttpConnectionMgr::GetOrCreateConnectionEntry( + nsHttpConnectionInfo* specificCI, bool prohibitWildCard, bool aNoHttp2, + bool aNoHttp3) { + // step 1 + ConnectionEntry* specificEnt = mCT.GetWeak(specificCI->HashKey()); + if (specificEnt && specificEnt->AvailableForDispatchNow()) { + return specificEnt; + } + + // step 1 repeated for an inverted anonymous flag; we return an entry + // only when it has an h2 established connection that is not authenticated + // with a client certificate. + RefPtr anonInvertedCI(specificCI->Clone()); + anonInvertedCI->SetAnonymous(!specificCI->GetAnonymous()); + ConnectionEntry* invertedEnt = mCT.GetWeak(anonInvertedCI->HashKey()); + if (invertedEnt) { + HttpConnectionBase* h2orh3conn = + GetH2orH3ActiveConn(invertedEnt, aNoHttp2, aNoHttp3); + if (h2orh3conn && h2orh3conn->IsExperienced() && + h2orh3conn->NoClientCertAuth()) { + MOZ_ASSERT(h2orh3conn->UsingSpdy() || h2orh3conn->UsingHttp3()); + LOG( + ("GetOrCreateConnectionEntry is coalescing h2/3 an/onymous " + "connections, ent=%p", + invertedEnt)); + return invertedEnt; + } + } + + if (!specificCI->UsingHttpsProxy()) { + prohibitWildCard = true; + } + + // step 2 + if (!prohibitWildCard && !aNoHttp3) { + RefPtr wildCardProxyCI; + DebugOnly rv = + specificCI->CreateWildCard(getter_AddRefs(wildCardProxyCI)); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + ConnectionEntry* wildCardEnt = mCT.GetWeak(wildCardProxyCI->HashKey()); + if (wildCardEnt && wildCardEnt->AvailableForDispatchNow()) { + return wildCardEnt; + } + } + + // step 3 + if (!specificEnt) { + RefPtr clone(specificCI->Clone()); + specificEnt = new ConnectionEntry(clone); + mCT.Put(clone->HashKey(), RefPtr{specificEnt}); + } + return specificEnt; +} + +void nsHttpConnectionMgr::DoSpeculativeConnection( + SpeculativeTransaction* aTrans, bool aFetchHTTPSRR) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + MOZ_ASSERT(aTrans); + + ConnectionEntry* ent = GetOrCreateConnectionEntry( + aTrans->ConnectionInfo(), false, aTrans->Caps() & NS_HTTP_DISALLOW_SPDY, + aTrans->Caps() & NS_HTTP_DISALLOW_HTTP3); + + uint32_t parallelSpeculativeConnectLimit = + aTrans->ParallelSpeculativeConnectLimit() + ? *aTrans->ParallelSpeculativeConnectLimit() + : gHttpHandler->ParallelSpeculativeConnectLimit(); + bool ignoreIdle = aTrans->IgnoreIdle() ? *aTrans->IgnoreIdle() : false; + bool isFromPredictor = + aTrans->IsFromPredictor() ? *aTrans->IsFromPredictor() : false; + bool allow1918 = aTrans->Allow1918() ? *aTrans->Allow1918() : false; + + bool keepAlive = aTrans->Caps() & NS_HTTP_ALLOW_KEEPALIVE; + if (mNumHalfOpenConns < parallelSpeculativeConnectLimit && + ((ignoreIdle && + (ent->IdleConnectionsLength() < parallelSpeculativeConnectLimit)) || + !ent->IdleConnectionsLength()) && + !(keepAlive && ent->RestrictConnections()) && + !AtActiveConnectionLimit(ent, aTrans->Caps())) { + if (aFetchHTTPSRR) { + Unused << aTrans->FetchHTTPSRR(); + } + DebugOnly rv = + CreateTransport(ent, aTrans, aTrans->Caps(), true, isFromPredictor, + false, allow1918, nullptr); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } else { + LOG( + ("OnMsgSpeculativeConnect Transport " + "not created due to existing connection count\n")); + } +} + +void nsHttpConnectionMgr::OnMsgSpeculativeConnect(int32_t, ARefBase* param) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + SpeculativeConnectArgs* args = static_cast(param); + + LOG( + ("nsHttpConnectionMgr::OnMsgSpeculativeConnect [ci=%s, " + "mFetchHTTPSRR=%d]\n", + args->mTrans->ConnectionInfo()->HashKey().get(), args->mFetchHTTPSRR)); + DoSpeculativeConnection(args->mTrans, args->mFetchHTTPSRR); +} + +bool nsHttpConnectionMgr::BeConservativeIfProxied(nsIProxyInfo* proxy) { + if (mBeConservativeForProxy) { + // The pref says to be conservative for proxies. + return true; + } + + if (!proxy) { + // There is no proxy, so be conservative by default. + return true; + } + + // Be conservative only if there is no proxy host set either. + // This logic was copied from nsSSLIOLayerAddToSocket. + nsAutoCString proxyHost; + proxy->GetHost(proxyHost); + return proxyHost.IsEmpty(); +} + +// register a connection to receive CanJoinConnection() for particular +// origin keys +void nsHttpConnectionMgr::RegisterOriginCoalescingKey(HttpConnectionBase* conn, + const nsACString& host, + int32_t port) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + nsHttpConnectionInfo* ci = conn ? conn->ConnectionInfo() : nullptr; + if (!ci || !conn->CanDirectlyActivate()) { + return; + } + + nsCString newKey; + BuildOriginFrameHashKey(newKey, ci, host, port); + nsTArray* listOfWeakConns = mCoalescingHash.Get(newKey); + if (!listOfWeakConns) { + listOfWeakConns = new nsTArray(1); + mCoalescingHash.Put(newKey, listOfWeakConns); + } + listOfWeakConns->AppendElement( + do_GetWeakReference(static_cast(conn))); + + LOG( + ("nsHttpConnectionMgr::RegisterOriginCoalescingKey " + "Established New Coalescing Key %s to %p %s\n", + newKey.get(), conn, ci->HashKey().get())); +} + +bool nsHttpConnectionMgr::GetConnectionData(nsTArray* aArg) { + for (auto iter = mCT.Iter(); !iter.Done(); iter.Next()) { + RefPtr ent = iter.Data(); + + if (ent->mConnInfo->GetPrivate()) { + continue; + } + aArg->AppendElement(ent->GetConnectionData()); + } + + return true; +} + +void nsHttpConnectionMgr::ResetIPFamilyPreference(nsHttpConnectionInfo* ci) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + ConnectionEntry* ent = mCT.GetWeak(ci->HashKey()); + if (ent) { + ent->ResetIPFamilyPreference(); + } +} + +void nsHttpConnectionMgr::ExcludeHttp2(const nsHttpConnectionInfo* ci) { + LOG(("nsHttpConnectionMgr::ExcludeHttp2 excluding ci %s", + ci->HashKey().BeginReading())); + ConnectionEntry* ent = mCT.GetWeak(ci->HashKey()); + if (!ent) { + LOG(("nsHttpConnectionMgr::ExcludeHttp2 no entry found?!")); + return; + } + + ent->DisallowHttp2(); +} + +void nsHttpConnectionMgr::ExcludeHttp3(const nsHttpConnectionInfo* ci) { + LOG(("nsHttpConnectionMgr::ExcludeHttp3 exclude ci %s", + ci->HashKey().BeginReading())); + ConnectionEntry* ent = mCT.GetWeak(ci->HashKey()); + if (!ent) { + LOG(("nsHttpConnectionMgr::ExcludeHttp3 no entry found?!")); + return; + } + + ent->DontReuseHttp3Conn(); +} + +void nsHttpConnectionMgr::MoveToWildCardConnEntry( + nsHttpConnectionInfo* specificCI, nsHttpConnectionInfo* wildCardCI, + HttpConnectionBase* proxyConn) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + MOZ_ASSERT(specificCI->UsingHttpsProxy()); + + LOG( + ("nsHttpConnectionMgr::MakeConnEntryWildCard conn %p has requested to " + "change CI from %s to %s\n", + proxyConn, specificCI->HashKey().get(), wildCardCI->HashKey().get())); + + ConnectionEntry* ent = mCT.GetWeak(specificCI->HashKey()); + LOG( + ("nsHttpConnectionMgr::MakeConnEntryWildCard conn %p using ent %p (spdy " + "%d)\n", + proxyConn, ent, ent ? ent->mUsingSpdy : 0)); + + if (!ent || !ent->mUsingSpdy) { + return; + } + + ConnectionEntry* wcEnt = + GetOrCreateConnectionEntry(wildCardCI, true, false, false); + if (wcEnt == ent) { + // nothing to do! + return; + } + wcEnt->mUsingSpdy = true; + + LOG( + ("nsHttpConnectionMgr::MakeConnEntryWildCard ent %p " + "idle=%zu active=%zu half=%zu pending=%zu\n", + ent, ent->IdleConnectionsLength(), ent->ActiveConnsLength(), + ent->HalfOpensLength(), ent->PendingQueueLength())); + + LOG( + ("nsHttpConnectionMgr::MakeConnEntryWildCard wc-ent %p " + "idle=%zu active=%zu half=%zu pending=%zu\n", + wcEnt, wcEnt->IdleConnectionsLength(), wcEnt->ActiveConnsLength(), + wcEnt->HalfOpensLength(), wcEnt->PendingQueueLength())); + + ent->MoveConnection(proxyConn, wcEnt); +} + +bool nsHttpConnectionMgr::MoveTransToNewConnEntry( + nsHttpTransaction* aTrans, nsHttpConnectionInfo* aNewCI) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + LOG(("nsHttpConnectionMgr::MoveTransToNewConnEntry: trans=%p aNewCI=%s", + aTrans, aNewCI->HashKey().get())); + + // Step 1: Get the transaction's connection entry. + ConnectionEntry* entry = mCT.GetWeak(aTrans->ConnectionInfo()->HashKey()); + if (!entry) { + return false; + } + + // Step 2: Try to find the undispatched transaction. + if (!entry->RemoveTransFromPendingQ(aTrans)) { + return false; + } + + // Step 3: Add the transaction. + aTrans->UpdateConnectionInfo(aNewCI); + Unused << ProcessNewTransaction(aTrans); + return true; +} + +void nsHttpConnectionMgr::IncreaseNumHalfOpenConns() { mNumHalfOpenConns++; } + +void nsHttpConnectionMgr::DecreaseNumHalfOpenConns() { + MOZ_ASSERT(mNumHalfOpenConns); + if (mNumHalfOpenConns) { // just in case + mNumHalfOpenConns--; + } +} + +already_AddRefed +nsHttpConnectionMgr::FindTransactionHelper(bool removeWhenFound, + ConnectionEntry* aEnt, + nsAHttpTransaction* aTrans) { + nsTArray>* pendingQ = + aEnt->GetTransactionPendingQHelper(aTrans); + + int32_t index = + pendingQ ? pendingQ->IndexOf(aTrans, 0, PendingComparator()) : -1; + + RefPtr info; + if (index != -1) { + info = (*pendingQ)[index]; + if (removeWhenFound) { + pendingQ->RemoveElementAt(index); + } + } + return info.forget(); +} + +nsHttpConnectionMgr* nsHttpConnectionMgr::AsHttpConnectionMgr() { return this; } + +HttpConnectionMgrParent* nsHttpConnectionMgr::AsHttpConnectionMgrParent() { + return nullptr; +} + +void nsHttpConnectionMgr::NewIdleConnectionAdded(uint32_t timeToLive) { + mNumIdleConns++; + + // If the added connection was first idle connection or has shortest + // time to live among the watched connections, pruning dead + // connections needs to be done when it can't be reused anymore. + if (!mTimer || NowInSeconds() + timeToLive < mTimeOfNextWakeUp) + PruneDeadConnectionsAfter(timeToLive); +} + +void nsHttpConnectionMgr::DecrementNumIdleConns() { + MOZ_ASSERT(mNumIdleConns); + mNumIdleConns--; + ConditionallyStopPruneDeadConnectionsTimer(); +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/nsHttpConnectionMgr.h b/netwerk/protocol/http/nsHttpConnectionMgr.h new file mode 100644 index 0000000000..66d3621627 --- /dev/null +++ b/netwerk/protocol/http/nsHttpConnectionMgr.h @@ -0,0 +1,453 @@ +/* vim:t ts=4 sw=2 sts=2 et cin: */ +/* 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/. */ + +#ifndef nsHttpConnectionMgr_h__ +#define nsHttpConnectionMgr_h__ + +#include "HalfOpenSocket.h" +#include "HttpConnectionBase.h" +#include "HttpConnectionMgrShell.h" +#include "nsHttpConnection.h" +#include "nsHttpTransaction.h" +#include "nsTArray.h" +#include "nsThreadUtils.h" +#include "nsClassHashtable.h" +#include "nsDataHashtable.h" +#include "mozilla/ReentrantMonitor.h" +#include "mozilla/TimeStamp.h" +#include "mozilla/Attributes.h" +#include "ARefBase.h" +#include "nsWeakReference.h" +#include "ConnectionEntry.h" +#include "TCPFastOpen.h" + +#include "nsINamed.h" +#include "nsIObserver.h" +#include "nsITimer.h" + +class nsIHttpUpgradeListener; + +namespace mozilla { +namespace net { +class EventTokenBucket; +class NullHttpTransaction; +struct HttpRetParams; + +//----------------------------------------------------------------------------- + +// message handlers have this signature +class nsHttpConnectionMgr; +typedef void (nsHttpConnectionMgr::*nsConnEventHandler)(int32_t, ARefBase*); + +class nsHttpConnectionMgr final : public HttpConnectionMgrShell, + public nsIObserver { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_HTTPCONNECTIONMGRSHELL + NS_DECL_NSIOBSERVER + + //------------------------------------------------------------------------- + // NOTE: functions below may only be called on the main thread. + //------------------------------------------------------------------------- + + nsHttpConnectionMgr(); + + //------------------------------------------------------------------------- + // NOTE: functions below may be called on any thread. + //------------------------------------------------------------------------- + + [[nodiscard]] nsresult CancelTransactions(nsHttpConnectionInfo*, + nsresult reason); + + //------------------------------------------------------------------------- + // NOTE: functions below may be called only on the socket thread. + //------------------------------------------------------------------------- + + // called to change the connection entry associated with conn from specific + // into a wildcard (i.e. http2 proxy friendy) mapping + void MoveToWildCardConnEntry(nsHttpConnectionInfo* specificCI, + nsHttpConnectionInfo* wildcardCI, + HttpConnectionBase* conn); + + // Move a transaction from the pendingQ of it's connection entry to another + // one. Returns true if the transaction is moved successfully, otherwise + // returns false. + bool MoveTransToNewConnEntry(nsHttpTransaction* aTrans, + nsHttpConnectionInfo* aNewCI); + + // This is used to force an idle connection to be closed and removed from + // the idle connection list. It is called when the idle connection detects + // that the network peer has closed the transport. + [[nodiscard]] nsresult CloseIdleConnection(nsHttpConnection*); + [[nodiscard]] nsresult RemoveIdleConnection(nsHttpConnection*); + + // The connection manager needs to know when a normal HTTP connection has been + // upgraded to SPDY because the dispatch and idle semantics are a little + // bit different. + void ReportSpdyConnection(nsHttpConnection*, bool usingSpdy); + + void ReportHttp3Connection(HttpConnectionBase*); + + bool GetConnectionData(nsTArray*); + + void ResetIPFamilyPreference(nsHttpConnectionInfo*); + + uint16_t MaxRequestDelay() { return mMaxRequestDelay; } + + // tracks and untracks active transactions according their throttle status + void AddActiveTransaction(nsHttpTransaction* aTrans); + void RemoveActiveTransaction(nsHttpTransaction* aTrans, + Maybe const& aOverride = Nothing()); + void UpdateActiveTransaction(nsHttpTransaction* aTrans); + + // called by nsHttpTransaction::WriteSegments. decides whether the + // transaction should limit reading its reponse data. There are various + // conditions this methods evaluates. If called by an active-tab + // non-throttled transaction, the throttling window time will be prolonged. + bool ShouldThrottle(nsHttpTransaction* aTrans); + + // prolongs the throttling time window to now + the window preferred delay. + // called when: + // - any transaction is activated + // - or when a currently unthrottled transaction for the active window + // receives data + void TouchThrottlingTimeWindow(bool aEnsureTicker = true); + + // return true iff the connection has pending transactions for the active tab. + // it's mainly used to disallow throttling (limit reading) of a response + // belonging to the same conn info to free up a connection ASAP. + // NOTE: relatively expensive to call, there are two hashtable lookups. + bool IsConnEntryUnderPressure(nsHttpConnectionInfo*); + + uint64_t CurrentTopLevelOuterContentWindowId() { + return mCurrentTopLevelOuterContentWindowId; + } + + void DoSpeculativeConnection(SpeculativeTransaction* aTrans, + bool aFetchHTTPSRR); + + HttpConnectionBase* GetH2orH3ActiveConn(ConnectionEntry* ent, bool aNoHttp2, + bool aNoHttp3); + + void IncreaseNumHalfOpenConns(); + void DecreaseNumHalfOpenConns(); + + // Wen a new idle connection has been added, this function is called to + // increment mNumIdleConns and update PruneDeadConnections timer. + void NewIdleConnectionAdded(uint32_t timeToLive); + void DecrementNumIdleConns(); + + private: + virtual ~nsHttpConnectionMgr(); + + //------------------------------------------------------------------------- + // NOTE: functions below may be called on any thread. + //------------------------------------------------------------------------- + + // Schedules next pruning of dead connection to happen after + // given time. + void PruneDeadConnectionsAfter(uint32_t time); + + // Stops timer scheduled for next pruning of dead connections if + // there are no more idle connections or active spdy ones + void ConditionallyStopPruneDeadConnectionsTimer(); + + // Stops timer used for the read timeout tick if there are no currently + // active connections. + void ConditionallyStopTimeoutTick(); + + // called to close active connections with no registered "traffic" + [[nodiscard]] nsresult PruneNoTraffic(); + + //------------------------------------------------------------------------- + // NOTE: functions below may be called only on the socket thread. + //------------------------------------------------------------------------- + + [[nodiscard]] bool ProcessPendingQForEntry(nsHttpConnectionInfo*); + + // public, so that the SPDY/http2 seesions can activate + void ActivateTimeoutTick(); + + already_AddRefed FindTransactionHelper( + bool removeWhenFound, ConnectionEntry* aEnt, nsAHttpTransaction* aTrans); + + public: + static nsAHttpConnection* MakeConnectionHandle(HttpConnectionBase* aWrapped); + void RegisterOriginCoalescingKey(HttpConnectionBase*, const nsACString& host, + int32_t port); + + protected: + friend class ConnectionEntry; + void IncrementActiveConnCount(); + void DecrementActiveConnCount(HttpConnectionBase*); + + private: + friend class HalfOpenSocket; + friend class PendingTransactionInfo; + + //------------------------------------------------------------------------- + // NOTE: these members may be accessed from any thread (use mReentrantMonitor) + //------------------------------------------------------------------------- + + ReentrantMonitor mReentrantMonitor; + nsCOMPtr mSocketThreadTarget; + + // connection limits + uint16_t mMaxUrgentExcessiveConns; + uint16_t mMaxConns; + uint16_t mMaxPersistConnsPerHost; + uint16_t mMaxPersistConnsPerProxy; + uint16_t mMaxRequestDelay; // in seconds + bool mThrottleEnabled; + uint32_t mThrottleVersion; + uint32_t mThrottleSuspendFor; + uint32_t mThrottleResumeFor; + uint32_t mThrottleReadLimit; + uint32_t mThrottleReadInterval; + uint32_t mThrottleHoldTime; + TimeDuration mThrottleMaxTime; + bool mBeConservativeForProxy; + Atomic mIsShuttingDown; + + //------------------------------------------------------------------------- + // NOTE: these members are only accessed on the socket transport thread + //------------------------------------------------------------------------- + + [[nodiscard]] bool ProcessPendingQForEntry(ConnectionEntry*, + bool considerAll); + bool DispatchPendingQ(nsTArray>& pendingQ, + ConnectionEntry* ent, bool considerAll); + + // This function selects transactions from mPendingTransactionTable to + // dispatch according to the following conditions: + // 1. When ActiveTabPriority() is false, only get transactions from the + // queue whose window id is 0. + // 2. If |considerAll| is false, either get transactions from the focused + // window queue or non-focused ones. + // 3. If |considerAll| is true, fill the |pendingQ| with the transactions from + // both focused window and non-focused window queues. + void PreparePendingQForDispatching( + ConnectionEntry* ent, nsTArray>& pendingQ, + bool considerAll); + + // Return |mMaxPersistConnsPerProxy| or |mMaxPersistConnsPerHost|, + // depending whether the proxy is used. + uint32_t MaxPersistConnections(ConnectionEntry* ent) const; + + bool AtActiveConnectionLimit(ConnectionEntry*, uint32_t caps); + [[nodiscard]] nsresult TryDispatchTransaction( + ConnectionEntry* ent, bool onlyReusedConnection, + PendingTransactionInfo* pendingTransInfo); + [[nodiscard]] nsresult TryDispatchTransactionOnIdleConn( + ConnectionEntry* ent, PendingTransactionInfo* pendingTransInfo, + bool respectUrgency, bool* allUrgent = nullptr); + [[nodiscard]] nsresult DispatchTransaction(ConnectionEntry*, + nsHttpTransaction*, + HttpConnectionBase*); + [[nodiscard]] nsresult DispatchAbstractTransaction(ConnectionEntry*, + nsAHttpTransaction*, + uint32_t, + HttpConnectionBase*, + int32_t); + [[nodiscard]] nsresult ProcessNewTransaction(nsHttpTransaction*); + [[nodiscard]] nsresult EnsureSocketThreadTarget(); + void ReportProxyTelemetry(ConnectionEntry* ent); + [[nodiscard]] nsresult CreateTransport( + ConnectionEntry*, nsAHttpTransaction*, uint32_t, bool, bool, bool, bool, + PendingTransactionInfo* pendingTransInfo); + void StartedConnect(); + void RecvdConnect(); + + ConnectionEntry* GetOrCreateConnectionEntry(nsHttpConnectionInfo*, + bool allowWildCard, bool aNoHttp2, + bool aNoHttp3); + + [[nodiscard]] nsresult MakeNewConnection( + ConnectionEntry* ent, PendingTransactionInfo* pendingTransInfo); + + // Manage h2/3 connection coalescing + // The hashtable contains arrays of weak pointers to HttpConnectionBases + nsClassHashtable> mCoalescingHash; + + HttpConnectionBase* FindCoalescableConnection(ConnectionEntry* ent, + bool justKidding, bool aNoHttp2, + bool aNoHttp3); + HttpConnectionBase* FindCoalescableConnectionByHashKey(ConnectionEntry* ent, + const nsCString& key, + bool justKidding, + bool aNoHttp2, + bool aNoHttp3); + void UpdateCoalescingForNewConn(HttpConnectionBase* conn, + ConnectionEntry* ent); + + void ProcessSpdyPendingQ(ConnectionEntry* ent); + void DispatchSpdyPendingQ(nsTArray>& pendingQ, + ConnectionEntry* ent, HttpConnectionBase* connH2, + HttpConnectionBase* connH3); + // used to marshall events to the socket transport thread. + [[nodiscard]] nsresult PostEvent(nsConnEventHandler handler, + int32_t iparam = 0, + ARefBase* vparam = nullptr); + + void OnMsgReclaimConnection(HttpConnectionBase*); + + // message handlers + void OnMsgShutdown(int32_t, ARefBase*); + void OnMsgShutdownConfirm(int32_t, ARefBase*); + void OnMsgNewTransaction(int32_t, ARefBase*); + void OnMsgNewTransactionWithStickyConn(int32_t, ARefBase*); + void OnMsgReschedTransaction(int32_t, ARefBase*); + void OnMsgUpdateClassOfServiceOnTransaction(int32_t, ARefBase*); + void OnMsgCancelTransaction(int32_t, ARefBase*); + void OnMsgCancelTransactions(int32_t, ARefBase*); + void OnMsgProcessPendingQ(int32_t, ARefBase*); + void OnMsgPruneDeadConnections(int32_t, ARefBase*); + void OnMsgSpeculativeConnect(int32_t, ARefBase*); + void OnMsgCompleteUpgrade(int32_t, ARefBase*); + void OnMsgUpdateParam(int32_t, ARefBase*); + void OnMsgDoShiftReloadConnectionCleanup(int32_t, ARefBase*); + void OnMsgProcessFeedback(int32_t, ARefBase*); + void OnMsgProcessAllSpdyPendingQ(int32_t, ARefBase*); + void OnMsgUpdateRequestTokenBucket(int32_t, ARefBase*); + void OnMsgVerifyTraffic(int32_t, ARefBase*); + void OnMsgPruneNoTraffic(int32_t, ARefBase*); + void OnMsgUpdateCurrentTopLevelOuterContentWindowId(int32_t, ARefBase*); + void OnMsgClearConnectionHistory(int32_t, ARefBase*); + + // Total number of active connections in all of the ConnectionEntry objects + // that are accessed from mCT connection table. + uint16_t mNumActiveConns; + // Total number of idle connections in all of the ConnectionEntry objects + // that are accessed from mCT connection table. + uint16_t mNumIdleConns; + // Total number of spdy or http3 connections which are a subset of the active + // conns + uint16_t mNumSpdyHttp3ActiveConns; + // Total number of connections in mHalfOpens ConnectionEntry objects + // that are accessed from mCT connection table + uint32_t mNumHalfOpenConns; + + // Holds time in seconds for next wake-up to prune dead connections. + uint64_t mTimeOfNextWakeUp; + // Timer for next pruning of dead connections. + nsCOMPtr mTimer; + // Timer for pruning stalled connections after changed network. + nsCOMPtr mTrafficTimer; + bool mPruningNoTraffic; + + // A 1s tick to call nsHttpConnection::ReadTimeoutTick on + // active http/1 connections and check for orphaned half opens. + // Disabled when there are no active or half open connections. + nsCOMPtr mTimeoutTick; + bool mTimeoutTickArmed; + uint32_t mTimeoutTickNext; + + // + // the connection table + // + // this table is indexed by connection key. each entry is a + // ConnectionEntry object. It is unlocked and therefore must only + // be accessed from the socket thread. + // + nsRefPtrHashtable mCT; + + // Read Timeout Tick handlers + void TimeoutTick(); + + // For diagnostics + void OnMsgPrintDiagnostics(int32_t, ARefBase*); + + nsCString mLogData; + uint64_t mCurrentTopLevelOuterContentWindowId; + + // Called on a pref change + void SetThrottlingEnabled(bool aEnable); + + // we only want to throttle for a limited amount of time after a new + // active transaction is added so that we don't block downloads on comet, + // socket and any kind of longstanding requests that don't need bandwidth. + // these methods track this time. + bool InThrottlingTimeWindow(); + + // Two hashtalbes keeping track of active transactions regarding window id and + // throttling. Used by the throttling algorithm to obtain number of + // transactions for the active tab and for inactive tabs according their + // throttle status. mActiveTransactions[0] are all unthrottled transactions, + // mActiveTransactions[1] throttled. + nsClassHashtable>> + mActiveTransactions[2]; + + // V1 specific + // Whether we are inside the "stop reading" interval, altered by the throttle + // ticker + bool mThrottlingInhibitsReading; + + TimeStamp mThrottlingWindowEndsAt; + + // ticker for the 'stop reading'/'resume reading' signal + nsCOMPtr mThrottleTicker; + // Checks if the combination of active transactions requires the ticker. + bool IsThrottleTickerNeeded(); + // The method also unschedules the delayed resume of background tabs timer + // if the ticker was about to be scheduled. + void EnsureThrottleTickerIfNeeded(); + // V1: + // Drops also the mThrottlingInhibitsReading flag. Immediate or delayed + // resume of currently throttled transactions is not affected by this method. + // V2: + // Immediate or delayed resume of currently throttled transactions is not + // affected by this method. + void DestroyThrottleTicker(); + // V1: + // Handler for the ticker: alters the mThrottlingInhibitsReading flag. + // V2: + // Handler for the ticker: calls ResumeReading() for all throttled + // transactions. + void ThrottlerTick(); + + // mechanism to delay immediate resume of background tabs and chrome initiated + // throttled transactions after the last transaction blocking their unthrottle + // has been removed. Needs to be delayed because during a page load there is + // a number of intervals when there is no transaction that would cause + // throttling. Hence, throttling of long standing responses, like downloads, + // would be mostly ineffective if resumed during every such interval. + nsCOMPtr mDelayedResumeReadTimer; + // Schedule the resume + void DelayedResumeBackgroundThrottledTransactions(); + // Simply destroys the timer + void CancelDelayedResumeBackgroundThrottledTransactions(); + // Handler for the timer: resumes all background throttled transactions + void ResumeBackgroundThrottledTransactions(); + + // Simple helpers, iterates the given hash/array and resume. + // @param excludeActive: skip active tabid transactions. + void ResumeReadOf( + nsClassHashtable>>&, + bool excludeActive = false); + void ResumeReadOf(nsTArray>*); + + // Cached status of the active tab active transactions existence, + // saves a lot of hashtable lookups + bool mActiveTabTransactionsExist; + bool mActiveTabUnthrottledTransactionsExist; + + void LogActiveTransactions(char); + + // When current active tab is changed, this function uses + // |previousWindowId| to select background transactions and + // mCurrentTopLevelOuterContentWindowId| to select foreground transactions. + // Then, it notifies selected transactions' connection of the new active tab + // id. + void NotifyConnectionOfWindowIdChange(uint64_t previousWindowId); + + // A test if be-conservative should be used when proxy is setup for the + // connection + bool BeConservativeIfProxied(nsIProxyInfo* proxy); +}; + +} // namespace net +} // namespace mozilla + +#endif // !nsHttpConnectionMgr_h__ diff --git a/netwerk/protocol/http/nsHttpDigestAuth.cpp b/netwerk/protocol/http/nsHttpDigestAuth.cpp new file mode 100644 index 0000000000..ed725da702 --- /dev/null +++ b/netwerk/protocol/http/nsHttpDigestAuth.cpp @@ -0,0 +1,662 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * 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/. */ + +// HttpLog.h should generally be included first +#include "HttpLog.h" + +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/Sprintf.h" +#include "mozilla/Unused.h" + +#include "nsHttp.h" +#include "nsHttpDigestAuth.h" +#include "nsIHttpAuthenticableChannel.h" +#include "nsISupportsPrimitives.h" +#include "nsIURI.h" +#include "nsString.h" +#include "nsEscape.h" +#include "nsNetCID.h" +#include "nsCRT.h" +#include "nsICryptoHash.h" +#include "nsComponentManagerUtils.h" + +namespace mozilla { +namespace net { + +StaticRefPtr nsHttpDigestAuth::gSingleton; + +already_AddRefed nsHttpDigestAuth::GetOrCreate() { + nsCOMPtr authenticator; + if (gSingleton) { + authenticator = gSingleton; + } else { + gSingleton = new nsHttpDigestAuth(); + ClearOnShutdown(&gSingleton); + authenticator = gSingleton; + } + + return authenticator.forget(); +} + +//----------------------------------------------------------------------------- +// nsHttpDigestAuth::nsISupports +//----------------------------------------------------------------------------- + +NS_IMPL_ISUPPORTS(nsHttpDigestAuth, nsIHttpAuthenticator) + +//----------------------------------------------------------------------------- +// nsHttpDigestAuth +//----------------------------------------------------------------------------- + +nsresult nsHttpDigestAuth::MD5Hash(const char* buf, uint32_t len) { + nsresult rv; + + // Cache a reference to the nsICryptoHash instance since we'll be calling + // this function frequently. + if (!mVerifier) { + mVerifier = do_CreateInstance(NS_CRYPTO_HASH_CONTRACTID, &rv); + if (NS_FAILED(rv)) { + LOG(("nsHttpDigestAuth: no crypto hash!\n")); + return rv; + } + } + + rv = mVerifier->Init(nsICryptoHash::MD5); + if (NS_FAILED(rv)) return rv; + + rv = mVerifier->Update((unsigned char*)buf, len); + if (NS_FAILED(rv)) return rv; + + nsAutoCString hashString; + rv = mVerifier->Finish(false, hashString); + if (NS_FAILED(rv)) return rv; + + NS_ENSURE_STATE(hashString.Length() == sizeof(mHashBuf)); + memcpy(mHashBuf, hashString.get(), hashString.Length()); + + return rv; +} + +nsresult nsHttpDigestAuth::GetMethodAndPath( + nsIHttpAuthenticableChannel* authChannel, bool isProxyAuth, + nsCString& httpMethod, nsCString& path) { + nsresult rv, rv2; + nsCOMPtr uri; + rv = authChannel->GetURI(getter_AddRefs(uri)); + if (NS_SUCCEEDED(rv)) { + bool proxyMethodIsConnect; + rv = authChannel->GetProxyMethodIsConnect(&proxyMethodIsConnect); + if (NS_SUCCEEDED(rv)) { + if (proxyMethodIsConnect && isProxyAuth) { + httpMethod.AssignLiteral("CONNECT"); + // + // generate hostname:port string. (unfortunately uri->GetHostPort + // leaves out the port if it matches the default value, so we can't + // just call it.) + // + int32_t port; + rv = uri->GetAsciiHost(path); + rv2 = uri->GetPort(&port); + if (NS_SUCCEEDED(rv) && NS_SUCCEEDED(rv2)) { + path.Append(':'); + path.AppendInt(port < 0 ? NS_HTTPS_DEFAULT_PORT : port); + } + } else { + rv = authChannel->GetRequestMethod(httpMethod); + rv2 = uri->GetPathQueryRef(path); + if (NS_SUCCEEDED(rv) && NS_SUCCEEDED(rv2)) { + // + // strip any fragment identifier from the URL path. + // + int32_t ref = path.RFindChar('#'); + if (ref != kNotFound) path.Truncate(ref); + // + // make sure we escape any UTF-8 characters in the URI path. the + // digest auth uri attribute needs to match the request-URI. + // + // XXX we should really ask the HTTP channel for this string + // instead of regenerating it here. + // + nsAutoCString buf; + rv = NS_EscapeURL(path, esc_OnlyNonASCII | esc_Spaces, buf, + mozilla::fallible); + if (NS_SUCCEEDED(rv)) { + path = buf; + } + } + } + } + } + return rv; +} + +//----------------------------------------------------------------------------- +// nsHttpDigestAuth::nsIHttpAuthenticator +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsHttpDigestAuth::ChallengeReceived(nsIHttpAuthenticableChannel* authChannel, + const char* challenge, bool isProxyAuth, + nsISupports** sessionState, + nsISupports** continuationState, + bool* result) { + nsAutoCString realm, domain, nonce, opaque; + bool stale; + uint16_t algorithm, qop; + + nsresult rv = ParseChallenge(challenge, realm, domain, nonce, opaque, &stale, + &algorithm, &qop); + if (NS_FAILED(rv)) return rv; + + // if the challenge has the "stale" flag set, then the user identity is not + // necessarily invalid. by returning FALSE here we can suppress username + // and password prompting that usually accompanies a 401/407 challenge. + *result = !stale; + + // clear any existing nonce_count since we have a new challenge. + NS_IF_RELEASE(*sessionState); + return NS_OK; +} + +NS_IMETHODIMP +nsHttpDigestAuth::GenerateCredentialsAsync( + nsIHttpAuthenticableChannel* authChannel, + nsIHttpAuthenticatorCallback* aCallback, const char* challenge, + bool isProxyAuth, const char16_t* domain, const char16_t* username, + const char16_t* password, nsISupports* sessionState, + nsISupports* continuationState, nsICancelable** aCancellable) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsHttpDigestAuth::GenerateCredentials( + nsIHttpAuthenticableChannel* authChannel, const char* challenge, + bool isProxyAuth, const char16_t* userdomain, const char16_t* username, + const char16_t* password, nsISupports** sessionState, + nsISupports** continuationState, uint32_t* aFlags, char** creds) + +{ + LOG(("nsHttpDigestAuth::GenerateCredentials [challenge=%s]\n", challenge)); + + NS_ENSURE_ARG_POINTER(creds); + + *aFlags = 0; + + bool isDigestAuth = !PL_strncasecmp(challenge, "digest ", 7); + NS_ENSURE_TRUE(isDigestAuth, NS_ERROR_UNEXPECTED); + + // IIS implementation requires extra quotes + bool requireExtraQuotes = false; + { + nsAutoCString serverVal; + Unused << authChannel->GetServerResponseHeader(serverVal); + if (!serverVal.IsEmpty()) { + requireExtraQuotes = + !PL_strncasecmp(serverVal.get(), "Microsoft-IIS", 13); + } + } + + nsresult rv; + nsAutoCString httpMethod; + nsAutoCString path; + rv = GetMethodAndPath(authChannel, isProxyAuth, httpMethod, path); + if (NS_FAILED(rv)) return rv; + + nsAutoCString realm, domain, nonce, opaque; + bool stale; + uint16_t algorithm, qop; + + rv = ParseChallenge(challenge, realm, domain, nonce, opaque, &stale, + &algorithm, &qop); + if (NS_FAILED(rv)) { + LOG( + ("nsHttpDigestAuth::GenerateCredentials [ParseChallenge failed " + "rv=%" PRIx32 "]\n", + static_cast(rv))); + return rv; + } + + char ha1_digest[EXPANDED_DIGEST_LENGTH + 1]; + char ha2_digest[EXPANDED_DIGEST_LENGTH + 1]; + char response_digest[EXPANDED_DIGEST_LENGTH + 1]; + char upload_data_digest[EXPANDED_DIGEST_LENGTH + 1]; + + if (qop & QOP_AUTH_INT) { + // we do not support auth-int "quality of protection" currently + qop &= ~QOP_AUTH_INT; + + NS_WARNING( + "no support for Digest authentication with data integrity quality of " + "protection"); + + /* TODO: to support auth-int, we need to get an MD5 digest of + * TODO: the data uploaded with this request. + * TODO: however, i am not sure how to read in the file in without + * TODO: disturbing the channel''s use of it. do i need to copy it + * TODO: somehow? + */ +#if 0 + if (http_channel != nullptr) + { + nsIInputStream * upload; + nsCOMPtr uc = do_QueryInterface(http_channel); + NS_ENSURE_TRUE(uc, NS_ERROR_UNEXPECTED); + uc->GetUploadStream(&upload); + if (upload) { + char * upload_buffer; + int upload_buffer_length = 0; + //TODO: read input stream into buffer + const char * digest = (const char*) + nsNetwerkMD5Digest(upload_buffer, upload_buffer_length); + ExpandToHex(digest, upload_data_digest); + NS_RELEASE(upload); + } + } +#endif + } + + if (!(algorithm & ALGO_MD5 || algorithm & ALGO_MD5_SESS)) { + // they asked only for algorithms that we do not support + NS_WARNING("unsupported algorithm requested by Digest authentication"); + return NS_ERROR_NOT_IMPLEMENTED; + } + + // + // the following are for increasing security. see RFC 2617 for more + // information. + // + // nonce_count allows the server to keep track of auth challenges (to help + // prevent spoofing). we increase this count every time. + // + char nonce_count[NONCE_COUNT_LENGTH + 1] = "00000001"; // in hex + if (*sessionState) { + nsCOMPtr v(do_QueryInterface(*sessionState)); + if (v) { + uint32_t nc; + v->GetData(&nc); + SprintfLiteral(nonce_count, "%08x", ++nc); + v->SetData(nc); + } + } else { + nsCOMPtr v( + do_CreateInstance(NS_SUPPORTS_PRUINT32_CONTRACTID)); + if (v) { + v->SetData(1); + v.forget(sessionState); + } + } + LOG((" nonce_count=%s\n", nonce_count)); + + // + // this lets the client verify the server response (via a server + // returned Authentication-Info header). also used for session info. + // + nsAutoCString cnonce; + static const char hexChar[] = "0123456789abcdef"; + for (int i = 0; i < 16; ++i) { + cnonce.Append(hexChar[(int)(15.0 * rand() / (RAND_MAX + 1.0))]); + } + LOG((" cnonce=%s\n", cnonce.get())); + + // + // calculate credentials + // + + NS_ConvertUTF16toUTF8 cUser(username), cPass(password); + rv = CalculateHA1(cUser, cPass, realm, algorithm, nonce, cnonce, ha1_digest); + if (NS_FAILED(rv)) return rv; + + rv = CalculateHA2(httpMethod, path, qop, upload_data_digest, ha2_digest); + if (NS_FAILED(rv)) return rv; + + rv = CalculateResponse(ha1_digest, ha2_digest, nonce, qop, nonce_count, + cnonce, response_digest); + if (NS_FAILED(rv)) return rv; + + // + // Values that need to match the quoted-string production from RFC 2616: + // + // username + // realm + // nonce + // opaque + // cnonce + // + + nsAutoCString authString; + + authString.AssignLiteral("Digest username="); + rv = AppendQuotedString(cUser, authString); + NS_ENSURE_SUCCESS(rv, rv); + + authString.AppendLiteral(", realm="); + rv = AppendQuotedString(realm, authString); + NS_ENSURE_SUCCESS(rv, rv); + + authString.AppendLiteral(", nonce="); + rv = AppendQuotedString(nonce, authString); + NS_ENSURE_SUCCESS(rv, rv); + + authString.AppendLiteral(", uri=\""); + authString += path; + if (algorithm & ALGO_SPECIFIED) { + authString.AppendLiteral("\", algorithm="); + if (algorithm & ALGO_MD5_SESS) + authString.AppendLiteral("MD5-sess"); + else + authString.AppendLiteral("MD5"); + } else { + authString += '\"'; + } + authString.AppendLiteral(", response=\""); + authString += response_digest; + authString += '\"'; + + if (!opaque.IsEmpty()) { + authString.AppendLiteral(", opaque="); + rv = AppendQuotedString(opaque, authString); + NS_ENSURE_SUCCESS(rv, rv); + } + + if (qop) { + authString.AppendLiteral(", qop="); + if (requireExtraQuotes) authString += '\"'; + authString.AppendLiteral("auth"); + if (qop & QOP_AUTH_INT) authString.AppendLiteral("-int"); + if (requireExtraQuotes) authString += '\"'; + authString.AppendLiteral(", nc="); + authString += nonce_count; + + authString.AppendLiteral(", cnonce="); + rv = AppendQuotedString(cnonce, authString); + NS_ENSURE_SUCCESS(rv, rv); + } + + *creds = ToNewCString(authString); + return NS_OK; +} + +NS_IMETHODIMP +nsHttpDigestAuth::GetAuthFlags(uint32_t* flags) { + *flags = REQUEST_BASED | REUSABLE_CHALLENGE | IDENTITY_ENCRYPTED; + // + // NOTE: digest auth credentials must be uniquely computed for each request, + // so we do not set the REUSABLE_CREDENTIALS flag. + // + return NS_OK; +} + +nsresult nsHttpDigestAuth::CalculateResponse( + const char* ha1_digest, const char* ha2_digest, const nsCString& nonce, + uint16_t qop, const char* nonce_count, const nsCString& cnonce, + char* result) { + uint32_t len = 2 * EXPANDED_DIGEST_LENGTH + nonce.Length() + 2; + + if (qop & QOP_AUTH || qop & QOP_AUTH_INT) { + len += cnonce.Length() + NONCE_COUNT_LENGTH + 3; + if (qop & QOP_AUTH_INT) + len += 8; // length of "auth-int" + else + len += 4; // length of "auth" + } + + nsAutoCString contents; + contents.SetCapacity(len); + + contents.Append(ha1_digest, EXPANDED_DIGEST_LENGTH); + contents.Append(':'); + contents.Append(nonce); + contents.Append(':'); + + if (qop & QOP_AUTH || qop & QOP_AUTH_INT) { + contents.Append(nonce_count, NONCE_COUNT_LENGTH); + contents.Append(':'); + contents.Append(cnonce); + contents.Append(':'); + if (qop & QOP_AUTH_INT) + contents.AppendLiteral("auth-int:"); + else + contents.AppendLiteral("auth:"); + } + + contents.Append(ha2_digest, EXPANDED_DIGEST_LENGTH); + + nsresult rv = MD5Hash(contents.get(), contents.Length()); + if (NS_SUCCEEDED(rv)) rv = ExpandToHex(mHashBuf, result); + return rv; +} + +nsresult nsHttpDigestAuth::ExpandToHex(const char* digest, char* result) { + int16_t index, value; + + for (index = 0; index < DIGEST_LENGTH; index++) { + value = (digest[index] >> 4) & 0xf; + if (value < 10) + result[index * 2] = value + '0'; + else + result[index * 2] = value - 10 + 'a'; + + value = digest[index] & 0xf; + if (value < 10) + result[(index * 2) + 1] = value + '0'; + else + result[(index * 2) + 1] = value - 10 + 'a'; + } + + result[EXPANDED_DIGEST_LENGTH] = 0; + return NS_OK; +} + +nsresult nsHttpDigestAuth::CalculateHA1(const nsCString& username, + const nsCString& password, + const nsCString& realm, + uint16_t algorithm, + const nsCString& nonce, + const nsCString& cnonce, char* result) { + int16_t len = username.Length() + password.Length() + realm.Length() + 2; + if (algorithm & ALGO_MD5_SESS) { + int16_t exlen = + EXPANDED_DIGEST_LENGTH + nonce.Length() + cnonce.Length() + 2; + if (exlen > len) len = exlen; + } + + nsAutoCString contents; + contents.SetCapacity(len); + + contents.Append(username); + contents.Append(':'); + contents.Append(realm); + contents.Append(':'); + contents.Append(password); + + nsresult rv; + rv = MD5Hash(contents.get(), contents.Length()); + if (NS_FAILED(rv)) return rv; + + if (algorithm & ALGO_MD5_SESS) { + char part1[EXPANDED_DIGEST_LENGTH + 1]; + rv = ExpandToHex(mHashBuf, part1); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + contents.Assign(part1, EXPANDED_DIGEST_LENGTH); + contents.Append(':'); + contents.Append(nonce); + contents.Append(':'); + contents.Append(cnonce); + + rv = MD5Hash(contents.get(), contents.Length()); + if (NS_FAILED(rv)) return rv; + } + + return ExpandToHex(mHashBuf, result); +} + +nsresult nsHttpDigestAuth::CalculateHA2(const nsCString& method, + const nsCString& path, uint16_t qop, + const char* bodyDigest, char* result) { + uint16_t methodLen = method.Length(); + uint32_t pathLen = path.Length(); + uint32_t len = methodLen + pathLen + 1; + + if (qop & QOP_AUTH_INT) { + len += EXPANDED_DIGEST_LENGTH + 1; + } + + nsAutoCString contents; + contents.SetCapacity(len); + + contents.Assign(method); + contents.Append(':'); + contents.Append(path); + + if (qop & QOP_AUTH_INT) { + contents.Append(':'); + contents.Append(bodyDigest, EXPANDED_DIGEST_LENGTH); + } + + nsresult rv = MD5Hash(contents.get(), contents.Length()); + if (NS_SUCCEEDED(rv)) rv = ExpandToHex(mHashBuf, result); + return rv; +} + +nsresult nsHttpDigestAuth::ParseChallenge(const char* challenge, + nsACString& realm, nsACString& domain, + nsACString& nonce, nsACString& opaque, + bool* stale, uint16_t* algorithm, + uint16_t* qop) { + // put an absurd, but maximum, length cap on the challenge so + // that calculations are 32 bit safe + if (strlen(challenge) > 16000000) { + return NS_ERROR_INVALID_ARG; + } + + const char* p = challenge + 6; // first 6 characters are "Digest" + + *stale = false; + *algorithm = ALGO_MD5; // default is MD5 + *qop = 0; + + for (;;) { + while (*p && (*p == ',' || nsCRT::IsAsciiSpace(*p))) ++p; + if (!*p) break; + + // name + int32_t nameStart = (p - challenge); + while (*p && !nsCRT::IsAsciiSpace(*p) && *p != '=') ++p; + if (!*p) return NS_ERROR_INVALID_ARG; + int32_t nameLength = (p - challenge) - nameStart; + + while (*p && (nsCRT::IsAsciiSpace(*p) || *p == '=')) ++p; + if (!*p) return NS_ERROR_INVALID_ARG; + + bool quoted = false; + if (*p == '"') { + ++p; + quoted = true; + } + + // value + int32_t valueStart = (p - challenge); + int32_t valueLength = 0; + if (quoted) { + while (*p && *p != '"') ++p; + if (*p != '"') return NS_ERROR_INVALID_ARG; + valueLength = (p - challenge) - valueStart; + ++p; + } else { + while (*p && !nsCRT::IsAsciiSpace(*p) && *p != ',') ++p; + valueLength = (p - challenge) - valueStart; + } + + // extract information + if (nameLength == 5 && + nsCRT::strncasecmp(challenge + nameStart, "realm", 5) == 0) { + realm.Assign(challenge + valueStart, valueLength); + } else if (nameLength == 6 && + nsCRT::strncasecmp(challenge + nameStart, "domain", 6) == 0) { + domain.Assign(challenge + valueStart, valueLength); + } else if (nameLength == 5 && + nsCRT::strncasecmp(challenge + nameStart, "nonce", 5) == 0) { + nonce.Assign(challenge + valueStart, valueLength); + } else if (nameLength == 6 && + nsCRT::strncasecmp(challenge + nameStart, "opaque", 6) == 0) { + opaque.Assign(challenge + valueStart, valueLength); + } else if (nameLength == 5 && + nsCRT::strncasecmp(challenge + nameStart, "stale", 5) == 0) { + if (nsCRT::strncasecmp(challenge + valueStart, "true", 4) == 0) + *stale = true; + else + *stale = false; + } else if (nameLength == 9 && + nsCRT::strncasecmp(challenge + nameStart, "algorithm", 9) == 0) { + // we want to clear the default, so we use = not |= here + *algorithm = ALGO_SPECIFIED; + if (valueLength == 3 && + nsCRT::strncasecmp(challenge + valueStart, "MD5", 3) == 0) + *algorithm |= ALGO_MD5; + else if (valueLength == 8 && + nsCRT::strncasecmp(challenge + valueStart, "MD5-sess", 8) == 0) + *algorithm |= ALGO_MD5_SESS; + } else if (nameLength == 3 && + nsCRT::strncasecmp(challenge + nameStart, "qop", 3) == 0) { + int32_t ipos = valueStart; + while (ipos < valueStart + valueLength) { + while (ipos < valueStart + valueLength && + (nsCRT::IsAsciiSpace(challenge[ipos]) || challenge[ipos] == ',')) + ipos++; + int32_t algostart = ipos; + while (ipos < valueStart + valueLength && + !nsCRT::IsAsciiSpace(challenge[ipos]) && challenge[ipos] != ',') + ipos++; + if ((ipos - algostart) == 4 && + nsCRT::strncasecmp(challenge + algostart, "auth", 4) == 0) + *qop |= QOP_AUTH; + else if ((ipos - algostart) == 8 && + nsCRT::strncasecmp(challenge + algostart, "auth-int", 8) == 0) + *qop |= QOP_AUTH_INT; + } + } + } + return NS_OK; +} + +nsresult nsHttpDigestAuth::AppendQuotedString(const nsACString& value, + nsACString& aHeaderLine) { + nsAutoCString quoted; + nsACString::const_iterator s, e; + value.BeginReading(s); + value.EndReading(e); + + // + // Encode string according to RFC 2616 quoted-string production + // + quoted.Append('"'); + for (; s != e; ++s) { + // + // CTL = + // + if (*s <= 31 || *s == 127) { + return NS_ERROR_FAILURE; + } + + // Escape two syntactically significant characters + if (*s == '"' || *s == '\\') { + quoted.Append('\\'); + } + + quoted.Append(*s); + } + // FIXME: bug 41489 + // We should RFC2047-encode non-Latin-1 values according to spec + quoted.Append('"'); + aHeaderLine.Append(quoted); + return NS_OK; +} + +} // namespace net +} // namespace mozilla + +// vim: ts=2 sw=2 diff --git a/netwerk/protocol/http/nsHttpDigestAuth.h b/netwerk/protocol/http/nsHttpDigestAuth.h new file mode 100644 index 0000000000..eb110d3b89 --- /dev/null +++ b/netwerk/protocol/http/nsHttpDigestAuth.h @@ -0,0 +1,93 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * 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/. */ + +#ifndef nsDigestAuth_h__ +#define nsDigestAuth_h__ + +#include "nsICryptoHash.h" +#include "nsIHttpAuthenticator.h" +#include "nsStringFwd.h" +#include "nsCOMPtr.h" +#include "mozilla/Attributes.h" +#include "mozilla/StaticPtr.h" + +namespace mozilla { +namespace net { + +#define ALGO_SPECIFIED 0x01 +#define ALGO_MD5 0x02 +#define ALGO_MD5_SESS 0x04 +#define QOP_AUTH 0x01 +#define QOP_AUTH_INT 0x02 + +#define DIGEST_LENGTH 16 +#define EXPANDED_DIGEST_LENGTH 32 +#define NONCE_COUNT_LENGTH 8 + +//----------------------------------------------------------------------------- +// nsHttpDigestAuth +//----------------------------------------------------------------------------- + +class nsHttpDigestAuth final : public nsIHttpAuthenticator { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIHTTPAUTHENTICATOR + + nsHttpDigestAuth() = default; + + static already_AddRefed GetOrCreate(); + + protected: + ~nsHttpDigestAuth() = default; + + [[nodiscard]] nsresult ExpandToHex(const char* digest, char* result); + + [[nodiscard]] nsresult CalculateResponse(const char* ha1_digest, + const char* ha2_digest, + const nsCString& nonce, uint16_t qop, + const char* nonce_count, + const nsCString& cnonce, + char* result); + + [[nodiscard]] nsresult CalculateHA1(const nsCString& username, + const nsCString& password, + const nsCString& realm, + uint16_t algorithm, + const nsCString& nonce, + const nsCString& cnonce, char* result); + + [[nodiscard]] nsresult CalculateHA2(const nsCString& http_method, + const nsCString& http_uri_path, + uint16_t qop, const char* body_digest, + char* result); + + [[nodiscard]] nsresult ParseChallenge(const char* challenge, + nsACString& realm, nsACString& domain, + nsACString& nonce, nsACString& opaque, + bool* stale, uint16_t* algorithm, + uint16_t* qop); + + // result is in mHashBuf + [[nodiscard]] nsresult MD5Hash(const char* buf, uint32_t len); + + [[nodiscard]] nsresult GetMethodAndPath(nsIHttpAuthenticableChannel*, bool, + nsCString&, nsCString&); + + // append the quoted version of value to aHeaderLine + [[nodiscard]] nsresult AppendQuotedString(const nsACString& value, + nsACString& aHeaderLine); + + protected: + nsCOMPtr mVerifier; + char mHashBuf[DIGEST_LENGTH]; + + static StaticRefPtr gSingleton; +}; + +} // namespace net +} // namespace mozilla + +#endif // nsHttpDigestAuth_h__ diff --git a/netwerk/protocol/http/nsHttpHandler.cpp b/netwerk/protocol/http/nsHttpHandler.cpp new file mode 100644 index 0000000000..3e86877ed0 --- /dev/null +++ b/netwerk/protocol/http/nsHttpHandler.cpp @@ -0,0 +1,3053 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=4 sw=2 sts=2 et cin: */ +/* 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/. */ + +// HttpLog.h should generally be included first +#include "HttpLog.h" + +#include "prsystem.h" + +#include "AltServiceChild.h" +#include "nsError.h" +#include "nsHttp.h" +#include "nsHttpHandler.h" +#include "nsHttpChannel.h" +#include "nsHttpAuthCache.h" +#include "nsStandardURL.h" +#include "LoadContextInfo.h" +#include "nsCategoryManagerUtils.h" +#include "nsDirectoryServiceDefs.h" +#include "nsSocketProviderService.h" +#include "nsISocketProvider.h" +#include "nsPrintfCString.h" +#include "nsCOMPtr.h" +#include "nsNetCID.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/Printf.h" +#include "mozilla/Sprintf.h" +#include "mozilla/StaticPrefs_network.h" +#include "mozilla/StaticPrefs_privacy.h" +#include "mozilla/StoragePrincipalHelper.h" +#include "nsAsyncRedirectVerifyHelper.h" +#include "nsSocketTransportService2.h" +#include "nsAlgorithm.h" +#include "ASpdySession.h" +#include "EventTokenBucket.h" +#include "Tickler.h" +#include "nsIXULAppInfo.h" +#include "nsICookieService.h" +#include "nsIObserverService.h" +#include "nsISiteSecurityService.h" +#include "nsIStreamConverterService.h" +#include "nsCRT.h" +#include "nsIParentalControlsService.h" +#include "nsPIDOMWindow.h" +#include "nsHttpChannelAuthProvider.h" +#include "nsINetworkLinkService.h" +#include "nsNetUtil.h" +#include "nsServiceManagerUtils.h" +#include "nsComponentManagerUtils.h" +#include "nsSocketTransportService2.h" +#include "nsIOService.h" +#include "nsISupportsPrimitives.h" +#include "nsIXULRuntime.h" +#include "nsCharSeparatedTokenizer.h" +#include "nsRFPService.h" +#include "mozilla/net/rust_helper.h" + +#include "mozilla/net/HttpConnectionMgrParent.h" +#include "mozilla/net/NeckoChild.h" +#include "mozilla/net/NeckoParent.h" +#include "mozilla/net/RequestContextService.h" +#include "mozilla/net/SocketProcessParent.h" +#include "mozilla/ipc/URIUtils.h" +#include "mozilla/Telemetry.h" +#include "mozilla/Unused.h" +#include "mozilla/AntiTrackingRedirectHeuristic.h" +#include "mozilla/DynamicFpiRedirectHeuristic.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/LazyIdleThread.h" +#include "mozilla/StaticPrefs_image.h" +#include "mozilla/SyncRunnable.h" + +#include "mozilla/dom/ContentParent.h" +#include "mozilla/dom/Navigator.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/network/Connection.h" + +#include "nsNSSComponent.h" +#include "TRRServiceChannel.h" + +#if defined(XP_UNIX) +# include +#endif + +#if defined(XP_WIN) +# include +# include "mozilla/WindowsVersion.h" +#endif + +#if defined(XP_MACOSX) +# include +# include "nsCocoaFeatures.h" +#endif + +#ifdef MOZ_TASK_TRACER +# include "GeckoTaskTracer.h" +#endif + +//----------------------------------------------------------------------------- +#include "mozilla/net/HttpChannelChild.h" + +#define UA_PREF_PREFIX "general.useragent." +#ifdef XP_WIN +# define UA_SPARE_PLATFORM +#endif + +#define HTTP_PREF_PREFIX "network.http." +#define INTL_ACCEPT_LANGUAGES "intl.accept_languages" +#define BROWSER_PREF_PREFIX "browser.cache." +#define H2MANDATORY_SUITE "security.ssl3.ecdhe_rsa_aes_128_gcm_sha256" +#define SAFE_HINT_HEADER_VALUE "safeHint.enabled" +#define SECURITY_PREFIX "security." +#define DOM_SECURITY_PREFIX "dom.security" +#define TCP_FAST_OPEN_ENABLE "network.tcp.tcp_fastopen_enable" +#define TCP_FAST_OPEN_FAILURE_LIMIT \ + "network.tcp.tcp_fastopen_consecutive_failure_limit" +#define TCP_FAST_OPEN_STALLS_LIMIT "network.tcp.tcp_fastopen_http_stalls_limit" +#define TCP_FAST_OPEN_STALLS_IDLE \ + "network.tcp.tcp_fastopen_http_check_for_stalls_only_if_idle_for" +#define TCP_FAST_OPEN_STALLS_TIMEOUT \ + "network.tcp.tcp_fastopen_http_stalls_timeout" + +#define ACCEPT_HEADER_STYLE "text/css,*/*;q=0.1" +#define ACCEPT_HEADER_ALL "*/*" + +#define UA_PREF(_pref) UA_PREF_PREFIX _pref +#define HTTP_PREF(_pref) HTTP_PREF_PREFIX _pref +#define BROWSER_PREF(_pref) BROWSER_PREF_PREFIX _pref + +#define NS_HTTP_PROTOCOL_FLAGS \ + (URI_STD | ALLOWS_PROXY | ALLOWS_PROXY_HTTP | URI_LOADABLE_BY_ANYONE) + +//----------------------------------------------------------------------------- + +using mozilla::dom::Promise; + +namespace mozilla::net { + +LazyLogModule gHttpLog("nsHttp"); + +#ifdef ANDROID +static nsCString GetDeviceModelId() { + // Assumed to be running on the main thread + // We need the device property in either case + nsAutoCString deviceModelId; + nsCOMPtr infoService = + do_GetService("@mozilla.org/system-info;1"); + MOZ_ASSERT(infoService, "Could not find a system info service"); + nsAutoString androidDevice; + nsresult rv = infoService->GetPropertyAsAString(u"device"_ns, androidDevice); + if (NS_SUCCEEDED(rv)) { + deviceModelId = NS_LossyConvertUTF16toASCII(androidDevice); + } + nsAutoCString deviceString; + rv = Preferences::GetCString(UA_PREF("device_string"), deviceString); + if (NS_SUCCEEDED(rv)) { + deviceString.Trim(" ", true, true); + deviceString.ReplaceSubstring("%DEVICEID%"_ns, deviceModelId); + return std::move(deviceString); + } + return std::move(deviceModelId); +} +#endif + +//----------------------------------------------------------------------------- +// nsHttpHandler +//----------------------------------------------------------------------------- + +StaticRefPtr gHttpHandler; + +/* static */ +already_AddRefed nsHttpHandler::GetInstance() { + if (!gHttpHandler) { + gHttpHandler = new nsHttpHandler(); + DebugOnly rv = gHttpHandler->Init(); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + // There is code that may be executed during the final cycle collection + // shutdown and still referencing gHttpHandler. + ClearOnShutdown(&gHttpHandler, + ShutdownPhase::ShutdownPostLastCycleCollection); + } + RefPtr httpHandler = gHttpHandler; + return httpHandler.forget(); +} + +/// Derive the HTTP Accept header for image requests based on the enabled prefs +/// for non-universal image types. This may be overridden in its entirety by +/// the image.http.accept pref. +static nsCString ImageAcceptHeader() { + nsCString mimeTypes; + + if (mozilla::StaticPrefs::image_avif_enabled()) { + mimeTypes.Append("image/avif,"); + } + + if (mozilla::StaticPrefs::image_webp_enabled()) { + mimeTypes.Append("image/webp,"); + } + + mimeTypes.Append("*/*"); + + return mimeTypes; +} + +static nsCString DocumentAcceptHeader(const nsCString& aImageAcceptHeader) { + nsPrintfCString mimeTypes( + "text/html,application/xhtml+xml,application/xml;q=0.9,%s;q=0.8", + aImageAcceptHeader.get()); + + return std::move(mimeTypes); +} + +nsHttpHandler::nsHttpHandler() + : mHttpVersion(HttpVersion::v1_1), + mProxyHttpVersion(HttpVersion::v1_1), + mCapabilities(NS_HTTP_ALLOW_KEEPALIVE), + mFastFallbackToIPv4(false), + mIdleTimeout(PR_SecondsToInterval(10)), + mSpdyTimeout(PR_SecondsToInterval(180)), + mResponseTimeout(PR_SecondsToInterval(300)), + mResponseTimeoutEnabled(false), + mNetworkChangedTimeout(5000), + mMaxRequestAttempts(6), + mMaxRequestDelay(10), + mIdleSynTimeout(250), + mFallbackSynTimeout(5), + mH2MandatorySuiteEnabled(false), + mMaxUrgentExcessiveConns(3), + mMaxConnections(24), + mMaxPersistentConnectionsPerServer(2), + mMaxPersistentConnectionsPerProxy(4), + mThrottleEnabled(true), + mThrottleVersion(2), + mThrottleSuspendFor(3000), + mThrottleResumeFor(200), + mThrottleReadLimit(8000), + mThrottleReadInterval(500), + mThrottleHoldTime(600), + mThrottleMaxTime(3000), + mSendWindowSize(1024), + mUrgentStartEnabled(true), + mTailBlockingEnabled(true), + mTailDelayQuantum(600), + mTailDelayQuantumAfterDCL(100), + mTailDelayMax(6000), + mTailTotalMax(0), + mRedirectionLimit(10), + mBeConservativeForProxy(true), + mPhishyUserPassLength(1), + mQoSBits(0x00), + mEnforceAssocReq(false), + mImageAcceptHeader(ImageAcceptHeader()), + mDocumentAcceptHeader(DocumentAcceptHeader(ImageAcceptHeader())), + mLastUniqueID(NowInSeconds()), + mSessionStartTime(0), + mLegacyAppName("Mozilla"), + mLegacyAppVersion("5.0"), + mProduct("Gecko"), + mCompatFirefoxEnabled(false), + mUserAgentIsDirty(true), + mAcceptLanguagesIsDirty(true), + mPromptTempRedirect(true), + mEnablePersistentHttpsCaching(false), + mSafeHintEnabled(false), + mParentalControlEnabled(false), + mHandlerActive(false), + mDebugObservations(false), + mEnableSpdy(false), + mHttp2Enabled(true), + mUseH2Deps(true), + mEnforceHttp2TlsProfile(true), + mCoalesceSpdy(true), + mSpdyPersistentSettings(false), + mAllowPush(true), + mEnableAltSvc(false), + mEnableAltSvcOE(false), + mEnableOriginExtension(false), + mEnableH2Websockets(true), + mDumpHpackTables(false), + mSpdySendingChunkSize(ASpdySession::kSendingChunkSize), + mSpdySendBufferSize(ASpdySession::kTCPSendBufferSize), + mSpdyPushAllowance(131072) // match default pref + , + mSpdyPullAllowance(ASpdySession::kInitialRwin), + mDefaultSpdyConcurrent(ASpdySession::kDefaultMaxConcurrent), + mSpdyPingThreshold(PR_SecondsToInterval(58)), + mSpdyPingTimeout(PR_SecondsToInterval(8)), + mConnectTimeout(90000), + mTLSHandshakeTimeout(30000), + mParallelSpeculativeConnectLimit(6), + mRequestTokenBucketEnabled(true), + mRequestTokenBucketMinParallelism(6), + mRequestTokenBucketHz(100), + mRequestTokenBucketBurst(32), + mCriticalRequestPrioritization(true), + mTCPKeepaliveShortLivedEnabled(false), + mTCPKeepaliveShortLivedTimeS(60), + mTCPKeepaliveShortLivedIdleTimeS(10), + mTCPKeepaliveLongLivedEnabled(false), + mTCPKeepaliveLongLivedIdleTimeS(600), + mEnforceH1Framing(FRAMECHECK_BARELY), + mDefaultHpackBuffer(4096), + mBug1563538(true), + mHttp3Enabled(true), + mQpackTableSize(4096), + mHttp3MaxBlockedStreams(10), + mMaxHttpResponseHeaderSize(393216), + mFocusedWindowTransactionRatio(0.9f), + mSpeculativeConnectEnabled(false), + mUseFastOpen(true), + mFastOpenConsecutiveFailureLimit(5), + mFastOpenConsecutiveFailureCounter(0), + mFastOpenStallsLimit(3), + mFastOpenStallsCounter(0), + mFastOpenStallsIdleTime(10), + mFastOpenStallsTimeout(20), + mActiveTabPriority(true), + mProcessId(0), + mNextChannelId(1), + mLastActiveTabLoadOptimizationLock( + "nsHttpConnectionMgr::LastActiveTabLoadOptimization"), + mHttpExclusionLock("nsHttpHandler::HttpExclusion"), + mThroughCaptivePortal(false) { + LOG(("Creating nsHttpHandler [this=%p].\n", this)); + + mUserAgentOverride.SetIsVoid(true); + + MOZ_ASSERT(!gHttpHandler, "HTTP handler already created!"); + + nsCOMPtr runtime = do_GetService("@mozilla.org/xre/runtime;1"); + if (runtime) { + runtime->GetProcessID(&mProcessId); + } + + mFastOpenSupported = false; + if (XRE_IsParentProcess()) { + SetFastOpenOSSupport(); + } +} + +void nsHttpHandler::SetFastOpenOSSupport() { +#if !defined(XP_WIN) && !defined(XP_LINUX) && !defined(ANDROID) && \ + !defined(HAS_CONNECTX) + return; +#elif defined(XP_WIN) + mFastOpenSupported = IsWindows10BuildOrLater(16299); + + if (mFastOpenSupported) { + // We have some problems with lavasoft software and tcp fast open. + if (GetModuleHandleW(L"pmls64.dll") || GetModuleHandleW(L"rlls64.dll")) { + mFastOpenSupported = false; + } + } +#else + + nsAutoCString version; + nsresult rv; +# ifdef ANDROID + nsCOMPtr infoService = + do_GetService("@mozilla.org/system-info;1"); + MOZ_ASSERT(infoService, "Could not find a system info service"); + rv = infoService->GetPropertyAsACString(u"sdk_version"_ns, version); +# else + char buf[SYS_INFO_BUFFER_LENGTH]; + if (PR_GetSystemInfo(PR_SI_RELEASE, buf, sizeof(buf)) == PR_SUCCESS) { + version = buf; + rv = NS_OK; + } else { + rv = NS_ERROR_FAILURE; + } +# endif + + LOG(("nsHttpHandler::SetFastOpenOSSupport version %s", version.get())); + + if (NS_SUCCEEDED(rv)) { + // set min version minus 1. +# if XP_MACOSX + int min_version[] = {17, 5}; // High Sierra 10.13.4 +# elif ANDROID + int min_version[] = {4, 4}; +# elif XP_LINUX + int min_version[] = {3, 6}; +# endif + int inx = 0; + nsCCharSeparatedTokenizer tokenizer(version, '.'); + while ((inx < 2) && tokenizer.hasMoreTokens()) { + nsAutoCString token(tokenizer.nextToken()); + const char* nondigit = NS_strspnp("0123456789", token.get()); + if (nondigit && *nondigit) { + break; + } + nsresult rv; + int32_t ver = token.ToInteger(&rv); + if (NS_FAILED(rv)) { + break; + } + if (ver > min_version[inx]) { + mFastOpenSupported = true; + break; + } else if (ver == min_version[inx] && inx == 1) { + mFastOpenSupported = true; + } else if (ver < min_version[inx]) { + break; + } + inx++; + } + } +#endif + LOG(("nsHttpHandler::SetFastOpenOSSupport %s supported.\n", + mFastOpenSupported ? "" : "not")); +} + +nsHttpHandler::~nsHttpHandler() { + LOG(("Deleting nsHttpHandler [this=%p]\n", this)); + + // make sure the connection manager is shutdown + if (mConnMgr) { + nsresult rv = mConnMgr->Shutdown(); + if (NS_FAILED(rv)) { + LOG( + ("nsHttpHandler [this=%p] " + "failed to shutdown connection manager (%08x)\n", + this, static_cast(rv))); + } + mConnMgr = nullptr; + } + + // Note: don't call NeckoChild::DestroyNeckoChild() here, as it's too late + // and it'll segfault. NeckoChild will get cleaned up by process exit. + + nsHttp::DestroyAtomTable(); +} + +static const char* gCallbackPrefs[] = { + HTTP_PREF_PREFIX, + UA_PREF_PREFIX, + INTL_ACCEPT_LANGUAGES, + BROWSER_PREF("disk_cache_ssl"), + H2MANDATORY_SUITE, + HTTP_PREF("tcp_keepalive.short_lived_connections"), + HTTP_PREF("tcp_keepalive.long_lived_connections"), + SAFE_HINT_HEADER_VALUE, + SECURITY_PREFIX, + DOM_SECURITY_PREFIX, + TCP_FAST_OPEN_ENABLE, + TCP_FAST_OPEN_FAILURE_LIMIT, + TCP_FAST_OPEN_STALLS_LIMIT, + TCP_FAST_OPEN_STALLS_IDLE, + TCP_FAST_OPEN_STALLS_TIMEOUT, + "image.http.accept", + "image.avif.enabled", + "image.webp.enabled", + nullptr, +}; + +nsresult nsHttpHandler::Init() { + nsresult rv; + + LOG(("nsHttpHandler::Init\n")); + MOZ_ASSERT(NS_IsMainThread()); + + rv = nsHttp::CreateAtomTable(); + if (NS_FAILED(rv)) return rv; + + nsCOMPtr service = do_GetService(NS_IOSERVICE_CONTRACTID, &rv); + if (NS_FAILED(rv)) { + NS_WARNING("unable to continue without io service"); + return rv; + } + mIOService = new nsMainThreadPtrHolder( + "nsHttpHandler::mIOService", service); + + gIOService->LaunchSocketProcess(); + + if (IsNeckoChild()) NeckoChild::InitNeckoChild(); + + InitUserAgentComponents(); + + // This perference is only used in parent process. + if (!IsNeckoChild()) { + mActiveTabPriority = + Preferences::GetBool(HTTP_PREF("active_tab_priority"), true); + } + + auto initQLogDir = [&]() { + nsCOMPtr qlogDir; + nsresult rv = + NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(qlogDir)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return EmptyCString(); + } + + nsAutoCString dirName("qlog_"); + dirName.AppendInt(mProcessId); + rv = qlogDir->AppendNative(dirName); + if (NS_WARN_IF(NS_FAILED(rv))) { + return EmptyCString(); + } + + return qlogDir->HumanReadablePath(); + }; + mHttp3QlogDir = initQLogDir(); + + // monitor some preference changes + Preferences::RegisterPrefixCallbacks(nsHttpHandler::PrefsChanged, + gCallbackPrefs, this); + PrefsChanged(nullptr); + Telemetry::ScalarSet(Telemetry::ScalarID::NETWORKING_HTTP3_ENABLED, + mHttp3Enabled); + + mMisc.AssignLiteral("rv:" MOZILLA_UAVERSION); + + mCompatFirefox.AssignLiteral("Firefox/" MOZILLA_UAVERSION); + + nsCOMPtr appInfo = + do_GetService("@mozilla.org/xre/app-info;1"); + + mAppName.AssignLiteral(MOZ_APP_UA_NAME); + if (mAppName.Length() == 0 && appInfo) { + // Try to get the UA name from appInfo, falling back to the name + appInfo->GetUAName(mAppName); + if (mAppName.Length() == 0) { + appInfo->GetName(mAppName); + } + appInfo->GetVersion(mAppVersion); + mAppName.StripChars(R"( ()<>@,;:\"/[]?={})"); + } else { + mAppVersion.AssignLiteral(MOZ_APP_UA_VERSION); + } + + // Generate the spoofed User Agent for fingerprinting resistance. + nsRFPService::GetSpoofedUserAgent(mSpoofedUserAgent, true); + + mSessionStartTime = NowInSeconds(); + mHandlerActive = true; + + rv = InitConnectionMgr(); + if (NS_FAILED(rv)) return rv; + + mAltSvcCache = MakeUnique(); + + mRequestContextService = RequestContextService::GetOrCreate(); + +#if defined(ANDROID) + mProductSub.AssignLiteral(MOZILLA_UAVERSION); +#else + mProductSub.AssignLiteral(LEGACY_UA_GECKO_TRAIL); +#endif + +#if DEBUG + // dump user agent prefs + LOG(("> legacy-app-name = %s\n", mLegacyAppName.get())); + LOG(("> legacy-app-version = %s\n", mLegacyAppVersion.get())); + LOG(("> platform = %s\n", mPlatform.get())); + LOG(("> oscpu = %s\n", mOscpu.get())); + LOG(("> misc = %s\n", mMisc.get())); + LOG(("> product = %s\n", mProduct.get())); + LOG(("> product-sub = %s\n", mProductSub.get())); + LOG(("> app-name = %s\n", mAppName.get())); + LOG(("> app-version = %s\n", mAppVersion.get())); + LOG(("> compat-firefox = %s\n", mCompatFirefox.get())); + LOG(("> user-agent = %s\n", UserAgent().get())); +#endif + + // Startup the http category + // Bring alive the objects in the http-protocol-startup category + NS_CreateServicesFromCategory( + NS_HTTP_STARTUP_CATEGORY, + static_cast(static_cast(this)), + NS_HTTP_STARTUP_TOPIC); + + nsCOMPtr obsService = + static_cast(gIOService); + if (obsService) { + // register the handler object as a weak callback as we don't need to worry + // about shutdown ordering. + obsService->AddObserver(this, "profile-change-net-teardown", true); + obsService->AddObserver(this, "profile-change-net-restore", true); + obsService->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, true); + obsService->AddObserver(this, "net:clear-active-logins", true); + obsService->AddObserver(this, "net:prune-dead-connections", true); + // Sent by the TorButton add-on in the Tor Browser + obsService->AddObserver(this, "net:prune-all-connections", true); + obsService->AddObserver(this, "net:cancel-all-connections", true); + obsService->AddObserver(this, "last-pb-context-exited", true); + obsService->AddObserver(this, "browser:purge-session-history", true); + obsService->AddObserver(this, NS_NETWORK_LINK_TOPIC, true); + obsService->AddObserver(this, "application-background", true); + obsService->AddObserver(this, "psm:user-certificate-added", true); + obsService->AddObserver(this, "psm:user-certificate-deleted", true); + obsService->AddObserver(this, "intl:app-locales-changed", true); + obsService->AddObserver(this, "browser-delayed-startup-finished", true); + obsService->AddObserver(this, "network:captive-portal-connectivity", true); + obsService->AddObserver(this, "network:reset-http3-excluded-list", true); + + if (!IsNeckoChild()) { + obsService->AddObserver( + this, "net:current-toplevel-outer-content-windowid", true); + } + + if (mFastOpenSupported) { + obsService->AddObserver(this, "captive-portal-login", true); + obsService->AddObserver(this, "captive-portal-login-success", true); + } + + // disabled as its a nop right now + // obsService->AddObserver(this, "net:failed-to-process-uri-content", true); + } + + MakeNewRequestTokenBucket(); + mWifiTickler = new Tickler(); + if (NS_FAILED(mWifiTickler->Init())) mWifiTickler = nullptr; + + nsCOMPtr pc = + do_CreateInstance("@mozilla.org/parental-controls-service;1"); + if (pc) { + pc->GetParentalControlsEnabled(&mParentalControlEnabled); + } + + return NS_OK; +} + +const nsCString& nsHttpHandler::Http3QlogDir() { + if (StaticPrefs::network_http_http3_enable_qlog()) { + return mHttp3QlogDir; + } + + return EmptyCString(); +} + +void nsHttpHandler::MakeNewRequestTokenBucket() { + LOG(("nsHttpHandler::MakeNewRequestTokenBucket this=%p child=%d\n", this, + IsNeckoChild())); + if (!mConnMgr || IsNeckoChild()) { + return; + } + RefPtr tokenBucket = + new EventTokenBucket(RequestTokenBucketHz(), RequestTokenBucketBurst()); + // NOTE The thread or socket may be gone already. + nsresult rv = mConnMgr->UpdateRequestTokenBucket(tokenBucket); + if (NS_FAILED(rv)) { + LOG((" failed to update request token bucket\n")); + } +} + +nsresult nsHttpHandler::InitConnectionMgr() { + // Init ConnectionManager only on parent! + if (IsNeckoChild()) { + return NS_OK; + } + + if (mConnMgr) { + return NS_OK; + } + + if (nsIOService::UseSocketProcess(true) && XRE_IsParentProcess()) { + mConnMgr = new HttpConnectionMgrParent(); + RefPtr self = this; + auto task = [self]() { + HttpConnectionMgrParent* parent = + self->mConnMgr->AsHttpConnectionMgrParent(); + Unused << SocketProcessParent::GetSingleton() + ->SendPHttpConnectionMgrConstructor( + parent, + HttpHandlerInitArgs( + self->mFastOpenSupported, self->mLegacyAppName, + self->mLegacyAppVersion, self->mPlatform, + self->mOscpu, self->mMisc, self->mProduct, + self->mProductSub, self->mAppName, + self->mAppVersion, self->mCompatFirefox, + self->mCompatDevice, self->mDeviceModelId)); + }; + gIOService->CallOrWaitForSocketProcess(std::move(task)); + } else { + MOZ_ASSERT(XRE_IsSocketProcess() || !nsIOService::UseSocketProcess()); + mConnMgr = new nsHttpConnectionMgr(); + } + + return mConnMgr->Init( + mMaxUrgentExcessiveConns, mMaxConnections, + mMaxPersistentConnectionsPerServer, mMaxPersistentConnectionsPerProxy, + mMaxRequestDelay, mThrottleEnabled, mThrottleVersion, mThrottleSuspendFor, + mThrottleResumeFor, mThrottleReadLimit, mThrottleReadInterval, + mThrottleHoldTime, mThrottleMaxTime, mBeConservativeForProxy); +} + +nsresult nsHttpHandler::AddStandardRequestHeaders( + nsHttpRequestHead* request, bool isSecure, + ExtContentPolicyType aContentPolicyType) { + nsresult rv; + + // Add the "User-Agent" header + rv = request->SetHeader(nsHttp::User_Agent, UserAgent(), false, + nsHttpHeaderArray::eVarietyRequestDefault); + if (NS_FAILED(rv)) return rv; + + // MIME based content negotiation lives! + // Add the "Accept" header. Note, this is set as an override because the + // service worker expects to see it. The other "default" headers are + // hidden from service worker interception. + nsAutoCString accept; + if (aContentPolicyType == ExtContentPolicy::TYPE_DOCUMENT || + aContentPolicyType == ExtContentPolicy::TYPE_SUBDOCUMENT) { + accept.Assign(mDocumentAcceptHeader); + } else if (aContentPolicyType == ExtContentPolicy::TYPE_IMAGE || + aContentPolicyType == ExtContentPolicy::TYPE_IMAGESET) { + accept.Assign(mImageAcceptHeader); + } else if (aContentPolicyType == ExtContentPolicy::TYPE_STYLESHEET) { + accept.Assign(ACCEPT_HEADER_STYLE); + } else { + accept.Assign(ACCEPT_HEADER_ALL); + } + + rv = request->SetHeader(nsHttp::Accept, accept, false, + nsHttpHeaderArray::eVarietyRequestOverride); + if (NS_FAILED(rv)) return rv; + + // Add the "Accept-Language" header. This header is also exposed to the + // service worker. + if (mAcceptLanguagesIsDirty) { + rv = SetAcceptLanguages(); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + + // Add the "Accept-Language" header + if (!mAcceptLanguages.IsEmpty()) { + rv = request->SetHeader(nsHttp::Accept_Language, mAcceptLanguages, false, + nsHttpHeaderArray::eVarietyRequestOverride); + if (NS_FAILED(rv)) return rv; + } + + // Add the "Accept-Encoding" header + if (isSecure) { + rv = request->SetHeader(nsHttp::Accept_Encoding, mHttpsAcceptEncodings, + false, nsHttpHeaderArray::eVarietyRequestDefault); + } else { + rv = request->SetHeader(nsHttp::Accept_Encoding, mHttpAcceptEncodings, + false, nsHttpHeaderArray::eVarietyRequestDefault); + } + if (NS_FAILED(rv)) return rv; + + // add the "Send Hint" header + if (mSafeHintEnabled || mParentalControlEnabled) { + rv = request->SetHeader(nsHttp::Prefer, "safe"_ns, false, + nsHttpHeaderArray::eVarietyRequestDefault); + if (NS_FAILED(rv)) return rv; + } + return NS_OK; +} + +nsresult nsHttpHandler::AddConnectionHeader(nsHttpRequestHead* request, + uint32_t caps) { + // RFC2616 section 19.6.2 states that the "Connection: keep-alive" + // and "Keep-alive" request headers should not be sent by HTTP/1.1 + // user-agents. But this is not a problem in practice, and the + // alternative proxy-connection is worse. see 570283 + + constexpr auto close = "close"_ns; + constexpr auto keepAlive = "keep-alive"_ns; + + const nsLiteralCString* connectionType = &close; + if (caps & NS_HTTP_ALLOW_KEEPALIVE) { + connectionType = &keepAlive; + } + + return request->SetHeader(nsHttp::Connection, *connectionType); +} + +bool nsHttpHandler::IsAcceptableEncoding(const char* enc, bool isSecure) { + if (!enc) return false; + + // we used to accept x-foo anytime foo was acceptable, but that's just + // continuing bad behavior.. so limit it to known x-* patterns + bool rv; + if (isSecure) { + rv = nsHttp::FindToken(mHttpsAcceptEncodings.get(), enc, HTTP_LWS ",") != + nullptr; + } else { + rv = nsHttp::FindToken(mHttpAcceptEncodings.get(), enc, HTTP_LWS ",") != + nullptr; + } + // gzip and deflate are inherently acceptable in modern HTTP - always + // process them if a stream converter can also be found. + if (!rv && + (!PL_strcasecmp(enc, "gzip") || !PL_strcasecmp(enc, "deflate") || + !PL_strcasecmp(enc, "x-gzip") || !PL_strcasecmp(enc, "x-deflate"))) { + rv = true; + } + LOG(("nsHttpHandler::IsAceptableEncoding %s https=%d %d\n", enc, isSecure, + rv)); + return rv; +} + +void nsHttpHandler::IncrementFastOpenConsecutiveFailureCounter() { + LOG( + ("nsHttpHandler::IncrementFastOpenConsecutiveFailureCounter - " + "failed=%d failure_limit=%d", + mFastOpenConsecutiveFailureCounter, mFastOpenConsecutiveFailureLimit)); + if (mFastOpenConsecutiveFailureCounter < mFastOpenConsecutiveFailureLimit) { + mFastOpenConsecutiveFailureCounter++; + if (mFastOpenConsecutiveFailureCounter == + mFastOpenConsecutiveFailureLimit) { + LOG( + ("nsHttpHandler::IncrementFastOpenConsecutiveFailureCounter - " + "Fast open failed too many times")); + } + } +} + +void nsHttpHandler::IncrementFastOpenStallsCounter() { + LOG( + ("nsHttpHandler::IncrementFastOpenStallsCounter - failed=%d " + "failure_limit=%d", + mFastOpenStallsCounter, mFastOpenStallsLimit)); + if (mFastOpenStallsCounter < mFastOpenStallsLimit) { + mFastOpenStallsCounter++; + if (mFastOpenStallsCounter == mFastOpenStallsLimit) { + LOG( + ("nsHttpHandler::IncrementFastOpenStallsCounter - " + "There are too many stalls involving TFO and TLS.")); + } + } +} + +nsresult nsHttpHandler::GetStreamConverterService( + nsIStreamConverterService** result) { + if (!mStreamConvSvc) { + nsresult rv; + nsCOMPtr service = + do_GetService(NS_STREAMCONVERTERSERVICE_CONTRACTID, &rv); + if (NS_FAILED(rv)) return rv; + mStreamConvSvc = new nsMainThreadPtrHolder( + "nsHttpHandler::mStreamConvSvc", service); + } + *result = do_AddRef(mStreamConvSvc.get()).take(); + return NS_OK; +} + +nsISiteSecurityService* nsHttpHandler::GetSSService() { + if (!mSSService) { + nsCOMPtr service = + do_GetService(NS_SSSERVICE_CONTRACTID); + mSSService = new nsMainThreadPtrHolder( + "nsHttpHandler::mSSService", service); + } + return mSSService; +} + +nsICookieService* nsHttpHandler::GetCookieService() { + if (!mCookieService) { + nsCOMPtr service = + do_GetService(NS_COOKIESERVICE_CONTRACTID); + mCookieService = new nsMainThreadPtrHolder( + "nsHttpHandler::mCookieService", service); + } + return mCookieService; +} + +nsresult nsHttpHandler::GetIOService(nsIIOService** result) { + NS_ENSURE_ARG_POINTER(result); + + *result = do_AddRef(mIOService.get()).take(); + return NS_OK; +} + +void nsHttpHandler::NotifyObservers(nsIChannel* chan, const char* event) { + LOG(("nsHttpHandler::NotifyObservers [chan=%p event=\"%s\"]\n", chan, event)); + nsCOMPtr obsService = services::GetObserverService(); + if (obsService) obsService->NotifyObservers(chan, event, nullptr); +} + +nsresult nsHttpHandler::AsyncOnChannelRedirect( + nsIChannel* oldChan, nsIChannel* newChan, uint32_t flags, + nsIEventTarget* mainThreadEventTarget) { + MOZ_ASSERT(NS_IsMainThread() && (oldChan && newChan)); + + nsCOMPtr oldURI; + oldChan->GetURI(getter_AddRefs(oldURI)); + MOZ_ASSERT(oldURI); + + nsCOMPtr newURI; + newChan->GetURI(getter_AddRefs(newURI)); + MOZ_ASSERT(newURI); + + AntiTrackingRedirectHeuristic(oldChan, oldURI, newChan, newURI); + + DynamicFpiRedirectHeuristic(oldChan, oldURI, newChan, newURI); + + // TODO E10S This helper has to be initialized on the other process + RefPtr redirectCallbackHelper = + new nsAsyncRedirectVerifyHelper(); + + return redirectCallbackHelper->Init(oldChan, newChan, flags, + mainThreadEventTarget); +} + +/* static */ +nsresult nsHttpHandler::GenerateHostPort(const nsCString& host, int32_t port, + nsACString& hostLine) { + return NS_GenerateHostPort(host, port, hostLine); +} + +//----------------------------------------------------------------------------- +// nsHttpHandler +//----------------------------------------------------------------------------- + +const nsCString& nsHttpHandler::UserAgent() { + if (nsContentUtils::ShouldResistFingerprinting() && + !mSpoofedUserAgent.IsEmpty()) { + LOG(("using spoofed userAgent : %s\n", mSpoofedUserAgent.get())); + return mSpoofedUserAgent; + } + + if (!mUserAgentOverride.IsVoid()) { + LOG(("using general.useragent.override : %s\n", mUserAgentOverride.get())); + return mUserAgentOverride; + } + + if (mUserAgentIsDirty) { + BuildUserAgent(); + mUserAgentIsDirty = false; + } + + return mUserAgent; +} + +void nsHttpHandler::BuildUserAgent() { + LOG(("nsHttpHandler::BuildUserAgent\n")); + + MOZ_ASSERT(!mLegacyAppName.IsEmpty() && !mLegacyAppVersion.IsEmpty(), + "HTTP cannot send practical requests without this much"); + + // preallocate to worst-case size, which should always be better + // than if we didn't preallocate at all. + mUserAgent.SetCapacity(mLegacyAppName.Length() + mLegacyAppVersion.Length() + +#ifndef UA_SPARE_PLATFORM + mPlatform.Length() + +#endif + mOscpu.Length() + mMisc.Length() + mProduct.Length() + + mProductSub.Length() + mAppName.Length() + + mAppVersion.Length() + mCompatFirefox.Length() + + mCompatDevice.Length() + mDeviceModelId.Length() + 13); + + // Application portion + mUserAgent.Assign(mLegacyAppName); + mUserAgent += '/'; + mUserAgent += mLegacyAppVersion; + mUserAgent += ' '; + + // Application comment + mUserAgent += '('; +#ifndef UA_SPARE_PLATFORM + if (!mPlatform.IsEmpty()) { + mUserAgent += mPlatform; + mUserAgent.AppendLiteral("; "); + } +#endif + if (!mCompatDevice.IsEmpty()) { + mUserAgent += mCompatDevice; + mUserAgent.AppendLiteral("; "); + } else if (!mOscpu.IsEmpty()) { + mUserAgent += mOscpu; + mUserAgent.AppendLiteral("; "); + } + if (!mDeviceModelId.IsEmpty()) { + mUserAgent += mDeviceModelId; + mUserAgent.AppendLiteral("; "); + } + mUserAgent += mMisc; + mUserAgent += ')'; + + // Product portion + mUserAgent += ' '; + mUserAgent += mProduct; + mUserAgent += '/'; + mUserAgent += mProductSub; + + bool isFirefox = mAppName.EqualsLiteral("Firefox"); + if (isFirefox || mCompatFirefoxEnabled) { + // "Firefox/x.y" (compatibility) app token + mUserAgent += ' '; + mUserAgent += mCompatFirefox; + } + if (!isFirefox) { + // App portion + mUserAgent += ' '; + mUserAgent += mAppName; + mUserAgent += '/'; + mUserAgent += mAppVersion; + } +} + +#ifdef XP_WIN +# define OSCPU_WINDOWS "Windows NT %ld.%ld" +# define OSCPU_WIN64 OSCPU_WINDOWS "; Win64; x64" +#endif + +void nsHttpHandler::InitUserAgentComponents() { + // Don't build user agent components in socket process, since the system info + // is not available. + if (XRE_IsSocketProcess()) { + mUserAgentIsDirty = true; + return; + } + +#ifndef MOZ_UA_OS_AGNOSTIC + // Gather platform. + mPlatform.AssignLiteral( +# if defined(ANDROID) + "Android" +# elif defined(XP_WIN) + "Windows" +# elif defined(XP_MACOSX) + "Macintosh" +# elif defined(XP_UNIX) + // We historically have always had X11 here, + // and there seems little a webpage can sensibly do + // based on it being something else, so use X11 for + // backwards compatibility in all cases. + "X11" +# endif + ); +#endif + +#ifdef ANDROID + nsCOMPtr infoService = + do_GetService("@mozilla.org/system-info;1"); + MOZ_ASSERT(infoService, "Could not find a system info service"); + nsresult rv; + // Add the Android version number to the Fennec platform identifier. +# if defined MOZ_WIDGET_ANDROID +# ifndef MOZ_UA_OS_AGNOSTIC // Don't add anything to mPlatform since it's + // empty. + nsAutoString androidVersion; + rv = infoService->GetPropertyAsAString(u"release_version"_ns, androidVersion); + if (NS_SUCCEEDED(rv)) { + mPlatform += " "; + // If the 2nd character is a ".", we know the major version is a single + // digit. If we're running on a version below 4 we pretend to be on + // Android KitKat (4.4) to work around scripts sniffing for low versions. + if (androidVersion[1] == 46 && androidVersion[0] < 52) { + mPlatform += "4.4"; + } else { + mPlatform += NS_LossyConvertUTF16toASCII(androidVersion); + } + } +# endif +# endif + // Add the `Mobile` or `Tablet` or `TV` token when running on device. + bool isTablet; + rv = infoService->GetPropertyAsBool(u"tablet"_ns, &isTablet); + if (NS_SUCCEEDED(rv) && isTablet) { + mCompatDevice.AssignLiteral("Tablet"); + } else { + bool isTV; + rv = infoService->GetPropertyAsBool(u"tv"_ns, &isTV); + if (NS_SUCCEEDED(rv) && isTV) { + mCompatDevice.AssignLiteral("TV"); + } else { + mCompatDevice.AssignLiteral("Mobile"); + } + } + + if (Preferences::GetBool(UA_PREF("use_device"), false)) { + mDeviceModelId = mozilla::net::GetDeviceModelId(); + } +#endif // ANDROID + +#ifndef MOZ_UA_OS_AGNOSTIC + // Gather OS/CPU. +# if defined(XP_WIN) + OSVERSIONINFO info = {sizeof(OSVERSIONINFO)}; +# pragma warning(push) +# pragma warning(disable : 4996) + if (GetVersionEx(&info)) { +# pragma warning(pop) + + const char* format; +# if defined _M_X64 || defined _M_AMD64 + format = OSCPU_WIN64; +# else + BOOL isWow64 = FALSE; + if (!IsWow64Process(GetCurrentProcess(), &isWow64)) { + isWow64 = FALSE; + } + format = isWow64 ? OSCPU_WIN64 : OSCPU_WINDOWS; +# endif + + SmprintfPointer buf = + mozilla::Smprintf(format, info.dwMajorVersion, info.dwMinorVersion); + if (buf) { + mOscpu = buf.get(); + } + } +# elif defined(XP_MACOSX) + SInt32 majorVersion = nsCocoaFeatures::macOSVersionMajor(); + SInt32 minorVersion = nsCocoaFeatures::macOSVersionMinor(); + + // Always return an "Intel" UA string, even on ARM64 macOS like Safari does. + mOscpu = + nsPrintfCString("Intel Mac OS X %d.%d", static_cast(majorVersion), + static_cast(minorVersion)); +# elif defined(XP_UNIX) + struct utsname name; + int ret = uname(&name); + if (ret >= 0) { + nsAutoCString buf; + buf = (char*)name.sysname; + buf += ' '; + +# ifdef AIX + // AIX uname returns machine specific info in the uname.machine + // field and does not return the cpu type like other platforms. + // We use the AIX version and release numbers instead. + buf += (char*)name.version; + buf += '.'; + buf += (char*)name.release; +# else + buf += (char*)name.machine; +# endif + + mOscpu.Assign(buf); + } +# endif +#endif + + mUserAgentIsDirty = true; +} + +uint32_t nsHttpHandler::MaxSocketCount() { + PR_CallOnce(&nsSocketTransportService::gMaxCountInitOnce, + nsSocketTransportService::DiscoverMaxCount); + // Don't use the full max count because sockets can be held in + // the persistent connection pool for a long time and that could + // starve other users. + + uint32_t maxCount = nsSocketTransportService::gMaxCount; + if (maxCount <= 8) + maxCount = 1; + else + maxCount -= 8; + + return maxCount; +} + +// static +void nsHttpHandler::PrefsChanged(const char* pref, void* self) { + static_cast(self)->PrefsChanged(pref); +} + +void nsHttpHandler::PrefsChanged(const char* pref) { + nsresult rv = NS_OK; + int32_t val; + + LOG(("nsHttpHandler::PrefsChanged [pref=%s]\n", pref)); + + if (pref) { + gIOService->NotifySocketProcessPrefsChanged(pref); + } + +#define PREF_CHANGED(p) ((pref == nullptr) || !PL_strcmp(pref, p)) +#define MULTI_PREF_CHANGED(p) \ + ((pref == nullptr) || !PL_strncmp(pref, p, sizeof(p) - 1)) + + // If a security pref changed, lets clear our connection pool reuse + if (MULTI_PREF_CHANGED(SECURITY_PREFIX)) { + LOG(("nsHttpHandler::PrefsChanged Security Pref Changed %s\n", pref)); + if (mConnMgr) { + rv = mConnMgr->DoShiftReloadConnectionCleanup(); + if (NS_FAILED(rv)) { + LOG( + ("nsHttpHandler::PrefsChanged " + "DoShiftReloadConnectionCleanup failed (%08x)\n", + static_cast(rv))); + } + rv = mConnMgr->PruneDeadConnections(); + if (NS_FAILED(rv)) { + LOG( + ("nsHttpHandler::PrefsChanged " + "PruneDeadConnections failed (%08x)\n", + static_cast(rv))); + } + } + } + + // + // UA components + // + + bool cVar = false; + + if (PREF_CHANGED(UA_PREF("compatMode.firefox"))) { + rv = Preferences::GetBool(UA_PREF("compatMode.firefox"), &cVar); + mCompatFirefoxEnabled = (NS_SUCCEEDED(rv) && cVar); + mUserAgentIsDirty = true; + } + + // general.useragent.override + if (PREF_CHANGED(UA_PREF("override"))) { + Preferences::GetCString(UA_PREF("override"), mUserAgentOverride); + mUserAgentIsDirty = true; + } + +#ifdef ANDROID + // general.useragent.use_device + if (PREF_CHANGED(UA_PREF("use_device"))) { + if (Preferences::GetBool(UA_PREF("use_device"), false)) { + if (!XRE_IsSocketProcess()) { + mDeviceModelId = mozilla::net::GetDeviceModelId(); + if (gIOService->SocketProcessReady()) { + Unused << SocketProcessParent::GetSingleton() + ->SendUpdateDeviceModelId(mDeviceModelId); + } + } + } else { + mDeviceModelId.Truncate(); + } + mUserAgentIsDirty = true; + } +#endif + + // + // HTTP options + // + + if (PREF_CHANGED(HTTP_PREF("keep-alive.timeout"))) { + rv = Preferences::GetInt(HTTP_PREF("keep-alive.timeout"), &val); + if (NS_SUCCEEDED(rv)) + mIdleTimeout = PR_SecondsToInterval(clamped(val, 1, 0xffff)); + } + + if (PREF_CHANGED(HTTP_PREF("request.max-attempts"))) { + rv = Preferences::GetInt(HTTP_PREF("request.max-attempts"), &val); + if (NS_SUCCEEDED(rv)) + mMaxRequestAttempts = (uint16_t)clamped(val, 1, 0xffff); + } + + if (PREF_CHANGED(HTTP_PREF("request.max-start-delay"))) { + rv = Preferences::GetInt(HTTP_PREF("request.max-start-delay"), &val); + if (NS_SUCCEEDED(rv)) { + mMaxRequestDelay = (uint16_t)clamped(val, 0, 0xffff); + if (mConnMgr) { + rv = mConnMgr->UpdateParam(nsHttpConnectionMgr::MAX_REQUEST_DELAY, + mMaxRequestDelay); + if (NS_FAILED(rv)) { + LOG( + ("nsHttpHandler::PrefsChanged (request.max-start-delay)" + "UpdateParam failed (%08x)\n", + static_cast(rv))); + } + } + } + } + + if (PREF_CHANGED(HTTP_PREF("response.timeout"))) { + rv = Preferences::GetInt(HTTP_PREF("response.timeout"), &val); + if (NS_SUCCEEDED(rv)) + mResponseTimeout = PR_SecondsToInterval(clamped(val, 0, 0xffff)); + } + + if (PREF_CHANGED(HTTP_PREF("network-changed.timeout"))) { + rv = Preferences::GetInt(HTTP_PREF("network-changed.timeout"), &val); + if (NS_SUCCEEDED(rv)) mNetworkChangedTimeout = clamped(val, 1, 600) * 1000; + } + + if (PREF_CHANGED(HTTP_PREF("max-connections"))) { + rv = Preferences::GetInt(HTTP_PREF("max-connections"), &val); + if (NS_SUCCEEDED(rv)) { + mMaxConnections = + (uint16_t)clamped((uint32_t)val, (uint32_t)1, MaxSocketCount()); + + if (mConnMgr) { + rv = mConnMgr->UpdateParam(nsHttpConnectionMgr::MAX_CONNECTIONS, + mMaxConnections); + if (NS_FAILED(rv)) { + LOG( + ("nsHttpHandler::PrefsChanged (max-connections)" + "UpdateParam failed (%08x)\n", + static_cast(rv))); + } + } + } + } + + if (PREF_CHANGED( + HTTP_PREF("max-urgent-start-excessive-connections-per-host"))) { + rv = Preferences::GetInt( + HTTP_PREF("max-urgent-start-excessive-connections-per-host"), &val); + if (NS_SUCCEEDED(rv)) { + mMaxUrgentExcessiveConns = (uint8_t)clamped(val, 1, 0xff); + if (mConnMgr) { + rv = mConnMgr->UpdateParam(nsHttpConnectionMgr::MAX_URGENT_START_Q, + mMaxUrgentExcessiveConns); + if (NS_FAILED(rv)) { + LOG( + ("nsHttpHandler::PrefsChanged " + "(max-urgent-start-excessive-connections-per-host)" + "UpdateParam failed (%08x)\n", + static_cast(rv))); + } + } + } + } + + if (PREF_CHANGED(HTTP_PREF("max-persistent-connections-per-server"))) { + rv = Preferences::GetInt(HTTP_PREF("max-persistent-connections-per-server"), + &val); + if (NS_SUCCEEDED(rv)) { + mMaxPersistentConnectionsPerServer = (uint8_t)clamped(val, 1, 0xff); + if (mConnMgr) { + rv = mConnMgr->UpdateParam( + nsHttpConnectionMgr::MAX_PERSISTENT_CONNECTIONS_PER_HOST, + mMaxPersistentConnectionsPerServer); + if (NS_FAILED(rv)) { + LOG( + ("nsHttpHandler::PrefsChanged " + "(max-persistent-connections-per-server)" + "UpdateParam failed (%08x)\n", + static_cast(rv))); + } + } + } + } + + if (PREF_CHANGED(HTTP_PREF("max-persistent-connections-per-proxy"))) { + rv = Preferences::GetInt(HTTP_PREF("max-persistent-connections-per-proxy"), + &val); + if (NS_SUCCEEDED(rv)) { + mMaxPersistentConnectionsPerProxy = (uint8_t)clamped(val, 1, 0xff); + if (mConnMgr) { + rv = mConnMgr->UpdateParam( + nsHttpConnectionMgr::MAX_PERSISTENT_CONNECTIONS_PER_PROXY, + mMaxPersistentConnectionsPerProxy); + if (NS_FAILED(rv)) { + LOG( + ("nsHttpHandler::PrefsChanged " + "(max-persistent-connections-per-proxy)" + "UpdateParam failed (%08x)\n", + static_cast(rv))); + } + } + } + } + + if (PREF_CHANGED(HTTP_PREF("redirection-limit"))) { + rv = Preferences::GetInt(HTTP_PREF("redirection-limit"), &val); + if (NS_SUCCEEDED(rv)) mRedirectionLimit = (uint8_t)clamped(val, 0, 0xff); + } + + if (PREF_CHANGED(HTTP_PREF("connection-retry-timeout"))) { + rv = Preferences::GetInt(HTTP_PREF("connection-retry-timeout"), &val); + if (NS_SUCCEEDED(rv)) mIdleSynTimeout = (uint16_t)clamped(val, 0, 3000); + } + + if (PREF_CHANGED(HTTP_PREF("fast-fallback-to-IPv4"))) { + rv = Preferences::GetBool(HTTP_PREF("fast-fallback-to-IPv4"), &cVar); + if (NS_SUCCEEDED(rv)) mFastFallbackToIPv4 = cVar; + } + + if (PREF_CHANGED(HTTP_PREF("fallback-connection-timeout"))) { + rv = Preferences::GetInt(HTTP_PREF("fallback-connection-timeout"), &val); + if (NS_SUCCEEDED(rv)) + mFallbackSynTimeout = (uint16_t)clamped(val, 0, 10 * 60); + } + + if (PREF_CHANGED(HTTP_PREF("version"))) { + nsAutoCString httpVersion; + Preferences::GetCString(HTTP_PREF("version"), httpVersion); + if (!httpVersion.IsVoid()) { + if (httpVersion.EqualsLiteral("1.1")) + mHttpVersion = HttpVersion::v1_1; + else if (httpVersion.EqualsLiteral("0.9")) + mHttpVersion = HttpVersion::v0_9; + else + mHttpVersion = HttpVersion::v1_0; + } + } + + if (PREF_CHANGED(HTTP_PREF("proxy.version"))) { + nsAutoCString httpVersion; + Preferences::GetCString(HTTP_PREF("proxy.version"), httpVersion); + if (!httpVersion.IsVoid()) { + if (httpVersion.EqualsLiteral("1.1")) + mProxyHttpVersion = HttpVersion::v1_1; + else + mProxyHttpVersion = HttpVersion::v1_0; + // it does not make sense to issue a HTTP/0.9 request to a proxy server + } + } + + if (PREF_CHANGED(HTTP_PREF("proxy.respect-be-conservative"))) { + rv = + Preferences::GetBool(HTTP_PREF("proxy.respect-be-conservative"), &cVar); + if (NS_SUCCEEDED(rv)) { + mBeConservativeForProxy = cVar; + if (mConnMgr) { + Unused << mConnMgr->UpdateParam( + nsHttpConnectionMgr::PROXY_BE_CONSERVATIVE, + static_cast(mBeConservativeForProxy)); + } + } + } + + if (PREF_CHANGED(HTTP_PREF("qos"))) { + rv = Preferences::GetInt(HTTP_PREF("qos"), &val); + if (NS_SUCCEEDED(rv)) mQoSBits = (uint8_t)clamped(val, 0, 0xff); + } + + if (PREF_CHANGED(HTTP_PREF("accept-encoding"))) { + nsAutoCString acceptEncodings; + rv = Preferences::GetCString(HTTP_PREF("accept-encoding"), acceptEncodings); + if (NS_SUCCEEDED(rv)) { + rv = SetAcceptEncodings(acceptEncodings.get(), false); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + } + + if (PREF_CHANGED(HTTP_PREF("accept-encoding.secure"))) { + nsAutoCString acceptEncodings; + rv = Preferences::GetCString(HTTP_PREF("accept-encoding.secure"), + acceptEncodings); + if (NS_SUCCEEDED(rv)) { + rv = SetAcceptEncodings(acceptEncodings.get(), true); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + } + + if (PREF_CHANGED(HTTP_PREF("default-socket-type"))) { + nsAutoCString sval; + rv = Preferences::GetCString(HTTP_PREF("default-socket-type"), sval); + if (NS_SUCCEEDED(rv)) { + if (sval.IsEmpty()) + mDefaultSocketType.SetIsVoid(true); + else { + // verify that this socket type is actually valid + nsCOMPtr sps = + nsSocketProviderService::GetOrCreate(); + if (sps) { + nsCOMPtr sp; + rv = sps->GetSocketProvider(sval.get(), getter_AddRefs(sp)); + if (NS_SUCCEEDED(rv)) { + // OK, this looks like a valid socket provider. + mDefaultSocketType.Assign(sval); + } + } + } + } + } + + if (PREF_CHANGED(HTTP_PREF("prompt-temp-redirect"))) { + rv = Preferences::GetBool(HTTP_PREF("prompt-temp-redirect"), &cVar); + if (NS_SUCCEEDED(rv)) { + mPromptTempRedirect = cVar; + } + } + + if (PREF_CHANGED(HTTP_PREF("assoc-req.enforce"))) { + cVar = false; + rv = Preferences::GetBool(HTTP_PREF("assoc-req.enforce"), &cVar); + if (NS_SUCCEEDED(rv)) mEnforceAssocReq = cVar; + } + + // enable Persistent caching for HTTPS - bug#205921 + if (PREF_CHANGED(BROWSER_PREF("disk_cache_ssl"))) { + cVar = false; + rv = Preferences::GetBool(BROWSER_PREF("disk_cache_ssl"), &cVar); + if (NS_SUCCEEDED(rv)) mEnablePersistentHttpsCaching = cVar; + } + + if (PREF_CHANGED(HTTP_PREF("phishy-userpass-length"))) { + rv = Preferences::GetInt(HTTP_PREF("phishy-userpass-length"), &val); + if (NS_SUCCEEDED(rv)) + mPhishyUserPassLength = (uint8_t)clamped(val, 0, 0xff); + } + + if (PREF_CHANGED(HTTP_PREF("spdy.enabled"))) { + rv = Preferences::GetBool(HTTP_PREF("spdy.enabled"), &cVar); + if (NS_SUCCEEDED(rv)) mEnableSpdy = cVar; + } + + if (PREF_CHANGED(HTTP_PREF("spdy.enabled.http2"))) { + rv = Preferences::GetBool(HTTP_PREF("spdy.enabled.http2"), &cVar); + if (NS_SUCCEEDED(rv)) mHttp2Enabled = cVar; + } + + if (PREF_CHANGED(HTTP_PREF("spdy.enabled.deps"))) { + rv = Preferences::GetBool(HTTP_PREF("spdy.enabled.deps"), &cVar); + if (NS_SUCCEEDED(rv)) mUseH2Deps = cVar; + } + + if (PREF_CHANGED(HTTP_PREF("spdy.enforce-tls-profile"))) { + rv = Preferences::GetBool(HTTP_PREF("spdy.enforce-tls-profile"), &cVar); + if (NS_SUCCEEDED(rv)) mEnforceHttp2TlsProfile = cVar; + } + + if (PREF_CHANGED(HTTP_PREF("spdy.coalesce-hostnames"))) { + rv = Preferences::GetBool(HTTP_PREF("spdy.coalesce-hostnames"), &cVar); + if (NS_SUCCEEDED(rv)) mCoalesceSpdy = cVar; + } + + if (PREF_CHANGED(HTTP_PREF("spdy.persistent-settings"))) { + rv = Preferences::GetBool(HTTP_PREF("spdy.persistent-settings"), &cVar); + if (NS_SUCCEEDED(rv)) mSpdyPersistentSettings = cVar; + } + + if (PREF_CHANGED(HTTP_PREF("spdy.timeout"))) { + rv = Preferences::GetInt(HTTP_PREF("spdy.timeout"), &val); + if (NS_SUCCEEDED(rv)) + mSpdyTimeout = PR_SecondsToInterval(clamped(val, 1, 0xffff)); + } + + if (PREF_CHANGED(HTTP_PREF("spdy.chunk-size"))) { + // keep this within http/2 ranges of 1 to 2^14-1 + rv = Preferences::GetInt(HTTP_PREF("spdy.chunk-size"), &val); + if (NS_SUCCEEDED(rv)) + mSpdySendingChunkSize = (uint32_t)clamped(val, 1, 0x3fff); + } + + // The amount of idle seconds on a spdy connection before initiating a + // server ping. 0 will disable. + if (PREF_CHANGED(HTTP_PREF("spdy.ping-threshold"))) { + rv = Preferences::GetInt(HTTP_PREF("spdy.ping-threshold"), &val); + if (NS_SUCCEEDED(rv)) + mSpdyPingThreshold = + PR_SecondsToInterval((uint16_t)clamped(val, 0, 0x7fffffff)); + } + + // The amount of seconds to wait for a spdy ping response before + // closing the session. + if (PREF_CHANGED(HTTP_PREF("spdy.ping-timeout"))) { + rv = Preferences::GetInt(HTTP_PREF("spdy.ping-timeout"), &val); + if (NS_SUCCEEDED(rv)) + mSpdyPingTimeout = + PR_SecondsToInterval((uint16_t)clamped(val, 0, 0x7fffffff)); + } + + if (PREF_CHANGED(HTTP_PREF("spdy.allow-push"))) { + rv = Preferences::GetBool(HTTP_PREF("spdy.allow-push"), &cVar); + if (NS_SUCCEEDED(rv)) mAllowPush = cVar; + } + + if (PREF_CHANGED(HTTP_PREF("altsvc.enabled"))) { + rv = Preferences::GetBool(HTTP_PREF("altsvc.enabled"), &cVar); + if (NS_SUCCEEDED(rv)) mEnableAltSvc = cVar; + } + + if (PREF_CHANGED(HTTP_PREF("altsvc.oe"))) { + rv = Preferences::GetBool(HTTP_PREF("altsvc.oe"), &cVar); + if (NS_SUCCEEDED(rv)) mEnableAltSvcOE = cVar; + } + + if (PREF_CHANGED(HTTP_PREF("originextension"))) { + rv = Preferences::GetBool(HTTP_PREF("originextension"), &cVar); + if (NS_SUCCEEDED(rv)) mEnableOriginExtension = cVar; + } + + if (PREF_CHANGED(HTTP_PREF("spdy.websockets"))) { + rv = Preferences::GetBool(HTTP_PREF("spdy.websockets"), &cVar); + if (NS_SUCCEEDED(rv)) { + mEnableH2Websockets = cVar; + } + } + + if (PREF_CHANGED(HTTP_PREF("spdy.push-allowance"))) { + rv = Preferences::GetInt(HTTP_PREF("spdy.push-allowance"), &val); + if (NS_SUCCEEDED(rv)) { + mSpdyPushAllowance = static_cast( + clamped(val, 1024, static_cast(ASpdySession::kInitialRwin))); + } + } + + if (PREF_CHANGED(HTTP_PREF("spdy.pull-allowance"))) { + rv = Preferences::GetInt(HTTP_PREF("spdy.pull-allowance"), &val); + if (NS_SUCCEEDED(rv)) { + mSpdyPullAllowance = + static_cast(clamped(val, 1024, 0x7fffffff)); + } + } + + if (PREF_CHANGED(HTTP_PREF("spdy.default-concurrent"))) { + rv = Preferences::GetInt(HTTP_PREF("spdy.default-concurrent"), &val); + if (NS_SUCCEEDED(rv)) { + mDefaultSpdyConcurrent = static_cast( + std::max(std::min(val, 9999), 1)); + } + } + + // The amount of seconds to wait for a spdy ping response before + // closing the session. + if (PREF_CHANGED(HTTP_PREF("spdy.send-buffer-size"))) { + rv = Preferences::GetInt(HTTP_PREF("spdy.send-buffer-size"), &val); + if (NS_SUCCEEDED(rv)) + mSpdySendBufferSize = (uint32_t)clamped(val, 1500, 0x7fffffff); + } + + if (PREF_CHANGED(HTTP_PREF("spdy.enable-hpack-dump"))) { + rv = Preferences::GetBool(HTTP_PREF("spdy.enable-hpack-dump"), &cVar); + if (NS_SUCCEEDED(rv)) { + mDumpHpackTables = cVar; + } + } + + // The maximum amount of time to wait for socket transport to be + // established + if (PREF_CHANGED(HTTP_PREF("connection-timeout"))) { + rv = Preferences::GetInt(HTTP_PREF("connection-timeout"), &val); + if (NS_SUCCEEDED(rv)) + // the pref is in seconds, but the variable is in milliseconds + mConnectTimeout = clamped(val, 1, 0xffff) * PR_MSEC_PER_SEC; + } + + // The maximum amount of time to wait for a tls handshake to finish. + if (PREF_CHANGED(HTTP_PREF("tls-handshake-timeout"))) { + rv = Preferences::GetInt(HTTP_PREF("tls-handshake-timeout"), &val); + if (NS_SUCCEEDED(rv)) + // the pref is in seconds, but the variable is in milliseconds + mTLSHandshakeTimeout = clamped(val, 1, 0xffff) * PR_MSEC_PER_SEC; + } + + // The maximum number of current global half open sockets allowable + // for starting a new speculative connection. + if (PREF_CHANGED(HTTP_PREF("speculative-parallel-limit"))) { + rv = Preferences::GetInt(HTTP_PREF("speculative-parallel-limit"), &val); + if (NS_SUCCEEDED(rv)) + mParallelSpeculativeConnectLimit = (uint32_t)clamped(val, 0, 1024); + } + + // Whether or not to block requests for non head js/css items (e.g. media) + // while those elements load. + if (PREF_CHANGED(HTTP_PREF("rendering-critical-requests-prioritization"))) { + rv = Preferences::GetBool( + HTTP_PREF("rendering-critical-requests-prioritization"), &cVar); + if (NS_SUCCEEDED(rv)) mCriticalRequestPrioritization = cVar; + } + + // on transition of network.http.diagnostics to true print + // a bunch of information to the console + if (pref && PREF_CHANGED(HTTP_PREF("diagnostics"))) { + rv = Preferences::GetBool(HTTP_PREF("diagnostics"), &cVar); + if (NS_SUCCEEDED(rv) && cVar) { + if (mConnMgr) mConnMgr->PrintDiagnostics(); + } + } + + if (PREF_CHANGED(HTTP_PREF("max_response_header_size"))) { + rv = Preferences::GetInt(HTTP_PREF("max_response_header_size"), &val); + if (NS_SUCCEEDED(rv)) { + mMaxHttpResponseHeaderSize = val; + } + } + + if (PREF_CHANGED(HTTP_PREF("throttle.enable"))) { + rv = Preferences::GetBool(HTTP_PREF("throttle.enable"), &mThrottleEnabled); + if (NS_SUCCEEDED(rv) && mConnMgr) { + Unused << mConnMgr->UpdateParam(nsHttpConnectionMgr::THROTTLING_ENABLED, + static_cast(mThrottleEnabled)); + } + } + + if (PREF_CHANGED(HTTP_PREF("throttle.version"))) { + Unused << Preferences::GetInt(HTTP_PREF("throttle.version"), &val); + mThrottleVersion = (uint32_t)clamped(val, 1, 2); + } + + if (PREF_CHANGED(HTTP_PREF("throttle.suspend-for"))) { + rv = Preferences::GetInt(HTTP_PREF("throttle.suspend-for"), &val); + mThrottleSuspendFor = (uint32_t)clamped(val, 0, 120000); + if (NS_SUCCEEDED(rv) && mConnMgr) { + Unused << mConnMgr->UpdateParam( + nsHttpConnectionMgr::THROTTLING_SUSPEND_FOR, mThrottleSuspendFor); + } + } + + if (PREF_CHANGED(HTTP_PREF("throttle.resume-for"))) { + rv = Preferences::GetInt(HTTP_PREF("throttle.resume-for"), &val); + mThrottleResumeFor = (uint32_t)clamped(val, 0, 120000); + if (NS_SUCCEEDED(rv) && mConnMgr) { + Unused << mConnMgr->UpdateParam( + nsHttpConnectionMgr::THROTTLING_RESUME_FOR, mThrottleResumeFor); + } + } + + if (PREF_CHANGED(HTTP_PREF("throttle.read-limit-bytes"))) { + rv = Preferences::GetInt(HTTP_PREF("throttle.read-limit-bytes"), &val); + mThrottleReadLimit = (uint32_t)clamped(val, 0, 500000); + if (NS_SUCCEEDED(rv) && mConnMgr) { + Unused << mConnMgr->UpdateParam( + nsHttpConnectionMgr::THROTTLING_READ_LIMIT, mThrottleReadLimit); + } + } + + if (PREF_CHANGED(HTTP_PREF("throttle.read-interval-ms"))) { + rv = Preferences::GetInt(HTTP_PREF("throttle.read-interval-ms"), &val); + mThrottleReadInterval = (uint32_t)clamped(val, 0, 120000); + if (NS_SUCCEEDED(rv) && mConnMgr) { + Unused << mConnMgr->UpdateParam( + nsHttpConnectionMgr::THROTTLING_READ_INTERVAL, mThrottleReadInterval); + } + } + + if (PREF_CHANGED(HTTP_PREF("throttle.hold-time-ms"))) { + rv = Preferences::GetInt(HTTP_PREF("throttle.hold-time-ms"), &val); + mThrottleHoldTime = (uint32_t)clamped(val, 0, 120000); + if (NS_SUCCEEDED(rv) && mConnMgr) { + Unused << mConnMgr->UpdateParam(nsHttpConnectionMgr::THROTTLING_HOLD_TIME, + mThrottleHoldTime); + } + } + + if (PREF_CHANGED(HTTP_PREF("throttle.max-time-ms"))) { + rv = Preferences::GetInt(HTTP_PREF("throttle.max-time-ms"), &val); + mThrottleMaxTime = (uint32_t)clamped(val, 0, 120000); + if (NS_SUCCEEDED(rv) && mConnMgr) { + Unused << mConnMgr->UpdateParam(nsHttpConnectionMgr::THROTTLING_MAX_TIME, + mThrottleMaxTime); + } + } + + if (PREF_CHANGED(HTTP_PREF("send_window_size"))) { + Unused << Preferences::GetInt(HTTP_PREF("send_window_size"), &val); + mSendWindowSize = val >= 0 ? val : 0; + } + + if (PREF_CHANGED(HTTP_PREF("on_click_priority"))) { + Unused << Preferences::GetBool(HTTP_PREF("on_click_priority"), + &mUrgentStartEnabled); + } + + if (PREF_CHANGED(HTTP_PREF("tailing.enabled"))) { + Unused << Preferences::GetBool(HTTP_PREF("tailing.enabled"), + &mTailBlockingEnabled); + } + if (PREF_CHANGED(HTTP_PREF("tailing.delay-quantum"))) { + Unused << Preferences::GetInt(HTTP_PREF("tailing.delay-quantum"), &val); + mTailDelayQuantum = (uint32_t)clamped(val, 0, 60000); + } + if (PREF_CHANGED(HTTP_PREF("tailing.delay-quantum-after-domcontentloaded"))) { + Unused << Preferences::GetInt( + HTTP_PREF("tailing.delay-quantum-after-domcontentloaded"), &val); + mTailDelayQuantumAfterDCL = (uint32_t)clamped(val, 0, 60000); + } + if (PREF_CHANGED(HTTP_PREF("tailing.delay-max"))) { + Unused << Preferences::GetInt(HTTP_PREF("tailing.delay-max"), &val); + mTailDelayMax = (uint32_t)clamped(val, 0, 60000); + } + if (PREF_CHANGED(HTTP_PREF("tailing.total-max"))) { + Unused << Preferences::GetInt(HTTP_PREF("tailing.total-max"), &val); + mTailTotalMax = (uint32_t)clamped(val, 0, 60000); + } + + if (PREF_CHANGED(HTTP_PREF("focused_window_transaction_ratio"))) { + float ratio = 0; + rv = Preferences::GetFloat(HTTP_PREF("focused_window_transaction_ratio"), + &ratio); + if (NS_SUCCEEDED(rv)) { + if (ratio > 0 && ratio < 1) { + mFocusedWindowTransactionRatio = ratio; + } else { + NS_WARNING("Wrong value for focused_window_transaction_ratio"); + } + } + } + + // + // INTL options + // + + if (PREF_CHANGED(INTL_ACCEPT_LANGUAGES)) { + // We don't want to set the new accept languages here since + // this pref is a complex type and it may be racy with flushing + // string resources. + mAcceptLanguagesIsDirty = true; + } + + // + // Tracking options + // + + // Hint option + if (PREF_CHANGED(SAFE_HINT_HEADER_VALUE)) { + cVar = false; + rv = Preferences::GetBool(SAFE_HINT_HEADER_VALUE, &cVar); + if (NS_SUCCEEDED(rv)) { + mSafeHintEnabled = cVar; + } + } + + // toggle to true anytime a token bucket related pref is changed.. that + // includes telemetry and allow-experiments because of the abtest profile + bool requestTokenBucketUpdated = false; + + // "security.ssl3.ecdhe_rsa_aes_128_gcm_sha256" is the required h2 interop + // suite. + + if (PREF_CHANGED(H2MANDATORY_SUITE)) { + cVar = false; + rv = Preferences::GetBool(H2MANDATORY_SUITE, &cVar); + if (NS_SUCCEEDED(rv)) { + mH2MandatorySuiteEnabled = cVar; + } + } + + // network.http.debug-observations + if (PREF_CHANGED("network.http.debug-observations")) { + cVar = false; + rv = Preferences::GetBool("network.http.debug-observations", &cVar); + if (NS_SUCCEEDED(rv)) { + mDebugObservations = cVar; + } + } + + if (PREF_CHANGED(HTTP_PREF("pacing.requests.enabled"))) { + rv = Preferences::GetBool(HTTP_PREF("pacing.requests.enabled"), &cVar); + if (NS_SUCCEEDED(rv)) { + mRequestTokenBucketEnabled = cVar; + requestTokenBucketUpdated = true; + } + } + if (PREF_CHANGED(HTTP_PREF("pacing.requests.min-parallelism"))) { + rv = + Preferences::GetInt(HTTP_PREF("pacing.requests.min-parallelism"), &val); + if (NS_SUCCEEDED(rv)) { + mRequestTokenBucketMinParallelism = + static_cast(clamped(val, 1, 1024)); + requestTokenBucketUpdated = true; + } + } + if (PREF_CHANGED(HTTP_PREF("pacing.requests.hz"))) { + rv = Preferences::GetInt(HTTP_PREF("pacing.requests.hz"), &val); + if (NS_SUCCEEDED(rv)) { + mRequestTokenBucketHz = static_cast(clamped(val, 1, 10000)); + requestTokenBucketUpdated = true; + } + } + if (PREF_CHANGED(HTTP_PREF("pacing.requests.burst"))) { + rv = Preferences::GetInt(HTTP_PREF("pacing.requests.burst"), &val); + if (NS_SUCCEEDED(rv)) { + mRequestTokenBucketBurst = val ? val : 1; + requestTokenBucketUpdated = true; + } + } + if (requestTokenBucketUpdated) { + MakeNewRequestTokenBucket(); + } + + // Keepalive values for initial and idle connections. + if (PREF_CHANGED(HTTP_PREF("tcp_keepalive.short_lived_connections"))) { + rv = Preferences::GetBool( + HTTP_PREF("tcp_keepalive.short_lived_connections"), &cVar); + if (NS_SUCCEEDED(rv) && cVar != mTCPKeepaliveShortLivedEnabled) { + mTCPKeepaliveShortLivedEnabled = cVar; + } + } + + if (PREF_CHANGED(HTTP_PREF("tcp_keepalive.short_lived_time"))) { + rv = Preferences::GetInt(HTTP_PREF("tcp_keepalive.short_lived_time"), &val); + if (NS_SUCCEEDED(rv) && val > 0) + mTCPKeepaliveShortLivedTimeS = clamped(val, 1, 300); // Max 5 mins. + } + + if (PREF_CHANGED(HTTP_PREF("tcp_keepalive.short_lived_idle_time"))) { + rv = Preferences::GetInt(HTTP_PREF("tcp_keepalive.short_lived_idle_time"), + &val); + if (NS_SUCCEEDED(rv) && val > 0) + mTCPKeepaliveShortLivedIdleTimeS = clamped(val, 1, kMaxTCPKeepIdle); + } + + // Keepalive values for Long-lived Connections. + if (PREF_CHANGED(HTTP_PREF("tcp_keepalive.long_lived_connections"))) { + rv = Preferences::GetBool(HTTP_PREF("tcp_keepalive.long_lived_connections"), + &cVar); + if (NS_SUCCEEDED(rv) && cVar != mTCPKeepaliveLongLivedEnabled) { + mTCPKeepaliveLongLivedEnabled = cVar; + } + } + + if (PREF_CHANGED(HTTP_PREF("tcp_keepalive.long_lived_idle_time"))) { + rv = Preferences::GetInt(HTTP_PREF("tcp_keepalive.long_lived_idle_time"), + &val); + if (NS_SUCCEEDED(rv) && val > 0) + mTCPKeepaliveLongLivedIdleTimeS = clamped(val, 1, kMaxTCPKeepIdle); + } + + if (PREF_CHANGED(HTTP_PREF("enforce-framing.http1")) || + PREF_CHANGED(HTTP_PREF("enforce-framing.soft")) || + PREF_CHANGED(HTTP_PREF("enforce-framing.strict_chunked_encoding"))) { + rv = Preferences::GetBool(HTTP_PREF("enforce-framing.http1"), &cVar); + if (NS_SUCCEEDED(rv) && cVar) { + mEnforceH1Framing = FRAMECHECK_STRICT; + } else { + rv = Preferences::GetBool( + HTTP_PREF("enforce-framing.strict_chunked_encoding"), &cVar); + if (NS_SUCCEEDED(rv) && cVar) { + mEnforceH1Framing = FRAMECHECK_STRICT_CHUNKED; + } else { + rv = Preferences::GetBool(HTTP_PREF("enforce-framing.soft"), &cVar); + if (NS_SUCCEEDED(rv) && cVar) { + mEnforceH1Framing = FRAMECHECK_BARELY; + } else { + mEnforceH1Framing = FRAMECHECK_LAX; + } + } + } + } + + if (PREF_CHANGED(TCP_FAST_OPEN_ENABLE)) { + rv = Preferences::GetBool(TCP_FAST_OPEN_ENABLE, &cVar); + if (NS_SUCCEEDED(rv)) { + mUseFastOpen = cVar; + } + } + + if (PREF_CHANGED(TCP_FAST_OPEN_FAILURE_LIMIT)) { + rv = Preferences::GetInt(TCP_FAST_OPEN_FAILURE_LIMIT, &val); + if (NS_SUCCEEDED(rv)) { + if (val < 0) { + val = 0; + } + mFastOpenConsecutiveFailureLimit = val; + } + } + + if (PREF_CHANGED(TCP_FAST_OPEN_STALLS_LIMIT)) { + rv = Preferences::GetInt(TCP_FAST_OPEN_STALLS_LIMIT, &val); + if (NS_SUCCEEDED(rv)) { + if (val < 0) { + val = 0; + } + mFastOpenStallsLimit = val; + } + } + + if (PREF_CHANGED(TCP_FAST_OPEN_STALLS_TIMEOUT)) { + rv = Preferences::GetInt(TCP_FAST_OPEN_STALLS_TIMEOUT, &val); + if (NS_SUCCEEDED(rv)) { + if (val < 0) { + val = 0; + } + mFastOpenStallsTimeout = val; + } + } + + if (PREF_CHANGED(TCP_FAST_OPEN_STALLS_IDLE)) { + rv = Preferences::GetInt(TCP_FAST_OPEN_STALLS_IDLE, &val); + if (NS_SUCCEEDED(rv)) { + if (val < 0) { + val = 0; + } + mFastOpenStallsIdleTime = val; + } + } + + if (PREF_CHANGED(HTTP_PREF("spdy.default-hpack-buffer"))) { + rv = Preferences::GetInt(HTTP_PREF("spdy.default-hpack-buffer"), &val); + if (NS_SUCCEEDED(rv)) { + mDefaultHpackBuffer = val; + } + } + + if (PREF_CHANGED(HTTP_PREF("spdy.bug1563538"))) { + rv = Preferences::GetBool(HTTP_PREF("spdy.bug1563538"), &cVar); + if (NS_SUCCEEDED(rv)) { + mBug1563538 = cVar; + } + } + + if (PREF_CHANGED(HTTP_PREF("http3.enabled"))) { + rv = Preferences::GetBool(HTTP_PREF("http3.enabled"), &cVar); + if (NS_SUCCEEDED(rv)) { + mHttp3Enabled = cVar; + } + } + + if (PREF_CHANGED(HTTP_PREF("http3.default-qpack-table-size"))) { + rv = Preferences::GetInt(HTTP_PREF("http3.default-qpack-table-size"), &val); + if (NS_SUCCEEDED(rv)) { + mQpackTableSize = val; + } + } + + if (PREF_CHANGED(HTTP_PREF("http3.default-max-stream-blocked"))) { + rv = Preferences::GetInt(HTTP_PREF("http3.default-max-stream-blocked"), + &val); + if (NS_SUCCEEDED(rv)) { + mHttp3MaxBlockedStreams = clamped(val, 0, 0xffff); + } + } + + const bool imageAcceptPrefChanged = PREF_CHANGED("image.http.accept") || + PREF_CHANGED("image.avif.enabled") || + PREF_CHANGED("image.webp.enabled"); + + if (imageAcceptPrefChanged) { + nsAutoCString userSetImageAcceptHeader; + + if (Preferences::HasUserValue("image.http.accept")) { + rv = Preferences::GetCString("image.http.accept", + userSetImageAcceptHeader); + if (NS_FAILED(rv)) { + userSetImageAcceptHeader.Truncate(); + } + } + + if (userSetImageAcceptHeader.IsEmpty()) { + mImageAcceptHeader.Assign(ImageAcceptHeader()); + } else { + mImageAcceptHeader.Assign(userSetImageAcceptHeader); + } + } + + if (PREF_CHANGED("network.http.accept") || imageAcceptPrefChanged) { + nsAutoCString userSetDocumentAcceptHeader; + + if (Preferences::HasUserValue("network.http.accept")) { + rv = Preferences::GetCString("network.http.accept", + userSetDocumentAcceptHeader); + if (NS_FAILED(rv)) { + userSetDocumentAcceptHeader.Truncate(); + } + } + + if (userSetDocumentAcceptHeader.IsEmpty()) { + mDocumentAcceptHeader.Assign(DocumentAcceptHeader(mImageAcceptHeader)); + } else { + mDocumentAcceptHeader.Assign(userSetDocumentAcceptHeader); + } + } + + if (PREF_CHANGED(HTTP_PREF("http3.alt-svc-mapping-for-testing"))) { + nsAutoCString altSvcMappings; + rv = Preferences::GetCString(HTTP_PREF("http3.alt-svc-mapping-for-testing"), + altSvcMappings); + if (NS_SUCCEEDED(rv)) { + for (const nsACString& tokenSubstring : + nsCCharSeparatedTokenizer(altSvcMappings, ',').ToRange()) { + nsAutoCString token{tokenSubstring}; + int32_t index = token.Find(";"); + if (index != kNotFound) { + auto* map = new nsCString(Substring(token, index + 1)); + mAltSvcMappingTemptativeMap.Put(Substring(token, 0, index), map); + } + } + } + } + + if (PREF_CHANGED(HTTP_PREF("http3.enable_qlog"))) { + // Initialize the directory. + nsCOMPtr qlogDir; + if (Preferences::GetBool(HTTP_PREF("http3.enable_qlog")) && + !mHttp3QlogDir.IsEmpty() && + NS_SUCCEEDED(NS_NewNativeLocalFile(mHttp3QlogDir, false, + getter_AddRefs(qlogDir)))) { + // Here we do main thread IO, but since this only happens + // when enabling a developer feature it's not a problem for users. + rv = qlogDir->Create(nsIFile::DIRECTORY_TYPE, 0755); + if (NS_FAILED(rv) && rv != NS_ERROR_FILE_ALREADY_EXISTS) { + NS_WARNING("Creating qlog dir failed"); + } + } + } + + // Enable HTTP response timeout if TCP Keepalives are disabled. + mResponseTimeoutEnabled = + !mTCPKeepaliveShortLivedEnabled && !mTCPKeepaliveLongLivedEnabled; + +#undef PREF_CHANGED +#undef MULTI_PREF_CHANGED +} + +/** + * Allocates a C string into that contains a ISO 639 language list + * notated with HTTP "q" values for output with a HTTP Accept-Language + * header. Previous q values will be stripped because the order of + * the langs imply the q value. The q values are calculated by dividing + * 1.0 amongst the number of languages present. + * + * Ex: passing: "en, ja" + * returns: "en,ja;q=0.5" + * + * passing: "en, ja, fr_CA" + * returns: "en,ja;q=0.7,fr_CA;q=0.3" + */ +static nsresult PrepareAcceptLanguages(const char* i_AcceptLanguages, + nsACString& o_AcceptLanguages) { + if (!i_AcceptLanguages) return NS_OK; + + const nsAutoCString ns_accept_languages(i_AcceptLanguages); + return rust_prepare_accept_languages(&ns_accept_languages, + &o_AcceptLanguages); +} + +nsresult nsHttpHandler::SetAcceptLanguages() { + if (!NS_IsMainThread()) { + nsCOMPtr mainThread; + nsresult rv = NS_GetMainThread(getter_AddRefs(mainThread)); + if (NS_FAILED(rv)) { + return rv; + } + + // Forward to the main thread synchronously. + SyncRunnable::DispatchToThread( + mainThread, new SyncRunnable(NS_NewRunnableFunction( + "nsHttpHandler::SetAcceptLanguages", + [&rv]() { rv = gHttpHandler->SetAcceptLanguages(); }))); + return rv; + } + + MOZ_ASSERT(NS_IsMainThread()); + + mAcceptLanguagesIsDirty = false; + + nsAutoCString acceptLanguages; + Preferences::GetLocalizedCString(INTL_ACCEPT_LANGUAGES, acceptLanguages); + + nsAutoCString buf; + nsresult rv = PrepareAcceptLanguages(acceptLanguages.get(), buf); + if (NS_SUCCEEDED(rv)) { + mAcceptLanguages.Assign(buf); + } + return rv; +} + +nsresult nsHttpHandler::SetAcceptEncodings(const char* aAcceptEncodings, + bool isSecure) { + if (isSecure) { + mHttpsAcceptEncodings = aAcceptEncodings; + } else { + // use legacy list if a secure override is not specified + mHttpAcceptEncodings = aAcceptEncodings; + if (mHttpsAcceptEncodings.IsEmpty()) { + mHttpsAcceptEncodings = aAcceptEncodings; + } + } + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsHttpHandler::nsISupports +//----------------------------------------------------------------------------- + +NS_IMPL_ISUPPORTS(nsHttpHandler, nsIHttpProtocolHandler, + nsIProxiedProtocolHandler, nsIProtocolHandler, nsIObserver, + nsISupportsWeakReference, nsISpeculativeConnect) + +//----------------------------------------------------------------------------- +// nsHttpHandler::nsIProtocolHandler +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsHttpHandler::GetScheme(nsACString& aScheme) { + aScheme.AssignLiteral("http"); + return NS_OK; +} + +NS_IMETHODIMP +nsHttpHandler::GetDefaultPort(int32_t* result) { + *result = NS_HTTP_DEFAULT_PORT; + return NS_OK; +} + +NS_IMETHODIMP +nsHttpHandler::GetProtocolFlags(uint32_t* result) { + *result = NS_HTTP_PROTOCOL_FLAGS; + return NS_OK; +} + +NS_IMETHODIMP +nsHttpHandler::NewChannel(nsIURI* uri, nsILoadInfo* aLoadInfo, + nsIChannel** result) { + LOG(("nsHttpHandler::NewChannel\n")); + + NS_ENSURE_ARG_POINTER(uri); + NS_ENSURE_ARG_POINTER(result); + + // Verify that we have been given a valid scheme + if (!uri->SchemeIs("http") && !uri->SchemeIs("https")) { + NS_WARNING("Invalid URI scheme"); + return NS_ERROR_UNEXPECTED; + } + + return NewProxiedChannel(uri, nullptr, 0, nullptr, aLoadInfo, result); +} + +NS_IMETHODIMP +nsHttpHandler::AllowPort(int32_t port, const char* scheme, bool* _retval) { + // don't override anything. + *_retval = false; + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsHttpHandler::nsIProxiedProtocolHandler +//----------------------------------------------------------------------------- + +nsresult nsHttpHandler::SetupChannelInternal( + HttpBaseChannel* aChannel, nsIURI* uri, nsIProxyInfo* givenProxyInfo, + uint32_t proxyResolveFlags, nsIURI* proxyURI, nsILoadInfo* aLoadInfo, + nsIChannel** result) { + RefPtr httpChannel = aChannel; + +#ifdef MOZ_TASK_TRACER + if (tasktracer::IsStartLogging()) { + nsAutoCString urispec; + uri->GetSpec(urispec); + tasktracer::AddLabel("nsHttpHandler::NewProxiedChannel2 %s", urispec.get()); + } +#endif + + nsCOMPtr proxyInfo; + if (givenProxyInfo) { + proxyInfo = do_QueryInterface(givenProxyInfo); + NS_ENSURE_ARG(proxyInfo); + } + + uint32_t caps = mCapabilities; + + uint64_t channelId; + nsresult rv = NewChannelId(channelId); + NS_ENSURE_SUCCESS(rv, rv); + + rv = httpChannel->Init(uri, caps, proxyInfo, proxyResolveFlags, proxyURI, + channelId, aLoadInfo->GetExternalContentPolicyType()); + if (NS_FAILED(rv)) return rv; + + // TRRServiceChannel doesn't need loadInfo. + if (aLoadInfo) { + // set the loadInfo on the new channel + rv = httpChannel->SetLoadInfo(aLoadInfo); + if (NS_FAILED(rv)) { + return rv; + } + } + + httpChannel.forget(result); + return NS_OK; +} + +NS_IMETHODIMP +nsHttpHandler::NewProxiedChannel(nsIURI* uri, nsIProxyInfo* givenProxyInfo, + uint32_t proxyResolveFlags, nsIURI* proxyURI, + nsILoadInfo* aLoadInfo, nsIChannel** result) { + HttpBaseChannel* httpChannel; + + LOG(("nsHttpHandler::NewProxiedChannel [proxyInfo=%p]\n", givenProxyInfo)); + + if (IsNeckoChild()) { + httpChannel = new HttpChannelChild(); + } else { + // HACK: make sure PSM gets initialized on the main thread. + net_EnsurePSMInit(); + httpChannel = new nsHttpChannel(); + } + + return SetupChannelInternal(httpChannel, uri, givenProxyInfo, + proxyResolveFlags, proxyURI, aLoadInfo, result); +} + +nsresult nsHttpHandler::CreateTRRServiceChannel( + nsIURI* uri, nsIProxyInfo* givenProxyInfo, uint32_t proxyResolveFlags, + nsIURI* proxyURI, nsILoadInfo* aLoadInfo, nsIChannel** result) { + HttpBaseChannel* httpChannel = new TRRServiceChannel(); + + LOG(("nsHttpHandler::CreateTRRServiceChannel [proxyInfo=%p]\n", + givenProxyInfo)); + + return SetupChannelInternal(httpChannel, uri, givenProxyInfo, + proxyResolveFlags, proxyURI, aLoadInfo, result); +} + +//----------------------------------------------------------------------------- +// nsHttpHandler::nsIHttpProtocolHandler +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsHttpHandler::GetUserAgent(nsACString& value) { + value = UserAgent(); + return NS_OK; +} + +NS_IMETHODIMP +nsHttpHandler::GetAppName(nsACString& value) { + value = mLegacyAppName; + return NS_OK; +} + +NS_IMETHODIMP +nsHttpHandler::GetAppVersion(nsACString& value) { + value = mLegacyAppVersion; + return NS_OK; +} + +NS_IMETHODIMP +nsHttpHandler::GetPlatform(nsACString& value) { + value = mPlatform; + return NS_OK; +} + +NS_IMETHODIMP +nsHttpHandler::GetOscpu(nsACString& value) { + value = mOscpu; + return NS_OK; +} + +NS_IMETHODIMP +nsHttpHandler::GetMisc(nsACString& value) { + value = mMisc; + return NS_OK; +} + +NS_IMETHODIMP +nsHttpHandler::GetAltSvcCacheKeys(nsTArray& value) { + return mAltSvcCache->GetAltSvcCacheKeys(value); +} + +NS_IMETHODIMP +nsHttpHandler::GetAuthCacheKeys(nsTArray& aValues) { + mAuthCache.CollectKeys(aValues); + mPrivateAuthCache.CollectKeys(aValues); + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsHttpHandler::nsIObserver +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsHttpHandler::Observe(nsISupports* subject, const char* topic, + const char16_t* data) { + MOZ_ASSERT(NS_IsMainThread()); + LOG(("nsHttpHandler::Observe [topic=\"%s\"]\n", topic)); + + nsresult rv; + if (!strcmp(topic, "profile-change-net-teardown") || + !strcmp(topic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)) { + mHandlerActive = false; + + // clear cache of all authentication credentials. + mAuthCache.ClearAll(); + mPrivateAuthCache.ClearAll(); + if (mWifiTickler) mWifiTickler->Cancel(); + + // Inform nsIOService that network is tearing down. + gIOService->SetHttpHandlerAlreadyShutingDown(); + + ShutdownConnectionManager(); + + // need to reset the session start time since cache validation may + // depend on this value. + mSessionStartTime = NowInSeconds(); + + // Since nsHttpHandler::Observe() is also called in socket process, we don't + // want to do telemetry twice. + if (XRE_IsParentProcess()) { + if (!StaticPrefs::privacy_donottrackheader_enabled()) { + Telemetry::Accumulate(Telemetry::DNT_USAGE, 2); + } else { + Telemetry::Accumulate(Telemetry::DNT_USAGE, 1); + } + + if (UseFastOpen()) { + Telemetry::Accumulate(Telemetry::TCP_FAST_OPEN_STATUS, 0); + } else if (!mFastOpenSupported) { + Telemetry::Accumulate(Telemetry::TCP_FAST_OPEN_STATUS, 1); + } else if (!mUseFastOpen) { + Telemetry::Accumulate(Telemetry::TCP_FAST_OPEN_STATUS, 2); + } else if (mFastOpenConsecutiveFailureCounter >= + mFastOpenConsecutiveFailureLimit) { + Telemetry::Accumulate(Telemetry::TCP_FAST_OPEN_STATUS, 3); + } else { + Telemetry::Accumulate(Telemetry::TCP_FAST_OPEN_STATUS, 4); + } + } + } else if (!strcmp(topic, "profile-change-net-restore")) { + // initialize connection manager + rv = InitConnectionMgr(); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + mAltSvcCache = MakeUnique(); + } else if (!strcmp(topic, "net:clear-active-logins")) { + mAuthCache.ClearAll(); + mPrivateAuthCache.ClearAll(); + } else if (!strcmp(topic, "net:cancel-all-connections")) { + if (mConnMgr) { + mConnMgr->AbortAndCloseAllConnections(0, nullptr); + } + } else if (!strcmp(topic, "net:prune-dead-connections")) { + if (mConnMgr) { + rv = mConnMgr->PruneDeadConnections(); + if (NS_FAILED(rv)) { + LOG((" PruneDeadConnections failed (%08x)\n", + static_cast(rv))); + } + } + } else if (!strcmp(topic, "net:prune-all-connections")) { + if (mConnMgr) { + rv = mConnMgr->DoShiftReloadConnectionCleanup(); + if (NS_FAILED(rv)) { + LOG((" DoShiftReloadConnectionCleanup failed (%08x)\n", + static_cast(rv))); + } + rv = mConnMgr->PruneDeadConnections(); + if (NS_FAILED(rv)) { + LOG((" PruneDeadConnections failed (%08x)\n", + static_cast(rv))); + } + } +#if 0 + } else if (!strcmp(topic, "net:failed-to-process-uri-content")) { + // nop right now - we used to cancel h1 pipelines based on this, + // but those are no longer implemented + nsCOMPtr uri = do_QueryInterface(subject); +#endif + } else if (!strcmp(topic, "last-pb-context-exited")) { + mPrivateAuthCache.ClearAll(); + if (mAltSvcCache) { + mAltSvcCache->ClearAltServiceMappings(); + } + } else if (!strcmp(topic, "browser:purge-session-history")) { + if (mConnMgr) { + Unused << mConnMgr->ClearConnectionHistory(); + } + if (mAltSvcCache) { + mAltSvcCache->ClearAltServiceMappings(); + } + } else if (!strcmp(topic, NS_NETWORK_LINK_TOPIC)) { + nsAutoCString converted = NS_ConvertUTF16toUTF8(data); + if (!strcmp(converted.get(), NS_NETWORK_LINK_DATA_CHANGED)) { + if (mConnMgr) { + rv = mConnMgr->PruneDeadConnections(); + if (NS_FAILED(rv)) { + LOG((" PruneDeadConnections failed (%08x)\n", + static_cast(rv))); + } + rv = mConnMgr->VerifyTraffic(); + if (NS_FAILED(rv)) { + LOG((" VerifyTraffic failed (%08x)\n", static_cast(rv))); + } + } + } + } else if (!strcmp(topic, "application-background")) { + // going to the background on android means we should close + // down idle connections for power conservation + if (mConnMgr) { + rv = mConnMgr->DoShiftReloadConnectionCleanup(); + if (NS_FAILED(rv)) { + LOG((" DoShiftReloadConnectionCleanup failed (%08x)\n", + static_cast(rv))); + } + } + } else if (!strcmp(topic, "net:current-toplevel-outer-content-windowid")) { + // The window id will be updated by HttpConnectionMgrParent. + if (XRE_IsParentProcess()) { + nsCOMPtr wrapper = do_QueryInterface(subject); + MOZ_RELEASE_ASSERT(wrapper); + + uint64_t windowId = 0; + wrapper->GetData(&windowId); + MOZ_ASSERT(windowId); + + static uint64_t sCurrentTopLevelOuterContentWindowId = 0; + if (sCurrentTopLevelOuterContentWindowId != windowId) { + sCurrentTopLevelOuterContentWindowId = windowId; + if (mConnMgr) { + mConnMgr->UpdateCurrentTopLevelOuterContentWindowId( + sCurrentTopLevelOuterContentWindowId); + } + } + } + } else if (!strcmp(topic, "captive-portal-login") || + !strcmp(topic, "captive-portal-login-success")) { + // We have detected a captive portal and we will reset the Fast Open + // failure counter. + ResetFastOpenConsecutiveFailureCounter(); + } else if (!strcmp(topic, "psm:user-certificate-added")) { + // A user certificate has just been added. + // We should immediately disable speculative connect + mSpeculativeConnectEnabled = false; + } else if (!strcmp(topic, "psm:user-certificate-deleted")) { + // If a user certificate has been removed, we need to check if there + // are others installed + MaybeEnableSpeculativeConnect(); + } else if (!strcmp(topic, "intl:app-locales-changed")) { + // If the locale changed, there's a chance the accept language did too + mAcceptLanguagesIsDirty = true; + } else if (!strcmp(topic, "browser-delayed-startup-finished")) { + MaybeEnableSpeculativeConnect(); + } else if (!strcmp(topic, "network:captive-portal-connectivity")) { + nsAutoCString data8 = NS_ConvertUTF16toUTF8(data); + mThroughCaptivePortal = data8.EqualsLiteral("captive"); + } else if (!strcmp(topic, "network:reset-http3-excluded-list")) { + MutexAutoLock lock(mHttpExclusionLock); + mExcludedHttp3Origins.Clear(); + } + + return NS_OK; +} + +// nsISpeculativeConnect + +static bool CanEnableSpeculativeConnect() { + nsCOMPtr component(do_GetService(PSM_COMPONENT_CONTRACTID)); + + MOZ_ASSERT(!NS_IsMainThread(), "Must run on the background thread"); + // Check if any 3rd party PKCS#11 module are installed, as they may produce + // client certificates + bool activeSmartCards = false; + nsresult rv = component->HasActiveSmartCards(&activeSmartCards); + if (NS_FAILED(rv) || activeSmartCards) { + return false; + } + + // If there are any client certificates installed, we can't enable speculative + // connect, as it may pop up the certificate chooser at any time. + bool hasUserCerts = false; + rv = component->HasUserCertsInstalled(&hasUserCerts); + if (NS_FAILED(rv) || hasUserCerts) { + return false; + } + + // No smart cards and no client certificates means + // we can enable speculative connect. + return true; +} + +void nsHttpHandler::MaybeEnableSpeculativeConnect() { + MOZ_ASSERT(NS_IsMainThread(), "Main thread only"); + + // We don't need to and can't check this in the child and socket process. + if (!XRE_IsParentProcess()) { + return; + } + + net_EnsurePSMInit(); + + NS_DispatchBackgroundTask( + NS_NewRunnableFunction("CanEnableSpeculativeConnect", [] { + gHttpHandler->mSpeculativeConnectEnabled = + CanEnableSpeculativeConnect(); + })); +} + +nsresult nsHttpHandler::SpeculativeConnectInternal( + nsIURI* aURI, nsIPrincipal* aPrincipal, nsIInterfaceRequestor* aCallbacks, + bool anonymous) { + if (IsNeckoChild()) { + gNeckoChild->SendSpeculativeConnect(aURI, aPrincipal, anonymous); + return NS_OK; + } + + if (!mHandlerActive) return NS_OK; + + MOZ_ASSERT(NS_IsMainThread()); + nsCOMPtr obsService = services::GetObserverService(); + if (mDebugObservations && obsService) { + // this is basically used for test coverage of an otherwise 'hintable' + // feature + obsService->NotifyObservers(nullptr, "speculative-connect-request", + nullptr); + for (auto* cp : + dom::ContentParent::AllProcesses(dom::ContentParent::eLive)) { + PNeckoParent* neckoParent = + SingleManagedOrNull(cp->ManagedPNeckoParent()); + if (!neckoParent) { + continue; + } + Unused << neckoParent->SendSpeculativeConnectRequest(); + } + } + + nsISiteSecurityService* sss = gHttpHandler->GetSSService(); + bool isStsHost = false; + if (!sss) return NS_OK; + + nsCOMPtr loadContext = do_GetInterface(aCallbacks); + uint32_t flags = 0; + if (loadContext && loadContext->UsePrivateBrowsing()) + flags |= nsISocketProvider::NO_PERMANENT_STORAGE; + + OriginAttributes originAttributes; + // If the principal is given, we use the originAttributes from this + // principal. Otherwise, we use the originAttributes from the loadContext. + if (aPrincipal) { + originAttributes = aPrincipal->OriginAttributesRef(); + } else if (loadContext) { + loadContext->GetOriginAttributes(originAttributes); + } + + StoragePrincipalHelper::UpdateOriginAttributesForNetworkState( + aURI, originAttributes); + + nsCOMPtr clone; + if (NS_SUCCEEDED(sss->IsSecureURI(nsISiteSecurityService::HEADER_HSTS, aURI, + flags, originAttributes, nullptr, nullptr, + &isStsHost)) && + isStsHost) { + if (NS_SUCCEEDED(NS_GetSecureUpgradedURI(aURI, getter_AddRefs(clone)))) { + aURI = clone.get(); + // (NOTE: We better make sure |clone| stays alive until the end + // of the function now, since our aURI arg now points to it!) + } + } + + nsAutoCString scheme; + nsresult rv = aURI->GetScheme(scheme); + if (NS_FAILED(rv)) return rv; + + // If this is HTTPS, make sure PSM is initialized as the channel + // creation path may have been bypassed + if (scheme.EqualsLiteral("https")) { + if (!IsNeckoChild()) { + // make sure PSM gets initialized on the main thread. + net_EnsurePSMInit(); + } + } + // Ensure that this is HTTP or HTTPS, otherwise we don't do preconnect here + else if (!scheme.EqualsLiteral("http")) + return NS_ERROR_UNEXPECTED; + + // Construct connection info object + if (aURI->SchemeIs("https") && !mSpeculativeConnectEnabled) { + return NS_ERROR_UNEXPECTED; + } + + nsAutoCString host; + rv = aURI->GetAsciiHost(host); + if (NS_FAILED(rv)) return rv; + + int32_t port = -1; + rv = aURI->GetPort(&port); + if (NS_FAILED(rv)) return rv; + + nsAutoCString username; + aURI->GetUsername(username); + + // TODO For now pass ""_ns for topWindowOrigin for all speculative + // connection attempts, but ideally we should pass the accurate top window + // origin here. This would require updating the nsISpeculativeConnect API + // and all of its consumers. + RefPtr ci = + new nsHttpConnectionInfo(host, port, ""_ns, username, ""_ns, nullptr, + originAttributes, aURI->SchemeIs("https")); + ci->SetAnonymous(anonymous); + + return SpeculativeConnect(ci, aCallbacks); +} + +NS_IMETHODIMP +nsHttpHandler::SpeculativeConnect(nsIURI* aURI, nsIPrincipal* aPrincipal, + nsIInterfaceRequestor* aCallbacks) { + return SpeculativeConnectInternal(aURI, aPrincipal, aCallbacks, false); +} + +NS_IMETHODIMP +nsHttpHandler::SpeculativeAnonymousConnect(nsIURI* aURI, + nsIPrincipal* aPrincipal, + nsIInterfaceRequestor* aCallbacks) { + return SpeculativeConnectInternal(aURI, aPrincipal, aCallbacks, true); +} + +void nsHttpHandler::TickleWifi(nsIInterfaceRequestor* cb) { + if (!cb || !mWifiTickler) return; + + // If B2G requires a similar mechanism nsINetworkManager, currently only avail + // on B2G, contains the necessary information on wifi and gateway + + nsCOMPtr domWindow = do_GetInterface(cb); + nsCOMPtr piWindow = do_QueryInterface(domWindow); + if (!piWindow) return; + + RefPtr navigator = piWindow->GetNavigator(); + if (!navigator) return; + + RefPtr networkProperties = + navigator->GetConnection(IgnoreErrors()); + if (!networkProperties) return; + + uint32_t gwAddress = networkProperties->GetDhcpGateway(); + bool isWifi = networkProperties->GetIsWifi(); + if (!gwAddress || !isWifi) return; + + mWifiTickler->SetIPV4Address(gwAddress); + mWifiTickler->Tickle(); +} + +//----------------------------------------------------------------------------- +// nsHttpsHandler implementation +//----------------------------------------------------------------------------- + +NS_IMPL_ISUPPORTS(nsHttpsHandler, nsIHttpProtocolHandler, + nsIProxiedProtocolHandler, nsIProtocolHandler, + nsISupportsWeakReference, nsISpeculativeConnect) + +nsresult nsHttpsHandler::Init() { + nsCOMPtr httpHandler( + do_GetService(NS_NETWORK_PROTOCOL_CONTRACTID_PREFIX "http")); + MOZ_ASSERT(httpHandler.get() != nullptr); + return NS_OK; +} + +NS_IMETHODIMP +nsHttpsHandler::GetScheme(nsACString& aScheme) { + aScheme.AssignLiteral("https"); + return NS_OK; +} + +NS_IMETHODIMP +nsHttpsHandler::GetDefaultPort(int32_t* aPort) { + *aPort = NS_HTTPS_DEFAULT_PORT; + return NS_OK; +} + +NS_IMETHODIMP +nsHttpsHandler::GetProtocolFlags(uint32_t* aProtocolFlags) { + *aProtocolFlags = NS_HTTP_PROTOCOL_FLAGS | URI_IS_POTENTIALLY_TRUSTWORTHY; + return NS_OK; +} + +NS_IMETHODIMP +nsHttpsHandler::NewChannel(nsIURI* aURI, nsILoadInfo* aLoadInfo, + nsIChannel** _retval) { + MOZ_ASSERT(gHttpHandler); + if (!gHttpHandler) return NS_ERROR_UNEXPECTED; + return gHttpHandler->NewChannel(aURI, aLoadInfo, _retval); +} + +NS_IMETHODIMP +nsHttpsHandler::AllowPort(int32_t aPort, const char* aScheme, bool* _retval) { + // don't override anything. + *_retval = false; + return NS_OK; +} + +NS_IMETHODIMP +nsHttpHandler::EnsureHSTSDataReadyNative( + RefPtr aCallback) { + MOZ_ASSERT(NS_IsMainThread()); + + nsCOMPtr uri; + nsresult rv = NS_NewURI(getter_AddRefs(uri), "http://example.com"); + NS_ENSURE_SUCCESS(rv, rv); + + bool shouldUpgrade = false; + bool willCallback = false; + OriginAttributes originAttributes; + auto func = [callback(aCallback)](bool aResult, nsresult aStatus) { + callback->DoCallback(aResult); + }; + rv = NS_ShouldSecureUpgrade(uri, nullptr, nullptr, false, false, + originAttributes, shouldUpgrade, std::move(func), + willCallback); + if (NS_FAILED(rv) || !willCallback) { + aCallback->DoCallback(false); + return rv; + } + + return rv; +} + +NS_IMETHODIMP +nsHttpHandler::EnsureHSTSDataReady(JSContext* aCx, Promise** aPromise) { + if (NS_WARN_IF(!aCx)) { + return NS_ERROR_FAILURE; + } + + nsIGlobalObject* globalObject = xpc::CurrentNativeGlobal(aCx); + if (NS_WARN_IF(!globalObject)) { + return NS_ERROR_FAILURE; + } + + ErrorResult result; + RefPtr promise = Promise::Create(globalObject, result); + if (NS_WARN_IF(result.Failed())) { + return result.StealNSResult(); + } + + if (IsNeckoChild()) { + gNeckoChild->SendEnsureHSTSData()->Then( + GetMainThreadSerialEventTarget(), __func__, + [promise(promise)]( + NeckoChild::EnsureHSTSDataPromise::ResolveOrRejectValue&& aResult) { + if (aResult.IsResolve()) { + promise->MaybeResolve(aResult.ResolveValue()); + } else { + promise->MaybeReject(NS_ERROR_FAILURE); + } + }); + promise.forget(aPromise); + return NS_OK; + } + + auto callback = [promise(promise)](bool aResult) { + promise->MaybeResolve(aResult); + }; + + RefPtr wrapper = + new HSTSDataCallbackWrapper(std::move(callback)); + promise.forget(aPromise); + return EnsureHSTSDataReadyNative(wrapper); +} + +void nsHttpHandler::ShutdownConnectionManager() { + // ensure connection manager is shutdown + if (mConnMgr) { + nsresult rv = mConnMgr->Shutdown(); + if (NS_FAILED(rv)) { + LOG( + ("nsHttpHandler::ShutdownConnectionManager\n" + " failed to shutdown connection manager\n")); + } + } +} + +nsresult nsHttpHandler::NewChannelId(uint64_t& channelId) { + channelId = + // channelId is sometimes passed to JavaScript code (e.g. devtools), + // and since on Linux PID_MAX_LIMIT is 2^22 we cannot + // shift PID more than 31 bits left. Otherwise resulting values + // will be exceed safe JavaScript integer range. + ((static_cast(mProcessId) << 31) & 0xFFFFFFFF80000000LL) | + mNextChannelId++; + return NS_OK; +} + +void nsHttpHandler::NotifyActiveTabLoadOptimization() { + SetLastActiveTabLoadOptimizationHit(TimeStamp::Now()); +} + +TimeStamp const nsHttpHandler::GetLastActiveTabLoadOptimizationHit() { + MutexAutoLock lock(mLastActiveTabLoadOptimizationLock); + + return mLastActiveTabLoadOptimizationHit; +} + +void nsHttpHandler::SetLastActiveTabLoadOptimizationHit(TimeStamp const& when) { + MutexAutoLock lock(mLastActiveTabLoadOptimizationLock); + + if (mLastActiveTabLoadOptimizationHit.IsNull() || + (!when.IsNull() && mLastActiveTabLoadOptimizationHit < when)) { + mLastActiveTabLoadOptimizationHit = when; + } +} + +bool nsHttpHandler::IsBeforeLastActiveTabLoadOptimization( + TimeStamp const& when) { + MutexAutoLock lock(mLastActiveTabLoadOptimizationLock); + + return !mLastActiveTabLoadOptimizationHit.IsNull() && + when <= mLastActiveTabLoadOptimizationHit; +} + +void nsHttpHandler::ExcludeHttp2(const nsHttpConnectionInfo* ci) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + mConnMgr->ExcludeHttp2(ci); + if (!mExcludedHttp2Origins.Contains(ci->GetOrigin())) { + MutexAutoLock lock(mHttpExclusionLock); + mExcludedHttp2Origins.PutEntry(ci->GetOrigin()); + } +} + +bool nsHttpHandler::IsHttp2Excluded(const nsHttpConnectionInfo* ci) { + MutexAutoLock lock(mHttpExclusionLock); + return mExcludedHttp2Origins.Contains(ci->GetOrigin()); +} + +void nsHttpHandler::ExcludeHttp3(const nsHttpConnectionInfo* ci) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + mConnMgr->ExcludeHttp3(ci); + if (!mExcludedHttp3Origins.Contains(ci->GetRoutedHost())) { + MutexAutoLock lock(mHttpExclusionLock); + mExcludedHttp3Origins.PutEntry(ci->GetRoutedHost()); + } +} + +bool nsHttpHandler::IsHttp3Excluded(const nsACString& aRoutedHost) { + MutexAutoLock lock(mHttpExclusionLock); + return mExcludedHttp3Origins.Contains(aRoutedHost); +} + +HttpTrafficAnalyzer* nsHttpHandler::GetHttpTrafficAnalyzer() { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + if (!StaticPrefs::network_traffic_analyzer_enabled()) { + return nullptr; + } + + return &mHttpTrafficAnalyzer; +} + +bool nsHttpHandler::IsHttp3VersionSupported(const nsACString& version) { + for (uint32_t i = 0; i < kHttp3VersionCount; i++) { + if (version.Equals(kHttp3Versions[i])) { + return true; + } + } + return false; +} + +nsresult nsHttpHandler::InitiateTransaction(HttpTransactionShell* aTrans, + int32_t aPriority) { + return mConnMgr->AddTransaction(aTrans, aPriority); +} +nsresult nsHttpHandler::InitiateTransactionWithStickyConn( + HttpTransactionShell* aTrans, int32_t aPriority, + HttpTransactionShell* aTransWithStickyConn) { + return mConnMgr->AddTransactionWithStickyConn(aTrans, aPriority, + aTransWithStickyConn); +} + +nsresult nsHttpHandler::RescheduleTransaction(HttpTransactionShell* trans, + int32_t priority) { + return mConnMgr->RescheduleTransaction(trans, priority); +} + +void nsHttpHandler::UpdateClassOfServiceOnTransaction( + HttpTransactionShell* trans, uint32_t classOfService) { + mConnMgr->UpdateClassOfServiceOnTransaction(trans, classOfService); +} + +nsresult nsHttpHandler::CancelTransaction(HttpTransactionShell* trans, + nsresult reason) { + return mConnMgr->CancelTransaction(trans, reason); +} + +void nsHttpHandler::AddHttpChannel(uint64_t aId, nsISupports* aChannel) { + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(NS_IsMainThread()); + + nsWeakPtr channel(do_GetWeakReference(aChannel)); + mIDToHttpChannelMap.Put(aId, std::move(channel)); +} + +void nsHttpHandler::RemoveHttpChannel(uint64_t aId) { + MOZ_ASSERT(XRE_IsParentProcess()); + if (!NS_IsMainThread()) { + nsCOMPtr idleRunnable(NewCancelableRunnableMethod( + "nsHttpHandler::RemoveHttpChannel", this, + &nsHttpHandler::RemoveHttpChannel, aId)); + + NS_DispatchToMainThreadQueue(do_AddRef(idleRunnable), + EventQueuePriority::Idle); + return; + } + + auto entry = mIDToHttpChannelMap.Lookup(aId); + if (entry) { + entry.Remove(); + } +} + +nsWeakPtr nsHttpHandler::GetWeakHttpChannel(uint64_t aId) { + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(NS_IsMainThread()); + + return mIDToHttpChannelMap.Get(aId); +} + +nsresult nsHttpHandler::CompleteUpgrade( + HttpTransactionShell* aTrans, nsIHttpUpgradeListener* aUpgradeListener) { + return mConnMgr->CompleteUpgrade(aTrans, aUpgradeListener); +} + +nsresult nsHttpHandler::DoShiftReloadConnectionCleanupWithConnInfo( + nsHttpConnectionInfo* aCi) { + MOZ_ASSERT(aCi); + return mConnMgr->DoShiftReloadConnectionCleanupWithConnInfo(aCi); +} + +void nsHttpHandler::ClearHostMapping(nsHttpConnectionInfo* aConnInfo) { + if (XRE_IsSocketProcess()) { + AltServiceChild::ClearHostMapping(aConnInfo); + return; + } + + AltServiceCache()->ClearHostMapping(aConnInfo); +} + +void nsHttpHandler::SetHttpHandlerInitArgs(const HttpHandlerInitArgs& aArgs) { + MOZ_ASSERT(XRE_IsSocketProcess()); + mFastOpenSupported = aArgs.mFastOpenSupported(); + mLegacyAppName = aArgs.mLegacyAppName(); + mLegacyAppVersion = aArgs.mLegacyAppVersion(); + mPlatform = aArgs.mPlatform(); + mOscpu = aArgs.mOscpu(); + mMisc = aArgs.mMisc(); + mProduct = aArgs.mProduct(); + mProductSub = aArgs.mProductSub(); + mAppName = aArgs.mAppName(); + mAppVersion = aArgs.mAppVersion(); + mCompatFirefox = aArgs.mCompatFirefox(); + mCompatDevice = aArgs.mCompatDevice(); + mDeviceModelId = aArgs.mDeviceModelId(); +} + +void nsHttpHandler::SetDeviceModelId(const nsCString& aModelId) { + MOZ_ASSERT(XRE_IsSocketProcess()); + mDeviceModelId = aModelId; +} + +void nsHttpHandler::MaybeAddAltSvcForTesting( + nsIURI* aUri, const nsACString& aUsername, + const nsACString& aTopWindowOrigin, bool aPrivateBrowsing, bool aIsolated, + nsIInterfaceRequestor* aCallbacks, + const OriginAttributes& aOriginAttributes) { + if (!IsHttp3Enabled() || mAltSvcMappingTemptativeMap.IsEmpty()) { + return; + } + + bool isHttps = false; + if (NS_FAILED(aUri->SchemeIs("https", &isHttps)) || !isHttps) { + // Only set forr HTTPS. + return; + } + + nsAutoCString originHost; + if (NS_FAILED(aUri->GetAsciiHost(originHost))) { + return; + } + + nsCString* map = mAltSvcMappingTemptativeMap.Get(originHost); + if (map) { + int32_t originPort = 80; + aUri->GetPort(&originPort); + LOG(("nsHttpHandler::MaybeAddAltSvcForTesting for %s map: %s", + originHost.get(), PromiseFlatCString(*map).get())); + AltSvcMapping::ProcessHeader(*map, nsCString("https"), originHost, + originPort, aUsername, aTopWindowOrigin, + aPrivateBrowsing, aIsolated, aCallbacks, + nullptr, 0, aOriginAttributes, true); + } +} + +bool nsHttpHandler::UseHTTPSRRAsAltSvcEnabled() const { + return StaticPrefs::network_dns_use_https_rr_as_altsvc(); +} + +bool nsHttpHandler::EchConfigEnabled() const { + return StaticPrefs::network_dns_echconfig_enabled(); +} + +bool nsHttpHandler::FallbackToOriginIfConfigsAreECHAndAllFailed() const { + return StaticPrefs:: + network_dns_echconfig_fallback_to_origin_when_all_failed(); +} + +bool nsHttpHandler::UseHTTPSRRForSpeculativeConnection() const { + return StaticPrefs::network_dns_use_https_rr_for_speculative_connection(); +} + +} // namespace mozilla::net diff --git a/netwerk/protocol/http/nsHttpHandler.h b/netwerk/protocol/http/nsHttpHandler.h new file mode 100644 index 0000000000..30a2818e46 --- /dev/null +++ b/netwerk/protocol/http/nsHttpHandler.h @@ -0,0 +1,939 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef nsHttpHandler_h__ +#define nsHttpHandler_h__ + +#include + +#include "nsHttp.h" +#include "nsHttpAuthCache.h" +#include "nsHttpConnectionMgr.h" +#include "AlternateServices.h" +#include "ASpdySession.h" +#include "HttpTrafficAnalyzer.h" + +#include "mozilla/Mutex.h" +#include "mozilla/StaticPtr.h" +#include "mozilla/TimeStamp.h" +#include "nsString.h" +#include "nsCOMPtr.h" +#include "nsWeakReference.h" + +#include "nsIHttpProtocolHandler.h" +#include "nsIObserver.h" +#include "nsISpeculativeConnect.h" +#include "nsDataHashtable.h" +#ifdef DEBUG +# include "nsIOService.h" +#endif + +// XXX These includes can be replaced by forward declarations by moving the On* +// method implementations to the cpp file +#include "nsIChannel.h" +#include "nsIHttpChannel.h" + +class nsIHttpUpgradeListener; +class nsIPrefBranch; +class nsICancelable; +class nsICookieService; +class nsIIOService; +class nsIRequestContextService; +class nsISiteSecurityService; +class nsIStreamConverterService; + +namespace mozilla { +namespace net { + +bool OnSocketThread(); + +class ATokenBucketEvent; +class EventTokenBucket; +class Tickler; +class nsHttpConnection; +class nsHttpConnectionInfo; +class HttpBaseChannel; +class HttpHandlerInitArgs; +class HttpTransactionShell; +class AltSvcMapping; +class TRR; +class TRRServiceChannel; +class SocketProcessChild; + +/* + * FRAMECHECK_LAX - no check + * FRAMECHECK_BARELY - allows: + * 1) that chunk-encoding does not have the last 0-size + * chunk. So, if a chunked-encoded transfer ends on exactly + * a chunk boundary we consider that fine. This will allows + * us to accept buggy servers that do not send the last + * chunk. It will make us not detect a certain amount of + * cut-offs. + * 2) When receiving a gzipped response, we consider a + * gzip stream that doesn't end fine according to the gzip + * decompressing state machine to be a partial transfer. + * If a gzipped transfer ends fine according to the + * decompressor, we do not check for size unalignments. + * This allows to allow HTTP gzipped responses where the + * Content-Length is not the same as the actual contents. + * 3) When receiving HTTP that isn't + * content-encoded/compressed (like in case 2) and not + * chunked (like in case 1), perform the size comparison + * between Content-Length: and the actual size received + * and consider a mismatch to mean a + * NS_ERROR_NET_PARTIAL_TRANSFER error. + * FRAMECHECK_STRICT_CHUNKED - This is the same as FRAMECHECK_BARELY only we + * enforce that the last 0-size chunk is received + * in case 1). + * FRAMECHECK_STRICT - we also do not allow case 2) and 3) from + * FRAMECHECK_BARELY. + */ +enum FrameCheckLevel { + FRAMECHECK_LAX, + FRAMECHECK_BARELY, + FRAMECHECK_STRICT_CHUNKED, + FRAMECHECK_STRICT +}; + +//----------------------------------------------------------------------------- +// nsHttpHandler - protocol handler for HTTP and HTTPS +//----------------------------------------------------------------------------- + +class nsHttpHandler final : public nsIHttpProtocolHandler, + public nsIObserver, + public nsSupportsWeakReference, + public nsISpeculativeConnect { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIPROTOCOLHANDLER + NS_DECL_NSIPROXIEDPROTOCOLHANDLER + NS_DECL_NSIHTTPPROTOCOLHANDLER + NS_DECL_NSIOBSERVER + NS_DECL_NSISPECULATIVECONNECT + + static already_AddRefed GetInstance(); + + [[nodiscard]] nsresult AddStandardRequestHeaders( + nsHttpRequestHead*, bool isSecure, + ExtContentPolicyType aContentPolicyType); + [[nodiscard]] nsresult AddConnectionHeader(nsHttpRequestHead*, + uint32_t capabilities); + bool IsAcceptableEncoding(const char* encoding, bool isSecure); + + const nsCString& UserAgent(); + + enum HttpVersion HttpVersion() { return mHttpVersion; } + enum HttpVersion ProxyHttpVersion() { return mProxyHttpVersion; } + uint8_t RedirectionLimit() { return mRedirectionLimit; } + PRIntervalTime IdleTimeout() { return mIdleTimeout; } + PRIntervalTime SpdyTimeout() { return mSpdyTimeout; } + PRIntervalTime ResponseTimeout() { + return mResponseTimeoutEnabled ? mResponseTimeout : 0; + } + PRIntervalTime ResponseTimeoutEnabled() { return mResponseTimeoutEnabled; } + uint32_t NetworkChangedTimeout() { return mNetworkChangedTimeout; } + uint16_t MaxRequestAttempts() { return mMaxRequestAttempts; } + const nsCString& DefaultSocketType() { return mDefaultSocketType; } + uint32_t PhishyUserPassLength() { return mPhishyUserPassLength; } + uint8_t GetQoSBits() { return mQoSBits; } + uint16_t GetIdleSynTimeout() { return mIdleSynTimeout; } + uint16_t GetFallbackSynTimeout() { return mFallbackSynTimeout; } + bool FastFallbackToIPv4() { return mFastFallbackToIPv4; } + uint32_t MaxSocketCount(); + bool EnforceAssocReq() { return mEnforceAssocReq; } + + bool IsPersistentHttpsCachingEnabled() { + return mEnablePersistentHttpsCaching; + } + + bool IsSpdyEnabled() { return mEnableSpdy; } + bool IsHttp2Enabled() { return mHttp2Enabled; } + bool EnforceHttp2TlsProfile() { return mEnforceHttp2TlsProfile; } + bool CoalesceSpdy() { return mCoalesceSpdy; } + bool UseSpdyPersistentSettings() { return mSpdyPersistentSettings; } + uint32_t SpdySendingChunkSize() { return mSpdySendingChunkSize; } + uint32_t SpdySendBufferSize() { return mSpdySendBufferSize; } + uint32_t SpdyPushAllowance() { return mSpdyPushAllowance; } + uint32_t SpdyPullAllowance() { return mSpdyPullAllowance; } + uint32_t DefaultSpdyConcurrent() { return mDefaultSpdyConcurrent; } + PRIntervalTime SpdyPingThreshold() { return mSpdyPingThreshold; } + PRIntervalTime SpdyPingTimeout() { return mSpdyPingTimeout; } + bool AllowPush() { return mAllowPush; } + bool AllowAltSvc() { return mEnableAltSvc; } + bool AllowAltSvcOE() { return mEnableAltSvcOE; } + bool AllowOriginExtension() { return mEnableOriginExtension; } + uint32_t ConnectTimeout() { return mConnectTimeout; } + uint32_t TLSHandshakeTimeout() { return mTLSHandshakeTimeout; } + uint32_t ParallelSpeculativeConnectLimit() { + return mParallelSpeculativeConnectLimit; + } + bool CriticalRequestPrioritization() { + return mCriticalRequestPrioritization; + } + + bool UseH2Deps() { return mUseH2Deps; } + bool IsH2WebsocketsEnabled() { return mEnableH2Websockets; } + + uint32_t MaxConnectionsPerOrigin() { + return mMaxPersistentConnectionsPerServer; + } + bool UseRequestTokenBucket() { return mRequestTokenBucketEnabled; } + uint16_t RequestTokenBucketMinParallelism() { + return mRequestTokenBucketMinParallelism; + } + uint32_t RequestTokenBucketHz() { return mRequestTokenBucketHz; } + uint32_t RequestTokenBucketBurst() { return mRequestTokenBucketBurst; } + + bool PromptTempRedirect() { return mPromptTempRedirect; } + bool IsUrgentStartEnabled() { return mUrgentStartEnabled; } + bool IsTailBlockingEnabled() { return mTailBlockingEnabled; } + uint32_t TailBlockingDelayQuantum(bool aAfterDOMContentLoaded) { + return aAfterDOMContentLoaded ? mTailDelayQuantumAfterDCL + : mTailDelayQuantum; + } + uint32_t TailBlockingDelayMax() { return mTailDelayMax; } + uint32_t TailBlockingTotalMax() { return mTailTotalMax; } + + uint32_t ThrottlingReadLimit() { + return mThrottleVersion == 1 ? 0 : mThrottleReadLimit; + } + int32_t SendWindowSize() { return mSendWindowSize * 1024; } + + // TCP Keepalive configuration values. + + // Returns true if TCP keepalive should be enabled for short-lived conns. + bool TCPKeepaliveEnabledForShortLivedConns() { + return mTCPKeepaliveShortLivedEnabled; + } + // Return time (secs) that a connection is consider short lived (for TCP + // keepalive purposes). After this time, the connection is long-lived. + int32_t GetTCPKeepaliveShortLivedTime() { + return mTCPKeepaliveShortLivedTimeS; + } + // Returns time (secs) before first TCP keepalive probes should be sent; + // same time used between successful keepalive probes. + int32_t GetTCPKeepaliveShortLivedIdleTime() { + return mTCPKeepaliveShortLivedIdleTimeS; + } + + // Returns true if TCP keepalive should be enabled for long-lived conns. + bool TCPKeepaliveEnabledForLongLivedConns() { + return mTCPKeepaliveLongLivedEnabled; + } + // Returns time (secs) before first TCP keepalive probes should be sent; + // same time used between successful keepalive probes. + int32_t GetTCPKeepaliveLongLivedIdleTime() { + return mTCPKeepaliveLongLivedIdleTimeS; + } + + bool UseFastOpen() { + return mUseFastOpen && mFastOpenSupported && + (mFastOpenStallsCounter < mFastOpenStallsLimit) && + (mFastOpenConsecutiveFailureCounter < + mFastOpenConsecutiveFailureLimit); + } + // If one of tcp connections return PR_NOT_TCP_SOCKET_ERROR while trying + // fast open, it means that Fast Open is turned off so we will not try again + // until a restart. This is only on Linux. + void SetFastOpenNotSupported() { mFastOpenSupported = false; } + + void IncrementFastOpenConsecutiveFailureCounter(); + + void ResetFastOpenConsecutiveFailureCounter() { + mFastOpenConsecutiveFailureCounter = 0; + } + + void IncrementFastOpenStallsCounter(); + uint32_t CheckIfConnectionIsStalledOnlyIfIdleForThisAmountOfSeconds() { + return mFastOpenStallsIdleTime; + } + uint32_t FastOpenStallsTimeout() { return mFastOpenStallsTimeout; } + + // returns the HTTP framing check level preference, as controlled with + // network.http.enforce-framing.http1 and network.http.enforce-framing.soft + FrameCheckLevel GetEnforceH1Framing() { return mEnforceH1Framing; } + + nsHttpAuthCache* AuthCache(bool aPrivate) { + return aPrivate ? &mPrivateAuthCache : &mAuthCache; + } + nsHttpConnectionMgr* ConnMgr() { + MOZ_ASSERT_IF(nsIOService::UseSocketProcess(), XRE_IsSocketProcess()); + return mConnMgr->AsHttpConnectionMgr(); + } + + AltSvcCache* AltServiceCache() const { + MOZ_ASSERT(XRE_IsParentProcess()); + return mAltSvcCache.get(); + } + + void ClearHostMapping(nsHttpConnectionInfo* aConnInfo); + + // cache support + uint32_t GenerateUniqueID() { return ++mLastUniqueID; } + uint32_t SessionStartTime() { return mSessionStartTime; } + + // + // Connection management methods: + // + // - the handler only owns idle connections; it does not own active + // connections. + // + // - the handler keeps a count of active connections to enforce the + // steady-state max-connections pref. + // + + // Called to kick-off a new transaction, by default the transaction + // will be put on the pending transaction queue if it cannot be + // initiated at this time. Callable from any thread. + [[nodiscard]] nsresult InitiateTransaction(HttpTransactionShell* trans, + int32_t priority); + + // This function is also called to kick-off a new transaction. But the new + // transaction will take a sticky connection from |transWithStickyConn| + // and reuse it. + [[nodiscard]] nsresult InitiateTransactionWithStickyConn( + HttpTransactionShell* trans, int32_t priority, + HttpTransactionShell* transWithStickyConn); + + // Called to change the priority of an existing transaction that has + // already been initiated. + [[nodiscard]] nsresult RescheduleTransaction(HttpTransactionShell* trans, + int32_t priority); + + void UpdateClassOfServiceOnTransaction(HttpTransactionShell* trans, + uint32_t classOfService); + + // Called to cancel a transaction, which may or may not be assigned to + // a connection. Callable from any thread. + [[nodiscard]] nsresult CancelTransaction(HttpTransactionShell* trans, + nsresult reason); + + // Called when a connection is done processing a transaction. Callable + // from any thread. + [[nodiscard]] nsresult ReclaimConnection(HttpConnectionBase* conn) { + return mConnMgr->ReclaimConnection(conn); + } + + [[nodiscard]] nsresult ProcessPendingQ(nsHttpConnectionInfo* cinfo) { + return mConnMgr->ProcessPendingQ(cinfo); + } + + [[nodiscard]] nsresult ProcessPendingQ() { + return mConnMgr->ProcessPendingQ(); + } + + [[nodiscard]] nsresult GetSocketThreadTarget(nsIEventTarget** target) { + return mConnMgr->GetSocketThreadTarget(target); + } + + [[nodiscard]] nsresult SpeculativeConnect(nsHttpConnectionInfo* ci, + nsIInterfaceRequestor* callbacks, + uint32_t caps = 0, + bool aFetchHTTPSRR = false) { + TickleWifi(callbacks); + RefPtr clone = ci->Clone(); + return mConnMgr->SpeculativeConnect(clone, callbacks, caps, nullptr, + aFetchHTTPSRR); + } + + [[nodiscard]] nsresult SpeculativeConnect(nsHttpConnectionInfo* ci, + nsIInterfaceRequestor* callbacks, + uint32_t caps, + SpeculativeTransaction* aTrans) { + RefPtr clone = ci->Clone(); + return mConnMgr->SpeculativeConnect(clone, callbacks, caps, aTrans); + } + + // Alternate Services Maps are main thread only + void UpdateAltServiceMapping(AltSvcMapping* map, nsProxyInfo* proxyInfo, + nsIInterfaceRequestor* callbacks, uint32_t caps, + const OriginAttributes& originAttributes) { + mAltSvcCache->UpdateAltServiceMapping(map, proxyInfo, callbacks, caps, + originAttributes); + } + + void UpdateAltServiceMappingWithoutValidation( + AltSvcMapping* map, nsProxyInfo* proxyInfo, + nsIInterfaceRequestor* callbacks, uint32_t caps, + const OriginAttributes& originAttributes) { + mAltSvcCache->UpdateAltServiceMappingWithoutValidation( + map, proxyInfo, callbacks, caps, originAttributes); + } + + already_AddRefed GetAltServiceMapping( + const nsACString& scheme, const nsACString& host, int32_t port, bool pb, + bool isolated, const nsACString& topWindowOrigin, + const OriginAttributes& originAttributes, bool aHttp2Allowed, + bool aHttp3Allowed) { + return mAltSvcCache->GetAltServiceMapping(scheme, host, port, pb, isolated, + topWindowOrigin, originAttributes, + aHttp2Allowed, aHttp3Allowed); + } + + // + // The HTTP handler caches pointers to specific XPCOM services, and + // provides the following helper routines for accessing those services: + // + [[nodiscard]] nsresult GetStreamConverterService(nsIStreamConverterService**); + [[nodiscard]] nsresult GetIOService(nsIIOService** service); + nsICookieService* GetCookieService(); // not addrefed + nsISiteSecurityService* GetSSService(); + + // Called by the channel synchronously during asyncOpen + void OnFailedOpeningRequest(nsIHttpChannel* chan) { + NotifyObservers(chan, NS_HTTP_ON_FAILED_OPENING_REQUEST_TOPIC); + } + + // Called by the channel synchronously during asyncOpen + void OnOpeningRequest(nsIHttpChannel* chan) { + NotifyObservers(chan, NS_HTTP_ON_OPENING_REQUEST_TOPIC); + } + + void OnOpeningDocumentRequest(nsIIdentChannel* chan) { + NotifyObservers(chan, NS_DOCUMENT_ON_OPENING_REQUEST_TOPIC); + } + + // Called by the channel before writing a request + void OnModifyRequest(nsIHttpChannel* chan) { + NotifyObservers(chan, NS_HTTP_ON_MODIFY_REQUEST_TOPIC); + } + + void OnModifyDocumentRequest(nsIIdentChannel* chan) { + NotifyObservers(chan, NS_DOCUMENT_ON_MODIFY_REQUEST_TOPIC); + } + + // Called by the channel before writing a request + void OnStopRequest(nsIHttpChannel* chan) { + NotifyObservers(chan, NS_HTTP_ON_STOP_REQUEST_TOPIC); + } + + // Called by the channel before setting up the transaction + void OnBeforeConnect(nsIHttpChannel* chan) { + NotifyObservers(chan, NS_HTTP_ON_BEFORE_CONNECT_TOPIC); + } + + // Called by the channel once headers are available + void OnExamineResponse(nsIHttpChannel* chan) { + NotifyObservers(chan, NS_HTTP_ON_EXAMINE_RESPONSE_TOPIC); + } + + // Called by the channel once headers have been merged with cached headers + void OnExamineMergedResponse(nsIHttpChannel* chan) { + NotifyObservers(chan, NS_HTTP_ON_EXAMINE_MERGED_RESPONSE_TOPIC); + } + + // Called by the channel once it made background cache revalidation + void OnBackgroundRevalidation(nsIHttpChannel* chan) { + NotifyObservers(chan, NS_HTTP_ON_BACKGROUND_REVALIDATION); + } + + // Called by channels before a redirect happens. This notifies both the + // channel's and the global redirect observers. + [[nodiscard]] nsresult AsyncOnChannelRedirect( + nsIChannel* oldChan, nsIChannel* newChan, uint32_t flags, + nsIEventTarget* mainThreadEventTarget = nullptr); + + // Called by the channel when the response is read from the cache without + // communicating with the server. + void OnExamineCachedResponse(nsIHttpChannel* chan) { + NotifyObservers(chan, NS_HTTP_ON_EXAMINE_CACHED_RESPONSE_TOPIC); + } + + // Called by the channel when the transaction pump is suspended because of + // trying to get credentials asynchronously. + void OnTransactionSuspendedDueToAuthentication(nsIHttpChannel* chan) { + NotifyObservers(chan, "http-on-transaction-suspended-authentication"); + } + + // Generates the host:port string for use in the Host: header as well as the + // CONNECT line for proxies. This handles IPv6 literals correctly. + [[nodiscard]] static nsresult GenerateHostPort(const nsCString& host, + int32_t port, + nsACString& hostLine); + + SpdyInformation* SpdyInfo() { return &mSpdyInfo; } + bool IsH2MandatorySuiteEnabled() { return mH2MandatorySuiteEnabled; } + + // returns true in between Init and Shutdown states + bool Active() { return mHandlerActive; } + + nsIRequestContextService* GetRequestContextService() { + return mRequestContextService.get(); + } + + void ShutdownConnectionManager(); + + uint32_t DefaultHpackBuffer() const { return mDefaultHpackBuffer; } + + bool Bug1563538() const { return mBug1563538; } + + bool IsHttp3VersionSupported(const nsACString& version); + + bool IsHttp3Enabled() const { return mHttp3Enabled; } + uint32_t DefaultQpackTableSize() const { return mQpackTableSize; } + uint16_t DefaultHttp3MaxBlockedStreams() const { + return (uint16_t)mHttp3MaxBlockedStreams; + } + + uint32_t MaxHttpResponseHeaderSize() const { + return mMaxHttpResponseHeaderSize; + } + + const nsCString& Http3QlogDir(); + + float FocusedWindowTransactionRatio() const { + return mFocusedWindowTransactionRatio; + } + + bool ActiveTabPriority() const { return mActiveTabPriority; } + + // Called when an optimization feature affecting active vs background tab load + // took place. Called only on the parent process and only updates + // mLastActiveTabLoadOptimizationHit timestamp to now. + void NotifyActiveTabLoadOptimization(); + TimeStamp const GetLastActiveTabLoadOptimizationHit(); + void SetLastActiveTabLoadOptimizationHit(TimeStamp const& when); + bool IsBeforeLastActiveTabLoadOptimization(TimeStamp const& when); + + bool DumpHpackTables() { return mDumpHpackTables; } + + HttpTrafficAnalyzer* GetHttpTrafficAnalyzer(); + + bool GetThroughCaptivePortal() { return mThroughCaptivePortal; } + + nsresult CompleteUpgrade(HttpTransactionShell* aTrans, + nsIHttpUpgradeListener* aUpgradeListener); + + nsresult DoShiftReloadConnectionCleanupWithConnInfo( + nsHttpConnectionInfo* aCI); + + void MaybeAddAltSvcForTesting(nsIURI* aUri, const nsACString& aUsername, + const nsACString& aTopWindowOrigin, + bool aPrivateBrowsing, bool aIsolated, + nsIInterfaceRequestor* aCallbacks, + const OriginAttributes& aOriginAttributes); + + bool UseHTTPSRRAsAltSvcEnabled() const; + + bool EchConfigEnabled() const; + // When EchConfig is enabled and all records with echConfig are failed, this + // functon indicate whether we can fallback to the origin server. + // In the case an HTTPS RRSet contains some RRs with echConfig and some + // without, we always fallback to the origin one. + bool FallbackToOriginIfConfigsAreECHAndAllFailed() const; + + bool UseHTTPSRRForSpeculativeConnection() const; + + private: + nsHttpHandler(); + + virtual ~nsHttpHandler(); + + [[nodiscard]] nsresult Init(); + + // + // Useragent/prefs helper methods + // + void BuildUserAgent(); + void InitUserAgentComponents(); + static void PrefsChanged(const char* pref, void* self); + void PrefsChanged(const char* pref); + + [[nodiscard]] nsresult SetAcceptLanguages(); + [[nodiscard]] nsresult SetAcceptEncodings(const char*, bool mIsSecure); + + [[nodiscard]] nsresult InitConnectionMgr(); + + void NotifyObservers(nsIChannel* chan, const char* event); + + void SetFastOpenOSSupport(); + + friend class SocketProcessChild; + void SetHttpHandlerInitArgs(const HttpHandlerInitArgs& aArgs); + void SetDeviceModelId(const nsCString& aModelId); + + // Checks if there are any user certs or active smart cards on a different + // thread. Updates mSpeculativeConnectEnabled when done. + void MaybeEnableSpeculativeConnect(); + + // We only allow TRR and TRRServiceChannel itself to create TRRServiceChannel. + friend class TRRServiceChannel; + friend class TRR; + nsresult CreateTRRServiceChannel(nsIURI* uri, nsIProxyInfo* givenProxyInfo, + uint32_t proxyResolveFlags, nsIURI* proxyURI, + nsILoadInfo* aLoadInfo, nsIChannel** result); + nsresult SetupChannelInternal(HttpBaseChannel* aChannel, nsIURI* uri, + nsIProxyInfo* givenProxyInfo, + uint32_t proxyResolveFlags, nsIURI* proxyURI, + nsILoadInfo* aLoadInfo, nsIChannel** result); + + private: + // cached services + nsMainThreadPtrHandle mIOService; + nsMainThreadPtrHandle mStreamConvSvc; + nsMainThreadPtrHandle mCookieService; + nsMainThreadPtrHandle mSSService; + + // the authentication credentials cache + nsHttpAuthCache mAuthCache; + nsHttpAuthCache mPrivateAuthCache; + + // the connection manager + RefPtr mConnMgr; + + UniquePtr mAltSvcCache; + + // + // prefs + // + + enum HttpVersion mHttpVersion; + enum HttpVersion mProxyHttpVersion; + uint32_t mCapabilities; + + bool mFastFallbackToIPv4; + PRIntervalTime mIdleTimeout; + PRIntervalTime mSpdyTimeout; + PRIntervalTime mResponseTimeout; + Atomic mResponseTimeoutEnabled; + uint32_t mNetworkChangedTimeout; // milliseconds + uint16_t mMaxRequestAttempts; + uint16_t mMaxRequestDelay; + uint16_t mIdleSynTimeout; + uint16_t mFallbackSynTimeout; // seconds + + bool mH2MandatorySuiteEnabled; + uint16_t mMaxUrgentExcessiveConns; + uint16_t mMaxConnections; + uint8_t mMaxPersistentConnectionsPerServer; + uint8_t mMaxPersistentConnectionsPerProxy; + + bool mThrottleEnabled; + uint32_t mThrottleVersion; + uint32_t mThrottleSuspendFor; + uint32_t mThrottleResumeFor; + uint32_t mThrottleReadLimit; + uint32_t mThrottleReadInterval; + uint32_t mThrottleHoldTime; + uint32_t mThrottleMaxTime; + + int32_t mSendWindowSize; + + bool mUrgentStartEnabled; + bool mTailBlockingEnabled; + uint32_t mTailDelayQuantum; + uint32_t mTailDelayQuantumAfterDCL; + uint32_t mTailDelayMax; + uint32_t mTailTotalMax; + + uint8_t mRedirectionLimit; + + bool mBeConservativeForProxy; + + // we'll warn the user if we load an URL containing a userpass field + // unless its length is less than this threshold. this warning is + // intended to protect the user against spoofing attempts that use + // the userpass field of the URL to obscure the actual origin server. + uint8_t mPhishyUserPassLength; + + uint8_t mQoSBits; + + bool mEnforceAssocReq; + + nsCString mImageAcceptHeader; + nsCString mDocumentAcceptHeader; + + nsCString mAcceptLanguages; + nsCString mHttpAcceptEncodings; + nsCString mHttpsAcceptEncodings; + + nsCString mDefaultSocketType; + + // cache support + uint32_t mLastUniqueID; + uint32_t mSessionStartTime; + + // useragent components + nsCString mLegacyAppName; + nsCString mLegacyAppVersion; + nsCString mPlatform; + nsCString mOscpu; + nsCString mMisc; + nsCString mProduct; + nsCString mProductSub; + nsCString mAppName; + nsCString mAppVersion; + nsCString mCompatFirefox; + bool mCompatFirefoxEnabled; + nsCString mCompatDevice; + nsCString mDeviceModelId; + + nsCString mUserAgent; + nsCString mSpoofedUserAgent; + nsCString mUserAgentOverride; + bool mUserAgentIsDirty; // true if mUserAgent should be rebuilt + bool mAcceptLanguagesIsDirty; + + bool mPromptTempRedirect; + + // Persistent HTTPS caching flag + bool mEnablePersistentHttpsCaching; + + // for broadcasting safe hint; + bool mSafeHintEnabled; + bool mParentalControlEnabled; + + // true in between init and shutdown states + Atomic mHandlerActive; + + // The value of 'hidden' network.http.debug-observations : 1; + uint32_t mDebugObservations : 1; + + uint32_t mEnableSpdy : 1; + uint32_t mHttp2Enabled : 1; + uint32_t mUseH2Deps : 1; + uint32_t mEnforceHttp2TlsProfile : 1; + uint32_t mCoalesceSpdy : 1; + uint32_t mSpdyPersistentSettings : 1; + uint32_t mAllowPush : 1; + uint32_t mEnableAltSvc : 1; + uint32_t mEnableAltSvcOE : 1; + uint32_t mEnableOriginExtension : 1; + uint32_t mEnableH2Websockets : 1; + uint32_t mDumpHpackTables : 1; + + // Try to use SPDY features instead of HTTP/1.1 over SSL + SpdyInformation mSpdyInfo; + + uint32_t mSpdySendingChunkSize; + uint32_t mSpdySendBufferSize; + uint32_t mSpdyPushAllowance; + uint32_t mSpdyPullAllowance; + uint32_t mDefaultSpdyConcurrent; + PRIntervalTime mSpdyPingThreshold; + PRIntervalTime mSpdyPingTimeout; + + // The maximum amount of time to wait for socket transport to be + // established. In milliseconds. + uint32_t mConnectTimeout; + + // The maximum amount of time to wait for a tls handshake to be + // established. In milliseconds. + uint32_t mTLSHandshakeTimeout; + + // The maximum number of current global half open sockets allowable + // when starting a new speculative connection. + uint32_t mParallelSpeculativeConnectLimit; + + // For Rate Pacing of HTTP/1 requests through a netwerk/base/EventTokenBucket + // Active requests <= *MinParallelism are not subject to the rate pacing + bool mRequestTokenBucketEnabled; + uint16_t mRequestTokenBucketMinParallelism; + uint32_t mRequestTokenBucketHz; // EventTokenBucket HZ + uint32_t mRequestTokenBucketBurst; // EventTokenBucket Burst + + // Whether or not to block requests for non head js/css items (e.g. media) + // while those elements load. + bool mCriticalRequestPrioritization; + + // TCP Keepalive configuration values. + + // True if TCP keepalive is enabled for short-lived conns. + bool mTCPKeepaliveShortLivedEnabled; + // Time (secs) indicating how long a conn is considered short-lived. + int32_t mTCPKeepaliveShortLivedTimeS; + // Time (secs) before first keepalive probe; between successful probes. + int32_t mTCPKeepaliveShortLivedIdleTimeS; + + // True if TCP keepalive is enabled for long-lived conns. + bool mTCPKeepaliveLongLivedEnabled; + // Time (secs) before first keepalive probe; between successful probes. + int32_t mTCPKeepaliveLongLivedIdleTimeS; + + // if true, generate NS_ERROR_PARTIAL_TRANSFER for h1 responses with + // incorrect content lengths or malformed chunked encodings + FrameCheckLevel mEnforceH1Framing; + + nsCOMPtr mRequestContextService; + + // The default size (in bytes) of the HPACK decompressor table. + uint32_t mDefaultHpackBuffer; + + // Pref for the whole fix that bug provides + Atomic mBug1563538; + + Atomic mHttp3Enabled; + // Http3 parameters + Atomic mQpackTableSize; + Atomic + mHttp3MaxBlockedStreams; // uint16_t is enough here, but Atomic only + // supports uint32_t or uint64_t. + nsCString mHttp3QlogDir; + + // The max size (in bytes) for received Http response header. + uint32_t mMaxHttpResponseHeaderSize; + + // The ratio for dispatching transactions from the focused window. + float mFocusedWindowTransactionRatio; + + // We may disable speculative connect if the browser has user certificates + // installed as that might randomly popup the certificate choosing window. + Atomic mSpeculativeConnectEnabled; + + Atomic mUseFastOpen; + Atomic mFastOpenSupported; + uint32_t mFastOpenConsecutiveFailureLimit; + uint32_t mFastOpenConsecutiveFailureCounter; + uint32_t mFastOpenStallsLimit; + uint32_t mFastOpenStallsCounter; + Atomic mFastOpenStallsIdleTime; + uint32_t mFastOpenStallsTimeout; + + // If true, the transactions from active tab will be dispatched first. + bool mActiveTabPriority; + + HttpTrafficAnalyzer mHttpTrafficAnalyzer; + + private: + // For Rate Pacing Certain Network Events. Only assign this pointer on + // socket thread. + void MakeNewRequestTokenBucket(); + RefPtr mRequestTokenBucket; + + public: + // Socket thread only + [[nodiscard]] nsresult SubmitPacedRequest(ATokenBucketEvent* event, + nsICancelable** cancel) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + if (!mRequestTokenBucket) { + return NS_ERROR_NOT_AVAILABLE; + } + return mRequestTokenBucket->SubmitEvent(event, cancel); + } + + // Socket thread only + void SetRequestTokenBucket(EventTokenBucket* aTokenBucket) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + mRequestTokenBucket = aTokenBucket; + } + + void StopRequestTokenBucket() { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + if (mRequestTokenBucket) { + mRequestTokenBucket->Stop(); + mRequestTokenBucket = nullptr; + } + } + + private: + RefPtr mWifiTickler; + void TickleWifi(nsIInterfaceRequestor* cb); + + private: + [[nodiscard]] nsresult SpeculativeConnectInternal( + nsIURI* aURI, nsIPrincipal* aPrincipal, nsIInterfaceRequestor* aCallbacks, + bool anonymous); + + // State for generating channelIds + uint32_t mProcessId; + Atomic mNextChannelId; + + // The last time any of the active tab page load optimization took place. + // This is accessed on multiple threads, hence a lock is needed. + // On the parent process this is updated to now every time a scheduling + // or rate optimization related to the active/background tab is hit. + // We carry this value through each http channel's onstoprequest notification + // to the parent process. On the content process then we just update this + // value from ipc onstoprequest arguments. This is a sufficent way of passing + // it down to the content process, since the value will be used only after + // onstoprequest notification coming from an http channel. + Mutex mLastActiveTabLoadOptimizationLock; + TimeStamp mLastActiveTabLoadOptimizationHit; + + Mutex mHttpExclusionLock; + + public: + [[nodiscard]] nsresult NewChannelId(uint64_t& channelId); + void AddHttpChannel(uint64_t aId, nsISupports* aChannel); + void RemoveHttpChannel(uint64_t aId); + nsWeakPtr GetWeakHttpChannel(uint64_t aId); + + void ExcludeHttp2(const nsHttpConnectionInfo* ci); + [[nodiscard]] bool IsHttp2Excluded(const nsHttpConnectionInfo* ci); + void ExcludeHttp3(const nsHttpConnectionInfo* ci); + [[nodiscard]] bool IsHttp3Excluded(const nsACString& aRoutedHost); + + private: + nsTHashtable mExcludedHttp2Origins; + nsTHashtable mExcludedHttp3Origins; + + bool mThroughCaptivePortal; + + // The mapping of channel id and the weak pointer of nsHttpChannel. + nsDataHashtable mIDToHttpChannelMap; + + // This is parsed pref network.http.http3.alt-svc-mapping-for-testing. + // The pref set artificial altSvc-s for origin for testing. + // This maps an origin to an altSvc. + nsClassHashtable mAltSvcMappingTemptativeMap; +}; + +extern StaticRefPtr gHttpHandler; + +//----------------------------------------------------------------------------- +// nsHttpsHandler - thin wrapper to distinguish the HTTP handler from the +// HTTPS handler (even though they share the same impl). +//----------------------------------------------------------------------------- + +class nsHttpsHandler : public nsIHttpProtocolHandler, + public nsSupportsWeakReference, + public nsISpeculativeConnect { + virtual ~nsHttpsHandler() = default; + + public: + // we basically just want to override GetScheme and GetDefaultPort... + // all other methods should be forwarded to the nsHttpHandler instance. + + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIPROTOCOLHANDLER + NS_FORWARD_NSIPROXIEDPROTOCOLHANDLER(gHttpHandler->) + NS_FORWARD_NSIHTTPPROTOCOLHANDLER(gHttpHandler->) + NS_FORWARD_NSISPECULATIVECONNECT(gHttpHandler->) + + nsHttpsHandler() = default; + + [[nodiscard]] nsresult Init(); +}; + +//----------------------------------------------------------------------------- +// HSTSDataCallbackWrapper - A threadsafe helper class to wrap the callback. +// +// We need this because dom::promise and EnsureHSTSDataResolver are not +// threadsafe. +//----------------------------------------------------------------------------- +class HSTSDataCallbackWrapper final { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(HSTSDataCallbackWrapper) + + explicit HSTSDataCallbackWrapper(std::function&& aCallback) + : mCallback(std::move(aCallback)) { + MOZ_ASSERT(NS_IsMainThread()); + } + + void DoCallback(bool aResult) { + MOZ_ASSERT(NS_IsMainThread()); + mCallback(aResult); + } + + private: + ~HSTSDataCallbackWrapper() = default; + + std::function mCallback; +}; + +} // namespace net +} // namespace mozilla + +#endif // nsHttpHandler_h__ diff --git a/netwerk/protocol/http/nsHttpHeaderArray.cpp b/netwerk/protocol/http/nsHttpHeaderArray.cpp new file mode 100644 index 0000000000..18ae69e99e --- /dev/null +++ b/netwerk/protocol/http/nsHttpHeaderArray.cpp @@ -0,0 +1,452 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=4 sw=2 sts=2 ci et: */ +/* 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/. */ + +// HttpLog.h should generally be included first +#include "HttpLog.h" + +#include "nsHttpHeaderArray.h" +#include "nsURLHelper.h" +#include "nsIHttpHeaderVisitor.h" +#include "nsHttpHandler.h" + +namespace mozilla { +namespace net { + +//----------------------------------------------------------------------------- +// nsHttpHeaderArray +//----------------------------------------------------------------------------- + +nsresult nsHttpHeaderArray::SetHeader( + const nsACString& headerName, const nsACString& value, bool merge, + nsHttpHeaderArray::HeaderVariety variety) { + nsHttpAtom header = nsHttp::ResolveAtom(PromiseFlatCString(headerName).get()); + if (!header) { + NS_WARNING("failed to resolve atom"); + return NS_ERROR_NOT_AVAILABLE; + } + return SetHeader(header, headerName, value, merge, variety); +} + +nsresult nsHttpHeaderArray::SetHeader( + nsHttpAtom header, const nsACString& value, bool merge, + nsHttpHeaderArray::HeaderVariety variety) { + return SetHeader(header, ""_ns, value, merge, variety); +} + +nsresult nsHttpHeaderArray::SetHeader( + nsHttpAtom header, const nsACString& headerName, const nsACString& value, + bool merge, nsHttpHeaderArray::HeaderVariety variety) { + MOZ_ASSERT( + (variety == eVarietyResponse) || (variety == eVarietyRequestDefault) || + (variety == eVarietyRequestOverride), + "Net original headers can only be set using SetHeader_internal()."); + + nsEntry* entry = nullptr; + int32_t index; + + index = LookupEntry(header, &entry); + + // If an empty value is passed in, then delete the header entry... + // unless we are merging, in which case this function becomes a NOP. + if (value.IsEmpty()) { + if (!merge && entry) { + if (entry->variety == eVarietyResponseNetOriginalAndResponse) { + MOZ_ASSERT(variety == eVarietyResponse); + entry->variety = eVarietyResponseNetOriginal; + } else { + mHeaders.RemoveElementAt(index); + } + } + return NS_OK; + } + + MOZ_ASSERT(!entry || variety != eVarietyRequestDefault, + "Cannot set default entry which overrides existing entry!"); + if (!entry) { + return SetHeader_internal(header, headerName, value, variety); + } else if (merge && !IsSingletonHeader(header)) { + return MergeHeader(header, entry, value, variety); + } else if (!IsIgnoreMultipleHeader(header)) { + // Replace the existing string with the new value + if (entry->variety == eVarietyResponseNetOriginalAndResponse) { + MOZ_ASSERT(variety == eVarietyResponse); + entry->variety = eVarietyResponseNetOriginal; + return SetHeader_internal(header, headerName, value, variety); + } + entry->value = value; + entry->variety = variety; + } + + return NS_OK; +} + +nsresult nsHttpHeaderArray::SetHeader_internal( + nsHttpAtom header, const nsACString& headerName, const nsACString& value, + nsHttpHeaderArray::HeaderVariety variety) { + nsEntry* entry = mHeaders.AppendElement(); + if (!entry) { + return NS_ERROR_OUT_OF_MEMORY; + } + entry->header = header; + // Only save original form of a header if it is different than the header + // atom string. + if (!headerName.Equals(header.get())) { + entry->headerNameOriginal = headerName; + } + entry->value = value; + entry->variety = variety; + return NS_OK; +} + +nsresult nsHttpHeaderArray::SetEmptyHeader(const nsACString& headerName, + HeaderVariety variety) { + nsHttpAtom header = nsHttp::ResolveAtom(PromiseFlatCString(headerName).get()); + if (!header) { + NS_WARNING("failed to resolve atom"); + return NS_ERROR_NOT_AVAILABLE; + } + + MOZ_ASSERT((variety == eVarietyResponse) || + (variety == eVarietyRequestDefault) || + (variety == eVarietyRequestOverride), + "Original headers can only be set using SetHeader_internal()."); + nsEntry* entry = nullptr; + + LookupEntry(header, &entry); + + if (entry && entry->variety != eVarietyResponseNetOriginalAndResponse) { + entry->value.Truncate(); + return NS_OK; + } else if (entry) { + MOZ_ASSERT(variety == eVarietyResponse); + entry->variety = eVarietyResponseNetOriginal; + } + + return SetHeader_internal(header, headerName, ""_ns, variety); +} + +nsresult nsHttpHeaderArray::SetHeaderFromNet( + nsHttpAtom header, const nsACString& headerNameOriginal, + const nsACString& value, bool response) { + // mHeader holds the consolidated (merged or updated) headers. + // mHeader for response header will keep the original heades as well. + nsEntry* entry = nullptr; + + LookupEntry(header, &entry); + + if (!entry) { + HeaderVariety variety = eVarietyRequestOverride; + if (response) { + variety = eVarietyResponseNetOriginalAndResponse; + } + return SetHeader_internal(header, headerNameOriginal, value, variety); + + } else if (!IsSingletonHeader(header)) { + HeaderVariety variety = eVarietyRequestOverride; + if (response) { + variety = eVarietyResponse; + } + nsresult rv = MergeHeader(header, entry, value, variety); + if (NS_FAILED(rv)) { + return rv; + } + if (response) { + rv = SetHeader_internal(header, headerNameOriginal, value, + eVarietyResponseNetOriginal); + } + return rv; + } else if (!IsIgnoreMultipleHeader(header)) { + // Multiple instances of non-mergeable header received from network + // - ignore if same value + if (!entry->value.Equals(value)) { + if (IsSuspectDuplicateHeader(header)) { + // reply may be corrupt/hacked (ex: CLRF injection attacks) + return NS_ERROR_CORRUPTED_CONTENT; + } // else silently drop value: keep value from 1st header seen + LOG(("Header %s silently dropped as non mergeable header\n", + header.get())); + } + if (response) { + return SetHeader_internal(header, headerNameOriginal, value, + eVarietyResponseNetOriginal); + } + } + + return NS_OK; +} + +nsresult nsHttpHeaderArray::SetResponseHeaderFromCache( + nsHttpAtom header, const nsACString& headerNameOriginal, + const nsACString& value, nsHttpHeaderArray::HeaderVariety variety) { + MOZ_ASSERT( + (variety == eVarietyResponse) || (variety == eVarietyResponseNetOriginal), + "Headers from cache can only be eVarietyResponse and " + "eVarietyResponseNetOriginal"); + + if (variety == eVarietyResponseNetOriginal) { + return SetHeader_internal(header, headerNameOriginal, value, + eVarietyResponseNetOriginal); + } + nsTArray::index_type index = 0; + do { + index = mHeaders.IndexOf(header, index, nsEntry::MatchHeader()); + if (index != mHeaders.NoIndex) { + nsEntry& entry = mHeaders[index]; + if (value.Equals(entry.value)) { + MOZ_ASSERT( + (entry.variety == eVarietyResponseNetOriginal) || + (entry.variety == eVarietyResponseNetOriginalAndResponse), + "This array must contain only eVarietyResponseNetOriginal" + " and eVarietyResponseNetOriginalAndRespons headers!"); + entry.variety = eVarietyResponseNetOriginalAndResponse; + return NS_OK; + } + index++; + } + } while (index != mHeaders.NoIndex); + // If we are here, we have not found an entry so add a new one. + return SetHeader_internal(header, headerNameOriginal, value, + eVarietyResponse); +} + +void nsHttpHeaderArray::ClearHeader(nsHttpAtom header) { + nsEntry* entry = nullptr; + int32_t index = LookupEntry(header, &entry); + if (entry) { + if (entry->variety == eVarietyResponseNetOriginalAndResponse) { + entry->variety = eVarietyResponseNetOriginal; + } else { + mHeaders.RemoveElementAt(index); + } + } +} + +const char* nsHttpHeaderArray::PeekHeader(nsHttpAtom header) const { + const nsEntry* entry = nullptr; + LookupEntry(header, &entry); + return entry ? entry->value.get() : nullptr; +} + +nsresult nsHttpHeaderArray::GetHeader(nsHttpAtom header, + nsACString& result) const { + const nsEntry* entry = nullptr; + LookupEntry(header, &entry); + if (!entry) return NS_ERROR_NOT_AVAILABLE; + result = entry->value; + return NS_OK; +} + +nsresult nsHttpHeaderArray::GetOriginalHeader(nsHttpAtom aHeader, + nsIHttpHeaderVisitor* aVisitor) { + NS_ENSURE_ARG_POINTER(aVisitor); + uint32_t index = 0; + nsresult rv = NS_ERROR_NOT_AVAILABLE; + while (true) { + index = mHeaders.IndexOf(aHeader, index, nsEntry::MatchHeader()); + if (index != UINT32_MAX) { + const nsEntry& entry = mHeaders[index]; + + MOZ_ASSERT((entry.variety == eVarietyResponseNetOriginalAndResponse) || + (entry.variety == eVarietyResponseNetOriginal) || + (entry.variety == eVarietyResponse), + "This must be a response header."); + index++; + if (entry.variety == eVarietyResponse) { + continue; + } + + nsAutoCString hdr; + if (entry.headerNameOriginal.IsEmpty()) { + hdr = nsDependentCString(entry.header); + } else { + hdr = entry.headerNameOriginal; + } + + rv = NS_OK; + if (NS_FAILED(aVisitor->VisitHeader(hdr, entry.value))) { + break; + } + } else { + // if there is no such a header, it will return + // NS_ERROR_NOT_AVAILABLE or NS_OK otherwise. + return rv; + } + } + return NS_OK; +} + +bool nsHttpHeaderArray::HasHeader(nsHttpAtom header) const { + const nsEntry* entry = nullptr; + LookupEntry(header, &entry); + return entry; +} + +nsresult nsHttpHeaderArray::VisitHeaders( + nsIHttpHeaderVisitor* visitor, nsHttpHeaderArray::VisitorFilter filter) { + NS_ENSURE_ARG_POINTER(visitor); + nsresult rv; + + uint32_t i, count = mHeaders.Length(); + for (i = 0; i < count; ++i) { + const nsEntry& entry = mHeaders[i]; + if (filter == eFilterSkipDefault && + entry.variety == eVarietyRequestDefault) { + continue; + } + if (filter == eFilterResponse && + entry.variety == eVarietyResponseNetOriginal) { + continue; + } + if (filter == eFilterResponseOriginal && + entry.variety == eVarietyResponse) { + continue; + } + + nsAutoCString hdr; + if (entry.headerNameOriginal.IsEmpty()) { + hdr = nsDependentCString(entry.header); + } else { + hdr = entry.headerNameOriginal; + } + rv = visitor->VisitHeader(hdr, entry.value); + if (NS_FAILED(rv)) { + return rv; + } + } + return NS_OK; +} + +/*static*/ +nsresult nsHttpHeaderArray::ParseHeaderLine(const nsACString& line, + nsHttpAtom* hdr, + nsACString* headerName, + nsACString* val) { + // + // BNF from section 4.2 of RFC 2616: + // + // message-header = field-name ":" [ field-value ] + // field-name = token + // field-value = *( field-content | LWS ) + // field-content = + // + + // We skip over mal-formed headers in the hope that we'll still be able to + // do something useful with the response. + int32_t split = line.FindChar(':'); + + if (split == kNotFound) { + LOG(("malformed header [%s]: no colon\n", PromiseFlatCString(line).get())); + return NS_ERROR_FAILURE; + } + + const nsACString& sub = Substring(line, 0, split); + const nsACString& sub2 = + Substring(line, split + 1, line.Length() - split - 1); + + // make sure we have a valid token for the field-name + if (!nsHttp::IsValidToken(sub)) { + LOG(("malformed header [%s]: field-name not a token\n", + PromiseFlatCString(line).get())); + return NS_ERROR_FAILURE; + } + + nsHttpAtom atom = nsHttp::ResolveAtom(sub); + if (!atom) { + LOG(("failed to resolve atom [%s]\n", PromiseFlatCString(line).get())); + return NS_ERROR_FAILURE; + } + + // skip over whitespace + char* p = + net_FindCharNotInSet(sub2.BeginReading(), sub2.EndReading(), HTTP_LWS); + + // trim trailing whitespace - bug 86608 + char* p2 = net_RFindCharNotInSet(p, sub2.EndReading(), HTTP_LWS); + + // assign return values + if (hdr) *hdr = atom; + if (val) val->Assign(p, p2 - p + 1); + if (headerName) headerName->Assign(sub); + + return NS_OK; +} + +void nsHttpHeaderArray::Flatten(nsACString& buf, bool pruneProxyHeaders, + bool pruneTransients) { + uint32_t i, count = mHeaders.Length(); + for (i = 0; i < count; ++i) { + const nsEntry& entry = mHeaders[i]; + // Skip original header. + if (entry.variety == eVarietyResponseNetOriginal) { + continue; + } + // prune proxy headers if requested + if (pruneProxyHeaders && ((entry.header == nsHttp::Proxy_Authorization) || + (entry.header == nsHttp::Proxy_Connection))) { + continue; + } + if (pruneTransients && + (entry.value.IsEmpty() || entry.header == nsHttp::Connection || + entry.header == nsHttp::Proxy_Connection || + entry.header == nsHttp::Keep_Alive || + entry.header == nsHttp::WWW_Authenticate || + entry.header == nsHttp::Proxy_Authenticate || + entry.header == nsHttp::Trailer || + entry.header == nsHttp::Transfer_Encoding || + entry.header == nsHttp::Upgrade || + // XXX this will cause problems when we start honoring + // Cache-Control: no-cache="set-cookie", what to do? + entry.header == nsHttp::Set_Cookie)) { + continue; + } + + if (entry.headerNameOriginal.IsEmpty()) { + buf.Append(entry.header); + } else { + buf.Append(entry.headerNameOriginal); + } + buf.AppendLiteral(": "); + buf.Append(entry.value); + buf.AppendLiteral("\r\n"); + } +} + +void nsHttpHeaderArray::FlattenOriginalHeader(nsACString& buf) { + uint32_t i, count = mHeaders.Length(); + for (i = 0; i < count; ++i) { + const nsEntry& entry = mHeaders[i]; + // Skip changed header. + if (entry.variety == eVarietyResponse) { + continue; + } + + if (entry.headerNameOriginal.IsEmpty()) { + buf.Append(entry.header); + } else { + buf.Append(entry.headerNameOriginal); + } + + buf.AppendLiteral(": "); + buf.Append(entry.value); + buf.AppendLiteral("\r\n"); + } +} + +const char* nsHttpHeaderArray::PeekHeaderAt( + uint32_t index, nsHttpAtom& header, nsACString& headerNameOriginal) const { + const nsEntry& entry = mHeaders[index]; + + header = entry.header; + headerNameOriginal = entry.headerNameOriginal; + return entry.value.get(); +} + +void nsHttpHeaderArray::Clear() { mHeaders.Clear(); } + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/nsHttpHeaderArray.h b/netwerk/protocol/http/nsHttpHeaderArray.h new file mode 100644 index 0000000000..342a02cf80 --- /dev/null +++ b/netwerk/protocol/http/nsHttpHeaderArray.h @@ -0,0 +1,289 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et 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/. */ + +#ifndef nsHttpHeaderArray_h__ +#define nsHttpHeaderArray_h__ + +#include "nsHttp.h" +#include "nsTArray.h" +#include "nsString.h" + +class nsIHttpHeaderVisitor; + +// This needs to be forward declared here so we can include only this header +// without also including PHttpChannelParams.h +namespace IPC { +template +struct ParamTraits; +} // namespace IPC + +namespace mozilla { +namespace net { + +class nsHttpHeaderArray { + public: + const char* PeekHeader(nsHttpAtom header) const; + + // For nsHttpResponseHead nsHttpHeaderArray will keep track of the original + // headers as they come from the network and the parse headers used in + // firefox. + // If the original and the firefox header are the same, we will keep just + // one copy and marked it as eVarietyResponseNetOriginalAndResponse. + // If firefox header representation changes a header coming from the + // network (e.g. merged it) or a eVarietyResponseNetOriginalAndResponse + // header has been changed by SetHeader method, we will keep the original + // header as eVarietyResponseNetOriginal and make a copy for the new header + // and mark it as eVarietyResponse. + enum HeaderVariety { + eVarietyUnknown, + // Used only for request header. + eVarietyRequestOverride, + eVarietyRequestDefault, + // Used only for response header. + eVarietyResponseNetOriginalAndResponse, + eVarietyResponseNetOriginal, + eVarietyResponse + }; + + // Used by internal setters: to set header from network use SetHeaderFromNet + [[nodiscard]] nsresult SetHeader(const nsACString& headerName, + const nsACString& value, bool merge, + HeaderVariety variety); + [[nodiscard]] nsresult SetHeader(nsHttpAtom header, const nsACString& value, + bool merge, HeaderVariety variety); + [[nodiscard]] nsresult SetHeader(nsHttpAtom header, + const nsACString& headerName, + const nsACString& value, bool merge, + HeaderVariety variety); + + // Used by internal setters to set an empty header + [[nodiscard]] nsresult SetEmptyHeader(const nsACString& headerName, + HeaderVariety variety); + + // Merges supported headers. For other duplicate values, determines if error + // needs to be thrown or 1st value kept. + // For the response header we keep the original headers as well. + [[nodiscard]] nsresult SetHeaderFromNet(nsHttpAtom header, + const nsACString& headerNameOriginal, + const nsACString& value, + bool response); + + [[nodiscard]] nsresult SetResponseHeaderFromCache( + nsHttpAtom header, const nsACString& headerNameOriginal, + const nsACString& value, HeaderVariety variety); + + [[nodiscard]] nsresult GetHeader(nsHttpAtom header, nsACString& value) const; + [[nodiscard]] nsresult GetOriginalHeader(nsHttpAtom aHeader, + nsIHttpHeaderVisitor* aVisitor); + void ClearHeader(nsHttpAtom h); + + // Find the location of the given header value, or null if none exists. + const char* FindHeaderValue(nsHttpAtom header, const char* value) const { + return nsHttp::FindToken(PeekHeader(header), value, HTTP_HEADER_VALUE_SEPS); + } + + // Determine if the given header value exists. + bool HasHeaderValue(nsHttpAtom header, const char* value) const { + return FindHeaderValue(header, value) != nullptr; + } + + bool HasHeader(nsHttpAtom header) const; + + enum VisitorFilter { + eFilterAll, + eFilterSkipDefault, + eFilterResponse, + eFilterResponseOriginal + }; + + [[nodiscard]] nsresult VisitHeaders(nsIHttpHeaderVisitor* visitor, + VisitorFilter filter = eFilterAll); + + // parse a header line, return the header atom and a pointer to the + // header value (the substring of the header line -- do not free). + [[nodiscard]] static nsresult ParseHeaderLine( + const nsACString& line, nsHttpAtom* header = nullptr, + nsACString* headerNameOriginal = nullptr, nsACString* value = nullptr); + + void Flatten(nsACString&, bool pruneProxyHeaders, bool pruneTransients); + void FlattenOriginalHeader(nsACString&); + + uint32_t Count() const { return mHeaders.Length(); } + + const char* PeekHeaderAt(uint32_t i, nsHttpAtom& header, + nsACString& headerNameOriginal) const; + + void Clear(); + + // Must be copy-constructable and assignable + struct nsEntry { + nsHttpAtom header; + nsCString headerNameOriginal; + nsCString value; + HeaderVariety variety = eVarietyUnknown; + + struct MatchHeader { + bool Equals(const nsEntry& aEntry, const nsHttpAtom& aHeader) const { + return aEntry.header == aHeader; + } + }; + + bool operator==(const nsEntry& aOther) const { + return header == aOther.header && value == aOther.value; + } + }; + + bool operator==(const nsHttpHeaderArray& aOther) const { + return mHeaders == aOther.mHeaders; + } + + private: + // LookupEntry function will never return eVarietyResponseNetOriginal. + // It will ignore original headers from the network. + int32_t LookupEntry(nsHttpAtom header, const nsEntry**) const; + int32_t LookupEntry(nsHttpAtom header, nsEntry**); + [[nodiscard]] nsresult MergeHeader(nsHttpAtom header, nsEntry* entry, + const nsACString& value, + HeaderVariety variety); + [[nodiscard]] nsresult SetHeader_internal(nsHttpAtom header, + const nsACString& headeName, + const nsACString& value, + HeaderVariety variety); + + // Header cannot be merged: only one value possible + bool IsSingletonHeader(nsHttpAtom header); + // Header cannot be merged, and subsequent values should be ignored + bool IsIgnoreMultipleHeader(nsHttpAtom header); + + // Subset of singleton headers: should never see multiple, different + // instances of these, else something fishy may be going on (like CLRF + // injection) + bool IsSuspectDuplicateHeader(nsHttpAtom header); + + // All members must be copy-constructable and assignable + CopyableTArray mHeaders; + + friend struct IPC::ParamTraits; + friend class nsHttpRequestHead; +}; + +//----------------------------------------------------------------------------- +// nsHttpHeaderArray : inline functions +//----------------------------------------------------------------------------- + +inline int32_t nsHttpHeaderArray::LookupEntry(nsHttpAtom header, + const nsEntry** entry) const { + uint32_t index = 0; + while (index != UINT32_MAX) { + index = mHeaders.IndexOf(header, index, nsEntry::MatchHeader()); + if (index != UINT32_MAX) { + if ((&mHeaders[index])->variety != eVarietyResponseNetOriginal) { + *entry = &mHeaders[index]; + return index; + } + index++; + } + } + + return index; +} + +inline int32_t nsHttpHeaderArray::LookupEntry(nsHttpAtom header, + nsEntry** entry) { + uint32_t index = 0; + while (index != UINT32_MAX) { + index = mHeaders.IndexOf(header, index, nsEntry::MatchHeader()); + if (index != UINT32_MAX) { + if ((&mHeaders[index])->variety != eVarietyResponseNetOriginal) { + *entry = &mHeaders[index]; + return index; + } + index++; + } + } + return index; +} + +inline bool nsHttpHeaderArray::IsSingletonHeader(nsHttpAtom header) { + return header == nsHttp::Content_Type || + header == nsHttp::Content_Disposition || + header == nsHttp::Content_Length || header == nsHttp::User_Agent || + header == nsHttp::Referer || header == nsHttp::Host || + header == nsHttp::Authorization || + header == nsHttp::Proxy_Authorization || + header == nsHttp::If_Modified_Since || + header == nsHttp::If_Unmodified_Since || header == nsHttp::From || + header == nsHttp::Location || header == nsHttp::Max_Forwards || + // Ignore-multiple-headers are singletons in the sense that they + // shouldn't be merged. + IsIgnoreMultipleHeader(header); +} + +// These are headers for which, in the presence of multiple values, we only +// consider the first. +inline bool nsHttpHeaderArray::IsIgnoreMultipleHeader(nsHttpAtom header) { + // https://tools.ietf.org/html/rfc6797#section-8: + // + // If a UA receives more than one STS header field in an HTTP + // response message over secure transport, then the UA MUST process + // only the first such header field. + return header == nsHttp::Strict_Transport_Security; +} + +[[nodiscard]] inline nsresult nsHttpHeaderArray::MergeHeader( + nsHttpAtom header, nsEntry* entry, const nsACString& value, + nsHttpHeaderArray::HeaderVariety variety) { + if (value.IsEmpty()) return NS_OK; // merge of empty header = no-op + + nsCString newValue = entry->value; + if (!newValue.IsEmpty()) { + // Append the new value to the existing value + if (header == nsHttp::Set_Cookie || header == nsHttp::WWW_Authenticate || + header == nsHttp::Proxy_Authenticate) { + // Special case these headers and use a newline delimiter to + // delimit the values from one another as commas may appear + // in the values of these headers contrary to what the spec says. + newValue.Append('\n'); + } else { + // Delimit each value from the others using a comma (per HTTP spec) + newValue.AppendLiteral(", "); + } + } + + newValue.Append(value); + if (entry->variety == eVarietyResponseNetOriginalAndResponse) { + MOZ_ASSERT(variety == eVarietyResponse); + entry->variety = eVarietyResponseNetOriginal; + // Copy entry->headerNameOriginal because in SetHeader_internal we are going + // to a new one and a realocation can happen. + nsCString headerNameOriginal = entry->headerNameOriginal; + nsresult rv = SetHeader_internal(header, headerNameOriginal, newValue, + eVarietyResponse); + if (NS_FAILED(rv)) { + return rv; + } + } else { + entry->value = newValue; + entry->variety = variety; + } + return NS_OK; +} + +inline bool nsHttpHeaderArray::IsSuspectDuplicateHeader(nsHttpAtom header) { + bool retval = header == nsHttp::Content_Length || + header == nsHttp::Content_Disposition || + header == nsHttp::Location; + + MOZ_ASSERT(!retval || IsSingletonHeader(header), + "Only non-mergeable headers should be in this list\n"); + + return retval; +} + +} // namespace net +} // namespace mozilla + +#endif diff --git a/netwerk/protocol/http/nsHttpNTLMAuth.cpp b/netwerk/protocol/http/nsHttpNTLMAuth.cpp new file mode 100644 index 0000000000..0a30de0510 --- /dev/null +++ b/netwerk/protocol/http/nsHttpNTLMAuth.cpp @@ -0,0 +1,404 @@ +/* vim:set ts=4 sw=2 sts=2 et ci: */ +/* 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/. */ + +// HttpLog.h should generally be included first +#include "HttpLog.h" + +#include "nsHttpNTLMAuth.h" +#include "nsIAuthModule.h" +#include "nsCOMPtr.h" +#include "nsServiceManagerUtils.h" +#include "plbase64.h" +#include "plstr.h" +#include "prnetdb.h" + +//----------------------------------------------------------------------------- + +#include "nsIPrefBranch.h" +#include "nsIHttpAuthenticableChannel.h" +#include "nsIURI.h" +#ifdef XP_WIN +# include "nsIChannel.h" +# include "nsIX509Cert.h" +# include "nsITransportSecurityInfo.h" +#endif +#include "mozilla/Attributes.h" +#include "mozilla/Base64.h" +#include "mozilla/CheckedInt.h" +#include "mozilla/Maybe.h" +#include "mozilla/Tokenizer.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/Unused.h" +#include "nsNetUtil.h" +#include "nsIChannel.h" +#include "nsUnicharUtils.h" +#include "mozilla/net/HttpAuthUtils.h" +#include "mozilla/ClearOnShutdown.h" + +namespace mozilla { +namespace net { + +static const char kAllowProxies[] = "network.automatic-ntlm-auth.allow-proxies"; +static const char kAllowNonFqdn[] = + "network.automatic-ntlm-auth.allow-non-fqdn"; +static const char kTrustedURIs[] = "network.automatic-ntlm-auth.trusted-uris"; +static const char kForceGeneric[] = "network.auth.force-generic-ntlm"; +static const char kSSOinPBmode[] = "network.auth.private-browsing-sso"; + +StaticRefPtr nsHttpNTLMAuth::gSingleton; + +static bool IsNonFqdn(nsIURI* uri) { + nsAutoCString host; + PRNetAddr addr; + + if (NS_FAILED(uri->GetAsciiHost(host))) return false; + + // return true if host does not contain a dot and is not an ip address + return !host.IsEmpty() && !host.Contains('.') && + PR_StringToNetAddr(host.BeginReading(), &addr) != PR_SUCCESS; +} + +// Check to see if we should use our generic (internal) NTLM auth module. +static bool ForceGenericNTLM() { + nsCOMPtr prefs = do_GetService(NS_PREFSERVICE_CONTRACTID); + if (!prefs) return false; + bool flag = false; + + if (NS_FAILED(prefs->GetBoolPref(kForceGeneric, &flag))) flag = false; + + LOG(("Force use of generic ntlm auth module: %d\n", flag)); + return flag; +} + +// Check to see if we should use default credentials for this host or proxy. +static bool CanUseDefaultCredentials(nsIHttpAuthenticableChannel* channel, + bool isProxyAuth) { + nsCOMPtr prefs = do_GetService(NS_PREFSERVICE_CONTRACTID); + if (!prefs) { + return false; + } + + // Proxy should go all the time, it's not considered a privacy leak + // to send default credentials to a proxy. + if (isProxyAuth) { + bool val; + if (NS_FAILED(prefs->GetBoolPref(kAllowProxies, &val))) val = false; + LOG(("Default credentials allowed for proxy: %d\n", val)); + return val; + } + + // Prevent using default credentials for authentication when we are in the + // private browsing mode (but not in "never remember history" mode) and when + // not explicitely allowed. Otherwise, it would cause a privacy data leak. + nsCOMPtr bareChannel = do_QueryInterface(channel); + MOZ_ASSERT(bareChannel); + + if (NS_UsePrivateBrowsing(bareChannel)) { + bool ssoInPb; + if (NS_SUCCEEDED(prefs->GetBoolPref(kSSOinPBmode, &ssoInPb)) && ssoInPb) { + return true; + } + + bool dontRememberHistory; + if (NS_SUCCEEDED(prefs->GetBoolPref("browser.privatebrowsing.autostart", + &dontRememberHistory)) && + !dontRememberHistory) { + return false; + } + } + + nsCOMPtr uri; + Unused << channel->GetURI(getter_AddRefs(uri)); + + bool allowNonFqdn; + if (NS_FAILED(prefs->GetBoolPref(kAllowNonFqdn, &allowNonFqdn))) + allowNonFqdn = false; + if (allowNonFqdn && uri && IsNonFqdn(uri)) { + LOG(("Host is non-fqdn, default credentials are allowed\n")); + return true; + } + + bool isTrustedHost = (uri && auth::URIMatchesPrefPattern(uri, kTrustedURIs)); + LOG(("Default credentials allowed for host: %d\n", isTrustedHost)); + return isTrustedHost; +} + +// Dummy class for session state object. This class doesn't hold any data. +// Instead we use its existence as a flag. See ChallengeReceived. +class nsNTLMSessionState final : public nsISupports { + ~nsNTLMSessionState() = default; + + public: + NS_DECL_ISUPPORTS +}; +NS_IMPL_ISUPPORTS0(nsNTLMSessionState) + +//----------------------------------------------------------------------------- + +already_AddRefed nsHttpNTLMAuth::GetOrCreate() { + nsCOMPtr authenticator; + if (gSingleton) { + authenticator = gSingleton; + } else { + gSingleton = new nsHttpNTLMAuth(); + ClearOnShutdown(&gSingleton); + authenticator = gSingleton; + } + + return authenticator.forget(); +} + +NS_IMPL_ISUPPORTS(nsHttpNTLMAuth, nsIHttpAuthenticator) + +NS_IMETHODIMP +nsHttpNTLMAuth::ChallengeReceived(nsIHttpAuthenticableChannel* channel, + const char* challenge, bool isProxyAuth, + nsISupports** sessionState, + nsISupports** continuationState, + bool* identityInvalid) { + LOG(("nsHttpNTLMAuth::ChallengeReceived [ss=%p cs=%p]\n", *sessionState, + *continuationState)); + + // Use the native NTLM if available + mUseNative = true; + + // NOTE: we don't define any session state, but we do use the pointer. + + *identityInvalid = false; + + // Start a new auth sequence if the challenge is exactly "NTLM". + // If native NTLM auth apis are available and enabled through prefs, + // try to use them. + if (PL_strcasecmp(challenge, "NTLM") == 0) { + nsCOMPtr module; + + // Check to see if we should default to our generic NTLM auth module + // through UseGenericNTLM. (We use native auth by default if the + // system provides it.) If *sessionState is non-null, we failed to + // instantiate a native NTLM module the last time, so skip trying again. + bool forceGeneric = ForceGenericNTLM(); + if (!forceGeneric && !*sessionState) { + // Check for approved default credentials hosts and proxies. If + // *continuationState is non-null, the last authentication attempt + // failed so skip default credential use. + if (!*continuationState && + CanUseDefaultCredentials(channel, isProxyAuth)) { + // Try logging in with the user's default credentials. If + // successful, |identityInvalid| is false, which will trigger + // a default credentials attempt once we return. + module = nsIAuthModule::CreateInstance("sys-ntlm"); + } +#ifdef XP_WIN + else { + // Try to use native NTLM and prompt the user for their domain, + // username, and password. (only supported by windows nsAuthSSPI + // module.) Note, for servers that use LMv1 a weak hash of the user's + // password will be sent. We rely on windows internal apis to decide + // whether we should support this older, less secure version of the + // protocol. + module = nsIAuthModule::CreateInstance("sys-ntlm"); + *identityInvalid = true; + } +#endif // XP_WIN + if (!module) LOG(("Native sys-ntlm auth module not found.\n")); + } + +#ifdef XP_WIN + // On windows, never fall back unless the user has specifically requested + // so. + if (!forceGeneric && !module) return NS_ERROR_UNEXPECTED; +#endif + + // If no native support was available. Fall back on our internal NTLM + // implementation. + if (!module) { + if (!*sessionState) { + // Remember the fact that we cannot use the "sys-ntlm" module, + // so we don't ever bother trying again for this auth domain. + RefPtr state = new nsNTLMSessionState(); + state.forget(sessionState); + } + + // Use our internal NTLM implementation. Note, this is less secure, + // see bug 520607 for details. + LOG(("Trying to fall back on internal ntlm auth.\n")); + module = nsIAuthModule::CreateInstance("ntlm"); + + mUseNative = false; + + // Prompt user for domain, username, and password. + *identityInvalid = true; + } + + // If this fails, then it means that we cannot do NTLM auth. + if (!module) { + LOG(("No ntlm auth modules available.\n")); + return NS_ERROR_UNEXPECTED; + } + + // A non-null continuation state implies that we failed to authenticate. + // Blow away the old authentication state, and use the new one. + module.forget(continuationState); + } + return NS_OK; +} + +NS_IMETHODIMP +nsHttpNTLMAuth::GenerateCredentialsAsync( + nsIHttpAuthenticableChannel* authChannel, + nsIHttpAuthenticatorCallback* aCallback, const char* challenge, + bool isProxyAuth, const char16_t* domain, const char16_t* username, + const char16_t* password, nsISupports* sessionState, + nsISupports* continuationState, nsICancelable** aCancellable) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsHttpNTLMAuth::GenerateCredentials(nsIHttpAuthenticableChannel* authChannel, + const char* challenge, bool isProxyAuth, + const char16_t* domain, + const char16_t* user, const char16_t* pass, + nsISupports** sessionState, + nsISupports** continuationState, + uint32_t* aFlags, char** creds) + +{ + LOG(("nsHttpNTLMAuth::GenerateCredentials\n")); + + *creds = nullptr; + *aFlags = 0; + + // if user or password is empty, ChallengeReceived returned + // identityInvalid = false, that means we are using default user + // credentials; see nsAuthSSPI::Init method for explanation of this + // condition + if (!user || !pass) *aFlags = USING_INTERNAL_IDENTITY; + + nsresult rv; + nsCOMPtr module = do_QueryInterface(*continuationState, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + void *inBuf, *outBuf; + uint32_t inBufLen, outBufLen; + Maybe> certArray; + + // initial challenge + if (PL_strcasecmp(challenge, "NTLM") == 0) { + // NTLM service name format is 'HTTP@host' for both http and https + nsCOMPtr uri; + rv = authChannel->GetURI(getter_AddRefs(uri)); + if (NS_FAILED(rv)) return rv; + nsAutoCString serviceName, host; + rv = uri->GetAsciiHost(host); + if (NS_FAILED(rv)) return rv; + serviceName.AppendLiteral("HTTP@"); + serviceName.Append(host); + // initialize auth module + uint32_t reqFlags = nsIAuthModule::REQ_DEFAULT; + if (isProxyAuth) reqFlags |= nsIAuthModule::REQ_PROXY_AUTH; + + rv = module->Init(serviceName.get(), reqFlags, domain, user, pass); + if (NS_FAILED(rv)) return rv; + +// This update enables updated Windows machines (Win7 or patched previous +// versions) and Linux machines running Samba (updated for Channel +// Binding), to perform Channel Binding when authenticating using NTLMv2 +// and an outer secure channel. +// +// Currently only implemented for Windows, linux support will be landing in +// a separate patch, update this #ifdef accordingly then. +#if defined(XP_WIN) /* || defined (LINUX) */ + // We should retrieve the server certificate and compute the CBT, + // but only when we are using the native NTLM implementation and + // not the internal one. + // It is a valid case not having the security info object. This + // occures when we connect an https site through an ntlm proxy. + // After the ssl tunnel has been created, we get here the second + // time and now generate the CBT from now valid security info. + nsCOMPtr channel = do_QueryInterface(authChannel, &rv); + if (NS_FAILED(rv)) return rv; + + nsCOMPtr security; + rv = channel->GetSecurityInfo(getter_AddRefs(security)); + if (NS_FAILED(rv)) return rv; + + nsCOMPtr secInfo = do_QueryInterface(security); + + if (mUseNative && secInfo) { + nsCOMPtr cert; + rv = secInfo->GetServerCert(getter_AddRefs(cert)); + if (NS_FAILED(rv)) return rv; + + certArray.emplace(); + rv = cert->GetRawDER(*certArray); + if (NS_FAILED(rv)) return rv; + + // If there is a server certificate, we pass it along the + // first time we call GetNextToken(). + inBufLen = certArray->Length(); + inBuf = certArray->Elements(); + } else { + // If there is no server certificate, we don't pass anything. + inBufLen = 0; + inBuf = nullptr; + } +#else // Extended protection update is just for Linux and Windows machines. + inBufLen = 0; + inBuf = nullptr; +#endif + } else { + // decode challenge; skip past "NTLM " to the start of the base64 + // encoded data. + int len = strlen(challenge); + if (len < 6) return NS_ERROR_UNEXPECTED; // bogus challenge + challenge += 5; + len -= 5; + + // strip off any padding (see bug 230351) + while (len && challenge[len - 1] == '=') len--; + + // decode into the input secbuffer + rv = Base64Decode(challenge, len, (char**)&inBuf, &inBufLen); + if (NS_FAILED(rv)) { + return rv; + } + } + + rv = module->GetNextToken(inBuf, inBufLen, &outBuf, &outBufLen); + if (NS_SUCCEEDED(rv)) { + // base64 encode data in output buffer and prepend "NTLM " + CheckedUint32 credsLen = ((CheckedUint32(outBufLen) + 2) / 3) * 4; + credsLen += 5; // "NTLM " + credsLen += 1; // null terminate + + if (!credsLen.isValid()) { + rv = NS_ERROR_FAILURE; + } else { + *creds = (char*)moz_xmalloc(credsLen.value()); + memcpy(*creds, "NTLM ", 5); + PL_Base64Encode((char*)outBuf, outBufLen, *creds + 5); + (*creds)[credsLen.value() - 1] = '\0'; // null terminate + } + + // OK, we are done with |outBuf| + free(outBuf); + } + + // inBuf needs to be freed if it's not pointing into certArray + if (inBuf && !certArray) { + free(inBuf); + } + + return rv; +} + +NS_IMETHODIMP +nsHttpNTLMAuth::GetAuthFlags(uint32_t* flags) { + *flags = CONNECTION_BASED | IDENTITY_INCLUDES_DOMAIN | IDENTITY_ENCRYPTED; + return NS_OK; +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/nsHttpNTLMAuth.h b/netwerk/protocol/http/nsHttpNTLMAuth.h new file mode 100644 index 0000000000..cd3906737b --- /dev/null +++ b/netwerk/protocol/http/nsHttpNTLMAuth.h @@ -0,0 +1,36 @@ +/* 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/. */ + +#ifndef nsHttpNTLMAuth_h__ +#define nsHttpNTLMAuth_h__ + +#include "nsIHttpAuthenticator.h" +#include "mozilla/StaticPtr.h" + +namespace mozilla { +namespace net { + +class nsHttpNTLMAuth : public nsIHttpAuthenticator { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIHTTPAUTHENTICATOR + + nsHttpNTLMAuth() : mUseNative(false) {} + + static already_AddRefed GetOrCreate(); + + private: + virtual ~nsHttpNTLMAuth() = default; + + // This flag indicates whether we are using the native NTLM implementation + // or the internal one. + bool mUseNative; + + static StaticRefPtr gSingleton; +}; + +} // namespace net +} // namespace mozilla + +#endif // !nsHttpNTLMAuth_h__ diff --git a/netwerk/protocol/http/nsHttpRequestHead.cpp b/netwerk/protocol/http/nsHttpRequestHead.cpp new file mode 100644 index 0000000000..0cbed53465 --- /dev/null +++ b/netwerk/protocol/http/nsHttpRequestHead.cpp @@ -0,0 +1,360 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +// HttpLog.h should generally be included first +#include "HttpLog.h" + +#include "nsHttpRequestHead.h" +#include "nsIHttpHeaderVisitor.h" + +//----------------------------------------------------------------------------- +// nsHttpRequestHead +//----------------------------------------------------------------------------- + +namespace mozilla { +namespace net { + +nsHttpRequestHead::nsHttpRequestHead() + : mMethod("GET"_ns), + mVersion(HttpVersion::v1_1), + mParsedMethod(kMethod_Get), + mHTTPS(false), + mRecursiveMutex("nsHttpRequestHead.mRecursiveMutex"), + mInVisitHeaders(false) { + MOZ_COUNT_CTOR(nsHttpRequestHead); +} + +nsHttpRequestHead::nsHttpRequestHead(const nsHttpRequestHead& aRequestHead) + : mRecursiveMutex("nsHttpRequestHead.mRecursiveMutex"), + mInVisitHeaders(false) { + nsHttpRequestHead& other = const_cast(aRequestHead); + RecursiveMutexAutoLock monitor(other.mRecursiveMutex); + + mHeaders = other.mHeaders; + mMethod = other.mMethod; + mVersion = other.mVersion; + mRequestURI = other.mRequestURI; + mPath = other.mPath; + mOrigin = other.mOrigin; + mParsedMethod = other.mParsedMethod; + mHTTPS = other.mHTTPS; + mInVisitHeaders = false; +} + +nsHttpRequestHead::~nsHttpRequestHead() { MOZ_COUNT_DTOR(nsHttpRequestHead); } + +nsHttpRequestHead& nsHttpRequestHead::operator=( + const nsHttpRequestHead& aRequestHead) { + nsHttpRequestHead& other = const_cast(aRequestHead); + RecursiveMutexAutoLock monitorOther(other.mRecursiveMutex); + RecursiveMutexAutoLock monitor(mRecursiveMutex); + + mHeaders = other.mHeaders; + mMethod = other.mMethod; + mVersion = other.mVersion; + mRequestURI = other.mRequestURI; + mPath = other.mPath; + mOrigin = other.mOrigin; + mParsedMethod = other.mParsedMethod; + mHTTPS = other.mHTTPS; + mInVisitHeaders = false; + return *this; +} + +// Don't use this function. It is only used by HttpChannelParent to avoid +// copying of request headers!!! +const nsHttpHeaderArray& nsHttpRequestHead::Headers() const { + nsHttpRequestHead& curr = const_cast(*this); + curr.mRecursiveMutex.AssertCurrentThreadIn(); + return mHeaders; +} + +void nsHttpRequestHead::SetHeaders(const nsHttpHeaderArray& aHeaders) { + RecursiveMutexAutoLock mon(mRecursiveMutex); + mHeaders = aHeaders; +} + +void nsHttpRequestHead::SetVersion(HttpVersion version) { + RecursiveMutexAutoLock mon(mRecursiveMutex); + mVersion = version; +} + +void nsHttpRequestHead::SetRequestURI(const nsACString& s) { + RecursiveMutexAutoLock mon(mRecursiveMutex); + mRequestURI = s; +} + +void nsHttpRequestHead::SetPath(const nsACString& s) { + RecursiveMutexAutoLock mon(mRecursiveMutex); + mPath = s; +} + +uint32_t nsHttpRequestHead::HeaderCount() { + RecursiveMutexAutoLock mon(mRecursiveMutex); + return mHeaders.Count(); +} + +nsresult nsHttpRequestHead::VisitHeaders( + nsIHttpHeaderVisitor* visitor, + nsHttpHeaderArray::VisitorFilter + filter /* = nsHttpHeaderArray::eFilterAll*/) { + RecursiveMutexAutoLock mon(mRecursiveMutex); + mInVisitHeaders = true; + nsresult rv = mHeaders.VisitHeaders(visitor, filter); + mInVisitHeaders = false; + return rv; +} + +void nsHttpRequestHead::Method(nsACString& aMethod) { + RecursiveMutexAutoLock mon(mRecursiveMutex); + aMethod = mMethod; +} + +HttpVersion nsHttpRequestHead::Version() { + RecursiveMutexAutoLock mon(mRecursiveMutex); + return mVersion; +} + +void nsHttpRequestHead::RequestURI(nsACString& aRequestURI) { + RecursiveMutexAutoLock mon(mRecursiveMutex); + aRequestURI = mRequestURI; +} + +void nsHttpRequestHead::Path(nsACString& aPath) { + RecursiveMutexAutoLock mon(mRecursiveMutex); + aPath = mPath.IsEmpty() ? mRequestURI : mPath; +} + +void nsHttpRequestHead::SetHTTPS(bool val) { + RecursiveMutexAutoLock mon(mRecursiveMutex); + mHTTPS = val; +} + +void nsHttpRequestHead::Origin(nsACString& aOrigin) { + RecursiveMutexAutoLock mon(mRecursiveMutex); + aOrigin = mOrigin; +} + +nsresult nsHttpRequestHead::SetHeader(const nsACString& h, const nsACString& v, + bool m /*= false*/) { + RecursiveMutexAutoLock mon(mRecursiveMutex); + + if (mInVisitHeaders) { + return NS_ERROR_FAILURE; + } + + return mHeaders.SetHeader(h, v, m, + nsHttpHeaderArray::eVarietyRequestOverride); +} + +nsresult nsHttpRequestHead::SetHeader(nsHttpAtom h, const nsACString& v, + bool m /*= false*/) { + RecursiveMutexAutoLock mon(mRecursiveMutex); + + if (mInVisitHeaders) { + return NS_ERROR_FAILURE; + } + + return mHeaders.SetHeader(h, v, m, + nsHttpHeaderArray::eVarietyRequestOverride); +} + +nsresult nsHttpRequestHead::SetHeader( + nsHttpAtom h, const nsACString& v, bool m, + nsHttpHeaderArray::HeaderVariety variety) { + RecursiveMutexAutoLock mon(mRecursiveMutex); + + if (mInVisitHeaders) { + return NS_ERROR_FAILURE; + } + + return mHeaders.SetHeader(h, v, m, variety); +} + +nsresult nsHttpRequestHead::SetEmptyHeader(const nsACString& h) { + RecursiveMutexAutoLock mon(mRecursiveMutex); + + if (mInVisitHeaders) { + return NS_ERROR_FAILURE; + } + + return mHeaders.SetEmptyHeader(h, nsHttpHeaderArray::eVarietyRequestOverride); +} + +nsresult nsHttpRequestHead::GetHeader(nsHttpAtom h, nsACString& v) { + v.Truncate(); + RecursiveMutexAutoLock mon(mRecursiveMutex); + return mHeaders.GetHeader(h, v); +} + +nsresult nsHttpRequestHead::ClearHeader(nsHttpAtom h) { + RecursiveMutexAutoLock mon(mRecursiveMutex); + + if (mInVisitHeaders) { + return NS_ERROR_FAILURE; + } + + mHeaders.ClearHeader(h); + return NS_OK; +} + +void nsHttpRequestHead::ClearHeaders() { + RecursiveMutexAutoLock mon(mRecursiveMutex); + + if (mInVisitHeaders) { + return; + } + + mHeaders.Clear(); +} + +bool nsHttpRequestHead::HasHeader(nsHttpAtom h) { + RecursiveMutexAutoLock mon(mRecursiveMutex); + return mHeaders.HasHeader(h); +} + +bool nsHttpRequestHead::HasHeaderValue(nsHttpAtom h, const char* v) { + RecursiveMutexAutoLock mon(mRecursiveMutex); + return mHeaders.HasHeaderValue(h, v); +} + +nsresult nsHttpRequestHead::SetHeaderOnce(nsHttpAtom h, const char* v, + bool merge /*= false */) { + RecursiveMutexAutoLock mon(mRecursiveMutex); + + if (mInVisitHeaders) { + return NS_ERROR_FAILURE; + } + + if (!merge || !mHeaders.HasHeaderValue(h, v)) { + return mHeaders.SetHeader(h, nsDependentCString(v), merge, + nsHttpHeaderArray::eVarietyRequestOverride); + } + return NS_OK; +} + +nsHttpRequestHead::ParsedMethodType nsHttpRequestHead::ParsedMethod() { + RecursiveMutexAutoLock mon(mRecursiveMutex); + return mParsedMethod; +} + +bool nsHttpRequestHead::EqualsMethod(ParsedMethodType aType) { + RecursiveMutexAutoLock mon(mRecursiveMutex); + return mParsedMethod == aType; +} + +void nsHttpRequestHead::ParseHeaderSet(const char* buffer) { + RecursiveMutexAutoLock mon(mRecursiveMutex); + nsHttpAtom hdr; + nsAutoCString headerNameOriginal; + nsAutoCString val; + while (buffer) { + const char* eof = strchr(buffer, '\r'); + if (!eof) { + break; + } + if (NS_SUCCEEDED(nsHttpHeaderArray::ParseHeaderLine( + nsDependentCSubstring(buffer, eof - buffer), &hdr, + &headerNameOriginal, &val))) { + DebugOnly rv = + mHeaders.SetHeaderFromNet(hdr, headerNameOriginal, val, false); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + buffer = eof + 1; + if (*buffer == '\n') { + buffer++; + } + } +} + +bool nsHttpRequestHead::IsHTTPS() { + RecursiveMutexAutoLock mon(mRecursiveMutex); + return mHTTPS; +} + +void nsHttpRequestHead::SetMethod(const nsACString& method) { + RecursiveMutexAutoLock mon(mRecursiveMutex); + + mMethod = method; + ParseMethod(mMethod, mParsedMethod); +} + +// static +void nsHttpRequestHead::ParseMethod(const nsCString& aRawMethod, + ParsedMethodType& aParsedMethod) { + aParsedMethod = kMethod_Custom; + if (!strcmp(aRawMethod.get(), "GET")) { + aParsedMethod = kMethod_Get; + } else if (!strcmp(aRawMethod.get(), "POST")) { + aParsedMethod = kMethod_Post; + } else if (!strcmp(aRawMethod.get(), "OPTIONS")) { + aParsedMethod = kMethod_Options; + } else if (!strcmp(aRawMethod.get(), "CONNECT")) { + aParsedMethod = kMethod_Connect; + } else if (!strcmp(aRawMethod.get(), "HEAD")) { + aParsedMethod = kMethod_Head; + } else if (!strcmp(aRawMethod.get(), "PUT")) { + aParsedMethod = kMethod_Put; + } else if (!strcmp(aRawMethod.get(), "TRACE")) { + aParsedMethod = kMethod_Trace; + } +} + +void nsHttpRequestHead::SetOrigin(const nsACString& scheme, + const nsACString& host, int32_t port) { + RecursiveMutexAutoLock mon(mRecursiveMutex); + mOrigin.Assign(scheme); + mOrigin.AppendLiteral("://"); + mOrigin.Append(host); + if (port >= 0) { + mOrigin.AppendLiteral(":"); + mOrigin.AppendInt(port); + } +} + +bool nsHttpRequestHead::IsSafeMethod() { + RecursiveMutexAutoLock mon(mRecursiveMutex); + // This code will need to be extended for new safe methods, otherwise + // they'll default to "not safe". + if ((mParsedMethod == kMethod_Get) || (mParsedMethod == kMethod_Head) || + (mParsedMethod == kMethod_Options) || (mParsedMethod == kMethod_Trace)) { + return true; + } + + if (mParsedMethod != kMethod_Custom) { + return false; + } + + return (!strcmp(mMethod.get(), "PROPFIND") || + !strcmp(mMethod.get(), "REPORT") || !strcmp(mMethod.get(), "SEARCH")); +} + +void nsHttpRequestHead::Flatten(nsACString& buf, bool pruneProxyHeaders) { + RecursiveMutexAutoLock mon(mRecursiveMutex); + // note: the first append is intentional. + + buf.Append(mMethod); + buf.Append(' '); + buf.Append(mRequestURI); + buf.AppendLiteral(" HTTP/"); + + switch (mVersion) { + case HttpVersion::v1_1: + buf.AppendLiteral("1.1"); + break; + case HttpVersion::v0_9: + buf.AppendLiteral("0.9"); + break; + default: + buf.AppendLiteral("1.0"); + } + + buf.AppendLiteral("\r\n"); + + mHeaders.Flatten(buf, pruneProxyHeaders, false); +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/nsHttpRequestHead.h b/netwerk/protocol/http/nsHttpRequestHead.h new file mode 100644 index 0000000000..9dcbda06ed --- /dev/null +++ b/netwerk/protocol/http/nsHttpRequestHead.h @@ -0,0 +1,147 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef nsHttpRequestHead_h__ +#define nsHttpRequestHead_h__ + +#include "nsHttp.h" +#include "nsHttpHeaderArray.h" +#include "nsString.h" +#include "mozilla/RecursiveMutex.h" + +class nsIHttpHeaderVisitor; + +// This needs to be forward declared here so we can include only this header +// without also including PHttpChannelParams.h +namespace IPC { +template +struct ParamTraits; +} // namespace IPC + +namespace mozilla { +namespace net { + +//----------------------------------------------------------------------------- +// nsHttpRequestHead represents the request line and headers from an HTTP +// request. +//----------------------------------------------------------------------------- + +class nsHttpRequestHead { + public: + nsHttpRequestHead(); + explicit nsHttpRequestHead(const nsHttpRequestHead& aRequestHead); + ~nsHttpRequestHead(); + + nsHttpRequestHead& operator=(const nsHttpRequestHead& aRequestHead); + + // The following function is only used in HttpChannelParent to avoid + // copying headers. If you use it be careful to do it only under + // nsHttpRequestHead lock!!! + const nsHttpHeaderArray& Headers() const; + void Enter() { mRecursiveMutex.Lock(); } + void Exit() { mRecursiveMutex.Unlock(); } + + void SetHeaders(const nsHttpHeaderArray& aHeaders); + + void SetMethod(const nsACString& method); + void SetVersion(HttpVersion version); + void SetRequestURI(const nsACString& s); + void SetPath(const nsACString& s); + uint32_t HeaderCount(); + + // Using this function it is possible to itereate through all headers + // automatically under one lock. + [[nodiscard]] nsresult VisitHeaders( + nsIHttpHeaderVisitor* visitor, + nsHttpHeaderArray::VisitorFilter filter = nsHttpHeaderArray::eFilterAll); + void Method(nsACString& aMethod); + HttpVersion Version(); + void RequestURI(nsACString& RequestURI); + void Path(nsACString& aPath); + void SetHTTPS(bool val); + bool IsHTTPS(); + + void SetOrigin(const nsACString& scheme, const nsACString& host, + int32_t port); + void Origin(nsACString& aOrigin); + + [[nodiscard]] nsresult SetHeader(const nsACString& h, const nsACString& v, + bool m = false); + [[nodiscard]] nsresult SetHeader(nsHttpAtom h, const nsACString& v, + bool m = false); + [[nodiscard]] nsresult SetHeader(nsHttpAtom h, const nsACString& v, bool m, + nsHttpHeaderArray::HeaderVariety variety); + [[nodiscard]] nsresult SetEmptyHeader(const nsACString& h); + [[nodiscard]] nsresult GetHeader(nsHttpAtom h, nsACString& v); + + [[nodiscard]] nsresult ClearHeader(nsHttpAtom h); + void ClearHeaders(); + + bool HasHeaderValue(nsHttpAtom h, const char* v); + // This function returns true if header is set even if it is an empty + // header. + bool HasHeader(nsHttpAtom h); + void Flatten(nsACString&, bool pruneProxyHeaders = false); + + // Don't allow duplicate values + [[nodiscard]] nsresult SetHeaderOnce(nsHttpAtom h, const char* v, + bool merge = false); + + bool IsSafeMethod(); + + enum ParsedMethodType { + kMethod_Custom, + kMethod_Get, + kMethod_Post, + kMethod_Options, + kMethod_Connect, + kMethod_Head, + kMethod_Put, + kMethod_Trace + }; + + static void ParseMethod(const nsCString& aRawMethod, + ParsedMethodType& aParsedMethod); + + ParsedMethodType ParsedMethod(); + bool EqualsMethod(ParsedMethodType aType); + bool IsGet() { return EqualsMethod(kMethod_Get); } + bool IsPost() { return EqualsMethod(kMethod_Post); } + bool IsOptions() { return EqualsMethod(kMethod_Options); } + bool IsConnect() { return EqualsMethod(kMethod_Connect); } + bool IsHead() { return EqualsMethod(kMethod_Head); } + bool IsPut() { return EqualsMethod(kMethod_Put); } + bool IsTrace() { return EqualsMethod(kMethod_Trace); } + void ParseHeaderSet(const char* buffer); + + private: + // All members must be copy-constructable and assignable + nsHttpHeaderArray mHeaders; + nsCString mMethod; + HttpVersion mVersion; + + // mRequestURI and mPath are strings instead of an nsIURI + // because this is used off the main thread + nsCString mRequestURI; + nsCString mPath; + + nsCString mOrigin; + ParsedMethodType mParsedMethod; + bool mHTTPS; + + // We are using RecursiveMutex instead of a Mutex because VisitHeader + // function calls nsIHttpHeaderVisitor::VisitHeader while under lock. + RecursiveMutex mRecursiveMutex; + + // During VisitHeader we sould not allow cal to SetHeader. + bool mInVisitHeaders; + + friend struct IPC::ParamTraits; +}; + +} // namespace net +} // namespace mozilla + +#endif // nsHttpRequestHead_h__ diff --git a/netwerk/protocol/http/nsHttpResponseHead.cpp b/netwerk/protocol/http/nsHttpResponseHead.cpp new file mode 100644 index 0000000000..3c6c23e80d --- /dev/null +++ b/netwerk/protocol/http/nsHttpResponseHead.cpp @@ -0,0 +1,1266 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=4 sw=2 sts=2 et cin: */ +/* 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/. */ + +// HttpLog.h should generally be included first +#include "HttpLog.h" + +#include "mozilla/Unused.h" +#include "nsHttpResponseHead.h" +#include "nsIHttpHeaderVisitor.h" +#include "nsPrintfCString.h" +#include "prtime.h" +#include "plstr.h" +#include "nsURLHelper.h" +#include "CacheControlParser.h" +#include + +namespace mozilla { +namespace net { + +//----------------------------------------------------------------------------- +// nsHttpResponseHead +//----------------------------------------------------------------------------- + +// Note that the code below MUST be synchronized with the IPC +// serialization/deserialization in PHttpChannelParams.h. +nsHttpResponseHead::nsHttpResponseHead(const nsHttpResponseHead& aOther) + : mRecursiveMutex("nsHttpResponseHead.mRecursiveMutex"), + mInVisitHeaders(false) { + nsHttpResponseHead& other = const_cast(aOther); + RecursiveMutexAutoLock monitor(other.mRecursiveMutex); + + mHeaders = other.mHeaders; + mVersion = other.mVersion; + mStatus = other.mStatus; + mStatusText = other.mStatusText; + mContentLength = other.mContentLength; + mContentType = other.mContentType; + mContentCharset = other.mContentCharset; + mHasCacheControl = other.mHasCacheControl; + mCacheControlPublic = other.mCacheControlPublic; + mCacheControlPrivate = other.mCacheControlPrivate; + mCacheControlNoStore = other.mCacheControlNoStore; + mCacheControlNoCache = other.mCacheControlNoCache; + mCacheControlImmutable = other.mCacheControlImmutable; + mCacheControlStaleWhileRevalidateSet = + other.mCacheControlStaleWhileRevalidateSet; + mCacheControlStaleWhileRevalidate = other.mCacheControlStaleWhileRevalidate; + mCacheControlMaxAgeSet = other.mCacheControlMaxAgeSet; + mCacheControlMaxAge = other.mCacheControlMaxAge; + mPragmaNoCache = other.mPragmaNoCache; +} + +nsHttpResponseHead& nsHttpResponseHead::operator=( + const nsHttpResponseHead& aOther) { + nsHttpResponseHead& other = const_cast(aOther); + RecursiveMutexAutoLock monitorOther(other.mRecursiveMutex); + RecursiveMutexAutoLock monitor(mRecursiveMutex); + + mHeaders = other.mHeaders; + mVersion = other.mVersion; + mStatus = other.mStatus; + mStatusText = other.mStatusText; + mContentLength = other.mContentLength; + mContentType = other.mContentType; + mContentCharset = other.mContentCharset; + mHasCacheControl = other.mHasCacheControl; + mCacheControlPublic = other.mCacheControlPublic; + mCacheControlPrivate = other.mCacheControlPrivate; + mCacheControlNoStore = other.mCacheControlNoStore; + mCacheControlNoCache = other.mCacheControlNoCache; + mCacheControlImmutable = other.mCacheControlImmutable; + mCacheControlStaleWhileRevalidateSet = + other.mCacheControlStaleWhileRevalidateSet; + mCacheControlStaleWhileRevalidate = other.mCacheControlStaleWhileRevalidate; + mCacheControlMaxAgeSet = other.mCacheControlMaxAgeSet; + mCacheControlMaxAge = other.mCacheControlMaxAge; + mPragmaNoCache = other.mPragmaNoCache; + + return *this; +} + +HttpVersion nsHttpResponseHead::Version() { + RecursiveMutexAutoLock monitor(mRecursiveMutex); + return mVersion; +} + +uint16_t nsHttpResponseHead::Status() const { + RecursiveMutexAutoLock monitor(mRecursiveMutex); + return mStatus; +} + +void nsHttpResponseHead::StatusText(nsACString& aStatusText) { + RecursiveMutexAutoLock monitor(mRecursiveMutex); + aStatusText = mStatusText; +} + +int64_t nsHttpResponseHead::ContentLength() { + RecursiveMutexAutoLock monitor(mRecursiveMutex); + return mContentLength; +} + +void nsHttpResponseHead::ContentType(nsACString& aContentType) const { + RecursiveMutexAutoLock monitor(mRecursiveMutex); + aContentType = mContentType; +} + +void nsHttpResponseHead::ContentCharset(nsACString& aContentCharset) { + RecursiveMutexAutoLock monitor(mRecursiveMutex); + aContentCharset = mContentCharset; +} + +bool nsHttpResponseHead::Public() { + RecursiveMutexAutoLock monitor(mRecursiveMutex); + return mCacheControlPublic; +} + +bool nsHttpResponseHead::Private() { + RecursiveMutexAutoLock monitor(mRecursiveMutex); + return mCacheControlPrivate; +} + +bool nsHttpResponseHead::NoStore() { + RecursiveMutexAutoLock monitor(mRecursiveMutex); + return mCacheControlNoStore; +} + +bool nsHttpResponseHead::NoCache() { + RecursiveMutexAutoLock monitor(mRecursiveMutex); + return NoCache_locked(); +} + +bool nsHttpResponseHead::Immutable() { + RecursiveMutexAutoLock monitor(mRecursiveMutex); + return mCacheControlImmutable; +} + +nsresult nsHttpResponseHead::SetHeader(const nsACString& hdr, + const nsACString& val, bool merge) { + RecursiveMutexAutoLock monitor(mRecursiveMutex); + + if (mInVisitHeaders) { + return NS_ERROR_FAILURE; + } + + nsHttpAtom atom = nsHttp::ResolveAtom(PromiseFlatCString(hdr).get()); + if (!atom) { + NS_WARNING("failed to resolve atom"); + return NS_ERROR_NOT_AVAILABLE; + } + + return SetHeader_locked(atom, hdr, val, merge); +} + +nsresult nsHttpResponseHead::SetHeader(nsHttpAtom hdr, const nsACString& val, + bool merge) { + RecursiveMutexAutoLock monitor(mRecursiveMutex); + + if (mInVisitHeaders) { + return NS_ERROR_FAILURE; + } + + return SetHeader_locked(hdr, ""_ns, val, merge); +} + +nsresult nsHttpResponseHead::SetHeader_locked(nsHttpAtom atom, + const nsACString& hdr, + const nsACString& val, + bool merge) { + nsresult rv = mHeaders.SetHeader(atom, hdr, val, merge, + nsHttpHeaderArray::eVarietyResponse); + if (NS_FAILED(rv)) return rv; + + // respond to changes in these headers. we need to reparse the entire + // header since the change may have merged in additional values. + if (atom == nsHttp::Cache_Control) + ParseCacheControl(mHeaders.PeekHeader(atom)); + else if (atom == nsHttp::Pragma) + ParsePragma(mHeaders.PeekHeader(atom)); + + return NS_OK; +} + +nsresult nsHttpResponseHead::GetHeader(nsHttpAtom h, nsACString& v) { + v.Truncate(); + RecursiveMutexAutoLock monitor(mRecursiveMutex); + return mHeaders.GetHeader(h, v); +} + +void nsHttpResponseHead::ClearHeader(nsHttpAtom h) { + RecursiveMutexAutoLock monitor(mRecursiveMutex); + mHeaders.ClearHeader(h); +} + +void nsHttpResponseHead::ClearHeaders() { + RecursiveMutexAutoLock monitor(mRecursiveMutex); + mHeaders.Clear(); +} + +bool nsHttpResponseHead::HasHeaderValue(nsHttpAtom h, const char* v) { + RecursiveMutexAutoLock monitor(mRecursiveMutex); + return mHeaders.HasHeaderValue(h, v); +} + +bool nsHttpResponseHead::HasHeader(nsHttpAtom h) const { + RecursiveMutexAutoLock monitor(mRecursiveMutex); + return mHeaders.HasHeader(h); +} + +void nsHttpResponseHead::SetContentType(const nsACString& s) { + RecursiveMutexAutoLock monitor(mRecursiveMutex); + mContentType = s; +} + +void nsHttpResponseHead::SetContentCharset(const nsACString& s) { + RecursiveMutexAutoLock monitor(mRecursiveMutex); + mContentCharset = s; +} + +void nsHttpResponseHead::SetContentLength(int64_t len) { + RecursiveMutexAutoLock monitor(mRecursiveMutex); + + mContentLength = len; + if (len < 0) + mHeaders.ClearHeader(nsHttp::Content_Length); + else { + DebugOnly rv = mHeaders.SetHeader( + nsHttp::Content_Length, nsPrintfCString("%" PRId64, len), false, + nsHttpHeaderArray::eVarietyResponse); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } +} + +void nsHttpResponseHead::Flatten(nsACString& buf, bool pruneTransients) { + RecursiveMutexAutoLock monitor(mRecursiveMutex); + if (mVersion == HttpVersion::v0_9) return; + + buf.AppendLiteral("HTTP/"); + if (mVersion == HttpVersion::v3_0) { + buf.AppendLiteral("3 "); + } else if (mVersion == HttpVersion::v2_0) { + buf.AppendLiteral("2 "); + } else if (mVersion == HttpVersion::v1_1) { + buf.AppendLiteral("1.1 "); + } else { + buf.AppendLiteral("1.0 "); + } + + buf.Append(nsPrintfCString("%u", unsigned(mStatus)) + " "_ns + mStatusText + + "\r\n"_ns); + + mHeaders.Flatten(buf, false, pruneTransients); +} + +void nsHttpResponseHead::FlattenNetworkOriginalHeaders(nsACString& buf) { + RecursiveMutexAutoLock monitor(mRecursiveMutex); + if (mVersion == HttpVersion::v0_9) { + return; + } + + mHeaders.FlattenOriginalHeader(buf); +} + +nsresult nsHttpResponseHead::ParseCachedHead(const char* block) { + RecursiveMutexAutoLock monitor(mRecursiveMutex); + LOG(("nsHttpResponseHead::ParseCachedHead [this=%p]\n", this)); + + // this command works on a buffer as prepared by Flatten, as such it is + // not very forgiving ;-) + + char* p = PL_strstr(block, "\r\n"); + if (!p) return NS_ERROR_UNEXPECTED; + + ParseStatusLine_locked(nsDependentCSubstring(block, p - block)); + + do { + block = p + 2; + + if (*block == 0) break; + + p = PL_strstr(block, "\r\n"); + if (!p) return NS_ERROR_UNEXPECTED; + + Unused << ParseHeaderLine_locked(nsDependentCSubstring(block, p - block), + false); + + } while (true); + + return NS_OK; +} + +nsresult nsHttpResponseHead::ParseCachedOriginalHeaders(char* block) { + RecursiveMutexAutoLock monitor(mRecursiveMutex); + LOG(("nsHttpResponseHead::ParseCachedOriginalHeader [this=%p]\n", this)); + + // this command works on a buffer as prepared by FlattenOriginalHeader, + // as such it is not very forgiving ;-) + + if (!block) { + return NS_ERROR_UNEXPECTED; + } + + char* p = block; + nsHttpAtom hdr; + nsAutoCString headerNameOriginal; + nsAutoCString val; + nsresult rv; + + do { + block = p; + + if (*block == 0) break; + + p = PL_strstr(block, "\r\n"); + if (!p) return NS_ERROR_UNEXPECTED; + + *p = 0; + if (NS_FAILED(nsHttpHeaderArray::ParseHeaderLine( + nsDependentCString(block, p - block), &hdr, &headerNameOriginal, + &val))) { + return NS_OK; + } + + rv = mHeaders.SetResponseHeaderFromCache( + hdr, headerNameOriginal, val, + nsHttpHeaderArray::eVarietyResponseNetOriginal); + + if (NS_FAILED(rv)) { + return rv; + } + + p = p + 2; + } while (true); + + return NS_OK; +} + +void nsHttpResponseHead::AssignDefaultStatusText() { + LOG(("response status line needs default reason phrase\n")); + + // if a http response doesn't contain a reason phrase, put one in based + // on the status code. The reason phrase is totally meaningless so its + // ok to have a default catch all here - but this makes debuggers and addons + // a little saner to use if we don't map things to "404 OK" or other nonsense. + // In particular, HTTP/2 does not use reason phrases at all so they need to + // always be injected. + + switch (mStatus) { + // start with the most common + case 200: + mStatusText.AssignLiteral("OK"); + break; + case 404: + mStatusText.AssignLiteral("Not Found"); + break; + case 301: + mStatusText.AssignLiteral("Moved Permanently"); + break; + case 304: + mStatusText.AssignLiteral("Not Modified"); + break; + case 307: + mStatusText.AssignLiteral("Temporary Redirect"); + break; + case 500: + mStatusText.AssignLiteral("Internal Server Error"); + break; + + // also well known + case 100: + mStatusText.AssignLiteral("Continue"); + break; + case 101: + mStatusText.AssignLiteral("Switching Protocols"); + break; + case 201: + mStatusText.AssignLiteral("Created"); + break; + case 202: + mStatusText.AssignLiteral("Accepted"); + break; + case 203: + mStatusText.AssignLiteral("Non Authoritative"); + break; + case 204: + mStatusText.AssignLiteral("No Content"); + break; + case 205: + mStatusText.AssignLiteral("Reset Content"); + break; + case 206: + mStatusText.AssignLiteral("Partial Content"); + break; + case 207: + mStatusText.AssignLiteral("Multi-Status"); + break; + case 208: + mStatusText.AssignLiteral("Already Reported"); + break; + case 300: + mStatusText.AssignLiteral("Multiple Choices"); + break; + case 302: + mStatusText.AssignLiteral("Found"); + break; + case 303: + mStatusText.AssignLiteral("See Other"); + break; + case 305: + mStatusText.AssignLiteral("Use Proxy"); + break; + case 308: + mStatusText.AssignLiteral("Permanent Redirect"); + break; + case 400: + mStatusText.AssignLiteral("Bad Request"); + break; + case 401: + mStatusText.AssignLiteral("Unauthorized"); + break; + case 402: + mStatusText.AssignLiteral("Payment Required"); + break; + case 403: + mStatusText.AssignLiteral("Forbidden"); + break; + case 405: + mStatusText.AssignLiteral("Method Not Allowed"); + break; + case 406: + mStatusText.AssignLiteral("Not Acceptable"); + break; + case 407: + mStatusText.AssignLiteral("Proxy Authentication Required"); + break; + case 408: + mStatusText.AssignLiteral("Request Timeout"); + break; + case 409: + mStatusText.AssignLiteral("Conflict"); + break; + case 410: + mStatusText.AssignLiteral("Gone"); + break; + case 411: + mStatusText.AssignLiteral("Length Required"); + break; + case 412: + mStatusText.AssignLiteral("Precondition Failed"); + break; + case 413: + mStatusText.AssignLiteral("Request Entity Too Large"); + break; + case 414: + mStatusText.AssignLiteral("Request URI Too Long"); + break; + case 415: + mStatusText.AssignLiteral("Unsupported Media Type"); + break; + case 416: + mStatusText.AssignLiteral("Requested Range Not Satisfiable"); + break; + case 417: + mStatusText.AssignLiteral("Expectation Failed"); + break; + case 418: + mStatusText.AssignLiteral("I'm a teapot"); + break; + case 421: + mStatusText.AssignLiteral("Misdirected Request"); + break; + case 422: + mStatusText.AssignLiteral("Unprocessable Entity"); + break; + case 423: + mStatusText.AssignLiteral("Locked"); + break; + case 424: + mStatusText.AssignLiteral("Failed Dependency"); + break; + case 425: + mStatusText.AssignLiteral("Too Early"); + break; + case 426: + mStatusText.AssignLiteral("Upgrade Required"); + break; + case 428: + mStatusText.AssignLiteral("Precondition Required"); + break; + case 429: + mStatusText.AssignLiteral("Too Many Requests"); + break; + case 431: + mStatusText.AssignLiteral("Request Header Fields Too Large"); + break; + case 451: + mStatusText.AssignLiteral("Unavailable For Legal Reasons"); + break; + case 501: + mStatusText.AssignLiteral("Not Implemented"); + break; + case 502: + mStatusText.AssignLiteral("Bad Gateway"); + break; + case 503: + mStatusText.AssignLiteral("Service Unavailable"); + break; + case 504: + mStatusText.AssignLiteral("Gateway Timeout"); + break; + case 505: + mStatusText.AssignLiteral("HTTP Version Unsupported"); + break; + case 506: + mStatusText.AssignLiteral("Variant Also Negotiates"); + break; + case 507: + mStatusText.AssignLiteral("Insufficient Storage "); + break; + case 508: + mStatusText.AssignLiteral("Loop Detected"); + break; + case 510: + mStatusText.AssignLiteral("Not Extended"); + break; + case 511: + mStatusText.AssignLiteral("Network Authentication Required"); + break; + default: + mStatusText.AssignLiteral("No Reason Phrase"); + break; + } +} + +void nsHttpResponseHead::ParseStatusLine(const nsACString& line) { + RecursiveMutexAutoLock monitor(mRecursiveMutex); + ParseStatusLine_locked(line); +} + +void nsHttpResponseHead::ParseStatusLine_locked(const nsACString& line) { + // + // Parse Status-Line:: HTTP-Version SP Status-Code SP Reason-Phrase CRLF + // + + const char* start = line.BeginReading(); + const char* end = line.EndReading(); + const char* p = start; + + // HTTP-Version + ParseVersion(start); + + int32_t index = line.FindChar(' '); + + if ((mVersion == HttpVersion::v0_9) || (index == -1)) { + mStatus = 200; + AssignDefaultStatusText(); + } else { + // Status-Code + p += index + 1; + mStatus = (uint16_t)atoi(p); + if (mStatus == 0) { + LOG(("mal-formed response status; assuming status = 200\n")); + mStatus = 200; + } + + // Reason-Phrase is whatever is remaining of the line + index = line.FindChar(' ', p - start); + if (index == -1) { + AssignDefaultStatusText(); + } else { + p = start + index + 1; + mStatusText = nsDependentCSubstring(p, end - p); + } + } + + LOG1(("Have status line [version=%u status=%u statusText=%s]\n", + unsigned(mVersion), unsigned(mStatus), mStatusText.get())); +} + +nsresult nsHttpResponseHead::ParseHeaderLine(const nsACString& line) { + RecursiveMutexAutoLock monitor(mRecursiveMutex); + return ParseHeaderLine_locked(line, true); +} + +nsresult nsHttpResponseHead::ParseHeaderLine_locked( + const nsACString& line, bool originalFromNetHeaders) { + nsHttpAtom hdr; + nsAutoCString headerNameOriginal; + nsAutoCString val; + + if (NS_FAILED(nsHttpHeaderArray::ParseHeaderLine( + line, &hdr, &headerNameOriginal, &val))) { + return NS_OK; + } + nsresult rv; + if (originalFromNetHeaders) { + rv = mHeaders.SetHeaderFromNet(hdr, headerNameOriginal, val, true); + } else { + rv = mHeaders.SetResponseHeaderFromCache( + hdr, headerNameOriginal, val, nsHttpHeaderArray::eVarietyResponse); + } + if (NS_FAILED(rv)) { + return rv; + } + + // leading and trailing LWS has been removed from |val| + + // handle some special case headers... + if (hdr == nsHttp::Content_Length) { + int64_t len; + const char* ignored; + // permit only a single value here. + if (nsHttp::ParseInt64(val.get(), &ignored, &len)) { + mContentLength = len; + } else { + // If this is a negative content length then just ignore it + LOG(("invalid content-length! %s\n", val.get())); + } + } else if (hdr == nsHttp::Content_Type) { + LOG(("ParseContentType [type=%s]\n", val.get())); + bool dummy; + net_ParseContentType(val, mContentType, mContentCharset, &dummy); + } else if (hdr == nsHttp::Cache_Control) + ParseCacheControl(val.get()); + else if (hdr == nsHttp::Pragma) + ParsePragma(val.get()); + return NS_OK; +} + +// From section 13.2.3 of RFC2616, we compute the current age of a cached +// response as follows: +// +// currentAge = max(max(0, responseTime - dateValue), ageValue) +// + now - requestTime +// +// where responseTime == now +// +// This is typically a very small number. +// +nsresult nsHttpResponseHead::ComputeCurrentAge(uint32_t now, + uint32_t requestTime, + uint32_t* result) { + RecursiveMutexAutoLock monitor(mRecursiveMutex); + uint32_t dateValue; + uint32_t ageValue; + + *result = 0; + + if (requestTime > now) { + // for calculation purposes lets not allow the request to happen in the + // future + requestTime = now; + } + + if (NS_FAILED(GetDateValue_locked(&dateValue))) { + LOG( + ("nsHttpResponseHead::ComputeCurrentAge [this=%p] " + "Date response header not set!\n", + this)); + // Assume we have a fast connection and that our clock + // is in sync with the server. + dateValue = now; + } + + // Compute apparent age + if (now > dateValue) *result = now - dateValue; + + // Compute corrected received age + if (NS_SUCCEEDED(GetAgeValue_locked(&ageValue))) + *result = std::max(*result, ageValue); + + // Compute current age + *result += (now - requestTime); + return NS_OK; +} + +// From section 13.2.4 of RFC2616, we compute the freshness lifetime of a cached +// response as follows: +// +// freshnessLifetime = max_age_value +// +// freshnessLifetime = expires_value - date_value +// +// freshnessLifetime = min(one-week, +// (date_value - last_modified_value) * 0.10) +// +// freshnessLifetime = 0 +// +nsresult nsHttpResponseHead::ComputeFreshnessLifetime(uint32_t* result) { + RecursiveMutexAutoLock monitor(mRecursiveMutex); + *result = 0; + + // Try HTTP/1.1 style max-age directive... + if (NS_SUCCEEDED(GetMaxAgeValue_locked(result))) return NS_OK; + + *result = 0; + + uint32_t date = 0, date2 = 0; + if (NS_FAILED(GetDateValue_locked(&date))) + date = NowInSeconds(); // synthesize a date header if none exists + + // Try HTTP/1.0 style expires header... + if (NS_SUCCEEDED(GetExpiresValue_locked(&date2))) { + if (date2 > date) *result = date2 - date; + // the Expires header can specify a date in the past. + return NS_OK; + } + + // These responses can be cached indefinitely. + if ((mStatus == 300) || (mStatus == 410) || + nsHttp::IsPermanentRedirect(mStatus)) { + LOG( + ("nsHttpResponseHead::ComputeFreshnessLifetime [this = %p] " + "Assign an infinite heuristic lifetime\n", + this)); + *result = uint32_t(-1); + return NS_OK; + } + + if (mStatus >= 400) { + LOG( + ("nsHttpResponseHead::ComputeFreshnessLifetime [this = %p] " + "Do not calculate heuristic max-age for most responses >= 400\n", + this)); + return NS_OK; + } + + // From RFC 7234 Section 4.2.2, heuristics can only be used on responses + // without explicit freshness whose status codes are defined as cacheable + // by default, and those responses without explicit freshness that have been + // marked as explicitly cacheable. + // Note that |MustValidate| handled most of non-cacheable status codes. + if ((mStatus == 302 || mStatus == 304 || mStatus == 307) && + !mCacheControlPublic && !mCacheControlPrivate) { + LOG(( + "nsHttpResponseHead::ComputeFreshnessLifetime [this = %p] " + "Do not calculate heuristic max-age for non-cacheable status code %u\n", + this, unsigned(mStatus))); + return NS_OK; + } + + // Fallback on heuristic using last modified header... + if (NS_SUCCEEDED(GetLastModifiedValue_locked(&date2))) { + LOG(("using last-modified to determine freshness-lifetime\n")); + LOG(("last-modified = %u, date = %u\n", date2, date)); + if (date2 <= date) { + // this only makes sense if last-modified is actually in the past + *result = (date - date2) / 10; + const uint32_t kOneWeek = 60 * 60 * 24 * 7; + *result = std::min(kOneWeek, *result); + return NS_OK; + } + } + + LOG( + ("nsHttpResponseHead::ComputeFreshnessLifetime [this = %p] " + "Insufficient information to compute a non-zero freshness " + "lifetime!\n", + this)); + + return NS_OK; +} + +bool nsHttpResponseHead::MustValidate() { + RecursiveMutexAutoLock monitor(mRecursiveMutex); + LOG(("nsHttpResponseHead::MustValidate ??\n")); + + // Some response codes are cacheable, but the rest are not. This switch should + // stay in sync with the list in nsHttpChannel::ContinueProcessResponse3 + switch (mStatus) { + // Success codes + case 200: + case 203: + case 204: + case 206: + // Cacheable redirects + case 300: + case 301: + case 302: + case 304: + case 307: + case 308: + // Gone forever + case 410: + break; + // Uncacheable redirects + case 303: + case 305: + // Other known errors + case 401: + case 407: + case 412: + case 416: + case 425: + case 429: + default: // revalidate unknown error pages + LOG(("Must validate since response is an uncacheable error page\n")); + return true; + } + + // The no-cache response header indicates that we must validate this + // cached response before reusing. + if (NoCache_locked()) { + LOG(("Must validate since response contains 'no-cache' header\n")); + return true; + } + + // Likewise, if the response is no-store, then we must validate this + // cached response before reusing. NOTE: it may seem odd that a no-store + // response may be cached, but indeed all responses are cached in order + // to support File->SaveAs, View->PageSource, and other browser features. + if (mCacheControlNoStore) { + LOG(("Must validate since response contains 'no-store' header\n")); + return true; + } + + // Compare the Expires header to the Date header. If the server sent an + // Expires header with a timestamp in the past, then we must validate this + // cached response before reusing. + if (ExpiresInPast_locked()) { + LOG(("Must validate since Expires < Date\n")); + return true; + } + + LOG(("no mandatory validation requirement\n")); + return false; +} + +bool nsHttpResponseHead::MustValidateIfExpired() { + // according to RFC2616, section 14.9.4: + // + // When the must-revalidate directive is present in a response received by a + // cache, that cache MUST NOT use the entry after it becomes stale to respond + // to a subsequent request without first revalidating it with the origin + // server. + // + return HasHeaderValue(nsHttp::Cache_Control, "must-revalidate"); +} + +bool nsHttpResponseHead::StaleWhileRevalidate(uint32_t now, + uint32_t expiration) { + RecursiveMutexAutoLock monitor(mRecursiveMutex); + + if (expiration <= 0 || !mCacheControlStaleWhileRevalidateSet) { + return false; + } + + // 'expiration' is the expiration time (an absolute unit), the swr window + // extends the expiration time. + CheckedInt stallValidUntil = expiration; + stallValidUntil += mCacheControlStaleWhileRevalidate; + if (!stallValidUntil.isValid()) { + // overflow means an indefinite stale window + return true; + } + + if (now > stallValidUntil.value()) { + return false; + } + + return true; +} + +bool nsHttpResponseHead::IsResumable() { + RecursiveMutexAutoLock monitor(mRecursiveMutex); + // even though some HTTP/1.0 servers may support byte range requests, we're + // not going to bother with them, since those servers wouldn't understand + // If-Range. Also, while in theory it may be possible to resume when the + // status code is not 200, it is unlikely to be worth the trouble, especially + // for non-2xx responses. + return mStatus == 200 && mVersion >= HttpVersion::v1_1 && + mHeaders.PeekHeader(nsHttp::Content_Length) && + (mHeaders.PeekHeader(nsHttp::ETag) || + mHeaders.PeekHeader(nsHttp::Last_Modified)) && + mHeaders.HasHeaderValue(nsHttp::Accept_Ranges, "bytes"); +} + +bool nsHttpResponseHead::ExpiresInPast() { + RecursiveMutexAutoLock monitor(mRecursiveMutex); + return ExpiresInPast_locked(); +} + +bool nsHttpResponseHead::ExpiresInPast_locked() const { + uint32_t maxAgeVal, expiresVal, dateVal; + + // Bug #203271. Ensure max-age directive takes precedence over Expires + if (NS_SUCCEEDED(GetMaxAgeValue_locked(&maxAgeVal))) { + return false; + } + + return NS_SUCCEEDED(GetExpiresValue_locked(&expiresVal)) && + NS_SUCCEEDED(GetDateValue_locked(&dateVal)) && expiresVal < dateVal; +} + +void nsHttpResponseHead::UpdateHeaders(nsHttpResponseHead* aOther) { + LOG(("nsHttpResponseHead::UpdateHeaders [this=%p]\n", this)); + + RecursiveMutexAutoLock monitor(mRecursiveMutex); + RecursiveMutexAutoLock monitorOther(aOther->mRecursiveMutex); + + uint32_t i, count = aOther->mHeaders.Count(); + for (i = 0; i < count; ++i) { + nsHttpAtom header; + nsAutoCString headerNameOriginal; + const char* val = + aOther->mHeaders.PeekHeaderAt(i, header, headerNameOriginal); + + if (!val) { + continue; + } + + // Ignore any hop-by-hop headers... + if (header == nsHttp::Connection || header == nsHttp::Proxy_Connection || + header == nsHttp::Keep_Alive || header == nsHttp::Proxy_Authenticate || + header == nsHttp::Proxy_Authorization || // not a response header! + header == nsHttp::TE || header == nsHttp::Trailer || + header == nsHttp::Transfer_Encoding || header == nsHttp::Upgrade || + // Ignore any non-modifiable headers... + header == nsHttp::Content_Location || header == nsHttp::Content_MD5 || + header == nsHttp::ETag || + // Assume Cache-Control: "no-transform" + header == nsHttp::Content_Encoding || header == nsHttp::Content_Range || + header == nsHttp::Content_Type || + // Ignore wacky headers too... + // this one is for MS servers that send "Content-Length: 0" + // on 304 responses + header == nsHttp::Content_Length) { + LOG(("ignoring response header [%s: %s]\n", header.get(), val)); + } else { + LOG(("new response header [%s: %s]\n", header.get(), val)); + + // overwrite the current header value with the new value... + DebugOnly rv = + SetHeader_locked(header, headerNameOriginal, nsDependentCString(val)); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + } +} + +void nsHttpResponseHead::Reset() { + LOG(("nsHttpResponseHead::Reset\n")); + + RecursiveMutexAutoLock monitor(mRecursiveMutex); + + mHeaders.Clear(); + + mVersion = HttpVersion::v1_1; + mStatus = 200; + mContentLength = -1; + mHasCacheControl = false; + mCacheControlPublic = false; + mCacheControlPrivate = false; + mCacheControlNoStore = false; + mCacheControlNoCache = false; + mCacheControlImmutable = false; + mCacheControlStaleWhileRevalidateSet = false; + mCacheControlStaleWhileRevalidate = 0; + mCacheControlMaxAgeSet = false; + mCacheControlMaxAge = 0; + mPragmaNoCache = false; + mStatusText.Truncate(); + mContentType.Truncate(); + mContentCharset.Truncate(); +} + +nsresult nsHttpResponseHead::ParseDateHeader(nsHttpAtom header, + uint32_t* result) const { + const char* val = mHeaders.PeekHeader(header); + if (!val) return NS_ERROR_NOT_AVAILABLE; + + PRTime time; + PRStatus st = PR_ParseTimeString(val, true, &time); + if (st != PR_SUCCESS) return NS_ERROR_NOT_AVAILABLE; + + *result = PRTimeToSeconds(time); + return NS_OK; +} + +nsresult nsHttpResponseHead::GetAgeValue(uint32_t* result) { + RecursiveMutexAutoLock monitor(mRecursiveMutex); + return GetAgeValue_locked(result); +} + +nsresult nsHttpResponseHead::GetAgeValue_locked(uint32_t* result) const { + const char* val = mHeaders.PeekHeader(nsHttp::Age); + if (!val) return NS_ERROR_NOT_AVAILABLE; + + *result = (uint32_t)atoi(val); + return NS_OK; +} + +// Return the value of the (HTTP 1.1) max-age directive, which itself is a +// component of the Cache-Control response header +nsresult nsHttpResponseHead::GetMaxAgeValue(uint32_t* result) { + RecursiveMutexAutoLock monitor(mRecursiveMutex); + return GetMaxAgeValue_locked(result); +} + +nsresult nsHttpResponseHead::GetMaxAgeValue_locked(uint32_t* result) const { + if (!mCacheControlMaxAgeSet) { + return NS_ERROR_NOT_AVAILABLE; + } + + *result = mCacheControlMaxAge; + return NS_OK; +} + +nsresult nsHttpResponseHead::GetDateValue(uint32_t* result) { + RecursiveMutexAutoLock monitor(mRecursiveMutex); + return GetDateValue_locked(result); +} + +nsresult nsHttpResponseHead::GetExpiresValue(uint32_t* result) { + RecursiveMutexAutoLock monitor(mRecursiveMutex); + return GetExpiresValue_locked(result); +} + +nsresult nsHttpResponseHead::GetExpiresValue_locked(uint32_t* result) const { + const char* val = mHeaders.PeekHeader(nsHttp::Expires); + if (!val) return NS_ERROR_NOT_AVAILABLE; + + PRTime time; + PRStatus st = PR_ParseTimeString(val, true, &time); + if (st != PR_SUCCESS) { + // parsing failed... RFC 2616 section 14.21 says we should treat this + // as an expiration time in the past. + *result = 0; + return NS_OK; + } + + if (time < 0) + *result = 0; + else + *result = PRTimeToSeconds(time); + return NS_OK; +} + +nsresult nsHttpResponseHead::GetLastModifiedValue(uint32_t* result) { + RecursiveMutexAutoLock monitor(mRecursiveMutex); + return ParseDateHeader(nsHttp::Last_Modified, result); +} + +bool nsHttpResponseHead::operator==(const nsHttpResponseHead& aOther) const { + nsHttpResponseHead& curr = const_cast(*this); + nsHttpResponseHead& other = const_cast(aOther); + RecursiveMutexAutoLock monitorOther(other.mRecursiveMutex); + RecursiveMutexAutoLock monitor(curr.mRecursiveMutex); + + return mHeaders == aOther.mHeaders && mVersion == aOther.mVersion && + mStatus == aOther.mStatus && mStatusText == aOther.mStatusText && + mContentLength == aOther.mContentLength && + mContentType == aOther.mContentType && + mContentCharset == aOther.mContentCharset && + mHasCacheControl == aOther.mHasCacheControl && + mCacheControlPublic == aOther.mCacheControlPublic && + mCacheControlPrivate == aOther.mCacheControlPrivate && + mCacheControlNoCache == aOther.mCacheControlNoCache && + mCacheControlNoStore == aOther.mCacheControlNoStore && + mCacheControlImmutable == aOther.mCacheControlImmutable && + mCacheControlStaleWhileRevalidateSet == + aOther.mCacheControlStaleWhileRevalidateSet && + mCacheControlStaleWhileRevalidate == + aOther.mCacheControlStaleWhileRevalidate && + mCacheControlMaxAgeSet == aOther.mCacheControlMaxAgeSet && + mCacheControlMaxAge == aOther.mCacheControlMaxAge && + mPragmaNoCache == aOther.mPragmaNoCache; +} + +int64_t nsHttpResponseHead::TotalEntitySize() { + RecursiveMutexAutoLock monitor(mRecursiveMutex); + const char* contentRange = mHeaders.PeekHeader(nsHttp::Content_Range); + if (!contentRange) return mContentLength; + + // Total length is after a slash + const char* slash = strrchr(contentRange, '/'); + if (!slash) return -1; // No idea what the length is + + slash++; + if (*slash == '*') // Server doesn't know the length + return -1; + + int64_t size; + if (!nsHttp::ParseInt64(slash, &size)) size = UINT64_MAX; + return size; +} + +//----------------------------------------------------------------------------- +// nsHttpResponseHead +//----------------------------------------------------------------------------- + +void nsHttpResponseHead::ParseVersion(const char* str) { + // Parse HTTP-Version:: "HTTP" "/" 1*DIGIT "." 1*DIGIT + + LOG(("nsHttpResponseHead::ParseVersion [version=%s]\n", str)); + + Tokenizer t(str, nullptr, ""); + // make sure we have HTTP at the beginning + if (!t.CheckWord("HTTP")) { + if (PL_strncasecmp(str, "ICY ", 4) == 0) { + // ShoutCast ICY is HTTP/1.0-like. Assume it is HTTP/1.0. + LOG(("Treating ICY as HTTP 1.0\n")); + mVersion = HttpVersion::v1_0; + return; + } + LOG(("looks like a HTTP/0.9 response\n")); + mVersion = HttpVersion::v0_9; + return; + } + + if (!t.CheckChar('/')) { + LOG(("server did not send a version number; assuming HTTP/1.0\n")); + // NCSA/1.5.2 has a bug in which it fails to send a version number + // if the request version is HTTP/1.1, so we fall back on HTTP/1.0 + mVersion = HttpVersion::v1_0; + return; + } + + uint32_t major; + if (!t.ReadInteger(&major)) { + LOG(("server did not send a correct version number; assuming HTTP/1.0")); + mVersion = HttpVersion::v1_0; + return; + } + + if (major == 3) { + mVersion = HttpVersion::v3_0; + return; + } + + if (major == 2) { + mVersion = HttpVersion::v2_0; + return; + } + + if (major != 1) { + LOG(("server did not send a correct version number; assuming HTTP/1.0")); + mVersion = HttpVersion::v1_0; + return; + } + + if (!t.CheckChar('.')) { + LOG(("mal-formed server version; assuming HTTP/1.0\n")); + mVersion = HttpVersion::v1_0; + return; + } + + uint32_t minor; + if (!t.ReadInteger(&minor)) { + LOG(("server did not send a correct version number; assuming HTTP/1.0")); + mVersion = HttpVersion::v1_0; + return; + } + + if (minor >= 1) { + // at least HTTP/1.1 + mVersion = HttpVersion::v1_1; + } else { + // treat anything else as version 1.0 + mVersion = HttpVersion::v1_0; + } +} + +void nsHttpResponseHead::ParseCacheControl(const char* val) { + if (!(val && *val)) { + // clear flags + mHasCacheControl = false; + mCacheControlPublic = false; + mCacheControlPrivate = false; + mCacheControlNoCache = false; + mCacheControlNoStore = false; + mCacheControlImmutable = false; + mCacheControlStaleWhileRevalidateSet = false; + mCacheControlStaleWhileRevalidate = 0; + mCacheControlMaxAgeSet = false; + mCacheControlMaxAge = 0; + return; + } + + nsDependentCString cacheControlRequestHeader(val); + CacheControlParser cacheControlRequest(cacheControlRequestHeader); + + mHasCacheControl = true; + mCacheControlPublic = cacheControlRequest.Public(); + mCacheControlPrivate = cacheControlRequest.Private(); + mCacheControlNoCache = cacheControlRequest.NoCache(); + mCacheControlNoStore = cacheControlRequest.NoStore(); + mCacheControlImmutable = cacheControlRequest.Immutable(); + mCacheControlStaleWhileRevalidateSet = + cacheControlRequest.StaleWhileRevalidate( + &mCacheControlStaleWhileRevalidate); + mCacheControlMaxAgeSet = cacheControlRequest.MaxAge(&mCacheControlMaxAge); +} + +void nsHttpResponseHead::ParsePragma(const char* val) { + LOG(("nsHttpResponseHead::ParsePragma [val=%s]\n", val)); + + if (!(val && *val)) { + // clear no-cache flag + mPragmaNoCache = false; + return; + } + + // Although 'Pragma: no-cache' is not a standard HTTP response header (it's a + // request header), caching is inhibited when this header is present so as to + // match existing Navigator behavior. + mPragmaNoCache = nsHttp::FindToken(val, "no-cache", HTTP_HEADER_VALUE_SEPS); +} + +nsresult nsHttpResponseHead::VisitHeaders( + nsIHttpHeaderVisitor* visitor, nsHttpHeaderArray::VisitorFilter filter) { + RecursiveMutexAutoLock monitor(mRecursiveMutex); + mInVisitHeaders = true; + nsresult rv = mHeaders.VisitHeaders(visitor, filter); + mInVisitHeaders = false; + return rv; +} + +nsresult nsHttpResponseHead::GetOriginalHeader(nsHttpAtom aHeader, + nsIHttpHeaderVisitor* aVisitor) { + RecursiveMutexAutoLock monitor(mRecursiveMutex); + mInVisitHeaders = true; + nsresult rv = mHeaders.GetOriginalHeader(aHeader, aVisitor); + mInVisitHeaders = false; + return rv; +} + +bool nsHttpResponseHead::HasContentType() const { + RecursiveMutexAutoLock monitor(mRecursiveMutex); + return !mContentType.IsEmpty(); +} + +bool nsHttpResponseHead::HasContentCharset() { + RecursiveMutexAutoLock monitor(mRecursiveMutex); + return !mContentCharset.IsEmpty(); +} + +bool nsHttpResponseHead::GetContentTypeOptionsHeader(nsACString& aOutput) { + aOutput.Truncate(); + + nsAutoCString contentTypeOptionsHeader; + Unused << GetHeader(nsHttp::X_Content_Type_Options, contentTypeOptionsHeader); + if (contentTypeOptionsHeader.IsEmpty()) { + // if there is no XCTO header, then there is nothing to do. + return false; + } + + // XCTO header might contain multiple values which are comma separated, so: + // a) let's skip all subsequent values + // e.g. " NoSniFF , foo " will be " NoSniFF " + int32_t idx = contentTypeOptionsHeader.Find(","); + if (idx > 0) { + contentTypeOptionsHeader = Substring(contentTypeOptionsHeader, 0, idx); + } + // b) let's trim all surrounding whitespace + // e.g. " NoSniFF " -> "NoSniFF" + nsHttp::TrimHTTPWhitespace(contentTypeOptionsHeader, + contentTypeOptionsHeader); + + aOutput.Assign(contentTypeOptionsHeader); + return true; +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/nsHttpResponseHead.h b/netwerk/protocol/http/nsHttpResponseHead.h new file mode 100644 index 0000000000..4126acd133 --- /dev/null +++ b/netwerk/protocol/http/nsHttpResponseHead.h @@ -0,0 +1,235 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef nsHttpResponseHead_h__ +#define nsHttpResponseHead_h__ + +#include "nsHttpHeaderArray.h" +#include "nsHttp.h" +#include "nsString.h" +#include "mozilla/RecursiveMutex.h" + +#ifdef Status +/* Xlib headers insist on this for some reason... Nuke it because + it'll override our member name */ +typedef Status __StatusTmp; +# undef Status +typedef __StatusTmp Status; +#endif + +class nsIHttpHeaderVisitor; + +// This needs to be forward declared here so we can include only this header +// without also including PHttpChannelParams.h +namespace IPC { +template +struct ParamTraits; +} // namespace IPC + +namespace mozilla { +namespace net { + +//----------------------------------------------------------------------------- +// nsHttpResponseHead represents the status line and headers from an HTTP +// response. +//----------------------------------------------------------------------------- + +class nsHttpResponseHead { + public: + nsHttpResponseHead() + : mVersion(HttpVersion::v1_1), + mStatus(200), + mContentLength(-1), + mHasCacheControl(false), + mCacheControlPublic(false), + mCacheControlPrivate(false), + mCacheControlNoStore(false), + mCacheControlNoCache(false), + mCacheControlImmutable(false), + mCacheControlStaleWhileRevalidateSet(false), + mCacheControlStaleWhileRevalidate(0), + mCacheControlMaxAgeSet(false), + mCacheControlMaxAge(0), + mPragmaNoCache(false), + mRecursiveMutex("nsHttpResponseHead.mRecursiveMutex"), + mInVisitHeaders(false) {} + + nsHttpResponseHead(const nsHttpResponseHead& aOther); + nsHttpResponseHead& operator=(const nsHttpResponseHead& aOther); + + void Enter() { mRecursiveMutex.Lock(); } + void Exit() { mRecursiveMutex.Unlock(); } + + HttpVersion Version(); + uint16_t Status() const; + void StatusText(nsACString& aStatusText); + int64_t ContentLength(); + void ContentType(nsACString& aContentType) const; + void ContentCharset(nsACString& aContentCharset); + bool Public(); + bool Private(); + bool NoStore(); + bool NoCache(); + bool Immutable(); + /** + * Full length of the entity. For byte-range requests, this may be larger + * than ContentLength(), which will only represent the requested part of the + * entity. + */ + int64_t TotalEntitySize(); + + [[nodiscard]] nsresult SetHeader(const nsACString& h, const nsACString& v, + bool m = false); + [[nodiscard]] nsresult SetHeader(nsHttpAtom h, const nsACString& v, + bool m = false); + [[nodiscard]] nsresult GetHeader(nsHttpAtom h, nsACString& v); + void ClearHeader(nsHttpAtom h); + void ClearHeaders(); + bool HasHeaderValue(nsHttpAtom h, const char* v); + bool HasHeader(nsHttpAtom h) const; + + void SetContentType(const nsACString& s); + void SetContentCharset(const nsACString& s); + void SetContentLength(int64_t); + + // write out the response status line and headers as a single text block, + // optionally pruning out transient headers (ie. headers that only make + // sense the first time the response is handled). + // Both functions append to the string supplied string. + void Flatten(nsACString&, bool pruneTransients); + void FlattenNetworkOriginalHeaders(nsACString& buf); + + // The next 2 functions parse flattened response head and original net + // headers. They are used when we are reading an entry from the cache. + // + // To keep proper order of the original headers we MUST call + // ParseCachedOriginalHeaders FIRST and then ParseCachedHead. + // + // block must be null terminated. + [[nodiscard]] nsresult ParseCachedHead(const char* block); + [[nodiscard]] nsresult ParseCachedOriginalHeaders(char* block); + + // parse the status line. + void ParseStatusLine(const nsACString& line); + + // parse a header line. + [[nodiscard]] nsresult ParseHeaderLine(const nsACString& line); + + // cache validation support methods + [[nodiscard]] nsresult ComputeFreshnessLifetime(uint32_t*); + [[nodiscard]] nsresult ComputeCurrentAge(uint32_t now, uint32_t requestTime, + uint32_t* result); + bool MustValidate(); + bool MustValidateIfExpired(); + + // return true if the response contains a valid Cache-control: + // stale-while-revalidate and |now| is less than or equal |expiration + + // stale-while-revalidate|. Otherwise false. + bool StaleWhileRevalidate(uint32_t now, uint32_t expiration); + + // returns true if the server appears to support byte range requests. + bool IsResumable(); + + // returns true if the Expires header has a value in the past relative to the + // value of the Date header. + bool ExpiresInPast(); + + // update headers... + void UpdateHeaders(nsHttpResponseHead* headers); + + // reset the response head to it's initial state + void Reset(); + + [[nodiscard]] nsresult GetAgeValue(uint32_t* result); + [[nodiscard]] nsresult GetMaxAgeValue(uint32_t* result); + [[nodiscard]] nsresult GetStaleWhileRevalidateValue(uint32_t* result); + [[nodiscard]] nsresult GetDateValue(uint32_t* result); + [[nodiscard]] nsresult GetExpiresValue(uint32_t* result); + [[nodiscard]] nsresult GetLastModifiedValue(uint32_t* result); + + bool operator==(const nsHttpResponseHead& aOther) const; + + // Using this function it is possible to itereate through all headers + // automatically under one lock. + [[nodiscard]] nsresult VisitHeaders(nsIHttpHeaderVisitor* visitor, + nsHttpHeaderArray::VisitorFilter filter); + [[nodiscard]] nsresult GetOriginalHeader(nsHttpAtom aHeader, + nsIHttpHeaderVisitor* aVisitor); + + bool HasContentType() const; + bool HasContentCharset(); + bool GetContentTypeOptionsHeader(nsACString& aOutput); + + private: + [[nodiscard]] nsresult SetHeader_locked(nsHttpAtom atom, const nsACString& h, + const nsACString& v, bool m = false); + void AssignDefaultStatusText(); + void ParseVersion(const char*); + void ParseCacheControl(const char*); + void ParsePragma(const char*); + + void ParseStatusLine_locked(const nsACString& line); + [[nodiscard]] nsresult ParseHeaderLine_locked(const nsACString& line, + bool originalFromNetHeaders); + + // these return failure if the header does not exist. + [[nodiscard]] nsresult ParseDateHeader(nsHttpAtom header, + uint32_t* result) const; + + bool ExpiresInPast_locked() const; + [[nodiscard]] nsresult GetAgeValue_locked(uint32_t* result) const; + [[nodiscard]] nsresult GetExpiresValue_locked(uint32_t* result) const; + [[nodiscard]] nsresult GetMaxAgeValue_locked(uint32_t* result) const; + [[nodiscard]] nsresult GetStaleWhileRevalidateValue_locked( + uint32_t* result) const; + + [[nodiscard]] nsresult GetDateValue_locked(uint32_t* result) const { + return ParseDateHeader(nsHttp::Date, result); + } + + [[nodiscard]] nsresult GetLastModifiedValue_locked(uint32_t* result) const { + return ParseDateHeader(nsHttp::Last_Modified, result); + } + + bool NoCache_locked() const { + // We ignore Pragma: no-cache if Cache-Control is set. + MOZ_ASSERT_IF(mCacheControlNoCache, mHasCacheControl); + return mHasCacheControl ? mCacheControlNoCache : mPragmaNoCache; + } + + private: + // All members must be copy-constructable and assignable + nsHttpHeaderArray mHeaders; + HttpVersion mVersion; + uint16_t mStatus; + nsCString mStatusText; + int64_t mContentLength; + nsCString mContentType; + nsCString mContentCharset; + bool mHasCacheControl; + bool mCacheControlPublic; + bool mCacheControlPrivate; + bool mCacheControlNoStore; + bool mCacheControlNoCache; + bool mCacheControlImmutable; + bool mCacheControlStaleWhileRevalidateSet; + uint32_t mCacheControlStaleWhileRevalidate; + bool mCacheControlMaxAgeSet; + uint32_t mCacheControlMaxAge; + bool mPragmaNoCache; + + // We are using RecursiveMutex instead of a Mutex because VisitHeader + // function calls nsIHttpHeaderVisitor::VisitHeader while under lock. + mutable RecursiveMutex mRecursiveMutex; + // During VisitHeader we sould not allow cal to SetHeader. + bool mInVisitHeaders; + + friend struct IPC::ParamTraits; +}; + +} // namespace net +} // namespace mozilla + +#endif // nsHttpResponseHead_h__ diff --git a/netwerk/protocol/http/nsHttpTransaction.cpp b/netwerk/protocol/http/nsHttpTransaction.cpp new file mode 100644 index 0000000000..8499af172d --- /dev/null +++ b/netwerk/protocol/http/nsHttpTransaction.cpp @@ -0,0 +1,3347 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=4 sw=2 sts=2 et cin: */ +/* 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/. */ + +// HttpLog.h should generally be included first +#include "nsHttpTransaction.h" + +#include +#include + +#include "HttpLog.h" +#include "HTTPSRecordResolver.h" +#include "NSSErrorsService.h" +#include "TCPFastOpenLayer.h" +#include "TunnelUtils.h" +#include "base/basictypes.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/Tokenizer.h" +#include "mozilla/StaticPrefs_network.h" +#include "nsCRT.h" +#include "nsComponentManagerUtils.h" // do_CreateInstance +#include "nsHttpBasicAuth.h" +#include "nsHttpChunkedDecoder.h" +#include "nsHttpDigestAuth.h" +#include "nsHttpHandler.h" +#include "nsHttpNTLMAuth.h" +#include "nsHttpNegotiateAuth.h" +#include "nsHttpRequestHead.h" +#include "nsHttpResponseHead.h" +#include "nsICancelable.h" +#include "nsIClassOfService.h" +#include "nsIDNSByTypeRecord.h" +#include "nsIDNSRecord.h" +#include "nsIDNSService.h" +#include "nsIEventTarget.h" +#include "nsIHttpActivityObserver.h" +#include "nsIHttpAuthenticator.h" +#include "nsIInputStream.h" +#include "nsIInputStreamPriority.h" +#include "nsIMultiplexInputStream.h" +#include "nsIOService.h" +#include "nsIPipe.h" +#include "nsIRequestContext.h" +#include "nsISeekableStream.h" +#include "nsISSLSocketControl.h" +#include "nsIThrottledInputChannel.h" +#include "nsITransport.h" +#include "nsMultiplexInputStream.h" +#include "nsNetCID.h" +#include "nsNetUtil.h" +#include "nsQueryObject.h" +#include "nsSocketTransportService2.h" +#include "nsStringStream.h" +#include "nsTransportUtils.h" +#include "sslerr.h" +#include "SpeculativeTransaction.h" + +//----------------------------------------------------------------------------- + +static NS_DEFINE_CID(kMultiplexInputStream, NS_MULTIPLEXINPUTSTREAM_CID); + +// Place a limit on how much non-compliant HTTP can be skipped while +// looking for a response header +#define MAX_INVALID_RESPONSE_BODY_SIZE (1024 * 128) + +using namespace mozilla::net; + +namespace mozilla { +namespace net { + +//----------------------------------------------------------------------------- +// nsHttpTransaction +//----------------------------------------------------------------------------- + +nsHttpTransaction::nsHttpTransaction() + : mLock("transaction lock"), + mChannelId(0), + mRequestSize(0), + mRequestHead(nullptr), + mResponseHead(nullptr), + mReader(nullptr), + mWriter(nullptr), + mContentLength(-1), + mContentRead(0), + mTransferSize(0), + mInvalidResponseBytesRead(0), + mPushedStream(nullptr), + mInitialRwin(0), + mChunkedDecoder(nullptr), + mStatus(NS_OK), + mPriority(0), + mRestartCount(0), + mCaps(0), + mHttpVersion(HttpVersion::UNKNOWN), + mHttpResponseCode(0), + mCurrentHttpResponseHeaderSize(0), + mThrottlingReadAllowance(THROTTLE_NO_LIMIT), + mCapsToClear(0), + mResponseIsComplete(false), + mClosed(false), + mReadingStopped(false), + mConnected(false), + mActivated(false), + mHaveStatusLine(false), + mHaveAllHeaders(false), + mTransactionDone(false), + mDidContentStart(false), + mNoContent(false), + mSentData(false), + mReceivedData(false), + mStatusEventPending(false), + mHasRequestBody(false), + mProxyConnectFailed(false), + mHttpResponseMatched(false), + mPreserveStream(false), + mDispatchedAsBlocking(false), + mResponseTimeoutEnabled(true), + mForceRestart(false), + mReuseOnRestart(false), + mContentDecoding(false), + mContentDecodingCheck(false), + mDeferredSendProgress(false), + mWaitingOnPipeOut(false), + mDoNotRemoveAltSvc(false), + mReportedStart(false), + mReportedResponseHeader(false), + mResponseHeadTaken(false), + mForTakeResponseTrailers(nullptr), + mResponseTrailersTaken(false), + mRestarted(false), + mTopLevelOuterContentWindowId(0), + mSubmittedRatePacing(false), + mPassedRatePacing(false), + mSynchronousRatePaceRequest(false), + mClassOfService(0), + mResolvedByTRR(false), + mEchConfigUsed(false), + m0RTTInProgress(false), + mDoNotTryEarlyData(false), + mEarlyDataDisposition(EARLY_NONE), + mFastOpenStatus(TFO_NOT_TRIED), + mTrafficCategory(HttpTrafficCategory::eInvalid), + mProxyConnectResponseCode(0), + mHTTPSSVCReceivedStage(HTTPSSVC_NOT_USED), + m421Received(false), + mDontRetryWithDirectRoute(false), + mFastFallbackTriggered(false), + mAllRecordsInH3ExcludedListBefore(false), + mHttp3BackupTimerCreated(false) { + this->mSelfAddr.inet = {}; + this->mPeerAddr.inet = {}; + LOG(("Creating nsHttpTransaction @%p\n", this)); + +#ifdef MOZ_VALGRIND + memset(&mSelfAddr, 0, sizeof(NetAddr)); + memset(&mPeerAddr, 0, sizeof(NetAddr)); +#endif + mSelfAddr.raw.family = PR_AF_UNSPEC; + mPeerAddr.raw.family = PR_AF_UNSPEC; + + mThroughCaptivePortal = gHttpHandler->GetThroughCaptivePortal(); +} + +void nsHttpTransaction::ResumeReading() { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + if (!mReadingStopped) { + return; + } + + LOG(("nsHttpTransaction::ResumeReading %p", this)); + + mReadingStopped = false; + + // This with either reengage the limit when still throttled in WriteSegments + // or simply reset to allow unlimeted reading again. + mThrottlingReadAllowance = THROTTLE_NO_LIMIT; + + if (mConnection) { + mConnection->TransactionHasDataToRecv(this); + nsresult rv = mConnection->ResumeRecv(); + if (NS_FAILED(rv)) { + LOG((" resume failed with rv=%" PRIx32, static_cast(rv))); + } + } +} + +bool nsHttpTransaction::EligibleForThrottling() const { + return (mClassOfService & + (nsIClassOfService::Throttleable | nsIClassOfService::DontThrottle | + nsIClassOfService::Leader | nsIClassOfService::Unblocked)) == + nsIClassOfService::Throttleable; +} + +void nsHttpTransaction::SetClassOfService(uint32_t cos) { + if (mClosed) { + return; + } + + bool wasThrottling = EligibleForThrottling(); + mClassOfService = cos; + bool isThrottling = EligibleForThrottling(); + + if (mConnection && wasThrottling != isThrottling) { + // Do nothing until we are actually activated. For now + // only remember the throttle flag. Call to UpdateActiveTransaction + // would add this transaction to the list too early. + gHttpHandler->ConnMgr()->UpdateActiveTransaction(this); + + if (mReadingStopped && !isThrottling) { + ResumeReading(); + } + } +} + +class ReleaseH2WSTrans final : public Runnable { + public: + explicit ReleaseH2WSTrans(RefPtr&& trans) + : Runnable("ReleaseH2WSTrans"), mTrans(std::move(trans)) {} + + NS_IMETHOD Run() override { + mTrans = nullptr; + return NS_OK; + } + + void Dispatch() { + nsCOMPtr sts = + do_GetService("@mozilla.org/network/socket-transport-service;1"); + Unused << sts->Dispatch(this, nsIEventTarget::DISPATCH_NORMAL); + } + + private: + RefPtr mTrans; +}; + +nsHttpTransaction::~nsHttpTransaction() { + LOG(("Destroying nsHttpTransaction @%p\n", this)); + + if (mPushedStream) { + mPushedStream->OnPushFailed(); + mPushedStream = nullptr; + } + + if (mTokenBucketCancel) { + mTokenBucketCancel->Cancel(NS_ERROR_ABORT); + mTokenBucketCancel = nullptr; + } + + // Force the callbacks and connection to be released right now + mCallbacks = nullptr; + mConnection = nullptr; + + delete mResponseHead; + delete mChunkedDecoder; + ReleaseBlockingTransaction(); + + if (mH2WSTransaction) { + RefPtr r = + new ReleaseH2WSTrans(std::move(mH2WSTransaction)); + r->Dispatch(); + } +} + +nsresult nsHttpTransaction::Init( + uint32_t caps, nsHttpConnectionInfo* cinfo, nsHttpRequestHead* requestHead, + nsIInputStream* requestBody, uint64_t requestContentLength, + bool requestBodyHasHeaders, nsIEventTarget* target, + nsIInterfaceRequestor* callbacks, nsITransportEventSink* eventsink, + uint64_t topLevelOuterContentWindowId, HttpTrafficCategory trafficCategory, + nsIRequestContext* requestContext, uint32_t classOfService, + uint32_t initialRwin, bool responseTimeoutEnabled, uint64_t channelId, + TransactionObserverFunc&& transactionObserver, + OnPushCallback&& aOnPushCallback, + HttpTransactionShell* transWithPushedStream, uint32_t aPushedStreamId) { + nsresult rv; + + LOG1(("nsHttpTransaction::Init [this=%p caps=%x]\n", this, caps)); + + MOZ_ASSERT(cinfo); + MOZ_ASSERT(requestHead); + MOZ_ASSERT(target); + MOZ_ASSERT(target->IsOnCurrentThread()); + + mChannelId = channelId; + mTransactionObserver = std::move(transactionObserver); + mOnPushCallback = std::move(aOnPushCallback); + mTopLevelOuterContentWindowId = topLevelOuterContentWindowId; + LOG((" window-id = %" PRIx64, mTopLevelOuterContentWindowId)); + + mTrafficCategory = trafficCategory; + + mActivityDistributor = services::GetHttpActivityDistributor(); + if (!mActivityDistributor) { + return NS_ERROR_NOT_AVAILABLE; + } + + bool activityDistributorActive; + rv = mActivityDistributor->GetIsActive(&activityDistributorActive); + if (NS_SUCCEEDED(rv) && activityDistributorActive) { + // there are some observers registered at activity distributor, gather + // nsISupports for the channel that called Init() + LOG( + ("nsHttpTransaction::Init() " + "mActivityDistributor is active " + "this=%p", + this)); + } else { + // there is no observer, so don't use it + activityDistributorActive = false; + mActivityDistributor = nullptr; + } + + LOG1(("nsHttpTransaction %p SetRequestContext %p\n", this, requestContext)); + mRequestContext = requestContext; + + SetClassOfService(classOfService); + mResponseTimeoutEnabled = responseTimeoutEnabled; + mInitialRwin = initialRwin; + + // create transport event sink proxy. it coalesces consecutive + // events of the same status type. + rv = net_NewTransportEventSinkProxy(getter_AddRefs(mTransportSink), eventsink, + target); + + if (NS_FAILED(rv)) return rv; + + mConnInfo = cinfo; + mCallbacks = callbacks; + mConsumerTarget = target; + mCaps = caps; + + if (requestHead->IsHead()) { + mNoContent = true; + } + + // grab a weak reference to the request head + mRequestHead = requestHead; + + mReqHeaderBuf = nsHttp::ConvertRequestHeadToString( + *requestHead, !!requestBody, requestBodyHasHeaders, + cinfo->UsingConnect()); + + if (LOG1_ENABLED()) { + LOG1(("http request [\n")); + LogHeaders(mReqHeaderBuf.get()); + LOG1(("]\n")); + } + + // report the request header + if (mActivityDistributor) { + RefPtr self = this; + nsCString requestBuf(mReqHeaderBuf); + NS_DispatchToMainThread( + NS_NewRunnableFunction("ObserveActivityWithArgs", [self, requestBuf]() { + nsresult rv = self->mActivityDistributor->ObserveActivityWithArgs( + HttpActivityArgs(self->mChannelId), + NS_HTTP_ACTIVITY_TYPE_HTTP_TRANSACTION, + NS_HTTP_ACTIVITY_SUBTYPE_REQUEST_HEADER, PR_Now(), 0, requestBuf); + if (NS_FAILED(rv)) { + LOG3(("ObserveActivity failed (%08x)", static_cast(rv))); + } + })); + } + + // Create a string stream for the request header buf (the stream holds + // a non-owning reference to the request header data, so we MUST keep + // mReqHeaderBuf around). + nsCOMPtr headers; + rv = NS_NewByteInputStream(getter_AddRefs(headers), mReqHeaderBuf, + NS_ASSIGNMENT_DEPEND); + if (NS_FAILED(rv)) return rv; + + mHasRequestBody = !!requestBody; + if (mHasRequestBody && !requestContentLength) { + mHasRequestBody = false; + } + + requestContentLength += mReqHeaderBuf.Length(); + + if (mHasRequestBody) { + // wrap the headers and request body in a multiplexed input stream. + nsCOMPtr multi; + rv = nsMultiplexInputStreamConstructor( + nullptr, NS_GET_IID(nsIMultiplexInputStream), getter_AddRefs(multi)); + if (NS_FAILED(rv)) return rv; + + rv = multi->AppendStream(headers); + if (NS_FAILED(rv)) return rv; + + rv = multi->AppendStream(requestBody); + if (NS_FAILED(rv)) return rv; + + // wrap the multiplexed input stream with a buffered input stream, so + // that we write data in the largest chunks possible. this is actually + // necessary to workaround some common server bugs (see bug 137155). + nsCOMPtr stream(do_QueryInterface(multi)); + rv = NS_NewBufferedInputStream(getter_AddRefs(mRequestStream), + stream.forget(), + nsIOService::gDefaultSegmentSize); + if (NS_FAILED(rv)) return rv; + } else { + mRequestStream = headers; + } + + nsCOMPtr throttled = do_QueryInterface(eventsink); + if (throttled) { + nsCOMPtr queue; + rv = throttled->GetThrottleQueue(getter_AddRefs(queue)); + // In case of failure, just carry on without throttling. + if (NS_SUCCEEDED(rv) && queue) { + nsCOMPtr wrappedStream; + rv = queue->WrapStream(mRequestStream, getter_AddRefs(wrappedStream)); + // Failure to throttle isn't sufficient reason to fail + // initialization + if (NS_SUCCEEDED(rv)) { + MOZ_ASSERT(wrappedStream != nullptr); + LOG( + ("nsHttpTransaction::Init %p wrapping input stream using throttle " + "queue %p\n", + this, queue.get())); + mRequestStream = wrappedStream; + } + } + } + + // make sure request content-length fits within js MAX_SAFE_INTEGER + mRequestSize = InScriptableRange(requestContentLength) + ? static_cast(requestContentLength) + : -1; + + // create pipe for response stream + rv = NS_NewPipe2(getter_AddRefs(mPipeIn), getter_AddRefs(mPipeOut), true, + true, nsIOService::gDefaultSegmentSize, + nsIOService::gDefaultSegmentCount); + if (NS_FAILED(rv)) return rv; + + if (transWithPushedStream && aPushedStreamId) { + RefPtr trans = + transWithPushedStream->AsHttpTransaction(); + MOZ_ASSERT(trans); + mPushedStream = trans->TakePushedStreamById(aPushedStreamId); + } + + if (gHttpHandler->UseHTTPSRRAsAltSvcEnabled()) { + mHTTPSSVCReceivedStage = HTTPSSVC_NOT_PRESENT; + + nsCOMPtr target; + Unused << gHttpHandler->GetSocketThreadTarget(getter_AddRefs(target)); + if (target) { + mResolver = new HTTPSRecordResolver(this); + nsCOMPtr dnsRequest; + rv = mResolver->FetchHTTPSRRInternal(target, getter_AddRefs(dnsRequest)); + if (NS_FAILED(rv) && (mCaps & NS_HTTP_WAIT_HTTPSSVC_RESULT)) { + return rv; + } + + { + MutexAutoLock lock(mLock); + mDNSRequest.swap(dnsRequest); + } + } + } + return NS_OK; +} + +static inline void CreateAndStartTimer(nsCOMPtr& aTimer, + nsITimerCallback* aCallback, + uint32_t aTimeout) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + MOZ_ASSERT(!aTimer); + + if (!aTimeout) { + return; + } + + NS_NewTimerWithCallback(getter_AddRefs(aTimer), aCallback, aTimeout, + nsITimer::TYPE_ONE_SHOT); +} + +void nsHttpTransaction::OnPendingQueueInserted() { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + if (mConnInfo->IsHttp3() && !mResolver) { + // Backup timer should only be created once. + if (!mHttp3BackupTimerCreated) { + CreateAndStartTimer(mHttp3BackupTimer, this, + StaticPrefs::network_http_http3_backup_timer_delay()); + mHttp3BackupTimerCreated = true; + } + } +} + +nsresult nsHttpTransaction::AsyncRead(nsIStreamListener* listener, + nsIRequest** pump) { + RefPtr transactionPump; + nsresult rv = + nsInputStreamPump::Create(getter_AddRefs(transactionPump), mPipeIn); + NS_ENSURE_SUCCESS(rv, rv); + + rv = transactionPump->AsyncRead(listener); + NS_ENSURE_SUCCESS(rv, rv); + + transactionPump.forget(pump); + return NS_OK; +} + +// This method should only be used on the socket thread +nsAHttpConnection* nsHttpTransaction::Connection() { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + return mConnection.get(); +} + +void nsHttpTransaction::SetH2WSConnRefTaken() { + if (!OnSocketThread()) { + nsCOMPtr event = + NewRunnableMethod("nsHttpTransaction::SetH2WSConnRefTaken", this, + &nsHttpTransaction::SetH2WSConnRefTaken); + gSocketTransportService->Dispatch(event, NS_DISPATCH_NORMAL); + return; + } + + if (mH2WSTransaction) { + // Need to let the websocket transaction/connection know we've reached + // this point so it can stop forwarding information through us and + // instead communicate directly with the websocket channel. + mH2WSTransaction->SetConnRefTaken(); + mH2WSTransaction = nullptr; + } +} + +UniquePtr nsHttpTransaction::TakeResponseHead() { + MOZ_ASSERT(!mResponseHeadTaken, "TakeResponseHead called 2x"); + + // Lock TakeResponseHead() against main thread + MutexAutoLock lock(*nsHttp::GetLock()); + + mResponseHeadTaken = true; + + // Even in OnStartRequest() the headers won't be available if we were + // canceled + if (!mHaveAllHeaders) { + NS_WARNING("response headers not available or incomplete"); + return nullptr; + } + + return WrapUnique(std::exchange(mResponseHead, nullptr)); +} + +UniquePtr nsHttpTransaction::TakeResponseTrailers() { + MOZ_ASSERT(!mResponseTrailersTaken, "TakeResponseTrailers called 2x"); + + // Lock TakeResponseTrailers() against main thread + MutexAutoLock lock(*nsHttp::GetLock()); + + mResponseTrailersTaken = true; + return std::move(mForTakeResponseTrailers); +} + +void nsHttpTransaction::SetProxyConnectFailed() { mProxyConnectFailed = true; } + +nsHttpRequestHead* nsHttpTransaction::RequestHead() { return mRequestHead; } + +uint32_t nsHttpTransaction::Http1xTransactionCount() { return 1; } + +nsresult nsHttpTransaction::TakeSubTransactions( + nsTArray>& outTransactions) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +//---------------------------------------------------------------------------- +// nsHttpTransaction::nsAHttpTransaction +//---------------------------------------------------------------------------- + +void nsHttpTransaction::SetConnection(nsAHttpConnection* conn) { + { + MutexAutoLock lock(mLock); + mConnection = conn; + if (mConnection) { + mIsHttp3Used = mConnection->Version() == HttpVersion::v3_0; + } + } +} + +void nsHttpTransaction::OnActivated() { + MOZ_ASSERT(OnSocketThread()); + + if (mActivated) { + return; + } + + if (mTrafficCategory != HttpTrafficCategory::eInvalid) { + HttpTrafficAnalyzer* hta = gHttpHandler->GetHttpTrafficAnalyzer(); + if (hta) { + hta->IncrementHttpTransaction(mTrafficCategory); + } + if (mConnection) { + mConnection->SetTrafficCategory(mTrafficCategory); + } + } + + if (mConnection && mRequestHead && + mConnection->Version() >= HttpVersion::v2_0) { + // So this is fun. On http/2, we want to send TE: Trailers, to be + // spec-compliant. So we add it to the request head here. The fun part + // is that adding a header to the request head at this point has no + // effect on what we send on the wire, as the headers are already + // flattened (in Init()) by the time we get here. So the *real* adding + // of the header happens in the h2 compression code. We still have to + // add the header to the request head here, though, so that devtools can + // show that we sent the header. FUN! + Unused << mRequestHead->SetHeader(nsHttp::TE, "Trailers"_ns); + } + + mActivated = true; + gHttpHandler->ConnMgr()->AddActiveTransaction(this); +} + +void nsHttpTransaction::GetSecurityCallbacks(nsIInterfaceRequestor** cb) { + MutexAutoLock lock(mLock); + nsCOMPtr tmp(mCallbacks); + tmp.forget(cb); +} + +void nsHttpTransaction::SetSecurityCallbacks( + nsIInterfaceRequestor* aCallbacks) { + { + MutexAutoLock lock(mLock); + mCallbacks = aCallbacks; + } + + if (gSocketTransportService) { + RefPtr event = + new UpdateSecurityCallbacks(this, aCallbacks); + gSocketTransportService->Dispatch(event, nsIEventTarget::DISPATCH_NORMAL); + } +} + +void nsHttpTransaction::OnTransportStatus(nsITransport* transport, + nsresult status, int64_t progress) { + LOG1(("nsHttpTransaction::OnSocketStatus [this=%p status=%" PRIx32 + " progress=%" PRId64 "]\n", + this, static_cast(status), progress)); + + if (status == NS_NET_STATUS_CONNECTED_TO || + status == NS_NET_STATUS_WAITING_FOR) { + nsISocketTransport* socketTransport = + mConnection ? mConnection->Transport() : nullptr; + if (socketTransport) { + MutexAutoLock lock(mLock); + socketTransport->GetSelfAddr(&mSelfAddr); + socketTransport->GetPeerAddr(&mPeerAddr); + socketTransport->ResolvedByTRR(&mResolvedByTRR); + socketTransport->GetEchConfigUsed(&mEchConfigUsed); + } + } + + // If the timing is enabled, and we are not using a persistent connection + // then the requestStart timestamp will be null, so we mark the timestamps + // for domainLookupStart/End and connectStart/End + // If we are using a persistent connection they will remain null, + // and the correct value will be returned in Performance. + if (TimingEnabled() && GetRequestStart().IsNull()) { + if (status == NS_NET_STATUS_RESOLVING_HOST) { + SetDomainLookupStart(TimeStamp::Now(), true); + } else if (status == NS_NET_STATUS_RESOLVED_HOST) { + SetDomainLookupEnd(TimeStamp::Now()); + } else if (status == NS_NET_STATUS_CONNECTING_TO) { + SetConnectStart(TimeStamp::Now()); + } else if (status == NS_NET_STATUS_CONNECTED_TO) { + TimeStamp tnow = TimeStamp::Now(); + SetConnectEnd(tnow, true); + { + MutexAutoLock lock(mLock); + mTimings.tcpConnectEnd = tnow; + // After a socket is connected we know for sure whether data + // has been sent on SYN packet and if not we should update TLS + // start timing. + if ((mFastOpenStatus != TFO_DATA_SENT) && + !mTimings.secureConnectionStart.IsNull()) { + mTimings.secureConnectionStart = tnow; + } + } + } else if (status == NS_NET_STATUS_TLS_HANDSHAKE_STARTING) { + { + MutexAutoLock lock(mLock); + mTimings.secureConnectionStart = TimeStamp::Now(); + } + } else if (status == NS_NET_STATUS_TLS_HANDSHAKE_ENDED) { + SetConnectEnd(TimeStamp::Now(), false); + } else if (status == NS_NET_STATUS_SENDING_TO) { + // Set the timestamp to Now(), only if it null + SetRequestStart(TimeStamp::Now(), true); + } + } + + if (!mTransportSink) return; + + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + // Need to do this before the STATUS_RECEIVING_FROM check below, to make + // sure that the activity distributor gets told about all status events. + if (mActivityDistributor) { + // upon STATUS_WAITING_FOR; report request body sent + if ((mHasRequestBody) && (status == NS_NET_STATUS_WAITING_FOR)) { + nsresult rv = mActivityDistributor->ObserveActivityWithArgs( + HttpActivityArgs(mChannelId), NS_HTTP_ACTIVITY_TYPE_HTTP_TRANSACTION, + NS_HTTP_ACTIVITY_SUBTYPE_REQUEST_BODY_SENT, PR_Now(), 0, ""_ns); + if (NS_FAILED(rv)) { + LOG3(("ObserveActivity failed (%08x)", static_cast(rv))); + } + } + + // report the status and progress + nsresult rv = mActivityDistributor->ObserveActivityWithArgs( + HttpActivityArgs(mChannelId), NS_HTTP_ACTIVITY_TYPE_SOCKET_TRANSPORT, + static_cast(status), PR_Now(), progress, ""_ns); + if (NS_FAILED(rv)) { + LOG3(("ObserveActivity failed (%08x)", static_cast(rv))); + } + } + + // nsHttpChannel synthesizes progress events in OnDataAvailable + if (status == NS_NET_STATUS_RECEIVING_FROM) return; + + int64_t progressMax; + + if (status == NS_NET_STATUS_SENDING_TO) { + // suppress progress when only writing request headers + if (!mHasRequestBody) { + LOG1( + ("nsHttpTransaction::OnTransportStatus %p " + "SENDING_TO without request body\n", + this)); + return; + } + + if (mReader) { + // A mRequestStream method is on the stack - wait. + LOG( + ("nsHttpTransaction::OnSocketStatus [this=%p] " + "Skipping Re-Entrant NS_NET_STATUS_SENDING_TO\n", + this)); + // its ok to coalesce several of these into one deferred event + mDeferredSendProgress = true; + return; + } + + nsCOMPtr seekable = do_QueryInterface(mRequestStream); + if (!seekable) { + LOG1( + ("nsHttpTransaction::OnTransportStatus %p " + "SENDING_TO without seekable request stream\n", + this)); + progress = 0; + } else { + int64_t prog = 0; + seekable->Tell(&prog); + progress = prog; + } + + // when uploading, we include the request headers in the progress + // notifications. + progressMax = mRequestSize; + } else { + progress = 0; + progressMax = 0; + } + + mTransportSink->OnTransportStatus(transport, status, progress, progressMax); +} + +bool nsHttpTransaction::IsDone() { return mTransactionDone; } + +nsresult nsHttpTransaction::Status() { return mStatus; } + +uint32_t nsHttpTransaction::Caps() { return mCaps & ~mCapsToClear; } + +void nsHttpTransaction::SetDNSWasRefreshed() { + MOZ_ASSERT(mConsumerTarget->IsOnCurrentThread(), + "SetDNSWasRefreshed on target thread only!"); + mCapsToClear |= NS_HTTP_REFRESH_DNS; +} + +nsresult nsHttpTransaction::ReadRequestSegment(nsIInputStream* stream, + void* closure, const char* buf, + uint32_t offset, uint32_t count, + uint32_t* countRead) { + // For the tracking of sent bytes that we used to do for the networkstats + // API, please see bug 1318883 where it was removed. + + nsHttpTransaction* trans = (nsHttpTransaction*)closure; + nsresult rv = trans->mReader->OnReadSegment(buf, count, countRead); + if (NS_FAILED(rv)) return rv; + + LOG(("nsHttpTransaction::ReadRequestSegment %p read=%u", trans, *countRead)); + + trans->mSentData = true; + return NS_OK; +} + +nsresult nsHttpTransaction::ReadSegments(nsAHttpSegmentReader* reader, + uint32_t count, uint32_t* countRead) { + LOG(("nsHttpTransaction::ReadSegments %p", this)); + + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + if (mTransactionDone) { + *countRead = 0; + return mStatus; + } + + if (!m0RTTInProgress) { + MaybeCancelFallbackTimer(); + } + + if (!mConnected && !m0RTTInProgress) { + mConnected = true; + nsCOMPtr info; + mConnection->GetSecurityInfo(getter_AddRefs(info)); + MutexAutoLock lock(mLock); + mSecurityInfo = std::move(info); + } + + mDeferredSendProgress = false; + mReader = reader; + nsresult rv = + mRequestStream->ReadSegments(ReadRequestSegment, this, count, countRead); + mReader = nullptr; + + if (m0RTTInProgress && (mEarlyDataDisposition == EARLY_NONE) && + NS_SUCCEEDED(rv) && (*countRead > 0)) { + mEarlyDataDisposition = EARLY_SENT; + } + + if (mDeferredSendProgress && mConnection && mConnection->Transport()) { + // to avoid using mRequestStream concurrently, OnTransportStatus() + // did not report upload status off the ReadSegments() stack from + // nsSocketTransport do it now. + OnTransportStatus(mConnection->Transport(), NS_NET_STATUS_SENDING_TO, 0); + } + mDeferredSendProgress = false; + + if (mForceRestart) { + // The forceRestart condition was dealt with on the stack, but it did not + // clear the flag because nsPipe in the readsegment stack clears out + // return codes, so we need to use the flag here as a cue to return + // ERETARGETED + if (NS_SUCCEEDED(rv)) { + rv = NS_BINDING_RETARGETED; + } + mForceRestart = false; + } + + // if read would block then we need to AsyncWait on the request stream. + // have callback occur on socket thread so we stay synchronized. + if (rv == NS_BASE_STREAM_WOULD_BLOCK) { + nsCOMPtr asyncIn = do_QueryInterface(mRequestStream); + if (asyncIn) { + nsCOMPtr target; + Unused << gHttpHandler->GetSocketThreadTarget(getter_AddRefs(target)); + if (target) + asyncIn->AsyncWait(this, 0, 0, target); + else { + NS_ERROR("no socket thread event target"); + rv = NS_ERROR_UNEXPECTED; + } + } + } + + return rv; +} + +nsresult nsHttpTransaction::WritePipeSegment(nsIOutputStream* stream, + void* closure, char* buf, + uint32_t offset, uint32_t count, + uint32_t* countWritten) { + nsHttpTransaction* trans = (nsHttpTransaction*)closure; + + if (trans->mTransactionDone) return NS_BASE_STREAM_CLOSED; // stop iterating + + if (trans->TimingEnabled()) { + // Set the timestamp to Now(), only if it null + trans->SetResponseStart(TimeStamp::Now(), true); + } + + // Bug 1153929 - add checks to fix windows crash + MOZ_ASSERT(trans->mWriter); + if (!trans->mWriter) { + return NS_ERROR_UNEXPECTED; + } + + nsresult rv; + // + // OK, now let the caller fill this segment with data. + // + rv = trans->mWriter->OnWriteSegment(buf, count, countWritten); + if (NS_FAILED(rv)) return rv; // caller didn't want to write anything + + LOG(("nsHttpTransaction::WritePipeSegment %p written=%u", trans, + *countWritten)); + + MOZ_ASSERT(*countWritten > 0, "bad writer"); + trans->mReceivedData = true; + trans->mTransferSize += *countWritten; + + // Let the transaction "play" with the buffer. It is free to modify + // the contents of the buffer and/or modify countWritten. + // - Bytes in HTTP headers don't count towards countWritten, so the input + // side of pipe (aka nsHttpChannel's mTransactionPump) won't hit + // OnInputStreamReady until all headers have been parsed. + // + rv = trans->ProcessData(buf, *countWritten, countWritten); + if (NS_FAILED(rv)) trans->Close(rv); + + return rv; // failure code only stops WriteSegments; it is not propagated. +} + +bool nsHttpTransaction::ShouldThrottle() { + if (mClassOfService & nsIClassOfService::DontThrottle) { + // We deliberately don't touch the throttling window here since + // DontThrottle requests are expected to be long-standing media + // streams and would just unnecessarily block running downloads. + // If we want to ballance bandwidth for media responses against + // running downloads, we need to find something smarter like + // changing the suspend/resume throttling intervals at-runtime. + return false; + } + + if (!gHttpHandler->ConnMgr()->ShouldThrottle(this)) { + // We are not obligated to throttle + return false; + } + + if (mContentRead < 16000) { + // Let the first bytes go, it may also well be all the content we get + LOG(("nsHttpTransaction::ShouldThrottle too few content (%" PRIi64 + ") this=%p", + mContentRead, this)); + return false; + } + + if (!(mClassOfService & nsIClassOfService::Throttleable) && + gHttpHandler->ConnMgr()->IsConnEntryUnderPressure(mConnInfo)) { + LOG(("nsHttpTransaction::ShouldThrottle entry pressure this=%p", this)); + // This is expensive to check (two hashtable lookups) but may help + // freeing connections for active tab transactions. + // Checking this only for transactions that are not explicitly marked + // as throttleable because trackers and (specially) downloads should + // keep throttling even under pressure. + return false; + } + + return true; +} + +void nsHttpTransaction::DontReuseConnection() { + LOG(("nsHttpTransaction::DontReuseConnection %p\n", this)); + if (!OnSocketThread()) { + LOG(("DontReuseConnection %p not on socket thread\n", this)); + nsCOMPtr event = + NewRunnableMethod("nsHttpTransaction::DontReuseConnection", this, + &nsHttpTransaction::DontReuseConnection); + gSocketTransportService->Dispatch(event, NS_DISPATCH_NORMAL); + return; + } + + if (mConnection) { + mConnection->DontReuse(); + } +} + +nsresult nsHttpTransaction::WriteSegments(nsAHttpSegmentWriter* writer, + uint32_t count, + uint32_t* countWritten) { + LOG(("nsHttpTransaction::WriteSegments %p", this)); + + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + if (mTransactionDone) { + return NS_SUCCEEDED(mStatus) ? NS_BASE_STREAM_CLOSED : mStatus; + } + + if (ShouldThrottle()) { + if (mThrottlingReadAllowance == THROTTLE_NO_LIMIT) { // no limit set + // V1: ThrottlingReadLimit() returns 0 + mThrottlingReadAllowance = gHttpHandler->ThrottlingReadLimit(); + } + } else { + mThrottlingReadAllowance = THROTTLE_NO_LIMIT; // don't limit + } + + if (mThrottlingReadAllowance == 0) { // depleted + if (gHttpHandler->ConnMgr()->CurrentTopLevelOuterContentWindowId() != + mTopLevelOuterContentWindowId) { + nsHttp::NotifyActiveTabLoadOptimization(); + } + + // Must remember that we have to call ResumeRecv() on our connection when + // called back by the conn manager to resume reading. + LOG(("nsHttpTransaction::WriteSegments %p response throttled", this)); + mReadingStopped = true; + // This makes the underlaying connection or stream wait for explicit resume. + // For h1 this means we stop reading from the socket. + // For h2 this means we stop updating recv window for the stream. + return NS_BASE_STREAM_WOULD_BLOCK; + } + + mWriter = writer; + + if (!mPipeOut) { + return NS_ERROR_UNEXPECTED; + } + + if (mThrottlingReadAllowance > 0) { + LOG(("nsHttpTransaction::WriteSegments %p limiting read from %u to %d", + this, count, mThrottlingReadAllowance)); + count = std::min(count, static_cast(mThrottlingReadAllowance)); + } + + nsresult rv = + mPipeOut->WriteSegments(WritePipeSegment, this, count, countWritten); + + mWriter = nullptr; + + if (mForceRestart) { + // The forceRestart condition was dealt with on the stack, but it did not + // clear the flag because nsPipe in the writesegment stack clears out + // return codes, so we need to use the flag here as a cue to return + // ERETARGETED + if (NS_SUCCEEDED(rv)) { + rv = NS_BINDING_RETARGETED; + } + mForceRestart = false; + } + + // if pipe would block then we need to AsyncWait on it. have callback + // occur on socket thread so we stay synchronized. + if (rv == NS_BASE_STREAM_WOULD_BLOCK) { + nsCOMPtr target; + Unused << gHttpHandler->GetSocketThreadTarget(getter_AddRefs(target)); + if (target) { + mPipeOut->AsyncWait(this, 0, 0, target); + mWaitingOnPipeOut = true; + } else { + NS_ERROR("no socket thread event target"); + rv = NS_ERROR_UNEXPECTED; + } + } else if (mThrottlingReadAllowance > 0 && NS_SUCCEEDED(rv)) { + MOZ_ASSERT(count >= *countWritten); + mThrottlingReadAllowance -= *countWritten; + } + + return rv; +} + +bool nsHttpTransaction::ProxyConnectFailed() { return mProxyConnectFailed; } + +bool nsHttpTransaction::DataSentToChildProcess() { return false; } + +already_AddRefed nsHttpTransaction::SecurityInfo() { + MutexAutoLock lock(mLock); + return do_AddRef(mSecurityInfo); +} + +bool nsHttpTransaction::HasStickyConnection() const { + return mCaps & NS_HTTP_STICKY_CONNECTION; +} + +bool nsHttpTransaction::ResponseIsComplete() { return mResponseIsComplete; } + +int64_t nsHttpTransaction::GetTransferSize() { return mTransferSize; } + +int64_t nsHttpTransaction::GetRequestSize() { return mRequestSize; } + +bool nsHttpTransaction::IsHttp3Used() { return mIsHttp3Used; } + +bool nsHttpTransaction::Http2Disabled() const { + return mCaps & NS_HTTP_DISALLOW_SPDY; +} + +bool nsHttpTransaction::Http3Disabled() const { + return mCaps & NS_HTTP_DISALLOW_HTTP3; +} + +already_AddRefed nsHttpTransaction::GetConnInfo() const { + RefPtr connInfo = mConnInfo->Clone(); + return connInfo.forget(); +} + +already_AddRefed +nsHttpTransaction::TakePushedStreamById(uint32_t aStreamId) { + MOZ_ASSERT(mConsumerTarget->IsOnCurrentThread()); + MOZ_ASSERT(aStreamId); + + auto entry = mIDToStreamMap.Lookup(aStreamId); + if (entry) { + RefPtr stream = entry.Data(); + entry.Remove(); + return stream.forget(); + } + + return nullptr; +} + +void nsHttpTransaction::OnPush(Http2PushedStreamWrapper* aStream) { + LOG(("nsHttpTransaction::OnPush %p aStream=%p", this, aStream)); + MOZ_ASSERT(aStream); + MOZ_ASSERT(mOnPushCallback); + MOZ_ASSERT(mConsumerTarget); + + RefPtr stream = aStream; + if (!mConsumerTarget->IsOnCurrentThread()) { + RefPtr self = this; + if (NS_FAILED(mConsumerTarget->Dispatch( + NS_NewRunnableFunction("nsHttpTransaction::OnPush", + [self, stream]() { self->OnPush(stream); }), + NS_DISPATCH_NORMAL))) { + stream->OnPushFailed(); + } + return; + } + + auto entry = mIDToStreamMap.LookupForAdd(stream->StreamID()); + MOZ_ASSERT(!entry); + if (!entry) { + entry.OrInsert([&stream]() { return stream; }); + } + + if (NS_FAILED(mOnPushCallback(stream->StreamID(), stream->GetResourceUrl(), + stream->GetRequestString(), this))) { + stream->OnPushFailed(); + mIDToStreamMap.Remove(stream->StreamID()); + } +} + +nsHttpTransaction* nsHttpTransaction::AsHttpTransaction() { return this; } + +HttpTransactionParent* nsHttpTransaction::AsHttpTransactionParent() { + return nullptr; +} + +nsHttpTransaction::HTTPSSVC_CONNECTION_FAILED_REASON +nsHttpTransaction::ErrorCodeToFailedReason(nsresult aErrorCode) { + HTTPSSVC_CONNECTION_FAILED_REASON reason = HTTPSSVC_CONNECTION_OTHERS; + switch (aErrorCode) { + case NS_ERROR_UNKNOWN_HOST: + reason = HTTPSSVC_CONNECTION_UNKNOWN_HOST; + break; + case NS_ERROR_CONNECTION_REFUSED: + reason = HTTPSSVC_CONNECTION_UNREACHABLE; + break; + default: + if (m421Received) { + reason = HTTPSSVC_CONNECTION_421_RECEIVED; + } else if (NS_ERROR_GET_MODULE(aErrorCode) == NS_ERROR_MODULE_SECURITY) { + reason = HTTPSSVC_CONNECTION_SECURITY_ERROR; + } + break; + } + return reason; +} + +bool nsHttpTransaction::PrepareSVCBRecordsForRetry( + const nsACString& aFailedDomainName, bool& aAllRecordsHaveEchConfig) { + MOZ_ASSERT(mRecordsForRetry.IsEmpty()); + if (!mHTTPSSVCRecord) { + return false; + } + + // If we already failed to connect with h3, don't select records that supports + // h3. + bool noHttp3 = mCaps & NS_HTTP_DISALLOW_HTTP3; + + nsTArray> records; + Unused << mHTTPSSVCRecord->GetAllRecordsWithEchConfig( + mCaps & NS_HTTP_DISALLOW_SPDY, noHttp3, &aAllRecordsHaveEchConfig, + &mAllRecordsInH3ExcludedListBefore, records); + + // Note that it's possible that we can't get any usable record here. For + // example, when http3 connection is failed, we won't select records with + // http3 alpn. + + // If not all records have echConfig, we'll directly fallback to the origin + // server. + if (!aAllRecordsHaveEchConfig) { + return false; + } + + // Take the records behind the failed one and put them into mRecordsForRetry. + for (const auto& record : records) { + nsAutoCString name; + record->GetName(name); + if (name == aFailedDomainName) { + // Skip the failed one. + continue; + } + + mRecordsForRetry.InsertElementAt(0, record); + } + + // Set mHTTPSSVCRecord to null to avoid this function being executed twice. + mHTTPSSVCRecord = nullptr; + return !mRecordsForRetry.IsEmpty(); +} + +already_AddRefed +nsHttpTransaction::PrepareFastFallbackConnInfo(bool aEchConfigUsed) { + MOZ_ASSERT(mHTTPSSVCRecord && mOrigConnInfo); + + RefPtr fallbackConnInfo; + nsCOMPtr fastFallbackRecord; + Unused << mHTTPSSVCRecord->GetServiceModeRecord( + mCaps & NS_HTTP_DISALLOW_SPDY, true, getter_AddRefs(fastFallbackRecord)); + + if (!fastFallbackRecord) { + if (aEchConfigUsed) { + LOG( + ("nsHttpTransaction::PrepareFastFallbackConnInfo [this=%p] no record " + "can be used", + this)); + return nullptr; + } + fallbackConnInfo = mOrigConnInfo; + } + + fallbackConnInfo = + mOrigConnInfo->CloneAndAdoptHTTPSSVCRecord(fastFallbackRecord); + return fallbackConnInfo.forget(); +} + +void nsHttpTransaction::PrepareConnInfoForRetry(nsresult aReason) { + LOG(("nsHttpTransaction::PrepareConnInfoForRetry [this=%p reason=%" PRIx32 + "]", + this, static_cast(aReason))); + RefPtr failedConnInfo = mConnInfo->Clone(); + mConnInfo = nullptr; + bool echConfigUsed = gHttpHandler->EchConfigEnabled() && + !failedConnInfo->GetEchConfig().IsEmpty(); + + if (mFastFallbackTriggered) { + mFastFallbackTriggered = false; + mConnInfo = PrepareFastFallbackConnInfo(echConfigUsed); + return; + } + + if (!echConfigUsed) { + LOG((" echConfig is not used, fallback to origin conn info")); + mOrigConnInfo.swap(mConnInfo); + return; + } + + Telemetry::HistogramID id = Telemetry::TRANSACTION_ECH_RETRY_OTHERS_COUNT; + auto updateCount = MakeScopeExit([&] { + uint32_t count = 0; + bool existed = mEchRetryCounterMap.Get(id, &count); + MOZ_ASSERT(existed, "table not initialized"); + if (existed) { + mEchRetryCounterMap.Put(id, ++count); + } + }); + + if (aReason == psm::GetXPCOMFromNSSError(SSL_ERROR_ECH_RETRY_WITHOUT_ECH)) { + LOG((" Got SSL_ERROR_ECH_RETRY_WITHOUT_ECH, use empty echConfig to retry")); + failedConnInfo->SetEchConfig(EmptyCString()); + failedConnInfo.swap(mConnInfo); + id = Telemetry::TRANSACTION_ECH_RETRY_WITHOUT_ECH_COUNT; + return; + } + + if (aReason == psm::GetXPCOMFromNSSError(SSL_ERROR_ECH_RETRY_WITH_ECH)) { + LOG((" Got SSL_ERROR_ECH_RETRY_WITH_ECH, use retry echConfig")); + MOZ_ASSERT(mConnection); + + nsCOMPtr secInfo; + if (mConnection) { + mConnection->GetSecurityInfo(getter_AddRefs(secInfo)); + } + + nsCOMPtr socketControl = do_QueryInterface(secInfo); + MOZ_ASSERT(socketControl); + + nsAutoCString retryEchConfig; + if (socketControl && + NS_SUCCEEDED(socketControl->GetRetryEchConfig(retryEchConfig))) { + MOZ_ASSERT(!retryEchConfig.IsEmpty()); + + failedConnInfo->SetEchConfig(retryEchConfig); + failedConnInfo.swap(mConnInfo); + } + id = Telemetry::TRANSACTION_ECH_RETRY_WITH_ECH_COUNT; + return; + } + + // Note that we retry the connection not only for SSL_ERROR_ECH_FAILED, but + // also for all failure cases. + if (aReason == psm::GetXPCOMFromNSSError(SSL_ERROR_ECH_FAILED) || + NS_FAILED(aReason)) { + LOG((" Got SSL_ERROR_ECH_FAILED, try other records")); + if (aReason == psm::GetXPCOMFromNSSError(SSL_ERROR_ECH_FAILED)) { + id = Telemetry::TRANSACTION_ECH_RETRY_ECH_FAILED_COUNT; + } + if (mRecordsForRetry.IsEmpty()) { + if (mHTTPSSVCRecord) { + bool allRecordsHaveEchConfig = true; + if (!PrepareSVCBRecordsForRetry(failedConnInfo->GetRoutedHost(), + allRecordsHaveEchConfig)) { + LOG( + (" Can't find other records with echConfig, " + "allRecordsHaveEchConfig=%d", + allRecordsHaveEchConfig)); + if (gHttpHandler->FallbackToOriginIfConfigsAreECHAndAllFailed() || + !allRecordsHaveEchConfig) { + mOrigConnInfo.swap(mConnInfo); + } + return; + } + } else { + LOG( + (" No available records to retry, " + "mAllRecordsInH3ExcludedListBefore=%d", + mAllRecordsInH3ExcludedListBefore)); + if (gHttpHandler->FallbackToOriginIfConfigsAreECHAndAllFailed() && + !mAllRecordsInH3ExcludedListBefore) { + mOrigConnInfo.swap(mConnInfo); + } + return; + } + } + + if (LOG5_ENABLED()) { + LOG(("SvcDomainName to retry: [")); + for (const auto& r : mRecordsForRetry) { + nsAutoCString name; + r->GetName(name); + LOG((" name=%s", name.get())); + } + LOG(("]")); + } + + RefPtr recordsForRetry = + mRecordsForRetry.PopLastElement().forget(); + mConnInfo = mOrigConnInfo->CloneAndAdoptHTTPSSVCRecord(recordsForRetry); + } +} + +void nsHttpTransaction::MaybeReportFailedSVCDomain( + nsresult aReason, nsHttpConnectionInfo* aFailedConnInfo) { + if (aReason == psm::GetXPCOMFromNSSError(SSL_ERROR_ECH_RETRY_WITHOUT_ECH) || + aReason != psm::GetXPCOMFromNSSError(SSL_ERROR_ECH_RETRY_WITH_ECH)) { + return; + } + + Telemetry::Accumulate(Telemetry::DNS_HTTPSSVC_CONNECTION_FAILED_REASON, + ErrorCodeToFailedReason(aReason)); + + nsCOMPtr dns = do_GetService(NS_DNSSERVICE_CONTRACTID); + if (dns) { + const nsCString& failedHost = aFailedConnInfo->GetRoutedHost().IsEmpty() + ? aFailedConnInfo->GetOrigin() + : aFailedConnInfo->GetRoutedHost(); + LOG(("add failed domain name [%s] -> [%s] to exclusion list", + aFailedConnInfo->GetOrigin().get(), failedHost.get())); + Unused << dns->ReportFailedSVCDomainName(aFailedConnInfo->GetOrigin(), + failedHost); + } +} + +void nsHttpTransaction::Close(nsresult reason) { + LOG(("nsHttpTransaction::Close [this=%p reason=%" PRIx32 "]\n", this, + static_cast(reason))); + + if (!mClosed) { + gHttpHandler->ConnMgr()->RemoveActiveTransaction(this); + mActivated = false; + } + + if (mDNSRequest) { + mDNSRequest->Cancel(NS_ERROR_ABORT); + mDNSRequest = nullptr; + } + + MaybeCancelFallbackTimer(); + + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + if (reason == NS_BINDING_RETARGETED) { + LOG((" close %p skipped due to ERETARGETED\n", this)); + return; + } + + if (mClosed) { + LOG((" already closed\n")); + return; + } + + // When we capture 407 from H2 proxy via CONNECT, prepare the response headers + // for authentication in http channel. + if (mTunnelProvider && reason == NS_ERROR_PROXY_AUTHENTICATION_FAILED) { + MOZ_ASSERT(mProxyConnectResponseCode == 407, "non-407 proxy auth failed"); + MOZ_ASSERT(!mFlat407Headers.IsEmpty(), "Contain status line at least"); + uint32_t unused = 0; + + // Reset the reason to avoid nsHttpChannel::ProcessFallback + reason = ProcessData(mFlat407Headers.BeginWriting(), + mFlat407Headers.Length(), &unused); + + if (NS_SUCCEEDED(reason)) { + // prevent restarting the transaction + mReceivedData = true; + } + + LOG(("nsHttpTransaction::Close [this=%p] overwrite reason to %" PRIx32 + " for 407 proxy via CONNECT\n", + this, static_cast(reason))); + } + + NotifyTransactionObserver(reason); + + if (mTokenBucketCancel) { + mTokenBucketCancel->Cancel(reason); + mTokenBucketCancel = nullptr; + } + + if (mActivityDistributor) { + // report the reponse is complete if not already reported + if (!mResponseIsComplete) { + nsresult rv = mActivityDistributor->ObserveActivityWithArgs( + HttpActivityArgs(mChannelId), NS_HTTP_ACTIVITY_TYPE_HTTP_TRANSACTION, + NS_HTTP_ACTIVITY_SUBTYPE_RESPONSE_COMPLETE, PR_Now(), + static_cast(mContentRead), ""_ns); + if (NS_FAILED(rv)) { + LOG3(("ObserveActivity failed (%08x)", static_cast(rv))); + } + } + + // report that this transaction is closing + nsresult rv = mActivityDistributor->ObserveActivityWithArgs( + HttpActivityArgs(mChannelId), NS_HTTP_ACTIVITY_TYPE_HTTP_TRANSACTION, + NS_HTTP_ACTIVITY_SUBTYPE_TRANSACTION_CLOSE, PR_Now(), 0, ""_ns); + if (NS_FAILED(rv)) { + LOG3(("ObserveActivity failed (%08x)", static_cast(rv))); + } + } + + // we must no longer reference the connection! find out if the + // connection was being reused before letting it go. + bool connReused = false; + bool isHttp2or3 = false; + if (mConnection) { + connReused = mConnection->IsReused(); + isHttp2or3 = mConnection->Version() >= HttpVersion::v2_0; + } + mConnected = false; + mTunnelProvider = nullptr; + + // + // if the connection was reset or closed before we wrote any part of the + // request or if we wrote the request but didn't receive any part of the + // response and the connection was being reused, then we can (and really + // should) assume that we wrote to a stale connection and we must therefore + // repeat the request over a new connection. + // + // We have decided to retry not only in case of the reused connections, but + // all safe methods(bug 1236277). + // + // NOTE: the conditions under which we will automatically retry the HTTP + // request have to be carefully selected to avoid duplication of the + // request from the point-of-view of the server. such duplication could + // have dire consequences including repeated purchases, etc. + // + // NOTE: because of the way SSL proxy CONNECT is implemented, it is + // possible that the transaction may have received data without having + // sent any data. for this reason, mSendData == FALSE does not imply + // mReceivedData == FALSE. (see bug 203057 for more info.) + // + // Never restart transactions that are marked as sticky to their conenction. + // We use that capability to identify transactions bound to connection based + // authentication. Reissuing them on a different connections will break + // this bondage. Major issue may arise when there is an NTLM message auth + // header on the transaction and we send it to a different NTLM authenticated + // connection. It will break that connection and also confuse the channel's + // auth provider, beliving the cached credentials are wrong and asking for + // the password mistakenly again from the user. + if ((reason == NS_ERROR_NET_RESET || reason == NS_OK || + reason == + psm::GetXPCOMFromNSSError(SSL_ERROR_DOWNGRADE_WITH_EARLY_DATA) || + mOrigConnInfo) && + (!(mCaps & NS_HTTP_STICKY_CONNECTION) || + (mCaps & NS_HTTP_CONNECTION_RESTARTABLE) || + (mEarlyDataDisposition == EARLY_425))) { + if (mForceRestart && NS_SUCCEEDED(Restart())) { + if (mResponseHead) { + mResponseHead->Reset(); + } + mContentRead = 0; + mContentLength = -1; + delete mChunkedDecoder; + mChunkedDecoder = nullptr; + mHaveStatusLine = false; + mHaveAllHeaders = false; + mHttpResponseMatched = false; + mResponseIsComplete = false; + mDidContentStart = false; + mNoContent = false; + mSentData = false; + mReceivedData = false; + mSupportsHTTP3 = false; + LOG(("transaction force restarted\n")); + // Only record the first restart attempt. + if (!mRestartCount) { + Telemetry::Accumulate(Telemetry::HTTP_TRANSACTION_RESTART_REASON, + TRANSACTION_RESTART_FORCED); + } + return; + } + + // reallySentData is meant to separate the instances where data has + // been sent by this transaction but buffered at a higher level while + // a TLS session (perhaps via a tunnel) is setup. + bool reallySentData = + mSentData && (!mConnection || mConnection->BytesWritten()); + + // If this is true, it means we failed to use the HTTPSSVC connection info + // to connect to the server. We need to retry with the original connection + // info. + bool restartToFallbackConnInfo = !reallySentData && mOrigConnInfo; + + if (reason == + psm::GetXPCOMFromNSSError(SSL_ERROR_DOWNGRADE_WITH_EARLY_DATA) || + (!mReceivedData && ((mRequestHead && mRequestHead->IsSafeMethod()) || + !reallySentData || connReused)) || + restartToFallbackConnInfo) { + if (restartToFallbackConnInfo) { + MaybeReportFailedSVCDomain(reason, mConnInfo); + PrepareConnInfoForRetry(reason); + mDontRetryWithDirectRoute = true; + LOG( + ("transaction will be restarted with the fallback connection info " + "key=%s", + mConnInfo ? mConnInfo->HashKey().get() : "None")); + } + + // if restarting fails, then we must proceed to close the pipe, + // which will notify the channel that the transaction failed. + // Note that when echConfig is enabled, it's possible that we don't have a + // usable connection info to retry. + if (mConnInfo && NS_SUCCEEDED(Restart())) { + // Only record the first restart attempt. + if (!mRestartCount) { + if (restartToFallbackConnInfo) { + Telemetry::Accumulate(Telemetry::HTTP_TRANSACTION_RESTART_REASON, + TRANSACTION_RESTART_HTTPSSVC_INVOLVED); + } else if (!reallySentData) { + Telemetry::Accumulate(Telemetry::HTTP_TRANSACTION_RESTART_REASON, + TRANSACTION_RESTART_NO_DATA_SENT); + } else if (reason == psm::GetXPCOMFromNSSError( + SSL_ERROR_DOWNGRADE_WITH_EARLY_DATA)) { + Telemetry::Accumulate( + Telemetry::HTTP_TRANSACTION_RESTART_REASON, + TRANSACTION_RESTART_DOWNGRADE_WITH_EARLY_DATA); + } else { + Telemetry::Accumulate(Telemetry::HTTP_TRANSACTION_RESTART_REASON, + TRANSACTION_RESTART_OTHERS); + } + } + return; + } + // mConnInfo could be set to null in PrepareConnInfoForRetry() when we + // can't find an available https rr to retry. We have to set mConnInfo + // back to mOrigConnInfo to make sure no crash when mConnInfo being + // accessed again. + if (!mConnInfo) { + mConnInfo.swap(mOrigConnInfo); + MOZ_ASSERT(mConnInfo); + } + } + } + + if (!mRestartCount) { + Telemetry::Accumulate(Telemetry::HTTP_TRANSACTION_RESTART_REASON, + TRANSACTION_RESTART_NONE); + } + + if (!mResponseIsComplete && NS_SUCCEEDED(reason) && isHttp2or3) { + // Responses without content-length header field are still complete if + // they are transfered over http2 or http3 and the stream is properly + // closed. + mResponseIsComplete = true; + } + + if ((mChunkedDecoder || (mContentLength >= int64_t(0))) && + (NS_SUCCEEDED(reason) && !mResponseIsComplete)) { + NS_WARNING("Partial transfer, incomplete HTTP response received"); + + if ((mHttpResponseCode / 100 == 2) && (mHttpVersion >= HttpVersion::v1_1)) { + FrameCheckLevel clevel = gHttpHandler->GetEnforceH1Framing(); + if (clevel >= FRAMECHECK_BARELY) { + // If clevel == FRAMECHECK_STRICT mark any incomplete response as + // partial. + // if clevel == FRAMECHECK_BARELY: 1) mark a chunked-encoded response + // that do not ends on exactly a chunk boundary as partial; We are not + // strict about the last 0-size chunk and do not mark as parial + // responses that do not have the last 0-size chunk but do end on a + // chunk boundary. (check mChunkedDecoder->GetChunkRemaining() != 0) + // 2) mark a transfer that is partial and it is not chunk-encoded or + // gzip-encoded or other content-encoding as partial. (check + // !mChunkedDecoder && !mContentDecoding && mContentDecodingCheck)) + // if clevel == FRAMECHECK_STRICT_CHUNKED mark a chunked-encoded + // response that ends on exactly a chunk boundary also as partial. + // Here a response must have the last 0-size chunk. + if ((clevel == FRAMECHECK_STRICT) || + (mChunkedDecoder && (mChunkedDecoder->GetChunkRemaining() || + (clevel == FRAMECHECK_STRICT_CHUNKED))) || + (!mChunkedDecoder && !mContentDecoding && mContentDecodingCheck)) { + reason = NS_ERROR_NET_PARTIAL_TRANSFER; + LOG(("Partial transfer, incomplete HTTP response received: %s", + mChunkedDecoder ? "broken chunk" : "c-l underrun")); + } + } + } + + if (mConnection) { + // whether or not we generate an error for the transaction + // bad framing means we don't want a pconn + mConnection->DontReuse(); + } + } + + bool relConn = true; + if (NS_SUCCEEDED(reason)) { + // the server has not sent the final \r\n terminating the header + // section, and there may still be a header line unparsed. let's make + // sure we parse the remaining header line, and then hopefully, the + // response will be usable (see bug 88792). + if (!mHaveAllHeaders) { + char data = '\n'; + uint32_t unused = 0; + Unused << ParseHead(&data, 1, &unused); + + if (mResponseHead->Version() == HttpVersion::v0_9) { + // Reject 0 byte HTTP/0.9 Responses - bug 423506 + LOG(("nsHttpTransaction::Close %p 0 Byte 0.9 Response", this)); + reason = NS_ERROR_NET_RESET; + } + } + + // honor the sticky connection flag... + if (mCaps & NS_HTTP_STICKY_CONNECTION) { + LOG((" keeping the connection because of STICKY_CONNECTION flag")); + relConn = false; + } + + // if the proxy connection has failed, we want the connection be held + // to allow the upper layers (think nsHttpChannel) to close it when + // the failure is unrecoverable. + // we can't just close it here, because mProxyConnectFailed is to a general + // flag and is also set for e.g. 407 which doesn't mean to kill the + // connection, specifically when connection oriented auth may be involved. + if (mProxyConnectFailed) { + LOG((" keeping the connection because of mProxyConnectFailed")); + relConn = false; + } + + // Use mOrigConnInfo as an indicator that this transaction is completed + // successfully with an HTTPSSVC record. + if (mOrigConnInfo) { + Telemetry::Accumulate(Telemetry::DNS_HTTPSSVC_CONNECTION_FAILED_REASON, + HTTPSSVC_CONNECTION_OK); + } + } + + // mTimings.responseEnd is normally recorded based on the end of a + // HTTP delimiter such as chunked-encodings or content-length. However, + // EOF or an error still require an end time be recorded. + if (TimingEnabled()) { + const TimingStruct timings = Timings(); + if (timings.responseEnd.IsNull() && !timings.responseStart.IsNull()) { + SetResponseEnd(TimeStamp::Now()); + } + } + + if (mTrafficCategory != HttpTrafficCategory::eInvalid) { + HttpTrafficAnalyzer* hta = gHttpHandler->GetHttpTrafficAnalyzer(); + if (hta) { + hta->AccumulateHttpTransferredSize(mTrafficCategory, mTransferSize, + mContentRead); + } + } + + if (mThroughCaptivePortal) { + Telemetry::ScalarAdd( + Telemetry::ScalarID::NETWORKING_HTTP_TRANSACTIONS_CAPTIVE_PORTAL, 1); + } + + if (relConn && mConnection) { + MutexAutoLock lock(mLock); + mConnection = nullptr; + } + + mStatus = reason; + mTransactionDone = true; // forcibly flag the transaction as complete + mClosed = true; + mResolver = nullptr; + ReleaseBlockingTransaction(); + + // release some resources that we no longer need + mRequestStream = nullptr; + mReqHeaderBuf.Truncate(); + mLineBuf.Truncate(); + if (mChunkedDecoder) { + delete mChunkedDecoder; + mChunkedDecoder = nullptr; + } + + for (auto iter = mEchRetryCounterMap.ConstIter(); !iter.Done(); iter.Next()) { + Telemetry::Accumulate(static_cast(iter.Key()), + iter.UserData()); + } + + // closing this pipe triggers the channel's OnStopRequest method. + mPipeOut->CloseWithStatus(reason); +} + +nsHttpConnectionInfo* nsHttpTransaction::ConnectionInfo() { + return mConnInfo.get(); +} + +bool // NOTE BASE CLASS +nsAHttpTransaction::ResponseTimeoutEnabled() const { + return false; +} + +PRIntervalTime // NOTE BASE CLASS +nsAHttpTransaction::ResponseTimeout() { + return gHttpHandler->ResponseTimeout(); +} + +bool nsHttpTransaction::ResponseTimeoutEnabled() const { + return mResponseTimeoutEnabled; +} + +//----------------------------------------------------------------------------- +// nsHttpTransaction +//----------------------------------------------------------------------------- + +nsresult nsHttpTransaction::Restart() { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + // limit the number of restart attempts - bug 92224 + if (++mRestartCount >= gHttpHandler->MaxRequestAttempts()) { + LOG(("reached max request attempts, failing transaction @%p\n", this)); + return NS_ERROR_NET_RESET; + } + + LOG(("restarting transaction @%p\n", this)); + mTunnelProvider = nullptr; + + if (mRequestHead) { + // Dispatching on a new connection better w/o an ambient connection proxy + // auth request header to not confuse the proxy authenticator. + nsAutoCString proxyAuth; + if (NS_SUCCEEDED( + mRequestHead->GetHeader(nsHttp::Proxy_Authorization, proxyAuth)) && + IsStickyAuthSchemeAt(proxyAuth)) { + Unused << mRequestHead->ClearHeader(nsHttp::Proxy_Authorization); + } + } + + // rewind streams in case we already wrote out the request + nsCOMPtr seekable = do_QueryInterface(mRequestStream); + if (seekable) seekable->Seek(nsISeekableStream::NS_SEEK_SET, 0); + + // clear old connection state... + { + MutexAutoLock lock(mLock); + mSecurityInfo = nullptr; + } + + if (mConnection) { + if (!mReuseOnRestart) { + mConnection->DontReuse(); + } + MutexAutoLock lock(mLock); + mConnection = nullptr; + } + + // Reset this to our default state, since this may change from one restart + // to the next + mReuseOnRestart = false; + + if (!mDoNotRemoveAltSvc && !mConnInfo->GetRoutedHost().IsEmpty() && + !mDontRetryWithDirectRoute) { + RefPtr ci; + mConnInfo->CloneAsDirectRoute(getter_AddRefs(ci)); + mConnInfo = ci; + if (mRequestHead) { + DebugOnly rv = + mRequestHead->SetHeader(nsHttp::Alternate_Service_Used, "0"_ns); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + } + + // Reset mDoNotRemoveAltSvc for the next try. + mDoNotRemoveAltSvc = false; + mRestarted = true; + + return gHttpHandler->InitiateTransaction(this, mPriority); +} + +bool nsHttpTransaction::TakeRestartedState() { + // This return true if the transaction has been restarted internally. Used to + // let the consuming nsHttpChannel reset proxy authentication. The flag is + // reset to false by this method. + return mRestarted.exchange(false); +} + +char* nsHttpTransaction::LocateHttpStart(char* buf, uint32_t len, + bool aAllowPartialMatch) { + MOZ_ASSERT(!aAllowPartialMatch || mLineBuf.IsEmpty()); + + static const char HTTPHeader[] = "HTTP/1."; + static const uint32_t HTTPHeaderLen = sizeof(HTTPHeader) - 1; + static const char HTTP2Header[] = "HTTP/2"; + static const uint32_t HTTP2HeaderLen = sizeof(HTTP2Header) - 1; + static const char HTTP3Header[] = "HTTP/3"; + static const uint32_t HTTP3HeaderLen = sizeof(HTTP3Header) - 1; + // ShoutCast ICY is treated as HTTP/1.0 + static const char ICYHeader[] = "ICY "; + static const uint32_t ICYHeaderLen = sizeof(ICYHeader) - 1; + + if (aAllowPartialMatch && (len < HTTPHeaderLen)) + return (PL_strncasecmp(buf, HTTPHeader, len) == 0) ? buf : nullptr; + + // mLineBuf can contain partial match from previous search + if (!mLineBuf.IsEmpty()) { + MOZ_ASSERT(mLineBuf.Length() < HTTPHeaderLen); + int32_t checkChars = std::min(len, HTTPHeaderLen - mLineBuf.Length()); + if (PL_strncasecmp(buf, HTTPHeader + mLineBuf.Length(), checkChars) == 0) { + mLineBuf.Append(buf, checkChars); + if (mLineBuf.Length() == HTTPHeaderLen) { + // We've found whole HTTPHeader sequence. Return pointer at the + // end of matched sequence since it is stored in mLineBuf. + return (buf + checkChars); + } + // Response matches pattern but is still incomplete. + return nullptr; + } + // Previous partial match together with new data doesn't match the + // pattern. Start the search again. + mLineBuf.Truncate(); + } + + bool firstByte = true; + while (len > 0) { + if (PL_strncasecmp(buf, HTTPHeader, + std::min(len, HTTPHeaderLen)) == 0) { + if (len < HTTPHeaderLen) { + // partial HTTPHeader sequence found + // save partial match to mLineBuf + mLineBuf.Assign(buf, len); + return nullptr; + } + + // whole HTTPHeader sequence found + return buf; + } + + // At least "SmarterTools/2.0.3974.16813" generates nonsensical + // HTTP/2.0 responses to our HTTP/1 requests. Treat the minimal case of + // it as HTTP/1.1 to be compatible with old versions of ourselves and + // other browsers + + if (firstByte && !mInvalidResponseBytesRead && len >= HTTP2HeaderLen && + (PL_strncasecmp(buf, HTTP2Header, HTTP2HeaderLen) == 0)) { + LOG(("nsHttpTransaction:: Identified HTTP/2.0 treating as 1.x\n")); + return buf; + } + + // HTTP/3.0 responses to our HTTP/1 requests. Treat the minimal case of + // it as HTTP/1.1 to be compatible with old versions of ourselves and + // other browsers + + if (firstByte && !mInvalidResponseBytesRead && len >= HTTP3HeaderLen && + (PL_strncasecmp(buf, HTTP3Header, HTTP3HeaderLen) == 0)) { + LOG(("nsHttpTransaction:: Identified HTTP/3.0 treating as 1.x\n")); + return buf; + } + + // Treat ICY (AOL/Nullsoft ShoutCast) non-standard header in same fashion + // as HTTP/2.0 is treated above. This will allow "ICY " to be interpretted + // as HTTP/1.0 in nsHttpResponseHead::ParseVersion + + if (firstByte && !mInvalidResponseBytesRead && len >= ICYHeaderLen && + (PL_strncasecmp(buf, ICYHeader, ICYHeaderLen) == 0)) { + LOG(("nsHttpTransaction:: Identified ICY treating as HTTP/1.0\n")); + return buf; + } + + if (!nsCRT::IsAsciiSpace(*buf)) firstByte = false; + buf++; + len--; + } + return nullptr; +} + +nsresult nsHttpTransaction::ParseLine(nsACString& line) { + LOG1(("nsHttpTransaction::ParseLine [%s]\n", PromiseFlatCString(line).get())); + nsresult rv = NS_OK; + + if (!mHaveStatusLine) { + mResponseHead->ParseStatusLine(line); + mHaveStatusLine = true; + // XXX this should probably never happen + if (mResponseHead->Version() == HttpVersion::v0_9) mHaveAllHeaders = true; + } else { + rv = mResponseHead->ParseHeaderLine(line); + } + return rv; +} + +nsresult nsHttpTransaction::ParseLineSegment(char* segment, uint32_t len) { + MOZ_ASSERT(!mHaveAllHeaders, "already have all headers"); + + if (!mLineBuf.IsEmpty() && mLineBuf.Last() == '\n') { + // trim off the new line char, and if this segment is + // not a continuation of the previous or if we haven't + // parsed the status line yet, then parse the contents + // of mLineBuf. + mLineBuf.Truncate(mLineBuf.Length() - 1); + if (!mHaveStatusLine || (*segment != ' ' && *segment != '\t')) { + nsresult rv = ParseLine(mLineBuf); + mLineBuf.Truncate(); + if (NS_FAILED(rv)) { + return rv; + } + } + } + + // append segment to mLineBuf... + mLineBuf.Append(segment, len); + + // a line buf with only a new line char signifies the end of headers. + if (mLineBuf.First() == '\n') { + mLineBuf.Truncate(); + // discard this response if it is a 100 continue or other 1xx status. + uint16_t status = mResponseHead->Status(); + if ((status != 101) && (status / 100 == 1)) { + LOG(("ignoring 1xx response\n")); + mHaveStatusLine = false; + mHttpResponseMatched = false; + mConnection->SetLastTransactionExpectedNoContent(true); + mResponseHead->Reset(); + return NS_OK; + } + mHaveAllHeaders = true; + } + return NS_OK; +} + +nsresult nsHttpTransaction::ParseHead(char* buf, uint32_t count, + uint32_t* countRead) { + nsresult rv; + uint32_t len; + char* eol; + + LOG(("nsHttpTransaction::ParseHead [count=%u]\n", count)); + + *countRead = 0; + + MOZ_ASSERT(!mHaveAllHeaders, "oops"); + + // allocate the response head object if necessary + if (!mResponseHead) { + mResponseHead = new nsHttpResponseHead(); + if (!mResponseHead) return NS_ERROR_OUT_OF_MEMORY; + + // report that we have a least some of the response + if (mActivityDistributor && !mReportedStart) { + mReportedStart = true; + rv = mActivityDistributor->ObserveActivityWithArgs( + HttpActivityArgs(mChannelId), NS_HTTP_ACTIVITY_TYPE_HTTP_TRANSACTION, + NS_HTTP_ACTIVITY_SUBTYPE_RESPONSE_START, PR_Now(), 0, ""_ns); + if (NS_FAILED(rv)) { + LOG3(("ObserveActivity failed (%08x)", static_cast(rv))); + } + } + } + + if (!mHttpResponseMatched) { + // Normally we insist on seeing HTTP/1.x in the first few bytes, + // but if we are on a persistent connection and the previous transaction + // was not supposed to have any content then we need to be prepared + // to skip over a response body that the server may have sent even + // though it wasn't allowed. + if (!mConnection || !mConnection->LastTransactionExpectedNoContent()) { + // tolerate only minor junk before the status line + mHttpResponseMatched = true; + char* p = LocateHttpStart(buf, std::min(count, 11), true); + if (!p) { + // Treat any 0.9 style response of a put as a failure. + if (mRequestHead->IsPut()) return NS_ERROR_ABORT; + + mResponseHead->ParseStatusLine(""_ns); + mHaveStatusLine = true; + mHaveAllHeaders = true; + return NS_OK; + } + if (p > buf) { + // skip over the junk + mInvalidResponseBytesRead += p - buf; + *countRead = p - buf; + buf = p; + } + } else { + char* p = LocateHttpStart(buf, count, false); + if (p) { + mInvalidResponseBytesRead += p - buf; + *countRead = p - buf; + buf = p; + mHttpResponseMatched = true; + } else { + mInvalidResponseBytesRead += count; + *countRead = count; + if (mInvalidResponseBytesRead > MAX_INVALID_RESPONSE_BODY_SIZE) { + LOG( + ("nsHttpTransaction::ParseHead() " + "Cannot find Response Header\n")); + // cannot go back and call this 0.9 anymore as we + // have thrown away a lot of the leading junk + return NS_ERROR_ABORT; + } + return NS_OK; + } + } + } + // otherwise we can assume that we don't have a HTTP/0.9 response. + + MOZ_ASSERT(mHttpResponseMatched); + while ((eol = static_cast(memchr(buf, '\n', count - *countRead))) != + nullptr) { + // found line in range [buf:eol] + len = eol - buf + 1; + + *countRead += len; + + // actually, the line is in the range [buf:eol-1] + if ((eol > buf) && (*(eol - 1) == '\r')) len--; + + buf[len - 1] = '\n'; + rv = ParseLineSegment(buf, len); + if (NS_FAILED(rv)) return rv; + + if (mHaveAllHeaders) return NS_OK; + + // skip over line + buf = eol + 1; + + if (!mHttpResponseMatched) { + // a 100 class response has caused us to throw away that set of + // response headers and look for the next response + return NS_ERROR_NET_INTERRUPT; + } + } + + // do something about a partial header line + if (!mHaveAllHeaders && (len = count - *countRead)) { + *countRead = count; + // ignore a trailing carriage return, and don't bother calling + // ParseLineSegment if buf only contains a carriage return. + if ((buf[len - 1] == '\r') && (--len == 0)) return NS_OK; + rv = ParseLineSegment(buf, len); + if (NS_FAILED(rv)) return rv; + } + return NS_OK; +} + +nsresult nsHttpTransaction::HandleContentStart() { + LOG(("nsHttpTransaction::HandleContentStart [this=%p]\n", this)); + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + if (mResponseHead) { + if (mEarlyDataDisposition == EARLY_ACCEPTED) { + if (mResponseHead->Status() == 425) { + // We will report this state when the final responce arrives. + mEarlyDataDisposition = EARLY_425; + } else { + Unused << mResponseHead->SetHeader(nsHttp::X_Firefox_Early_Data, + "accepted"_ns); + } + } else if (mEarlyDataDisposition == EARLY_SENT) { + Unused << mResponseHead->SetHeader(nsHttp::X_Firefox_Early_Data, + "sent"_ns); + } else if (mEarlyDataDisposition == EARLY_425) { + Unused << mResponseHead->SetHeader(nsHttp::X_Firefox_Early_Data, + "received 425"_ns); + mEarlyDataDisposition = EARLY_NONE; + } // no header on NONE case + + if (mFastOpenStatus == TFO_DATA_SENT) { + Unused << mResponseHead->SetHeader(nsHttp::X_Firefox_TCP_Fast_Open, + "data sent"_ns); + } else if (mFastOpenStatus == TFO_TRIED) { + Unused << mResponseHead->SetHeader(nsHttp::X_Firefox_TCP_Fast_Open, + "tried negotiating"_ns); + } else if (mFastOpenStatus == TFO_FAILED) { + Unused << mResponseHead->SetHeader(nsHttp::X_Firefox_TCP_Fast_Open, + "failed"_ns); + } // no header on TFO_NOT_TRIED case + + if (LOG3_ENABLED()) { + LOG3(("http response [\n")); + nsAutoCString headers; + mResponseHead->Flatten(headers, false); + headers.AppendLiteral(" OriginalHeaders"); + headers.AppendLiteral("\r\n"); + mResponseHead->FlattenNetworkOriginalHeaders(headers); + LogHeaders(headers.get()); + LOG3(("]\n")); + } + + CheckForStickyAuthScheme(); + + // Save http version, mResponseHead isn't available anymore after + // TakeResponseHead() is called + mHttpVersion = mResponseHead->Version(); + mHttpResponseCode = mResponseHead->Status(); + + // notify the connection, give it a chance to cause a reset. + bool reset = false; + nsresult rv = mConnection->OnHeadersAvailable(this, mRequestHead, + mResponseHead, &reset); + NS_ENSURE_SUCCESS(rv, rv); + + // looks like we should ignore this response, resetting... + if (reset) { + LOG(("resetting transaction's response head\n")); + mHaveAllHeaders = false; + mHaveStatusLine = false; + mReceivedData = false; + mSentData = false; + mHttpResponseMatched = false; + mResponseHead->Reset(); + // wait to be called again... + return NS_OK; + } + + // check if this is a no-content response + switch (mResponseHead->Status()) { + case 101: + mPreserveStream = true; + [[fallthrough]]; // to other no content cases: + case 204: + case 205: + case 304: + mNoContent = true; + LOG(("this response should not contain a body.\n")); + break; + case 421: + LOG(("Misdirected Request.\n")); + gHttpHandler->ClearHostMapping(mConnInfo); + + m421Received = true; + + // Unsticky the connection to allow restart. This can get set when an + // NTLM proxy is successfully authenticated. + mCaps |= (NS_HTTP_REFRESH_DNS | NS_HTTP_CONNECTION_RESTARTABLE); + mCaps &= ~NS_HTTP_STICKY_CONNECTION; + + // retry on a new connection - just in case + if (!mRestartCount) { + mCaps &= ~NS_HTTP_ALLOW_KEEPALIVE; + mForceRestart = true; // force restart has built in loop protection + return NS_ERROR_NET_RESET; + } + break; + case 425: + LOG(("Too Early.")); + if ((mEarlyDataDisposition == EARLY_425) && !mDoNotTryEarlyData) { + mDoNotTryEarlyData = true; + mForceRestart = true; // force restart has built in loop protection + if (mConnection->Version() >= HttpVersion::v2_0) { + mReuseOnRestart = true; + } + return NS_ERROR_NET_RESET; + } + break; + } + + // Remember whether HTTP3 is supported + if ((mHttpVersion >= HttpVersion::v2_0) && + (mResponseHead->Status() < 500) && (mResponseHead->Status() != 421)) { + nsAutoCString altSvc; + Unused << mResponseHead->GetHeader(nsHttp::Alternate_Service, altSvc); + if (!altSvc.IsEmpty() || nsHttp::IsReasonableHeaderValue(altSvc)) { + for (uint32_t i = 0; i < kHttp3VersionCount; i++) { + if (PL_strstr(altSvc.get(), kHttp3Versions[i].get())) { + mSupportsHTTP3 = true; + break; + } + } + } + } + + // Report telemetry + if (mSupportsHTTP3) { + Accumulate(Telemetry::TRANSACTION_WAIT_TIME_HTTP2_SUP_HTTP3, + mPendingDurationTime.ToMilliseconds()); + } + + // If we're only connecting then we're going to be upgrading this + // connection since we were successful. Any data from now on belongs to + // the upgrade handler. If we're not successful the content body doesn't + // matter. Proxy http errors are treated as network errors. This + // connection won't be reused since it's marked sticky and no + // keep-alive. + if (mCaps & NS_HTTP_CONNECT_ONLY) { + MOZ_ASSERT(!(mCaps & NS_HTTP_ALLOW_KEEPALIVE) && + (mCaps & NS_HTTP_STICKY_CONNECTION), + "connection should be sticky and no keep-alive"); + // The transaction will expect the server to close the socket if + // there's no content length instead of doing the upgrade. + mNoContent = true; + } + + if (mResponseHead->Status() == 200 && mH2WSTransaction) { + // http/2 websockets do not have response bodies + mNoContent = true; + } + + if (mResponseHead->Status() == 200 && + mConnection->IsProxyConnectInProgress()) { + // successful CONNECTs do not have response bodies + mNoContent = true; + } + mConnection->SetLastTransactionExpectedNoContent(mNoContent); + + if (mNoContent) { + mContentLength = 0; + } else { + // grab the content-length from the response headers + mContentLength = mResponseHead->ContentLength(); + + // handle chunked encoding here, so we'll know immediately when + // we're done with the socket. please note that _all_ other + // decoding is done when the channel receives the content data + // so as not to block the socket transport thread too much. + if (mResponseHead->Version() >= HttpVersion::v1_0 && + mResponseHead->HasHeaderValue(nsHttp::Transfer_Encoding, "chunked")) { + // we only support the "chunked" transfer encoding right now. + mChunkedDecoder = new nsHttpChunkedDecoder(); + LOG(("nsHttpTransaction %p chunked decoder created\n", this)); + // Ignore server specified Content-Length. + if (mContentLength != int64_t(-1)) { + LOG(("nsHttpTransaction %p chunked with C-L ignores C-L\n", this)); + mContentLength = -1; + if (mConnection) { + mConnection->DontReuse(); + } + } + } else if (mContentLength == int64_t(-1)) + LOG(("waiting for the server to close the connection.\n")); + } + } + + mDidContentStart = true; + return NS_OK; +} + +// called on the socket thread +nsresult nsHttpTransaction::HandleContent(char* buf, uint32_t count, + uint32_t* contentRead, + uint32_t* contentRemaining) { + nsresult rv; + + LOG(("nsHttpTransaction::HandleContent [this=%p count=%u]\n", this, count)); + + *contentRead = 0; + *contentRemaining = 0; + + MOZ_ASSERT(mConnection); + + if (!mDidContentStart) { + rv = HandleContentStart(); + if (NS_FAILED(rv)) return rv; + // Do not write content to the pipe if we haven't started streaming yet + if (!mDidContentStart) return NS_OK; + } + + if (mChunkedDecoder) { + // give the buf over to the chunked decoder so it can reformat the + // data and tell us how much is really there. + rv = mChunkedDecoder->HandleChunkedContent(buf, count, contentRead, + contentRemaining); + if (NS_FAILED(rv)) return rv; + } else if (mContentLength >= int64_t(0)) { + // HTTP/1.0 servers have been known to send erroneous Content-Length + // headers. So, unless the connection is persistent, we must make + // allowances for a possibly invalid Content-Length header. Thus, if + // NOT persistent, we simply accept everything in |buf|. + if (mConnection->IsPersistent() || mPreserveStream || + mHttpVersion >= HttpVersion::v1_1) { + int64_t remaining = mContentLength - mContentRead; + *contentRead = uint32_t(std::min(count, remaining)); + *contentRemaining = count - *contentRead; + } else { + *contentRead = count; + // mContentLength might need to be increased... + int64_t position = mContentRead + int64_t(count); + if (position > mContentLength) { + mContentLength = position; + // mResponseHead->SetContentLength(mContentLength); + } + } + } else { + // when we are just waiting for the server to close the connection... + // (no explicit content-length given) + *contentRead = count; + } + + if (*contentRead) { + // update count of content bytes read and report progress... + mContentRead += *contentRead; + } + + LOG1( + ("nsHttpTransaction::HandleContent [this=%p count=%u read=%u " + "mContentRead=%" PRId64 " mContentLength=%" PRId64 "]\n", + this, count, *contentRead, mContentRead, mContentLength)); + + // check for end-of-file + if ((mContentRead == mContentLength) || + (mChunkedDecoder && mChunkedDecoder->ReachedEOF())) { + MutexAutoLock lock(*nsHttp::GetLock()); + if (mChunkedDecoder) { + mForTakeResponseTrailers = mChunkedDecoder->TakeTrailers(); + } + + // the transaction is done with a complete response. + mTransactionDone = true; + mResponseIsComplete = true; + ReleaseBlockingTransaction(); + + if (TimingEnabled()) { + SetResponseEnd(TimeStamp::Now()); + } + + // report the entire response has arrived + if (mActivityDistributor) { + rv = mActivityDistributor->ObserveActivityWithArgs( + HttpActivityArgs(mChannelId), NS_HTTP_ACTIVITY_TYPE_HTTP_TRANSACTION, + NS_HTTP_ACTIVITY_SUBTYPE_RESPONSE_COMPLETE, PR_Now(), + static_cast(mContentRead), ""_ns); + if (NS_FAILED(rv)) { + LOG3(("ObserveActivity failed (%08x)", static_cast(rv))); + } + } + } + + return NS_OK; +} + +nsresult nsHttpTransaction::ProcessData(char* buf, uint32_t count, + uint32_t* countRead) { + nsresult rv; + + LOG1(("nsHttpTransaction::ProcessData [this=%p count=%u]\n", this, count)); + + *countRead = 0; + + // we may not have read all of the headers yet... + if (!mHaveAllHeaders) { + uint32_t bytesConsumed = 0; + + do { + uint32_t localBytesConsumed = 0; + char* localBuf = buf + bytesConsumed; + uint32_t localCount = count - bytesConsumed; + + rv = ParseHead(localBuf, localCount, &localBytesConsumed); + if (NS_FAILED(rv) && rv != NS_ERROR_NET_INTERRUPT) return rv; + bytesConsumed += localBytesConsumed; + } while (rv == NS_ERROR_NET_INTERRUPT); + + mCurrentHttpResponseHeaderSize += bytesConsumed; + if (mCurrentHttpResponseHeaderSize > + gHttpHandler->MaxHttpResponseHeaderSize()) { + LOG(("nsHttpTransaction %p The response header exceeds the limit.\n", + this)); + return NS_ERROR_FILE_TOO_BIG; + } + count -= bytesConsumed; + + // if buf has some content in it, shift bytes to top of buf. + if (count && bytesConsumed) memmove(buf, buf + bytesConsumed, count); + + // report the completed response header + if (mActivityDistributor && mResponseHead && mHaveAllHeaders && + !mReportedResponseHeader) { + mReportedResponseHeader = true; + nsAutoCString completeResponseHeaders; + mResponseHead->Flatten(completeResponseHeaders, false); + completeResponseHeaders.AppendLiteral("\r\n"); + rv = mActivityDistributor->ObserveActivityWithArgs( + HttpActivityArgs(mChannelId), NS_HTTP_ACTIVITY_TYPE_HTTP_TRANSACTION, + NS_HTTP_ACTIVITY_SUBTYPE_RESPONSE_HEADER, PR_Now(), 0, + completeResponseHeaders); + if (NS_FAILED(rv)) { + LOG3(("ObserveActivity failed (%08x)", static_cast(rv))); + } + } + } + + // even though count may be 0, we still want to call HandleContent + // so it can complete the transaction if this is a "no-content" response. + if (mHaveAllHeaders) { + uint32_t countRemaining = 0; + // + // buf layout: + // + // +--------------------------------------+----------------+-----+ + // | countRead | countRemaining | | + // +--------------------------------------+----------------+-----+ + // + // count : bytes read from the socket + // countRead : bytes corresponding to this transaction + // countRemaining : bytes corresponding to next transaction on conn + // + // NOTE: + // count > countRead + countRemaining <==> chunked transfer encoding + // + rv = HandleContent(buf, count, countRead, &countRemaining); + if (NS_FAILED(rv)) return rv; + // we may have read more than our share, in which case we must give + // the excess bytes back to the connection + if (mResponseIsComplete && countRemaining && + (mConnection->Version() != HttpVersion::v3_0)) { + MOZ_ASSERT(mConnection); + rv = mConnection->PushBack(buf + *countRead, countRemaining); + NS_ENSURE_SUCCESS(rv, rv); + } + + if (!mContentDecodingCheck && mResponseHead) { + mContentDecoding = mResponseHead->HasHeader(nsHttp::Content_Encoding); + mContentDecodingCheck = true; + } + } + + return NS_OK; +} + +// Called when the transaction marked for blocking is associated with a +// connection (i.e. added to a new h1 conn, an idle http connection, etc..) It +// is safe to call this multiple times with it only having an effect once. +void nsHttpTransaction::DispatchedAsBlocking() { + if (mDispatchedAsBlocking) return; + + LOG(("nsHttpTransaction %p dispatched as blocking\n", this)); + + if (!mRequestContext) return; + + LOG( + ("nsHttpTransaction adding blocking transaction %p from " + "request context %p\n", + this, mRequestContext.get())); + + mRequestContext->AddBlockingTransaction(); + mDispatchedAsBlocking = true; +} + +void nsHttpTransaction::RemoveDispatchedAsBlocking() { + if (!mRequestContext || !mDispatchedAsBlocking) { + LOG(("nsHttpTransaction::RemoveDispatchedAsBlocking this=%p not blocking", + this)); + return; + } + + uint32_t blockers = 0; + nsresult rv = mRequestContext->RemoveBlockingTransaction(&blockers); + + LOG( + ("nsHttpTransaction removing blocking transaction %p from " + "request context %p. %d blockers remain.\n", + this, mRequestContext.get(), blockers)); + + if (NS_SUCCEEDED(rv) && !blockers) { + LOG( + ("nsHttpTransaction %p triggering release of blocked channels " + " with request context=%p\n", + this, mRequestContext.get())); + rv = gHttpHandler->ConnMgr()->ProcessPendingQ(); + if (NS_FAILED(rv)) { + LOG( + ("nsHttpTransaction::RemoveDispatchedAsBlocking\n" + " failed to process pending queue\n")); + } + } + + mDispatchedAsBlocking = false; +} + +void nsHttpTransaction::ReleaseBlockingTransaction() { + RemoveDispatchedAsBlocking(); + LOG( + ("nsHttpTransaction %p request context set to null " + "in ReleaseBlockingTransaction() - was %p\n", + this, mRequestContext.get())); + mRequestContext = nullptr; +} + +void nsHttpTransaction::DisableSpdy() { + mCaps |= NS_HTTP_DISALLOW_SPDY; + if (mConnInfo) { + // This is our clone of the connection info, not the persistent one that + // is owned by the connection manager, so we're safe to change this here + mConnInfo->SetNoSpdy(true); + } +} + +void nsHttpTransaction::DisableHttp3() { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + // mOrigConnInfo is an indicator that HTTPS RR is used, so don't mess up the + // connection info. + // When HTTPS RR is used, PrepareConnInfoForRetry() could select other h3 + // record to connect. + if (mOrigConnInfo) { + LOG(("nsHttpTransaction::DisableHttp3 this=%p mOrigConnInfo=%s", this, + mOrigConnInfo->HashKey().get())); + return; + } + + mCaps |= NS_HTTP_DISALLOW_HTTP3; + MOZ_ASSERT(mConnInfo); + if (mConnInfo) { + // After CloneAsDirectRoute(), http3 will be disabled. + RefPtr connInfo; + mConnInfo->CloneAsDirectRoute(getter_AddRefs(connInfo)); + if (mRequestHead) { + DebugOnly rv = + mRequestHead->SetHeader(nsHttp::Alternate_Service_Used, "0"_ns); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + MOZ_ASSERT(!connInfo->IsHttp3()); + mConnInfo.swap(connInfo); + } +} + +void nsHttpTransaction::CheckForStickyAuthScheme() { + LOG(("nsHttpTransaction::CheckForStickyAuthScheme this=%p", this)); + + MOZ_ASSERT(mHaveAllHeaders); + MOZ_ASSERT(mResponseHead); + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + CheckForStickyAuthSchemeAt(nsHttp::WWW_Authenticate); + CheckForStickyAuthSchemeAt(nsHttp::Proxy_Authenticate); +} + +void nsHttpTransaction::CheckForStickyAuthSchemeAt(nsHttpAtom const& header) { + if (mCaps & NS_HTTP_STICKY_CONNECTION) { + LOG((" already sticky")); + return; + } + + nsAutoCString auth; + if (NS_FAILED(mResponseHead->GetHeader(header, auth))) { + return; + } + + if (IsStickyAuthSchemeAt(auth)) { + LOG((" connection made sticky")); + // This is enough to make this transaction keep it's current connection, + // prevents the connection from being released back to the pool. + mCaps |= NS_HTTP_STICKY_CONNECTION; + } +} + +bool nsHttpTransaction::IsStickyAuthSchemeAt(nsACString const& auth) { + Tokenizer p(auth); + nsAutoCString schema; + while (p.ReadWord(schema)) { + ToLowerCase(schema); + + // using a new instance because of thread safety of auth modules refcnt + nsCOMPtr authenticator; + if (schema.EqualsLiteral("negotiate")) { + authenticator = new nsHttpNegotiateAuth(); + } else if (schema.EqualsLiteral("basic")) { + authenticator = new nsHttpBasicAuth(); + } else if (schema.EqualsLiteral("digest")) { + authenticator = new nsHttpDigestAuth(); + } else if (schema.EqualsLiteral("ntlm")) { + authenticator = new nsHttpNTLMAuth(); + } + if (authenticator) { + uint32_t flags; + nsresult rv = authenticator->GetAuthFlags(&flags); + if (NS_SUCCEEDED(rv) && + (flags & nsIHttpAuthenticator::CONNECTION_BASED)) { + return true; + } + } + + // schemes are separated with LFs, nsHttpHeaderArray::MergeHeader + p.SkipUntil(Tokenizer::Token::NewLine()); + p.SkipWhites(Tokenizer::INCLUDE_NEW_LINE); + } + + return false; +} + +const TimingStruct nsHttpTransaction::Timings() { + mozilla::MutexAutoLock lock(mLock); + TimingStruct timings = mTimings; + return timings; +} + +void nsHttpTransaction::BootstrapTimings(TimingStruct times) { + mozilla::MutexAutoLock lock(mLock); + mTimings = times; +} + +void nsHttpTransaction::SetDomainLookupStart(mozilla::TimeStamp timeStamp, + bool onlyIfNull) { + mozilla::MutexAutoLock lock(mLock); + if (onlyIfNull && !mTimings.domainLookupStart.IsNull()) { + return; // We only set the timestamp if it was previously null + } + mTimings.domainLookupStart = timeStamp; +} + +void nsHttpTransaction::SetDomainLookupEnd(mozilla::TimeStamp timeStamp, + bool onlyIfNull) { + mozilla::MutexAutoLock lock(mLock); + if (onlyIfNull && !mTimings.domainLookupEnd.IsNull()) { + return; // We only set the timestamp if it was previously null + } + mTimings.domainLookupEnd = timeStamp; +} + +void nsHttpTransaction::SetConnectStart(mozilla::TimeStamp timeStamp, + bool onlyIfNull) { + mozilla::MutexAutoLock lock(mLock); + if (onlyIfNull && !mTimings.connectStart.IsNull()) { + return; // We only set the timestamp if it was previously null + } + mTimings.connectStart = timeStamp; +} + +void nsHttpTransaction::SetConnectEnd(mozilla::TimeStamp timeStamp, + bool onlyIfNull) { + mozilla::MutexAutoLock lock(mLock); + if (onlyIfNull && !mTimings.connectEnd.IsNull()) { + return; // We only set the timestamp if it was previously null + } + mTimings.connectEnd = timeStamp; +} + +void nsHttpTransaction::SetRequestStart(mozilla::TimeStamp timeStamp, + bool onlyIfNull) { + mozilla::MutexAutoLock lock(mLock); + if (onlyIfNull && !mTimings.requestStart.IsNull()) { + return; // We only set the timestamp if it was previously null + } + mTimings.requestStart = timeStamp; +} + +void nsHttpTransaction::SetResponseStart(mozilla::TimeStamp timeStamp, + bool onlyIfNull) { + mozilla::MutexAutoLock lock(mLock); + if (onlyIfNull && !mTimings.responseStart.IsNull()) { + return; // We only set the timestamp if it was previously null + } + mTimings.responseStart = timeStamp; +} + +void nsHttpTransaction::SetResponseEnd(mozilla::TimeStamp timeStamp, + bool onlyIfNull) { + mozilla::MutexAutoLock lock(mLock); + if (onlyIfNull && !mTimings.responseEnd.IsNull()) { + return; // We only set the timestamp if it was previously null + } + mTimings.responseEnd = timeStamp; +} + +mozilla::TimeStamp nsHttpTransaction::GetDomainLookupStart() { + mozilla::MutexAutoLock lock(mLock); + return mTimings.domainLookupStart; +} + +mozilla::TimeStamp nsHttpTransaction::GetDomainLookupEnd() { + mozilla::MutexAutoLock lock(mLock); + return mTimings.domainLookupEnd; +} + +mozilla::TimeStamp nsHttpTransaction::GetConnectStart() { + mozilla::MutexAutoLock lock(mLock); + return mTimings.connectStart; +} + +mozilla::TimeStamp nsHttpTransaction::GetTcpConnectEnd() { + mozilla::MutexAutoLock lock(mLock); + return mTimings.tcpConnectEnd; +} + +mozilla::TimeStamp nsHttpTransaction::GetSecureConnectionStart() { + mozilla::MutexAutoLock lock(mLock); + return mTimings.secureConnectionStart; +} + +mozilla::TimeStamp nsHttpTransaction::GetConnectEnd() { + mozilla::MutexAutoLock lock(mLock); + return mTimings.connectEnd; +} + +mozilla::TimeStamp nsHttpTransaction::GetRequestStart() { + mozilla::MutexAutoLock lock(mLock); + return mTimings.requestStart; +} + +mozilla::TimeStamp nsHttpTransaction::GetResponseStart() { + mozilla::MutexAutoLock lock(mLock); + return mTimings.responseStart; +} + +mozilla::TimeStamp nsHttpTransaction::GetResponseEnd() { + mozilla::MutexAutoLock lock(mLock); + return mTimings.responseEnd; +} + +//----------------------------------------------------------------------------- +// nsHttpTransaction deletion event +//----------------------------------------------------------------------------- + +class DeleteHttpTransaction : public Runnable { + public: + explicit DeleteHttpTransaction(nsHttpTransaction* trans) + : Runnable("net::DeleteHttpTransaction"), mTrans(trans) {} + + NS_IMETHOD Run() override { + delete mTrans; + return NS_OK; + } + + private: + nsHttpTransaction* mTrans; +}; + +void nsHttpTransaction::DeleteSelfOnConsumerThread() { + LOG(("nsHttpTransaction::DeleteSelfOnConsumerThread [this=%p]\n", this)); + + bool val; + if (!mConsumerTarget || + (NS_SUCCEEDED(mConsumerTarget->IsOnCurrentThread(&val)) && val)) { + delete this; + } else { + LOG(("proxying delete to consumer thread...\n")); + nsCOMPtr event = new DeleteHttpTransaction(this); + if (NS_FAILED(mConsumerTarget->Dispatch(event, NS_DISPATCH_NORMAL))) + NS_WARNING("failed to dispatch nsHttpDeleteTransaction event"); + } +} + +bool nsHttpTransaction::TryToRunPacedRequest() { + if (mSubmittedRatePacing) return mPassedRatePacing; + + mSubmittedRatePacing = true; + mSynchronousRatePaceRequest = true; + Unused << gHttpHandler->SubmitPacedRequest( + this, getter_AddRefs(mTokenBucketCancel)); + mSynchronousRatePaceRequest = false; + return mPassedRatePacing; +} + +void nsHttpTransaction::OnTokenBucketAdmitted() { + mPassedRatePacing = true; + mTokenBucketCancel = nullptr; + + if (!mSynchronousRatePaceRequest) { + nsresult rv = gHttpHandler->ConnMgr()->ProcessPendingQ(mConnInfo); + if (NS_FAILED(rv)) { + LOG( + ("nsHttpTransaction::OnTokenBucketAdmitted\n" + " failed to process pending queue\n")); + } + } +} + +void nsHttpTransaction::CancelPacing(nsresult reason) { + if (mTokenBucketCancel) { + mTokenBucketCancel->Cancel(reason); + mTokenBucketCancel = nullptr; + } +} + +//----------------------------------------------------------------------------- +// nsHttpTransaction::nsISupports +//----------------------------------------------------------------------------- + +NS_IMPL_ADDREF(nsHttpTransaction) + +NS_IMETHODIMP_(MozExternalRefCountType) +nsHttpTransaction::Release() { + nsrefcnt count; + MOZ_ASSERT(0 != mRefCnt, "dup release"); + count = --mRefCnt; + NS_LOG_RELEASE(this, count, "nsHttpTransaction"); + if (0 == count) { + mRefCnt = 1; /* stablize */ + // it is essential that the transaction be destroyed on the consumer + // thread (we could be holding the last reference to our consumer). + DeleteSelfOnConsumerThread(); + return 0; + } + return count; +} + +NS_IMPL_QUERY_INTERFACE(nsHttpTransaction, nsIInputStreamCallback, + nsIOutputStreamCallback) + +//----------------------------------------------------------------------------- +// nsHttpTransaction::nsIInputStreamCallback +//----------------------------------------------------------------------------- + +// called on the socket thread +NS_IMETHODIMP +nsHttpTransaction::OnInputStreamReady(nsIAsyncInputStream* out) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + if (mConnection) { + mConnection->TransactionHasDataToWrite(this); + nsresult rv = mConnection->ResumeSend(); + if (NS_FAILED(rv)) NS_ERROR("ResumeSend failed"); + } + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsHttpTransaction::nsIOutputStreamCallback +//----------------------------------------------------------------------------- + +// called on the socket thread +NS_IMETHODIMP +nsHttpTransaction::OnOutputStreamReady(nsIAsyncOutputStream* out) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + mWaitingOnPipeOut = false; + if (mConnection) { + mConnection->TransactionHasDataToRecv(this); + nsresult rv = mConnection->ResumeRecv(); + if (NS_FAILED(rv)) NS_ERROR("ResumeRecv failed"); + } + return NS_OK; +} + +void nsHttpTransaction::GetNetworkAddresses(NetAddr& self, NetAddr& peer, + bool& aResolvedByTRR, + bool& aEchConfigUsed) { + MutexAutoLock lock(mLock); + self = mSelfAddr; + peer = mPeerAddr; + aResolvedByTRR = mResolvedByTRR; + aEchConfigUsed = mEchConfigUsed; +} + +bool nsHttpTransaction::CanDo0RTT() { + if (mRequestHead->IsSafeMethod() && !mDoNotTryEarlyData && + (!mConnection || !mConnection->IsProxyConnectInProgress())) { + return true; + } + return false; +} + +bool nsHttpTransaction::Do0RTT() { + if (mRequestHead->IsSafeMethod() && !mDoNotTryEarlyData && + (!mConnection || !mConnection->IsProxyConnectInProgress())) { + m0RTTInProgress = true; + } + return m0RTTInProgress; +} + +nsresult nsHttpTransaction::Finish0RTT(bool aRestart, + bool aAlpnChanged /* ignored */) { + LOG(("nsHttpTransaction::Finish0RTT %p %d %d\n", this, aRestart, + aAlpnChanged)); + MOZ_ASSERT(m0RTTInProgress); + m0RTTInProgress = false; + + MaybeCancelFallbackTimer(); + + if (!aRestart && (mEarlyDataDisposition == EARLY_SENT)) { + // note that if this is invoked by a 3 param version of finish0rtt this + // disposition might be reverted + mEarlyDataDisposition = EARLY_ACCEPTED; + } + if (aRestart) { + // Reset request headers to be sent again. + nsCOMPtr seekable = do_QueryInterface(mRequestStream); + if (seekable) { + seekable->Seek(nsISeekableStream::NS_SEEK_SET, 0); + } else { + return NS_ERROR_FAILURE; + } + } else if (!mConnected) { + // this is code that was skipped in ::ReadSegments while in 0RTT + mConnected = true; + nsCOMPtr info; + mConnection->GetSecurityInfo(getter_AddRefs(info)); + MutexAutoLock lock(mLock); + mSecurityInfo = std::move(info); + } + return NS_OK; +} + +nsresult nsHttpTransaction::RestartOnFastOpenError() { + // This will happen on connection error during Fast Open or if connect + // during Fast Open takes too long. So we should not have received any + // data! + MOZ_ASSERT(!mReceivedData); + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + LOG( + ("nsHttpTransaction::RestartOnFastOpenError - restarting transaction " + "%p\n", + this)); + + // rewind streams in case we already wrote out the request + nsCOMPtr seekable = do_QueryInterface(mRequestStream); + if (seekable) seekable->Seek(nsISeekableStream::NS_SEEK_SET, 0); + // clear old connection state... + { + MutexAutoLock lock(mLock); + mSecurityInfo = nullptr; + } + + if (!mConnInfo->GetRoutedHost().IsEmpty()) { + RefPtr ci; + mConnInfo->CloneAsDirectRoute(getter_AddRefs(ci)); + mConnInfo = ci; + if (mRequestHead) { + DebugOnly rv = + mRequestHead->SetHeader(nsHttp::Alternate_Service_Used, "0"_ns); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + } + mEarlyDataDisposition = EARLY_NONE; + m0RTTInProgress = false; + mFastOpenStatus = TFO_FAILED; + mTimings = TimingStruct(); + return NS_OK; +} + +void nsHttpTransaction::SetFastOpenStatus(uint8_t aStatus) { + LOG(("nsHttpTransaction::SetFastOpenStatus %d [this=%p]\n", aStatus, this)); + mFastOpenStatus = aStatus; +} + +void nsHttpTransaction::Refused0RTT() { + LOG(("nsHttpTransaction::Refused0RTT %p\n", this)); + if (mEarlyDataDisposition == EARLY_ACCEPTED) { + mEarlyDataDisposition = EARLY_SENT; // undo accepted state + } +} + +void nsHttpTransaction::SetHttpTrailers(nsCString& aTrailers) { + LOG(("nsHttpTransaction::SetHttpTrailers %p", this)); + LOG(("[\n %s\n]", aTrailers.BeginReading())); + + // Introduce a local variable to minimize the critical section. + UniquePtr httpTrailers(new nsHttpHeaderArray()); + // Given it's usually null, use double-check locking for performance. + if (mForTakeResponseTrailers) { + MutexAutoLock lock(*nsHttp::GetLock()); + if (mForTakeResponseTrailers) { + // Copy the trailer. |TakeResponseTrailers| gets the original trailer + // until the final swap. + *httpTrailers = *mForTakeResponseTrailers; + } + } + + int32_t cur = 0; + int32_t len = aTrailers.Length(); + while (cur < len) { + int32_t newline = aTrailers.FindCharInSet("\n", cur); + if (newline == -1) { + newline = len; + } + + int32_t end = + (newline && aTrailers[newline - 1] == '\r') ? newline - 1 : newline; + nsDependentCSubstring line(aTrailers, cur, end); + nsHttpAtom hdr; + nsAutoCString hdrNameOriginal; + nsAutoCString val; + if (NS_SUCCEEDED(httpTrailers->ParseHeaderLine(line, &hdr, &hdrNameOriginal, + &val))) { + if (hdr == nsHttp::Server_Timing) { + Unused << httpTrailers->SetHeaderFromNet(hdr, hdrNameOriginal, val, + true); + } + } + + cur = newline + 1; + } + + if (httpTrailers->Count() == 0) { + // Didn't find a Server-Timing header, so get rid of this. + httpTrailers = nullptr; + } + + MutexAutoLock lock(*nsHttp::GetLock()); + std::swap(mForTakeResponseTrailers, httpTrailers); +} + +bool nsHttpTransaction::IsWebsocketUpgrade() { + if (mRequestHead) { + nsAutoCString upgradeHeader; + if (NS_SUCCEEDED(mRequestHead->GetHeader(nsHttp::Upgrade, upgradeHeader)) && + upgradeHeader.LowerCaseEqualsLiteral("websocket")) { + return true; + } + } + return false; +} + +void nsHttpTransaction::SetH2WSTransaction( + SpdyConnectTransaction* aH2WSTransaction) { + MOZ_ASSERT(OnSocketThread()); + + mH2WSTransaction = aH2WSTransaction; +} + +void nsHttpTransaction::OnProxyConnectComplete(int32_t aResponseCode) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + MOZ_ASSERT(mConnInfo->UsingConnect()); + + LOG(("nsHttpTransaction::OnProxyConnectComplete %p aResponseCode=%d", this, + aResponseCode)); + + mProxyConnectResponseCode = aResponseCode; +} + +int32_t nsHttpTransaction::GetProxyConnectResponseCode() { + return mProxyConnectResponseCode; +} + +void nsHttpTransaction::SetFlat407Headers(const nsACString& aHeaders) { + MOZ_ASSERT(mProxyConnectResponseCode == 407); + MOZ_ASSERT(!mResponseHead); + + LOG(("nsHttpTransaction::SetFlat407Headers %p", this)); + mFlat407Headers = aHeaders; +} + +void nsHttpTransaction::NotifyTransactionObserver(nsresult reason) { + MOZ_ASSERT(OnSocketThread()); + + if (!mTransactionObserver) { + return; + } + + bool versionOk = false; + bool authOk = false; + + LOG(("nsHttpTransaction::NotifyTransactionObserver %p reason %" PRIx32 + " conn %p\n", + this, static_cast(reason), mConnection.get())); + + if (mConnection) { + HttpVersion version = mConnection->Version(); + versionOk = (((reason == NS_BASE_STREAM_CLOSED) || (reason == NS_OK)) && + ((mConnection->Version() == HttpVersion::v2_0) || + (mConnection->Version() == HttpVersion::v3_0))); + + nsCOMPtr secInfo; + mConnection->GetSecurityInfo(getter_AddRefs(secInfo)); + nsCOMPtr socketControl = do_QueryInterface(secInfo); + LOG( + ("nsHttpTransaction::NotifyTransactionObserver" + " version %u socketControl %p\n", + static_cast(version), socketControl.get())); + if (socketControl) { + authOk = !socketControl->GetFailedVerification(); + } + } + + TransactionObserverResult result; + result.versionOk() = versionOk; + result.authOk() = authOk; + result.closeReason() = reason; + + TransactionObserverFunc obs = nullptr; + std::swap(obs, mTransactionObserver); + obs(std::move(result)); +} + +void nsHttpTransaction::UpdateConnectionInfo(nsHttpConnectionInfo* aConnInfo) { + MOZ_ASSERT(aConnInfo); + + if (mActivated) { + MOZ_ASSERT(false, "Should not update conn info after activated"); + return; + } + + mOrigConnInfo = mConnInfo->Clone(); + mConnInfo = aConnInfo; +} + +nsresult nsHttpTransaction::OnHTTPSRRAvailable( + nsIDNSHTTPSSVCRecord* aHTTPSSVCRecord, + nsISVCBRecord* aHighestPriorityRecord) { + LOG(("nsHttpTransaction::OnHTTPSRRAvailable [this=%p] mActivated=%d", this, + mActivated)); + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + { + MutexAutoLock lock(mLock); + mDNSRequest = nullptr; + } + + uint32_t receivedStage = HTTPSSVC_NO_USABLE_RECORD; + // Make sure we set the correct value to |mHTTPSSVCReceivedStage|, since we + // also use this value to indicate whether HTTPS RR is used or not. + auto updateHTTPSSVCReceivedStage = + MakeScopeExit([&] { mHTTPSSVCReceivedStage = receivedStage; }); + + MakeDontWaitHTTPSSVC(); + + nsCOMPtr record = aHTTPSSVCRecord; + if (!record) { + return NS_ERROR_FAILURE; + } + + bool hasIPAddress = false; + Unused << record->GetHasIPAddresses(&hasIPAddress); + + if (mActivated) { + receivedStage = hasIPAddress ? HTTPSSVC_WITH_IPHINT_RECEIVED_STAGE_2 + : HTTPSSVC_WITHOUT_IPHINT_RECEIVED_STAGE_2; + return NS_OK; + } + + receivedStage = hasIPAddress ? HTTPSSVC_WITH_IPHINT_RECEIVED_STAGE_1 + : HTTPSSVC_WITHOUT_IPHINT_RECEIVED_STAGE_1; + + nsCOMPtr svcbRecord = aHighestPriorityRecord; + if (!svcbRecord) { + LOG((" no usable record!")); + nsCOMPtr dns = do_GetService(NS_DNSSERVICE_CONTRACTID); + bool allRecordsExcluded = false; + Unused << record->GetAllRecordsExcluded(&allRecordsExcluded); + Telemetry::Accumulate(Telemetry::DNS_HTTPSSVC_CONNECTION_FAILED_REASON, + allRecordsExcluded + ? HTTPSSVC_CONNECTION_ALL_RECORDS_EXCLUDED + : HTTPSSVC_CONNECTION_NO_USABLE_RECORD); + if (allRecordsExcluded && + StaticPrefs::network_dns_httpssvc_reset_exclustion_list() && dns) { + Unused << dns->ResetExcludedSVCDomainName(mConnInfo->GetOrigin()); + if (NS_FAILED(record->GetServiceModeRecord(mCaps & NS_HTTP_DISALLOW_SPDY, + mCaps & NS_HTTP_DISALLOW_HTTP3, + getter_AddRefs(svcbRecord)))) { + return NS_ERROR_FAILURE; + } + } else { + return NS_ERROR_FAILURE; + } + } + + // Remember this RR set. In the case that the connection establishment failed, + // we will use other records to retry. + mHTTPSSVCRecord = record; + + RefPtr newInfo = + mConnInfo->CloneAndAdoptHTTPSSVCRecord(svcbRecord); + bool needFastFallback = !mConnInfo->IsHttp3() && newInfo->IsHttp3(); + if (!gHttpHandler->ConnMgr()->MoveTransToNewConnEntry(this, newInfo)) { + // MoveTransToNewConnEntry() returning fail means this transaction is + // not in the connection entry's pending queue. This could happen if + // OnLookupComplete() is called before this transaction is added in the + // queue. We still need to update the connection info, so this transaction + // can be added to the right connection entry. + UpdateConnectionInfo(newInfo); + } + + if (needFastFallback) { + CreateAndStartTimer( + mFastFallbackTimer, this, + StaticPrefs::network_dns_httpssvc_http3_fast_fallback_timeout()); + } + + // Prefetch the A/AAAA records of the target name. + nsAutoCString targetName; + Unused << svcbRecord->GetName(targetName); + if (mResolver) { + mResolver->PrefetchAddrRecord(targetName, mCaps & NS_HTTP_REFRESH_DNS); + } + + // echConfig is used, so initialize the retry counters to 0. + if (!mConnInfo->GetEchConfig().IsEmpty()) { + mEchRetryCounterMap.Put(Telemetry::TRANSACTION_ECH_RETRY_WITH_ECH_COUNT, 0); + mEchRetryCounterMap.Put(Telemetry::TRANSACTION_ECH_RETRY_WITHOUT_ECH_COUNT, + 0); + mEchRetryCounterMap.Put(Telemetry::TRANSACTION_ECH_RETRY_ECH_FAILED_COUNT, + 0); + mEchRetryCounterMap.Put(Telemetry::TRANSACTION_ECH_RETRY_OTHERS_COUNT, 0); + } + + return NS_OK; +} + +uint32_t nsHttpTransaction::HTTPSSVCReceivedStage() { + return mHTTPSSVCReceivedStage; +} + +void nsHttpTransaction::MaybeCancelFallbackTimer() { + if (mFastFallbackTimer) { + mFastFallbackTimer->Cancel(); + mFastFallbackTimer = nullptr; + } + + if (mHttp3BackupTimer) { + mHttp3BackupTimer->Cancel(); + mHttp3BackupTimer = nullptr; + } +} + +void nsHttpTransaction::OnBackupConnectionReady() { + LOG(("nsHttpTransaction::OnBackupConnectionReady [%p] mConnected=%d", this, + mConnected)); + if (mConnected || mClosed) { + return; + } + + HandleFallback(mBackupConnInfo); + + mCaps |= NS_HTTP_DISALLOW_HTTP3; + DebugOnly rv = + mRequestHead->SetHeader(nsHttp::Alternate_Service_Used, "0"_ns); + MOZ_ASSERT(NS_SUCCEEDED(rv)); +} + +void nsHttpTransaction::OnHttp3BackupTimer() { + LOG(("nsHttpTransaction::OnHttp3BackupTimer [%p]", this)); + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + MOZ_ASSERT(mConnInfo->IsHttp3()); + + mHttp3BackupTimer = nullptr; + + mConnInfo->CloneAsDirectRoute(getter_AddRefs(mBackupConnInfo)); + MOZ_ASSERT(!mBackupConnInfo->IsHttp3()); + + RefPtr self = this; + auto callback = [self](bool aSucceded) { + if (aSucceded) { + self->OnBackupConnectionReady(); + } + }; + + RefPtr trans = new SpeculativeTransaction( + mBackupConnInfo, mCallbacks, mCaps | NS_HTTP_DISALLOW_HTTP3, + std::move(callback)); + gHttpHandler->ConnMgr()->DoSpeculativeConnection(trans, false); +} + +void nsHttpTransaction::OnFastFallbackTimer() { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG(("nsHttpTransaction::OnFastFallbackTimer [%p] mConnected=%d", this, + mConnected)); + + mFastFallbackTimer = nullptr; + mFastFallbackTriggered = true; + + MOZ_ASSERT(mHTTPSSVCRecord && mOrigConnInfo); + if (!mHTTPSSVCRecord || !mOrigConnInfo) { + return; + } + + RefPtr fallbackConnInfo; + bool echConfigUsed = + gHttpHandler->EchConfigEnabled() && !mConnInfo->GetEchConfig().IsEmpty(); + fallbackConnInfo = PrepareFastFallbackConnInfo(echConfigUsed); + + // Need to backup the origin conn info, since UpdateConnectionInfo() will be + // called in HandleFallback() and mOrigConnInfo will be + // replaced. + RefPtr backup = mOrigConnInfo; + HandleFallback(fallbackConnInfo); + mOrigConnInfo.swap(backup); + + if (mResolver) { + mResolver->PrefetchAddrRecord(fallbackConnInfo->GetRoutedHost(), + mCaps & NS_HTTP_REFRESH_DNS); + } +} + +void nsHttpTransaction::HandleFallback( + nsHttpConnectionInfo* aFallbackConnInfo) { + if (mConnection) { + MOZ_ASSERT(mActivated); + // Close the transaction with NS_ERROR_NET_RESET, since we know doing this + // will make transaction to be restarted. + mConnection->CloseTransaction(this, NS_ERROR_NET_RESET); + return; + } + + if (!aFallbackConnInfo) { + // Nothing to do here. + return; + } + + LOG(("nsHttpTransaction %p HandleFallback to connInfo[%s]", this, + aFallbackConnInfo->HashKey().get())); + + bool foundInPendingQ = + gHttpHandler->ConnMgr()->MoveTransToNewConnEntry(this, aFallbackConnInfo); + if (!foundInPendingQ) { + MOZ_ASSERT(false, "transaction not in entry"); + return; + } + + // rewind streams in case we already wrote out the request + nsCOMPtr seekable = do_QueryInterface(mRequestStream); + if (seekable) { + seekable->Seek(nsISeekableStream::NS_SEEK_SET, 0); + } +} + +NS_IMETHODIMP +nsHttpTransaction::Notify(nsITimer* aTimer) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + if (!gHttpHandler || !gHttpHandler->ConnMgr()) { + return NS_OK; + } + + if (aTimer == mFastFallbackTimer) { + OnFastFallbackTimer(); + } else if (aTimer == mHttp3BackupTimer) { + OnHttp3BackupTimer(); + } + + return NS_OK; +} + +bool nsHttpTransaction::GetSupportsHTTP3() { return mSupportsHTTP3; } + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/nsHttpTransaction.h b/netwerk/protocol/http/nsHttpTransaction.h new file mode 100644 index 0000000000..52e4953de0 --- /dev/null +++ b/netwerk/protocol/http/nsHttpTransaction.h @@ -0,0 +1,545 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef nsHttpTransaction_h__ +#define nsHttpTransaction_h__ + +#include "nsHttp.h" +#include "nsAHttpTransaction.h" +#include "HttpTransactionShell.h" +#include "nsAHttpConnection.h" +#include "EventTokenBucket.h" +#include "nsCOMPtr.h" +#include "nsIAsyncOutputStream.h" +#include "nsThreadUtils.h" +#include "nsIInterfaceRequestor.h" +#include "nsIAsyncOutputStream.h" +#include "nsITimer.h" +#include "TimingStruct.h" +#include "Http2Push.h" +#include "mozilla/net/DNS.h" +#include "mozilla/net/NeckoChannelParams.h" +#include "ARefBase.h" + +//----------------------------------------------------------------------------- + +class nsIHttpActivityObserver; +class nsIDNSHTTPSSVCRecord; +class nsIEventTarget; +class nsIInputStream; +class nsIOutputStream; +class nsIRequestContext; +class nsISVCBRecord; + +namespace mozilla { +namespace net { + +class HTTPSRecordResolver; +class nsHttpChunkedDecoder; +class nsHttpHeaderArray; +class nsHttpRequestHead; +class nsHttpResponseHead; +class NullHttpTransaction; +class SpdyConnectTransaction; + +//----------------------------------------------------------------------------- +// nsHttpTransaction represents a single HTTP transaction. It is thread-safe, +// intended to run on the socket thread. +//----------------------------------------------------------------------------- + +class nsHttpTransaction final : public nsAHttpTransaction, + public HttpTransactionShell, + public ATokenBucketEvent, + public nsIInputStreamCallback, + public nsIOutputStreamCallback, + public ARefBase, + public nsITimerCallback { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSAHTTPTRANSACTION + NS_DECL_HTTPTRANSACTIONSHELL + NS_DECL_NSIINPUTSTREAMCALLBACK + NS_DECL_NSIOUTPUTSTREAMCALLBACK + NS_DECL_NSITIMERCALLBACK + + nsHttpTransaction(); + + void OnActivated() override; + + // attributes + nsHttpResponseHead* ResponseHead() { + return mHaveAllHeaders ? mResponseHead : nullptr; + } + + nsIEventTarget* ConsumerTarget() { return mConsumerTarget; } + + // Called to set/find out if the transaction generated a complete response. + void SetResponseIsComplete() { mResponseIsComplete = true; } + + void EnableKeepAlive() { mCaps |= NS_HTTP_ALLOW_KEEPALIVE; } + void MakeSticky() { mCaps |= NS_HTTP_STICKY_CONNECTION; } + void MakeNonSticky() override { mCaps &= ~NS_HTTP_STICKY_CONNECTION; } + + void MakeDontWaitHTTPSSVC() { mCaps &= ~NS_HTTP_WAIT_HTTPSSVC_RESULT; } + + // SetPriority() may only be used by the connection manager. + void SetPriority(int32_t priority) { mPriority = priority; } + int32_t Priority() { return mPriority; } + + void PrintDiagnostics(nsCString& log); + + // Sets mPendingTime to the current time stamp or to a null time stamp (if now + // is false) + void SetPendingTime(bool now = true) { + if (!now && !mPendingTime.IsNull()) { + // Remember how long it took. We will use this vaule to record + // TRANSACTION_WAIT_TIME_HTTP2_SUP_HTTP3 telemetry, but we need to wait + // for the response headers. + mPendingDurationTime = TimeStamp::Now() - mPendingTime; + } + mPendingTime = now ? TimeStamp::Now() : TimeStamp(); + } + const TimeStamp GetPendingTime() { return mPendingTime; } + + // overload of nsAHttpTransaction::RequestContext() + nsIRequestContext* RequestContext() override { return mRequestContext.get(); } + void DispatchedAsBlocking(); + void RemoveDispatchedAsBlocking(); + + void DisableSpdy() override; + void DisableHttp3() override; + + nsHttpTransaction* QueryHttpTransaction() override { return this; } + + already_AddRefed GetPushedStream() { + return do_AddRef(mPushedStream); + } + already_AddRefed TakePushedStream() { + return mPushedStream.forget(); + } + + uint32_t InitialRwin() const { return mInitialRwin; }; + bool ChannelPipeFull() { return mWaitingOnPipeOut; } + + // Locked methods to get and set timing info + void BootstrapTimings(TimingStruct times); + void SetConnectStart(mozilla::TimeStamp timeStamp, bool onlyIfNull = false); + void SetConnectEnd(mozilla::TimeStamp timeStamp, bool onlyIfNull = false); + void SetRequestStart(mozilla::TimeStamp timeStamp, bool onlyIfNull = false); + void SetResponseStart(mozilla::TimeStamp timeStamp, bool onlyIfNull = false); + void SetResponseEnd(mozilla::TimeStamp timeStamp, bool onlyIfNull = false); + + [[nodiscard]] bool Do0RTT() override; + [[nodiscard]] nsresult Finish0RTT(bool aRestart, + bool aAlpnChanged /* ignored */) override; + + // After Finish0RTT early data may have failed but the caller did not request + // restart - this indicates that state for dev tools + void Refused0RTT(); + + [[nodiscard]] bool CanDo0RTT() override; + [[nodiscard]] nsresult RestartOnFastOpenError() override; + + uint64_t TopLevelOuterContentWindowId() override { + return mTopLevelOuterContentWindowId; + } + + void SetFastOpenStatus(uint8_t aStatus) override; + + void SetHttpTrailers(nsCString& aTrailers); + + bool IsWebsocketUpgrade(); + void SetH2WSTransaction(SpdyConnectTransaction*); + + void OnProxyConnectComplete(int32_t aResponseCode) override; + void SetFlat407Headers(const nsACString& aHeaders); + + // This is only called by Http2PushedStream::TryOnPush when a new pushed + // stream is available. The newly added stream will be taken by another + // transaction. + void OnPush(Http2PushedStreamWrapper* aStream); + + void UpdateConnectionInfo(nsHttpConnectionInfo* aConnInfo); + + void SetClassOfService(uint32_t cos); + + virtual nsresult OnHTTPSRRAvailable( + nsIDNSHTTPSSVCRecord* aHTTPSSVCRecord, + nsISVCBRecord* aHighestPriorityRecord) override; + + private: + friend class DeleteHttpTransaction; + virtual ~nsHttpTransaction(); + + [[nodiscard]] nsresult Restart(); + char* LocateHttpStart(char* buf, uint32_t len, bool aAllowPartialMatch); + [[nodiscard]] nsresult ParseLine(nsACString& line); + [[nodiscard]] nsresult ParseLineSegment(char* seg, uint32_t len); + [[nodiscard]] nsresult ParseHead(char*, uint32_t count, uint32_t* countRead); + [[nodiscard]] nsresult HandleContentStart(); + [[nodiscard]] nsresult HandleContent(char*, uint32_t count, + uint32_t* contentRead, + uint32_t* contentRemaining); + [[nodiscard]] nsresult ProcessData(char*, uint32_t, uint32_t*); + void DeleteSelfOnConsumerThread(); + void ReleaseBlockingTransaction(); + + [[nodiscard]] static nsresult ReadRequestSegment(nsIInputStream*, void*, + const char*, uint32_t, + uint32_t, uint32_t*); + [[nodiscard]] static nsresult WritePipeSegment(nsIOutputStream*, void*, char*, + uint32_t, uint32_t, uint32_t*); + + bool TimingEnabled() const { return mCaps & NS_HTTP_TIMING_ENABLED; } + + bool ResponseTimeoutEnabled() const final; + + void ReuseConnectionOnRestartOK(bool reuseOk) override { + mReuseOnRestart = reuseOk; + } + + // Called right after we parsed the response head. Checks for connection + // based authentication schemes in reponse headers for WWW and Proxy + // authentication. If such is found in any of them, NS_HTTP_STICKY_CONNECTION + // is set in mCaps. We need the sticky flag be set early to keep the + // connection from very start of the authentication process. + void CheckForStickyAuthScheme(); + void CheckForStickyAuthSchemeAt(nsHttpAtom const& header); + bool IsStickyAuthSchemeAt(nsACString const& auth); + + // Called from WriteSegments. Checks for conditions whether to throttle + // reading the content. When this returns true, WriteSegments returns + // WOULD_BLOCK. + bool ShouldThrottle(); + + void NotifyTransactionObserver(nsresult reason); + + // When echConfig is enabled, this function put other available records + // in mRecordsForRetry. Returns true when mRecordsForRetry is not empty, + // otherwise returns false. + bool PrepareSVCBRecordsForRetry(const nsACString& aFailedDomainName, + bool& aAllRecordsHaveEchConfig); + // This function setups a new connection info for restarting this transaction. + void PrepareConnInfoForRetry(nsresult aReason); + // This function is used to select the next non http3 record and is only + // executed when the fast fallback timer is triggered. + already_AddRefed PrepareFastFallbackConnInfo( + bool aEchConfigUsed); + + void MaybeReportFailedSVCDomain(nsresult aReason, + nsHttpConnectionInfo* aFailedConnInfo); + + already_AddRefed TakePushedStreamById( + uint32_t aStreamId); + + // IMPORTANT: when adding new values, always add them to the end, otherwise + // it will mess up telemetry. + enum HTTPSSVC_CONNECTION_FAILED_REASON : uint32_t { + HTTPSSVC_CONNECTION_OK = 0, + HTTPSSVC_CONNECTION_UNKNOWN_HOST = 1, + HTTPSSVC_CONNECTION_UNREACHABLE = 2, + HTTPSSVC_CONNECTION_421_RECEIVED = 3, + HTTPSSVC_CONNECTION_SECURITY_ERROR = 4, + HTTPSSVC_CONNECTION_NO_USABLE_RECORD = 5, + HTTPSSVC_CONNECTION_ALL_RECORDS_EXCLUDED = 6, + HTTPSSVC_CONNECTION_OTHERS = 7, + }; + HTTPSSVC_CONNECTION_FAILED_REASON ErrorCodeToFailedReason( + nsresult aErrorCode); + + void OnHttp3BackupTimer(); + void OnBackupConnectionReady(); + void OnFastFallbackTimer(); + void HandleFallback(nsHttpConnectionInfo* aFallbackConnInfo); + void MaybeCancelFallbackTimer(); + + private: + class UpdateSecurityCallbacks : public Runnable { + public: + UpdateSecurityCallbacks(nsHttpTransaction* aTrans, + nsIInterfaceRequestor* aCallbacks) + : Runnable("net::nsHttpTransaction::UpdateSecurityCallbacks"), + mTrans(aTrans), + mCallbacks(aCallbacks) {} + + NS_IMETHOD Run() override { + if (mTrans->mConnection) + mTrans->mConnection->SetSecurityCallbacks(mCallbacks); + return NS_OK; + } + + private: + RefPtr mTrans; + nsCOMPtr mCallbacks; + }; + + Mutex mLock; + + nsCOMPtr mCallbacks; + nsCOMPtr mTransportSink; + nsCOMPtr mConsumerTarget; + nsCOMPtr mSecurityInfo; + nsCOMPtr mPipeIn; + nsCOMPtr mPipeOut; + nsCOMPtr mRequestContext; + + uint64_t mChannelId; + nsCOMPtr mActivityDistributor; + + nsCString mReqHeaderBuf; // flattened request headers + nsCOMPtr mRequestStream; + int64_t mRequestSize; + + RefPtr mConnection; + RefPtr mConnInfo; + // This is only set in UpdateConnectionInfo() when we have received a SVCB RR. + // When echConfig is not used and the connection is failed, this transaction + // will be restarted with this origin connection info directly. + // When echConfig is enabled, there are two cases below. + // 1. If all records have echConfig, we will retry other records except the + // failed one. In the case all other records with echConfig are failed and the + // pref network.dns.echconfig.fallback_to_origin_when_all_failed is true, this + // origin connection info will be used. + // 2. If only some records have echConfig and some not, we always fallback to + // this origin conn info. + RefPtr mOrigConnInfo; + nsHttpRequestHead* mRequestHead; // weak ref + nsHttpResponseHead* mResponseHead; // owning pointer + + nsAHttpSegmentReader* mReader; + nsAHttpSegmentWriter* mWriter; + + nsCString mLineBuf; // may contain a partial line + + int64_t mContentLength; // equals -1 if unknown + int64_t mContentRead; // count of consumed content bytes + Atomic mTransferSize; // count of received bytes + + // After a 304/204 or other "no-content" style response we will skip over + // up to MAX_INVALID_RESPONSE_BODY_SZ bytes when looking for the next + // response header to deal with servers that actually sent a response + // body where they should not have. This member tracks how many bytes have + // so far been skipped. + uint32_t mInvalidResponseBytesRead; + + RefPtr mPushedStream; + uint32_t mInitialRwin; + + nsHttpChunkedDecoder* mChunkedDecoder; + + TimingStruct mTimings; + + nsresult mStatus; + + int16_t mPriority; + + uint16_t + mRestartCount; // the number of times this transaction has been restarted + uint32_t mCaps; + + HttpVersion mHttpVersion; + uint16_t mHttpResponseCode; + nsCString mFlat407Headers; + + uint32_t mCurrentHttpResponseHeaderSize; + + int32_t const THROTTLE_NO_LIMIT = -1; + // This can have 3 possible values: + // * THROTTLE_NO_LIMIT - this means the transaction is not in any way limited + // to read the response, this is the default + // * a positive number - a limit is set because the transaction is obligated + // to throttle the response read, this is decresed with + // every piece of data the transaction receives + // * zero - when the transaction depletes the limit for reading, this makes it + // stop reading and return WOULD_BLOCK from WriteSegments; + // transaction then waits for a call of ResumeReading that resets + // this member back to THROTTLE_NO_LIMIT + int32_t mThrottlingReadAllowance; + + // mCapsToClear holds flags that should be cleared in mCaps, e.g. unset + // NS_HTTP_REFRESH_DNS when DNS refresh request has completed to avoid + // redundant requests on the network. The member itself is atomic, but + // access to it from the networking thread may happen either before or + // after the main thread modifies it. To deal with raciness, only unsetting + // bitfields should be allowed: 'lost races' will thus err on the + // conservative side, e.g. by going ahead with a 2nd DNS refresh. + Atomic mCapsToClear; + Atomic mResponseIsComplete; + Atomic mClosed; + + // True iff WriteSegments was called while this transaction should be + // throttled (stop reading) Used to resume read on unblock of reading. Conn + // manager is responsible for calling back to resume reading. + bool mReadingStopped; + + // state flags, all logically boolean, but not packed together into a + // bitfield so as to avoid bitfield-induced races. See bug 560579. + bool mConnected; + bool mActivated; + bool mHaveStatusLine; + bool mHaveAllHeaders; + bool mTransactionDone; + bool mDidContentStart; + bool mNoContent; // expecting an empty entity body + bool mSentData; + bool mReceivedData; + bool mStatusEventPending; + bool mHasRequestBody; + bool mProxyConnectFailed; + bool mHttpResponseMatched; + bool mPreserveStream; + bool mDispatchedAsBlocking; + bool mResponseTimeoutEnabled; + bool mForceRestart; + bool mReuseOnRestart; + bool mContentDecoding; + bool mContentDecodingCheck; + bool mDeferredSendProgress; + bool mWaitingOnPipeOut; + + bool mIsHttp3Used = false; + bool mDoNotRemoveAltSvc; + + // mClosed := transaction has been explicitly closed + // mTransactionDone := transaction ran to completion or was interrupted + // mResponseComplete := transaction ran to completion + + // For Restart-In-Progress Functionality + bool mReportedStart; + bool mReportedResponseHeader; + + // protected by nsHttp::GetLock() + bool mResponseHeadTaken; + UniquePtr mForTakeResponseTrailers; + bool mResponseTrailersTaken; + + // Set when this transaction was restarted by call to Restart(). Used to tell + // the http channel to reset proxy authentication. + Atomic mRestarted; + + // The time when the transaction was submitted to the Connection Manager + TimeStamp mPendingTime; + TimeDuration mPendingDurationTime; + + uint64_t mTopLevelOuterContentWindowId; + + // For Rate Pacing via an EventTokenBucket + public: + // called by the connection manager to run this transaction through the + // token bucket. If the token bucket admits the transaction immediately it + // returns true. The function is called repeatedly until it returns true. + bool TryToRunPacedRequest(); + + // ATokenBucketEvent pure virtual implementation. Called by the token bucket + // when the transaction is ready to run. If this happens asynchrounously to + // token bucket submission the transaction just posts an event that causes + // the pending transaction queue to be rerun (and TryToRunPacedRequest() to + // be run again. + void OnTokenBucketAdmitted() override; // ATokenBucketEvent + + // CancelPacing() can be used to tell the token bucket to remove this + // transaction from the list of pending transactions. This is used when a + // transaction is believed to be HTTP/1 (and thus subject to rate pacing) + // but later can be dispatched via spdy (not subject to rate pacing). + void CancelPacing(nsresult reason); + + // Called by the connetion manager on the socket thread when reading for this + // previously throttled transaction has to be resumed. + void ResumeReading(); + + // This examins classification of this transaction whether the Throttleable + // class has been set while Leader, Unblocked, DontThrottle has not. + bool EligibleForThrottling() const; + + private: + bool mSubmittedRatePacing; + bool mPassedRatePacing; + bool mSynchronousRatePaceRequest; + nsCOMPtr mTokenBucketCancel; + + public: + uint32_t ClassOfService() { return mClassOfService; } + + private: + Atomic mClassOfService; + + public: + // setting TunnelProvider to non-null means the transaction should only + // be dispatched on a specific ConnectionInfo Hash Key (as opposed to a + // generic wild card one). That means in the specific case of carrying this + // transaction on an HTTP/2 tunnel it will only be dispatched onto an + // existing tunnel instead of triggering creation of a new one. + // The tunnel provider is used for ASpdySession::MaybeReTunnel() checks. + + void SetTunnelProvider(ASpdySession* provider) { mTunnelProvider = provider; } + ASpdySession* TunnelProvider() { return mTunnelProvider; } + nsIInterfaceRequestor* SecurityCallbacks() { return mCallbacks; } + // Called when this transaction is inserted in the pending queue. + void OnPendingQueueInserted(); + + private: + RefPtr mTunnelProvider; + TransactionObserverFunc mTransactionObserver; + NetAddr mSelfAddr; + NetAddr mPeerAddr; + bool mResolvedByTRR; + bool mEchConfigUsed = false; + + bool m0RTTInProgress; + bool mDoNotTryEarlyData; + enum { + EARLY_NONE, + EARLY_SENT, + EARLY_ACCEPTED, + EARLY_425 + } mEarlyDataDisposition; + + uint8_t mFastOpenStatus; + + // H2 websocket support + RefPtr mH2WSTransaction; + + HttpTrafficCategory mTrafficCategory; + bool mThroughCaptivePortal; + Atomic mProxyConnectResponseCode; + + OnPushCallback mOnPushCallback; + nsDataHashtable> + mIDToStreamMap; + + nsCOMPtr mDNSRequest; + Atomic mHTTPSSVCReceivedStage; + bool m421Received = false; + nsCOMPtr mHTTPSSVCRecord; + nsTArray> mRecordsForRetry; + bool mDontRetryWithDirectRoute = false; + bool mFastFallbackTriggered = false; + bool mAllRecordsInH3ExcludedListBefore = false; + bool mHttp3BackupTimerCreated = false; + nsCOMPtr mFastFallbackTimer; + nsCOMPtr mHttp3BackupTimer; + RefPtr mBackupConnInfo; + RefPtr mResolver; + + // IMPORTANT: when adding new values, always add them to the end, otherwise + // it will mess up telemetry. + enum TRANSACTION_RESTART_REASON : uint32_t { + TRANSACTION_RESTART_NONE = 0, // The transacion was not restarted. + TRANSACTION_RESTART_FORCED = 1, // The transaction was forced to restart. + TRANSACTION_RESTART_HTTPSSVC_INVOLVED = 2, + TRANSACTION_RESTART_NO_DATA_SENT = 3, + TRANSACTION_RESTART_DOWNGRADE_WITH_EARLY_DATA = 4, + TRANSACTION_RESTART_OTHERS = 5, + }; + + nsDataHashtable mEchRetryCounterMap; + + bool mSupportsHTTP3 = false; +}; + +} // namespace net +} // namespace mozilla + +#endif // nsHttpTransaction_h__ diff --git a/netwerk/protocol/http/nsIBackgroundChannelRegistrar.idl b/netwerk/protocol/http/nsIBackgroundChannelRegistrar.idl new file mode 100644 index 0000000000..1368125450 --- /dev/null +++ b/netwerk/protocol/http/nsIBackgroundChannelRegistrar.idl @@ -0,0 +1,63 @@ +/* -*- 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 "nsISupports.idl" + +%{ C++ +namespace mozilla { +namespace net { +class HttpBackgroundChannelParent; +class HttpChannelParent; +} +} +%} + +[ptr] native HttpChannelParent(mozilla::net::HttpChannelParent); +[ptr] native HttpBackgroundChannelParent(mozilla::net::HttpBackgroundChannelParent); + +/* + * Registrar for pairing HttpChannelParent and HttpBackgroundChannelParent via + * channel Id. HttpChannelParent::OnBackgroundParentReady and + * HttpBackgroundChannelParent::LinkToChannel will be invoked to notify the + * existence of associated channel object. + */ +[builtinclass, uuid(8acaa9b1-f0c4-4ade-baeb-39b0d4b96e5b)] +interface nsIBackgroundChannelRegistrar : nsISupports +{ + /* + * Link the provided channel parent actor with the given channel Id. + * callbacks will be invoked immediately when the HttpBackgroundChannelParent + * associated with the same channel Id is found. Store the HttpChannelParent + * until a matched linkBackgroundChannel is invoked. + * + * @param aKey the channel Id + * @param aChannel the channel parent actor to be paired + */ + [noscript,notxpcom,nostdcall] void linkHttpChannel(in uint64_t aKey, + in HttpChannelParent aChannel); + + /* + * Link the provided background channel with the given channel Id. + * callbacks will be invoked immediately when the HttpChannelParent associated + * with the same channel Id is found. Store the HttpBackgroundChannelParent + * until a matched linkHttpChannel is invoked. + * + * @param aKey the channel Id + * @param aBgChannel the background channel to be paired + */ + [noscript,notxpcom,nostdcall] void linkBackgroundChannel(in uint64_t aKey, + in HttpBackgroundChannelParent aBgChannel); + + /* + * Delete previous stored HttpChannelParent or HttpBackgroundChannelParent + * if no need to wait for the paired channel object, e.g. background channel + * is destroyed before pairing is completed. + * + * @param aKey the channel Id + */ + [noscript,notxpcom,nostdcall] void deleteChannel(in uint64_t aKey); + +}; diff --git a/netwerk/protocol/http/nsICorsPreflightCallback.h b/netwerk/protocol/http/nsICorsPreflightCallback.h new file mode 100644 index 0000000000..aa16dbf12d --- /dev/null +++ b/netwerk/protocol/http/nsICorsPreflightCallback.h @@ -0,0 +1,31 @@ +/* -*- 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/. */ + +#ifndef nsICorsPreflightCallback_h__ +#define nsICorsPreflightCallback_h__ + +#include "nsISupports.h" +#include "nsID.h" +#include "nsError.h" + +#define NS_ICORSPREFLIGHTCALLBACK_IID \ + { \ + 0x3758cfbb, 0x259f, 0x4074, { \ + 0xa8, 0xc0, 0x98, 0xe0, 0x4b, 0x3c, 0xc0, 0xe3 \ + } \ + } + +class nsICorsPreflightCallback : public nsISupports { + public: + NS_DECLARE_STATIC_IID_ACCESSOR(nsICorsPreflightCallback); + NS_IMETHOD OnPreflightSucceeded() = 0; + NS_IMETHOD OnPreflightFailed(nsresult aError) = 0; +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(nsICorsPreflightCallback, + NS_ICORSPREFLIGHTCALLBACK_IID); + +#endif diff --git a/netwerk/protocol/http/nsIHttpActivityObserver.idl b/netwerk/protocol/http/nsIHttpActivityObserver.idl new file mode 100644 index 0000000000..d3286e2354 --- /dev/null +++ b/netwerk/protocol/http/nsIHttpActivityObserver.idl @@ -0,0 +1,162 @@ +/* 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 "nsISupports.idl" + +%{ C++ +namespace mozilla { +namespace net { +class HttpActivityArgs; +} // namespace net +} // namespace mozilla +%} + +[ref] native HttpActivityArgs(const mozilla::net::HttpActivityArgs); + +/** + * nsIHttpActivityObserver + * + * This interface provides a way for http activities to be reported + * to observers. + */ +[scriptable, uuid(412880C8-6C36-48d8-BF8F-84F91F892503)] +interface nsIHttpActivityObserver : nsISupports +{ + /** + * observe activity from the http transport + * + * @param aHttpChannel + * nsISupports interface for the the http channel that + * generated this activity + * @param aActivityType + * The value of this aActivityType will be one of + * ACTIVITY_TYPE_SOCKET_TRANSPORT or + * ACTIVITY_TYPE_HTTP_TRANSACTION + * @param aActivitySubtype + * The value of this aActivitySubtype, will be depend + * on the value of aActivityType. When aActivityType + * is ACTIVITY_TYPE_SOCKET_TRANSPORT + * aActivitySubtype will be one of the + * nsISocketTransport::STATUS_???? values defined in + * nsISocketTransport.idl + * OR when aActivityType + * is ACTIVITY_TYPE_HTTP_TRANSACTION + * aActivitySubtype will be one of the + * nsIHttpActivityObserver::ACTIVITY_SUBTYPE_???? values + * defined below + * @param aTimestamp + * microseconds past the epoch of Jan 1, 1970 + * @param aExtraSizeData + * Any extra size data optionally available with + * this activity + * @param aExtraStringData + * Any extra string data optionally available with + * this activity + */ + [must_use] + void observeActivity(in nsISupports aHttpChannel, + in uint32_t aActivityType, + in uint32_t aActivitySubtype, + in PRTime aTimestamp, + in uint64_t aExtraSizeData, + in ACString aExtraStringData); + + /** + * This attribute is true when this interface is active and should + * observe http activities. When false, observeActivity() should not + * be called. It is present for compatibility reasons and should be + * implemented only by nsHttpActivityDistributor. + */ + [must_use] readonly attribute boolean isActive; + + /** + * This function is for internal use only. Every time a http transaction + * is created in socket process, we use this function to set the value of + * |isActive|. We need this since the real value of |isActive| is + * only available in parent process. + */ + [noscript] void setIsActive(in boolean aActived); + + /** + * This function is used when the real http channel is not available. + * We use the information in |HttpActivityArgs| to get the http channel or + * create a |NullHttpChannel|. + * + * @param aArgs + * See the definition of |HttpActivityArgs| in PSocketProcess.ipdl. + */ + [noscript, must_use] + void observeActivityWithArgs(in HttpActivityArgs aArgs, + in uint32_t aActivityType, + in uint32_t aActivitySubtype, + in PRTime aTimestamp, + in uint64_t aExtraSizeData, + in ACString aExtraStringData); + + const unsigned long ACTIVITY_TYPE_SOCKET_TRANSPORT = 0x0001; + const unsigned long ACTIVITY_TYPE_HTTP_TRANSACTION = 0x0002; + + const unsigned long ACTIVITY_SUBTYPE_REQUEST_HEADER = 0x5001; + const unsigned long ACTIVITY_SUBTYPE_REQUEST_BODY_SENT = 0x5002; + const unsigned long ACTIVITY_SUBTYPE_RESPONSE_START = 0x5003; + const unsigned long ACTIVITY_SUBTYPE_RESPONSE_HEADER = 0x5004; + const unsigned long ACTIVITY_SUBTYPE_RESPONSE_COMPLETE = 0x5005; + const unsigned long ACTIVITY_SUBTYPE_TRANSACTION_CLOSE = 0x5006; + + /** + * When aActivityType is ACTIVITY_TYPE_SOCKET_TRANSPORT + * and aActivitySubtype is STATUS_SENDING_TO + * aExtraSizeData will contain the count of bytes sent + * There may be more than one of these activities reported + * for a single http transaction, each aExtraSizeData + * represents only that portion of the total bytes sent + * + * When aActivityType is ACTIVITY_TYPE_HTTP_TRANSACTION + * and aActivitySubtype is ACTIVITY_SUBTYPE_REQUEST_HEADER + * aExtraStringData will contain the text of the header + * + * When aActivityType is ACTIVITY_TYPE_HTTP_TRANSACTION + * and aActivitySubtype is ACTIVITY_SUBTYPE_RESPONSE_HEADER + * aExtraStringData will contain the text of the header + * + * When aActivityType is ACTIVITY_TYPE_HTTP_TRANSACTION + * and aActivitySubtype is ACTIVITY_SUBTYPE_RESPONSE_COMPLETE + * aExtraSizeData will contain the count of total bytes received + */ +}; + +%{C++ + +#define NS_HTTP_ACTIVITY_TYPE_SOCKET_TRANSPORT \ + nsIHttpActivityObserver::ACTIVITY_TYPE_SOCKET_TRANSPORT +#define NS_HTTP_ACTIVITY_TYPE_HTTP_TRANSACTION \ + nsIHttpActivityObserver::ACTIVITY_TYPE_HTTP_TRANSACTION + +#define NS_HTTP_ACTIVITY_SUBTYPE_REQUEST_HEADER \ + nsIHttpActivityObserver::ACTIVITY_SUBTYPE_REQUEST_HEADER +#define NS_HTTP_ACTIVITY_SUBTYPE_REQUEST_BODY_SENT \ + nsIHttpActivityObserver::ACTIVITY_SUBTYPE_REQUEST_BODY_SENT +#define NS_HTTP_ACTIVITY_SUBTYPE_RESPONSE_START \ + nsIHttpActivityObserver::ACTIVITY_SUBTYPE_RESPONSE_START +#define NS_HTTP_ACTIVITY_SUBTYPE_RESPONSE_HEADER \ + nsIHttpActivityObserver::ACTIVITY_SUBTYPE_RESPONSE_HEADER +#define NS_HTTP_ACTIVITY_SUBTYPE_RESPONSE_COMPLETE \ + nsIHttpActivityObserver::ACTIVITY_SUBTYPE_RESPONSE_COMPLETE +#define NS_HTTP_ACTIVITY_SUBTYPE_TRANSACTION_CLOSE \ + nsIHttpActivityObserver::ACTIVITY_SUBTYPE_TRANSACTION_CLOSE + +%} + +/** + * nsIHttpActivityDistributor + * + * This interface provides a way to register and unregister observers to the + * http activities. + */ +[scriptable, builtinclass, uuid(7C512CB8-582A-4625-B5B6-8639755271B5)] +interface nsIHttpActivityDistributor : nsIHttpActivityObserver +{ + void addObserver(in nsIHttpActivityObserver aObserver); + void removeObserver(in nsIHttpActivityObserver aObserver); +}; diff --git a/netwerk/protocol/http/nsIHttpAuthManager.idl b/netwerk/protocol/http/nsIHttpAuthManager.idl new file mode 100644 index 0000000000..5acd652942 --- /dev/null +++ b/netwerk/protocol/http/nsIHttpAuthManager.idl @@ -0,0 +1,115 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* 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 "nsISupports.idl" + +interface nsIPrincipal; + +/** + * nsIHttpAuthManager + * + * This service provides access to cached HTTP authentication + * user credentials (domain, username, password) for sites + * visited during the current browser session. + * + * This interface exists to provide other HTTP stacks with the + * ability to share HTTP authentication credentials with Necko. + * This is currently used by the Java plugin (version 1.5 and + * higher) to avoid duplicate authentication prompts when the + * Java client fetches content from a HTTP site that the user + * has already logged into. + */ +[scriptable, builtinclass, uuid(54f90444-c52b-4d2d-8916-c59a2bb25938)] +interface nsIHttpAuthManager : nsISupports +{ + /** + * Lookup auth identity. + * + * @param aScheme + * the URL scheme (e.g., "http"). NOTE: for proxy authentication, + * this should be "http" (this includes authentication for CONNECT + * tunneling). + * @param aHost + * the host of the server issuing a challenge (ASCII only). + * @param aPort + * the port of the server issuing a challenge. + * @param aAuthType + * optional string identifying auth type used (e.g., "basic") + * @param aRealm + * optional string identifying auth realm. + * @param aPath + * optional string identifying auth path. empty for proxy auth. + * @param aUserDomain + * return value containing user domain. + * @param aUserName + * return value containing user name. + * @param aUserPassword + * return value containing user password. + * @param aIsPrivate + * whether to look up a private or public identity (they are + * stored separately, for use by private browsing) + * @param aPrincipal + * the principal from which to derive information about which + * app/mozbrowser is in use for this request + */ + [must_use] void getAuthIdentity(in ACString aScheme, + in ACString aHost, + in int32_t aPort, + in ACString aAuthType, + in ACString aRealm, + in ACString aPath, + out AString aUserDomain, + out AString aUserName, + out AString aUserPassword, + [optional] in bool aIsPrivate, + [optional] in nsIPrincipal aPrincipal); + + /** + * Store auth identity. + * + * @param aScheme + * the URL scheme (e.g., "http"). NOTE: for proxy authentication, + * this should be "http" (this includes authentication for CONNECT + * tunneling). + * @param aHost + * the host of the server issuing a challenge (ASCII only). + * @param aPort + * the port of the server issuing a challenge. + * @param aAuthType + * optional string identifying auth type used (e.g., "basic") + * @param aRealm + * optional string identifying auth realm. + * @param aPath + * optional string identifying auth path. empty for proxy auth. + * @param aUserDomain + * optional string containing user domain. + * @param aUserName + * optional string containing user name. + * @param aUserPassword + * optional string containing user password. + * @param aIsPrivate + * whether to store a private or public identity (they are + * stored separately, for use by private browsing) + * @param aPrincipal + * the principal from which to derive information about which + * app/mozbrowser is in use for this request + */ + [must_use] void setAuthIdentity(in ACString aScheme, + in ACString aHost, + in int32_t aPort, + in ACString aAuthType, + in ACString aRealm, + in ACString aPath, + in AString aUserDomain, + in AString aUserName, + in AString aUserPassword, + [optional] in boolean aIsPrivate, + [optional] in nsIPrincipal aPrincipal); + + /** + * Clear all auth cache. + */ + [must_use] void clearAll(); +}; diff --git a/netwerk/protocol/http/nsIHttpAuthenticableChannel.idl b/netwerk/protocol/http/nsIHttpAuthenticableChannel.idl new file mode 100644 index 0000000000..17ad88773d --- /dev/null +++ b/netwerk/protocol/http/nsIHttpAuthenticableChannel.idl @@ -0,0 +1,122 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* 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 "nsIProxiedChannel.idl" +#include "nsIRequest.idl" + +interface nsILoadGroup; +interface nsIURI; +interface nsIInterfaceRequestor; + +[builtinclass, uuid(701093ac-5c7f-429c-99e3-423b041fccb4)] +interface nsIHttpAuthenticableChannel : nsIProxiedChannel +{ + /** + * If the channel being authenticated is using SSL. + */ + [must_use] readonly attribute boolean isSSL; + + /** + * Returns if the proxy HTTP method used is CONNECT. If no proxy is being + * used it must return PR_FALSE. + */ + [must_use] readonly attribute boolean proxyMethodIsConnect; + + /** + * Cancels the current request. See nsIRequest. + */ + [must_use] void cancel(in nsresult aStatus); + + /** + * The load flags of this request. See nsIRequest. + */ + [must_use] readonly attribute nsLoadFlags loadFlags; + + /** + * The URI corresponding to the channel. See nsIChannel. + */ + [must_use] readonly attribute nsIURI URI; + + /** + * The load group of this request. It is here for querying its + * notificationCallbacks. See nsIRequest. + */ + [must_use] readonly attribute nsILoadGroup loadGroup; + + /** + * The notification callbacks for the channel. See nsIChannel. + */ + [must_use] readonly attribute nsIInterfaceRequestor notificationCallbacks; + + /** + * The HTTP request method. See nsIHttpChannel. + */ + [must_use] readonly attribute ACString requestMethod; + + /** + * The "Server" response header. + * Return NS_ERROR_NOT_AVAILABLE if not available. + */ + [must_use] readonly attribute ACString serverResponseHeader; + + /** + * The Proxy-Authenticate response header. + */ + [must_use] readonly attribute ACString proxyChallenges; + + /** + * The WWW-Authenticate response header. + */ + [must_use] readonly attribute ACString WWWChallenges; + + /** + * Sets the Proxy-Authorization request header. An empty string + * will clear it. + */ + [must_use] void setProxyCredentials(in ACString credentials); + + /** + * Sets the Authorization request header. An empty string + * will clear it. + */ + [must_use] void setWWWCredentials(in ACString credentials); + + /** + * Called when authentication information is ready and has been set on this + * object using setWWWCredentials/setProxyCredentials. Implementations can + * continue with the request and send the given information to the server. + * + * It is called asynchronously from + * nsIHttpChannelAuthProvider::processAuthentication if that method returns + * NS_ERROR_IN_PROGRESS. + * + * @note Any exceptions thrown from this method should be ignored. + */ + [must_use] void onAuthAvailable(); + + /** + * Notifies that the prompt was cancelled. It is called asynchronously + * from nsIHttpChannelAuthProvider::processAuthentication if that method + * returns NS_ERROR_IN_PROGRESS. + * + * @param userCancel + * If the user was cancelled has cancelled the authentication prompt. + */ + [must_use] void onAuthCancelled(in boolean userCancel); + + /** + * Tells the channel to drop and close any sticky connection, since this + * connection oriented schema cannot be negotiated second time on + * the same connection. + */ + [must_use] void closeStickyConnection(); + + /** + * Tells the channel to mark the connection as allowed to restart on + * authentication retry. This is allowed when the request is a start + * of a new authentication round. + */ + void connectionRestartable(in boolean restartable); +}; diff --git a/netwerk/protocol/http/nsIHttpAuthenticator.idl b/netwerk/protocol/http/nsIHttpAuthenticator.idl new file mode 100644 index 0000000000..f9e19bed7d --- /dev/null +++ b/netwerk/protocol/http/nsIHttpAuthenticator.idl @@ -0,0 +1,221 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* 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 "nsISupports.idl" + +interface nsIHttpAuthenticableChannel; +interface nsIHttpAuthenticatorCallback; +interface nsICancelable; + +/** + * nsIHttpAuthenticator + * + * Interface designed to allow for pluggable HTTP authentication modules. + * Implementations are registered under the ContractID: + * + * "@mozilla.org/network/http-authenticator;1?scheme=" + * + * where is the lower-cased value of the authentication scheme + * found in the server challenge per the rules of RFC 2617. + */ +[builtinclass, uuid(fef7db8a-a4e2-49d1-9685-19ed7e309b7d)] +interface nsIHttpAuthenticator : nsISupports +{ + /** + * Upon receipt of a server challenge, this function is called to determine + * whether or not the current user identity has been rejected. If true, + * then the user will be prompted by the channel to enter (or revise) their + * identity. Following this, generateCredentials will be called. + * + * If the IDENTITY_IGNORED auth flag is set, then the aInvalidateIdentity + * return value will be ignored, and user prompting will be suppressed. + * + * @param aChannel + * the http channel that received the challenge. + * @param aChallenge + * the challenge from the WWW-Authenticate/Proxy-Authenticate + * server response header. (possibly from the auth cache.) + * @param aProxyAuth + * flag indicating whether or not aChallenge is from a proxy. + * @param aSessionState + * see description below for generateCredentials. + * @param aContinuationState + * see description below for generateCredentials. + * @param aInvalidateIdentity + * return value indicating whether or not to prompt the user for a + * revised identity. + */ + [must_use] + void challengeReceived(in nsIHttpAuthenticableChannel aChannel, + in string aChallenge, + in boolean aProxyAuth, + inout nsISupports aSessionState, + inout nsISupports aContinuationState, + out boolean aInvalidatesIdentity); + + /** + * Called to generate the authentication credentials for a particular + * server/proxy challenge asynchronously. Credentials will be sent back + * to the server via an Authorization/Proxy-Authorization header. + * + * @param aChannel + * the http channel requesting credentials + * @param aCallback + * callback function to be called when credentials are available + * @param aChallenge + * the challenge from the WWW-Authenticate/Proxy-Authenticate + * server response header. (possibly from the auth cache.) + * @param aProxyAuth + * flag indicating whether or not aChallenge is from a proxy. + * @param aDomain + * string containing the domain name (if appropriate) + * @param aUser + * string containing the user name + * @param aPassword + * string containing the password + * @param aSessionState + * state stored along side the user's identity in the auth cache + * for the lifetime of the browser session. if a new auth cache + * entry is created for this challenge, then this parameter will + * be null. on return, the result will be stored in the new auth + * cache entry. this parameter is non-null when an auth cache entry + * is being reused. currently modification of session state is not + * communicated to caller, thus caching credentials obtained by + * asynchronous way is not supported. + * @param aContinuationState + * state held by the channel between consecutive calls to + * generateCredentials, assuming multiple calls are required + * to authenticate. this state is held for at most the lifetime of + * the channel. + * @pram aCancel + * returns cancellable runnable object which caller can use to cancel + * calling aCallback when finished. + */ + [must_use] + void generateCredentialsAsync(in nsIHttpAuthenticableChannel aChannel, + in nsIHttpAuthenticatorCallback aCallback, + in string aChallenge, + in boolean aProxyAuth, + in wstring aDomain, + in wstring aUser, + in wstring aPassword, + in nsISupports aSessionState, + in nsISupports aContinuationState, + out nsICancelable aCancel); + /** + * Called to generate the authentication credentials for a particular + * server/proxy challenge. This is the value that will be sent back + * to the server via an Authorization/Proxy-Authorization header. + * + * This function may be called using a cached challenge provided the + * authenticator sets the REUSABLE_CHALLENGE flag. + * + * @param aChannel + * the http channel requesting credentials + * @param aChallenge + * the challenge from the WWW-Authenticate/Proxy-Authenticate + * server response header. (possibly from the auth cache.) + * @param aProxyAuth + * flag indicating whether or not aChallenge is from a proxy. + * @param aDomain + * string containing the domain name (if appropriate) + * @param aUser + * string containing the user name + * @param aPassword + * string containing the password + * @param aSessionState + * state stored along side the user's identity in the auth cache + * for the lifetime of the browser session. if a new auth cache + * entry is created for this challenge, then this parameter will + * be null. on return, the result will be stored in the new auth + * cache entry. this parameter is non-null when an auth cache entry + * is being reused. + * @param aContinuationState + * state held by the channel between consecutive calls to + * generateCredentials, assuming multiple calls are required + * to authenticate. this state is held for at most the lifetime of + * the channel. + * @param aFlags + * authenticator may return one of the generate flags bellow. + */ + [must_use] + string generateCredentials(in nsIHttpAuthenticableChannel aChannel, + in string aChallenge, + in boolean aProxyAuth, + in wstring aDomain, + in wstring aUser, + in wstring aPassword, + inout nsISupports aSessionState, + inout nsISupports aContinuationState, + out unsigned long aFlags); + + /** + * Generate flags + */ + + /** + * Indicates that the authenticator has used an out-of-band or internal + * source of identity and tells the consumer that it must not cache + * the returned identity because it might not be valid and would overwrite + * the cached identity. See bug 542318 comment 32. + */ + const unsigned long USING_INTERNAL_IDENTITY = (1<<0); + + /** + * Flags defining various properties of the authenticator. + */ + [must_use] readonly attribute unsigned long authFlags; + + /** + * A request based authentication scheme only authenticates an individual + * request (or a set of requests under the same authentication domain as + * defined by RFC 2617). BASIC and DIGEST are request based authentication + * schemes. + */ + const unsigned long REQUEST_BASED = (1<<0); + + /** + * A connection based authentication scheme authenticates an individual + * connection. Multiple requests may be issued over the connection without + * repeating the authentication steps. Connection based authentication + * schemes can associate state with the connection being authenticated via + * the aContinuationState parameter (see generateCredentials). + */ + const unsigned long CONNECTION_BASED = (1<<1); + + /** + * The credentials returned from generateCredentials may be reused with any + * other URLs within "the protection space" as defined by RFC 2617 section + * 1.2. If this flag is not set, then generateCredentials must be called + * for each request within the protection space. REUSABLE_CREDENTIALS + * implies REUSABLE_CHALLENGE. + */ + const unsigned long REUSABLE_CREDENTIALS = (1<<2); + + /** + * A challenge may be reused to later generate credentials in anticipation + * of a duplicate server challenge for URLs within "the protection space" + * as defined by RFC 2617 section 1.2. + */ + const unsigned long REUSABLE_CHALLENGE = (1<<3); + + /** + * This flag indicates that the identity of the user is not required by + * this authentication scheme. + */ + const unsigned long IDENTITY_IGNORED = (1<<10); + + /** + * This flag indicates that the identity of the user includes a domain + * attribute that the user must supply. + */ + const unsigned long IDENTITY_INCLUDES_DOMAIN = (1<<11); + + /** + * This flag indicates that the identity will be sent encrypted. It does + * not make sense to combine this flag with IDENTITY_IGNORED. + */ + const unsigned long IDENTITY_ENCRYPTED = (1<<12); +}; diff --git a/netwerk/protocol/http/nsIHttpChannel.idl b/netwerk/protocol/http/nsIHttpChannel.idl new file mode 100644 index 0000000000..03803d490d --- /dev/null +++ b/netwerk/protocol/http/nsIHttpChannel.idl @@ -0,0 +1,510 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* 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 "nsIChannel.idl" + +interface nsIHttpHeaderVisitor; +interface nsIReferrerInfo; + +%{C++ +#include "GeckoProfiler.h" +%} + +/** + * nsIHttpChannel + * + * This interface allows for the modification of HTTP request parameters and + * the inspection of the resulting HTTP response status and headers when they + * become available. + */ +[builtinclass, scriptable, uuid(c5a4a073-4539-49c7-a3f2-cec3f0619c6c)] +interface nsIHttpChannel : nsIIdentChannel +{ + /************************************************************************** + * REQUEST CONFIGURATION + * + * Modifying request parameters after asyncOpen has been called is an error. + */ + + /** + * Set/get the HTTP request method (default is "GET"). Both setter and + * getter are case sensitive. + * + * This attribute may only be set before the channel is opened. + * + * NOTE: The data for a "POST" or "PUT" request can be configured via + * nsIUploadChannel; however, after setting the upload data, it may be + * necessary to set the request method explicitly. The documentation + * for nsIUploadChannel has further details. + * + * @throws NS_ERROR_IN_PROGRESS if set after the channel has been opened. + */ + [must_use] attribute ACString requestMethod; + + /** + * Get/set the referrer information. This contains the referrer (URI) of the + * resource from which this channel's URI was obtained (see RFC2616 section + * 14.36) and the referrer policy applied to the referrer. + * + * This attribute may only be set before the channel is opened. + * + * Setting this attribute will clone new referrerInfo object by default. + * + * NOTE: The channel may silently refuse to set the Referer header if the + * URI does not pass certain security checks (e.g., a "https://" URL will + * never be sent as the referrer for a plaintext HTTP request). The + * implementation is not required to throw an exception when the referrer + * URI is rejected. + * + * @throws NS_ERROR_IN_PROGRESS if set after the channel has been opened. + * @throws NS_ERROR_FAILURE if used for setting referrer during + * visitRequestHeaders. Getting the value will not throw. + */ + [must_use, infallible] attribute nsIReferrerInfo referrerInfo; + + /** + * Set referrer Info without clone new object. + * Use this api only when you are passing a referrerInfo to the channel with + * 1-1 relationship. Don't use this api if you will reuse the referrer info + * object later. For example when to use: + * channel.setReferrerInfoWithoutClone(new ReferrerInfo()); + * + */ + [must_use, noscript] + void setReferrerInfoWithoutClone(in nsIReferrerInfo aReferrerInfo); + + /** + * Returns the network protocol used to fetch the resource as identified + * by the ALPN Protocol ID. + * + * @throws NS_ERROR_NOT_AVAILABLE if called before the response + * has been received (before onStartRequest). + */ + [must_use] readonly attribute ACString protocolVersion; + + /** + * size consumed by the response header fields and the response payload body + */ + [must_use] readonly attribute uint64_t transferSize; + + /** + * size consumed by the request header fields and the request payload body + */ + [must_use] readonly attribute uint64_t requestSize; + + /** + * The size of the message body received by the client, + * after removing any applied content-codings + */ + [must_use] readonly attribute uint64_t decodedBodySize; + + /** + * The size in octets of the payload body, prior to removing content-codings + */ + [must_use] readonly attribute uint64_t encodedBodySize; + + /** + * Get the value of a particular request header. + * + * @param aHeader + * The case-insensitive name of the request header to query (e.g., + * "Cache-Control"). + * + * @return the value of the request header. + * @throws NS_ERROR_NOT_AVAILABLE if the header is not set. + */ + [must_use] ACString getRequestHeader(in ACString aHeader); + + /** + * Set the value of a particular request header. + * + * This method allows, for example, the cookies module to add "Cookie" + * headers to the outgoing HTTP request. + * + * This method may only be called before the channel is opened. + * + * @param aHeader + * The case-insensitive name of the request header to set (e.g., + * "Cookie"). + * @param aValue + * The request header value to set (e.g., "X=1"). + * @param aMerge + * If true, the new header value will be merged with any existing + * values for the specified header. This flag is ignored if the + * specified header does not support merging (e.g., the "Content- + * Type" header can only have one value). The list of headers for + * which this flag is ignored is an implementation detail. If this + * flag is false, then the header value will be replaced with the + * contents of |aValue|. + * + * If aValue is empty and aMerge is false, the header will be cleared. + * + * @throws NS_ERROR_IN_PROGRESS if called after the channel has been + * opened. + * @throws NS_ERROR_FAILURE if called during visitRequestHeaders. + */ + [must_use] void setRequestHeader(in ACString aHeader, + in ACString aValue, + in boolean aMerge); + + /** + * Creates and sets new ReferrerInfo object + * @param aUrl referrer url + * @param aPolicy referrer policy of the created object + * @param aSendReferrer indicates if the referrer should not be sent or not + * even when it's available. + */ + [must_use] void setNewReferrerInfo(in ACString aUrl, + in nsIReferrerInfo_ReferrerPolicyIDL aPolicy, + in boolean aSendReferrer); + + /** + * Set a request header with empty value. + * + * This should be used with caution in the cases where the behavior of + * setRequestHeader ignoring empty header values is undesirable. + * + * This method may only be called before the channel is opened. + * + * @param aHeader + * The case-insensitive name of the request header to set (e.g., + * "Cookie"). + * + * @throws NS_ERROR_IN_PROGRESS if called after the channel has been + * opened. + * @throws NS_ERROR_FAILURE if called during visitRequestHeaders. + */ + [must_use] void setEmptyRequestHeader(in ACString aHeader); + + /** + * Call this method to visit all request headers. Calling setRequestHeader + * while visiting request headers has undefined behavior. Don't do it! + * + * @param aVisitor + * the header visitor instance. + */ + [must_use] void visitRequestHeaders(in nsIHttpHeaderVisitor aVisitor); + + /** + * Call this method to visit all non-default (UA-provided) request headers. + * Calling setRequestHeader while visiting request headers has undefined + * behavior. Don't do it! + * + * @param aVisitor + * the header visitor instance. + */ + [must_use] + void visitNonDefaultRequestHeaders(in nsIHttpHeaderVisitor aVisitor); + + /** + * Call this method to see if we need to strip the request body headers + * for the new http channel. This should be called during redirection. + */ + [must_use] bool ShouldStripRequestBodyHeader(in ACString aMethod); + + /** + * This attribute no longer has any effect, it remains for backwards compat + * + * @throws NS_ERROR_FAILURE if set after the channel has been opened. + */ + [must_use] attribute boolean allowPipelining; + + /** + * This attribute of the channel indicates whether or not + * the underlying HTTP transaction should be honor stored Strict Transport + * Security directives for its principal. It defaults to true. Using + * OCSP to bootstrap the HTTPs is the likely use case for setting it to + * false. + * + * This attribute may only be set before the channel is opened. + * + * @throws NS_ERROR_IN_PROGRESS or NS_ERROR_ALREADY_OPENED + * if called after the channel has been opened. + */ + [must_use] attribute boolean allowSTS; + + /** + * This attribute specifies the number of redirects this channel is allowed + * to make. If zero, the channel will fail to redirect and will generate + * a NS_ERROR_REDIRECT_LOOP failure status. + * + * NOTE: An HTTP redirect results in a new channel being created. If the + * new channel supports nsIHttpChannel, then it will be assigned a value + * to its |redirectionLimit| attribute one less than the value of the + * redirected channel's |redirectionLimit| attribute. The initial value + * for this attribute may be a configurable preference (depending on the + * implementation). + */ + [must_use] attribute unsigned long redirectionLimit; + + /************************************************************************** + * RESPONSE INFO + * + * Accessing response info before the onStartRequest event is an error. + */ + + /** + * Get the HTTP response code (e.g., 200). + * + * @throws NS_ERROR_NOT_AVAILABLE if called before the response + * has been received (before onStartRequest). + */ + [must_use] readonly attribute unsigned long responseStatus; + + /** + * Get the HTTP response status text (e.g., "OK"). + * + * NOTE: This returns the raw (possibly 8-bit) text from the server. There + * are no assumptions made about the charset of the returned text. You + * have been warned! + * + * @throws NS_ERROR_NOT_AVAILABLE if called before the response + * has been received (before onStartRequest). + */ + [must_use] readonly attribute ACString responseStatusText; + + /** + * Returns true if the HTTP response code indicates success. The value of + * nsIRequest::status will be NS_OK even when processing a 404 response + * because a 404 response may include a message body that (in some cases) + * should be shown to the user. + * + * Use this attribute to distinguish server error pages from normal pages, + * instead of comparing the response status manually against the set of + * valid response codes, if that is required by your application. + * + * @throws NS_ERROR_NOT_AVAILABLE if called before the response + * has been received (before onStartRequest). + */ + [must_use] readonly attribute boolean requestSucceeded; + + /** Indicates whether channel should be treated as the main one for the + * current document. If manually set to true, will always remain true. Otherwise, + * will be true if LOAD_DOCUMENT_URI is set in the channel's loadflags. + */ + [must_use] attribute boolean isMainDocumentChannel; + + /** + * Get the value of a particular response header. + * + * @param aHeader + * The case-insensitive name of the response header to query (e.g., + * "Set-Cookie"). + * + * @return the value of the response header. + * + * @throws NS_ERROR_NOT_AVAILABLE if called before the response + * has been received (before onStartRequest) or if the header is + * not set in the response. + */ + [must_use] ACString getResponseHeader(in ACString header); + + /** + * Set the value of a particular response header. + * + * This method allows, for example, the HTML content sink to inform the HTTP + * channel about HTTP-EQUIV headers found in HTML tags. + * + * @param aHeader + * The case-insensitive name of the response header to set (e.g., + * "Cache-control"). + * @param aValue + * The response header value to set (e.g., "no-cache"). + * @param aMerge + * If true, the new header value will be merged with any existing + * values for the specified header. This flag is ignored if the + * specified header does not support merging (e.g., the "Content- + * Type" header can only have one value). The list of headers for + * which this flag is ignored is an implementation detail. If this + * flag is false, then the header value will be replaced with the + * contents of |aValue|. + * + * If aValue is empty and aMerge is false, the header will be cleared. + * + * @throws NS_ERROR_NOT_AVAILABLE if called before the response + * has been received (before onStartRequest). + * @throws NS_ERROR_ILLEGAL_VALUE if changing the value of this response + * header is not allowed. + * @throws NS_ERROR_FAILURE if called during visitResponseHeaders, + * VisitOriginalResponseHeaders or getOriginalResponseHeader. + */ + [must_use] void setResponseHeader(in ACString header, + in ACString value, + in boolean merge); + + /** + * Call this method to visit all response headers. Calling + * setResponseHeader while visiting response headers has undefined + * behavior. Don't do it! + * + * @param aVisitor + * the header visitor instance. + * + * @throws NS_ERROR_NOT_AVAILABLE if called before the response + * has been received (before onStartRequest). + */ + [must_use] void visitResponseHeaders(in nsIHttpHeaderVisitor aVisitor); + + /** + * Get the value(s) of a particular response header in the form and order + * it has been received from the remote peer. There can be multiple headers + * with the same name. + * + * @param aHeader + * The case-insensitive name of the response header to query (e.g., + * "Set-Cookie"). + * + * @param aVisitor + * the header visitor instance. + * + * @throws NS_ERROR_NOT_AVAILABLE if called before the response + * has been received (before onStartRequest) or if the header is + * not set in the response. + */ + [must_use] void getOriginalResponseHeader(in ACString aHeader, + in nsIHttpHeaderVisitor aVisitor); + + /** + * Call this method to visit all response headers in the form and order as + * they have been received from the remote peer. + * Calling setResponseHeader while visiting response headers has undefined + * behavior. Don't do it! + * + * @param aVisitor + * the header visitor instance. + * + * @throws NS_ERROR_NOT_AVAILABLE if called before the response + * has been received (before onStartRequest). + */ + [must_use] + void visitOriginalResponseHeaders(in nsIHttpHeaderVisitor aVisitor); + + /** + * Returns true if the server sent a "Cache-Control: no-store" response + * header. + * + * @throws NS_ERROR_NOT_AVAILABLE if called before the response + * has been received (before onStartRequest). + */ + [must_use] boolean isNoStoreResponse(); + + /** + * Returns true if the server sent the equivalent of a "Cache-control: + * no-cache" response header. Equivalent response headers include: + * "Pragma: no-cache", "Expires: 0", and "Expires" with a date value + * in the past relative to the value of the "Date" header. + * + * @throws NS_ERROR_NOT_AVAILABLE if called before the response + * has been received (before onStartRequest). + */ + [must_use] boolean isNoCacheResponse(); + + /** + * Returns true if the server sent a "Cache-Control: private" response + * header. + * + * @throws NS_ERROR_NOT_AVAILABLE if called before the response + * has been received (before onStartRequest). + */ + [must_use] boolean isPrivateResponse(); + + /** + * Instructs the channel to immediately redirect to a new destination. + * Can only be called on channels that have not yet called their + * listener's OnStartRequest(). Generally that means the latest time + * this can be used is one of: + * "http-on-examine-response" + * "http-on-examine-merged-response" + * "http-on-examine-cached-response" + * + * When non-null URL is set before AsyncOpen: + * we attempt to redirect to the targetURI before we even start building + * and sending the request to the cache or the origin server. + * If the redirect is vetoed, we fail the channel. + * + * When set between AsyncOpen and first call to OnStartRequest being called: + * we attempt to redirect before we start delivery of network or cached + * response to the listener. If vetoed, we continue with delivery of + * the original content to the channel listener. + * + * When passed aTargetURI is null the channel behaves normally (can be + * rewritten). + * + * This method provides no explicit conflict resolution. The last + * caller to call it wins. + * + * @throws NS_ERROR_NOT_AVAILABLE if called after the channel has already + * started to deliver the content to its listener. + */ + [must_use] void redirectTo(in nsIURI aTargetURI); + + /** + * Flags a channel to be upgraded to HTTPS. + * + * Upgrading to a secure channel must happen before or during + * "http-on-modify-request". If redirectTo is called early as well, it + * will win and upgradeToSecure will be a no-op. + * + * @throws NS_ERROR_NOT_AVAILABLE if called after the channel has already + * started to deliver the content to its listener. + */ + [must_use] void upgradeToSecure(); + + /** + * Identifies the request context for this load. + */ + [noscript, must_use] attribute uint64_t requestContextID; + + /** + * ID of the top-level document's inner window. Identifies the content + * this channels is being load in. + */ + [must_use] attribute uint64_t topLevelContentWindowId; + + /** + * Returns the allowing status for flash plugin for this channel. + */ + cenum FlashPluginState : 8 { + FlashPluginUnknown = 0, + FlashPluginAllowed = 1, + FlashPluginDenied = 2, + FlashPluginDeniedInSubdocuments = 3, + + // Keep this equal to the last value. + FlashPluginLastValue = 3, + }; + [infallible] readonly attribute nsIHttpChannel_FlashPluginState flashPluginState; + + /** + * ID of the top-level outer content window. Identifies this channel's + * top-level window it comes from. + * + * NOTE: The setter of this attribute is currently for xpcshell test only. + * Don't alter it otherwise. + */ + [must_use] attribute uint64_t topLevelOuterContentWindowId; + + /** + * In e10s, the information that the CORS response blocks the load is in the + * parent, which doesn't know the true window id of the request, so we may + * need to proxy the request to the child. + * + * @param aMessage + * The message to print in the console. + * + * @param aCategory + * The category under which the message should be displayed. + */ + void logBlockedCORSRequest(in AString aMessage, in ACString aCategory); + + void logMimeTypeMismatch(in ACString aMessageName, + in boolean aWarning, + in AString aURL, + in AString aContentType); + +%{ C++ + virtual void SetSource(mozilla::UniquePtr aSource) {} +%} +}; diff --git a/netwerk/protocol/http/nsIHttpChannelAuthProvider.idl b/netwerk/protocol/http/nsIHttpChannelAuthProvider.idl new file mode 100644 index 0000000000..73282a4bef --- /dev/null +++ b/netwerk/protocol/http/nsIHttpChannelAuthProvider.idl @@ -0,0 +1,86 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et 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 "nsICancelable.idl" + +interface nsIHttpChannel; +interface nsIHttpAuthenticableChannel; + +/** + * nsIHttpChannelAuthProvider + * + * This interface is intended for providing authentication for http-style + * channels, like nsIHttpChannel and nsIWebSocket, which implement the + * nsIHttpAuthenticableChannel interface. + * + * When requesting pages AddAuthorizationHeaders MUST be called + * in order to get the http cached headers credentials. When the request is + * unsuccessful because of receiving either a 401 or 407 http response code + * ProcessAuthentication MUST be called and the page MUST be requested again + * with the new credentials that the user has provided. After a successful + * request, checkForSuperfluousAuth MAY be called, and disconnect MUST be + * called. + */ + +[builtinclass, uuid(788f331b-2e1f-436c-b405-4f88a31a105b)] +interface nsIHttpChannelAuthProvider : nsICancelable +{ + /** + * Initializes the http authentication support for the channel. + * Implementations must hold a weak reference of the channel. + */ + [must_use] void init(in nsIHttpAuthenticableChannel channel); + + /** + * Upon receipt of a server challenge, this function is called to determine + * the credentials to send. + * + * @param httpStatus + * the http status received. + * @param sslConnectFailed + * if the last ssl tunnel connection attempt was or not successful. + * @param callback + * the callback to be called when it returns NS_ERROR_IN_PROGRESS. + * The implementation must hold a weak reference. + * + * @returns NS_OK if the credentials were got and set successfully. + * NS_ERROR_IN_PROGRESS if the credentials are going to be asked to + * the user. The channel reference must be + * alive until the feedback from + * nsIHttpAuthenticableChannel's methods or + * until disconnect be called. + */ + [must_use] void processAuthentication(in unsigned long httpStatus, + in boolean sslConnectFailed); + + /** + * Add credentials from the http auth cache. + * + * @param dontUseCachedWWWCreds + * When true, the method will not add any Authorization headers from + * the auth cache. + */ + [must_use] void addAuthorizationHeaders(in boolean dontUseCachedWWWCreds); + + /** + * Check if an unnecessary(and maybe malicious) url authentication has been + * provided. + */ + [must_use] void checkForSuperfluousAuth(); + + /** + * Cancel pending user auth prompts and release the callback and channel + * weak references. + */ + [must_use] void disconnect(in nsresult status); + + /** + * Clear the proxy ident to not consider it invalid on re-athentication. + * Called when the channel finds out its transaction has been internally + * restarted. + */ + void clearProxyIdent(); +}; diff --git a/netwerk/protocol/http/nsIHttpChannelChild.idl b/netwerk/protocol/http/nsIHttpChannelChild.idl new file mode 100644 index 0000000000..c5509f011d --- /dev/null +++ b/netwerk/protocol/http/nsIHttpChannelChild.idl @@ -0,0 +1,38 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* 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 "nsISupports.idl" + +%{ C++ +namespace mozilla { +class OriginAttributes; +} // mozilla namespace +%} + +[ptr] native RequestHeaderTuples(mozilla::net::RequestHeaderTuples); +[ref] native MaybeCorsPreflightArgsRef(mozilla::Maybe); +[ref] native const_OriginAttributes(const mozilla::OriginAttributes); + +interface nsIPrincipal; +interface nsIURI; + +[builtinclass, uuid(d02b96ed-2789-4f42-a25c-7abe63de7c18)] +interface nsIHttpChannelChild : nsISupports +{ + [must_use] void addCookiesToRequest(); + + // Headers that the channel client has set via SetRequestHeader. + [must_use] readonly attribute RequestHeaderTuples clientSetRequestHeaders; + + // Headers that the channel client has set via SetRequestHeader. + [notxpcom, nostdcall] + void GetClientSetCorsPreflightParameters(in MaybeCorsPreflightArgsRef args); + + // This method is called by nsCORSListenerProxy if we need to remove + // an entry from the CORS preflight cache in the parent process. + [must_use] + void removeCorsPreflightCacheEntry(in nsIURI aURI, in nsIPrincipal aRequestingPrincipal, + in const_OriginAttributes aOriginAttributes); +}; diff --git a/netwerk/protocol/http/nsIHttpChannelInternal.idl b/netwerk/protocol/http/nsIHttpChannelInternal.idl new file mode 100644 index 0000000000..ea079ee221 --- /dev/null +++ b/netwerk/protocol/http/nsIHttpChannelInternal.idl @@ -0,0 +1,439 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* 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 "nsISupports.idl" +#include "nsILoadInfo.idl" + +%{C++ +#include "nsStringFwd.h" +#include "nsTArrayForwardDeclare.h" +template class nsCOMArray; +namespace mozilla { +class TimeStamp; +} +%} +[ptr] native StringArray(nsTArray); +[ref] native CStringArrayRef(const nsTArray); +[ref] native securityMessagesArray(nsCOMArray); + +native TimeStamp(mozilla::TimeStamp); + +interface nsIAsyncInputStream; +interface nsIAsyncOutputStream; +interface nsIPrincipal; +interface nsIProxyInfo; +interface nsISecurityConsoleMessage; +interface nsISocketTransport; +interface nsIURI; + +/** + * The callback interface for nsIHttpChannelInternal::HTTPUpgrade() + */ + +[scriptable, uuid(5b515449-ab64-4dba-b3cd-da8fc2f83064)] +interface nsIHttpUpgradeListener : nsISupports +{ + [must_use] void onTransportAvailable(in nsISocketTransport aTransport, + in nsIAsyncInputStream aSocketIn, + in nsIAsyncOutputStream aSocketOut); + + [must_use] void onUpgradeFailed(in nsresult aErrorCode); +}; + +/** + * Dumping ground for http. This interface will never be frozen. If you are + * using any feature exposed by this interface, be aware that this interface + * will change and you will be broken. You have been warned. + */ +[builtinclass, scriptable, uuid(4e28263d-1e03-46f4-aa5c-9512f91957f9)] +interface nsIHttpChannelInternal : nsISupports +{ + /** + * An http channel can own a reference to the document URI + */ + [must_use] attribute nsIURI documentURI; + + /** + * Get the major/minor version numbers for the request + */ + [must_use] + void getRequestVersion(out unsigned long major, out unsigned long minor); + + /** + * Get the major/minor version numbers for the response + */ + [must_use] + void getResponseVersion(out unsigned long major, out unsigned long minor); + + /* + * Retrieves all security messages from the security message queue + * and empties the queue after retrieval + */ + [noscript, must_use] + void takeAllSecurityMessages(in securityMessagesArray aMessages); + + /** + * Helper method to set a cookie with a consumer-provided + * cookie header, _but_ using the channel's other information + * (URI's, prompters, date headers etc). + * + * @param aCookieHeader + * The cookie header to be parsed. + */ + [must_use] void setCookie(in ACString aCookieHeader); + + /** + * Setup this channel as an application cache fallback channel. + */ + [must_use] void setupFallbackChannel(in string aFallbackKey); + + /** + * Returns true in case this channel is used for auth; + * (the response header includes 'www-authenticate'). + */ + [noscript, must_use] readonly attribute bool isAuthChannel; + + /** + * This flag is set to force relevant cookies to be sent with this load + * even if normally they wouldn't be. + */ + const unsigned long THIRD_PARTY_FORCE_ALLOW = 1 << 0; + + /** + * When set, these flags modify the algorithm used to decide whether to + * send 3rd party cookies for a given channel. + */ + [must_use] attribute unsigned long thirdPartyFlags; + + /** + * This attribute was added before the "flags" above and is retained here + * for compatibility. When set to true, has the same effect as + * THIRD_PARTY_FORCE_ALLOW, described above. + */ + [must_use] attribute boolean forceAllowThirdPartyCookie; + + /** + * External handlers may set this to true to notify the channel + * that it is open on behalf of a download. + */ + [must_use] attribute boolean channelIsForDownload; + + /** + * The local IP address to which this channel is bound, in the + * format produced by PR_NetAddrToString. May be IPv4 or IPv6. + * Note: in the presence of NAT, this may not be the same as the + * address that the remote host thinks it's talking to. + * + * May throw NS_ERROR_NOT_AVAILABLE if accessed when the channel's + * endpoints are not yet determined, or in any case when + * nsIHttpActivityObserver.isActive is false. See bugs 534698 and 526207. + */ + [must_use] readonly attribute AUTF8String localAddress; + + /** + * The local port number to which this channel is bound. + * + * May throw NS_ERROR_NOT_AVAILABLE if accessed when the channel's + * endpoints are not yet determined, or in any case when + * nsIHttpActivityObserver.isActive is false. See bugs 534698 and 526207. + */ + [must_use] readonly attribute int32_t localPort; + + /** + * The IP address of the remote host that this channel is + * connected to, in the format produced by PR_NetAddrToString. + * + * May throw NS_ERROR_NOT_AVAILABLE if accessed when the channel's + * endpoints are not yet determined, or in any case when + * nsIHttpActivityObserver.isActive is false. See bugs 534698 and 526207. + */ + [must_use] readonly attribute AUTF8String remoteAddress; + + /** + * The remote port number that this channel is connected to. + * + * May throw NS_ERROR_NOT_AVAILABLE if accessed when the channel's + * endpoints are not yet determined, or in any case when + * nsIHttpActivityObserver.isActive is false. See bugs 534698 and 526207. + */ + [must_use] readonly attribute int32_t remotePort; + + /** + * Transfer chain of redirected cache-keys. + */ + [noscript, must_use] + void setCacheKeysRedirectChain(in StringArray cacheKeys); + + /** + * HTTPUpgrade allows for the use of HTTP to bootstrap another protocol + * via the RFC 2616 Upgrade request header in conjunction with a 101 level + * response. The nsIHttpUpgradeListener will have its + * onTransportAvailable() method invoked if a matching 101 is processed. + * The arguments to onTransportAvailable provide the new protocol the low + * level tranport streams that are no longer used by HTTP. If any errors + * occur during the upgrade but the original request has (potentially) + * already received onStopRequest, the nsIHttpUpgradeListener will have its + * onUpgradeFailed() method invoked instead of onTransportAvailable(). + * + * The onStartRequest and onStopRequest events are still delivered and the + * listener gets full control over the socket if and when onTransportAvailable + * is delivered. Note that if onStopRequest is called with an error, no + * methods on the nsIHttpUpgradeListener might be invoked at all. + * + * @param aProtocolName + * The value of the HTTP Upgrade request header + * @param aListener + * The callback object used to handle a successful upgrade + */ + [must_use] void HTTPUpgrade(in ACString aProtocolName, + in nsIHttpUpgradeListener aListener); + + /** + * Enable only CONNECT to a proxy. Fails if no HTTPUpgrade listener + * has been defined. An ALPN header is set using the upgrade protocol. + * + * Load flags are set with INHIBIT_CACHING, LOAD_ANONYMOUS, + * LOAD_BYPASS_CACHE, and LOAD_BYPASS_SERVICE_WORKER. + * + * Proxy resolve flags are set with RESOLVE_PREFER_HTTPS_PROXY and + * RESOLVE_ALWAYS_TUNNEL. + */ + [must_use] void setConnectOnly(); + + /** + * True iff the channel is CONNECT only. + */ + [must_use] readonly attribute boolean onlyConnect; + + /** + * Enable/Disable Spdy negotiation on per channel basis. + * The network.http.spdy.enabled preference is still a pre-requisite + * for starting spdy. + */ + [must_use] attribute boolean allowSpdy; + + /** + * Enable/Disable HTTP3 negotiation on per channel basis. + * The network.http.http3.enabled preference is still a pre-requisite + * for starting HTTP3. + */ + [must_use] attribute boolean allowHttp3; + + /** + * This attribute en/disables the timeout for the first byte of an HTTP + * response. Enabled by default. + */ + [must_use] attribute boolean responseTimeoutEnabled; + + /** + * If the underlying transport supports RWIN manipulation, this is the + * intiial window value for the channel. HTTP/2 implements this. + * 0 means no override from system default. Set before opening channel. + */ + [must_use] attribute unsigned long initialRwin; + + /** + * Get value of the URI passed to nsIHttpChannel.redirectTo() if any. + * May return null when redirectTo() has not been called. + */ + [must_use] readonly attribute nsIURI apiRedirectToURI; + + /** + * Enable/Disable use of Alternate Services with this channel. + * The network.http.altsvc.enabled preference is still a pre-requisite. + */ + [must_use] attribute boolean allowAltSvc; + + /** + * If true, do not use newer protocol features that might have interop problems + * on the Internet. Intended only for use with critical infra like the updater. + * default is false. + */ + [must_use] attribute boolean beConservative; + + /** + * True if channel is used by the internal trusted recursive resolver + * This flag places data for the request in a cache segment specific to TRR + */ + [noscript, must_use] attribute boolean isTRRServiceChannel; + + /** + * If the channel's remote IP was resolved using TRR. + * Is false for resources loaded from the cache or resources that have an + * IP literal host. + */ + [noscript, must_use] readonly attribute boolean isResolvedByTRR; + + /** + * An opaque flags for non-standard behavior of the TLS system. + * It is unlikely this will need to be set outside of telemetry studies + * relating to the TLS implementation. + */ + [must_use] attribute unsigned long tlsFlags; + + [must_use] readonly attribute PRTime lastModifiedTime; + + /** + * Set by nsCORSListenerProxy if credentials should be included in + * cross-origin requests. false indicates "same-origin", users should still + * check flag LOAD_ANONYMOUS! + */ + [must_use] attribute boolean corsIncludeCredentials; + + const unsigned long CORS_MODE_SAME_ORIGIN = 0; + const unsigned long CORS_MODE_NO_CORS = 1; + const unsigned long CORS_MODE_CORS = 2; + const unsigned long CORS_MODE_NAVIGATE = 3; + /** + * Set by nsCORSListenerProxy to indicate CORS load type. Defaults to CORS_MODE_NO_CORS. + */ + [must_use] attribute unsigned long corsMode; + + const unsigned long REDIRECT_MODE_FOLLOW = 0; + const unsigned long REDIRECT_MODE_ERROR = 1; + const unsigned long REDIRECT_MODE_MANUAL = 2; + /** + * Set to indicate Request.redirect mode exposed during ServiceWorker + * interception. No policy enforcement is performed by the channel for this + * value. + */ + [must_use] attribute unsigned long redirectMode; + + const unsigned long FETCH_CACHE_MODE_DEFAULT = 0; + const unsigned long FETCH_CACHE_MODE_NO_STORE = 1; + const unsigned long FETCH_CACHE_MODE_RELOAD = 2; + const unsigned long FETCH_CACHE_MODE_NO_CACHE = 3; + const unsigned long FETCH_CACHE_MODE_FORCE_CACHE = 4; + const unsigned long FETCH_CACHE_MODE_ONLY_IF_CACHED = 5; + /** + * Set to indicate Request.cache mode, which simulates the fetch API + * semantics, and is also used for exposing this value to the Web page + * during service worker interception. + */ + [must_use] attribute unsigned long fetchCacheMode; + + /** + * The URI of the top-level window that's associated with this channel. + */ + [must_use] readonly attribute nsIURI topWindowURI; + + /** + * Set top-level window URI to this channel only when the topWindowURI + * is null and there is no window associated to this channel. + * Note that the current usage of this method is only for xpcshell test. + */ + [must_use] void setTopWindowURIIfUnknown(in nsIURI topWindowURI); + + /** + * Read the proxy URI, which, if non-null, will be used to resolve + * proxies for this channel. + */ + [must_use] readonly attribute nsIURI proxyURI; + + /** + * Make cross-origin CORS loads happen with a CORS preflight, and specify + * the CORS preflight parameters. + */ + [noscript, notxpcom, nostdcall] + void setCorsPreflightParameters(in CStringArrayRef unsafeHeaders, + in boolean shouldStripRequestBodyHeader); + + [noscript, notxpcom, nostdcall] + void setAltDataForChild(in boolean aIsForChild); + + /** + * Prevent the use of alt-data cache for this request. Use by the + * extension StreamFilter class to force use of the regular cache. + */ + [noscript, notxpcom, nostdcall] + void disableAltDataCache(); + + /** + * When set to true, the channel will not pop any authentication prompts up + * to the user. When provided or cached credentials lead to an + * authentication failure, that failure will be propagated to the channel + * listener. Must be called before opening the channel, otherwise throws. + */ + [infallible] + attribute boolean blockAuthPrompt; + + /** + * Set to indicate Request.integrity. + */ + [must_use] attribute AString integrityMetadata; + + /** + * The connection info's hash key. We use it to test connection separation. + */ + [must_use] readonly attribute ACString connectionInfoHashKey; + + /** + * If this channel was created as the result of a redirect, then this + * value will reflect the redirect flags passed to the + * SetupReplacementChannel() method. + */ + [noscript, infallible] + attribute unsigned long lastRedirectFlags; + + // This is use to determine the duration since navigation started. + [noscript] attribute TimeStamp navigationStartTimeStamp; + + /** + * Cancel a channel because we have determined that it needs to be blocked + * for safe-browsing protection. This is an internal API that is meant to + * be called by the channel classifier. Please DO NOT use this API if you + * don't know whether you should be using it. + */ + [noscript] void cancelByURLClassifier(in nsresult aErrorCode); + + /** + * The channel will be loaded over IPv6, disabling IPv4. + */ + [noscript, notxpcom, nostdcall] + void setIPv4Disabled(); + + /** + * The channel will be loaded over IPv4, disabling IPv6. + */ + [noscript, notxpcom, nostdcall] + void setIPv6Disabled(); + + /** + * Returns a cached CrossOriginOpenerPolicy that is computed just before we + * determine if there is a policy mismatch. + * @throws NS_ERROR_NOT_AVAILABLE if it has not been computed yet + */ + readonly attribute nsILoadInfo_CrossOriginOpenerPolicy crossOriginOpenerPolicy; + + /** + * Called during onStartRequest to compute the cross-origin-opener-policy + * for a given channel. + */ + [noscript] + nsILoadInfo_CrossOriginOpenerPolicy computeCrossOriginOpenerPolicy( + in nsILoadInfo_CrossOriginOpenerPolicy aInitiatorPolicy); + + [noscript] + bool hasCrossOriginOpenerPolicyMismatch(); + + [noscript] + nsILoadInfo_CrossOriginEmbedderPolicy getResponseEmbedderPolicy(); + + [notxpcom, nostdcall] attribute boolean hasNonEmptySandboxingFlag; + + [noscript, notxpcom, nostdcall] + void DoDiagnosticAssertWhenOnStopNotCalledOnDestroy(); + + /** + * If this is called, this channel's transaction will not be dispatched + * until the HTTPSSVC record is available. + */ + [must_use] void setWaitForHTTPSSVCRecord(); + + /** + * This attribute indicates if the channel has support for HTTP3 + */ + [must_use] readonly attribute boolean supportsHTTP3; +}; diff --git a/netwerk/protocol/http/nsIHttpHeaderVisitor.idl b/netwerk/protocol/http/nsIHttpHeaderVisitor.idl new file mode 100644 index 0000000000..b09974de5d --- /dev/null +++ b/netwerk/protocol/http/nsIHttpHeaderVisitor.idl @@ -0,0 +1,26 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* 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 "nsISupports.idl" + +/** + * Implement this interface to visit http headers. + */ +[scriptable, function, uuid(35412859-b9d9-423c-8866-2d4559fdd2be)] +interface nsIHttpHeaderVisitor : nsISupports +{ + /** + * Called by the nsIHttpChannel implementation when visiting request and + * response headers. + * + * @param aHeader + * the header being visited. + * @param aValue + * the header value (possibly a comma delimited list). + * + * @throw any exception to terminate enumeration + */ + [must_use] void visitHeader(in ACString aHeader, in ACString aValue); +}; diff --git a/netwerk/protocol/http/nsIHttpProtocolHandler.idl b/netwerk/protocol/http/nsIHttpProtocolHandler.idl new file mode 100644 index 0000000000..e3d6fb5d1f --- /dev/null +++ b/netwerk/protocol/http/nsIHttpProtocolHandler.idl @@ -0,0 +1,195 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* 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 "nsIProxiedProtocolHandler.idl" + +%{C++ +namespace mozilla { +namespace net { +class HSTSDataCallbackWrapper; +} +} +%} + +native HSTSDataCallbackWrapperAlreadyAddRefed(RefPtr); + +[scriptable, builtinclass, uuid(c48126d9-2ddd-485b-a51a-378e917e75f8)] +interface nsIHttpProtocolHandler : nsIProxiedProtocolHandler +{ + /** + * Get the HTTP advertised user agent string. + */ + [must_use] readonly attribute ACString userAgent; + + /** + * Get the application name. + * + * @return The name of this application (eg. "Mozilla"). + */ + [must_use] readonly attribute ACString appName; + + /** + * Get the application version string. + * + * @return The complete version (major and minor) string. (eg. "5.0") + */ + [must_use] readonly attribute ACString appVersion; + + /** + * Get the current platform. + * + * @return The platform this application is running on + * (eg. "Windows", "Macintosh", "X11") + */ + [must_use] readonly attribute ACString platform; + + /** + * Get the current oscpu. + * + * @return The oscpu this application is running on + */ + [must_use] readonly attribute ACString oscpu; + + /** + * Get the application comment misc portion. + */ + [must_use] readonly attribute ACString misc; + + /** + * Get the Alt-Svc cache keys (used for testing). + */ + [must_use] readonly attribute Array altSvcCacheKeys; + + /** + * Get the auth cache keys (used for testing). + */ + [must_use] readonly attribute Array authCacheKeys; + + /** + * This function is used to ensure HSTS data storage is ready to read after + * the returned promise is resolved. + * Note that this function should only used for testing. + * See bug 1521729 for more details. + */ + [implicit_jscontext] + Promise EnsureHSTSDataReady(); + + /** + * A C++ friendly version of EnsureHSTSDataReady + */ + [noscript] + void EnsureHSTSDataReadyNative(in HSTSDataCallbackWrapperAlreadyAddRefed aCallback); +}; + +%{C++ +// ----------- Categories ----------- +/** + * At initialization time, the HTTP handler will initialize each service + * registered under this category: + */ +#define NS_HTTP_STARTUP_CATEGORY "http-startup-category" + +// ----------- Observer topics ----------- +/** + * nsIObserver notification corresponding to startup category. Services + * registered under the startup category will receive this observer topic at + * startup if they implement nsIObserver. The "subject" of the notification + * is the nsIHttpProtocolHandler instance. + */ +#define NS_HTTP_STARTUP_TOPIC "http-startup" + +/** + * Called when asyncOpen synchronously failes e.g. because of any synchronously + * performed security checks. This only fires on the child process, but if + * needed can be implemented also on the parent process. + */ +#define NS_HTTP_ON_FAILED_OPENING_REQUEST_TOPIC "http-on-failed-opening-request" + + /** + * This observer topic is notified when an HTTP channel is opened. + * It is similar to http-on-modify-request, except that + * 1) The notification is guaranteed to occur before on-modify-request, during + * the AsyncOpen call itself. + * 2) It only occurs for the initial open of a channel, not for internal + * asyncOpens that happen during redirects, etc. + * 3) Some information (most notably nsIProxiedChannel.proxyInfo) may not be set + * on the channel object yet. + * + * The "subject" of the notification is the nsIHttpChannel instance. + * + * Generally the 'http-on-modify-request' notification is preferred unless the + * synchronous, during-asyncOpen behavior that this notification provides is + * required. + */ +#define NS_HTTP_ON_OPENING_REQUEST_TOPIC "http-on-opening-request" + + /** + * This observer topic is notified when a document channel is opened. + * It is similar to http-on-opening-request. + */ +#define NS_DOCUMENT_ON_OPENING_REQUEST_TOPIC "document-on-opening-request" + +/** + * Before an HTTP request is sent to the server, this observer topic is + * notified. The observer of this topic can then choose to set any additional + * headers for this request before the request is actually sent to the server. + * The "subject" of the notification is the nsIHttpChannel instance. + */ +#define NS_HTTP_ON_MODIFY_REQUEST_TOPIC "http-on-modify-request" + +/** + * Before an HTTP request is sent to the server via a document channel this + * observer topic is notified. + * It is similar to http-on-modify-request. +*/ +#define NS_DOCUMENT_ON_MODIFY_REQUEST_TOPIC "document-on-modify-request" + +/** + * Before an HTTP connection to the server is created, this observer topic is + * notified. This observer happens after HSTS upgrades, etc. are set, providing + * access to the full set of request headers. The observer of this topic can + * choose to set any additional headers for this request before the request is + * actually sent to the server. The "subject" of the notification is the + * nsIHttpChannel instance. + */ +#define NS_HTTP_ON_BEFORE_CONNECT_TOPIC "http-on-before-connect" + +/** + * After an HTTP server response is received, this observer topic is notified. + * The observer of this topic can interrogate the response. The "subject" of + * the notification is the nsIHttpChannel instance. + */ +#define NS_HTTP_ON_EXAMINE_RESPONSE_TOPIC "http-on-examine-response" + +/** + * The observer of this topic is notified after the received HTTP response + * is merged with data from the browser cache. This means that, among other + * things, the Content-Type header will be set correctly. + */ +#define NS_HTTP_ON_EXAMINE_MERGED_RESPONSE_TOPIC "http-on-examine-merged-response" + +/** + * The observer of this topic is notified about a background revalidation that + * started by hitting a request that fell into stale-while-revalidate window. + * This notification points to the channel that performed the revalidation and + * after this notification the cache entry has been validated or updated. + */ +#define NS_HTTP_ON_BACKGROUND_REVALIDATION "http-on-background-revalidation" + +/** + * The observer of this topic is notified before data is read from the cache. + * The notification is sent if and only if there is no network communication + * at all. + */ +#define NS_HTTP_ON_EXAMINE_CACHED_RESPONSE_TOPIC "http-on-examine-cached-response" + +/** + * This topic is notified for every http channel right after it called + * OnStopRequest on its listener, regardless whether it was finished + * successfully, failed or has been canceled. + */ +#define NS_HTTP_ON_STOP_REQUEST_TOPIC "http-on-stop-request" + +%} diff --git a/netwerk/protocol/http/nsIRaceCacheWithNetwork.idl b/netwerk/protocol/http/nsIRaceCacheWithNetwork.idl new file mode 100644 index 0000000000..b2b05ec8db --- /dev/null +++ b/netwerk/protocol/http/nsIRaceCacheWithNetwork.idl @@ -0,0 +1,55 @@ +/* -*- 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 "nsISupports.idl" + +/** + * This holds methods used to race the cache with the network for a specific + * channel. This interface is was designed with nsHttpChannel in mind, and it's + * expected this will be the only class implementing it. + */ +[scriptable, builtinclass, uuid(4d963475-8b16-4c58-b804-8a23d49436c5)] +interface nsIRaceCacheWithNetwork : nsISupports +{ + + /**************************************************************************** + * TEST ONLY: The following methods are for testing purposes only. Do not use + * them to do anything important in your code. + **************************************************************************** + + /** + * Triggers network activity after given timeout. If timeout is 0, network + * activity is triggered immediately. If the cache.asyncOpenURI callbacks + * have already been called, the network activity may have already been triggered + * or the content may have already been delivered from the cache, so this + * operation will have no effect. + * + * @param timeout + * - the delay in milliseconds until the network will be triggered. + */ + void test_triggerNetwork(in long timeout); + + /** + * Normally a HTTP channel would immediately call AsyncOpenURI leading to the + * cache storage to lookup the cache entry and return it. In order to + * simmulate real life conditions where fetching a cache entry takes a long + * time, we set a timer to delay the operation. + * Can only be called on the main thread. + * + * @param timeout + * - the delay in milliseconds until the cache open will be triggered. + */ + void test_delayCacheEntryOpeningBy(in long timeout); + + /** + * Immediatelly triggers AsyncOpenURI if the timer hasn't fired. + * Can only be called on the main thread. + * This is only called in tests to reliably trigger the opening of the cache + * entry. + * @throws NS_ERROR_NOT_AVAILABLE if opening the cache wasn't delayed. + */ + void test_triggerDelayedOpenCacheEntry(); +}; diff --git a/netwerk/protocol/http/nsIWellKnownOpportunisticUtils.idl b/netwerk/protocol/http/nsIWellKnownOpportunisticUtils.idl new file mode 100644 index 0000000000..fa90891172 --- /dev/null +++ b/netwerk/protocol/http/nsIWellKnownOpportunisticUtils.idl @@ -0,0 +1,23 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* 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/. */ + +/* + For parsing JSON from http://httpwg.org/http-extensions/opsec.html +*/ + +#include "nsISupports.idl" + +%{C++ +#define NS_WELLKNOWNOPPORTUNISTICUTILS_CONTRACTID "@mozilla.org/network/well-known-opportunistic-utils;1" +%} + +[scriptable, uuid(b4f96c89-5238-450c-8bda-e12c26f1d150)] +interface nsIWellKnownOpportunisticUtils : nsISupports +{ + [must_use] void verify(in ACString aJSON, + in ACString aOrigin); + + [must_use] readonly attribute bool valid; +}; diff --git a/netwerk/protocol/http/nsServerTiming.cpp b/netwerk/protocol/http/nsServerTiming.cpp new file mode 100644 index 0000000000..b1b2fe54d1 --- /dev/null +++ b/netwerk/protocol/http/nsServerTiming.cpp @@ -0,0 +1,110 @@ +/* -*- 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 "nsServerTiming.h" + +#include "nsHttp.h" + +NS_IMPL_ISUPPORTS(nsServerTiming, nsIServerTiming) + +NS_IMETHODIMP +nsServerTiming::GetName(nsACString& aName) { + aName.Assign(mName); + return NS_OK; +} + +NS_IMETHODIMP +nsServerTiming::GetDuration(double* aDuration) { + *aDuration = mDuration; + return NS_OK; +} + +NS_IMETHODIMP +nsServerTiming::GetDescription(nsACString& aDescription) { + aDescription.Assign(mDescription); + return NS_OK; +} + +namespace mozilla { +namespace net { + +static double ParseDouble(const nsACString& aString) { + nsresult rv; + double val = PromiseFlatCString(aString).ToDouble(&rv); + return NS_FAILED(rv) ? 0.0f : val; +} + +void ServerTimingParser::Parse() { + // https://w3c.github.io/server-timing/#the-server-timing-header-field + // Server-Timing = #server-timing-metric + // server-timing-metric = metric-name *( OWS ";" OWS server-timing-param + // ) metric-name = token server-timing-param = + // server-timing-param-name OWS "=" OWS + // server-timing-param-value + // server-timing-param-name = token + // server-timing-param-value = token / quoted-string + + ParsedHeaderValueListList parsedHeader(mValue, false); + for (uint32_t index = 0; index < parsedHeader.mValues.Length(); ++index) { + if (parsedHeader.mValues[index].mValues.IsEmpty()) { + continue; + } + + // According to spec, the first ParsedHeaderPair's name is metric-name. + RefPtr timingHeader = new nsServerTiming(); + mServerTimingHeaders.AppendElement(timingHeader); + timingHeader->SetName(parsedHeader.mValues[index].mValues[0].mName); + + if (parsedHeader.mValues[index].mValues.Length() == 1) { + continue; + } + + // Try to find duration and description from the rest ParsedHeaderPairs. + bool foundDuration = false; + bool foundDescription = false; + for (uint32_t pairIndex = 1; + pairIndex < parsedHeader.mValues[index].mValues.Length(); + ++pairIndex) { + nsDependentCSubstring& currentName = + parsedHeader.mValues[index].mValues[pairIndex].mName; + nsDependentCSubstring& currentValue = + parsedHeader.mValues[index].mValues[pairIndex].mValue; + + // We should only take the value from the first + // occurrence of server-timing-param-name ("dur" and "desc"). + // This is true whether or not the value makes any sense (or, indeed, if + // there even is a value). + if (currentName.LowerCaseEqualsASCII("dur") && !foundDuration) { + if (currentValue.BeginReading()) { + timingHeader->SetDuration(ParseDouble(currentValue)); + } else { + timingHeader->SetDuration(0.0); + } + foundDuration = true; + } else if (currentName.LowerCaseEqualsASCII("desc") && + !foundDescription) { + if (!currentValue.IsEmpty()) { + timingHeader->SetDescription(currentValue); + } else { + timingHeader->SetDescription(""_ns); + } + foundDescription = true; + } + + if (foundDuration && foundDescription) { + break; + } + } + } +} + +nsTArray>&& +ServerTimingParser::TakeServerTimingHeaders() { + return std::move(mServerTimingHeaders); +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/nsServerTiming.h b/netwerk/protocol/http/nsServerTiming.h new file mode 100644 index 0000000000..ef6b9319a6 --- /dev/null +++ b/netwerk/protocol/http/nsServerTiming.h @@ -0,0 +1,54 @@ +/* -*- 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/. */ + +#ifndef nsServerTiming_h__ +#define nsServerTiming_h__ + +#include "nsITimedChannel.h" +#include "nsString.h" +#include "nsTArray.h" + +class nsServerTiming final : public nsIServerTiming { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSISERVERTIMING + + nsServerTiming() = default; + + void SetName(const nsACString& aName) { mName = aName; } + + void SetDuration(double aDuration) { mDuration = aDuration; } + + void SetDescription(const nsACString& aDescription) { + mDescription = aDescription; + } + + private: + virtual ~nsServerTiming() = default; + + nsCString mName; + double mDuration; + nsCString mDescription; +}; + +namespace mozilla { +namespace net { + +class ServerTimingParser { + public: + explicit ServerTimingParser(const nsCString& value) : mValue(value) {} + void Parse(); + nsTArray>&& TakeServerTimingHeaders(); + + private: + nsCString mValue; + nsTArray> mServerTimingHeaders; +}; + +} // namespace net +} // namespace mozilla + +#endif diff --git a/netwerk/protocol/moz.build b/netwerk/protocol/moz.build new file mode 100644 index 0000000000..35d389dfdb --- /dev/null +++ b/netwerk/protocol/moz.build @@ -0,0 +1,10 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +DIRS += ["about", "data", "file", "ftp"] +if CONFIG["MOZ_WIDGET_TOOLKIT"] == "gtk": + DIRS += ["gio"] +DIRS += ["http", "res", "viewsource", "websocket"] diff --git a/netwerk/protocol/res/ExtensionProtocolHandler.cpp b/netwerk/protocol/res/ExtensionProtocolHandler.cpp new file mode 100644 index 0000000000..3e4e314577 --- /dev/null +++ b/netwerk/protocol/res/ExtensionProtocolHandler.cpp @@ -0,0 +1,987 @@ +/* -*- 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 "ExtensionProtocolHandler.h" + +#include "mozilla/BinarySearch.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/dom/ContentChild.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/Promise-inl.h" +#include "mozilla/ExtensionPolicyService.h" +#include "mozilla/FileUtils.h" +#include "mozilla/ipc/IPCStreamUtils.h" +#include "mozilla/ipc/URIUtils.h" +#include "mozilla/net/NeckoChild.h" +#include "mozilla/RefPtr.h" +#include "mozilla/ResultExtensions.h" + +#include "FileDescriptor.h" +#include "FileDescriptorFile.h" +#include "LoadInfo.h" +#include "nsContentUtils.h" +#include "nsServiceManagerUtils.h" +#include "nsDirectoryServiceDefs.h" +#include "nsIFile.h" +#include "nsIFileChannel.h" +#include "nsIFileStreams.h" +#include "nsIFileURL.h" +#include "nsIJARChannel.h" +#include "nsIMIMEService.h" +#include "nsIURL.h" +#include "nsIChannel.h" +#include "nsIInputStreamPump.h" +#include "nsIJARURI.h" +#include "nsIStreamListener.h" +#include "nsIInputStream.h" +#include "nsIStreamConverterService.h" +#include "nsNetUtil.h" +#include "nsReadableUtils.h" +#include "nsURLHelper.h" +#include "prio.h" +#include "SimpleChannel.h" + +#if defined(XP_WIN) +# include "nsILocalFileWin.h" +# include "WinUtils.h" +#endif + +#if defined(XP_MACOSX) +# include "nsMacUtilsImpl.h" +#endif + +#define EXTENSION_SCHEME "moz-extension" +using mozilla::dom::Promise; +using mozilla::ipc::FileDescriptor; + +// A list of file extensions containing purely static data, which can be loaded +// from an extension before the extension is fully ready. The main purpose of +// this is to allow image resources from theme XPIs to load quickly during +// browser startup. +// +// The layout of this array is chosen in order to prevent the need for runtime +// relocation, which an array of char* pointers would require. It also has the +// benefit of being more compact when the difference in length between the +// longest and average string is less than 8 bytes. The length of the +// char[] array must match the size of the longest entry in the list. +// +// This list must be kept sorted. +static const char sStaticFileExtensions[][5] = { + // clang-format off + "bmp", + "gif", + "ico", + "jpeg", + "jpg", + "png", + "svg", + // clang-format on +}; + +namespace mozilla { + +namespace net { + +using extensions::URLInfo; + +LazyLogModule gExtProtocolLog("ExtProtocol"); +#undef LOG +#define LOG(...) MOZ_LOG(gExtProtocolLog, LogLevel::Debug, (__VA_ARGS__)) + +StaticRefPtr ExtensionProtocolHandler::sSingleton; + +/** + * Helper class used with SimpleChannel to asynchronously obtain an input + * stream or file descriptor from the parent for a remote moz-extension load + * from the child. + */ +class ExtensionStreamGetter : public RefCounted { + public: + // To use when getting a remote input stream for a resource + // in an unpacked extension. + ExtensionStreamGetter(nsIURI* aURI, nsILoadInfo* aLoadInfo) + : mURI(aURI), mLoadInfo(aLoadInfo), mIsJarChannel(false) { + MOZ_ASSERT(aURI); + MOZ_ASSERT(aLoadInfo); + + SetupEventTarget(); + } + + // To use when getting an FD for a packed extension JAR file + // in order to load a resource. + ExtensionStreamGetter(nsIURI* aURI, nsILoadInfo* aLoadInfo, + already_AddRefed&& aJarChannel, + nsIFile* aJarFile) + : mURI(aURI), + mLoadInfo(aLoadInfo), + mJarChannel(std::move(aJarChannel)), + mJarFile(aJarFile), + mIsJarChannel(true) { + MOZ_ASSERT(aURI); + MOZ_ASSERT(aLoadInfo); + MOZ_ASSERT(mJarChannel); + MOZ_ASSERT(aJarFile); + + SetupEventTarget(); + } + + ~ExtensionStreamGetter() = default; + + void SetupEventTarget() { + mMainThreadEventTarget = nsContentUtils::GetEventTargetByLoadInfo( + mLoadInfo, TaskCategory::Other); + if (!mMainThreadEventTarget) { + mMainThreadEventTarget = GetMainThreadSerialEventTarget(); + } + } + + // Get an input stream or file descriptor from the parent asynchronously. + Result GetAsync(nsIStreamListener* aListener, + nsIChannel* aChannel); + + // Handle an input stream being returned from the parent + void OnStream(already_AddRefed aStream); + + // Handle file descriptor being returned from the parent + void OnFD(const FileDescriptor& aFD); + + static void CancelRequest(nsIStreamListener* aListener, nsIChannel* aChannel, + nsresult aResult); + + MOZ_DECLARE_REFCOUNTED_TYPENAME(ExtensionStreamGetter) + + private: + nsCOMPtr mURI; + nsCOMPtr mLoadInfo; + nsCOMPtr mJarChannel; + nsCOMPtr mJarFile; + nsCOMPtr mListener; + nsCOMPtr mChannel; + nsCOMPtr mMainThreadEventTarget; + bool mIsJarChannel; +}; + +class ExtensionJARFileOpener final : public nsISupports { + public: + ExtensionJARFileOpener(nsIFile* aFile, + NeckoParent::GetExtensionFDResolver& aResolve) + : mFile(aFile), mResolve(aResolve) { + MOZ_ASSERT(aFile); + MOZ_ASSERT(aResolve); + } + + NS_IMETHOD OpenFile() { + MOZ_ASSERT(!NS_IsMainThread()); + AutoFDClose prFileDesc; + +#if defined(XP_WIN) + nsresult rv; + nsCOMPtr winFile = do_QueryInterface(mFile, &rv); + MOZ_ASSERT(winFile); + if (NS_SUCCEEDED(rv)) { + rv = winFile->OpenNSPRFileDescShareDelete(PR_RDONLY, 0, + &prFileDesc.rwget()); + } +#else + nsresult rv = mFile->OpenNSPRFileDesc(PR_RDONLY, 0, &prFileDesc.rwget()); +#endif /* XP_WIN */ + + if (NS_SUCCEEDED(rv)) { + mFD = FileDescriptor(FileDescriptor::PlatformHandleType( + PR_FileDesc2NativeHandle(prFileDesc))); + } + + nsCOMPtr event = + mozilla::NewRunnableMethod("ExtensionJarFileFDResolver", this, + &ExtensionJARFileOpener::SendBackFD); + + rv = NS_DispatchToMainThread(event, nsIEventTarget::DISPATCH_NORMAL); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "NS_DispatchToMainThread"); + return NS_OK; + } + + NS_IMETHOD SendBackFD() { + MOZ_ASSERT(NS_IsMainThread()); + mResolve(mFD); + mResolve = nullptr; + return NS_OK; + } + + NS_DECL_THREADSAFE_ISUPPORTS + + private: + virtual ~ExtensionJARFileOpener() = default; + + nsCOMPtr mFile; + NeckoParent::GetExtensionFDResolver mResolve; + FileDescriptor mFD; +}; + +NS_IMPL_ISUPPORTS(ExtensionJARFileOpener, nsISupports) + +// The amount of time, in milliseconds, that the file opener thread will remain +// allocated after it is used. This value chosen because to match other uses +// of LazyIdleThread. +#define DEFAULT_THREAD_TIMEOUT_MS 30000 + +// Request an FD or input stream from the parent. +Result ExtensionStreamGetter::GetAsync( + nsIStreamListener* aListener, nsIChannel* aChannel) { + MOZ_ASSERT(IsNeckoChild()); + MOZ_ASSERT(mMainThreadEventTarget); + + mListener = aListener; + mChannel = aChannel; + + RefPtr self = this; + if (mIsJarChannel) { + // Request an FD for this moz-extension URI + gNeckoChild->SendGetExtensionFD(mURI)->Then( + mMainThreadEventTarget, __func__, + [self](const FileDescriptor& fd) { self->OnFD(fd); }, + [self](const mozilla::ipc::ResponseRejectReason) { + self->OnFD(FileDescriptor()); + }); + return Ok(); + } + + // Request an input stream for this moz-extension URI + gNeckoChild->SendGetExtensionStream(mURI)->Then( + mMainThreadEventTarget, __func__, + [self](const RefPtr& stream) { + self->OnStream(do_AddRef(stream)); + }, + [self](const mozilla::ipc::ResponseRejectReason) { + self->OnStream(nullptr); + }); + return Ok(); +} + +// static +void ExtensionStreamGetter::CancelRequest(nsIStreamListener* aListener, + nsIChannel* aChannel, + nsresult aResult) { + MOZ_ASSERT(aListener); + MOZ_ASSERT(aChannel); + + aListener->OnStartRequest(aChannel); + aListener->OnStopRequest(aChannel, aResult); + aChannel->Cancel(NS_BINDING_ABORTED); +} + +// Handle an input stream sent from the parent. +void ExtensionStreamGetter::OnStream(already_AddRefed aStream) { + MOZ_ASSERT(IsNeckoChild()); + MOZ_ASSERT(mListener); + MOZ_ASSERT(mMainThreadEventTarget); + + nsCOMPtr stream = std::move(aStream); + + // We must keep an owning reference to the listener + // until we pass it on to AsyncRead. + nsCOMPtr listener = std::move(mListener); + + MOZ_ASSERT(mChannel); + + if (!stream) { + // The parent didn't send us back a stream. + CancelRequest(listener, mChannel, NS_ERROR_FILE_ACCESS_DENIED); + return; + } + + nsCOMPtr pump; + nsresult rv = NS_NewInputStreamPump(getter_AddRefs(pump), stream.forget(), 0, + 0, false, mMainThreadEventTarget); + if (NS_FAILED(rv)) { + CancelRequest(listener, mChannel, rv); + return; + } + + rv = pump->AsyncRead(listener); + if (NS_FAILED(rv)) { + CancelRequest(listener, mChannel, rv); + } +} + +// Handle an FD sent from the parent. +void ExtensionStreamGetter::OnFD(const FileDescriptor& aFD) { + MOZ_ASSERT(IsNeckoChild()); + MOZ_ASSERT(mListener); + MOZ_ASSERT(mChannel); + + if (!aFD.IsValid()) { + OnStream(nullptr); + return; + } + + // We must keep an owning reference to the listener + // until we pass it on to AsyncOpen. + nsCOMPtr listener = std::move(mListener); + + RefPtr fdFile = new FileDescriptorFile(aFD, mJarFile); + mJarChannel->SetJarFile(fdFile); + nsresult rv = mJarChannel->AsyncOpen(listener); + if (NS_FAILED(rv)) { + CancelRequest(listener, mChannel, rv); + } +} + +NS_IMPL_QUERY_INTERFACE(ExtensionProtocolHandler, + nsISubstitutingProtocolHandler, nsIProtocolHandler, + nsIProtocolHandlerWithDynamicFlags, + nsISupportsWeakReference) +NS_IMPL_ADDREF_INHERITED(ExtensionProtocolHandler, SubstitutingProtocolHandler) +NS_IMPL_RELEASE_INHERITED(ExtensionProtocolHandler, SubstitutingProtocolHandler) + +already_AddRefed +ExtensionProtocolHandler::GetSingleton() { + if (!sSingleton) { + sSingleton = new ExtensionProtocolHandler(); + ClearOnShutdown(&sSingleton); + } + return do_AddRef(sSingleton); +} + +ExtensionProtocolHandler::ExtensionProtocolHandler() + : SubstitutingProtocolHandler(EXTENSION_SCHEME) +#if !defined(XP_WIN) +# if defined(XP_MACOSX) + , + mAlreadyCheckedDevRepo(false) +# endif /* XP_MACOSX */ + , + mAlreadyCheckedAppDir(false) +#endif /* ! XP_WIN */ +{ + // Note, extensions.webextensions.protocol.remote=false is for + // debugging purposes only. With process-level sandboxing, child + // processes (specifically content and extension processes), will + // not be able to load most moz-extension URI's when the pref is + // set to false. + mUseRemoteFileChannels = + IsNeckoChild() && + Preferences::GetBool("extensions.webextensions.protocol.remote"); +} + +static inline ExtensionPolicyService& EPS() { + return ExtensionPolicyService::GetSingleton(); +} + +nsresult ExtensionProtocolHandler::GetFlagsForURI(nsIURI* aURI, + uint32_t* aFlags) { + uint32_t flags = + URI_STD | URI_IS_LOCAL_RESOURCE | URI_IS_POTENTIALLY_TRUSTWORTHY; + + URLInfo url(aURI); + if (auto* policy = EPS().GetByURL(url)) { + // In general a moz-extension URI is only loadable by chrome, but a + // whitelisted subset are web-accessible (and cross-origin fetchable). Check + // that whitelist. + if (policy->IsPathWebAccessible(url.FilePath())) { + flags |= URI_LOADABLE_BY_ANYONE | URI_FETCHABLE_BY_ANYONE; + } else { + flags |= URI_DANGEROUS_TO_LOAD; + } + + // Disallow in private windows if the extension does not have permission. + if (!policy->PrivateBrowsingAllowed()) { + flags |= URI_DISALLOW_IN_PRIVATE_CONTEXT; + } + } else { + // In case there is no policy, then default to treating moz-extension URIs + // as unsafe and generally only allow chrome: to load such. + flags |= URI_DANGEROUS_TO_LOAD; + } + + *aFlags = flags; + return NS_OK; +} + +bool ExtensionProtocolHandler::ResolveSpecialCases(const nsACString& aHost, + const nsACString& aPath, + const nsACString& aPathname, + nsACString& aResult) { + MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread(), + "The ExtensionPolicyService is not thread safe"); + // Create special moz-extension://foo/_generated_background_page.html page + // for all registered extensions. We can't just do this as a substitution + // because substitutions can only match on host. + if (!SubstitutingProtocolHandler::HasSubstitution(aHost)) { + return false; + } + + if (aPathname.EqualsLiteral("/_generated_background_page.html")) { + Unused << EPS().GetGeneratedBackgroundPageUrl(aHost, aResult); + return !aResult.IsEmpty(); + } + + return false; +} + +// For file or JAR URI's, substitute in a remote channel. +Result ExtensionProtocolHandler::SubstituteRemoteChannel( + nsIURI* aURI, nsILoadInfo* aLoadInfo, nsIChannel** aRetVal) { + MOZ_ASSERT(IsNeckoChild()); + MOZ_TRY(aURI ? NS_OK : NS_ERROR_INVALID_ARG); + MOZ_TRY(aLoadInfo ? NS_OK : NS_ERROR_INVALID_ARG); + + nsAutoCString unResolvedSpec; + MOZ_TRY(aURI->GetSpec(unResolvedSpec)); + + nsAutoCString resolvedSpec; + MOZ_TRY(ResolveURI(aURI, resolvedSpec)); + + // Use the target URI scheme to determine if this is a packed or unpacked + // extension URI. For unpacked extensions, we'll request an input stream + // from the parent. For a packed extension, we'll request a file descriptor + // for the JAR file. + nsAutoCString scheme; + MOZ_TRY(net_ExtractURLScheme(resolvedSpec, scheme)); + + if (scheme.EqualsLiteral("file")) { + // Unpacked extension + SubstituteRemoteFileChannel(aURI, aLoadInfo, resolvedSpec, aRetVal); + return Ok(); + } + + if (scheme.EqualsLiteral("jar")) { + // Packed extension + return SubstituteRemoteJarChannel(aURI, aLoadInfo, resolvedSpec, aRetVal); + } + + // Only unpacked resource files and JAR files are remoted. + // No other moz-extension loads should be reading from the filesystem. + return Ok(); +} + +void OpenWhenReady( + Promise* aPromise, nsIStreamListener* aListener, nsIChannel* aChannel, + const std::function& aCallback) { + nsCOMPtr listener(aListener); + nsCOMPtr channel(aChannel); + + Unused << aPromise->ThenWithCycleCollectedArgs( + [channel, aCallback]( + JSContext* aCx, JS::HandleValue aValue, + nsIStreamListener* aListener) -> already_AddRefed { + nsresult rv = aCallback(aListener, channel); + if (NS_FAILED(rv)) { + ExtensionStreamGetter::CancelRequest(aListener, channel, rv); + } + return nullptr; + }, + listener); +} + +nsresult ExtensionProtocolHandler::SubstituteChannel(nsIURI* aURI, + nsILoadInfo* aLoadInfo, + nsIChannel** result) { + if (mUseRemoteFileChannels) { + MOZ_TRY(SubstituteRemoteChannel(aURI, aLoadInfo, result)); + } + + auto* policy = EPS().GetByURL(aURI); + NS_ENSURE_TRUE(policy, NS_ERROR_UNEXPECTED); + + RefPtr readyPromise(policy->ReadyPromise()); + + nsresult rv; + nsCOMPtr url = do_QueryInterface(aURI, &rv); + MOZ_TRY(rv); + + nsAutoCString ext; + MOZ_TRY(url->GetFileExtension(ext)); + ToLowerCase(ext); + + nsCOMPtr channel; + if (ext.EqualsLiteral("css")) { + // Filter CSS files to replace locale message tokens with localized strings. + static const auto convert = [](nsIStreamListener* listener, + nsIChannel* channel, + nsIChannel* origChannel) -> nsresult { + nsresult rv; + nsCOMPtr convService = + do_GetService(NS_STREAMCONVERTERSERVICE_CONTRACTID, &rv); + MOZ_TRY(rv); + + nsCOMPtr uri; + MOZ_TRY(channel->GetURI(getter_AddRefs(uri))); + + const char* kFromType = "application/vnd.mozilla.webext.unlocalized"; + const char* kToType = "text/css"; + + nsCOMPtr converter; + MOZ_TRY(convService->AsyncConvertData(kFromType, kToType, listener, uri, + getter_AddRefs(converter))); + + return origChannel->AsyncOpen(converter); + }; + + channel = NS_NewSimpleChannel( + aURI, aLoadInfo, *result, + [readyPromise](nsIStreamListener* listener, nsIChannel* channel, + nsIChannel* origChannel) -> RequestOrReason { + if (readyPromise) { + nsCOMPtr chan(channel); + OpenWhenReady( + readyPromise, listener, origChannel, + [chan](nsIStreamListener* aListener, nsIChannel* aChannel) { + return convert(aListener, chan, aChannel); + }); + } else { + MOZ_TRY(convert(listener, channel, origChannel)); + } + return RequestOrReason(origChannel); + }); + } else if (readyPromise) { + size_t matchIdx; + if (BinarySearchIf( + sStaticFileExtensions, 0, ArrayLength(sStaticFileExtensions), + [&ext](const char* aOther) { return ext.Compare(aOther); }, + &matchIdx)) { + // This is a static resource that shouldn't depend on the extension being + // ready. Don't bother waiting for it. + return NS_OK; + } + + channel = NS_NewSimpleChannel( + aURI, aLoadInfo, *result, + [readyPromise](nsIStreamListener* listener, nsIChannel* channel, + nsIChannel* origChannel) -> RequestOrReason { + OpenWhenReady(readyPromise, listener, origChannel, + [](nsIStreamListener* aListener, nsIChannel* aChannel) { + return aChannel->AsyncOpen(aListener); + }); + + return RequestOrReason(origChannel); + }); + } else { + return NS_OK; + } + + NS_ENSURE_TRUE(channel, NS_ERROR_OUT_OF_MEMORY); + if (aLoadInfo) { + nsCOMPtr loadInfo = + static_cast(aLoadInfo)->CloneForNewRequest(); + (*result)->SetLoadInfo(loadInfo); + } + + channel.swap(*result); + return NS_OK; +} + +Result ExtensionProtocolHandler::AllowExternalResource( + nsIFile* aExtensionDir, nsIFile* aRequestedFile) { + MOZ_ASSERT(!IsNeckoChild()); + +#if defined(XP_WIN) + // On Windows, dev builds don't use symlinks so we never need to + // allow a resource from outside of the extension dir. + return false; +#else + if (!mozilla::IsDevelopmentBuild()) { + return false; + } + + // On Mac and Linux unpackaged dev builds, system extensions use + // symlinks to point to resources in the repo dir which we have to + // allow loading. Before we allow an unpacked extension to load a + // resource outside of the extension dir, we make sure the extension + // dir is within the app directory. + bool result; + MOZ_TRY_VAR(result, AppDirContains(aExtensionDir)); + if (!result) { + return false; + } + +# if defined(XP_MACOSX) + // Additionally, on Mac dev builds, we make sure that the requested + // resource is within the repo dir. We don't perform this check on Linux + // because we don't have a reliable path to the repo dir on Linux. + return DevRepoContains(aRequestedFile); +# else /* XP_MACOSX */ + return true; +# endif +#endif /* defined(XP_WIN) */ +} + +#if defined(XP_MACOSX) +// The |aRequestedFile| argument must already be Normalize()'d +Result ExtensionProtocolHandler::DevRepoContains( + nsIFile* aRequestedFile) { + MOZ_ASSERT(mozilla::IsDevelopmentBuild()); + MOZ_ASSERT(!IsNeckoChild()); + + // On the first invocation, set mDevRepo + if (!mAlreadyCheckedDevRepo) { + mAlreadyCheckedDevRepo = true; + MOZ_TRY(nsMacUtilsImpl::GetRepoDir(getter_AddRefs(mDevRepo))); + if (MOZ_LOG_TEST(gExtProtocolLog, LogLevel::Debug)) { + nsAutoCString repoPath; + Unused << mDevRepo->GetNativePath(repoPath); + LOG("Repo path: %s", repoPath.get()); + } + } + + bool result = false; + if (mDevRepo) { + MOZ_TRY(mDevRepo->Contains(aRequestedFile, &result)); + } + return result; +} +#endif /* XP_MACOSX */ + +#if !defined(XP_WIN) +Result ExtensionProtocolHandler::AppDirContains( + nsIFile* aExtensionDir) { + MOZ_ASSERT(mozilla::IsDevelopmentBuild()); + MOZ_ASSERT(!IsNeckoChild()); + + // On the first invocation, set mAppDir + if (!mAlreadyCheckedAppDir) { + mAlreadyCheckedAppDir = true; + MOZ_TRY(NS_GetSpecialDirectory(NS_GRE_DIR, getter_AddRefs(mAppDir))); + if (MOZ_LOG_TEST(gExtProtocolLog, LogLevel::Debug)) { + nsAutoCString appDirPath; + Unused << mAppDir->GetNativePath(appDirPath); + LOG("AppDir path: %s", appDirPath.get()); + } + } + + bool result = false; + if (mAppDir) { + MOZ_TRY(mAppDir->Contains(aExtensionDir, &result)); + } + return result; +} +#endif /* !defined(XP_WIN) */ + +static void LogExternalResourceError(nsIFile* aExtensionDir, + nsIFile* aRequestedFile) { + MOZ_ASSERT(aExtensionDir); + MOZ_ASSERT(aRequestedFile); + + LOG("Rejecting external unpacked extension resource [%s] from " + "extension directory [%s]", + aRequestedFile->HumanReadablePath().get(), + aExtensionDir->HumanReadablePath().get()); +} + +Result, nsresult> ExtensionProtocolHandler::NewStream( + nsIURI* aChildURI, bool* aTerminateSender) { + MOZ_ASSERT(!IsNeckoChild()); + MOZ_TRY(aChildURI ? NS_OK : NS_ERROR_INVALID_ARG); + MOZ_TRY(aTerminateSender ? NS_OK : NS_ERROR_INVALID_ARG); + + *aTerminateSender = true; + nsresult rv; + + // We should never receive a URI that isn't for a moz-extension because + // these requests ordinarily come from the child's ExtensionProtocolHandler. + // Ensure this request is for a moz-extension URI. A rogue child process + // could send us any URI. + if (!aChildURI->SchemeIs(EXTENSION_SCHEME)) { + return Err(NS_ERROR_UNKNOWN_PROTOCOL); + } + + // For errors after this point, we want to propagate the error to + // the child, but we don't force the child to be terminated because + // the error is likely to be due to a bug in the extension. + *aTerminateSender = false; + + /* + * Make sure there is a substitution installed for the host found + * in the child's request URI and make sure the host resolves to + * a directory. + */ + + nsAutoCString host; + MOZ_TRY(aChildURI->GetAsciiHost(host)); + + // Lookup the directory this host string resolves to + nsCOMPtr baseURI; + MOZ_TRY(GetSubstitution(host, getter_AddRefs(baseURI))); + + // The result should be a file URL for the extension base dir + nsCOMPtr fileURL = do_QueryInterface(baseURI, &rv); + MOZ_TRY(rv); + + nsCOMPtr extensionDir; + MOZ_TRY(fileURL->GetFile(getter_AddRefs(extensionDir))); + + bool isDirectory = false; + MOZ_TRY(extensionDir->IsDirectory(&isDirectory)); + if (!isDirectory) { + // The host should map to a directory for unpacked extensions + return Err(NS_ERROR_FILE_NOT_DIRECTORY); + } + + // Make sure the child URI resolves to a file URI then get a file + // channel for the request. The resultant channel should be a + // file channel because we only request remote streams for unpacked + // extension resource loads where the URI resolves to a file. + nsAutoCString resolvedSpec; + MOZ_TRY(ResolveURI(aChildURI, resolvedSpec)); + + nsAutoCString resolvedScheme; + MOZ_TRY(net_ExtractURLScheme(resolvedSpec, resolvedScheme)); + if (!resolvedScheme.EqualsLiteral("file")) { + return Err(NS_ERROR_UNEXPECTED); + } + + nsCOMPtr ioService = do_GetIOService(&rv); + MOZ_TRY(rv); + + nsCOMPtr resolvedURI; + MOZ_TRY(ioService->NewURI(resolvedSpec, nullptr, nullptr, + getter_AddRefs(resolvedURI))); + + // We use the system principal to get a file channel for the request, + // but only after we've checked (above) that the child URI is of + // moz-extension scheme and that the URI host maps to a directory. + nsCOMPtr channel; + MOZ_TRY(NS_NewChannel(getter_AddRefs(channel), resolvedURI, + nsContentUtils::GetSystemPrincipal(), + nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + nsIContentPolicy::TYPE_OTHER)); + + nsCOMPtr fileChannel = do_QueryInterface(channel, &rv); + MOZ_TRY(rv); + + nsCOMPtr requestedFile; + MOZ_TRY(fileChannel->GetFile(getter_AddRefs(requestedFile))); + + /* + * Make sure the file we resolved to is within the extension directory. + */ + + // Normalize paths for sane comparisons. nsIFile::Contains depends on + // it for reliable subpath checks. + MOZ_TRY(extensionDir->Normalize()); + MOZ_TRY(requestedFile->Normalize()); +#if defined(XP_WIN) + if (!widget::WinUtils::ResolveJunctionPointsAndSymLinks(extensionDir) || + !widget::WinUtils::ResolveJunctionPointsAndSymLinks(requestedFile)) { + return Err(NS_ERROR_FILE_ACCESS_DENIED); + } +#endif + + bool isResourceFromExtensionDir = false; + MOZ_TRY(extensionDir->Contains(requestedFile, &isResourceFromExtensionDir)); + if (!isResourceFromExtensionDir) { + bool isAllowed; + MOZ_TRY_VAR(isAllowed, AllowExternalResource(extensionDir, requestedFile)); + if (!isAllowed) { + LogExternalResourceError(extensionDir, requestedFile); + return Err(NS_ERROR_FILE_ACCESS_DENIED); + } + } + + nsCOMPtr inputStream; + MOZ_TRY_VAR(inputStream, + NS_NewLocalFileInputStream(requestedFile, PR_RDONLY, -1, + nsIFileInputStream::DEFER_OPEN)); + + return inputStream; +} + +Result ExtensionProtocolHandler::NewFD( + nsIURI* aChildURI, bool* aTerminateSender, + NeckoParent::GetExtensionFDResolver& aResolve) { + MOZ_ASSERT(!IsNeckoChild()); + MOZ_TRY(aChildURI ? NS_OK : NS_ERROR_INVALID_ARG); + MOZ_TRY(aTerminateSender ? NS_OK : NS_ERROR_INVALID_ARG); + + *aTerminateSender = true; + nsresult rv; + + // Ensure this is a moz-extension URI + if (!aChildURI->SchemeIs(EXTENSION_SCHEME)) { + return Err(NS_ERROR_UNKNOWN_PROTOCOL); + } + + // For errors after this point, we want to propagate the error to + // the child, but we don't force the child to be terminated. + *aTerminateSender = false; + + nsAutoCString host; + MOZ_TRY(aChildURI->GetAsciiHost(host)); + + // We expect the host string to map to a JAR file because the URI + // should refer to a web accessible resource for an enabled extension. + nsCOMPtr subURI; + MOZ_TRY(GetSubstitution(host, getter_AddRefs(subURI))); + + nsCOMPtr jarURI = do_QueryInterface(subURI, &rv); + MOZ_TRY(rv); + + nsCOMPtr innerFileURI; + MOZ_TRY(jarURI->GetJARFile(getter_AddRefs(innerFileURI))); + + nsCOMPtr innerFileURL = do_QueryInterface(innerFileURI, &rv); + MOZ_TRY(rv); + + nsCOMPtr jarFile; + MOZ_TRY(innerFileURL->GetFile(getter_AddRefs(jarFile))); + + if (!mFileOpenerThread) { + mFileOpenerThread = new LazyIdleThread(DEFAULT_THREAD_TIMEOUT_MS, + "ExtensionProtocolHandler"_ns); + } + + RefPtr fileOpener = + new ExtensionJARFileOpener(jarFile, aResolve); + + nsCOMPtr event = mozilla::NewRunnableMethod( + "ExtensionJarFileOpener", fileOpener, &ExtensionJARFileOpener::OpenFile); + + MOZ_TRY(mFileOpenerThread->Dispatch(event, nsIEventTarget::DISPATCH_NORMAL)); + + return Ok(); +} + +// Set the channel's content type using the provided URI's type + +// static +void ExtensionProtocolHandler::SetContentType(nsIURI* aURI, + nsIChannel* aChannel) { + nsresult rv; + nsCOMPtr mime = do_GetService("@mozilla.org/mime;1", &rv); + if (NS_SUCCEEDED(rv)) { + nsAutoCString contentType; + rv = mime->GetTypeFromURI(aURI, contentType); + if (NS_SUCCEEDED(rv)) { + Unused << aChannel->SetContentType(contentType); + } + } +} + +// Gets a SimpleChannel that wraps the provided ExtensionStreamGetter + +// static +void ExtensionProtocolHandler::NewSimpleChannel( + nsIURI* aURI, nsILoadInfo* aLoadinfo, ExtensionStreamGetter* aStreamGetter, + nsIChannel** aRetVal) { + nsCOMPtr channel = NS_NewSimpleChannel( + aURI, aLoadinfo, aStreamGetter, + [](nsIStreamListener* listener, nsIChannel* simpleChannel, + ExtensionStreamGetter* getter) -> RequestOrReason { + MOZ_TRY(getter->GetAsync(listener, simpleChannel)); + return RequestOrReason(nullptr); + }); + + SetContentType(aURI, channel); + channel.swap(*aRetVal); +} + +// Gets a SimpleChannel that wraps the provided channel + +// static +void ExtensionProtocolHandler::NewSimpleChannel(nsIURI* aURI, + nsILoadInfo* aLoadinfo, + nsIChannel* aChannel, + nsIChannel** aRetVal) { + nsCOMPtr channel = NS_NewSimpleChannel( + aURI, aLoadinfo, aChannel, + [](nsIStreamListener* listener, nsIChannel* simpleChannel, + nsIChannel* origChannel) -> RequestOrReason { + nsresult rv = origChannel->AsyncOpen(listener); + if (NS_FAILED(rv)) { + simpleChannel->Cancel(NS_BINDING_ABORTED); + return Err(rv); + } + return RequestOrReason(origChannel); + }); + + SetContentType(aURI, channel); + channel.swap(*aRetVal); +} + +void ExtensionProtocolHandler::SubstituteRemoteFileChannel( + nsIURI* aURI, nsILoadInfo* aLoadinfo, nsACString& aResolvedFileSpec, + nsIChannel** aRetVal) { + MOZ_ASSERT(IsNeckoChild()); + + RefPtr streamGetter = + new ExtensionStreamGetter(aURI, aLoadinfo); + + NewSimpleChannel(aURI, aLoadinfo, streamGetter, aRetVal); +} + +static Result LogCacheCheck(const nsIJARChannel* aJarChannel, + nsIJARURI* aJarURI, bool aIsCached) { + nsresult rv; + + nsCOMPtr innerFileURI; + MOZ_TRY(aJarURI->GetJARFile(getter_AddRefs(innerFileURI))); + + nsCOMPtr innerFileURL = do_QueryInterface(innerFileURI, &rv); + MOZ_TRY(rv); + + nsCOMPtr jarFile; + MOZ_TRY(innerFileURL->GetFile(getter_AddRefs(jarFile))); + + nsAutoCString uriSpec, jarSpec; + Unused << aJarURI->GetSpec(uriSpec); + Unused << innerFileURI->GetSpec(jarSpec); + LOG("[JARChannel %p] Cache %s: %s (%s)", aJarChannel, + aIsCached ? "hit" : "miss", uriSpec.get(), jarSpec.get()); + + return Ok(); +} + +Result ExtensionProtocolHandler::SubstituteRemoteJarChannel( + nsIURI* aURI, nsILoadInfo* aLoadinfo, nsACString& aResolvedSpec, + nsIChannel** aRetVal) { + MOZ_ASSERT(IsNeckoChild()); + nsresult rv; + + // Build a JAR URI for this jar:file:// URI and use it to extract the + // inner file URI. + nsCOMPtr uri; + MOZ_TRY(NS_NewURI(getter_AddRefs(uri), aResolvedSpec)); + + nsCOMPtr jarURI = do_QueryInterface(uri, &rv); + MOZ_TRY(rv); + + nsCOMPtr jarChannel = do_QueryInterface(*aRetVal, &rv); + MOZ_TRY(rv); + + bool isCached = false; + MOZ_TRY(jarChannel->EnsureCached(&isCached)); + if (MOZ_LOG_TEST(gExtProtocolLog, LogLevel::Debug)) { + Unused << LogCacheCheck(jarChannel, jarURI, isCached); + } + + if (isCached) { + // Using a SimpleChannel with an ExtensionStreamGetter here (like the + // non-cached JAR case) isn't needed to load the extension resource + // because we don't need to ask the parent for an FD for the JAR, but + // wrapping the JARChannel in a SimpleChannel allows HTTP forwarding to + // moz-extension URI's to work because HTTP forwarding requires the + // target channel implement nsIChildChannel. + NewSimpleChannel(aURI, aLoadinfo, jarChannel.get(), aRetVal); + return Ok(); + } + + nsCOMPtr innerFileURI; + MOZ_TRY(jarURI->GetJARFile(getter_AddRefs(innerFileURI))); + + nsCOMPtr innerFileURL = do_QueryInterface(innerFileURI, &rv); + MOZ_TRY(rv); + + nsCOMPtr jarFile; + MOZ_TRY(innerFileURL->GetFile(getter_AddRefs(jarFile))); + + RefPtr streamGetter = + new ExtensionStreamGetter(aURI, aLoadinfo, jarChannel.forget(), jarFile); + + NewSimpleChannel(aURI, aLoadinfo, streamGetter, aRetVal); + return Ok(); +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/res/ExtensionProtocolHandler.h b/netwerk/protocol/res/ExtensionProtocolHandler.h new file mode 100644 index 0000000000..ec253702e9 --- /dev/null +++ b/netwerk/protocol/res/ExtensionProtocolHandler.h @@ -0,0 +1,236 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef ExtensionProtocolHandler_h___ +#define ExtensionProtocolHandler_h___ + +#include "mozilla/net/NeckoParent.h" +#include "mozilla/LazyIdleThread.h" +#include "mozilla/Result.h" +#include "SubstitutingProtocolHandler.h" + +namespace mozilla { +namespace net { + +class ExtensionStreamGetter; + +class ExtensionProtocolHandler final + : public nsISubstitutingProtocolHandler, + public nsIProtocolHandlerWithDynamicFlags, + public SubstitutingProtocolHandler, + public nsSupportsWeakReference { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_NSIPROTOCOLHANDLERWITHDYNAMICFLAGS + NS_FORWARD_NSIPROTOCOLHANDLER(SubstitutingProtocolHandler::) + NS_FORWARD_NSISUBSTITUTINGPROTOCOLHANDLER(SubstitutingProtocolHandler::) + + static already_AddRefed GetSingleton(); + + /** + * To be called in the parent process to obtain an input stream for a + * a web accessible resource from an unpacked WebExtension dir. + * + * @param aChildURI a moz-extension URI sent from the child that refers + * to a web accessible resource file in an enabled unpacked extension + * @param aTerminateSender out param set to true when the params are invalid + * and indicate the child should be terminated. If |aChildURI| is + * not a moz-extension URI, the child is in an invalid state and + * should be terminated. + * @return NS_OK with |aTerminateSender| set to false on success. On + * failure, returns an error and sets |aTerminateSender| to indicate + * whether or not the child process should be terminated. + * A moz-extension URI from the child that doesn't resolve to a + * resource file within the extension could be the result of a bug + * in the extension and doesn't result in |aTerminateSender| being + * set to true. + */ + Result, nsresult> NewStream(nsIURI* aChildURI, + bool* aTerminateSender); + + /** + * To be called in the parent process to obtain a file descriptor for an + * enabled WebExtension JAR file. + * + * @param aChildURI a moz-extension URI sent from the child that refers + * to a web accessible resource file in an enabled unpacked extension + * @param aTerminateSender out param set to true when the params are invalid + * and indicate the child should be terminated. If |aChildURI| is + * not a moz-extension URI, the child is in an invalid state and + * should be terminated. + * @param aPromise a promise that will be resolved asynchronously when the + * file descriptor is available. + * @return NS_OK with |aTerminateSender| set to false on success. On + * failure, returns an error and sets |aTerminateSender| to indicate + * whether or not the child process should be terminated. + * A moz-extension URI from the child that doesn't resolve to an + * enabled WebExtension JAR could be the result of a bug in the + * extension and doesn't result in |aTerminateSender| being + * set to true. + */ + Result NewFD(nsIURI* aChildURI, bool* aTerminateSender, + NeckoParent::GetExtensionFDResolver& aResolve); + + protected: + ~ExtensionProtocolHandler() = default; + + private: + explicit ExtensionProtocolHandler(); + + [[nodiscard]] bool ResolveSpecialCases(const nsACString& aHost, + const nsACString& aPath, + const nsACString& aPathname, + nsACString& aResult) override; + + // |result| is an inout param. On entry to this function, *result + // is expected to be non-null and already addrefed. This function + // may release the object stored in *result on entry and write + // a new pointer to an already addrefed channel to *result. + [[nodiscard]] virtual nsresult SubstituteChannel( + nsIURI* uri, nsILoadInfo* aLoadInfo, nsIChannel** result) override; + + /** + * For moz-extension URI's that resolve to file or JAR URI's, replaces + * the provided channel with a channel that will proxy the load to the + * parent process. For moz-extension URI's that resolve to other types + * of URI's (not file or JAR), the provide channel is not replaced and + * NS_OK is returned. + * + * @param aURI the moz-extension URI + * @param aLoadInfo the loadinfo for the request + * @param aRetVal in/out channel param referring to the channel that + * might need to be substituted with a remote channel. + * @return NS_OK if the channel does not need to be substituted or + * or the replacement channel was created successfully. + * Otherwise returns an error. + */ + Result SubstituteRemoteChannel(nsIURI* aURI, + nsILoadInfo* aLoadInfo, + nsIChannel** aRetVal); + + /** + * Replaces a file channel with a remote file channel for loading a + * web accessible resource for an unpacked extension from the parent. + * + * @param aURI the moz-extension URI + * @param aLoadInfo the loadinfo for the request + * @param aResolvedFileSpec the resolved URI spec for the file. + * @param aRetVal in/out param referring to the new remote channel. + * The reference to the input param file channel is dropped and + * replaced with a reference to a new channel that remotes + * the file access. The new channel encapsulates a request to + * the parent for an IPCStream for the file. + */ + void SubstituteRemoteFileChannel(nsIURI* aURI, nsILoadInfo* aLoadinfo, + nsACString& aResolvedFileSpec, + nsIChannel** aRetVal); + + /** + * Replaces a JAR channel with a remote JAR channel for loading a + * an extension JAR file from the parent. + * + * @param aURI the moz-extension URI + * @param aLoadInfo the loadinfo for the request + * @param aResolvedFileSpec the resolved URI spec for the file. + * @param aRetVal in/out param referring to the new remote channel. + * The input param JAR channel is replaced with a new channel + * that remotes the JAR file access. The new channel encapsulates + * a request to the parent for the JAR file FD. + */ + Result SubstituteRemoteJarChannel(nsIURI* aURI, + nsILoadInfo* aLoadinfo, + nsACString& aResolvedSpec, + nsIChannel** aRetVal); + + /** + * Sets the aResult outparam to true if this unpacked extension load of + * a resource that is outside the extension dir should be allowed. This + * is only allowed for system extensions on Mac and Linux dev builds. + * + * @param aExtensionDir the extension directory. Argument must be an + * nsIFile for which Normalize() has already been called. + * @param aRequestedFile the requested web-accessible resource file. Argument + * must be an nsIFile for which Normalize() has already been called. + */ + Result AllowExternalResource(nsIFile* aExtensionDir, + nsIFile* aRequestedFile); + + // Set the channel's content type using the provided URI's type + static void SetContentType(nsIURI* aURI, nsIChannel* aChannel); + + // Gets a SimpleChannel that wraps the provided ExtensionStreamGetter + static void NewSimpleChannel(nsIURI* aURI, nsILoadInfo* aLoadinfo, + ExtensionStreamGetter* aStreamGetter, + nsIChannel** aRetVal); + + // Gets a SimpleChannel that wraps the provided channel + static void NewSimpleChannel(nsIURI* aURI, nsILoadInfo* aLoadinfo, + nsIChannel* aChannel, nsIChannel** aRetVal); + +#if defined(XP_MACOSX) + /** + * Sets the aResult outparam to true if we are a developer build with the + * repo dir environment variable set and the requested file resides in the + * repo dir. Developer builds may load system extensions with web-accessible + * resources that are symlinks to files in the repo dir. This method is for + * checking if an unpacked resource requested by the child is from the repo. + * The requested file must be already Normalized(). Only compile this for + * Mac because the repo dir isn't always available on Linux. + * + * @param aRequestedFile the requested web-accessible resource file. Argument + * must be an nsIFile for which Normalize() has already been called. + */ + Result DevRepoContains(nsIFile* aRequestedFile); + + // On development builds, this points to development repo. Lazily set. + nsCOMPtr mDevRepo; + + // Set to true once we've already tried to load the dev repo path, + // allowing for lazy initialization of |mDevRepo|. + bool mAlreadyCheckedDevRepo; +#endif /* XP_MACOSX */ + +#if !defined(XP_WIN) + /** + * Sets the aResult outparam to true if we are a developer build and the + * provided directory is within the NS_GRE_DIR directory. Developer builds + * may load system extensions with web-accessible resources that are symlinks + * to files outside of the extension dir to the repo dir. This method is for + * checking if an extension directory is within NS_GRE_DIR. In that case, we + * consider the extension a system extension and allow it to use symlinks to + * resources outside of the extension dir. This exception is only applied + * to loads for unpacked extensions in unpackaged developer builds. + * The requested dir must be already Normalized(). + * + * @param aExtensionDir the extension directory. Argument must be an + * nsIFile for which Normalize() has already been called. + */ + Result AppDirContains(nsIFile* aExtensionDir); + + // On development builds, cache the NS_GRE_DIR repo. Lazily set. + nsCOMPtr mAppDir; + + // Set to true once we've already read the AppDir, allowing for lazy + // initialization of |mAppDir|. + bool mAlreadyCheckedAppDir; +#endif /* !defined(XP_WIN) */ + + // Used for opening JAR files off the main thread when we just need to + // obtain a file descriptor to send back to the child. + RefPtr mFileOpenerThread; + + // To allow parent IPDL actors to invoke methods on this handler when + // handling moz-extension requests from the child. + static StaticRefPtr sSingleton; + + // Set to true when this instance of the handler must proxy loads of + // extension web-accessible resources to the parent process. + bool mUseRemoteFileChannels; +}; + +} // namespace net +} // namespace mozilla + +#endif /* ExtensionProtocolHandler_h___ */ diff --git a/netwerk/protocol/res/PageThumbProtocolHandler.cpp b/netwerk/protocol/res/PageThumbProtocolHandler.cpp new file mode 100644 index 0000000000..02ee4e67cf --- /dev/null +++ b/netwerk/protocol/res/PageThumbProtocolHandler.cpp @@ -0,0 +1,468 @@ +/* -*- 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 "PageThumbProtocolHandler.h" + +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/ipc/URIParams.h" +#include "mozilla/ipc/URIUtils.h" +#include "mozilla/net/NeckoChild.h" +#include "mozilla/RefPtr.h" +#include "mozilla/ResultExtensions.h" + +#include "LoadInfo.h" +#include "nsContentUtils.h" +#include "nsServiceManagerUtils.h" +#include "nsIFile.h" +#include "nsIFileChannel.h" +#include "nsIFileStreams.h" +#include "nsIMIMEService.h" +#include "nsIURL.h" +#include "nsIChannel.h" +#include "nsIPageThumbsStorageService.h" +#include "nsIInputStreamPump.h" +#include "nsIStreamListener.h" +#include "nsIInputStream.h" +#include "nsNetUtil.h" +#include "nsURLHelper.h" +#include "prio.h" +#include "SimpleChannel.h" + +#define PAGE_THUMB_HOST "thumbnails" +#define PAGE_THUMB_SCHEME "moz-page-thumb" + +namespace mozilla { +namespace net { + +LazyLogModule gPageThumbProtocolLog("PageThumbProtocol"); + +#undef LOG +#define LOG(level, ...) \ + MOZ_LOG(gPageThumbProtocolLog, LogLevel::level, (__VA_ARGS__)) + +StaticRefPtr PageThumbProtocolHandler::sSingleton; + +/** + * Helper class used with SimpleChannel to asynchronously obtain an input + * stream from the parent for a remote moz-page-thumb load from the child. + */ +class PageThumbStreamGetter : public RefCounted { + public: + PageThumbStreamGetter(nsIURI* aURI, nsILoadInfo* aLoadInfo) + : mURI(aURI), mLoadInfo(aLoadInfo) { + MOZ_ASSERT(aURI); + MOZ_ASSERT(aLoadInfo); + + SetupEventTarget(); + } + + ~PageThumbStreamGetter() = default; + + void SetupEventTarget() { + mMainThreadEventTarget = nsContentUtils::GetEventTargetByLoadInfo( + mLoadInfo, TaskCategory::Other); + if (!mMainThreadEventTarget) { + mMainThreadEventTarget = GetMainThreadSerialEventTarget(); + } + } + + // Get an input stream from the parent asynchronously. + Result GetAsync(nsIStreamListener* aListener, + nsIChannel* aChannel); + + // Handle an input stream being returned from the parent + void OnStream(already_AddRefed aStream); + + static void CancelRequest(nsIStreamListener* aListener, nsIChannel* aChannel, + nsresult aResult); + + MOZ_DECLARE_REFCOUNTED_TYPENAME(PageThumbStreamGetter) + + private: + nsCOMPtr mURI; + nsCOMPtr mLoadInfo; + nsCOMPtr mListener; + nsCOMPtr mChannel; + nsCOMPtr mMainThreadEventTarget; +}; + +// Request an input stream from the parent. +Result PageThumbStreamGetter::GetAsync( + nsIStreamListener* aListener, nsIChannel* aChannel) { + MOZ_ASSERT(IsNeckoChild()); + MOZ_ASSERT(mMainThreadEventTarget); + + mListener = aListener; + mChannel = aChannel; + + RefPtr self = this; + + // Request an input stream for this moz-page-thumb URI. + gNeckoChild->SendGetPageThumbStream(mURI)->Then( + mMainThreadEventTarget, __func__, + [self](const RefPtr& stream) { + self->OnStream(do_AddRef(stream)); + }, + [self](const mozilla::ipc::ResponseRejectReason) { + self->OnStream(nullptr); + }); + return Ok(); +} + +// static +void PageThumbStreamGetter::CancelRequest(nsIStreamListener* aListener, + nsIChannel* aChannel, + nsresult aResult) { + MOZ_ASSERT(aListener); + MOZ_ASSERT(aChannel); + + aListener->OnStartRequest(aChannel); + aListener->OnStopRequest(aChannel, aResult); + aChannel->Cancel(NS_BINDING_ABORTED); +} + +// Handle an input stream sent from the parent. +void PageThumbStreamGetter::OnStream(already_AddRefed aStream) { + MOZ_ASSERT(IsNeckoChild()); + MOZ_ASSERT(mListener); + MOZ_ASSERT(mMainThreadEventTarget); + + nsCOMPtr stream = std::move(aStream); + + // We must keep an owning reference to the listener until we pass it on + // to AsyncRead. + nsCOMPtr listener = mListener.forget(); + + MOZ_ASSERT(mChannel); + + if (!stream) { + // The parent didn't send us back a stream. + CancelRequest(listener, mChannel, NS_ERROR_FILE_ACCESS_DENIED); + return; + } + + nsCOMPtr pump; + nsresult rv = NS_NewInputStreamPump(getter_AddRefs(pump), stream.forget(), 0, + 0, false, mMainThreadEventTarget); + if (NS_FAILED(rv)) { + CancelRequest(listener, mChannel, rv); + return; + } + + rv = pump->AsyncRead(listener); + if (NS_FAILED(rv)) { + CancelRequest(listener, mChannel, rv); + } +} + +NS_IMPL_QUERY_INTERFACE(PageThumbProtocolHandler, + nsISubstitutingProtocolHandler, nsIProtocolHandler, + nsIProtocolHandlerWithDynamicFlags, + nsISupportsWeakReference) +NS_IMPL_ADDREF_INHERITED(PageThumbProtocolHandler, SubstitutingProtocolHandler) +NS_IMPL_RELEASE_INHERITED(PageThumbProtocolHandler, SubstitutingProtocolHandler) + +already_AddRefed +PageThumbProtocolHandler::GetSingleton() { + if (!sSingleton) { + sSingleton = new PageThumbProtocolHandler(); + ClearOnShutdown(&sSingleton); + } + + return do_AddRef(sSingleton); +} + +PageThumbProtocolHandler::PageThumbProtocolHandler() + : SubstitutingProtocolHandler(PAGE_THUMB_SCHEME) {} + +nsresult PageThumbProtocolHandler::GetFlagsForURI(nsIURI* aURI, + uint32_t* aFlags) { + // A moz-page-thumb URI is only loadable by chrome pages in the parent + // process, or privileged content running in the privileged about content + // process. + *aFlags = URI_STD | URI_IS_UI_RESOURCE | URI_IS_LOCAL_RESOURCE | + URI_NORELATIVE | URI_NOAUTH; + + return NS_OK; +} + +RefPtr PageThumbProtocolHandler::NewStream( + nsIURI* aChildURI, bool* aTerminateSender) { + MOZ_ASSERT(!IsNeckoChild()); + MOZ_ASSERT(NS_IsMainThread()); + + if (!aChildURI || !aTerminateSender) { + return PageThumbStreamPromise::CreateAndReject(NS_ERROR_INVALID_ARG, + __func__); + } + + *aTerminateSender = true; + nsresult rv; + + // We should never receive a URI that isn't for a moz-page-thumb because + // these requests ordinarily come from the child's PageThumbProtocolHandler. + // Ensure this request is for a moz-page-thumb URI. A compromised child + // process could send us any URI. + bool isPageThumbScheme = false; + if (NS_FAILED(aChildURI->SchemeIs(PAGE_THUMB_SCHEME, &isPageThumbScheme)) || + !isPageThumbScheme) { + return PageThumbStreamPromise::CreateAndReject(NS_ERROR_UNKNOWN_PROTOCOL, + __func__); + } + + // We should never receive a URI that does not have "thumbnails" as the host. + nsAutoCString host; + if (NS_FAILED(aChildURI->GetAsciiHost(host)) || + !host.EqualsLiteral(PAGE_THUMB_HOST)) { + return PageThumbStreamPromise::CreateAndReject(NS_ERROR_UNEXPECTED, + __func__); + } + + // For errors after this point, we want to propagate the error to + // the child, but we don't force the child process to be terminated. + *aTerminateSender = false; + + // Make sure the child URI resolves to a file URI. We will then get a file + // channel for the request. The resultant channel should be a file channel + // because we only request remote streams for resource loads where the URI + // resolves to a file. + nsAutoCString resolvedSpec; + rv = ResolveURI(aChildURI, resolvedSpec); + if (NS_FAILED(rv)) { + return PageThumbStreamPromise::CreateAndReject(rv, __func__); + } + + nsAutoCString resolvedScheme; + rv = net_ExtractURLScheme(resolvedSpec, resolvedScheme); + if (NS_FAILED(rv) || !resolvedScheme.EqualsLiteral("file")) { + return PageThumbStreamPromise::CreateAndReject(NS_ERROR_UNEXPECTED, + __func__); + } + + nsCOMPtr ioService = do_GetIOService(&rv); + if (NS_FAILED(rv)) { + return PageThumbStreamPromise::CreateAndReject(rv, __func__); + } + + nsCOMPtr resolvedURI; + rv = ioService->NewURI(resolvedSpec, nullptr, nullptr, + getter_AddRefs(resolvedURI)); + if (NS_FAILED(rv)) { + return PageThumbStreamPromise::CreateAndReject(rv, __func__); + } + + // We use the system principal to get a file channel for the request, + // but only after we've checked (above) that the child URI is of + // moz-page-thumb scheme and that the URI host matches PAGE_THUMB_HOST. + nsCOMPtr channel; + rv = NS_NewChannel(getter_AddRefs(channel), resolvedURI, + nsContentUtils::GetSystemPrincipal(), + nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + nsIContentPolicy::TYPE_OTHER); + if (NS_FAILED(rv)) { + return PageThumbStreamPromise::CreateAndReject(rv, __func__); + } + + auto promiseHolder = MakeUnique>(); + RefPtr promise = promiseHolder->Ensure(__func__); + + rv = NS_DispatchBackgroundTask( + NS_NewRunnableFunction( + "PageThumbProtocolHandler::NewStream", + [channel, holder = std::move(promiseHolder)]() { + nsresult rv; + + nsCOMPtr fileChannel = + do_QueryInterface(channel, &rv); + if (NS_FAILED(rv)) { + holder->Reject(rv, __func__); + } + + nsCOMPtr requestedFile; + rv = fileChannel->GetFile(getter_AddRefs(requestedFile)); + if (NS_FAILED(rv)) { + holder->Reject(rv, __func__); + return; + } + + nsCOMPtr inputStream; + rv = NS_NewLocalFileInputStream(getter_AddRefs(inputStream), + requestedFile, PR_RDONLY, -1); + if (NS_FAILED(rv)) { + holder->Reject(rv, __func__); + return; + } + + holder->Resolve(inputStream, __func__); + }), + NS_DISPATCH_EVENT_MAY_BLOCK); + + if (NS_FAILED(rv)) { + return PageThumbStreamPromise::CreateAndReject(rv, __func__); + } + + return promise; +} + +bool PageThumbProtocolHandler::ResolveSpecialCases(const nsACString& aHost, + const nsACString& aPath, + const nsACString& aPathname, + nsACString& aResult) { + // This should match the scheme in PageThumbs.jsm. We will only resolve + // URIs for thumbnails generated by PageThumbs here. + if (!aHost.EqualsLiteral(PAGE_THUMB_HOST)) { + // moz-page-thumb should always have a "thumbnails" host. We do not intend + // to allow substitution rules to be created for moz-page-thumb. + return false; + } + + // Regardless of the outcome, the scheme will be resolved to file://. + aResult.Assign("file://"); + + if (IsNeckoChild()) { + // We will resolve the URI in the parent if load is performed in the child + // because the child does not have access to the profile directory path. + // Technically we could retrieve the path from dom::ContentChild, but I + // would prefer to obtain the path from PageThumbsStorageService (which + // depends on OS.Path). Here, we resolve to the same URI, with the file:// + // scheme. This won't ever be accessed directly by the content process, + // and is mainly used to keep the substitution protocol handler mechanism + // happy. + aResult.Append(aHost); + aResult.Append(aPath); + } else { + // Resolve the URI in the parent to the thumbnail file URI since we will + // attempt to open the channel to load the file after this. + nsAutoString thumbnailUrl; + nsresult rv = GetThumbnailPath(aPath, thumbnailUrl); + if (NS_WARN_IF(NS_FAILED(rv))) { + return false; + } + + aResult.Append(NS_ConvertUTF16toUTF8(thumbnailUrl)); + } + + return true; +} + +nsresult PageThumbProtocolHandler::SubstituteChannel(nsIURI* aURI, + nsILoadInfo* aLoadInfo, + nsIChannel** aRetVal) { + // Check if URI resolves to a file URI. + nsAutoCString resolvedSpec; + MOZ_TRY(ResolveURI(aURI, resolvedSpec)); + + nsAutoCString scheme; + MOZ_TRY(net_ExtractURLScheme(resolvedSpec, scheme)); + + if (!scheme.EqualsLiteral("file")) { + NS_WARNING("moz-page-thumb URIs should only resolve to file URIs."); + return NS_ERROR_NO_INTERFACE; + } + + // Load the URI remotely if accessed from a child. + if (IsNeckoChild()) { + MOZ_TRY(SubstituteRemoteChannel(aURI, aLoadInfo, aRetVal)); + } + + return NS_OK; +} + +Result PageThumbProtocolHandler::SubstituteRemoteChannel( + nsIURI* aURI, nsILoadInfo* aLoadInfo, nsIChannel** aRetVal) { + MOZ_ASSERT(IsNeckoChild()); + MOZ_TRY(aURI ? NS_OK : NS_ERROR_INVALID_ARG); + MOZ_TRY(aLoadInfo ? NS_OK : NS_ERROR_INVALID_ARG); + +#ifdef DEBUG + nsAutoCString resolvedSpec; + MOZ_TRY(ResolveURI(aURI, resolvedSpec)); + + nsAutoCString scheme; + MOZ_TRY(net_ExtractURLScheme(resolvedSpec, scheme)); + + MOZ_ASSERT(scheme.EqualsLiteral("file")); +#endif /* DEBUG */ + + RefPtr streamGetter = + new PageThumbStreamGetter(aURI, aLoadInfo); + + NewSimpleChannel(aURI, aLoadInfo, streamGetter, aRetVal); + return Ok(); +} + +nsresult PageThumbProtocolHandler::GetThumbnailPath(const nsACString& aPath, + nsString& aThumbnailPath) { + MOZ_ASSERT(!IsNeckoChild()); + + // Ensures that the provided path has a query string. We will start parsing + // from there. + int32_t queryIndex = aPath.FindChar('?'); + if (queryIndex <= 0) { + return NS_ERROR_MALFORMED_URI; + } + + nsresult rv; + + nsCOMPtr pageThumbsStorage = + do_GetService("@mozilla.org/thumbnails/pagethumbs-service;1", &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Extract URL from query string. + nsAutoString url; + bool found = + URLParams::Extract(Substring(aPath, queryIndex + 1), u"url"_ns, url); + if (!found || url.IsVoid()) { + return NS_ERROR_NOT_AVAILABLE; + } + + // Use PageThumbsStorageService to get the local file path of the screenshot + // for the given URL. + rv = pageThumbsStorage->GetFilePathForURL(url, aThumbnailPath); + + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +// static +void PageThumbProtocolHandler::SetContentType(nsIURI* aURI, + nsIChannel* aChannel) { + nsresult rv; + nsCOMPtr mime = do_GetService("@mozilla.org/mime;1", &rv); + if (NS_SUCCEEDED(rv)) { + nsAutoCString contentType; + rv = mime->GetTypeFromURI(aURI, contentType); + if (NS_SUCCEEDED(rv)) { + Unused << aChannel->SetContentType(contentType); + } + } +} + +// static +void PageThumbProtocolHandler::NewSimpleChannel( + nsIURI* aURI, nsILoadInfo* aLoadinfo, PageThumbStreamGetter* aStreamGetter, + nsIChannel** aRetVal) { + nsCOMPtr channel = NS_NewSimpleChannel( + aURI, aLoadinfo, aStreamGetter, + [](nsIStreamListener* listener, nsIChannel* simpleChannel, + PageThumbStreamGetter* getter) -> RequestOrReason { + MOZ_TRY(getter->GetAsync(listener, simpleChannel)); + return RequestOrReason(nullptr); + }); + + SetContentType(aURI, channel); + channel.swap(*aRetVal); +} + +#undef LOG + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/res/PageThumbProtocolHandler.h b/netwerk/protocol/res/PageThumbProtocolHandler.h new file mode 100644 index 0000000000..8c5c90acff --- /dev/null +++ b/netwerk/protocol/res/PageThumbProtocolHandler.h @@ -0,0 +1,123 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* 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/. */ + +#ifndef PageThumbProtocolHandler_h___ +#define PageThumbProtocolHandler_h___ + +#include "mozilla/Result.h" +#include "SubstitutingProtocolHandler.h" + +namespace mozilla { +namespace net { + +using PageThumbStreamPromise = + mozilla::MozPromise, nsresult, false>; + +class PageThumbStreamGetter; + +class PageThumbProtocolHandler final + : public nsISubstitutingProtocolHandler, + public nsIProtocolHandlerWithDynamicFlags, + public SubstitutingProtocolHandler, + public nsSupportsWeakReference { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_NSIPROTOCOLHANDLERWITHDYNAMICFLAGS + NS_FORWARD_NSIPROTOCOLHANDLER(SubstitutingProtocolHandler::) + NS_FORWARD_NSISUBSTITUTINGPROTOCOLHANDLER(SubstitutingProtocolHandler::) + + static already_AddRefed GetSingleton(); + + /** + * To be called in the parent process to obtain an input stream for the + * given thumbnail. + * + * @param aChildURI a moz-page-thumb URI sent from the child. + * @param aTerminateSender out param set to true when the params are invalid + * and indicate the child should be terminated. If |aChildURI| is + * not a moz-page-thumb URI, the child is in an invalid state and + * should be terminated. This outparam will be set synchronously. + * + * @return PageThumbStreamPromise + * The PageThumbStreamPromise will resolve with an nsIInputStream on + * success, and reject with an nsresult on failure. + */ + RefPtr NewStream(nsIURI* aChildURI, + bool* aTerminateSender); + + protected: + ~PageThumbProtocolHandler() = default; + + private: + explicit PageThumbProtocolHandler(); + + [[nodiscard]] bool ResolveSpecialCases(const nsACString& aHost, + const nsACString& aPath, + const nsACString& aPathname, + nsACString& aResult) override; + + /** + * On entry to this function, *aRetVal is expected to be non-null and already + * addrefed. This function may release the object stored in *aRetVal on entry + * and write a new pointer to an already addrefed channel to *aRetVal. + * + * @param aURI the moz-page-thumb URI. + * @param aLoadInfo the loadinfo for the request. + * @param aRetVal in/out channel param referring to the channel that + * might need to be substituted with a remote channel. + * @return NS_OK if channel has been substituted successfully or no + * substitution at all. Otherwise, returns an error. This function + * will return NS_ERROR_NO_INTERFACE if the URI resolves to a + * non file:// URI. + */ + [[nodiscard]] virtual nsresult SubstituteChannel( + nsIURI* aURI, nsILoadInfo* aLoadInfo, nsIChannel** aRetVal) override; + + /** + * This replaces the provided channel with a channel that will proxy the load + * to the parent process. + * + * @param aURI the moz-page-thumb URI. + * @param aLoadInfo the loadinfo for the request. + * @param aRetVal in/out channel param referring to the channel that + * might need to be substituted with a remote channel. + * @return NS_OK if the replacement channel was created successfully. + * Otherwise, returns an error. + */ + Result SubstituteRemoteChannel(nsIURI* aURI, + nsILoadInfo* aLoadInfo, + nsIChannel** aRetVal); + + /* + * Extracts the URL from the query string in the given moz-page-thumb URI + * and queries PageThumbsStorageService using the extracted URL to obtain + * the local file path of the screenshot. This should only be called from + * the parent because PageThumbsStorageService relies on the path of the + * profile directory, which is unavailable in the child. + * + * @param aPath the path of the moz-page-thumb URI. + * @param aThumbnailPath in/out string param referring to the thumbnail path. + * @return NS_OK if the thumbnail path was obtained successfully. Otherwise + * returns an error. + */ + nsresult GetThumbnailPath(const nsACString& aPath, nsString& aThumbnailPath); + + // To allow parent IPDL actors to invoke methods on this handler when + // handling moz-page-thumb requests from the child. + static StaticRefPtr sSingleton; + + // Set the channel's content type using the provided URI's type. + static void SetContentType(nsIURI* aURI, nsIChannel* aChannel); + + // Gets a SimpleChannel that wraps the provided channel. + static void NewSimpleChannel(nsIURI* aURI, nsILoadInfo* aLoadinfo, + PageThumbStreamGetter* aStreamGetter, + nsIChannel** aRetVal); +}; + +} // namespace net +} // namespace mozilla + +#endif /* PageThumbProtocolHandler_h___ */ diff --git a/netwerk/protocol/res/SubstitutingJARURI.h b/netwerk/protocol/res/SubstitutingJARURI.h new file mode 100644 index 0000000000..fe9208750c --- /dev/null +++ b/netwerk/protocol/res/SubstitutingJARURI.h @@ -0,0 +1,161 @@ +/* -*- 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/. */ + +#ifndef SubstitutingJARURI_h +#define SubstitutingJARURI_h + +#include "nsIURL.h" +#include "nsJARURI.h" +#include "nsISerializable.h" + +namespace mozilla { +namespace net { + +#define NS_SUBSTITUTINGJARURI_IMPL_CID \ + { /* 8f8c54ed-aba7-4ebf-ba6f-e58aec0aba4c */ \ + 0x8f8c54ed, 0xaba7, 0x4ebf, { \ + 0xba, 0x6f, 0xe5, 0x8a, 0xec, 0x0a, 0xba, 0x4c \ + } \ + } + +// Provides a Substituting URI for resource://-like substitutions which +// allows consumers to access the underlying jar resource. +class SubstitutingJARURI : public nsIJARURI, + public nsIStandardURL, + public nsISerializable { + protected: + // Contains the resource://-like URI to be mapped. nsIURI and nsIURL will + // forward to this. + nsCOMPtr mSource; + // Contains the resolved jar resource, nsIJARURI forwards to this to let + // consumer acccess the underlying jar resource. + nsCOMPtr mResolved; + virtual ~SubstitutingJARURI() = default; + + public: + SubstitutingJARURI(nsIURL* source, nsIJARURI* resolved); + // For deserializing + explicit SubstitutingJARURI() = default; + + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSISERIALIZABLE + + NS_DECLARE_STATIC_IID_ACCESSOR(NS_SUBSTITUTINGJARURI_IMPL_CID) + + NS_FORWARD_SAFE_NSIURL(mSource) + NS_FORWARD_SAFE_NSIJARURI(mResolved) + + // enum used in a few places to specify how .ref attribute should be handled + enum RefHandlingEnum { eIgnoreRef, eHonorRef }; + + // We cannot forward Equals* methods to |mSource| so we override them here + NS_IMETHOD EqualsExceptRef(nsIURI* aOther, bool* aResult) override; + NS_IMETHOD Equals(nsIURI* aOther, bool* aResult) override; + + // Helper to share code between Equals methods. + virtual nsresult EqualsInternal(nsIURI* aOther, + RefHandlingEnum aRefHandlingMode, + bool* aResult); + + // Forward the rest of nsIURI to mSource + NS_IMETHOD GetSpec(nsACString& aSpec) override { + return !mSource ? NS_ERROR_NULL_POINTER : mSource->GetSpec(aSpec); + } + NS_IMETHOD GetPrePath(nsACString& aPrePath) override { + return !mSource ? NS_ERROR_NULL_POINTER : mSource->GetPrePath(aPrePath); + } + NS_IMETHOD GetScheme(nsACString& aScheme) override { + return !mSource ? NS_ERROR_NULL_POINTER : mSource->GetScheme(aScheme); + } + NS_IMETHOD GetUserPass(nsACString& aUserPass) override { + return !mSource ? NS_ERROR_NULL_POINTER : mSource->GetUserPass(aUserPass); + } + NS_IMETHOD GetUsername(nsACString& aUsername) override { + return !mSource ? NS_ERROR_NULL_POINTER : mSource->GetUsername(aUsername); + } + NS_IMETHOD GetPassword(nsACString& aPassword) override { + return !mSource ? NS_ERROR_NULL_POINTER : mSource->GetPassword(aPassword); + } + NS_IMETHOD GetHostPort(nsACString& aHostPort) override { + return !mSource ? NS_ERROR_NULL_POINTER : mSource->GetHostPort(aHostPort); + } + NS_IMETHOD GetHost(nsACString& aHost) override { + return !mSource ? NS_ERROR_NULL_POINTER : mSource->GetHost(aHost); + } + NS_IMETHOD GetPort(int32_t* aPort) override { + return !mSource ? NS_ERROR_NULL_POINTER : mSource->GetPort(aPort); + } + NS_IMETHOD GetPathQueryRef(nsACString& aPathQueryRef) override { + return !mSource ? NS_ERROR_NULL_POINTER + : mSource->GetPathQueryRef(aPathQueryRef); + } + NS_IMETHOD SchemeIs(const char* scheme, bool* _retval) override { + return !mSource ? NS_ERROR_NULL_POINTER + : mSource->SchemeIs(scheme, _retval); + } + NS_IMETHOD Resolve(const nsACString& relativePath, + nsACString& _retval) override { + return !mSource ? NS_ERROR_NULL_POINTER + : mSource->Resolve(relativePath, _retval); + } + NS_IMETHOD GetAsciiSpec(nsACString& aAsciiSpec) override { + return !mSource ? NS_ERROR_NULL_POINTER : mSource->GetAsciiSpec(aAsciiSpec); + } + NS_IMETHOD GetAsciiHostPort(nsACString& aAsciiHostPort) override { + return !mSource ? NS_ERROR_NULL_POINTER + : mSource->GetAsciiHostPort(aAsciiHostPort); + } + NS_IMETHOD GetAsciiHost(nsACString& aAsciiHost) override { + return !mSource ? NS_ERROR_NULL_POINTER : mSource->GetAsciiHost(aAsciiHost); + } + NS_IMETHOD GetRef(nsACString& aRef) override { + return !mSource ? NS_ERROR_NULL_POINTER : mSource->GetRef(aRef); + } + NS_IMETHOD GetSpecIgnoringRef(nsACString& aSpecIgnoringRef) override { + return !mSource ? NS_ERROR_NULL_POINTER + : mSource->GetSpecIgnoringRef(aSpecIgnoringRef); + } + NS_IMETHOD GetHasRef(bool* aHasRef) override { + return !mSource ? NS_ERROR_NULL_POINTER : mSource->GetHasRef(aHasRef); + } + NS_IMETHOD GetFilePath(nsACString& aFilePath) override { + return !mSource ? NS_ERROR_NULL_POINTER : mSource->GetFilePath(aFilePath); + } + NS_IMETHOD GetQuery(nsACString& aQuery) override { + return !mSource ? NS_ERROR_NULL_POINTER : mSource->GetQuery(aQuery); + } + NS_IMETHOD GetDisplayHost(nsACString& aDisplayHost) override { + return !mSource ? NS_ERROR_NULL_POINTER + : mSource->GetDisplayHost(aDisplayHost); + } + NS_IMETHOD GetDisplayHostPort(nsACString& aDisplayHostPort) override { + return !mSource ? NS_ERROR_NULL_POINTER + : mSource->GetDisplayHostPort(aDisplayHostPort); + } + NS_IMETHOD GetDisplaySpec(nsACString& aDisplaySpec) override { + return !mSource ? NS_ERROR_NULL_POINTER + : mSource->GetDisplaySpec(aDisplaySpec); + } + NS_IMETHOD GetDisplayPrePath(nsACString& aDisplayPrePath) override { + return !mSource ? NS_ERROR_NULL_POINTER + : mSource->GetDisplayPrePath(aDisplayPrePath); + } + NS_IMETHOD Mutate(nsIURIMutator** _retval) override { + return !mSource ? NS_ERROR_NULL_POINTER : mSource->Mutate(_retval); + } + NS_IMETHOD_(void) Serialize(mozilla::ipc::URIParams& aParams) override { + MOZ_ASSERT(mSource); + mSource->Serialize(aParams); + } +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(SubstitutingJARURI, + NS_SUBSTITUTINGJARURI_IMPL_CID) + +} // namespace net +} // namespace mozilla + +#endif /* SubstitutingJARURI_h */ diff --git a/netwerk/protocol/res/SubstitutingProtocolHandler.cpp b/netwerk/protocol/res/SubstitutingProtocolHandler.cpp new file mode 100644 index 0000000000..baf368761b --- /dev/null +++ b/netwerk/protocol/res/SubstitutingProtocolHandler.cpp @@ -0,0 +1,653 @@ +/* -*- 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/ModuleUtils.h" +#include "mozilla/Unused.h" +#include "mozilla/chrome/RegistryMessageUtils.h" +#include "mozilla/dom/ContentParent.h" + +#include "SubstitutingProtocolHandler.h" +#include "SubstitutingURL.h" +#include "SubstitutingJARURI.h" +#include "nsIChannel.h" +#include "nsIIOService.h" +#include "nsIFile.h" +#include "nsNetCID.h" +#include "nsNetUtil.h" +#include "nsReadableUtils.h" +#include "nsURLHelper.h" +#include "nsEscape.h" +#include "nsIObjectInputStream.h" +#include "nsIObjectOutputStream.h" +#include "nsIClassInfoImpl.h" + +using mozilla::dom::ContentParent; + +namespace mozilla { +namespace net { + +// Log module for Substituting Protocol logging. We keep the pre-existing module +// name of "nsResProtocol" to avoid disruption. +static LazyLogModule gResLog("nsResProtocol"); + +static NS_DEFINE_CID(kSubstitutingURLCID, NS_SUBSTITUTINGURL_CID); +static NS_DEFINE_CID(kSubstitutingJARURIImplCID, + NS_SUBSTITUTINGJARURI_IMPL_CID); + +//--------------------------------------------------------------------------------- +// SubstitutingURL : overrides nsStandardURL::GetFile to provide nsIFile +// resolution +//--------------------------------------------------------------------------------- + +// The list of interfaces should be in sync with nsStandardURL +// Queries this list of interfaces. If none match, it queries mURI. +NS_IMPL_NSIURIMUTATOR_ISUPPORTS(SubstitutingURL::Mutator, nsIURISetters, + nsIURIMutator, nsIStandardURLMutator, + nsIURLMutator, nsIFileURLMutator, + nsISerializable) + +NS_IMPL_CLASSINFO(SubstitutingURL, nullptr, nsIClassInfo::THREADSAFE, + NS_SUBSTITUTINGURL_CID) +// Empty CI getter. We only need nsIClassInfo for Serialization +NS_IMPL_CI_INTERFACE_GETTER0(SubstitutingURL) + +NS_IMPL_ADDREF_INHERITED(SubstitutingURL, nsStandardURL) +NS_IMPL_RELEASE_INHERITED(SubstitutingURL, nsStandardURL) +NS_IMPL_QUERY_INTERFACE_CI_INHERITED0(SubstitutingURL, nsStandardURL) + +nsresult SubstitutingURL::EnsureFile() { + nsAutoCString ourScheme; + nsresult rv = GetScheme(ourScheme); + NS_ENSURE_SUCCESS(rv, rv); + + // Get the handler associated with this scheme. It would be nice to just + // pass this in when constructing SubstitutingURLs, but we need a generic + // factory constructor. + nsCOMPtr io = do_GetIOService(&rv); + nsCOMPtr handler; + rv = io->GetProtocolHandler(ourScheme.get(), getter_AddRefs(handler)); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr substHandler = + do_QueryInterface(handler); + MOZ_ASSERT(substHandler); + + nsAutoCString spec; + rv = substHandler->ResolveURI(this, spec); + if (NS_FAILED(rv)) return rv; + + nsAutoCString scheme; + rv = net_ExtractURLScheme(spec, scheme); + if (NS_FAILED(rv)) return rv; + + // Bug 585869: + // In most cases, the scheme is jar if it's not file. + // Regardless, net_GetFileFromURLSpec should be avoided + // when the scheme isn't file. + if (!scheme.EqualsLiteral("file")) return NS_ERROR_NO_INTERFACE; + + return net_GetFileFromURLSpec(spec, getter_AddRefs(mFile)); +} + +/* virtual */ +nsStandardURL* SubstitutingURL::StartClone() { + SubstitutingURL* clone = new SubstitutingURL(); + return clone; +} + +void SubstitutingURL::Serialize(ipc::URIParams& aParams) { + nsStandardURL::Serialize(aParams); + aParams.get_StandardURLParams().isSubstituting() = true; +} + +// SubstitutingJARURI + +SubstitutingJARURI::SubstitutingJARURI(nsIURL* source, nsIJARURI* resolved) + : mSource(source), mResolved(resolved) {} + +// SubstitutingJARURI::nsIURI + +NS_IMETHODIMP +SubstitutingJARURI::Equals(nsIURI* aOther, bool* aResult) { + return EqualsInternal(aOther, eHonorRef, aResult); +} + +NS_IMETHODIMP +SubstitutingJARURI::EqualsExceptRef(nsIURI* aOther, bool* aResult) { + return EqualsInternal(aOther, eIgnoreRef, aResult); +} + +nsresult SubstitutingJARURI::EqualsInternal(nsIURI* aOther, + RefHandlingEnum aRefHandlingMode, + bool* aResult) { + *aResult = false; + if (!aOther) { + return NS_OK; + } + + nsresult rv; + RefPtr other; + rv = + aOther->QueryInterface(kSubstitutingJARURIImplCID, getter_AddRefs(other)); + if (NS_FAILED(rv)) { + return NS_OK; + } + + // We only need to check the source as the resolved URI is the same for a + // given source + return aRefHandlingMode == eHonorRef + ? mSource->Equals(other->mSource, aResult) + : mSource->EqualsExceptRef(other->mSource, aResult); +} + +// SubstitutingJARURI::nsISerializable + +NS_IMETHODIMP +SubstitutingJARURI::Read(nsIObjectInputStream* aStream) { + MOZ_ASSERT(!mSource); + MOZ_ASSERT(!mResolved); + NS_ENSURE_ARG_POINTER(aStream); + + nsresult rv; + nsCOMPtr source; + rv = aStream->ReadObject(true, getter_AddRefs(source)); + NS_ENSURE_SUCCESS(rv, rv); + + mSource = do_QueryInterface(source, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr resolved; + rv = aStream->ReadObject(true, getter_AddRefs(resolved)); + NS_ENSURE_SUCCESS(rv, rv); + + mResolved = do_QueryInterface(resolved, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +NS_IMETHODIMP +SubstitutingJARURI::Write(nsIObjectOutputStream* aStream) { + NS_ENSURE_ARG_POINTER(aStream); + + nsresult rv; + rv = aStream->WriteCompoundObject(mSource, NS_GET_IID(nsISupports), true); + NS_ENSURE_SUCCESS(rv, rv); + + rv = aStream->WriteCompoundObject(mResolved, NS_GET_IID(nsISupports), true); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +NS_IMPL_CLASSINFO(SubstitutingJARURI, nullptr, nsIClassInfo::MAIN_THREAD_ONLY, + NS_SUBSTITUTINGJARURI_CID) + +NS_IMPL_ADDREF(SubstitutingJARURI) +NS_IMPL_RELEASE(SubstitutingJARURI) + +NS_INTERFACE_MAP_BEGIN(SubstitutingJARURI) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIURI) + NS_INTERFACE_MAP_ENTRY(nsIJARURI) + NS_INTERFACE_MAP_ENTRY(nsIURL) + NS_INTERFACE_MAP_ENTRY(nsIStandardURL) + NS_INTERFACE_MAP_ENTRY(nsISerializable) + if (aIID.Equals(kSubstitutingJARURIImplCID)) { + foundInterface = static_cast(this); + } else + NS_INTERFACE_MAP_ENTRY(nsIURI) + NS_IMPL_QUERY_CLASSINFO(SubstitutingJARURI) +NS_INTERFACE_MAP_END + +NS_IMPL_CI_INTERFACE_GETTER(SubstitutingJARURI, nsIURI, nsIJARURI, nsIURL, + nsIStandardURL, nsISerializable) + +SubstitutingProtocolHandler::SubstitutingProtocolHandler(const char* aScheme, + uint32_t aFlags, + bool aEnforceFileOrJar) + : mScheme(aScheme), + mSubstitutionsLock("SubstitutingProtocolHandler::mSubstitutions"), + mSubstitutions(16), + mEnforceFileOrJar(aEnforceFileOrJar) { + mFlags.emplace(aFlags); + ConstructInternal(); +} + +SubstitutingProtocolHandler::SubstitutingProtocolHandler(const char* aScheme) + : mScheme(aScheme), + mSubstitutionsLock("SubstitutingProtocolHandler::mSubstitutions"), + mSubstitutions(16), + mEnforceFileOrJar(true) { + ConstructInternal(); +} + +void SubstitutingProtocolHandler::ConstructInternal() { + nsresult rv; + mIOService = do_GetIOService(&rv); + MOZ_RELEASE_ASSERT(NS_SUCCEEDED(rv) && mIOService); +} + +// +// IPC marshalling. +// + +nsresult SubstitutingProtocolHandler::CollectSubstitutions( + nsTArray& aMappings) { + AutoReadLock lock(mSubstitutionsLock); + for (auto iter = mSubstitutions.ConstIter(); !iter.Done(); iter.Next()) { + SubstitutionEntry& entry = iter.Data(); + nsCOMPtr uri = entry.baseURI; + SerializedURI serialized; + if (uri) { + nsresult rv = uri->GetSpec(serialized.spec); + NS_ENSURE_SUCCESS(rv, rv); + } + SubstitutionMapping substitution = {mScheme, nsCString(iter.Key()), + serialized, entry.flags}; + aMappings.AppendElement(substitution); + } + + return NS_OK; +} + +nsresult SubstitutingProtocolHandler::SendSubstitution(const nsACString& aRoot, + nsIURI* aBaseURI, + uint32_t aFlags) { + if (GeckoProcessType_Content == XRE_GetProcessType()) { + return NS_OK; + } + + nsTArray parents; + ContentParent::GetAll(parents); + if (!parents.Length()) { + return NS_OK; + } + + SubstitutionMapping mapping; + mapping.scheme = mScheme; + mapping.path = aRoot; + if (aBaseURI) { + nsresult rv = aBaseURI->GetSpec(mapping.resolvedURI.spec); + NS_ENSURE_SUCCESS(rv, rv); + } + mapping.flags = aFlags; + + for (uint32_t i = 0; i < parents.Length(); i++) { + Unused << parents[i]->SendRegisterChromeItem(mapping); + } + + return NS_OK; +} + +//---------------------------------------------------------------------------- +// nsIProtocolHandler +//---------------------------------------------------------------------------- + +nsresult SubstitutingProtocolHandler::GetScheme(nsACString& result) { + result = mScheme; + return NS_OK; +} + +nsresult SubstitutingProtocolHandler::GetDefaultPort(int32_t* result) { + *result = -1; + return NS_OK; +} + +nsresult SubstitutingProtocolHandler::GetProtocolFlags(uint32_t* result) { + if (mFlags.isNothing()) { + NS_WARNING( + "Trying to get protocol flags the wrong way - use " + "nsIProtocolHandlerWithDynamicFlags instead"); + return NS_ERROR_NOT_AVAILABLE; + } + + *result = mFlags.ref(); + return NS_OK; +} + +nsresult SubstitutingProtocolHandler::NewURI(const nsACString& aSpec, + const char* aCharset, + nsIURI* aBaseURI, + nsIURI** aResult) { + // unescape any %2f and %2e to make sure nsStandardURL coalesces them. + // Later net_GetFileFromURLSpec() will do a full unescape and we want to + // treat them the same way the file system will. (bugs 380994, 394075) + nsresult rv; + nsAutoCString spec; + const char* src = aSpec.BeginReading(); + const char* end = aSpec.EndReading(); + const char* last = src; + + spec.SetCapacity(aSpec.Length() + 1); + for (; src < end; ++src) { + if (*src == '%' && (src < end - 2) && *(src + 1) == '2') { + char ch = '\0'; + if (*(src + 2) == 'f' || *(src + 2) == 'F') { + ch = '/'; + } else if (*(src + 2) == 'e' || *(src + 2) == 'E') { + ch = '.'; + } + + if (ch) { + if (last < src) { + spec.Append(last, src - last); + } + spec.Append(ch); + src += 2; + last = src + 1; // src will be incremented by the loop + } + } + if (*src == '?' || *src == '#') { + break; // Don't escape %2f and %2e in the query or ref parts of the URI + } + } + + if (last < end) { + spec.Append(last, end - last); + } + + nsCOMPtr base(aBaseURI); + nsCOMPtr uri; + rv = NS_MutateURI(new SubstitutingURL::Mutator()) + .Apply(NS_MutatorMethod(&nsIStandardURLMutator::Init, + nsIStandardURL::URLTYPE_STANDARD, -1, spec, + aCharset, base, nullptr)) + .Finalize(uri); + if (NS_FAILED(rv)) return rv; + + nsAutoCString host; + rv = uri->GetHost(host); + if (NS_FAILED(rv)) return rv; + + // "android" is the only root that would return the RESOLVE_JAR_URI flag + // see nsResProtocolHandler::GetSubstitutionInternal + if (MustResolveJAR(host)) { + return ResolveJARURI(uri, aResult); + } + + uri.forget(aResult); + return NS_OK; +} + +nsresult SubstitutingProtocolHandler::ResolveJARURI(nsIURL* aURL, + nsIURI** aResult) { + nsAutoCString spec; + nsresult rv = ResolveURI(aURL, spec); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr resolvedURI; + rv = NS_NewURI(getter_AddRefs(resolvedURI), spec); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr innermostURI = NS_GetInnermostURI(resolvedURI); + nsAutoCString scheme; + innermostURI->GetScheme(scheme); + + // We only ever want to resolve to a local jar. + NS_ENSURE_TRUE(scheme.EqualsLiteral("file"), NS_ERROR_UNEXPECTED); + + nsCOMPtr jarURI(do_QueryInterface(resolvedURI)); + if (!jarURI) { + // This substitution does not resolve to a jar: URL, so we just + // return the plain SubstitutionURL + nsCOMPtr url = aURL; + url.forget(aResult); + return NS_OK; + } + + nsCOMPtr result = new SubstitutingJARURI(aURL, jarURI); + result.forget(aResult); + + return rv; +} + +nsresult SubstitutingProtocolHandler::NewChannel(nsIURI* uri, + nsILoadInfo* aLoadInfo, + nsIChannel** result) { + NS_ENSURE_ARG_POINTER(uri); + NS_ENSURE_ARG_POINTER(aLoadInfo); + + nsAutoCString spec; + nsresult rv = ResolveURI(uri, spec); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr newURI; + rv = NS_NewURI(getter_AddRefs(newURI), spec); + NS_ENSURE_SUCCESS(rv, rv); + + // We don't want to allow the inner protocol handler to modify the result + // principal URI since we want either |uri| or anything pre-set by upper + // layers to prevail. + nsCOMPtr savedResultPrincipalURI; + rv = + aLoadInfo->GetResultPrincipalURI(getter_AddRefs(savedResultPrincipalURI)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = NS_NewChannelInternal(result, newURI, aLoadInfo); + NS_ENSURE_SUCCESS(rv, rv); + + rv = aLoadInfo->SetResultPrincipalURI(savedResultPrincipalURI); + NS_ENSURE_SUCCESS(rv, rv); + rv = (*result)->SetOriginalURI(uri); + NS_ENSURE_SUCCESS(rv, rv); + + return SubstituteChannel(uri, aLoadInfo, result); +} + +nsresult SubstitutingProtocolHandler::AllowPort(int32_t port, + const char* scheme, + bool* _retval) { + // don't override anything. + *_retval = false; + return NS_OK; +} + +//---------------------------------------------------------------------------- +// nsISubstitutingProtocolHandler +//---------------------------------------------------------------------------- + +nsresult SubstitutingProtocolHandler::SetSubstitution(const nsACString& root, + nsIURI* baseURI) { + // Add-ons use this API but they should not be able to make anything + // content-accessible. + return SetSubstitutionWithFlags(root, baseURI, 0); +} + +nsresult SubstitutingProtocolHandler::SetSubstitutionWithFlags( + const nsACString& origRoot, nsIURI* baseURI, uint32_t flags) { + nsAutoCString root; + ToLowerCase(origRoot, root); + + if (!baseURI) { + { + AutoWriteLock lock(mSubstitutionsLock); + mSubstitutions.Remove(root); + } + + return SendSubstitution(root, baseURI, flags); + } + + // If baseURI isn't a same-scheme URI, we can set the substitution + // immediately. + nsAutoCString scheme; + nsresult rv = baseURI->GetScheme(scheme); + NS_ENSURE_SUCCESS(rv, rv); + if (!scheme.Equals(mScheme)) { + if (mEnforceFileOrJar && !scheme.EqualsLiteral("file") && + !scheme.EqualsLiteral("jar") && !scheme.EqualsLiteral("app") && + !scheme.EqualsLiteral("resource")) { + NS_WARNING("Refusing to create substituting URI to non-file:// target"); + return NS_ERROR_INVALID_ARG; + } + + { + AutoWriteLock lock(mSubstitutionsLock); + SubstitutionEntry& entry = mSubstitutions.GetOrInsert(root); + entry.baseURI = baseURI; + entry.flags = flags; + } + + return SendSubstitution(root, baseURI, flags); + } + + // baseURI is a same-type substituting URI, let's resolve it first. + nsAutoCString newBase; + rv = ResolveURI(baseURI, newBase); + if (NS_FAILED(rv)) return rv; + + nsCOMPtr newBaseURI; + rv = + mIOService->NewURI(newBase, nullptr, nullptr, getter_AddRefs(newBaseURI)); + NS_ENSURE_SUCCESS(rv, rv); + + { + AutoWriteLock lock(mSubstitutionsLock); + SubstitutionEntry& entry = mSubstitutions.GetOrInsert(root); + entry.baseURI = newBaseURI; + entry.flags = flags; + } + + return SendSubstitution(root, newBaseURI, flags); +} + +nsresult SubstitutingProtocolHandler::GetSubstitution( + const nsACString& origRoot, nsIURI** result) { + NS_ENSURE_ARG_POINTER(result); + + nsAutoCString root; + ToLowerCase(origRoot, root); + + { + AutoReadLock lock(mSubstitutionsLock); + SubstitutionEntry entry; + if (mSubstitutions.Get(root, &entry)) { + nsCOMPtr baseURI = entry.baseURI; + baseURI.forget(result); + return NS_OK; + } + } + + uint32_t flags; + return GetSubstitutionInternal(root, result, &flags); +} + +nsresult SubstitutingProtocolHandler::GetSubstitutionFlags( + const nsACString& root, uint32_t* flags) { +#ifdef DEBUG + nsAutoCString lcRoot; + ToLowerCase(root, lcRoot); + MOZ_ASSERT(root.Equals(lcRoot), + "GetSubstitutionFlags should never receive mixed-case root name"); +#endif + + *flags = 0; + + { + AutoReadLock lock(mSubstitutionsLock); + + SubstitutionEntry entry; + if (mSubstitutions.Get(root, &entry)) { + *flags = entry.flags; + return NS_OK; + } + } + + nsCOMPtr baseURI; + return GetSubstitutionInternal(root, getter_AddRefs(baseURI), flags); +} + +nsresult SubstitutingProtocolHandler::HasSubstitution( + const nsACString& origRoot, bool* result) { + NS_ENSURE_ARG_POINTER(result); + + nsAutoCString root; + ToLowerCase(origRoot, root); + + *result = HasSubstitution(root); + return NS_OK; +} + +nsresult SubstitutingProtocolHandler::ResolveURI(nsIURI* uri, + nsACString& result) { + nsresult rv; + + nsAutoCString host; + nsAutoCString path; + nsAutoCString pathname; + + nsCOMPtr url = do_QueryInterface(uri); + if (!url) { + return NS_ERROR_MALFORMED_URI; + } + + rv = uri->GetAsciiHost(host); + if (NS_FAILED(rv)) return rv; + + rv = uri->GetPathQueryRef(path); + if (NS_FAILED(rv)) return rv; + + rv = url->GetFilePath(pathname); + if (NS_FAILED(rv)) return rv; + + if (ResolveSpecialCases(host, path, pathname, result)) { + return NS_OK; + } + + nsCOMPtr baseURI; + rv = GetSubstitution(host, getter_AddRefs(baseURI)); + if (NS_FAILED(rv)) return rv; + + // Unescape the path so we can perform some checks on it. + NS_UnescapeURL(pathname); + if (pathname.FindChar('\\') != -1) { + return NS_ERROR_MALFORMED_URI; + } + + // Some code relies on an empty path resolving to a file rather than a + // directory. + NS_ASSERTION(path.CharAt(0) == '/', "Path must begin with '/'"); + if (path.Length() == 1) { + rv = baseURI->GetSpec(result); + } else { + // Make sure we always resolve the path as file-relative to our target URI. + // When the baseURI is a nsIFileURL, and the directory it points to doesn't + // exist, it doesn't end with a /. In that case, a file-relative resolution + // is going to pick something in the parent directory, so we resolve using + // an absolute path derived from the full path in that case. + nsCOMPtr baseDir = do_QueryInterface(baseURI); + if (baseDir) { + nsAutoCString basePath; + rv = baseURI->GetFilePath(basePath); + if (NS_SUCCEEDED(rv) && !StringEndsWith(basePath, "/"_ns)) { + // Cf. the assertion above, path already starts with a /, so prefixing + // with a string that doesn't end with one will leave us wit the right + // amount of /. + path.Insert(basePath, 0); + } else { + // Allow to fall through below. + baseDir = nullptr; + } + } + if (!baseDir) { + path.Insert('.', 0); + } + rv = baseURI->Resolve(path, result); + } + + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (MOZ_LOG_TEST(gResLog, LogLevel::Debug)) { + nsAutoCString spec; + uri->GetAsciiSpec(spec); + MOZ_LOG(gResLog, LogLevel::Debug, + ("%s\n -> %s\n", spec.get(), PromiseFlatCString(result).get())); + } + return rv; +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/res/SubstitutingProtocolHandler.h b/netwerk/protocol/res/SubstitutingProtocolHandler.h new file mode 100644 index 0000000000..57bce8c7fe --- /dev/null +++ b/netwerk/protocol/res/SubstitutingProtocolHandler.h @@ -0,0 +1,134 @@ +/* -*- 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/. */ + +#ifndef SubstitutingProtocolHandler_h___ +#define SubstitutingProtocolHandler_h___ + +#include "nsISubstitutingProtocolHandler.h" + +#include "nsDataHashtable.h" +#include "nsStandardURL.h" +#include "nsJARURI.h" +#include "mozilla/chrome/RegistryMessageUtils.h" +#include "mozilla/Maybe.h" +#include "mozilla/RWLock.h" + +class nsIIOService; + +namespace mozilla { +namespace net { + +// +// Base class for resource://-like substitution protocols. +// +// If you add a new protocol, make sure to change nsChromeRegistryChrome +// to properly invoke CollectSubstitutions at the right time. +class SubstitutingProtocolHandler { + public: + SubstitutingProtocolHandler(const char* aScheme, uint32_t aFlags, + bool aEnforceFileOrJar = true); + explicit SubstitutingProtocolHandler(const char* aScheme); + + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(SubstitutingProtocolHandler); + NS_DECL_NON_VIRTUAL_NSIPROTOCOLHANDLER; + NS_DECL_NON_VIRTUAL_NSISUBSTITUTINGPROTOCOLHANDLER; + + bool HasSubstitution(const nsACString& aRoot) const { + AutoReadLock lock(const_cast(mSubstitutionsLock)); + return mSubstitutions.Get(aRoot, nullptr); + } + + nsresult NewURI(const nsACString& aSpec, const char* aCharset, + nsIURI* aBaseURI, nsIURI** aResult); + + [[nodiscard]] nsresult CollectSubstitutions( + nsTArray& aResources); + + protected: + virtual ~SubstitutingProtocolHandler() = default; + void ConstructInternal(); + + [[nodiscard]] nsresult SendSubstitution(const nsACString& aRoot, + nsIURI* aBaseURI, uint32_t aFlags); + + nsresult GetSubstitutionFlags(const nsACString& root, uint32_t* flags); + + // Override this in the subclass to try additional lookups after checking + // mSubstitutions. + [[nodiscard]] virtual nsresult GetSubstitutionInternal( + const nsACString& aRoot, nsIURI** aResult, uint32_t* aFlags) { + *aResult = nullptr; + *aFlags = 0; + return NS_ERROR_NOT_AVAILABLE; + } + + // Override this in the subclass to check for special case when resolving URIs + // _before_ checking substitutions. + [[nodiscard]] virtual bool ResolveSpecialCases(const nsACString& aHost, + const nsACString& aPath, + const nsACString& aPathname, + nsACString& aResult) { + return false; + } + + // This method should only return true if GetSubstitutionInternal would + // return the RESOLVE_JAR_URI flag. + [[nodiscard]] virtual bool MustResolveJAR(const nsACString& aRoot) { + return false; + } + + // Override this in the subclass to check for special case when opening + // channels. + [[nodiscard]] virtual nsresult SubstituteChannel(nsIURI* uri, + nsILoadInfo* aLoadInfo, + nsIChannel** result) { + return NS_OK; + } + + nsIIOService* IOService() { return mIOService; } + + private: + struct SubstitutionEntry { + SubstitutionEntry() : flags(0) {} + + ~SubstitutionEntry() = default; + + nsCOMPtr baseURI; + uint32_t flags; + }; + + // Notifies all observers that a new substitution from |aRoot| to + // |aBaseURI| has been set/installed for this protocol handler. + void NotifyObservers(const nsACString& aRoot, nsIURI* aBaseURI); + + nsCString mScheme; + Maybe mFlags; + + RWLock mSubstitutionsLock; + nsDataHashtable mSubstitutions; + nsCOMPtr mIOService; + + // Returns a SubstitutingJARURI if |aUrl| maps to a |jar:| URI, + // otherwise will return |aURL| + nsresult ResolveJARURI(nsIURL* aURL, nsIURI** aResult); + + // In general, we expect the principal of a document loaded from a + // substituting URI to be a content principal for that URI (rather than + // a principal for whatever is underneath). However, this only works if + // the protocol handler for the underlying URI doesn't set an explicit + // owner (which chrome:// does, for example). So we want to require that + // substituting URIs only map to other URIs of the same type, or to + // file:// and jar:// URIs. + // + // Enforcing this for ye olde resource:// URIs could carry compat risks, so + // we just try to enforce it on new protocols going forward. + bool mEnforceFileOrJar; +}; + +} // namespace net +} // namespace mozilla + +#endif /* SubstitutingProtocolHandler_h___ */ diff --git a/netwerk/protocol/res/SubstitutingURL.h b/netwerk/protocol/res/SubstitutingURL.h new file mode 100644 index 0000000000..b9012f1665 --- /dev/null +++ b/netwerk/protocol/res/SubstitutingURL.h @@ -0,0 +1,66 @@ +/* -*- 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/. */ + +#ifndef SubstitutingURL_h +#define SubstitutingURL_h + +#include "nsStandardURL.h" + +class nsIIOService; + +namespace mozilla { +namespace net { + +// SubstitutingURL : overrides nsStandardURL::GetFile to provide nsIFile +// resolution +class SubstitutingURL : public nsStandardURL { + public: + NS_DECL_ISUPPORTS_INHERITED + virtual nsStandardURL* StartClone() override; + [[nodiscard]] virtual nsresult EnsureFile() override; + + private: + explicit SubstitutingURL() : nsStandardURL(true) {} + explicit SubstitutingURL(bool aSupportsFileURL) : nsStandardURL(true) { + MOZ_ASSERT(aSupportsFileURL); + } + virtual nsresult Clone(nsIURI** aURI) override { + return nsStandardURL::Clone(aURI); + } + virtual ~SubstitutingURL() = default; + + public: + class Mutator : public TemplatedMutator { + NS_DECL_ISUPPORTS + public: + explicit Mutator() = default; + + private: + virtual ~Mutator() = default; + + SubstitutingURL* Create() override { return new SubstitutingURL(); } + }; + + NS_IMETHOD Mutate(nsIURIMutator** aMutator) override { + RefPtr mutator = new SubstitutingURL::Mutator(); + nsresult rv = mutator->InitFromURI(this); + if (NS_FAILED(rv)) { + return rv; + } + mutator.forget(aMutator); + return NS_OK; + } + + NS_IMETHOD_(void) Serialize(ipc::URIParams& aParams) override; + + friend BaseURIMutator; + friend TemplatedMutator; +}; + +} // namespace net +} // namespace mozilla + +#endif /* SubstitutingURL_h */ diff --git a/netwerk/protocol/res/moz.build b/netwerk/protocol/res/moz.build new file mode 100644 index 0000000000..04bdfeff79 --- /dev/null +++ b/netwerk/protocol/res/moz.build @@ -0,0 +1,40 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +XPIDL_SOURCES += [ + "nsIResProtocolHandler.idl", + "nsISubstitutingProtocolHandler.idl", +] + +XPIDL_MODULE = "necko_res" + +EXPORTS.mozilla.net += [ + "ExtensionProtocolHandler.h", + "PageThumbProtocolHandler.h", + "SubstitutingJARURI.h", + "SubstitutingProtocolHandler.h", + "SubstitutingURL.h", +] + +EXPORTS += [ + "nsResProtocolHandler.h", +] + +UNIFIED_SOURCES += [ + "ExtensionProtocolHandler.cpp", + "nsResProtocolHandler.cpp", + "PageThumbProtocolHandler.cpp", + "SubstitutingProtocolHandler.cpp", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" + +LOCAL_INCLUDES += [ + "/netwerk/base", + "/xpcom/base", +] diff --git a/netwerk/protocol/res/nsIResProtocolHandler.idl b/netwerk/protocol/res/nsIResProtocolHandler.idl new file mode 100644 index 0000000000..7046f2f1d4 --- /dev/null +++ b/netwerk/protocol/res/nsIResProtocolHandler.idl @@ -0,0 +1,15 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* 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 "nsISubstitutingProtocolHandler.idl" + +/** + * Protocol handler interface for the resource:// protocol + */ +[scriptable, uuid(241d34ac-9ed5-46d7-910c-7a9d914aa0c5)] +interface nsIResProtocolHandler : nsISubstitutingProtocolHandler +{ + boolean allowContentToAccess(in nsIURI url); +}; diff --git a/netwerk/protocol/res/nsISubstitutingProtocolHandler.idl b/netwerk/protocol/res/nsISubstitutingProtocolHandler.idl new file mode 100644 index 0000000000..cf2e0d42ab --- /dev/null +++ b/netwerk/protocol/res/nsISubstitutingProtocolHandler.idl @@ -0,0 +1,62 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 "nsIProtocolHandler.idl" + + +/** + * Protocol handler superinterface for a protocol which performs substitutions + * from URIs of its scheme to URIs of another scheme. + */ +[scriptable, uuid(154c64fd-a69e-4105-89f8-bd7dfe621372)] +interface nsISubstitutingProtocolHandler : nsIProtocolHandler +{ + /** + * Content script may access files in this package. + */ + const short ALLOW_CONTENT_ACCESS = 1 << 0; + /** + * This substitution exposes nsIJARURI instead of a nsIFileURL. By default + * NewURI will always return a nsIFileURL even when the URL is jar: + */ + const short RESOLVE_JAR_URI = 1 << 1; + + /** + * Sets the substitution for the root key: + * resource://root/path ==> baseURI.resolve(path) + * + * A null baseURI removes the specified substitution. + * + * The root key will be converted to lower-case to conform to + * case-insensitive URI hostname matching behavior. + */ + [must_use] void setSubstitution(in ACString root, in nsIURI baseURI); + + /** + * Same as setSubstitution, but with specific flags. + */ + [must_use] void setSubstitutionWithFlags(in ACString root, in nsIURI baseURI, in uint32_t flags); + + /** + * Gets the substitution for the root key. + * + * @throws NS_ERROR_NOT_AVAILABLE if none exists. + */ + [must_use] nsIURI getSubstitution(in ACString root); + + /** + * Returns TRUE if the substitution exists and FALSE otherwise. + */ + [must_use] boolean hasSubstitution(in ACString root); + + /** + * Utility function to resolve a substituted URI. A resolved URI is not + * guaranteed to reference a resource that exists (ie. opening a channel to + * the resolved URI may fail). + * + * @throws NS_ERROR_NOT_AVAILABLE if resURI.host() is an unknown root key. + */ + [must_use] AUTF8String resolveURI(in nsIURI resURI); +}; diff --git a/netwerk/protocol/res/nsResProtocolHandler.cpp b/netwerk/protocol/res/nsResProtocolHandler.cpp new file mode 100644 index 0000000000..6918799e35 --- /dev/null +++ b/netwerk/protocol/res/nsResProtocolHandler.cpp @@ -0,0 +1,194 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/chrome/RegistryMessageUtils.h" +#include "mozilla/dom/ContentParent.h" +#include "mozilla/Unused.h" + +#include "nsResProtocolHandler.h" +#include "nsNetCID.h" +#include "nsNetUtil.h" +#include "nsURLHelper.h" +#include "nsEscape.h" + +#include "mozilla/Omnijar.h" + +using mozilla::LogLevel; +using mozilla::Unused; +using mozilla::dom::ContentParent; + +#define kAPP "app" +#define kGRE "gre" +#define kAndroid "android" + +mozilla::StaticRefPtr nsResProtocolHandler::sSingleton; + +already_AddRefed nsResProtocolHandler::GetSingleton() { + if (!sSingleton) { + RefPtr handler = new nsResProtocolHandler(); + if (NS_WARN_IF(NS_FAILED(handler->Init()))) { + return nullptr; + } + sSingleton = handler; + ClearOnShutdown(&sSingleton); + } + return do_AddRef(sSingleton); +} + +nsresult nsResProtocolHandler::Init() { + nsresult rv; + rv = mozilla::Omnijar::GetURIString(mozilla::Omnijar::APP, mAppURI); + NS_ENSURE_SUCCESS(rv, rv); + rv = mozilla::Omnijar::GetURIString(mozilla::Omnijar::GRE, mGREURI); + NS_ENSURE_SUCCESS(rv, rv); + + // mozilla::Omnijar::GetURIString always returns a string ending with /, + // and we want to remove it. + mGREURI.Truncate(mGREURI.Length() - 1); + if (mAppURI.Length()) { + mAppURI.Truncate(mAppURI.Length() - 1); + } else { + mAppURI = mGREURI; + } + +#ifdef ANDROID + rv = GetApkURI(mApkURI); +#endif + + // XXXbsmedberg Neil wants a resource://pchrome/ for the profile chrome dir... + // but once I finish multiple chrome registration I'm not sure that it is + // needed + + // XXX dveditz: resource://pchrome/ defeats profile directory salting + // if web content can load it. Tread carefully. + + return rv; +} + +#ifdef ANDROID +nsresult nsResProtocolHandler::GetApkURI(nsACString& aResult) { + nsCString::const_iterator start, iter; + mGREURI.BeginReading(start); + mGREURI.EndReading(iter); + nsCString::const_iterator start_iter = start; + + // This is like jar:jar:file://path/to/apk/base.apk!/path/to/omni.ja!/ + bool found = FindInReadable("!/"_ns, start_iter, iter); + NS_ENSURE_TRUE(found, NS_ERROR_UNEXPECTED); + + // like jar:jar:file://path/to/apk/base.apk!/ + const nsDependentCSubstring& withoutPath = Substring(start, iter); + NS_ENSURE_TRUE(withoutPath.Length() >= 4, NS_ERROR_UNEXPECTED); + + // Let's make sure we're removing what we expect to remove + NS_ENSURE_TRUE(Substring(withoutPath, 0, 4).EqualsLiteral("jar:"), + NS_ERROR_UNEXPECTED); + + // like jar:file://path/to/apk/base.apk!/ + aResult = ToNewCString(Substring(withoutPath, 4)); + + // Remove the trailing / + NS_ENSURE_TRUE(aResult.Length() >= 1, NS_ERROR_UNEXPECTED); + aResult.Truncate(aResult.Length() - 1); + return NS_OK; +} +#endif + +//---------------------------------------------------------------------------- +// nsResProtocolHandler::nsISupports +//---------------------------------------------------------------------------- + +NS_IMPL_QUERY_INTERFACE(nsResProtocolHandler, nsIResProtocolHandler, + nsISubstitutingProtocolHandler, nsIProtocolHandler, + nsISupportsWeakReference) +NS_IMPL_ADDREF_INHERITED(nsResProtocolHandler, SubstitutingProtocolHandler) +NS_IMPL_RELEASE_INHERITED(nsResProtocolHandler, SubstitutingProtocolHandler) + +NS_IMETHODIMP +nsResProtocolHandler::AllowContentToAccess(nsIURI* aURI, bool* aResult) { + *aResult = false; + + nsAutoCString host; + nsresult rv = aURI->GetAsciiHost(host); + NS_ENSURE_SUCCESS(rv, rv); + + uint32_t flags; + rv = GetSubstitutionFlags(host, &flags); + NS_ENSURE_SUCCESS(rv, rv); + + *aResult = flags & nsISubstitutingProtocolHandler::ALLOW_CONTENT_ACCESS; + return NS_OK; +} + +nsresult nsResProtocolHandler::GetSubstitutionInternal(const nsACString& aRoot, + nsIURI** aResult, + uint32_t* aFlags) { + nsAutoCString uri; + + if (!ResolveSpecialCases(aRoot, "/"_ns, "/"_ns, uri)) { + return NS_ERROR_NOT_AVAILABLE; + } + + if (aRoot.Equals(kAndroid)) { + *aFlags = nsISubstitutingProtocolHandler::RESOLVE_JAR_URI; + } else { + *aFlags = 0; // No content access. + } + return NS_NewURI(aResult, uri); +} + +bool nsResProtocolHandler::ResolveSpecialCases(const nsACString& aHost, + const nsACString& aPath, + const nsACString& aPathname, + nsACString& aResult) { + if (aHost.EqualsLiteral("") || aHost.EqualsLiteral(kAPP)) { + aResult.Assign(mAppURI); + } else if (aHost.Equals(kGRE)) { + aResult.Assign(mGREURI); +#ifdef ANDROID + } else if (aHost.Equals(kAndroid)) { + aResult.Assign(mApkURI); +#endif + } else { + return false; + } + aResult.Append(aPath); + return true; +} + +nsresult nsResProtocolHandler::SetSubstitution(const nsACString& aRoot, + nsIURI* aBaseURI) { + MOZ_ASSERT(!aRoot.EqualsLiteral("")); + MOZ_ASSERT(!aRoot.EqualsLiteral(kAPP)); + MOZ_ASSERT(!aRoot.EqualsLiteral(kGRE)); + MOZ_ASSERT(!aRoot.EqualsLiteral(kAndroid)); + return SubstitutingProtocolHandler::SetSubstitution(aRoot, aBaseURI); +} + +nsresult nsResProtocolHandler::SetSubstitutionWithFlags(const nsACString& aRoot, + nsIURI* aBaseURI, + uint32_t aFlags) { + MOZ_ASSERT(!aRoot.EqualsLiteral("")); + MOZ_ASSERT(!aRoot.EqualsLiteral(kAPP)); + MOZ_ASSERT(!aRoot.EqualsLiteral(kGRE)); + MOZ_ASSERT(!aRoot.EqualsLiteral(kAndroid)); + return SubstitutingProtocolHandler::SetSubstitutionWithFlags(aRoot, aBaseURI, + aFlags); +} + +nsresult nsResProtocolHandler::HasSubstitution(const nsACString& aRoot, + bool* aResult) { + if (aRoot.EqualsLiteral(kAPP) || aRoot.EqualsLiteral(kGRE) +#ifdef ANDROID + || aRoot.EqualsLiteral(kAndroid) +#endif + ) { + *aResult = true; + return NS_OK; + } + + return mozilla::net::SubstitutingProtocolHandler::HasSubstitution(aRoot, + aResult); +} diff --git a/netwerk/protocol/res/nsResProtocolHandler.h b/netwerk/protocol/res/nsResProtocolHandler.h new file mode 100644 index 0000000000..2d672dfad6 --- /dev/null +++ b/netwerk/protocol/res/nsResProtocolHandler.h @@ -0,0 +1,80 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef nsResProtocolHandler_h___ +#define nsResProtocolHandler_h___ + +#include "mozilla/net/SubstitutingProtocolHandler.h" + +#include "nsIResProtocolHandler.h" +#include "nsInterfaceHashtable.h" +#include "nsWeakReference.h" + +struct SubstitutionMapping; +class nsResProtocolHandler final + : public nsIResProtocolHandler, + public mozilla::net::SubstitutingProtocolHandler, + public nsSupportsWeakReference { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_NSIRESPROTOCOLHANDLER + + static already_AddRefed GetSingleton(); + + NS_FORWARD_NSIPROTOCOLHANDLER(mozilla::net::SubstitutingProtocolHandler::) + + nsResProtocolHandler() + : mozilla::net::SubstitutingProtocolHandler( + "resource", + URI_STD | URI_IS_UI_RESOURCE | URI_IS_LOCAL_RESOURCE | + URI_IS_POTENTIALLY_TRUSTWORTHY, + /* aEnforceFileOrJar = */ false) {} + + NS_IMETHOD SetSubstitution(const nsACString& aRoot, + nsIURI* aBaseURI) override; + NS_IMETHOD SetSubstitutionWithFlags(const nsACString& aRoot, nsIURI* aBaseURI, + uint32_t aFlags) override; + NS_IMETHOD HasSubstitution(const nsACString& aRoot, bool* aResult) override; + + NS_IMETHOD GetSubstitution(const nsACString& aRoot, + nsIURI** aResult) override { + return mozilla::net::SubstitutingProtocolHandler::GetSubstitution(aRoot, + aResult); + } + + NS_IMETHOD ResolveURI(nsIURI* aResURI, nsACString& aResult) override { + return mozilla::net::SubstitutingProtocolHandler::ResolveURI(aResURI, + aResult); + } + + protected: + [[nodiscard]] nsresult GetSubstitutionInternal(const nsACString& aRoot, + nsIURI** aResult, + uint32_t* aFlags) override; + virtual ~nsResProtocolHandler() = default; + + [[nodiscard]] bool ResolveSpecialCases(const nsACString& aHost, + const nsACString& aPath, + const nsACString& aPathname, + nsACString& aResult) override; + + [[nodiscard]] virtual bool MustResolveJAR(const nsACString& aRoot) override { + return aRoot.EqualsLiteral("android"); + } + + private: + [[nodiscard]] nsresult Init(); + static mozilla::StaticRefPtr sSingleton; + + nsCString mAppURI; + nsCString mGREURI; +#ifdef ANDROID + // Used for resource://android URIs + nsCString mApkURI; + nsresult GetApkURI(nsACString& aResult); +#endif +}; + +#endif /* nsResProtocolHandler_h___ */ diff --git a/netwerk/protocol/viewsource/moz.build b/netwerk/protocol/viewsource/moz.build new file mode 100644 index 0000000000..adf19615f0 --- /dev/null +++ b/netwerk/protocol/viewsource/moz.build @@ -0,0 +1,29 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +XPIDL_SOURCES += [ + "nsIViewSourceChannel.idl", +] + +XPIDL_MODULE = "necko_viewsource" + +UNIFIED_SOURCES += [ + "nsViewSourceChannel.cpp", + "nsViewSourceHandler.cpp", +] + +EXPORTS += [ + "nsViewSourceHandler.h", +] + +FINAL_LIBRARY = "xul" +LOCAL_INCLUDES += [ + "/netwerk/base", + # For nsHttpChannel.h + "/netwerk/protocol/http", +] + +include("/ipc/chromium/chromium-config.mozbuild") diff --git a/netwerk/protocol/viewsource/nsIViewSourceChannel.idl b/netwerk/protocol/viewsource/nsIViewSourceChannel.idl new file mode 100644 index 0000000000..3fb4b8c70e --- /dev/null +++ b/netwerk/protocol/viewsource/nsIViewSourceChannel.idl @@ -0,0 +1,43 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* 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 "nsIChannel.idl" + +[uuid(3e9800f8-edb7-4c9a-9285-09b4f045b019)] +interface nsIViewSourceChannel : nsIChannel +{ + /** + * The actual (MIME) content type of the data. + * + * nsIViewSourceChannel returns a content type of + * "application/x-view-source" if you ask it for the contentType + * attribute. + * + * However, callers interested in finding out or setting the + * actual content type can utilize this attribute. + */ + [must_use] attribute ACString originalContentType; + + /** + * Whether the channel was created to view the source of a srcdoc document. + */ + [must_use] readonly attribute boolean isSrcdocChannel; + + /** + * Set to indicate the base URI. If this channel is a srcdoc channel, it + * returns the base URI provided by the embedded channel. It is used to + * provide an indication of the base URI in circumstances where it isn't + * otherwise recoverable. Returns null when it isn't set and isn't a + * srcdoc channel. + */ + [must_use] attribute nsIURI baseURI; + + /** + * Get the inner channel wrapped by this nsIViewSourceChannel. + */ + [notxpcom, nostdcall] nsIChannel getInnerChannel(); +}; + + diff --git a/netwerk/protocol/viewsource/nsViewSourceChannel.cpp b/netwerk/protocol/viewsource/nsViewSourceChannel.cpp new file mode 100644 index 0000000000..cce3de1b36 --- /dev/null +++ b/netwerk/protocol/viewsource/nsViewSourceChannel.cpp @@ -0,0 +1,1175 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=4 sw=2 sts=2 et: */ +/* 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 "nsViewSourceChannel.h" +#include "mozilla/DebugOnly.h" +#include "mozilla/NullPrincipal.h" +#include "nsContentSecurityManager.h" +#include "nsContentUtils.h" +#include "nsHttpChannel.h" +#include "nsIHttpHeaderVisitor.h" +#include "nsIIOService.h" +#include "nsIInputStreamChannel.h" +#include "nsIReferrerInfo.h" +#include "nsMimeTypes.h" +#include "nsNetUtil.h" +#include "nsServiceManagerUtils.h" + +NS_IMPL_ADDREF(nsViewSourceChannel) +NS_IMPL_RELEASE(nsViewSourceChannel) +/* + This QI uses NS_INTERFACE_MAP_ENTRY_CONDITIONAL to check for + non-nullness of mHttpChannel, mCachingChannel, and mUploadChannel. +*/ +NS_INTERFACE_MAP_BEGIN(nsViewSourceChannel) + NS_INTERFACE_MAP_ENTRY(nsIViewSourceChannel) + NS_INTERFACE_MAP_ENTRY(nsIStreamListener) + NS_INTERFACE_MAP_ENTRY(nsIRequestObserver) + NS_INTERFACE_MAP_ENTRY(nsIInterfaceRequestor) + NS_INTERFACE_MAP_ENTRY(nsIChannelEventSink) + NS_INTERFACE_MAP_ENTRY_CONDITIONAL(nsIHttpChannel, mHttpChannel) + NS_INTERFACE_MAP_ENTRY_CONDITIONAL(nsIIdentChannel, mHttpChannel) + NS_INTERFACE_MAP_ENTRY_CONDITIONAL(nsIHttpChannelInternal, + mHttpChannelInternal) + NS_INTERFACE_MAP_ENTRY_CONDITIONAL(nsICachingChannel, mCachingChannel) + NS_INTERFACE_MAP_ENTRY_CONDITIONAL(nsICacheInfoChannel, mCacheInfoChannel) + NS_INTERFACE_MAP_ENTRY_CONDITIONAL(nsIApplicationCacheChannel, + mApplicationCacheChannel) + NS_INTERFACE_MAP_ENTRY_CONDITIONAL(nsIUploadChannel, mUploadChannel) + NS_INTERFACE_MAP_ENTRY_CONDITIONAL(nsIFormPOSTActionChannel, mPostChannel) + NS_INTERFACE_MAP_ENTRY_CONDITIONAL(nsIChildChannel, mChildChannel) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsIRequest, nsIViewSourceChannel) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsIChannel, nsIViewSourceChannel) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIViewSourceChannel) +NS_INTERFACE_MAP_END + +nsresult nsViewSourceChannel::Init(nsIURI* uri, nsILoadInfo* aLoadInfo) { + mOriginalURI = uri; + + nsAutoCString path; + nsresult rv = uri->GetPathQueryRef(path); + if (NS_FAILED(rv)) return rv; + + nsCOMPtr pService(do_GetIOService(&rv)); + if (NS_FAILED(rv)) return rv; + + nsAutoCString scheme; + rv = pService->ExtractScheme(path, scheme); + if (NS_FAILED(rv)) return rv; + + // prevent viewing source of javascript URIs (see bug 204779) + if (scheme.EqualsLiteral("javascript")) { + NS_WARNING("blocking view-source:javascript:"); + return NS_ERROR_INVALID_ARG; + } + + nsCOMPtr newChannelURI; + rv = pService->NewURI(path, nullptr, nullptr, getter_AddRefs(newChannelURI)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = pService->NewChannelFromURIWithLoadInfo(newChannelURI, aLoadInfo, + getter_AddRefs(mChannel)); + NS_ENSURE_SUCCESS(rv, rv); + + mIsSrcdocChannel = false; + + mChannel->SetOriginalURI(mOriginalURI); + UpdateChannelInterfaces(); + + return NS_OK; +} + +nsresult nsViewSourceChannel::InitSrcdoc(nsIURI* aURI, nsIURI* aBaseURI, + const nsAString& aSrcdoc, + nsILoadInfo* aLoadInfo) { + nsresult rv; + + nsCOMPtr inStreamURI; + // Need to strip view-source: from the URI. Hardcoded to + // about:srcdoc as this is the only permissible URI for srcdoc + // loads + rv = NS_NewURI(getter_AddRefs(inStreamURI), u"about:srcdoc"_ns); + NS_ENSURE_SUCCESS(rv, rv); + + rv = NS_NewInputStreamChannelInternal(getter_AddRefs(mChannel), inStreamURI, + aSrcdoc, "text/html"_ns, aLoadInfo, + true); + + NS_ENSURE_SUCCESS(rv, rv); + mOriginalURI = aURI; + mIsSrcdocChannel = true; + + mChannel->SetOriginalURI(mOriginalURI); + UpdateChannelInterfaces(); + + rv = UpdateLoadInfoResultPrincipalURI(); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr isc = do_QueryInterface(mChannel); + MOZ_ASSERT(isc); + isc->SetBaseURI(aBaseURI); + return NS_OK; +} + +void nsViewSourceChannel::UpdateChannelInterfaces() { + mHttpChannel = do_QueryInterface(mChannel); + mHttpChannelInternal = do_QueryInterface(mChannel); + mCachingChannel = do_QueryInterface(mChannel); + mCacheInfoChannel = do_QueryInterface(mChannel); + mApplicationCacheChannel = do_QueryInterface(mChannel); + mUploadChannel = do_QueryInterface(mChannel); + mPostChannel = do_QueryInterface(mChannel); + mChildChannel = do_QueryInterface(mChannel); +} + +void nsViewSourceChannel::ReleaseListeners() { + mListener = nullptr; + mCallbacks = nullptr; +} + +nsresult nsViewSourceChannel::UpdateLoadInfoResultPrincipalURI() { + nsresult rv; + + MOZ_ASSERT(mChannel); + + nsCOMPtr channelLoadInfo = mChannel->LoadInfo(); + nsCOMPtr channelResultPrincipalURI; + rv = channelLoadInfo->GetResultPrincipalURI( + getter_AddRefs(channelResultPrincipalURI)); + if (NS_FAILED(rv)) { + return rv; + } + + if (!channelResultPrincipalURI) { + mChannel->GetOriginalURI(getter_AddRefs(channelResultPrincipalURI)); + return NS_OK; + } + + if (!channelResultPrincipalURI) { + return NS_ERROR_UNEXPECTED; + } + + bool alreadyViewSource; + if (NS_SUCCEEDED(channelResultPrincipalURI->SchemeIs("view-source", + &alreadyViewSource)) && + alreadyViewSource) { + return NS_OK; + } + + nsCOMPtr updatedResultPrincipalURI; + rv = BuildViewSourceURI(channelResultPrincipalURI, + getter_AddRefs(updatedResultPrincipalURI)); + if (NS_FAILED(rv)) { + return rv; + } + + rv = channelLoadInfo->SetResultPrincipalURI(updatedResultPrincipalURI); + if (NS_FAILED(rv)) { + return rv; + } + + return NS_OK; +} + +nsresult nsViewSourceChannel::BuildViewSourceURI(nsIURI* aURI, + nsIURI** aResult) { + nsresult rv; + + // protect ourselves against broken channel implementations + if (!aURI) { + NS_ERROR("no URI to build view-source uri!"); + return NS_ERROR_UNEXPECTED; + } + + nsAutoCString spec; + rv = aURI->GetSpec(spec); + if (NS_FAILED(rv)) { + return rv; + } + + return NS_NewURI(aResult, "view-source:"_ns + spec); +} + +//////////////////////////////////////////////////////////////////////////////// +// nsIRequest methods: + +NS_IMETHODIMP +nsViewSourceChannel::GetName(nsACString& result) { + nsCOMPtr uri; + GetURI(getter_AddRefs(uri)); + if (uri) { + return uri->GetSpec(result); + } + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsViewSourceChannel::GetTransferSize(uint64_t* aTransferSize) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsViewSourceChannel::GetRequestSize(uint64_t* aRequestSize) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsViewSourceChannel::GetDecodedBodySize(uint64_t* aDecodedBodySize) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsViewSourceChannel::GetEncodedBodySize(uint64_t* aEncodedBodySize) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsViewSourceChannel::IsPending(bool* result) { + NS_ENSURE_TRUE(mChannel, NS_ERROR_FAILURE); + + return mChannel->IsPending(result); +} + +NS_IMETHODIMP +nsViewSourceChannel::GetStatus(nsresult* status) { + NS_ENSURE_TRUE(mChannel, NS_ERROR_FAILURE); + + return mChannel->GetStatus(status); +} + +NS_IMETHODIMP +nsViewSourceChannel::Cancel(nsresult status) { + NS_ENSURE_TRUE(mChannel, NS_ERROR_FAILURE); + + return mChannel->Cancel(status); +} + +NS_IMETHODIMP +nsViewSourceChannel::GetCanceled(bool* aCanceled) { + NS_ENSURE_TRUE(mChannel, NS_ERROR_FAILURE); + + return mChannel->GetCanceled(aCanceled); +} + +NS_IMETHODIMP +nsViewSourceChannel::Suspend(void) { + NS_ENSURE_TRUE(mChannel, NS_ERROR_FAILURE); + + return mChannel->Suspend(); +} + +NS_IMETHODIMP +nsViewSourceChannel::Resume(void) { + NS_ENSURE_TRUE(mChannel, NS_ERROR_FAILURE); + + return mChannel->Resume(); +} + +//////////////////////////////////////////////////////////////////////////////// +// nsIChannel methods: + +NS_IMETHODIMP +nsViewSourceChannel::GetOriginalURI(nsIURI** aURI) { + *aURI = do_AddRef(mOriginalURI).take(); + return NS_OK; +} + +NS_IMETHODIMP +nsViewSourceChannel::SetOriginalURI(nsIURI* aURI) { + NS_ENSURE_ARG_POINTER(aURI); + mOriginalURI = aURI; + return NS_OK; +} + +NS_IMETHODIMP +nsViewSourceChannel::GetURI(nsIURI** aURI) { + NS_ENSURE_TRUE(mChannel, NS_ERROR_FAILURE); + + nsCOMPtr uri; + nsresult rv = mChannel->GetURI(getter_AddRefs(uri)); + if (NS_FAILED(rv)) { + return rv; + } + + return BuildViewSourceURI(uri, aURI); +} + +NS_IMETHODIMP +nsViewSourceChannel::Open(nsIInputStream** aStream) { + NS_ENSURE_TRUE(mChannel, NS_ERROR_FAILURE); + return Open(aStream); +} + +NS_IMETHODIMP +nsViewSourceChannel::AsyncOpen(nsIStreamListener* aListener) { + // We can't ensure GetInitialSecurityCheckDone here + + NS_ENSURE_TRUE(mChannel, NS_ERROR_FAILURE); + + mListener = aListener; + + /* + * We want to add ourselves to the loadgroup before opening + * mChannel, since we want to make sure we're in the loadgroup + * when mChannel finishes and fires OnStopRequest() + */ + + nsCOMPtr loadGroup; + mChannel->GetLoadGroup(getter_AddRefs(loadGroup)); + if (loadGroup) + loadGroup->AddRequest(static_cast(this), nullptr); + + nsresult rv = NS_OK; + rv = mChannel->AsyncOpen(this); + + if (NS_FAILED(rv) && loadGroup) + loadGroup->RemoveRequest(static_cast(this), nullptr, + rv); + + if (NS_SUCCEEDED(rv)) { + // We do this here to make sure all notification callbacks changes have been + // made first before we inject this view-source channel. + mChannel->GetNotificationCallbacks(getter_AddRefs(mCallbacks)); + mChannel->SetNotificationCallbacks(this); + + MOZ_ASSERT(mCallbacks != this, "We have a cycle"); + + mOpened = true; + } + + if (NS_FAILED(rv)) { + ReleaseListeners(); + } + + return rv; +} + +/* + * Both the view source channel and mChannel are added to the + * loadgroup. There should never be more than one request in the + * loadgroup that has LOAD_DOCUMENT_URI set. The one that has this + * flag set is the request whose URI is used to refetch the document, + * so it better be the viewsource channel. + * + * Therefore, we need to make sure that + * 1) The load flags on mChannel _never_ include LOAD_DOCUMENT_URI + * 2) The load flags on |this| include LOAD_DOCUMENT_URI when it was + * set via SetLoadFlags (mIsDocument keeps track of this flag). + */ + +NS_IMETHODIMP +nsViewSourceChannel::GetLoadFlags(uint32_t* aLoadFlags) { + NS_ENSURE_TRUE(mChannel, NS_ERROR_FAILURE); + + nsresult rv = mChannel->GetLoadFlags(aLoadFlags); + if (NS_FAILED(rv)) return rv; + + // This should actually be just LOAD_DOCUMENT_URI but the win32 compiler + // fails to deal due to amiguous inheritance. nsIChannel::LOAD_DOCUMENT_URI + // also fails; the Win32 compiler thinks that's supposed to be a method. + if (mIsDocument) *aLoadFlags |= ::nsIChannel::LOAD_DOCUMENT_URI; + + return rv; +} + +NS_IMETHODIMP +nsViewSourceChannel::SetLoadFlags(uint32_t aLoadFlags) { + NS_ENSURE_TRUE(mChannel, NS_ERROR_FAILURE); + + // "View source" always wants the currently cached content. + // We also want to have _this_ channel, not mChannel to be the + // 'document' channel in the loadgroup. + + // These should actually be just LOAD_FROM_CACHE and LOAD_DOCUMENT_URI but + // the win32 compiler fails to deal due to amiguous inheritance. + // nsIChannel::LOAD_DOCUMENT_URI/nsIRequest::LOAD_FROM_CACHE also fails; the + // Win32 compiler thinks that's supposed to be a method. + mIsDocument = (aLoadFlags & ::nsIChannel::LOAD_DOCUMENT_URI) ? true : false; + + nsresult rv = + mChannel->SetLoadFlags((aLoadFlags | ::nsIRequest::LOAD_FROM_CACHE) & + ~::nsIChannel::LOAD_DOCUMENT_URI); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (mHttpChannel) { + rv = mHttpChannel->SetIsMainDocumentChannel( + aLoadFlags & ::nsIChannel::LOAD_DOCUMENT_URI); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsViewSourceChannel::GetTRRMode(nsIRequest::TRRMode* aTRRMode) { + return nsIViewSourceChannel::GetTRRModeImpl(aTRRMode); +} + +NS_IMETHODIMP +nsViewSourceChannel::SetTRRMode(nsIRequest::TRRMode aTRRMode) { + return nsIViewSourceChannel::SetTRRModeImpl(aTRRMode); +} + +NS_IMETHODIMP +nsViewSourceChannel::GetContentType(nsACString& aContentType) { + NS_ENSURE_TRUE(mChannel, NS_ERROR_FAILURE); + + aContentType.Truncate(); + + if (mContentType.IsEmpty()) { + // Get the current content type + nsresult rv; + nsAutoCString contentType; + rv = mChannel->GetContentType(contentType); + if (NS_FAILED(rv)) return rv; + + // If we don't know our type, just say so. The unknown + // content decoder will then kick in automatically, and it + // will call our SetOriginalContentType method instead of our + // SetContentType method to set the type it determines. + if (!contentType.EqualsLiteral(UNKNOWN_CONTENT_TYPE)) { + contentType = VIEWSOURCE_CONTENT_TYPE; + } + + mContentType = contentType; + } + + aContentType = mContentType; + return NS_OK; +} + +NS_IMETHODIMP +nsViewSourceChannel::SetContentType(const nsACString& aContentType) { + // Our GetContentType() currently returns VIEWSOURCE_CONTENT_TYPE + // + // However, during the parsing phase the parser calls our + // channel's GetContentType(). Returning the string above trips up + // the parser. In order to avoid messy changes and not to have the + // parser depend on nsIViewSourceChannel Vidur proposed the + // following solution: + // + // The ViewSourceChannel initially returns a content type of + // VIEWSOURCE_CONTENT_TYPE. Based on this type decisions to + // create a viewer for doing a view source are made. After the + // viewer is created, nsLayoutDLF::CreateInstance() calls this + // SetContentType() with the original content type. When it's + // time for the parser to find out the content type it will call + // our channel's GetContentType() and it will get the original + // content type, such as, text/html and everything is kosher from + // then on. + + if (!mOpened) { + // We do not take hints + return NS_ERROR_NOT_AVAILABLE; + } + + mContentType = aContentType; + return NS_OK; +} + +NS_IMETHODIMP +nsViewSourceChannel::GetContentCharset(nsACString& aContentCharset) { + NS_ENSURE_TRUE(mChannel, NS_ERROR_FAILURE); + + return mChannel->GetContentCharset(aContentCharset); +} + +NS_IMETHODIMP +nsViewSourceChannel::SetContentCharset(const nsACString& aContentCharset) { + NS_ENSURE_TRUE(mChannel, NS_ERROR_FAILURE); + + return mChannel->SetContentCharset(aContentCharset); +} + +// We don't forward these methods becacuse content-disposition isn't whitelisted +// (see GetResponseHeader/VisitResponseHeaders). +NS_IMETHODIMP +nsViewSourceChannel::GetContentDisposition(uint32_t* aContentDisposition) { + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP +nsViewSourceChannel::SetContentDisposition(uint32_t aContentDisposition) { + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP +nsViewSourceChannel::GetContentDispositionFilename( + nsAString& aContentDispositionFilename) { + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP +nsViewSourceChannel::SetContentDispositionFilename( + const nsAString& aContentDispositionFilename) { + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP +nsViewSourceChannel::GetContentDispositionHeader( + nsACString& aContentDispositionHeader) { + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP +nsViewSourceChannel::GetContentLength(int64_t* aContentLength) { + NS_ENSURE_TRUE(mChannel, NS_ERROR_FAILURE); + + return mChannel->GetContentLength(aContentLength); +} + +NS_IMETHODIMP +nsViewSourceChannel::SetContentLength(int64_t aContentLength) { + NS_ENSURE_TRUE(mChannel, NS_ERROR_FAILURE); + + return mChannel->SetContentLength(aContentLength); +} + +NS_IMETHODIMP +nsViewSourceChannel::GetLoadGroup(nsILoadGroup** aLoadGroup) { + NS_ENSURE_TRUE(mChannel, NS_ERROR_FAILURE); + + return mChannel->GetLoadGroup(aLoadGroup); +} + +NS_IMETHODIMP +nsViewSourceChannel::SetLoadGroup(nsILoadGroup* aLoadGroup) { + NS_ENSURE_TRUE(mChannel, NS_ERROR_FAILURE); + + return mChannel->SetLoadGroup(aLoadGroup); +} + +NS_IMETHODIMP +nsViewSourceChannel::GetOwner(nsISupports** aOwner) { + NS_ENSURE_TRUE(mChannel, NS_ERROR_FAILURE); + + return mChannel->GetOwner(aOwner); +} + +NS_IMETHODIMP +nsViewSourceChannel::SetOwner(nsISupports* aOwner) { + NS_ENSURE_TRUE(mChannel, NS_ERROR_FAILURE); + + return mChannel->SetOwner(aOwner); +} + +NS_IMETHODIMP +nsViewSourceChannel::GetLoadInfo(nsILoadInfo** aLoadInfo) { + NS_ENSURE_TRUE(mChannel, NS_ERROR_FAILURE); + + return mChannel->GetLoadInfo(aLoadInfo); +} + +NS_IMETHODIMP +nsViewSourceChannel::SetLoadInfo(nsILoadInfo* aLoadInfo) { + NS_ENSURE_TRUE(mChannel, NS_ERROR_FAILURE); + MOZ_RELEASE_ASSERT(aLoadInfo, "loadinfo can't be null"); + return mChannel->SetLoadInfo(aLoadInfo); +} + +NS_IMETHODIMP +nsViewSourceChannel::GetIsDocument(bool* aIsDocument) { + NS_ENSURE_TRUE(mChannel, NS_ERROR_FAILURE); + + return mChannel->GetIsDocument(aIsDocument); +} + +NS_IMETHODIMP +nsViewSourceChannel::GetNotificationCallbacks( + nsIInterfaceRequestor** aNotificationCallbacks) { + NS_ENSURE_TRUE(mChannel, NS_ERROR_FAILURE); + + return mChannel->GetNotificationCallbacks(aNotificationCallbacks); +} + +NS_IMETHODIMP +nsViewSourceChannel::SetNotificationCallbacks( + nsIInterfaceRequestor* aNotificationCallbacks) { + NS_ENSURE_TRUE(mChannel, NS_ERROR_FAILURE); + + return mChannel->SetNotificationCallbacks(aNotificationCallbacks); +} + +NS_IMETHODIMP +nsViewSourceChannel::GetSecurityInfo(nsISupports** aSecurityInfo) { + NS_ENSURE_TRUE(mChannel, NS_ERROR_FAILURE); + + return mChannel->GetSecurityInfo(aSecurityInfo); +} + +// nsIViewSourceChannel methods +NS_IMETHODIMP +nsViewSourceChannel::GetOriginalContentType(nsACString& aContentType) { + NS_ENSURE_TRUE(mChannel, NS_ERROR_FAILURE); + + return mChannel->GetContentType(aContentType); +} + +NS_IMETHODIMP +nsViewSourceChannel::SetOriginalContentType(const nsACString& aContentType) { + NS_ENSURE_TRUE(mChannel, NS_ERROR_FAILURE); + + // clear our cached content-type value + mContentType.Truncate(); + + return mChannel->SetContentType(aContentType); +} + +NS_IMETHODIMP +nsViewSourceChannel::GetIsSrcdocChannel(bool* aIsSrcdocChannel) { + *aIsSrcdocChannel = mIsSrcdocChannel; + return NS_OK; +} + +NS_IMETHODIMP +nsViewSourceChannel::GetBaseURI(nsIURI** aBaseURI) { + if (mIsSrcdocChannel) { + nsCOMPtr isc = do_QueryInterface(mChannel); + if (isc) { + return isc->GetBaseURI(aBaseURI); + } + } + *aBaseURI = mBaseURI; + NS_IF_ADDREF(*aBaseURI); + return NS_OK; +} + +NS_IMETHODIMP +nsViewSourceChannel::SetBaseURI(nsIURI* aBaseURI) { + mBaseURI = aBaseURI; + return NS_OK; +} + +nsIChannel* nsViewSourceChannel::GetInnerChannel() { return mChannel; } + +NS_IMETHODIMP +nsViewSourceChannel::GetProtocolVersion(nsACString& aProtocolVersion) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +// nsIRequestObserver methods +NS_IMETHODIMP +nsViewSourceChannel::OnStartRequest(nsIRequest* aRequest) { + NS_ENSURE_TRUE(mListener, NS_ERROR_FAILURE); + // The channel may have gotten redirected... Time to update our info + mChannel = do_QueryInterface(aRequest); + UpdateChannelInterfaces(); + + nsresult rv = UpdateLoadInfoResultPrincipalURI(); + if (NS_FAILED(rv)) { + Cancel(rv); + } + + return mListener->OnStartRequest(static_cast(this)); +} + +NS_IMETHODIMP +nsViewSourceChannel::OnStopRequest(nsIRequest* aRequest, nsresult aStatus) { + NS_ENSURE_TRUE(mListener, NS_ERROR_FAILURE); + if (mChannel) { + nsCOMPtr loadGroup; + mChannel->GetLoadGroup(getter_AddRefs(loadGroup)); + if (loadGroup) { + loadGroup->RemoveRequest(static_cast(this), + nullptr, aStatus); + } + } + + nsresult rv = mListener->OnStopRequest( + static_cast(this), aStatus); + + ReleaseListeners(); + + return rv; +} + +// nsIStreamListener methods +NS_IMETHODIMP +nsViewSourceChannel::OnDataAvailable(nsIRequest* aRequest, + nsIInputStream* aInputStream, + uint64_t aSourceOffset, uint32_t aLength) { + NS_ENSURE_TRUE(mListener, NS_ERROR_FAILURE); + return mListener->OnDataAvailable(static_cast(this), + aInputStream, aSourceOffset, aLength); +} + +// nsIHttpChannel methods + +// We want to forward most of nsIHttpChannel over to mHttpChannel, but we want +// to override GetRequestHeader and VisitHeaders. The reason is that we don't +// want various headers like Link: and Refresh: applying to view-source. +NS_IMETHODIMP +nsViewSourceChannel::GetChannelId(uint64_t* aChannelId) { + NS_ENSURE_ARG_POINTER(aChannelId); + return !mHttpChannel ? NS_ERROR_NULL_POINTER + : mHttpChannel->GetChannelId(aChannelId); +} + +NS_IMETHODIMP +nsViewSourceChannel::SetChannelId(uint64_t aChannelId) { + return !mHttpChannel ? NS_ERROR_NULL_POINTER + : mHttpChannel->SetChannelId(aChannelId); +} + +NS_IMETHODIMP +nsViewSourceChannel::GetTopLevelContentWindowId(uint64_t* aWindowId) { + return !mHttpChannel ? NS_ERROR_NULL_POINTER + : mHttpChannel->GetTopLevelContentWindowId(aWindowId); +} + +NS_IMETHODIMP +nsViewSourceChannel::SetTopLevelContentWindowId(uint64_t aWindowId) { + return !mHttpChannel ? NS_ERROR_NULL_POINTER + : mHttpChannel->SetTopLevelContentWindowId(aWindowId); +} + +NS_IMETHODIMP +nsViewSourceChannel::GetTopLevelOuterContentWindowId(uint64_t* aWindowId) { + return !mHttpChannel + ? NS_ERROR_NULL_POINTER + : mHttpChannel->GetTopLevelOuterContentWindowId(aWindowId); +} + +NS_IMETHODIMP +nsViewSourceChannel::SetTopLevelOuterContentWindowId(uint64_t aWindowId) { + return !mHttpChannel + ? NS_ERROR_NULL_POINTER + : mHttpChannel->SetTopLevelOuterContentWindowId(aWindowId); +} + +NS_IMETHODIMP +nsViewSourceChannel::GetFlashPluginState( + nsIHttpChannel::FlashPluginState* aResult) { + return !mHttpChannel ? NS_ERROR_NULL_POINTER + : mHttpChannel->GetFlashPluginState(aResult); +} + +NS_IMETHODIMP +nsViewSourceChannel::GetRequestMethod(nsACString& aRequestMethod) { + return !mHttpChannel ? NS_ERROR_NULL_POINTER + : mHttpChannel->GetRequestMethod(aRequestMethod); +} + +NS_IMETHODIMP +nsViewSourceChannel::SetRequestMethod(const nsACString& aRequestMethod) { + return !mHttpChannel ? NS_ERROR_NULL_POINTER + : mHttpChannel->SetRequestMethod(aRequestMethod); +} + +NS_IMETHODIMP +nsViewSourceChannel::GetReferrerInfo(nsIReferrerInfo** aReferrerInfo) { + return !mHttpChannel ? NS_ERROR_NULL_POINTER + : mHttpChannel->GetReferrerInfo(aReferrerInfo); +} + +NS_IMETHODIMP +nsViewSourceChannel::SetReferrerInfo(nsIReferrerInfo* aReferrerInfo) { + return !mHttpChannel ? NS_ERROR_NULL_POINTER + : mHttpChannel->SetReferrerInfo(aReferrerInfo); +} + +NS_IMETHODIMP nsViewSourceChannel::SetReferrerInfoWithoutClone( + nsIReferrerInfo* aReferrerInfo) { + return !mHttpChannel + ? NS_ERROR_NULL_POINTER + : mHttpChannel->SetReferrerInfoWithoutClone(aReferrerInfo); +} + +NS_IMETHODIMP +nsViewSourceChannel::GetRequestHeader(const nsACString& aHeader, + nsACString& aValue) { + aValue.Truncate(); + return !mHttpChannel ? NS_ERROR_NULL_POINTER + : mHttpChannel->GetRequestHeader(aHeader, aValue); +} + +NS_IMETHODIMP +nsViewSourceChannel::SetRequestHeader(const nsACString& aHeader, + const nsACString& aValue, bool aMerge) { + return !mHttpChannel + ? NS_ERROR_NULL_POINTER + : mHttpChannel->SetRequestHeader(aHeader, aValue, aMerge); +} + +NS_IMETHODIMP +nsViewSourceChannel::SetNewReferrerInfo( + const nsACString& aUrl, nsIReferrerInfo::ReferrerPolicyIDL aPolicy, + bool aSendReferrer) { + return !mHttpChannel + ? NS_ERROR_NULL_POINTER + : mHttpChannel->SetNewReferrerInfo(aUrl, aPolicy, aSendReferrer); +} + +NS_IMETHODIMP +nsViewSourceChannel::SetEmptyRequestHeader(const nsACString& aHeader) { + return !mHttpChannel ? NS_ERROR_NULL_POINTER + : mHttpChannel->SetEmptyRequestHeader(aHeader); +} + +NS_IMETHODIMP +nsViewSourceChannel::VisitRequestHeaders(nsIHttpHeaderVisitor* aVisitor) { + return !mHttpChannel ? NS_ERROR_NULL_POINTER + : mHttpChannel->VisitRequestHeaders(aVisitor); +} + +NS_IMETHODIMP +nsViewSourceChannel::VisitNonDefaultRequestHeaders( + nsIHttpHeaderVisitor* aVisitor) { + return !mHttpChannel ? NS_ERROR_NULL_POINTER + : mHttpChannel->VisitNonDefaultRequestHeaders(aVisitor); +} + +NS_IMETHODIMP +nsViewSourceChannel::ShouldStripRequestBodyHeader(const nsACString& aMethod, + bool* aResult) { + return !mHttpChannel + ? NS_ERROR_NULL_POINTER + : mHttpChannel->ShouldStripRequestBodyHeader(aMethod, aResult); +} + +NS_IMETHODIMP +nsViewSourceChannel::GetAllowPipelining(bool* aAllowPipelining) { + return !mHttpChannel ? NS_ERROR_NULL_POINTER + : mHttpChannel->GetAllowPipelining(aAllowPipelining); +} + +NS_IMETHODIMP +nsViewSourceChannel::SetAllowPipelining(bool aAllowPipelining) { + return !mHttpChannel ? NS_ERROR_NULL_POINTER + : mHttpChannel->SetAllowPipelining(aAllowPipelining); +} + +NS_IMETHODIMP +nsViewSourceChannel::GetAllowSTS(bool* aAllowSTS) { + return !mHttpChannel ? NS_ERROR_NULL_POINTER + : mHttpChannel->GetAllowSTS(aAllowSTS); +} + +NS_IMETHODIMP +nsViewSourceChannel::SetAllowSTS(bool aAllowSTS) { + return !mHttpChannel ? NS_ERROR_NULL_POINTER + : mHttpChannel->SetAllowSTS(aAllowSTS); +} + +NS_IMETHODIMP +nsViewSourceChannel::GetRedirectionLimit(uint32_t* aRedirectionLimit) { + return !mHttpChannel ? NS_ERROR_NULL_POINTER + : mHttpChannel->GetRedirectionLimit(aRedirectionLimit); +} + +NS_IMETHODIMP +nsViewSourceChannel::SetRedirectionLimit(uint32_t aRedirectionLimit) { + return !mHttpChannel ? NS_ERROR_NULL_POINTER + : mHttpChannel->SetRedirectionLimit(aRedirectionLimit); +} + +NS_IMETHODIMP +nsViewSourceChannel::GetResponseStatus(uint32_t* aResponseStatus) { + return !mHttpChannel ? NS_ERROR_NULL_POINTER + : mHttpChannel->GetResponseStatus(aResponseStatus); +} + +NS_IMETHODIMP +nsViewSourceChannel::GetResponseStatusText(nsACString& aResponseStatusText) { + return !mHttpChannel + ? NS_ERROR_NULL_POINTER + : mHttpChannel->GetResponseStatusText(aResponseStatusText); +} + +NS_IMETHODIMP +nsViewSourceChannel::GetRequestSucceeded(bool* aRequestSucceeded) { + return !mHttpChannel ? NS_ERROR_NULL_POINTER + : mHttpChannel->GetRequestSucceeded(aRequestSucceeded); +} + +NS_IMETHODIMP +nsViewSourceChannel::GetResponseHeader(const nsACString& aHeader, + nsACString& aValue) { + aValue.Truncate(); + if (!mHttpChannel) return NS_ERROR_NULL_POINTER; + + if (!aHeader.Equals("Content-Type"_ns, nsCaseInsensitiveCStringComparator) && + !aHeader.Equals("Content-Security-Policy"_ns, + nsCaseInsensitiveCStringComparator) && + !aHeader.Equals("Content-Security-Policy-Report-Only"_ns, + nsCaseInsensitiveCStringComparator) && + !aHeader.Equals("X-Frame-Options"_ns, + nsCaseInsensitiveCStringComparator)) { + // We simulate the NS_ERROR_NOT_AVAILABLE error which is produced by + // GetResponseHeader via nsHttpHeaderArray::GetHeader when the entry is + // not present, such that it appears as though no headers except for the + // whitelisted ones were set on this channel. + return NS_ERROR_NOT_AVAILABLE; + } + + return mHttpChannel->GetResponseHeader(aHeader, aValue); +} + +NS_IMETHODIMP +nsViewSourceChannel::SetResponseHeader(const nsACString& header, + const nsACString& value, bool merge) { + return !mHttpChannel ? NS_ERROR_NULL_POINTER + : mHttpChannel->SetResponseHeader(header, value, merge); +} + +NS_IMETHODIMP +nsViewSourceChannel::VisitResponseHeaders(nsIHttpHeaderVisitor* aVisitor) { + if (!mHttpChannel) return NS_ERROR_NULL_POINTER; + + constexpr auto contentTypeStr = "Content-Type"_ns; + nsAutoCString contentType; + nsresult rv = mHttpChannel->GetResponseHeader(contentTypeStr, contentType); + if (NS_SUCCEEDED(rv)) { + return aVisitor->VisitHeader(contentTypeStr, contentType); + } + return NS_OK; +} + +NS_IMETHODIMP +nsViewSourceChannel::GetOriginalResponseHeader(const nsACString& aHeader, + nsIHttpHeaderVisitor* aVisitor) { + nsAutoCString value; + nsresult rv = GetResponseHeader(aHeader, value); + if (NS_FAILED(rv)) { + return rv; + } + return aVisitor->VisitHeader(aHeader, value); +} + +NS_IMETHODIMP +nsViewSourceChannel::VisitOriginalResponseHeaders( + nsIHttpHeaderVisitor* aVisitor) { + return VisitResponseHeaders(aVisitor); +} + +NS_IMETHODIMP +nsViewSourceChannel::IsNoStoreResponse(bool* _retval) { + return !mHttpChannel ? NS_ERROR_NULL_POINTER + : mHttpChannel->IsNoStoreResponse(_retval); +} + +NS_IMETHODIMP +nsViewSourceChannel::IsNoCacheResponse(bool* _retval) { + return !mHttpChannel ? NS_ERROR_NULL_POINTER + : mHttpChannel->IsNoCacheResponse(_retval); +} + +NS_IMETHODIMP +nsViewSourceChannel::IsPrivateResponse(bool* _retval) { + return !mHttpChannel ? NS_ERROR_NULL_POINTER + : mHttpChannel->IsPrivateResponse(_retval); +} + +NS_IMETHODIMP +nsViewSourceChannel::RedirectTo(nsIURI* uri) { + return !mHttpChannel ? NS_ERROR_NULL_POINTER : mHttpChannel->RedirectTo(uri); +} + +NS_IMETHODIMP +nsViewSourceChannel::UpgradeToSecure() { + return !mHttpChannel ? NS_ERROR_NULL_POINTER + : mHttpChannel->UpgradeToSecure(); +} + +NS_IMETHODIMP +nsViewSourceChannel::GetRequestContextID(uint64_t* _retval) { + return !mHttpChannel ? NS_ERROR_NULL_POINTER + : mHttpChannel->GetRequestContextID(_retval); +} + +NS_IMETHODIMP +nsViewSourceChannel::SetRequestContextID(uint64_t rcid) { + return !mHttpChannel ? NS_ERROR_NULL_POINTER + : mHttpChannel->SetRequestContextID(rcid); +} + +NS_IMETHODIMP +nsViewSourceChannel::GetIsMainDocumentChannel(bool* aValue) { + return !mHttpChannel ? NS_ERROR_NULL_POINTER + : mHttpChannel->GetIsMainDocumentChannel(aValue); +} + +NS_IMETHODIMP +nsViewSourceChannel::SetIsMainDocumentChannel(bool aValue) { + return !mHttpChannel ? NS_ERROR_NULL_POINTER + : mHttpChannel->SetIsMainDocumentChannel(aValue); +} + +// Have to manually forward SetCorsPreflightParameters since it's [notxpcom] +void nsViewSourceChannel::SetCorsPreflightParameters( + const nsTArray& aUnsafeHeaders, + bool aShouldStripRequestBodyHeader) { + mHttpChannelInternal->SetCorsPreflightParameters( + aUnsafeHeaders, aShouldStripRequestBodyHeader); +} + +void nsViewSourceChannel::SetAltDataForChild(bool aIsForChild) { + mHttpChannelInternal->SetAltDataForChild(aIsForChild); +} + +void nsViewSourceChannel::DisableAltDataCache() { + mHttpChannelInternal->DisableAltDataCache(); +} + +NS_IMETHODIMP +nsViewSourceChannel::LogBlockedCORSRequest(const nsAString& aMessage, + const nsACString& aCategory) { + if (!mHttpChannel) { + NS_WARNING( + "nsViewSourceChannel::LogBlockedCORSRequest mHttpChannel is null"); + return NS_ERROR_UNEXPECTED; + } + return mHttpChannel->LogBlockedCORSRequest(aMessage, aCategory); +} + +NS_IMETHODIMP +nsViewSourceChannel::LogMimeTypeMismatch(const nsACString& aMessageName, + bool aWarning, const nsAString& aURL, + const nsAString& aContentType) { + if (!mHttpChannel) { + NS_WARNING("nsViewSourceChannel::LogMimeTypeMismatch mHttpChannel is null"); + return NS_ERROR_UNEXPECTED; + } + return mHttpChannel->LogMimeTypeMismatch(aMessageName, aWarning, aURL, + aContentType); +} + +const nsTArray& +nsViewSourceChannel::PreferredAlternativeDataTypes() { + if (mCacheInfoChannel) { + return mCacheInfoChannel->PreferredAlternativeDataTypes(); + } + return mEmptyArray; +} + +void nsViewSourceChannel::SetIPv4Disabled() { + if (mHttpChannelInternal) { + mHttpChannelInternal->SetIPv4Disabled(); + } +} + +void nsViewSourceChannel::SetIPv6Disabled() { + if (mHttpChannelInternal) { + mHttpChannelInternal->SetIPv6Disabled(); + } +} + +bool nsViewSourceChannel::GetHasNonEmptySandboxingFlag() { + if (mHttpChannelInternal) { + return mHttpChannelInternal->GetHasNonEmptySandboxingFlag(); + } + return false; +} + +void nsViewSourceChannel::SetHasNonEmptySandboxingFlag( + bool aHasNonEmptySandboxingFlag) { + if (mHttpChannelInternal) { + mHttpChannelInternal->SetHasNonEmptySandboxingFlag( + aHasNonEmptySandboxingFlag); + } +} + +void nsViewSourceChannel::DoDiagnosticAssertWhenOnStopNotCalledOnDestroy() { + if (mHttpChannelInternal) { + mHttpChannelInternal->DoDiagnosticAssertWhenOnStopNotCalledOnDestroy(); + } +} + +// nsIChildChannel methods + +NS_IMETHODIMP +nsViewSourceChannel::ConnectParent(uint32_t aRegistarId) { + NS_ENSURE_TRUE(mChildChannel, NS_ERROR_NULL_POINTER); + + return mChildChannel->ConnectParent(aRegistarId); +} + +NS_IMETHODIMP +nsViewSourceChannel::CompleteRedirectSetup(nsIStreamListener* aListener) { + NS_ENSURE_TRUE(mChildChannel, NS_ERROR_NULL_POINTER); + + mListener = aListener; + + /* + * We want to add ourselves to the loadgroup before opening + * mChannel, since we want to make sure we're in the loadgroup + * when mChannel finishes and fires OnStopRequest() + */ + + nsCOMPtr loadGroup; + mChannel->GetLoadGroup(getter_AddRefs(loadGroup)); + if (loadGroup) { + loadGroup->AddRequest(static_cast(this), nullptr); + } + + nsresult rv = NS_OK; + rv = mChildChannel->CompleteRedirectSetup(this); + + if (NS_FAILED(rv) && loadGroup) { + loadGroup->RemoveRequest(static_cast(this), nullptr, + rv); + } + + if (NS_SUCCEEDED(rv)) { + mOpened = true; + } + + return rv; +} + +// nsIChannelEventSink + +NS_IMETHODIMP +nsViewSourceChannel::AsyncOnChannelRedirect( + nsIChannel* oldChannel, nsIChannel* newChannel, uint32_t flags, + nsIAsyncVerifyRedirectCallback* callback) { + nsresult rv; + + // We want consumers of the new inner channel be able to recognize it's + // actually used for view-source. Hence we modify its original URI here. + // This will not affect security checks because those have already been + // synchronously done before our event sink is called. Note that result + // principal URI is still modified at the OnStartRequest notification. + nsCOMPtr newChannelOrigURI; + rv = newChannel->GetOriginalURI(getter_AddRefs(newChannelOrigURI)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr newChannelUpdatedOrigURI; + rv = BuildViewSourceURI(newChannelOrigURI, + getter_AddRefs(newChannelUpdatedOrigURI)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = newChannel->SetOriginalURI(newChannelUpdatedOrigURI); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr sink(do_QueryInterface(mCallbacks)); + if (sink) { + return sink->AsyncOnChannelRedirect(oldChannel, newChannel, flags, + callback); + } + + callback->OnRedirectVerifyCallback(NS_OK); + return NS_OK; +} + +// nsIInterfaceRequestor + +NS_IMETHODIMP +nsViewSourceChannel::GetInterface(const nsIID& aIID, void** aResult) { + if (aIID.Equals(NS_GET_IID(nsIChannelEventSink))) { + nsCOMPtr self(this); + self.forget(aResult); + return NS_OK; + } + + if (mCallbacks) { + return mCallbacks->GetInterface(aIID, aResult); + } + + return NS_ERROR_NO_INTERFACE; +} diff --git a/netwerk/protocol/viewsource/nsViewSourceChannel.h b/netwerk/protocol/viewsource/nsViewSourceChannel.h new file mode 100644 index 0000000000..66704d1fa8 --- /dev/null +++ b/netwerk/protocol/viewsource/nsViewSourceChannel.h @@ -0,0 +1,104 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef nsViewSourceChannel_h___ +#define nsViewSourceChannel_h___ + +#include "mozilla/Attributes.h" +#include "mozilla/net/NeckoChannelParams.h" +#include "nsCOMPtr.h" +#include "nsIApplicationCacheChannel.h" +#include "nsICachingChannel.h" +#include "nsIChannelEventSink.h" +#include "nsIFormPOSTActionChannel.h" +#include "nsIHttpChannel.h" +#include "nsIHttpChannelInternal.h" +#include "nsIInterfaceRequestor.h" +#include "nsIStreamListener.h" +#include "nsIURI.h" +#include "nsIViewSourceChannel.h" +#include "nsIChildChannel.h" +#include "nsString.h" + +class nsViewSourceChannel final : public nsIViewSourceChannel, + public nsIStreamListener, + public nsIHttpChannel, + public nsIHttpChannelInternal, + public nsICachingChannel, + public nsIApplicationCacheChannel, + public nsIFormPOSTActionChannel, + public nsIChildChannel, + public nsIInterfaceRequestor, + public nsIChannelEventSink { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIREQUEST + NS_DECL_NSICHANNEL + NS_DECL_NSIIDENTCHANNEL + NS_DECL_NSIVIEWSOURCECHANNEL + NS_DECL_NSISTREAMLISTENER + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSIHTTPCHANNEL + NS_DECL_NSICHILDCHANNEL + NS_DECL_NSIINTERFACEREQUESTOR + NS_DECL_NSICHANNELEVENTSINK + NS_FORWARD_SAFE_NSICACHEINFOCHANNEL(mCacheInfoChannel) + NS_FORWARD_SAFE_NSICACHINGCHANNEL(mCachingChannel) + NS_FORWARD_SAFE_NSIAPPLICATIONCACHECHANNEL(mApplicationCacheChannel) + NS_FORWARD_SAFE_NSIAPPLICATIONCACHECONTAINER(mApplicationCacheChannel) + NS_FORWARD_SAFE_NSIUPLOADCHANNEL(mUploadChannel) + NS_FORWARD_SAFE_NSIFORMPOSTACTIONCHANNEL(mPostChannel) + NS_FORWARD_SAFE_NSIHTTPCHANNELINTERNAL(mHttpChannelInternal) + + // nsViewSourceChannel methods: + nsViewSourceChannel() + : mIsDocument(false), mOpened(false), mIsSrcdocChannel(false) {} + + [[nodiscard]] nsresult Init(nsIURI* uri, nsILoadInfo* aLoadInfo); + + [[nodiscard]] nsresult InitSrcdoc(nsIURI* aURI, nsIURI* aBaseURI, + const nsAString& aSrcdoc, + nsILoadInfo* aLoadInfo); + + // Updates or sets the result principal URI of the underlying channel's + // loadinfo to be prefixed with the "view-source:" schema as: + // + // mChannel.loadInfo.resultPrincipalURI = "view-source:" + + // (mChannel.loadInfo.resultPrincipalURI | mChannel.orignalURI); + nsresult UpdateLoadInfoResultPrincipalURI(); + + protected: + ~nsViewSourceChannel() = default; + void ReleaseListeners(); + + nsTArray mEmptyArray; + + // Clones aURI and prefixes it with "view-source:" schema, + nsresult BuildViewSourceURI(nsIURI* aURI, nsIURI** aResult); + + // Called to update the forwarding channel members after the `mChannel` field + // has been changed to reflect the new inner channel. + void UpdateChannelInterfaces(); + + nsCOMPtr mCallbacks; + nsCOMPtr mChannel; + nsCOMPtr mHttpChannel; + nsCOMPtr mHttpChannelInternal; + nsCOMPtr mCachingChannel; + nsCOMPtr mCacheInfoChannel; + nsCOMPtr mApplicationCacheChannel; + nsCOMPtr mUploadChannel; + nsCOMPtr mPostChannel; + nsCOMPtr mChildChannel; + nsCOMPtr mListener; + nsCOMPtr mOriginalURI; + nsCOMPtr mBaseURI; + nsCString mContentType; + bool mIsDocument; // keeps track of the LOAD_DOCUMENT_URI flag + bool mOpened; + bool mIsSrcdocChannel; +}; + +#endif /* nsViewSourceChannel_h___ */ diff --git a/netwerk/protocol/viewsource/nsViewSourceHandler.cpp b/netwerk/protocol/viewsource/nsViewSourceHandler.cpp new file mode 100644 index 0000000000..3ee94f058e --- /dev/null +++ b/netwerk/protocol/viewsource/nsViewSourceHandler.cpp @@ -0,0 +1,153 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=4 sw=2 sts=2 et: */ +/* 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 "nsViewSourceHandler.h" +#include "nsViewSourceChannel.h" +#include "nsNetUtil.h" +#include "nsSimpleNestedURI.h" + +#define VIEW_SOURCE "view-source" + +#define DEFAULT_FLAGS \ + (URI_NORELATIVE | URI_NOAUTH | URI_DANGEROUS_TO_LOAD | URI_NON_PERSISTABLE) + +namespace mozilla { +namespace net { + +//////////////////////////////////////////////////////////////////////////////// + +NS_IMPL_ISUPPORTS(nsViewSourceHandler, nsIProtocolHandler, + nsIProtocolHandlerWithDynamicFlags) + +//////////////////////////////////////////////////////////////////////////////// +// nsIProtocolHandler methods: + +NS_IMETHODIMP +nsViewSourceHandler::GetScheme(nsACString& result) { + result.AssignLiteral(VIEW_SOURCE); + return NS_OK; +} + +NS_IMETHODIMP +nsViewSourceHandler::GetDefaultPort(int32_t* result) { + *result = -1; + return NS_OK; +} + +NS_IMETHODIMP +nsViewSourceHandler::GetProtocolFlags(uint32_t* result) { + *result = DEFAULT_FLAGS; + return NS_OK; +} + +NS_IMETHODIMP +nsViewSourceHandler::GetFlagsForURI(nsIURI* aURI, uint32_t* result) { + *result = DEFAULT_FLAGS; + nsCOMPtr nestedURI(do_QueryInterface(aURI)); + if (!nestedURI) { + return NS_OK; + } + + nsCOMPtr innerURI; + nestedURI->GetInnerURI(getter_AddRefs(innerURI)); + nsCOMPtr netUtil = do_GetNetUtil(); + bool isLoadable = false; + nsresult rv = + netUtil->ProtocolHasFlags(innerURI, URI_LOADABLE_BY_ANYONE, &isLoadable); + NS_ENSURE_SUCCESS(rv, rv); + if (isLoadable) { + *result |= URI_LOADABLE_BY_EXTENSIONS; + } + return NS_OK; +} + +// static +nsresult nsViewSourceHandler::CreateNewURI(const nsACString& aSpec, + const char* aCharset, + nsIURI* aBaseURI, nsIURI** aResult) { + *aResult = nullptr; + + // Extract inner URL and normalize to ASCII. This is done to properly + // support IDN in cases like "view-source:http://www.szalagavató.hu/" + + int32_t colon = aSpec.FindChar(':'); + if (colon == kNotFound) return NS_ERROR_MALFORMED_URI; + + nsCOMPtr innerURI; + nsresult rv = NS_NewURI(getter_AddRefs(innerURI), Substring(aSpec, colon + 1), + aCharset, aBaseURI); + if (NS_FAILED(rv)) return rv; + + nsAutoCString asciiSpec; + rv = innerURI->GetAsciiSpec(asciiSpec); + if (NS_FAILED(rv)) return rv; + + // put back our scheme and construct a simple-uri wrapper + + asciiSpec.Insert(VIEW_SOURCE ":", 0); + + nsCOMPtr uri; + rv = NS_MutateURI(new nsSimpleNestedURI::Mutator()) + .Apply(NS_MutatorMethod(&nsINestedURIMutator::Init, innerURI)) + .SetSpec(asciiSpec) + .Finalize(uri); + if (NS_FAILED(rv)) { + return rv; + } + + uri.swap(*aResult); + return rv; +} + +NS_IMETHODIMP +nsViewSourceHandler::NewChannel(nsIURI* uri, nsILoadInfo* aLoadInfo, + nsIChannel** result) { + NS_ENSURE_ARG_POINTER(uri); + RefPtr channel = new nsViewSourceChannel(); + + nsresult rv = channel->Init(uri, aLoadInfo); + if (NS_FAILED(rv)) { + return rv; + } + + *result = channel.forget().downcast().take(); + return NS_OK; +} + +nsresult nsViewSourceHandler::NewSrcdocChannel(nsIURI* aURI, nsIURI* aBaseURI, + const nsAString& aSrcdoc, + nsILoadInfo* aLoadInfo, + nsIChannel** outChannel) { + NS_ENSURE_ARG_POINTER(aURI); + RefPtr channel = new nsViewSourceChannel(); + + nsresult rv = channel->InitSrcdoc(aURI, aBaseURI, aSrcdoc, aLoadInfo); + if (NS_FAILED(rv)) { + return rv; + } + + *outChannel = static_cast(channel.forget().take()); + return NS_OK; +} + +NS_IMETHODIMP +nsViewSourceHandler::AllowPort(int32_t port, const char* scheme, + bool* _retval) { + // don't override anything. + *_retval = false; + return NS_OK; +} + +nsViewSourceHandler::nsViewSourceHandler() { gInstance = this; } + +nsViewSourceHandler::~nsViewSourceHandler() { gInstance = nullptr; } + +nsViewSourceHandler* nsViewSourceHandler::gInstance = nullptr; + +nsViewSourceHandler* nsViewSourceHandler::GetInstance() { return gInstance; } + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/viewsource/nsViewSourceHandler.h b/netwerk/protocol/viewsource/nsViewSourceHandler.h new file mode 100644 index 0000000000..b9e9f3ab7c --- /dev/null +++ b/netwerk/protocol/viewsource/nsViewSourceHandler.h @@ -0,0 +1,48 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef nsViewSourceHandler_h___ +#define nsViewSourceHandler_h___ + +#include "nsIProtocolHandler.h" +#include "nsNetUtil.h" +#include "mozilla/Attributes.h" + +class nsILoadInfo; + +namespace mozilla { +namespace net { + +class nsViewSourceHandler final : public nsIProtocolHandlerWithDynamicFlags, + public nsIProtocolHandler { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIPROTOCOLHANDLER + NS_DECL_NSIPROTOCOLHANDLERWITHDYNAMICFLAGS + + nsViewSourceHandler(); + + // Creates a new nsViewSourceChannel to view the source of an about:srcdoc + // URI with contents specified by srcdoc. + [[nodiscard]] nsresult NewSrcdocChannel(nsIURI* aURI, nsIURI* aBaseURI, + const nsAString& aSrcdoc, + nsILoadInfo* aLoadInfo, + nsIChannel** outChannel); + + static nsViewSourceHandler* GetInstance(); + + static nsresult CreateNewURI(const nsACString& aSpec, const char* aCharset, + nsIURI* aBaseURI, nsIURI** aResult); + + private: + ~nsViewSourceHandler(); + + static nsViewSourceHandler* gInstance; +}; + +} // namespace net +} // namespace mozilla + +#endif /* !defined( nsViewSourceHandler_h___ ) */ diff --git a/netwerk/protocol/websocket/BaseWebSocketChannel.cpp b/netwerk/protocol/websocket/BaseWebSocketChannel.cpp new file mode 100644 index 0000000000..af3037e270 --- /dev/null +++ b/netwerk/protocol/websocket/BaseWebSocketChannel.cpp @@ -0,0 +1,367 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et 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 "WebSocketLog.h" +#include "BaseWebSocketChannel.h" +#include "MainThreadUtils.h" +#include "nsILoadGroup.h" +#include "nsINode.h" +#include "nsIInterfaceRequestor.h" +#include "nsProxyRelease.h" +#include "nsStandardURL.h" +#include "LoadInfo.h" +#include "mozilla/dom/ContentChild.h" +#include "nsITransportProvider.h" + +using mozilla::dom::ContentChild; + +namespace mozilla { +namespace net { + +LazyLogModule webSocketLog("nsWebSocket"); +static uint64_t gNextWebSocketID = 0; + +// We use only 53 bits for the WebSocket serial ID so that it can be converted +// to and from a JS value without loss of precision. The upper bits of the +// WebSocket serial ID hold the process ID. The lower bits identify the +// WebSocket. +static const uint64_t kWebSocketIDTotalBits = 53; +static const uint64_t kWebSocketIDProcessBits = 22; +static const uint64_t kWebSocketIDWebSocketBits = + kWebSocketIDTotalBits - kWebSocketIDProcessBits; + +BaseWebSocketChannel::BaseWebSocketChannel() + : mWasOpened(0), + mClientSetPingInterval(0), + mClientSetPingTimeout(0), + mEncrypted(false), + mPingForced(false), + mIsServerSide(false), + mPingInterval(0), + mPingResponseTimeout(10000), + mHttpChannelId(0) { + // Generation of a unique serial ID. + uint64_t processID = 0; + if (XRE_IsContentProcess()) { + ContentChild* cc = ContentChild::GetSingleton(); + processID = cc->GetID(); + } + + uint64_t processBits = + processID & ((uint64_t(1) << kWebSocketIDProcessBits) - 1); + + // Make sure no actual webSocket ends up with mWebSocketID == 0 but less then + // what the kWebSocketIDProcessBits allows. + if (++gNextWebSocketID >= (uint64_t(1) << kWebSocketIDWebSocketBits)) { + gNextWebSocketID = 1; + } + + uint64_t webSocketBits = + gNextWebSocketID & ((uint64_t(1) << kWebSocketIDWebSocketBits) - 1); + mSerial = (processBits << kWebSocketIDWebSocketBits) | webSocketBits; +} + +//----------------------------------------------------------------------------- +// BaseWebSocketChannel::nsIWebSocketChannel +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +BaseWebSocketChannel::GetOriginalURI(nsIURI** aOriginalURI) { + LOG(("BaseWebSocketChannel::GetOriginalURI() %p\n", this)); + + if (!mOriginalURI) return NS_ERROR_NOT_INITIALIZED; + *aOriginalURI = do_AddRef(mOriginalURI).take(); + return NS_OK; +} + +NS_IMETHODIMP +BaseWebSocketChannel::GetURI(nsIURI** aURI) { + LOG(("BaseWebSocketChannel::GetURI() %p\n", this)); + + if (!mOriginalURI) return NS_ERROR_NOT_INITIALIZED; + if (mURI) { + *aURI = do_AddRef(mURI).take(); + } else { + *aURI = do_AddRef(mOriginalURI).take(); + } + return NS_OK; +} + +NS_IMETHODIMP +BaseWebSocketChannel::GetNotificationCallbacks( + nsIInterfaceRequestor** aNotificationCallbacks) { + LOG(("BaseWebSocketChannel::GetNotificationCallbacks() %p\n", this)); + NS_IF_ADDREF(*aNotificationCallbacks = mCallbacks); + return NS_OK; +} + +NS_IMETHODIMP +BaseWebSocketChannel::SetNotificationCallbacks( + nsIInterfaceRequestor* aNotificationCallbacks) { + LOG(("BaseWebSocketChannel::SetNotificationCallbacks() %p\n", this)); + mCallbacks = aNotificationCallbacks; + return NS_OK; +} + +NS_IMETHODIMP +BaseWebSocketChannel::GetLoadGroup(nsILoadGroup** aLoadGroup) { + LOG(("BaseWebSocketChannel::GetLoadGroup() %p\n", this)); + NS_IF_ADDREF(*aLoadGroup = mLoadGroup); + return NS_OK; +} + +NS_IMETHODIMP +BaseWebSocketChannel::SetLoadGroup(nsILoadGroup* aLoadGroup) { + LOG(("BaseWebSocketChannel::SetLoadGroup() %p\n", this)); + mLoadGroup = aLoadGroup; + return NS_OK; +} + +NS_IMETHODIMP +BaseWebSocketChannel::SetLoadInfo(nsILoadInfo* aLoadInfo) { + MOZ_RELEASE_ASSERT(aLoadInfo, "loadinfo can't be null"); + mLoadInfo = aLoadInfo; + return NS_OK; +} + +NS_IMETHODIMP +BaseWebSocketChannel::GetLoadInfo(nsILoadInfo** aLoadInfo) { + NS_IF_ADDREF(*aLoadInfo = mLoadInfo); + return NS_OK; +} + +NS_IMETHODIMP +BaseWebSocketChannel::GetExtensions(nsACString& aExtensions) { + LOG(("BaseWebSocketChannel::GetExtensions() %p\n", this)); + aExtensions = mNegotiatedExtensions; + return NS_OK; +} + +NS_IMETHODIMP +BaseWebSocketChannel::GetProtocol(nsACString& aProtocol) { + LOG(("BaseWebSocketChannel::GetProtocol() %p\n", this)); + aProtocol = mProtocol; + return NS_OK; +} + +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* aSeconds) { + // stored in ms but should only have second resolution + MOZ_ASSERT(!(mPingInterval % 1000)); + + *aSeconds = mPingInterval / 1000; + return NS_OK; +} + +NS_IMETHODIMP +BaseWebSocketChannel::SetPingInterval(uint32_t aSeconds) { + MOZ_ASSERT(NS_IsMainThread()); + + if (mWasOpened) { + return NS_ERROR_IN_PROGRESS; + } + + mPingInterval = aSeconds * 1000; + mClientSetPingInterval = 1; + + return NS_OK; +} + +NS_IMETHODIMP +BaseWebSocketChannel::GetPingTimeout(uint32_t* aSeconds) { + // stored in ms but should only have second resolution + MOZ_ASSERT(!(mPingResponseTimeout % 1000)); + + *aSeconds = mPingResponseTimeout / 1000; + return NS_OK; +} + +NS_IMETHODIMP +BaseWebSocketChannel::SetPingTimeout(uint32_t aSeconds) { + MOZ_ASSERT(NS_IsMainThread()); + + if (mWasOpened) { + return NS_ERROR_IN_PROGRESS; + } + + mPingResponseTimeout = aSeconds * 1000; + mClientSetPingTimeout = 1; + + return NS_OK; +} + +NS_IMETHODIMP +BaseWebSocketChannel::InitLoadInfoNative( + nsINode* aLoadingNode, nsIPrincipal* aLoadingPrincipal, + nsIPrincipal* aTriggeringPrincipal, + nsICookieJarSettings* aCookieJarSettings, uint32_t aSecurityFlags, + nsContentPolicyType aContentPolicyType, uint32_t aSandboxFlags) { + mLoadInfo = new LoadInfo( + aLoadingPrincipal, aTriggeringPrincipal, aLoadingNode, aSecurityFlags, + aContentPolicyType, Maybe(), + Maybe(), aSandboxFlags); + if (aCookieJarSettings) { + mLoadInfo->SetCookieJarSettings(aCookieJarSettings); + } + return NS_OK; +} + +NS_IMETHODIMP +BaseWebSocketChannel::InitLoadInfo(nsINode* aLoadingNode, + nsIPrincipal* aLoadingPrincipal, + nsIPrincipal* aTriggeringPrincipal, + uint32_t aSecurityFlags, + nsContentPolicyType aContentPolicyType) { + return InitLoadInfoNative(aLoadingNode, aLoadingPrincipal, + aTriggeringPrincipal, nullptr, aSecurityFlags, + aContentPolicyType, 0); +} + +NS_IMETHODIMP +BaseWebSocketChannel::GetSerial(uint32_t* aSerial) { + if (!aSerial) { + return NS_ERROR_FAILURE; + } + + *aSerial = mSerial; + return NS_OK; +} + +NS_IMETHODIMP +BaseWebSocketChannel::SetSerial(uint32_t aSerial) { + mSerial = aSerial; + return NS_OK; +} + +NS_IMETHODIMP +BaseWebSocketChannel::SetServerParameters( + nsITransportProvider* aProvider, const nsACString& aNegotiatedExtensions) { + MOZ_ASSERT(aProvider); + mServerTransportProvider = aProvider; + mNegotiatedExtensions = aNegotiatedExtensions; + mIsServerSide = true; + return NS_OK; +} + +NS_IMETHODIMP +BaseWebSocketChannel::GetHttpChannelId(uint64_t* aHttpChannelId) { + *aHttpChannelId = mHttpChannelId; + return NS_OK; +} + +//----------------------------------------------------------------------------- +// BaseWebSocketChannel::nsIProtocolHandler +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +BaseWebSocketChannel::GetScheme(nsACString& aScheme) { + LOG(("BaseWebSocketChannel::GetScheme() %p\n", this)); + + if (mEncrypted) + aScheme.AssignLiteral("wss"); + else + aScheme.AssignLiteral("ws"); + return NS_OK; +} + +NS_IMETHODIMP +BaseWebSocketChannel::GetDefaultPort(int32_t* aDefaultPort) { + LOG(("BaseWebSocketChannel::GetDefaultPort() %p\n", this)); + + if (mEncrypted) + *aDefaultPort = kDefaultWSSPort; + else + *aDefaultPort = kDefaultWSPort; + return NS_OK; +} + +NS_IMETHODIMP +BaseWebSocketChannel::GetProtocolFlags(uint32_t* aProtocolFlags) { + LOG(("BaseWebSocketChannel::GetProtocolFlags() %p\n", this)); + + *aProtocolFlags = URI_NORELATIVE | URI_NON_PERSISTABLE | ALLOWS_PROXY | + ALLOWS_PROXY_HTTP | URI_DOES_NOT_RETURN_DATA | + URI_DANGEROUS_TO_LOAD; + if (mEncrypted) { + *aProtocolFlags |= URI_IS_POTENTIALLY_TRUSTWORTHY; + } + return NS_OK; +} + +NS_IMETHODIMP +BaseWebSocketChannel::NewChannel(nsIURI* aURI, nsILoadInfo* aLoadInfo, + nsIChannel** outChannel) { + LOG(("BaseWebSocketChannel::NewChannel() %p\n", this)); + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +BaseWebSocketChannel::AllowPort(int32_t port, const char* scheme, + bool* _retval) { + LOG(("BaseWebSocketChannel::AllowPort() %p\n", this)); + + // do not override any blacklisted ports + *_retval = false; + return NS_OK; +} + +//----------------------------------------------------------------------------- +// BaseWebSocketChannel::nsIThreadRetargetableRequest +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +BaseWebSocketChannel::RetargetDeliveryTo(nsIEventTarget* aTargetThread) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aTargetThread); + MOZ_ASSERT(!mTargetThread, + "Delivery target should be set once, before AsyncOpen"); + MOZ_ASSERT(!mWasOpened, "Should not be called after AsyncOpen!"); + + mTargetThread = aTargetThread; + MOZ_ASSERT(mTargetThread); + return NS_OK; +} + +NS_IMETHODIMP +BaseWebSocketChannel::GetDeliveryTarget(nsIEventTarget** aTargetThread) { + MOZ_ASSERT(NS_IsMainThread()); + + nsCOMPtr target = mTargetThread; + if (!target) { + target = GetCurrentEventTarget(); + } + target.forget(aTargetThread); + return NS_OK; +} + +BaseWebSocketChannel::ListenerAndContextContainer::ListenerAndContextContainer( + nsIWebSocketListener* aListener, nsISupports* aContext) + : mListener(aListener), mContext(aContext) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mListener); +} + +BaseWebSocketChannel::ListenerAndContextContainer:: + ~ListenerAndContextContainer() { + MOZ_ASSERT(mListener); + + NS_ReleaseOnMainThread( + "BaseWebSocketChannel::ListenerAndContextContainer::mListener", + mListener.forget()); + NS_ReleaseOnMainThread( + "BaseWebSocketChannel::ListenerAndContextContainer::mContext", + mContext.forget()); +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/websocket/BaseWebSocketChannel.h b/netwerk/protocol/websocket/BaseWebSocketChannel.h new file mode 100644 index 0000000000..469b939e5d --- /dev/null +++ b/netwerk/protocol/websocket/BaseWebSocketChannel.h @@ -0,0 +1,127 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et 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/. */ + +#ifndef mozilla_net_BaseWebSocketChannel_h +#define mozilla_net_BaseWebSocketChannel_h + +#include "nsIWebSocketChannel.h" +#include "nsIWebSocketListener.h" +#include "nsIProtocolHandler.h" +#include "nsIThread.h" +#include "nsIThreadRetargetableRequest.h" +#include "nsCOMPtr.h" +#include "nsString.h" + +namespace mozilla { +namespace net { + +const static int32_t kDefaultWSPort = 80; +const static int32_t kDefaultWSSPort = 443; + +class BaseWebSocketChannel : public nsIWebSocketChannel, + public nsIProtocolHandler, + public nsIThreadRetargetableRequest { + public: + BaseWebSocketChannel(); + + NS_DECL_NSIPROTOCOLHANDLER + NS_DECL_NSITHREADRETARGETABLEREQUEST + + NS_IMETHOD QueryInterface(const nsIID& uuid, void** result) override = 0; + NS_IMETHOD_(MozExternalRefCountType) AddRef(void) override = 0; + NS_IMETHOD_(MozExternalRefCountType) Release(void) override = 0; + + // Partial implementation of nsIWebSocketChannel + // + NS_IMETHOD GetOriginalURI(nsIURI** aOriginalURI) override; + NS_IMETHOD GetURI(nsIURI** aURI) override; + NS_IMETHOD GetNotificationCallbacks( + nsIInterfaceRequestor** aNotificationCallbacks) override; + NS_IMETHOD SetNotificationCallbacks( + nsIInterfaceRequestor* aNotificationCallbacks) override; + NS_IMETHOD GetLoadGroup(nsILoadGroup** aLoadGroup) override; + NS_IMETHOD SetLoadGroup(nsILoadGroup* aLoadGroup) override; + NS_IMETHOD SetLoadInfo(nsILoadInfo* aLoadInfo) override; + NS_IMETHOD GetLoadInfo(nsILoadInfo** aLoadInfo) override; + NS_IMETHOD GetExtensions(nsACString& aExtensions) override; + NS_IMETHOD GetProtocol(nsACString& aProtocol) override; + NS_IMETHOD SetProtocol(const nsACString& aProtocol) override; + NS_IMETHOD GetPingInterval(uint32_t* aSeconds) override; + NS_IMETHOD SetPingInterval(uint32_t aSeconds) override; + NS_IMETHOD GetPingTimeout(uint32_t* aSeconds) override; + NS_IMETHOD SetPingTimeout(uint32_t aSeconds) override; + NS_IMETHOD InitLoadInfoNative(nsINode* aLoadingNode, + nsIPrincipal* aLoadingPrincipal, + nsIPrincipal* aTriggeringPrincipal, + nsICookieJarSettings* aCookieJarSettings, + uint32_t aSecurityFlags, + nsContentPolicyType aContentPolicyType, + uint32_t aSandboxFlags) override; + NS_IMETHOD InitLoadInfo(nsINode* aLoadingNode, + nsIPrincipal* aLoadingPrincipal, + nsIPrincipal* aTriggeringPrincipal, + uint32_t aSecurityFlags, + nsContentPolicyType aContentPolicyType) override; + NS_IMETHOD GetSerial(uint32_t* aSerial) override; + NS_IMETHOD SetSerial(uint32_t aSerial) override; + NS_IMETHOD SetServerParameters( + nsITransportProvider* aProvider, + const nsACString& aNegotiatedExtensions) override; + NS_IMETHOD GetHttpChannelId(uint64_t* aHttpChannelId) override; + + // Off main thread URI access. + virtual void GetEffectiveURL(nsAString& aEffectiveURL) const = 0; + virtual bool IsEncrypted() const = 0; + + class ListenerAndContextContainer final { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(ListenerAndContextContainer) + + ListenerAndContextContainer(nsIWebSocketListener* aListener, + nsISupports* aContext); + + nsCOMPtr mListener; + nsCOMPtr mContext; + + private: + ~ListenerAndContextContainer(); + }; + + protected: + nsCOMPtr mOriginalURI; + nsCOMPtr mURI; + RefPtr mListenerMT; + nsCOMPtr mCallbacks; + nsCOMPtr mLoadGroup; + nsCOMPtr mLoadInfo; + nsCOMPtr mTargetThread; + nsCOMPtr mServerTransportProvider; + + nsCString mProtocol; + nsCString mOrigin; + + nsCString mNegotiatedExtensions; + + uint32_t mWasOpened : 1; + uint32_t mClientSetPingInterval : 1; + uint32_t mClientSetPingTimeout : 1; + + Atomic mEncrypted; + bool mPingForced; + bool mIsServerSide; + + uint32_t mPingInterval; /* milliseconds */ + uint32_t mPingResponseTimeout; /* milliseconds */ + + uint32_t mSerial; + + uint64_t mHttpChannelId; +}; + +} // namespace net +} // namespace mozilla + +#endif // mozilla_net_BaseWebSocketChannel_h diff --git a/netwerk/protocol/websocket/IPCTransportProvider.cpp b/netwerk/protocol/websocket/IPCTransportProvider.cpp new file mode 100644 index 0000000000..a209769f97 --- /dev/null +++ b/netwerk/protocol/websocket/IPCTransportProvider.cpp @@ -0,0 +1,82 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et ft=cpp : */ +/* 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/net/IPCTransportProvider.h" + +#include "nsISocketTransport.h" +#include "nsIAsyncInputStream.h" +#include "nsIAsyncOutputStream.h" + +namespace mozilla { +namespace net { + +NS_IMPL_ISUPPORTS(TransportProviderParent, nsITransportProvider, + nsIHttpUpgradeListener) + +NS_IMETHODIMP +TransportProviderParent::SetListener(nsIHttpUpgradeListener* aListener) { + MOZ_ASSERT(aListener); + mListener = aListener; + + MaybeNotify(); + + return NS_OK; +} + +NS_IMETHODIMP +TransportProviderParent::GetIPCChild( + mozilla::net::PTransportProviderChild** aChild) { + MOZ_CRASH("Don't call this in parent process"); + *aChild = nullptr; + return NS_OK; +} + +NS_IMETHODIMP +TransportProviderParent::OnTransportAvailable( + nsISocketTransport* aTransport, nsIAsyncInputStream* aSocketIn, + nsIAsyncOutputStream* aSocketOut) { + MOZ_ASSERT(aTransport && aSocketOut && aSocketOut); + mTransport = aTransport; + mSocketIn = aSocketIn; + mSocketOut = aSocketOut; + + MaybeNotify(); + + return NS_OK; +} + +NS_IMETHODIMP +TransportProviderParent::OnUpgradeFailed(nsresult aErrorCode) { return NS_OK; } + +void TransportProviderParent::MaybeNotify() { + if (!mListener || !mTransport) { + return; + } + + DebugOnly rv = + mListener->OnTransportAvailable(mTransport, mSocketIn, mSocketOut); + MOZ_ASSERT(NS_SUCCEEDED(rv)); +} + +NS_IMPL_ISUPPORTS(TransportProviderChild, nsITransportProvider) + +TransportProviderChild::~TransportProviderChild() { Send__delete__(this); } + +NS_IMETHODIMP +TransportProviderChild::SetListener(nsIHttpUpgradeListener* aListener) { + MOZ_CRASH("Don't call this in child process"); + return NS_OK; +} + +NS_IMETHODIMP +TransportProviderChild::GetIPCChild( + mozilla::net::PTransportProviderChild** aChild) { + *aChild = this; + return NS_OK; +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/websocket/IPCTransportProvider.h b/netwerk/protocol/websocket/IPCTransportProvider.h new file mode 100644 index 0000000000..e4c8fe218a --- /dev/null +++ b/netwerk/protocol/websocket/IPCTransportProvider.h @@ -0,0 +1,89 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et ft=cpp : */ +/* 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/. */ + +#ifndef mozilla_net_IPCTransportProvider_h +#define mozilla_net_IPCTransportProvider_h + +#include "nsISupportsImpl.h" +#include "mozilla/net/PTransportProviderParent.h" +#include "mozilla/net/PTransportProviderChild.h" +#include "nsIHttpChannelInternal.h" +#include "nsITransportProvider.h" + +/* + * No, the ownership model for TransportProvider is that the child object is + * refcounted "normally". I.e. ipdl code doesn't hold a strong reference to + * TransportProviderChild. + * + * When TransportProviderChild goes away, it sends a __delete__ message to the + * parent. + * + * On the parent side, ipdl holds a strong reference to TransportProviderParent. + * When the actor is deallocatde it releases the reference to the + * TransportProviderParent. + * + * So effectively the child holds a strong reference to the parent, and are + * otherwise normally refcounted and have their lifetime determined by that + * refcount. + * + * The only other caveat is that the creation happens from the parent. + * So to create a TransportProvider, a constructor is sent from the parent to + * the child. At this time the child gets its first addref. + * + * A reference to the TransportProvider is then sent as part of some other + * message from the parent to the child. + * + * The receiver of that message can then grab the TransportProviderChild and + * without addreffing it, effectively using the refcount that the + * TransportProviderChild got on creation. + */ + +class nsISocketTransport; +class nsIAsyncInputStream; +class nsIAsyncOutputStream; + +namespace mozilla { +namespace net { + +class TransportProviderParent final : public PTransportProviderParent, + public nsITransportProvider, + public nsIHttpUpgradeListener { + public: + TransportProviderParent() = default; + + NS_DECL_ISUPPORTS + NS_DECL_NSITRANSPORTPROVIDER + NS_DECL_NSIHTTPUPGRADELISTENER + + void ActorDestroy(ActorDestroyReason aWhy) override{}; + + private: + ~TransportProviderParent() = default; + + void MaybeNotify(); + + nsCOMPtr mListener; + nsCOMPtr mTransport; + nsCOMPtr mSocketIn; + nsCOMPtr mSocketOut; +}; + +class TransportProviderChild final : public PTransportProviderChild, + public nsITransportProvider { + public: + TransportProviderChild() = default; + + NS_DECL_ISUPPORTS + NS_DECL_NSITRANSPORTPROVIDER + + private: + ~TransportProviderChild(); +}; + +} // namespace net +} // namespace mozilla + +#endif diff --git a/netwerk/protocol/websocket/PTransportProvider.ipdl b/netwerk/protocol/websocket/PTransportProvider.ipdl new file mode 100644 index 0000000000..329a381b6b --- /dev/null +++ b/netwerk/protocol/websocket/PTransportProvider.ipdl @@ -0,0 +1,27 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et ft=cpp : */ +/* 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 protocol PNecko; + +namespace mozilla { +namespace net { + +/* + * The only thing this protocol manages is used for is passing a + * PTransportProvider object from parent to child and then back to the parent + * again. Hence there's no need for any messages on the protocol itself. + */ + +async protocol PTransportProvider +{ + manager PNecko; + +parent: + async __delete__(); +}; + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/websocket/PWebSocket.ipdl b/netwerk/protocol/websocket/PWebSocket.ipdl new file mode 100644 index 0000000000..f3d0eebe82 --- /dev/null +++ b/netwerk/protocol/websocket/PWebSocket.ipdl @@ -0,0 +1,66 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et tw=80 ft=cpp : */ + +/* 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 protocol PNecko; +include protocol PBrowser; +include protocol PTransportProvider; +include IPCStream; +include NeckoChannelParams; + +include protocol PFileDescriptorSet; //FIXME: bug #792908 +include protocol PChildToParentStream; //FIXME: bug #792908 +include protocol PParentToChildStream; //FIXME: bug #792908 + +using class IPC::SerializedLoadContext from "SerializedLoadContext.h"; +using refcounted class nsIURI from "mozilla/ipc/URIUtils.h"; + +namespace mozilla { +namespace net { + +async protocol PWebSocket +{ + manager PNecko; + +parent: + // Forwarded methods corresponding to methods on nsIWebSocketChannel + async AsyncOpen(nsIURI aURI, + nsCString aOrigin, + uint64_t aInnerWindowID, + nsCString aProtocol, + bool aSecure, + // ping values only meaningful if client set them + uint32_t aPingInterval, + bool aClientSetPingInterval, + uint32_t aPingTimeout, + bool aClientSetPingTimeout, + LoadInfoArgs? aLoadInfoArgs, + PTransportProvider? aProvider, + nsCString aNegotiatedExtensions); + async Close(uint16_t code, nsCString reason); + async SendMsg(nsCString aMsg); + async SendBinaryMsg(nsCString aMsg); + async SendBinaryStream(IPCStream aStream, uint32_t aLength); + + async DeleteSelf(); + +child: + // Forwarded notifications corresponding to the nsIWebSocketListener interface + async OnStart(nsCString aProtocol, nsCString aExtensions, + nsString aEffectiveURL, bool aEncrypted, + uint64_t aHttpChannelId); + async OnStop(nsresult aStatusCode); + async OnMessageAvailable(nsDependentCSubstring aMsg, bool aMoreData); + async OnBinaryMessageAvailable(nsDependentCSubstring aMsg, bool aMoreData); + async OnAcknowledge(uint32_t aSize); + async OnServerClose(uint16_t code, nsCString aReason); + + async __delete__(); + +}; + +} //namespace net +} //namespace mozilla diff --git a/netwerk/protocol/websocket/PWebSocketEventListener.ipdl b/netwerk/protocol/websocket/PWebSocketEventListener.ipdl new file mode 100644 index 0000000000..c29b99d757 --- /dev/null +++ b/netwerk/protocol/websocket/PWebSocketEventListener.ipdl @@ -0,0 +1,52 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et tw=80 ft=cpp : */ + +/* 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 protocol PNecko; + +using class mozilla::net::WebSocketFrameData from "mozilla/net/WebSocketFrame.h"; + +namespace mozilla { +namespace net { + +async protocol PWebSocketEventListener +{ + manager PNecko; + +child: + async WebSocketCreated(uint32_t awebSocketSerialID, + nsString aURI, + nsCString aProtocols); + + async WebSocketOpened(uint32_t awebSocketSerialID, + nsString aEffectiveURI, + nsCString aProtocols, + nsCString aExtensions, + uint64_t aHttpChannelId); + + async WebSocketMessageAvailable(uint32_t awebSocketSerialID, + nsCString aData, + uint16_t aMessageType); + + async WebSocketClosed(uint32_t awebSocketSerialID, + bool aWasClean, + uint16_t aCode, + nsString aReason); + + async FrameReceived(uint32_t aWebSocketSerialID, + WebSocketFrameData aFrameData); + + async FrameSent(uint32_t aWebSocketSerialID, + WebSocketFrameData aFrameData); + + async __delete__(); + +parent: + async Close(); +}; + +} //namespace net +} //namespace mozilla diff --git a/netwerk/protocol/websocket/WebSocketChannel.cpp b/netwerk/protocol/websocket/WebSocketChannel.cpp new file mode 100644 index 0000000000..0193e4903e --- /dev/null +++ b/netwerk/protocol/websocket/WebSocketChannel.cpp @@ -0,0 +1,4023 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et 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 "WebSocketFrame.h" +#include "WebSocketLog.h" +#include "WebSocketChannel.h" + +#include "mozilla/Atomics.h" +#include "mozilla/Attributes.h" +#include "mozilla/EndianUtils.h" +#include "mozilla/MathAlgorithms.h" +#include "mozilla/Utf8.h" +#include "mozilla/net/WebSocketEventService.h" + +#include "nsIURI.h" +#include "nsIChannel.h" +#include "nsICryptoHash.h" +#include "nsIRunnable.h" +#include "nsIPrefBranch.h" +#include "nsICancelable.h" +#include "nsIClassOfService.h" +#include "nsIDNSRecord.h" +#include "nsIDNSService.h" +#include "nsIIOService.h" +#include "nsIProtocolProxyService.h" +#include "nsIProxyInfo.h" +#include "nsIProxiedChannel.h" +#include "nsIAsyncVerifyRedirectCallback.h" +#include "nsIDashboardEventNotifier.h" +#include "nsIEventTarget.h" +#include "nsIHttpChannel.h" +#include "nsIProtocolHandler.h" +#include "nsIRandomGenerator.h" +#include "nsISocketTransport.h" +#include "nsThreadUtils.h" +#include "nsINetworkLinkService.h" +#include "nsIObserverService.h" +#include "nsCharSeparatedTokenizer.h" + +#include "nsComponentManagerUtils.h" +#include "nsNetCID.h" +#include "nsServiceManagerUtils.h" +#include "nsCRT.h" +#include "nsThreadUtils.h" +#include "nsError.h" +#include "mozilla/Base64.h" +#include "nsStringStream.h" +#include "nsAlgorithm.h" +#include "nsProxyRelease.h" +#include "nsNetUtil.h" +#include "nsINode.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/StaticMutex.h" +#include "mozilla/Telemetry.h" +#include "mozilla/TimeStamp.h" +#include "nsSocketTransportService2.h" +#include "nsINSSErrorsService.h" + +#include "plbase64.h" +#include "prmem.h" +#include "prnetdb.h" +#include "zlib.h" +#include + +// rather than slurp up all of nsIWebSocket.idl, which lives outside necko, just +// dupe one constant we need from it +#define CLOSE_GOING_AWAY 1001 + +using namespace mozilla; +using namespace mozilla::net; + +namespace mozilla { +namespace net { + +NS_IMPL_ISUPPORTS(WebSocketChannel, nsIWebSocketChannel, nsIHttpUpgradeListener, + nsIRequestObserver, nsIStreamListener, nsIProtocolHandler, + nsIInputStreamCallback, nsIOutputStreamCallback, + nsITimerCallback, nsIDNSListener, nsIProtocolProxyCallback, + nsIInterfaceRequestor, nsIChannelEventSink, + nsIThreadRetargetableRequest, nsIObserver, nsINamed) + +// We implement RFC 6455, which uses Sec-WebSocket-Version: 13 on the wire. +#define SEC_WEBSOCKET_VERSION "13" + +/* + * About SSL unsigned certificates + * + * wss will not work to a host using an unsigned certificate unless there + * is already an exception (i.e. it cannot popup a dialog asking for + * a security exception). This is similar to how an inlined img will + * fail without a dialog if fails for the same reason. This should not + * be a problem in practice as it is expected the websocket javascript + * is served from the same host as the websocket server (or of course, + * a valid cert could just be provided). + * + */ + +// some helper classes + +//----------------------------------------------------------------------------- +// FailDelayManager +// +// Stores entries (searchable by {host, port}) of connections that have recently +// failed, so we can do delay of reconnects per RFC 6455 Section 7.2.3 +//----------------------------------------------------------------------------- + +// Initial reconnect delay is randomly chosen between 200-400 ms. +// This is a gentler backoff than the 0-5 seconds the spec offhandedly suggests. +const uint32_t kWSReconnectInitialBaseDelay = 200; +const uint32_t kWSReconnectInitialRandomDelay = 200; + +// Base lifetime (in ms) of a FailDelay: kept longer if more failures occur +const uint32_t kWSReconnectBaseLifeTime = 60 * 1000; +// Maximum reconnect delay (in ms) +const uint32_t kWSReconnectMaxDelay = 60 * 1000; + +// hold record of failed connections, and calculates needed delay for reconnects +// to same host/port. +class FailDelay { + public: + FailDelay(nsCString address, int32_t port) + : mAddress(std::move(address)), mPort(port) { + mLastFailure = TimeStamp::Now(); + mNextDelay = kWSReconnectInitialBaseDelay + + (rand() % kWSReconnectInitialRandomDelay); + } + + // Called to update settings when connection fails again. + void FailedAgain() { + mLastFailure = TimeStamp::Now(); + // We use a truncated exponential backoff as suggested by RFC 6455, + // but multiply by 1.5 instead of 2 to be more gradual. + mNextDelay = static_cast( + std::min(kWSReconnectMaxDelay, mNextDelay * 1.5)); + LOG( + ("WebSocket: FailedAgain: host=%s, port=%d: incremented delay to " + "%" PRIu32, + mAddress.get(), mPort, mNextDelay)); + } + + // returns 0 if there is no need to delay (i.e. delay interval is over) + uint32_t RemainingDelay(TimeStamp rightNow) { + TimeDuration dur = rightNow - mLastFailure; + uint32_t sinceFail = (uint32_t)dur.ToMilliseconds(); + if (sinceFail > mNextDelay) return 0; + + return mNextDelay - sinceFail; + } + + bool IsExpired(TimeStamp rightNow) { + return (mLastFailure + TimeDuration::FromMilliseconds( + kWSReconnectBaseLifeTime + mNextDelay)) <= + rightNow; + } + + nsCString mAddress; // IP address (or hostname if using proxy) + int32_t mPort; + + private: + TimeStamp mLastFailure; // Time of last failed attempt + // mLastFailure + mNextDelay is the soonest we'll allow a reconnect + uint32_t mNextDelay; // milliseconds +}; + +class FailDelayManager { + public: + FailDelayManager() { + MOZ_COUNT_CTOR(FailDelayManager); + + mDelaysDisabled = false; + + nsCOMPtr prefService = + do_GetService(NS_PREFSERVICE_CONTRACTID); + if (!prefService) { + return; + } + bool boolpref = true; + nsresult rv; + rv = prefService->GetBoolPref("network.websocket.delay-failed-reconnects", + &boolpref); + if (NS_SUCCEEDED(rv) && !boolpref) { + mDelaysDisabled = true; + } + } + + ~FailDelayManager() { MOZ_COUNT_DTOR(FailDelayManager); } + + void Add(nsCString& address, int32_t port) { + if (mDelaysDisabled) return; + + UniquePtr record(new FailDelay(address, port)); + mEntries.AppendElement(std::move(record)); + } + + // Element returned may not be valid after next main thread event: don't keep + // pointer to it around + FailDelay* Lookup(nsCString& address, int32_t port, + uint32_t* outIndex = nullptr) { + if (mDelaysDisabled) return nullptr; + + FailDelay* result = nullptr; + TimeStamp rightNow = TimeStamp::Now(); + + // We also remove expired entries during search: iterate from end to make + // indexing simpler + for (int32_t i = mEntries.Length() - 1; i >= 0; --i) { + FailDelay* fail = mEntries[i].get(); + if (fail->mAddress.Equals(address) && fail->mPort == port) { + if (outIndex) *outIndex = i; + result = fail; + // break here: removing more entries would mess up *outIndex. + // Any remaining expired entries will be deleted next time Lookup + // finds nothing, which is the most common case anyway. + break; + } else if (fail->IsExpired(rightNow)) { + mEntries.RemoveElementAt(i); + } + } + return result; + } + + // returns true if channel connects immediately, or false if it's delayed + void DelayOrBegin(WebSocketChannel* ws) { + if (!mDelaysDisabled) { + uint32_t failIndex = 0; + FailDelay* fail = Lookup(ws->mAddress, ws->mPort, &failIndex); + + if (fail) { + TimeStamp rightNow = TimeStamp::Now(); + + uint32_t remainingDelay = fail->RemainingDelay(rightNow); + if (remainingDelay) { + // reconnecting within delay interval: delay by remaining time + nsresult rv; + rv = NS_NewTimerWithCallback(getter_AddRefs(ws->mReconnectDelayTimer), + ws, remainingDelay, + nsITimer::TYPE_ONE_SHOT); + if (NS_SUCCEEDED(rv)) { + LOG( + ("WebSocket: delaying websocket [this=%p] by %lu ms, changing" + " state to CONNECTING_DELAYED", + ws, (unsigned long)remainingDelay)); + ws->mConnecting = CONNECTING_DELAYED; + return; + } + // if timer fails (which is very unlikely), drop down to BeginOpen + // call + } else if (fail->IsExpired(rightNow)) { + mEntries.RemoveElementAt(failIndex); + } + } + } + + // Delays disabled, or no previous failure, or we're reconnecting after + // scheduled delay interval has passed: connect. + ws->BeginOpen(true); + } + + // Remove() also deletes all expired entries as it iterates: better for + // battery life than using a periodic timer. + void Remove(nsCString& address, int32_t port) { + TimeStamp rightNow = TimeStamp::Now(); + + // iterate from end, to make deletion indexing easier + for (int32_t i = mEntries.Length() - 1; i >= 0; --i) { + FailDelay* entry = mEntries[i].get(); + if ((entry->mAddress.Equals(address) && entry->mPort == port) || + entry->IsExpired(rightNow)) { + mEntries.RemoveElementAt(i); + } + } + } + + private: + nsTArray> mEntries; + bool mDelaysDisabled; +}; + +//----------------------------------------------------------------------------- +// nsWSAdmissionManager +// +// 1) Ensures that only one websocket at a time is CONNECTING to a given IP +// address (or hostname, if using proxy), per RFC 6455 Section 4.1. +// 2) Delays reconnects to IP/host after connection failure, per Section 7.2.3 +//----------------------------------------------------------------------------- + +class nsWSAdmissionManager { + public: + static void Init() { + StaticMutexAutoLock lock(sLock); + if (!sManager) { + sManager = new nsWSAdmissionManager(); + } + } + + static void Shutdown() { + StaticMutexAutoLock lock(sLock); + delete sManager; + sManager = nullptr; + } + + // Determine if we will open connection immediately (returns true), or + // delay/queue the connection (returns false) + static void ConditionallyConnect(WebSocketChannel* ws) { + LOG(("Websocket: ConditionallyConnect: [this=%p]", ws)); + MOZ_ASSERT(NS_IsMainThread(), "not main thread"); + MOZ_ASSERT(ws->mConnecting == NOT_CONNECTING, "opening state"); + + StaticMutexAutoLock lock(sLock); + if (!sManager) { + return; + } + + // If there is already another WS channel connecting to this IP address, + // defer BeginOpen and mark as waiting in queue. + bool found = (sManager->IndexOf(ws->mAddress) >= 0); + + // Always add ourselves to queue, even if we'll connect immediately + UniquePtr newdata(new nsOpenConn(ws->mAddress, ws)); + sManager->mQueue.AppendElement(std::move(newdata)); + + if (found) { + LOG( + ("Websocket: some other channel is connecting, changing state to " + "CONNECTING_QUEUED")); + ws->mConnecting = CONNECTING_QUEUED; + } else { + sManager->mFailures.DelayOrBegin(ws); + } + } + + static void OnConnected(WebSocketChannel* aChannel) { + LOG(("Websocket: OnConnected: [this=%p]", aChannel)); + + MOZ_ASSERT(NS_IsMainThread(), "not main thread"); + MOZ_ASSERT(aChannel->mConnecting == CONNECTING_IN_PROGRESS, + "Channel completed connect, but not connecting?"); + + StaticMutexAutoLock lock(sLock); + if (!sManager) { + return; + } + + LOG(("Websocket: changing state to NOT_CONNECTING")); + aChannel->mConnecting = NOT_CONNECTING; + + // Remove from queue + sManager->RemoveFromQueue(aChannel); + + // Connection succeeded, so stop keeping track of any previous failures + sManager->mFailures.Remove(aChannel->mAddress, aChannel->mPort); + + // Check for queued connections to same host. + // Note: still need to check for failures, since next websocket with same + // host may have different port + sManager->ConnectNext(aChannel->mAddress); + } + + // Called every time a websocket channel ends its session (including going + // away w/o ever successfully creating a connection) + static void OnStopSession(WebSocketChannel* aChannel, nsresult aReason) { + LOG(("Websocket: OnStopSession: [this=%p, reason=0x%08" PRIx32 "]", + aChannel, static_cast(aReason))); + + StaticMutexAutoLock lock(sLock); + if (!sManager) { + return; + } + + if (NS_FAILED(aReason)) { + // Have we seen this failure before? + FailDelay* knownFailure = + sManager->mFailures.Lookup(aChannel->mAddress, aChannel->mPort); + if (knownFailure) { + if (aReason == NS_ERROR_NOT_CONNECTED) { + // Don't count close() before connection as a network error + LOG( + ("Websocket close() before connection to %s, %d completed" + " [this=%p]", + aChannel->mAddress.get(), (int)aChannel->mPort, aChannel)); + } else { + // repeated failure to connect: increase delay for next connection + knownFailure->FailedAgain(); + } + } else { + // new connection failure: record it. + LOG(("WebSocket: connection to %s, %d failed: [this=%p]", + aChannel->mAddress.get(), (int)aChannel->mPort, aChannel)); + sManager->mFailures.Add(aChannel->mAddress, aChannel->mPort); + } + } + + if (aChannel->mConnecting) { + MOZ_ASSERT(NS_IsMainThread(), "not main thread"); + + // Only way a connecting channel may get here w/o failing is if it was + // closed with GOING_AWAY (1001) because of navigation, tab close, etc. + MOZ_ASSERT( + NS_FAILED(aReason) || aChannel->mScriptCloseCode == CLOSE_GOING_AWAY, + "websocket closed while connecting w/o failing?"); + + sManager->RemoveFromQueue(aChannel); + + bool wasNotQueued = (aChannel->mConnecting != CONNECTING_QUEUED); + LOG(("Websocket: changing state to NOT_CONNECTING")); + aChannel->mConnecting = NOT_CONNECTING; + if (wasNotQueued) { + sManager->ConnectNext(aChannel->mAddress); + } + } + } + + static void IncrementSessionCount() { + StaticMutexAutoLock lock(sLock); + if (!sManager) { + return; + } + sManager->mSessionCount++; + } + + static void DecrementSessionCount() { + StaticMutexAutoLock lock(sLock); + if (!sManager) { + return; + } + sManager->mSessionCount--; + } + + static void GetSessionCount(int32_t& aSessionCount) { + StaticMutexAutoLock lock(sLock); + if (!sManager) { + return; + } + aSessionCount = sManager->mSessionCount; + } + + private: + nsWSAdmissionManager() : mSessionCount(0) { + MOZ_COUNT_CTOR(nsWSAdmissionManager); + } + + ~nsWSAdmissionManager() { MOZ_COUNT_DTOR(nsWSAdmissionManager); } + + class nsOpenConn { + public: + nsOpenConn(nsCString& addr, WebSocketChannel* channel) + : mAddress(addr), mChannel(channel) { + MOZ_COUNT_CTOR(nsOpenConn); + } + MOZ_COUNTED_DTOR(nsOpenConn) + + nsCString mAddress; + WebSocketChannel* mChannel; + }; + + void ConnectNext(nsCString& hostName) { + MOZ_ASSERT(NS_IsMainThread(), "not main thread"); + + int32_t index = IndexOf(hostName); + if (index >= 0) { + WebSocketChannel* chan = mQueue[index]->mChannel; + + MOZ_ASSERT(chan->mConnecting == CONNECTING_QUEUED, + "transaction not queued but in queue"); + LOG(("WebSocket: ConnectNext: found channel [this=%p] in queue", chan)); + + mFailures.DelayOrBegin(chan); + } + } + + void RemoveFromQueue(WebSocketChannel* aChannel) { + LOG(("Websocket: RemoveFromQueue: [this=%p]", aChannel)); + int32_t index = IndexOf(aChannel); + MOZ_ASSERT(index >= 0, "connection to remove not in queue"); + if (index >= 0) { + mQueue.RemoveElementAt(index); + } + } + + int32_t IndexOf(nsCString& aStr) { + for (uint32_t i = 0; i < mQueue.Length(); i++) + if (aStr == (mQueue[i])->mAddress) return i; + return -1; + } + + int32_t IndexOf(WebSocketChannel* aChannel) { + for (uint32_t i = 0; i < mQueue.Length(); i++) + if (aChannel == (mQueue[i])->mChannel) return i; + return -1; + } + + // SessionCount might be decremented from the main or the socket + // thread, so manage it with atomic counters + Atomic mSessionCount; + + // Queue for websockets that have not completed connecting yet. + // The first nsOpenConn with a given address will be either be + // CONNECTING_IN_PROGRESS or CONNECTING_DELAYED. Later ones with the same + // hostname must be CONNECTING_QUEUED. + // + // We could hash hostnames instead of using a single big vector here, but the + // dataset is expected to be small. + nsTArray> mQueue; + + FailDelayManager mFailures; + + static nsWSAdmissionManager* sManager; + static StaticMutex sLock; +}; + +nsWSAdmissionManager* nsWSAdmissionManager::sManager; +StaticMutex nsWSAdmissionManager::sLock; + +//----------------------------------------------------------------------------- +// CallOnMessageAvailable +//----------------------------------------------------------------------------- + +class CallOnMessageAvailable final : public Runnable { + public: + CallOnMessageAvailable(WebSocketChannel* aChannel, nsACString& aData, + int32_t aLen) + : Runnable("net::CallOnMessageAvailable"), + mChannel(aChannel), + mListenerMT(aChannel->mListenerMT), + mData(aData), + mLen(aLen) {} + + NS_IMETHOD Run() override { + MOZ_ASSERT(mChannel->IsOnTargetThread()); + + if (mListenerMT) { + nsresult rv; + if (mLen < 0) { + rv = mListenerMT->mListener->OnMessageAvailable(mListenerMT->mContext, + mData); + } else { + rv = mListenerMT->mListener->OnBinaryMessageAvailable( + mListenerMT->mContext, mData); + } + if (NS_FAILED(rv)) { + LOG( + ("OnMessageAvailable or OnBinaryMessageAvailable " + "failed with 0x%08" PRIx32, + static_cast(rv))); + } + } + + return NS_OK; + } + + private: + ~CallOnMessageAvailable() = default; + + RefPtr mChannel; + RefPtr mListenerMT; + nsCString mData; + int32_t mLen; +}; + +//----------------------------------------------------------------------------- +// CallOnStop +//----------------------------------------------------------------------------- + +class CallOnStop final : public Runnable { + public: + CallOnStop(WebSocketChannel* aChannel, nsresult aReason) + : Runnable("net::CallOnStop"), + mChannel(aChannel), + mListenerMT(mChannel->mListenerMT), + mReason(aReason) {} + + NS_IMETHOD Run() override { + MOZ_ASSERT(mChannel->IsOnTargetThread()); + + if (mListenerMT) { + nsresult rv = + mListenerMT->mListener->OnStop(mListenerMT->mContext, mReason); + if (NS_FAILED(rv)) { + LOG( + ("WebSocketChannel::CallOnStop " + "OnStop failed (%08" PRIx32 ")\n", + static_cast(rv))); + } + mChannel->mListenerMT = nullptr; + } + + return NS_OK; + } + + private: + ~CallOnStop() = default; + + RefPtr mChannel; + RefPtr mListenerMT; + nsresult mReason; +}; + +//----------------------------------------------------------------------------- +// CallOnServerClose +//----------------------------------------------------------------------------- + +class CallOnServerClose final : public Runnable { + public: + CallOnServerClose(WebSocketChannel* aChannel, uint16_t aCode, + nsACString& aReason) + : Runnable("net::CallOnServerClose"), + mChannel(aChannel), + mListenerMT(mChannel->mListenerMT), + mCode(aCode), + mReason(aReason) {} + + NS_IMETHOD Run() override { + MOZ_ASSERT(mChannel->IsOnTargetThread()); + + if (mListenerMT) { + nsresult rv = mListenerMT->mListener->OnServerClose(mListenerMT->mContext, + mCode, mReason); + if (NS_FAILED(rv)) { + LOG( + ("WebSocketChannel::CallOnServerClose " + "OnServerClose failed (%08" PRIx32 ")\n", + static_cast(rv))); + } + } + return NS_OK; + } + + private: + ~CallOnServerClose() = default; + + RefPtr mChannel; + RefPtr mListenerMT; + uint16_t mCode; + nsCString mReason; +}; + +//----------------------------------------------------------------------------- +// CallAcknowledge +//----------------------------------------------------------------------------- + +class CallAcknowledge final : public Runnable { + public: + CallAcknowledge(WebSocketChannel* aChannel, uint32_t aSize) + : Runnable("net::CallAcknowledge"), + mChannel(aChannel), + mListenerMT(mChannel->mListenerMT), + mSize(aSize) {} + + NS_IMETHOD Run() override { + MOZ_ASSERT(mChannel->IsOnTargetThread()); + + LOG(("WebSocketChannel::CallAcknowledge: Size %u\n", mSize)); + if (mListenerMT) { + nsresult rv = + mListenerMT->mListener->OnAcknowledge(mListenerMT->mContext, mSize); + if (NS_FAILED(rv)) { + LOG(("WebSocketChannel::CallAcknowledge: Acknowledge failed (%08" PRIx32 + ")\n", + static_cast(rv))); + } + } + return NS_OK; + } + + private: + ~CallAcknowledge() = default; + + RefPtr mChannel; + RefPtr mListenerMT; + uint32_t mSize; +}; + +//----------------------------------------------------------------------------- +// CallOnTransportAvailable +//----------------------------------------------------------------------------- + +class CallOnTransportAvailable final : public Runnable { + public: + CallOnTransportAvailable(WebSocketChannel* aChannel, + nsISocketTransport* aTransport, + nsIAsyncInputStream* aSocketIn, + nsIAsyncOutputStream* aSocketOut) + : Runnable("net::CallOnTransportAvailble"), + mChannel(aChannel), + mTransport(aTransport), + mSocketIn(aSocketIn), + mSocketOut(aSocketOut) {} + + NS_IMETHOD Run() override { + LOG(("WebSocketChannel::CallOnTransportAvailable %p\n", this)); + return mChannel->OnTransportAvailable(mTransport, mSocketIn, mSocketOut); + } + + private: + ~CallOnTransportAvailable() = default; + + RefPtr mChannel; + nsCOMPtr mTransport; + nsCOMPtr mSocketIn; + nsCOMPtr mSocketOut; +}; + +//----------------------------------------------------------------------------- +// PMCECompression +//----------------------------------------------------------------------------- + +class PMCECompression { + public: + PMCECompression(bool aNoContextTakeover, int32_t aLocalMaxWindowBits, + int32_t aRemoteMaxWindowBits) + : mActive(false), + mNoContextTakeover(aNoContextTakeover), + mResetDeflater(false), + mMessageDeflated(false) { + this->mDeflater.next_in = nullptr; + this->mDeflater.avail_in = 0; + this->mDeflater.total_in = 0; + this->mDeflater.next_out = nullptr; + this->mDeflater.avail_out = 0; + this->mDeflater.total_out = 0; + this->mDeflater.msg = nullptr; + this->mDeflater.state = nullptr; + this->mDeflater.data_type = 0; + this->mDeflater.adler = 0; + this->mDeflater.reserved = 0; + this->mInflater.next_in = nullptr; + this->mInflater.avail_in = 0; + this->mInflater.total_in = 0; + this->mInflater.next_out = nullptr; + this->mInflater.avail_out = 0; + this->mInflater.total_out = 0; + this->mInflater.msg = nullptr; + this->mInflater.state = nullptr; + this->mInflater.data_type = 0; + this->mInflater.adler = 0; + this->mInflater.reserved = 0; + MOZ_COUNT_CTOR(PMCECompression); + + mDeflater.zalloc = mInflater.zalloc = Z_NULL; + mDeflater.zfree = mInflater.zfree = Z_NULL; + mDeflater.opaque = mInflater.opaque = Z_NULL; + + if (deflateInit2(&mDeflater, Z_DEFAULT_COMPRESSION, Z_DEFLATED, + -aLocalMaxWindowBits, 8, Z_DEFAULT_STRATEGY) == Z_OK) { + if (inflateInit2(&mInflater, -aRemoteMaxWindowBits) == Z_OK) { + mActive = true; + } else { + deflateEnd(&mDeflater); + } + } + } + + ~PMCECompression() { + MOZ_COUNT_DTOR(PMCECompression); + + if (mActive) { + inflateEnd(&mInflater); + deflateEnd(&mDeflater); + } + } + + bool Active() { return mActive; } + + void SetMessageDeflated() { + MOZ_ASSERT(!mMessageDeflated); + mMessageDeflated = true; + } + bool IsMessageDeflated() { return mMessageDeflated; } + + bool UsingContextTakeover() { return !mNoContextTakeover; } + + nsresult Deflate(uint8_t* data, uint32_t dataLen, nsACString& _retval) { + if (mResetDeflater || mNoContextTakeover) { + if (deflateReset(&mDeflater) != Z_OK) { + return NS_ERROR_UNEXPECTED; + } + mResetDeflater = false; + } + + mDeflater.avail_out = kBufferLen; + mDeflater.next_out = mBuffer; + mDeflater.avail_in = dataLen; + mDeflater.next_in = data; + + while (true) { + int zerr = deflate(&mDeflater, Z_SYNC_FLUSH); + + if (zerr != Z_OK) { + mResetDeflater = true; + return NS_ERROR_UNEXPECTED; + } + + uint32_t deflated = kBufferLen - mDeflater.avail_out; + if (deflated > 0) { + _retval.Append(reinterpret_cast(mBuffer), deflated); + } + + mDeflater.avail_out = kBufferLen; + mDeflater.next_out = mBuffer; + + if (mDeflater.avail_in > 0) { + continue; // There is still some data to deflate + } + + if (deflated == kBufferLen) { + continue; // There was not enough space in the buffer + } + + break; + } + + if (_retval.Length() < 4) { + MOZ_ASSERT(false, "Expected trailing not found in deflated data!"); + mResetDeflater = true; + return NS_ERROR_UNEXPECTED; + } + + _retval.Truncate(_retval.Length() - 4); + + return NS_OK; + } + + nsresult Inflate(uint8_t* data, uint32_t dataLen, nsACString& _retval) { + mMessageDeflated = false; + + Bytef trailingData[] = {0x00, 0x00, 0xFF, 0xFF}; + bool trailingDataUsed = false; + + mInflater.avail_out = kBufferLen; + mInflater.next_out = mBuffer; + mInflater.avail_in = dataLen; + mInflater.next_in = data; + + while (true) { + int zerr = inflate(&mInflater, Z_NO_FLUSH); + + if (zerr == Z_STREAM_END) { + Bytef* saveNextIn = mInflater.next_in; + uint32_t saveAvailIn = mInflater.avail_in; + Bytef* saveNextOut = mInflater.next_out; + uint32_t saveAvailOut = mInflater.avail_out; + + inflateReset(&mInflater); + + mInflater.next_in = saveNextIn; + mInflater.avail_in = saveAvailIn; + mInflater.next_out = saveNextOut; + mInflater.avail_out = saveAvailOut; + } else if (zerr != Z_OK && zerr != Z_BUF_ERROR) { + return NS_ERROR_INVALID_CONTENT_ENCODING; + } + + uint32_t inflated = kBufferLen - mInflater.avail_out; + if (inflated > 0) { + if (!_retval.Append(reinterpret_cast(mBuffer), inflated, + fallible)) { + return NS_ERROR_OUT_OF_MEMORY; + } + } + + mInflater.avail_out = kBufferLen; + mInflater.next_out = mBuffer; + + if (mInflater.avail_in > 0) { + continue; // There is still some data to inflate + } + + if (inflated == kBufferLen) { + continue; // There was not enough space in the buffer + } + + if (!trailingDataUsed) { + trailingDataUsed = true; + mInflater.avail_in = sizeof(trailingData); + mInflater.next_in = trailingData; + continue; + } + + return NS_OK; + } + } + + private: + bool mActive; + bool mNoContextTakeover; + bool mResetDeflater; + bool mMessageDeflated; + z_stream mDeflater; + z_stream mInflater; + const static uint32_t kBufferLen = 4096; + uint8_t mBuffer[kBufferLen]; +}; + +//----------------------------------------------------------------------------- +// OutboundMessage +//----------------------------------------------------------------------------- + +enum WsMsgType { + kMsgTypeString = 0, + kMsgTypeBinaryString, + kMsgTypeStream, + kMsgTypePing, + kMsgTypePong, + kMsgTypeFin +}; + +static const char* msgNames[] = {"text", "binaryString", "binaryStream", + "ping", "pong", "close"}; + +class OutboundMessage { + public: + OutboundMessage(WsMsgType type, const nsACString& str) + : mMsg(mozilla::AsVariant(pString(str))), + mMsgType(type), + mDeflated(false) { + MOZ_COUNT_CTOR(OutboundMessage); + } + + OutboundMessage(nsIInputStream* stream, uint32_t length) + : mMsg(mozilla::AsVariant(StreamWithLength(stream, length))), + mMsgType(kMsgTypeStream), + mDeflated(false) { + MOZ_COUNT_CTOR(OutboundMessage); + } + + ~OutboundMessage() { + MOZ_COUNT_DTOR(OutboundMessage); + switch (mMsgType) { + case kMsgTypeString: + case kMsgTypeBinaryString: + case kMsgTypePing: + case kMsgTypePong: + break; + case kMsgTypeStream: + // for now this only gets hit if msg deleted w/o being sent + if (mMsg.as().mStream) { + mMsg.as().mStream->Close(); + } + break; + case kMsgTypeFin: + break; // do-nothing: avoid compiler warning + } + } + + WsMsgType GetMsgType() const { return mMsgType; } + int32_t Length() { + if (mMsg.is()) { + return mMsg.as().mValue.Length(); + } + + return mMsg.as().mLength; + } + int32_t OrigLength() { + if (mMsg.is()) { + pString& ref = mMsg.as(); + return mDeflated ? ref.mOrigValue.Length() : ref.mValue.Length(); + } + + return mMsg.as().mLength; + } + + uint8_t* BeginWriting() { + MOZ_ASSERT(mMsgType != kMsgTypeStream, + "Stream should have been converted to string by now"); + if (!mMsg.as().mValue.IsVoid()) { + return (uint8_t*)mMsg.as().mValue.BeginWriting(); + } + return nullptr; + } + + uint8_t* BeginReading() { + MOZ_ASSERT(mMsgType != kMsgTypeStream, + "Stream should have been converted to string by now"); + if (!mMsg.as().mValue.IsVoid()) { + return (uint8_t*)mMsg.as().mValue.BeginReading(); + } + return nullptr; + } + + uint8_t* BeginOrigReading() { + MOZ_ASSERT(mMsgType != kMsgTypeStream, + "Stream should have been converted to string by now"); + if (!mDeflated) return BeginReading(); + if (!mMsg.as().mOrigValue.IsVoid()) { + return (uint8_t*)mMsg.as().mOrigValue.BeginReading(); + } + return nullptr; + } + + nsresult ConvertStreamToString() { + MOZ_ASSERT(mMsgType == kMsgTypeStream, "Not a stream!"); + nsAutoCString temp; + { + StreamWithLength& ref = mMsg.as(); + nsresult rv = NS_ReadInputStreamToString(ref.mStream, temp, ref.mLength); + + NS_ENSURE_SUCCESS(rv, rv); + if (temp.Length() != ref.mLength) { + return NS_ERROR_UNEXPECTED; + } + ref.mStream->Close(); + } + + mMsg = mozilla::AsVariant(pString(temp)); + mMsgType = kMsgTypeBinaryString; + + return NS_OK; + } + + bool DeflatePayload(PMCECompression* aCompressor) { + MOZ_ASSERT(mMsgType != kMsgTypeStream, + "Stream should have been converted to string by now"); + MOZ_ASSERT(!mDeflated); + + nsresult rv; + pString& ref = mMsg.as(); + if (ref.mValue.Length() == 0) { + // Empty message + return false; + } + + nsAutoCString temp; + rv = aCompressor->Deflate(BeginReading(), ref.mValue.Length(), temp); + if (NS_FAILED(rv)) { + LOG( + ("WebSocketChannel::OutboundMessage: Deflating payload failed " + "[rv=0x%08" PRIx32 "]\n", + static_cast(rv))); + return false; + } + + if (!aCompressor->UsingContextTakeover() && + temp.Length() > ref.mValue.Length()) { + // When "_no_context_takeover" was negotiated, do not send deflated + // payload if it's larger that the original one. OTOH, it makes sense + // to send the larger deflated payload when the sliding window is not + // reset between messages because if we would skip some deflated block + // we would need to empty the sliding window which could affect the + // compression of the subsequent messages. + LOG( + ("WebSocketChannel::OutboundMessage: Not deflating message since the " + "deflated payload is larger than the original one [deflated=%d, " + "original=%d]", + temp.Length(), ref.mValue.Length())); + return false; + } + + mDeflated = true; + mMsg.as().mOrigValue = mMsg.as().mValue; + mMsg.as().mValue = temp; + return true; + } + + private: + struct pString { + nsCString mValue; + nsCString mOrigValue; + explicit pString(const nsACString& value) + : mValue(value), mOrigValue(VoidCString()) {} + }; + struct StreamWithLength { + nsCOMPtr mStream; + uint32_t mLength; + explicit StreamWithLength(nsIInputStream* stream, uint32_t Length) + : mStream(stream), mLength(Length) {} + }; + mozilla::Variant mMsg; + WsMsgType mMsgType; + bool mDeflated; +}; + +//----------------------------------------------------------------------------- +// OutboundEnqueuer +//----------------------------------------------------------------------------- + +class OutboundEnqueuer final : public Runnable { + public: + OutboundEnqueuer(WebSocketChannel* aChannel, OutboundMessage* aMsg) + : Runnable("OutboundEnquerer"), mChannel(aChannel), mMessage(aMsg) {} + + NS_IMETHOD Run() override { + mChannel->EnqueueOutgoingMessage(mChannel->mOutgoingMessages, mMessage); + return NS_OK; + } + + private: + ~OutboundEnqueuer() = default; + + RefPtr mChannel; + OutboundMessage* mMessage; +}; + +//----------------------------------------------------------------------------- +// WebSocketChannel +//----------------------------------------------------------------------------- + +WebSocketChannel::WebSocketChannel() + : mPort(0), + mCloseTimeout(20000), + mOpenTimeout(20000), + mConnecting(NOT_CONNECTING), + mMaxConcurrentConnections(200), + mInnerWindowID(0), + mGotUpgradeOK(0), + mRecvdHttpUpgradeTransport(0), + mAutoFollowRedirects(0), + mAllowPMCE(1), + mPingOutstanding(0), + mReleaseOnTransmit(0), + mDataStarted(false), + mRequestedClose(false), + mClientClosed(false), + mServerClosed(false), + mStopped(false), + mCalledOnStop(false), + mTCPClosed(false), + mOpenedHttpChannel(false), + mIncrementedSessionCount(false), + mDecrementedSessionCount(false), + mMaxMessageSize(INT32_MAX), + mStopOnClose(NS_OK), + mServerCloseCode(CLOSE_ABNORMAL), + mScriptCloseCode(0), + mFragmentOpcode(nsIWebSocketFrame::OPCODE_CONTINUATION), + mFragmentAccumulator(0), + mBuffered(0), + mBufferSize(kIncomingBufferInitialSize), + mCurrentOut(nullptr), + mCurrentOutSent(0), + mHdrOutToSend(0), + mHdrOut(nullptr), + mDynamicOutputSize(0), + mDynamicOutput(nullptr), + mPrivateBrowsing(false), + mConnectionLogService(nullptr), + mMutex("WebSocketChannel::mMutex") { + MOZ_ASSERT(NS_IsMainThread(), "not main thread"); + + LOG(("WebSocketChannel::WebSocketChannel() %p\n", this)); + + nsWSAdmissionManager::Init(); + + mFramePtr = mBuffer = static_cast(moz_xmalloc(mBufferSize)); + + nsresult rv; + mConnectionLogService = + do_GetService("@mozilla.org/network/dashboard;1", &rv); + if (NS_FAILED(rv)) LOG(("Failed to initiate dashboard service.")); + + mService = WebSocketEventService::GetOrCreate(); +} + +WebSocketChannel::~WebSocketChannel() { + LOG(("WebSocketChannel::~WebSocketChannel() %p\n", this)); + + if (mWasOpened) { + MOZ_ASSERT(mCalledOnStop, "WebSocket was opened but OnStop was not called"); + MOZ_ASSERT(mStopped, "WebSocket was opened but never stopped"); + } + MOZ_ASSERT(!mCancelable, "DNS/Proxy Request still alive at destruction"); + MOZ_ASSERT(!mConnecting, "Should not be connecting in destructor"); + + free(mBuffer); + free(mDynamicOutput); + delete mCurrentOut; + + while ((mCurrentOut = mOutgoingPingMessages.PopFront())) { + delete mCurrentOut; + } + while ((mCurrentOut = mOutgoingPongMessages.PopFront())) { + delete mCurrentOut; + } + while ((mCurrentOut = mOutgoingMessages.PopFront())) { + delete mCurrentOut; + } + + mListenerMT = nullptr; + + NS_ReleaseOnMainThread("WebSocketChannel::mLoadGroup", mLoadGroup.forget()); + NS_ReleaseOnMainThread("WebSocketChannel::mLoadInfo", mLoadInfo.forget()); + NS_ReleaseOnMainThread("WebSocketChannel::mService", mService.forget()); +} + +NS_IMETHODIMP +WebSocketChannel::Observe(nsISupports* subject, const char* topic, + const char16_t* data) { + LOG(("WebSocketChannel::Observe [topic=\"%s\"]\n", topic)); + + if (strcmp(topic, NS_NETWORK_LINK_TOPIC) == 0) { + nsCString converted = NS_ConvertUTF16toUTF8(data); + const char* state = converted.get(); + + if (strcmp(state, NS_NETWORK_LINK_DATA_CHANGED) == 0) { + LOG(("WebSocket: received network CHANGED event")); + + if (!mSocketThread) { + // there has not been an asyncopen yet on the object and then we need + // no ping. + LOG(("WebSocket: early object, no ping needed")); + } else { + // Next we check mDataStarted, which we need to do on mTargetThread. + if (!IsOnTargetThread()) { + mTargetThread->Dispatch( + NewRunnableMethod("net::WebSocketChannel::OnNetworkChanged", this, + &WebSocketChannel::OnNetworkChanged), + NS_DISPATCH_NORMAL); + } else { + nsresult rv = OnNetworkChanged(); + if (NS_FAILED(rv)) { + LOG(("WebSocket: OnNetworkChanged failed (%08" PRIx32 ")", + static_cast(rv))); + } + } + } + } + } + + return NS_OK; +} + +nsresult WebSocketChannel::OnNetworkChanged() { + if (IsOnTargetThread()) { + LOG(("WebSocketChannel::OnNetworkChanged() - on target thread %p", this)); + + if (!mDataStarted) { + LOG(("WebSocket: data not started yet, no ping needed")); + return NS_OK; + } + + return mSocketThread->Dispatch( + NewRunnableMethod("net::WebSocketChannel::OnNetworkChanged", this, + &WebSocketChannel::OnNetworkChanged), + NS_DISPATCH_NORMAL); + } + + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + LOG(("WebSocketChannel::OnNetworkChanged() - on socket thread %p", this)); + + if (mPingOutstanding) { + // If there's an outstanding ping that's expected to get a pong back + // we let that do its thing. + LOG(("WebSocket: pong already pending")); + return NS_OK; + } + + if (mPingForced) { + // avoid more than one + LOG(("WebSocket: forced ping timer already fired")); + return NS_OK; + } + + LOG(("nsWebSocketChannel:: Generating Ping as network changed\n")); + + if (!mPingTimer) { + // The ping timer is only conditionally running already. If it wasn't + // already created do it here. + mPingTimer = NS_NewTimer(); + if (!mPingTimer) { + LOG(("WebSocket: unable to create ping timer!")); + NS_WARNING("unable to create ping timer!"); + return NS_ERROR_OUT_OF_MEMORY; + } + } + // Trigger the ping timeout asap to fire off a new ping. Wait just + // a little bit to better avoid multi-triggers. + mPingForced = true; + mPingTimer->InitWithCallback(this, 200, nsITimer::TYPE_ONE_SHOT); + + return NS_OK; +} + +void WebSocketChannel::Shutdown() { nsWSAdmissionManager::Shutdown(); } + +bool WebSocketChannel::IsOnTargetThread() { + MOZ_ASSERT(mTargetThread); + bool isOnTargetThread = false; + nsresult rv = mTargetThread->IsOnCurrentThread(&isOnTargetThread); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + return NS_FAILED(rv) ? false : isOnTargetThread; +} + +void WebSocketChannel::GetEffectiveURL(nsAString& aEffectiveURL) const { + aEffectiveURL = mEffectiveURL; +} + +bool WebSocketChannel::IsEncrypted() const { return mEncrypted; } + +void WebSocketChannel::BeginOpen(bool aCalledFromAdmissionManager) { + MOZ_ASSERT(NS_IsMainThread(), "not main thread"); + + LOG(("WebSocketChannel::BeginOpen() %p\n", this)); + + // Important that we set CONNECTING_IN_PROGRESS before any call to + // AbortSession here: ensures that any remaining queued connection(s) are + // scheduled in OnStopSession + LOG(("Websocket: changing state to CONNECTING_IN_PROGRESS")); + mConnecting = CONNECTING_IN_PROGRESS; + + if (aCalledFromAdmissionManager) { + // When called from nsWSAdmissionManager post an event to avoid potential + // re-entering of nsWSAdmissionManager and its lock. + NS_DispatchToMainThread( + NewRunnableMethod("net::WebSocketChannel::BeginOpenInternal", this, + &WebSocketChannel::BeginOpenInternal), + NS_DISPATCH_NORMAL); + } else { + BeginOpenInternal(); + } +} + +void WebSocketChannel::BeginOpenInternal() { + LOG(("WebSocketChannel::BeginOpenInternal() %p\n", this)); + + nsresult rv; + + if (mRedirectCallback) { + LOG(("WebSocketChannel::BeginOpenInternal: Resuming Redirect\n")); + rv = mRedirectCallback->OnRedirectVerifyCallback(NS_OK); + mRedirectCallback = nullptr; + return; + } + + nsCOMPtr localChannel = do_QueryInterface(mChannel, &rv); + if (NS_FAILED(rv)) { + LOG(("WebSocketChannel::BeginOpenInternal: cannot async open\n")); + AbortSession(NS_ERROR_UNEXPECTED); + return; + } + + rv = NS_MaybeOpenChannelUsingAsyncOpen(localChannel, this); + + if (NS_FAILED(rv)) { + LOG(("WebSocketChannel::BeginOpenInternal: cannot async open\n")); + AbortSession(NS_ERROR_CONNECTION_REFUSED); + return; + } + mOpenedHttpChannel = true; + + rv = NS_NewTimerWithCallback(getter_AddRefs(mOpenTimer), this, mOpenTimeout, + nsITimer::TYPE_ONE_SHOT); + if (NS_FAILED(rv)) { + LOG( + ("WebSocketChannel::BeginOpenInternal: cannot initialize open " + "timer\n")); + AbortSession(NS_ERROR_UNEXPECTED); + return; + } +} + +bool WebSocketChannel::IsPersistentFramePtr() { + return (mFramePtr >= mBuffer && mFramePtr < mBuffer + mBufferSize); +} + +// Extends the internal buffer by count and returns the total +// amount of data available for read +// +// Accumulated fragment size is passed in instead of using the member +// variable beacuse when transitioning from the stack to the persistent +// read buffer we want to explicitly include them in the buffer instead +// of as already existing data. +bool WebSocketChannel::UpdateReadBuffer(uint8_t* buffer, uint32_t count, + uint32_t accumulatedFragments, + uint32_t* available) { + LOG(("WebSocketChannel::UpdateReadBuffer() %p [%p %u]\n", this, buffer, + count)); + + if (!mBuffered) mFramePtr = mBuffer; + + MOZ_ASSERT(IsPersistentFramePtr(), "update read buffer bad mFramePtr"); + MOZ_ASSERT(mFramePtr - accumulatedFragments >= mBuffer, + "reserved FramePtr bad"); + + if (mBuffered + count <= mBufferSize) { + // append to existing buffer + LOG(("WebSocketChannel: update read buffer absorbed %u\n", count)); + } else if (mBuffered + count - (mFramePtr - accumulatedFragments - mBuffer) <= + mBufferSize) { + // make room in existing buffer by shifting unused data to start + mBuffered -= (mFramePtr - mBuffer - accumulatedFragments); + LOG(("WebSocketChannel: update read buffer shifted %u\n", mBuffered)); + ::memmove(mBuffer, mFramePtr - accumulatedFragments, mBuffered); + mFramePtr = mBuffer + accumulatedFragments; + } else { + // existing buffer is not sufficient, extend it + mBufferSize += count + 8192 + mBufferSize / 3; + LOG(("WebSocketChannel: update read buffer extended to %u\n", mBufferSize)); + uint8_t* old = mBuffer; + mBuffer = (uint8_t*)realloc(mBuffer, mBufferSize); + if (!mBuffer) { + mBuffer = old; + return false; + } + mFramePtr = mBuffer + (mFramePtr - old); + } + + ::memcpy(mBuffer + mBuffered, buffer, count); + mBuffered += count; + + if (available) *available = mBuffered - (mFramePtr - mBuffer); + + return true; +} + +nsresult WebSocketChannel::ProcessInput(uint8_t* buffer, uint32_t count) { + LOG(("WebSocketChannel::ProcessInput %p [%d %d]\n", this, count, mBuffered)); + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + nsresult rv; + + // 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; + } else { + if (!UpdateReadBuffer(buffer, count, mFragmentAccumulator, &avail)) { + return NS_ERROR_FILE_TOO_BIG; + } + } + + uint8_t* payload; + uint32_t totalAvail = avail; + + while (avail >= 2) { + int64_t payloadLength64 = mFramePtr[1] & kPayloadLengthBitsMask; + uint8_t finBit = mFramePtr[0] & kFinalFragBit; + uint8_t rsvBits = mFramePtr[0] & kRsvBitsMask; + uint8_t rsvBit1 = mFramePtr[0] & kRsv1Bit; + uint8_t rsvBit2 = mFramePtr[0] & kRsv2Bit; + uint8_t rsvBit3 = mFramePtr[0] & kRsv3Bit; + uint8_t opcode = mFramePtr[0] & kOpcodeBitsMask; + uint8_t maskBit = mFramePtr[1] & kMaskBit; + uint32_t mask = 0; + + uint32_t framingLength = 2; + if (maskBit) framingLength += 4; + + if (payloadLength64 < 126) { + if (avail < framingLength) break; + } else if (payloadLength64 == 126) { + // 16 bit length field + framingLength += 2; + if (avail < framingLength) break; + + payloadLength64 = mFramePtr[2] << 8 | mFramePtr[3]; + + if (payloadLength64 < 126) { + // Section 5.2 says that the minimal number of bytes MUST + // be used to encode the length in all cases + LOG(("WebSocketChannel:: non-minimal-encoded payload length")); + return NS_ERROR_ILLEGAL_VALUE; + } + + } else { + // 64 bit length + framingLength += 8; + if (avail < framingLength) break; + + if (mFramePtr[2] & 0x80) { + // Section 4.2 says that the most significant bit MUST be + // 0. (i.e. this is really a 63 bit value) + LOG(("WebSocketChannel:: high bit of 64 bit length set")); + return NS_ERROR_ILLEGAL_VALUE; + } + + // copy this in case it is unaligned + payloadLength64 = NetworkEndian::readInt64(mFramePtr + 2); + + if (payloadLength64 <= 0xffff) { + // Section 5.2 says that the minimal number of bytes MUST + // be used to encode the length in all cases + LOG(("WebSocketChannel:: non-minimal-encoded payload length")); + return NS_ERROR_ILLEGAL_VALUE; + } + } + + payload = mFramePtr + framingLength; + avail -= framingLength; + + LOG(("WebSocketChannel::ProcessInput: payload %" PRId64 " avail %" PRIu32 + "\n", + payloadLength64, avail)); + + CheckedInt payloadLengthChecked(payloadLength64); + payloadLengthChecked += mFragmentAccumulator; + if (!payloadLengthChecked.isValid() || + payloadLengthChecked.value() > mMaxMessageSize) { + return NS_ERROR_FILE_TOO_BIG; + } + + uint32_t payloadLength = static_cast(payloadLength64); + + if (avail < payloadLength) break; + + LOG(("WebSocketChannel::ProcessInput: Frame accumulated - opcode %d\n", + opcode)); + + if (!maskBit && mIsServerSide) { + LOG( + ("WebSocketChannel::ProcessInput: unmasked frame received " + "from client\n")); + return NS_ERROR_ILLEGAL_VALUE; + } + + if (maskBit) { + if (!mIsServerSide) { + // The server should not be allowed to send masked frames to clients. + // But we've been allowing it for some time, so this should be + // deprecated with care. + LOG(("WebSocketChannel:: Client RECEIVING masked frame.")); + } + + mask = NetworkEndian::readUint32(payload - 4); + } + + if (mask) { + ApplyMask(mask, payload, payloadLength); + } else if (mIsServerSide) { + LOG( + ("WebSocketChannel::ProcessInput: masked frame with mask 0 received" + "from client\n")); + return NS_ERROR_ILLEGAL_VALUE; + } + + // Control codes are required to have the fin bit set + if (!finBit && (opcode & kControlFrameMask)) { + LOG(("WebSocketChannel:: fragmented control frame code %d\n", opcode)); + return NS_ERROR_ILLEGAL_VALUE; + } + + if (rsvBits) { + // PMCE sets RSV1 bit in the first fragment when the non-control frame + // is deflated + if (mPMCECompressor && rsvBits == kRsv1Bit && mFragmentAccumulator == 0 && + !(opcode & kControlFrameMask)) { + mPMCECompressor->SetMessageDeflated(); + LOG(("WebSocketChannel::ProcessInput: received deflated frame\n")); + } else { + LOG(("WebSocketChannel::ProcessInput: unexpected reserved bits %x\n", + rsvBits)); + return NS_ERROR_ILLEGAL_VALUE; + } + } + + if (!finBit || opcode == nsIWebSocketFrame::OPCODE_CONTINUATION) { + // This is part of a fragment response + + // Only the first frame has a non zero op code: Make sure we don't see a + // first frame while some old fragments are open + if ((mFragmentAccumulator != 0) && + (opcode != nsIWebSocketFrame::OPCODE_CONTINUATION)) { + LOG(("WebSocketChannel:: nested fragments\n")); + return NS_ERROR_ILLEGAL_VALUE; + } + + LOG(("WebSocketChannel:: Accumulating Fragment %" PRIu32 "\n", + payloadLength)); + + if (opcode == nsIWebSocketFrame::OPCODE_CONTINUATION) { + // Make sure this continuation fragment isn't the first fragment + if (mFragmentOpcode == nsIWebSocketFrame::OPCODE_CONTINUATION) { + LOG(("WebSocketHeandler:: continuation code in first fragment\n")); + return NS_ERROR_ILLEGAL_VALUE; + } + + // For frag > 1 move the data body back on top of the headers + // so we have contiguous stream of data + MOZ_ASSERT(mFramePtr + framingLength == payload, + "payload offset from frameptr wrong"); + ::memmove(mFramePtr, payload, avail); + payload = mFramePtr; + if (mBuffered) mBuffered -= framingLength; + } else { + mFragmentOpcode = opcode; + } + + if (finBit) { + LOG(("WebSocketChannel:: Finalizing Fragment\n")); + payload -= mFragmentAccumulator; + payloadLength += mFragmentAccumulator; + avail += mFragmentAccumulator; + mFragmentAccumulator = 0; + opcode = mFragmentOpcode; + // reset to detect if next message illegally starts with continuation + mFragmentOpcode = nsIWebSocketFrame::OPCODE_CONTINUATION; + } else { + opcode = nsIWebSocketFrame::OPCODE_CONTINUATION; + mFragmentAccumulator += payloadLength; + } + } else if (mFragmentAccumulator != 0 && !(opcode & kControlFrameMask)) { + // This frame is not part of a fragment sequence but we + // have an open fragment.. it must be a control code or else + // we have a problem + LOG(("WebSocketChannel:: illegal fragment sequence\n")); + return NS_ERROR_ILLEGAL_VALUE; + } + + if (mServerClosed) { + LOG(("WebSocketChannel:: ignoring read frame code %d after close\n", + opcode)); + // nop + } else if (mStopped) { + LOG(("WebSocketChannel:: ignoring read frame code %d after completion\n", + opcode)); + } else if (opcode == nsIWebSocketFrame::OPCODE_TEXT) { + bool isDeflated = mPMCECompressor && mPMCECompressor->IsMessageDeflated(); + LOG(("WebSocketChannel:: %stext frame received\n", + isDeflated ? "deflated " : "")); + + if (mListenerMT) { + nsCString utf8Data; + + if (isDeflated) { + rv = mPMCECompressor->Inflate(payload, payloadLength, utf8Data); + if (NS_FAILED(rv)) { + return rv; + } + LOG( + ("WebSocketChannel:: message successfully inflated " + "[origLength=%d, newLength=%d]\n", + payloadLength, utf8Data.Length())); + } else { + if (!utf8Data.Assign((const char*)payload, payloadLength, + mozilla::fallible)) { + return NS_ERROR_OUT_OF_MEMORY; + } + } + + // Section 8.1 says to fail connection if invalid utf-8 in text message + if (!IsUtf8(utf8Data)) { + LOG(("WebSocketChannel:: text frame invalid utf-8\n")); + return NS_ERROR_CANNOT_CONVERT_DATA; + } + + RefPtr frame = mService->CreateFrameIfNeeded( + finBit, rsvBit1, rsvBit2, rsvBit3, opcode, maskBit, mask, utf8Data); + + if (frame) { + mService->FrameReceived(mSerial, mInnerWindowID, frame.forget()); + } + + mTargetThread->Dispatch(new CallOnMessageAvailable(this, utf8Data, -1), + NS_DISPATCH_NORMAL); + if (mConnectionLogService && !mPrivateBrowsing) { + mConnectionLogService->NewMsgReceived(mHost, mSerial, count); + LOG(("Added new msg received for %s", mHost.get())); + } + } + } else if (opcode & kControlFrameMask) { + // control frames + if (payloadLength > 125) { + LOG(("WebSocketChannel:: bad control frame code %d length %d\n", opcode, + payloadLength)); + return NS_ERROR_ILLEGAL_VALUE; + } + + RefPtr frame = mService->CreateFrameIfNeeded( + finBit, rsvBit1, rsvBit2, rsvBit3, opcode, maskBit, mask, payload, + payloadLength); + + if (opcode == nsIWebSocketFrame::OPCODE_CLOSE) { + LOG(("WebSocketChannel:: close received\n")); + mServerClosed = true; + + mServerCloseCode = CLOSE_NO_STATUS; + if (payloadLength >= 2) { + mServerCloseCode = NetworkEndian::readUint16(payload); + LOG(("WebSocketChannel:: close recvd code %u\n", mServerCloseCode)); + uint16_t msglen = static_cast(payloadLength - 2); + if (msglen > 0) { + mServerCloseReason.SetLength(msglen); + memcpy(mServerCloseReason.BeginWriting(), (const char*)payload + 2, + msglen); + + // section 8.1 says to replace received non utf-8 sequences + // (which are non-conformant to send) with u+fffd, + // but secteam feels that silently rewriting messages is + // inappropriate - so we will fail the connection instead. + if (!IsUtf8(mServerCloseReason)) { + LOG(("WebSocketChannel:: close frame invalid utf-8\n")); + return NS_ERROR_CANNOT_CONVERT_DATA; + } + + LOG(("WebSocketChannel:: close msg %s\n", + mServerCloseReason.get())); + } + } + + if (mCloseTimer) { + mCloseTimer->Cancel(); + mCloseTimer = nullptr; + } + + if (frame) { + // We send the frame immediately becuase we want to have it dispatched + // before the CallOnServerClose. + mService->FrameReceived(mSerial, mInnerWindowID, frame.forget()); + frame = nullptr; + } + + if (mListenerMT) { + mTargetThread->Dispatch( + new CallOnServerClose(this, mServerCloseCode, mServerCloseReason), + NS_DISPATCH_NORMAL); + } + + if (mClientClosed) ReleaseSession(); + } else if (opcode == nsIWebSocketFrame::OPCODE_PING) { + LOG(("WebSocketChannel:: ping received\n")); + GeneratePong(payload, payloadLength); + } else if (opcode == nsIWebSocketFrame::OPCODE_PONG) { + // opcode OPCODE_PONG: the mere act of receiving the packet is all we + // need to do for the pong to trigger the activity timers + LOG(("WebSocketChannel:: pong received\n")); + } else { + /* unknown control frame opcode */ + LOG(("WebSocketChannel:: unknown control op code %d\n", opcode)); + return NS_ERROR_ILLEGAL_VALUE; + } + + if (mFragmentAccumulator) { + // Remove the control frame from the stream so we have a contiguous + // data buffer of reassembled fragments + LOG(("WebSocketChannel:: Removing Control From Read buffer\n")); + MOZ_ASSERT(mFramePtr + framingLength == payload, + "payload offset from frameptr wrong"); + ::memmove(mFramePtr, payload + payloadLength, avail - payloadLength); + payload = mFramePtr; + avail -= payloadLength; + if (mBuffered) mBuffered -= framingLength + payloadLength; + payloadLength = 0; + } + + if (frame) { + mService->FrameReceived(mSerial, mInnerWindowID, frame.forget()); + } + } else if (opcode == nsIWebSocketFrame::OPCODE_BINARY) { + bool isDeflated = mPMCECompressor && mPMCECompressor->IsMessageDeflated(); + LOG(("WebSocketChannel:: %sbinary frame received\n", + isDeflated ? "deflated " : "")); + + if (mListenerMT) { + nsCString binaryData; + + if (isDeflated) { + rv = mPMCECompressor->Inflate(payload, payloadLength, binaryData); + if (NS_FAILED(rv)) { + return rv; + } + LOG( + ("WebSocketChannel:: message successfully inflated " + "[origLength=%d, newLength=%d]\n", + payloadLength, binaryData.Length())); + } else { + if (!binaryData.Assign((const char*)payload, payloadLength, + mozilla::fallible)) { + return NS_ERROR_OUT_OF_MEMORY; + } + } + + RefPtr frame = + mService->CreateFrameIfNeeded(finBit, rsvBit1, rsvBit2, rsvBit3, + opcode, maskBit, mask, binaryData); + if (frame) { + mService->FrameReceived(mSerial, mInnerWindowID, frame.forget()); + } + + mTargetThread->Dispatch( + new CallOnMessageAvailable(this, binaryData, binaryData.Length()), + NS_DISPATCH_NORMAL); + // To add the header to 'Networking Dashboard' log + if (mConnectionLogService && !mPrivateBrowsing) { + mConnectionLogService->NewMsgReceived(mHost, mSerial, count); + LOG(("Added new received msg for %s", mHost.get())); + } + } + } else if (opcode != nsIWebSocketFrame::OPCODE_CONTINUATION) { + /* unknown opcode */ + LOG(("WebSocketChannel:: unknown op code %d\n", opcode)); + return NS_ERROR_ILLEGAL_VALUE; + } + + mFramePtr = payload + payloadLength; + avail -= payloadLength; + totalAvail = avail; + } + + // Adjust the stateful buffer. If we were operating off the stack and + // now have a partial message then transition to the buffer, or if + // we were working off the buffer but no longer have any active state + // then transition to the stack + if (!IsPersistentFramePtr()) { + mBuffered = 0; + + if (mFragmentAccumulator) { + LOG(("WebSocketChannel:: Setup Buffer due to fragment")); + + if (!UpdateReadBuffer(mFramePtr - mFragmentAccumulator, + totalAvail + mFragmentAccumulator, 0, nullptr)) { + return NS_ERROR_FILE_TOO_BIG; + } + + // UpdateReadBuffer will reset the frameptr to the beginning + // of new saved state, so we need to skip past processed framgents + mFramePtr += mFragmentAccumulator; + } else if (totalAvail) { + LOG(("WebSocketChannel:: Setup Buffer due to partial frame")); + if (!UpdateReadBuffer(mFramePtr, totalAvail, 0, nullptr)) { + return NS_ERROR_FILE_TOO_BIG; + } + } + } else if (!mFragmentAccumulator && !totalAvail) { + // If we were working off a saved buffer state and there is no partial + // frame or fragment in process, then revert to stack behavior + LOG(("WebSocketChannel:: Internal buffering not needed anymore")); + mBuffered = 0; + + // release memory if we've been processing a large message + if (mBufferSize > kIncomingBufferStableSize) { + mBufferSize = kIncomingBufferStableSize; + free(mBuffer); + mBuffer = (uint8_t*)moz_xmalloc(mBufferSize); + } + } + return NS_OK; +} + +/* static */ +void WebSocketChannel::ApplyMask(uint32_t mask, uint8_t* data, uint64_t len) { + if (!data || len == 0) return; + + // Optimally we want to apply the mask 32 bits at a time, + // but the buffer might not be alligned. So we first deal with + // 0 to 3 bytes of preamble individually + + while (len && (reinterpret_cast(data) & 3)) { + *data ^= mask >> 24; + mask = RotateLeft(mask, 8); + data++; + len--; + } + + // perform mask on full words of data + + uint32_t* iData = (uint32_t*)data; + uint32_t* end = iData + (len / 4); + NetworkEndian::writeUint32(&mask, mask); + for (; iData < end; iData++) *iData ^= mask; + mask = NetworkEndian::readUint32(&mask); + data = (uint8_t*)iData; + len = len % 4; + + // There maybe up to 3 trailing bytes that need to be dealt with + // individually + + while (len) { + *data ^= mask >> 24; + mask = RotateLeft(mask, 8); + data++; + len--; + } +} + +void WebSocketChannel::GeneratePing() { + nsAutoCString buf; + buf.AssignLiteral("PING"); + EnqueueOutgoingMessage(mOutgoingPingMessages, + new OutboundMessage(kMsgTypePing, buf)); +} + +void WebSocketChannel::GeneratePong(uint8_t* payload, uint32_t len) { + nsAutoCString buf; + buf.SetLength(len); + if (buf.Length() < len) { + LOG(("WebSocketChannel::GeneratePong Allocation Failure\n")); + return; + } + + memcpy(buf.BeginWriting(), payload, len); + EnqueueOutgoingMessage(mOutgoingPongMessages, + new OutboundMessage(kMsgTypePong, buf)); +} + +void WebSocketChannel::EnqueueOutgoingMessage(nsDeque& aQueue, + OutboundMessage* aMsg) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + LOG( + ("WebSocketChannel::EnqueueOutgoingMessage %p " + "queueing msg %p [type=%s len=%d]\n", + this, aMsg, msgNames[aMsg->GetMsgType()], aMsg->Length())); + + aQueue.Push(aMsg); + OnOutputStreamReady(mSocketOut); +} + +uint16_t WebSocketChannel::ResultToCloseCode(nsresult resultCode) { + if (NS_SUCCEEDED(resultCode)) return CLOSE_NORMAL; + + switch (resultCode) { + case NS_ERROR_FILE_TOO_BIG: + case NS_ERROR_OUT_OF_MEMORY: + return CLOSE_TOO_LARGE; + case NS_ERROR_CANNOT_CONVERT_DATA: + return CLOSE_INVALID_PAYLOAD; + case NS_ERROR_UNEXPECTED: + return CLOSE_INTERNAL_ERROR; + default: + return CLOSE_PROTOCOL_ERROR; + } +} + +void WebSocketChannel::PrimeNewOutgoingMessage() { + LOG(("WebSocketChannel::PrimeNewOutgoingMessage() %p\n", this)); + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + MOZ_ASSERT(!mCurrentOut, "Current message in progress"); + + nsresult rv = NS_OK; + + mCurrentOut = mOutgoingPongMessages.PopFront(); + if (mCurrentOut) { + MOZ_ASSERT(mCurrentOut->GetMsgType() == kMsgTypePong, "Not pong message!"); + } else { + mCurrentOut = mOutgoingPingMessages.PopFront(); + if (mCurrentOut) + MOZ_ASSERT(mCurrentOut->GetMsgType() == kMsgTypePing, + "Not ping message!"); + else + mCurrentOut = mOutgoingMessages.PopFront(); + } + + if (!mCurrentOut) return; + + auto cleanupAfterFailure = + MakeScopeExit([&] { DeleteCurrentOutGoingMessage(); }); + + WsMsgType msgType = mCurrentOut->GetMsgType(); + + LOG( + ("WebSocketChannel::PrimeNewOutgoingMessage " + "%p found queued msg %p [type=%s len=%d]\n", + this, mCurrentOut, msgNames[msgType], mCurrentOut->Length())); + + mCurrentOutSent = 0; + mHdrOut = mOutHeader; + + uint8_t maskBit = mIsServerSide ? 0 : kMaskBit; + uint8_t maskSize = mIsServerSide ? 0 : 4; + + uint8_t* payload = nullptr; + + if (msgType == kMsgTypeFin) { + // This is a demand to create a close message + if (mClientClosed) { + DeleteCurrentOutGoingMessage(); + PrimeNewOutgoingMessage(); + cleanupAfterFailure.release(); + return; + } + + mClientClosed = true; + mOutHeader[0] = kFinalFragBit | nsIWebSocketFrame::OPCODE_CLOSE; + mOutHeader[1] = maskBit; + + // payload is offset 2 plus size of the mask + payload = mOutHeader + 2 + maskSize; + + // The close reason code sits in the first 2 bytes of payload + // If the channel user provided a code and reason during Close() + // and there isn't an internal error, use that. + if (NS_SUCCEEDED(mStopOnClose)) { + if (mScriptCloseCode) { + NetworkEndian::writeUint16(payload, mScriptCloseCode); + mOutHeader[1] += 2; + mHdrOutToSend = 4 + maskSize; + if (!mScriptCloseReason.IsEmpty()) { + MOZ_ASSERT(mScriptCloseReason.Length() <= 123, + "Close Reason Too Long"); + mOutHeader[1] += mScriptCloseReason.Length(); + mHdrOutToSend += mScriptCloseReason.Length(); + memcpy(payload + 2, mScriptCloseReason.BeginReading(), + mScriptCloseReason.Length()); + } + } else { + // No close code/reason, so payload length = 0. We must still send mask + // even though it's not used. Keep payload offset so we write mask + // below. + mHdrOutToSend = 2 + maskSize; + } + } else { + NetworkEndian::writeUint16(payload, ResultToCloseCode(mStopOnClose)); + mOutHeader[1] += 2; + mHdrOutToSend = 4 + maskSize; + } + + if (mServerClosed) { + /* bidi close complete */ + mReleaseOnTransmit = 1; + } else if (NS_FAILED(mStopOnClose)) { + /* result of abort session - give up */ + StopSession(mStopOnClose); + } else { + /* wait for reciprocal close from server */ + rv = NS_NewTimerWithCallback(getter_AddRefs(mCloseTimer), this, + mCloseTimeout, nsITimer::TYPE_ONE_SHOT); + if (NS_FAILED(rv)) { + StopSession(rv); + } + } + } else { + switch (msgType) { + case kMsgTypePong: + mOutHeader[0] = kFinalFragBit | nsIWebSocketFrame::OPCODE_PONG; + break; + case kMsgTypePing: + mOutHeader[0] = kFinalFragBit | nsIWebSocketFrame::OPCODE_PING; + break; + case kMsgTypeString: + mOutHeader[0] = kFinalFragBit | nsIWebSocketFrame::OPCODE_TEXT; + break; + case kMsgTypeStream: + // HACK ALERT: read in entire stream into string. + // Will block socket transport thread if file is blocking. + // TODO: bug 704447: don't block socket thread! + rv = mCurrentOut->ConvertStreamToString(); + if (NS_FAILED(rv)) { + AbortSession(NS_ERROR_FILE_TOO_BIG); + return; + } + // Now we're a binary string + msgType = kMsgTypeBinaryString; + + // no break: fall down into binary string case + [[fallthrough]]; + + case kMsgTypeBinaryString: + mOutHeader[0] = kFinalFragBit | nsIWebSocketFrame::OPCODE_BINARY; + break; + case kMsgTypeFin: + MOZ_ASSERT(false, "unreachable"); // avoid compiler warning + break; + } + + // deflate the payload if PMCE is negotiated + if (mPMCECompressor && + (msgType == kMsgTypeString || msgType == kMsgTypeBinaryString)) { + if (mCurrentOut->DeflatePayload(mPMCECompressor.get())) { + // The payload was deflated successfully, set RSV1 bit + mOutHeader[0] |= kRsv1Bit; + + LOG( + ("WebSocketChannel::PrimeNewOutgoingMessage %p current msg %p was " + "deflated [origLength=%d, newLength=%d].\n", + this, mCurrentOut, mCurrentOut->OrigLength(), + mCurrentOut->Length())); + } + } + + if (mCurrentOut->Length() < 126) { + mOutHeader[1] = mCurrentOut->Length() | maskBit; + mHdrOutToSend = 2 + maskSize; + } else if (mCurrentOut->Length() <= 0xffff) { + mOutHeader[1] = 126 | maskBit; + NetworkEndian::writeUint16(mOutHeader + sizeof(uint16_t), + mCurrentOut->Length()); + mHdrOutToSend = 4 + maskSize; + } else { + mOutHeader[1] = 127 | maskBit; + NetworkEndian::writeUint64(mOutHeader + 2, mCurrentOut->Length()); + mHdrOutToSend = 10 + maskSize; + } + payload = mOutHeader + mHdrOutToSend; + } + + MOZ_ASSERT(payload, "payload offset not found"); + + uint32_t mask = 0; + if (!mIsServerSide) { + // Perform the sending mask. Never use a zero mask + do { + uint8_t* buffer; + static_assert(4 == sizeof(mask), "Size of the mask should be equal to 4"); + nsresult rv = + mRandomGenerator->GenerateRandomBytes(sizeof(mask), &buffer); + if (NS_FAILED(rv)) { + LOG( + ("WebSocketChannel::PrimeNewOutgoingMessage(): " + "GenerateRandomBytes failure %" PRIx32 "\n", + static_cast(rv))); + AbortSession(rv); + return; + } + memcpy(&mask, buffer, sizeof(mask)); + free(buffer); + } while (!mask); + NetworkEndian::writeUint32(payload - sizeof(uint32_t), mask); + } + + LOG(("WebSocketChannel::PrimeNewOutgoingMessage() using mask %08x\n", mask)); + + // We don't mask the framing, but occasionally we stick a little payload + // data in the buffer used for the framing. Close frames are the current + // example. This data needs to be masked, but it is never more than a + // handful of bytes and might rotate the mask, so we can just do it locally. + // For real data frames we ship the bulk of the payload off to ApplyMask() + + RefPtr frame = mService->CreateFrameIfNeeded( + mOutHeader[0] & WebSocketChannel::kFinalFragBit, + mOutHeader[0] & WebSocketChannel::kRsv1Bit, + mOutHeader[0] & WebSocketChannel::kRsv2Bit, + mOutHeader[0] & WebSocketChannel::kRsv3Bit, + mOutHeader[0] & WebSocketChannel::kOpcodeBitsMask, + mOutHeader[1] & WebSocketChannel::kMaskBit, mask, payload, + mHdrOutToSend - (payload - mOutHeader), mCurrentOut->BeginOrigReading(), + mCurrentOut->OrigLength()); + + if (frame) { + mService->FrameSent(mSerial, mInnerWindowID, frame.forget()); + } + + if (mask) { + while (payload < (mOutHeader + mHdrOutToSend)) { + *payload ^= mask >> 24; + mask = RotateLeft(mask, 8); + payload++; + } + + // Mask the real message payloads + ApplyMask(mask, mCurrentOut->BeginWriting(), mCurrentOut->Length()); + } + + int32_t len = mCurrentOut->Length(); + + // for small frames, copy it all together for a contiguous write + if (len && len <= kCopyBreak) { + memcpy(mOutHeader + mHdrOutToSend, mCurrentOut->BeginWriting(), len); + mHdrOutToSend += len; + mCurrentOutSent = len; + } + + // Transmitting begins - mHdrOutToSend bytes from mOutHeader and + // mCurrentOut->Length() bytes from mCurrentOut. The latter may be + // coaleseced into the former for small messages or as the result of the + // compression process. + + cleanupAfterFailure.release(); +} + +void WebSocketChannel::DeleteCurrentOutGoingMessage() { + delete mCurrentOut; + mCurrentOut = nullptr; + mCurrentOutSent = 0; +} + +void WebSocketChannel::EnsureHdrOut(uint32_t size) { + LOG(("WebSocketChannel::EnsureHdrOut() %p [%d]\n", this, size)); + + if (mDynamicOutputSize < size) { + mDynamicOutputSize = size; + mDynamicOutput = (uint8_t*)moz_xrealloc(mDynamicOutput, mDynamicOutputSize); + } + + mHdrOut = mDynamicOutput; +} + +namespace { + +class RemoveObserverRunnable : public Runnable { + RefPtr mChannel; + + public: + explicit RemoveObserverRunnable(WebSocketChannel* aChannel) + : Runnable("net::RemoveObserverRunnable"), mChannel(aChannel) {} + + NS_IMETHOD Run() override { + nsCOMPtr observerService = + mozilla::services::GetObserverService(); + if (!observerService) { + NS_WARNING("failed to get observer service"); + return NS_OK; + } + + observerService->RemoveObserver(mChannel, NS_NETWORK_LINK_TOPIC); + return NS_OK; + } +}; + +} // namespace + +void WebSocketChannel::CleanupConnection() { + LOG(("WebSocketChannel::CleanupConnection() %p", this)); + + if (mLingeringCloseTimer) { + mLingeringCloseTimer->Cancel(); + mLingeringCloseTimer = nullptr; + } + + if (mSocketIn) { + if (mDataStarted) { + mSocketIn->AsyncWait(nullptr, 0, 0, nullptr); + } + mSocketIn = nullptr; + } + + if (mSocketOut) { + mSocketOut->AsyncWait(nullptr, 0, 0, nullptr); + mSocketOut = nullptr; + } + + if (mTransport) { + mTransport->SetSecurityCallbacks(nullptr); + mTransport->SetEventSink(nullptr, nullptr); + mTransport->Close(NS_BASE_STREAM_CLOSED); + mTransport = nullptr; + } + + if (mConnectionLogService && !mPrivateBrowsing) { + mConnectionLogService->RemoveHost(mHost, mSerial); + } + + // This method can run in any thread, but the observer has to be removed on + // the main-thread. + NS_DispatchToMainThread(new RemoveObserverRunnable(this)); + + DecrementSessionCount(); +} + +void WebSocketChannel::StopSession(nsresult reason) { + LOG(("WebSocketChannel::StopSession() %p [%" PRIx32 "]\n", this, + static_cast(reason))); + + { + MutexAutoLock lock(mMutex); + if (mStopped) { + return; + } + mStopped = true; + } + + DoStopSession(reason); +} + +void WebSocketChannel::DoStopSession(nsresult reason) { + LOG(("WebSocketChannel::DoStopSession() %p [%" PRIx32 "]\n", this, + static_cast(reason))); + + // normally this should be called on socket thread, but it is ok to call it + // from OnStartRequest before the socket thread machine has gotten underway + + MOZ_ASSERT(mStopped); + MOZ_ASSERT(OnSocketThread() || mTCPClosed || !mDataStarted); + + if (!mOpenedHttpChannel) { + // The HTTP channel information will never be used in this case + NS_ReleaseOnMainThread("WebSocketChannel::mChannel", mChannel.forget()); + NS_ReleaseOnMainThread("WebSocketChannel::mHttpChannel", + mHttpChannel.forget()); + NS_ReleaseOnMainThread("WebSocketChannel::mLoadGroup", mLoadGroup.forget()); + NS_ReleaseOnMainThread("WebSocketChannel::mCallbacks", mCallbacks.forget()); + } + + if (mCloseTimer) { + mCloseTimer->Cancel(); + mCloseTimer = nullptr; + } + + if (mOpenTimer) { + mOpenTimer->Cancel(); + mOpenTimer = nullptr; + } + + if (mReconnectDelayTimer) { + mReconnectDelayTimer->Cancel(); + mReconnectDelayTimer = nullptr; + } + + if (mPingTimer) { + mPingTimer->Cancel(); + mPingTimer = nullptr; + } + + if (mSocketIn && !mTCPClosed && mDataStarted) { + // Drain, within reason, this socket. if we leave any data + // unconsumed (including the tcp fin) a RST will be generated + // The right thing to do here is shutdown(SHUT_WR) and then wait + // a little while to see if any data comes in.. but there is no + // reason to delay things for that when the websocket handshake + // is supposed to guarantee a quiet connection except for that fin. + + char buffer[512]; + uint32_t count = 0; + uint32_t total = 0; + nsresult rv; + do { + total += count; + rv = mSocketIn->Read(buffer, 512, &count); + if (rv != NS_BASE_STREAM_WOULD_BLOCK && (NS_FAILED(rv) || count == 0)) + mTCPClosed = true; + } while (NS_SUCCEEDED(rv) && count > 0 && total < 32000); + } + + int32_t sessionCount = kLingeringCloseThreshold; + nsWSAdmissionManager::GetSessionCount(sessionCount); + + if (!mTCPClosed && mTransport && sessionCount < kLingeringCloseThreshold) { + // 7.1.1 says that the client SHOULD wait for the server to close the TCP + // connection. This is so we can reuse port numbers before 2 MSL expires, + // which is not really as much of a concern for us as the amount of state + // that might be accrued by keeping this channel object around waiting for + // the server. We handle the SHOULD by waiting a short time in the common + // case, but not waiting in the case of high concurrency. + // + // Normally this will be taken care of in AbortSession() after mTCPClosed + // is set when the server close arrives without waiting for the timeout to + // expire. + + LOG(("WebSocketChannel::DoStopSession: Wait for Server TCP close")); + + nsresult rv; + rv = NS_NewTimerWithCallback(getter_AddRefs(mLingeringCloseTimer), this, + kLingeringCloseTimeout, + nsITimer::TYPE_ONE_SHOT); + if (NS_FAILED(rv)) CleanupConnection(); + } else { + CleanupConnection(); + } + + if (mCancelable) { + mCancelable->Cancel(NS_ERROR_UNEXPECTED); + mCancelable = nullptr; + } + + mPMCECompressor = nullptr; + + if (!mCalledOnStop) { + mCalledOnStop = true; + + nsWSAdmissionManager::OnStopSession(this, reason); + + RefPtr runnable = new CallOnStop(this, reason); + mTargetThread->Dispatch(runnable, NS_DISPATCH_NORMAL); + } +} + +void WebSocketChannel::AbortSession(nsresult reason) { + LOG(("WebSocketChannel::AbortSession() %p [reason %" PRIx32 + "] stopped = %d\n", + this, static_cast(reason), !!mStopped)); + + MOZ_ASSERT(NS_FAILED(reason), "reason must be a failure!"); + + // normally this should be called on socket thread, but it is ok to call it + // from the main thread before StartWebsocketData() has completed + + // When we are failing we need to close the TCP connection immediately + // as per 7.1.1 + mTCPClosed = true; + + if (mLingeringCloseTimer) { + MOZ_ASSERT(mStopped, "Lingering without Stop"); + LOG(("WebSocketChannel:: Cleanup connection based on TCP Close")); + CleanupConnection(); + return; + } + + { + MutexAutoLock lock(mMutex); + if (mStopped) { + return; + } + + if (mTransport && reason != NS_BASE_STREAM_CLOSED && !mRequestedClose && + !mClientClosed && !mServerClosed && mDataStarted) { + mRequestedClose = true; + mStopOnClose = reason; + mSocketThread->Dispatch( + new OutboundEnqueuer(this, + new OutboundMessage(kMsgTypeFin, VoidCString())), + nsIEventTarget::DISPATCH_NORMAL); + return; + } + + mStopped = true; + } + + DoStopSession(reason); +} + +// ReleaseSession is called on orderly shutdown +void WebSocketChannel::ReleaseSession() { + LOG(("WebSocketChannel::ReleaseSession() %p stopped = %d\n", this, + !!mStopped)); + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + StopSession(NS_OK); +} + +void WebSocketChannel::IncrementSessionCount() { + if (!mIncrementedSessionCount) { + nsWSAdmissionManager::IncrementSessionCount(); + mIncrementedSessionCount = true; + } +} + +void WebSocketChannel::DecrementSessionCount() { + // Make sure we decrement session count only once, and only if we incremented + // it. This code is thread-safe: sWebSocketAdmissions->DecrementSessionCount + // is atomic, and mIncrementedSessionCount/mDecrementedSessionCount are set at + // times when they'll never be a race condition for checking/setting them. + if (mIncrementedSessionCount && !mDecrementedSessionCount) { + nsWSAdmissionManager::DecrementSessionCount(); + mDecrementedSessionCount = true; + } +} + +namespace { +enum ExtensionParseMode { eParseServerSide, eParseClientSide }; +} + +static nsresult ParseWebSocketExtension(const nsACString& aExtension, + ExtensionParseMode aMode, + bool& aClientNoContextTakeover, + bool& aServerNoContextTakeover, + int32_t& aClientMaxWindowBits, + int32_t& aServerMaxWindowBits) { + nsCCharSeparatedTokenizer tokens(aExtension, ';'); + + if (!tokens.hasMoreTokens() || + !tokens.nextToken().EqualsLiteral("permessage-deflate")) { + LOG( + ("WebSocketChannel::ParseWebSocketExtension: " + "HTTP Sec-WebSocket-Extensions negotiated unknown value %s\n", + PromiseFlatCString(aExtension).get())); + return NS_ERROR_ILLEGAL_VALUE; + } + + aClientNoContextTakeover = aServerNoContextTakeover = false; + aClientMaxWindowBits = aServerMaxWindowBits = -1; + + while (tokens.hasMoreTokens()) { + auto token = tokens.nextToken(); + + int32_t nameEnd, valueStart; + int32_t delimPos = token.FindChar('='); + if (delimPos == kNotFound) { + nameEnd = token.Length(); + valueStart = token.Length(); + } else { + nameEnd = delimPos; + valueStart = delimPos + 1; + } + + auto paramName = Substring(token, 0, nameEnd); + auto paramValue = Substring(token, valueStart); + + if (paramName.EqualsLiteral("client_no_context_takeover")) { + if (!paramValue.IsEmpty()) { + LOG( + ("WebSocketChannel::ParseWebSocketExtension: parameter " + "client_no_context_takeover must not have value, found %s\n", + PromiseFlatCString(paramValue).get())); + return NS_ERROR_ILLEGAL_VALUE; + } + if (aClientNoContextTakeover) { + LOG( + ("WebSocketChannel::ParseWebSocketExtension: found multiple " + "parameters client_no_context_takeover\n")); + return NS_ERROR_ILLEGAL_VALUE; + } + aClientNoContextTakeover = true; + } else if (paramName.EqualsLiteral("server_no_context_takeover")) { + if (!paramValue.IsEmpty()) { + LOG( + ("WebSocketChannel::ParseWebSocketExtension: parameter " + "server_no_context_takeover must not have value, found %s\n", + PromiseFlatCString(paramValue).get())); + return NS_ERROR_ILLEGAL_VALUE; + } + if (aServerNoContextTakeover) { + LOG( + ("WebSocketChannel::ParseWebSocketExtension: found multiple " + "parameters server_no_context_takeover\n")); + return NS_ERROR_ILLEGAL_VALUE; + } + aServerNoContextTakeover = true; + } else if (paramName.EqualsLiteral("client_max_window_bits")) { + if (aClientMaxWindowBits != -1) { + LOG( + ("WebSocketChannel::ParseWebSocketExtension: found multiple " + "parameters client_max_window_bits\n")); + return NS_ERROR_ILLEGAL_VALUE; + } + + if (aMode == eParseServerSide && paramValue.IsEmpty()) { + // Use -2 to indicate that "client_max_window_bits" has been parsed, + // but had no value. + aClientMaxWindowBits = -2; + } else { + nsresult errcode; + aClientMaxWindowBits = + PromiseFlatCString(paramValue).ToInteger(&errcode); + if (NS_FAILED(errcode) || aClientMaxWindowBits < 8 || + aClientMaxWindowBits > 15) { + LOG( + ("WebSocketChannel::ParseWebSocketExtension: found invalid " + "parameter client_max_window_bits %s\n", + PromiseFlatCString(paramValue).get())); + return NS_ERROR_ILLEGAL_VALUE; + } + } + } else if (paramName.EqualsLiteral("server_max_window_bits")) { + if (aServerMaxWindowBits != -1) { + LOG( + ("WebSocketChannel::ParseWebSocketExtension: found multiple " + "parameters server_max_window_bits\n")); + return NS_ERROR_ILLEGAL_VALUE; + } + + nsresult errcode; + aServerMaxWindowBits = PromiseFlatCString(paramValue).ToInteger(&errcode); + if (NS_FAILED(errcode) || aServerMaxWindowBits < 8 || + aServerMaxWindowBits > 15) { + LOG( + ("WebSocketChannel::ParseWebSocketExtension: found invalid " + "parameter server_max_window_bits %s\n", + PromiseFlatCString(paramValue).get())); + return NS_ERROR_ILLEGAL_VALUE; + } + } else { + LOG( + ("WebSocketChannel::ParseWebSocketExtension: found unknown " + "parameter %s\n", + PromiseFlatCString(paramName).get())); + return NS_ERROR_ILLEGAL_VALUE; + } + } + + if (aClientMaxWindowBits == -2) { + aClientMaxWindowBits = -1; + } + + return NS_OK; +} + +nsresult WebSocketChannel::HandleExtensions() { + LOG(("WebSocketChannel::HandleExtensions() %p\n", this)); + + nsresult rv; + nsAutoCString extensions; + + MOZ_ASSERT(NS_IsMainThread(), "not main thread"); + + rv = mHttpChannel->GetResponseHeader("Sec-WebSocket-Extensions"_ns, + extensions); + extensions.CompressWhitespace(); + if (extensions.IsEmpty()) { + return NS_OK; + } + + LOG( + ("WebSocketChannel::HandleExtensions: received " + "Sec-WebSocket-Extensions header: %s\n", + extensions.get())); + + bool clientNoContextTakeover; + bool serverNoContextTakeover; + int32_t clientMaxWindowBits; + int32_t serverMaxWindowBits; + + rv = ParseWebSocketExtension(extensions, eParseClientSide, + clientNoContextTakeover, serverNoContextTakeover, + clientMaxWindowBits, serverMaxWindowBits); + if (NS_FAILED(rv)) { + AbortSession(rv); + return rv; + } + + if (!mAllowPMCE) { + LOG( + ("WebSocketChannel::HandleExtensions: " + "Recvd permessage-deflate which wasn't offered\n")); + AbortSession(NS_ERROR_ILLEGAL_VALUE); + return NS_ERROR_ILLEGAL_VALUE; + } + + if (clientMaxWindowBits == -1) { + clientMaxWindowBits = 15; + } + if (serverMaxWindowBits == -1) { + serverMaxWindowBits = 15; + } + + mPMCECompressor = MakeUnique( + clientNoContextTakeover, clientMaxWindowBits, serverMaxWindowBits); + if (mPMCECompressor->Active()) { + LOG( + ("WebSocketChannel::HandleExtensions: PMCE negotiated, %susing " + "context takeover, clientMaxWindowBits=%d, " + "serverMaxWindowBits=%d\n", + clientNoContextTakeover ? "NOT " : "", clientMaxWindowBits, + serverMaxWindowBits)); + + mNegotiatedExtensions = "permessage-deflate"; + } else { + LOG( + ("WebSocketChannel::HandleExtensions: Cannot init PMCE " + "compression object\n")); + mPMCECompressor = nullptr; + AbortSession(NS_ERROR_UNEXPECTED); + return NS_ERROR_UNEXPECTED; + } + + return NS_OK; +} + +void ProcessServerWebSocketExtensions(const nsACString& aExtensions, + nsACString& aNegotiatedExtensions) { + aNegotiatedExtensions.Truncate(); + + nsCOMPtr prefService = + do_GetService(NS_PREFSERVICE_CONTRACTID); + if (prefService) { + bool boolpref; + nsresult rv = prefService->GetBoolPref( + "network.websocket.extensions.permessage-deflate", &boolpref); + if (NS_SUCCEEDED(rv) && !boolpref) { + return; + } + } + + for (const auto& ext : + nsCCharSeparatedTokenizer(aExtensions, ',').ToRange()) { + bool clientNoContextTakeover; + bool serverNoContextTakeover; + int32_t clientMaxWindowBits; + int32_t serverMaxWindowBits; + + nsresult rv = ParseWebSocketExtension( + ext, eParseServerSide, clientNoContextTakeover, serverNoContextTakeover, + clientMaxWindowBits, serverMaxWindowBits); + if (NS_FAILED(rv)) { + // Ignore extensions that we can't parse + continue; + } + + aNegotiatedExtensions.AssignLiteral("permessage-deflate"); + if (clientNoContextTakeover) { + aNegotiatedExtensions.AppendLiteral(";client_no_context_takeover"); + } + if (serverNoContextTakeover) { + aNegotiatedExtensions.AppendLiteral(";server_no_context_takeover"); + } + if (clientMaxWindowBits != -1) { + aNegotiatedExtensions.AppendLiteral(";client_max_window_bits="); + aNegotiatedExtensions.AppendInt(clientMaxWindowBits); + } + if (serverMaxWindowBits != -1) { + aNegotiatedExtensions.AppendLiteral(";server_max_window_bits="); + aNegotiatedExtensions.AppendInt(serverMaxWindowBits); + } + + return; + } +} + +nsresult CalculateWebSocketHashedSecret(const nsACString& aKey, + nsACString& aHash) { + nsresult rv; + nsCString key = aKey + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"_ns; + nsCOMPtr hasher = + do_CreateInstance(NS_CRYPTO_HASH_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + rv = hasher->Init(nsICryptoHash::SHA1); + NS_ENSURE_SUCCESS(rv, rv); + rv = hasher->Update((const uint8_t*)key.BeginWriting(), key.Length()); + NS_ENSURE_SUCCESS(rv, rv); + return hasher->Finish(true, aHash); +} + +nsresult WebSocketChannel::SetupRequest() { + LOG(("WebSocketChannel::SetupRequest() %p\n", this)); + + nsresult rv; + + if (mLoadGroup) { + rv = mHttpChannel->SetLoadGroup(mLoadGroup); + NS_ENSURE_SUCCESS(rv, rv); + } + + rv = mHttpChannel->SetLoadFlags( + nsIRequest::LOAD_BACKGROUND | nsIRequest::INHIBIT_CACHING | + nsIRequest::LOAD_BYPASS_CACHE | nsIChannel::LOAD_BYPASS_SERVICE_WORKER); + NS_ENSURE_SUCCESS(rv, rv); + + // we never let websockets be blocked by head CSS/JS loads to avoid + // potential deadlock where server generation of CSS/JS requires + // an XHR signal. + nsCOMPtr cos(do_QueryInterface(mChannel)); + if (cos) { + cos->AddClassFlags(nsIClassOfService::Unblocked); + } + + // draft-ietf-hybi-thewebsocketprotocol-07 illustrates Upgrade: websocket + // in lower case, so go with that. It is technically case insensitive. + rv = mChannel->HTTPUpgrade("websocket"_ns, this); + NS_ENSURE_SUCCESS(rv, rv); + + rv = mHttpChannel->SetRequestHeader("Sec-WebSocket-Version"_ns, + nsLiteralCString(SEC_WEBSOCKET_VERSION), + false); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + if (!mOrigin.IsEmpty()) { + rv = mHttpChannel->SetRequestHeader("Origin"_ns, mOrigin, false); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + + if (!mProtocol.IsEmpty()) { + rv = mHttpChannel->SetRequestHeader("Sec-WebSocket-Protocol"_ns, mProtocol, + true); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + + if (mAllowPMCE) { + rv = mHttpChannel->SetRequestHeader("Sec-WebSocket-Extensions"_ns, + "permessage-deflate"_ns, false); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + + uint8_t* secKey; + nsAutoCString secKeyString; + + rv = mRandomGenerator->GenerateRandomBytes(16, &secKey); + NS_ENSURE_SUCCESS(rv, rv); + rv = Base64Encode(reinterpret_cast(secKey), 16, secKeyString); + free(secKey); + if (NS_FAILED(rv)) { + return rv; + } + + rv = mHttpChannel->SetRequestHeader("Sec-WebSocket-Key"_ns, secKeyString, + false); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + LOG(("WebSocketChannel::SetupRequest: client key %s\n", secKeyString.get())); + + // prepare the value we expect to see in + // the sec-websocket-accept response header + rv = CalculateWebSocketHashedSecret(secKeyString, mHashedSecret); + NS_ENSURE_SUCCESS(rv, rv); + LOG(("WebSocketChannel::SetupRequest: expected server key %s\n", + mHashedSecret.get())); + + mHttpChannelId = mHttpChannel->ChannelId(); + + return NS_OK; +} + +nsresult WebSocketChannel::DoAdmissionDNS() { + nsresult rv; + + nsCString hostName; + rv = mURI->GetHost(hostName); + NS_ENSURE_SUCCESS(rv, rv); + mAddress = hostName; + rv = mURI->GetPort(&mPort); + NS_ENSURE_SUCCESS(rv, rv); + if (mPort == -1) mPort = (mEncrypted ? kDefaultWSSPort : kDefaultWSPort); + nsCOMPtr dns = do_GetService(NS_DNSSERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr main = GetMainThreadEventTarget(); + MOZ_ASSERT(!mCancelable); + return dns->AsyncResolveNative( + hostName, nsIDNSService::RESOLVE_TYPE_DEFAULT, 0, nullptr, this, main, + mLoadInfo->GetOriginAttributes(), getter_AddRefs(mCancelable)); +} + +nsresult WebSocketChannel::ApplyForAdmission() { + LOG(("WebSocketChannel::ApplyForAdmission() %p\n", this)); + + // Websockets has a policy of 1 session at a time being allowed in the + // CONNECTING state per server IP address (not hostname) + + // Check to see if a proxy is being used before making DNS call + nsCOMPtr pps = + do_GetService(NS_PROTOCOLPROXYSERVICE_CONTRACTID); + + if (!pps) { + // go straight to DNS + // expect the callback in ::OnLookupComplete + LOG(( + "WebSocketChannel::ApplyForAdmission: checking for concurrent open\n")); + return DoAdmissionDNS(); + } + + MOZ_ASSERT(!mCancelable); + + nsresult rv; + rv = pps->AsyncResolve( + mHttpChannel, + nsIProtocolProxyService::RESOLVE_PREFER_SOCKS_PROXY | + nsIProtocolProxyService::RESOLVE_PREFER_HTTPS_PROXY | + nsIProtocolProxyService::RESOLVE_ALWAYS_TUNNEL, + this, nullptr, getter_AddRefs(mCancelable)); + NS_ASSERTION(NS_FAILED(rv) || mCancelable, + "nsIProtocolProxyService::AsyncResolve succeeded but didn't " + "return a cancelable object!"); + return rv; +} + +// Called after both OnStartRequest and OnTransportAvailable have +// executed. This essentially ends the handshake and starts the websockets +// protocol state machine. +nsresult WebSocketChannel::CallStartWebsocketData() { + LOG(("WebSocketChannel::CallStartWebsocketData() %p", this)); + MOZ_ASSERT(NS_IsMainThread(), "not main thread"); + + if (mOpenTimer) { + mOpenTimer->Cancel(); + mOpenTimer = nullptr; + } + + if (!IsOnTargetThread()) { + return mTargetThread->Dispatch( + NewRunnableMethod("net::WebSocketChannel::StartWebsocketData", this, + &WebSocketChannel::StartWebsocketData), + NS_DISPATCH_NORMAL); + } + + return StartWebsocketData(); +} + +nsresult WebSocketChannel::StartWebsocketData() { + nsresult rv; + + { + MutexAutoLock lock(mMutex); + LOG(("WebSocketChannel::StartWebsocketData() %p", this)); + MOZ_ASSERT(!mDataStarted, "StartWebsocketData twice"); + + if (mStopped) { + LOG( + ("WebSocketChannel::StartWebsocketData channel already closed, not " + "starting data")); + return NS_ERROR_NOT_AVAILABLE; + } + + mDataStarted = true; + } + + rv = mSocketIn->AsyncWait(this, 0, 0, mSocketThread); + if (NS_FAILED(rv)) { + LOG( + ("WebSocketChannel::StartWebsocketData mSocketIn->AsyncWait() failed " + "with error 0x%08" PRIx32, + static_cast(rv))); + return mSocketThread->Dispatch( + NewRunnableMethod("net::WebSocketChannel::AbortSession", this, + &WebSocketChannel::AbortSession, rv), + NS_DISPATCH_NORMAL); + } + + if (mPingInterval) { + rv = mSocketThread->Dispatch( + NewRunnableMethod("net::WebSocketChannel::StartPinging", this, + &WebSocketChannel::StartPinging), + NS_DISPATCH_NORMAL); + if (NS_FAILED(rv)) { + LOG( + ("WebSocketChannel::StartWebsocketData Could not start pinging, " + "rv=0x%08" PRIx32, + static_cast(rv))); + return rv; + } + } + + LOG(("WebSocketChannel::StartWebsocketData Notifying Listener %p", + mListenerMT ? mListenerMT->mListener.get() : nullptr)); + + if (mListenerMT) { + rv = mListenerMT->mListener->OnStart(mListenerMT->mContext); + if (NS_FAILED(rv)) { + LOG( + ("WebSocketChannel::StartWebsocketData " + "mListenerMT->mListener->OnStart() failed with error 0x%08" PRIx32, + static_cast(rv))); + } + } + + return NS_OK; +} + +nsresult WebSocketChannel::StartPinging() { + LOG(("WebSocketChannel::StartPinging() %p", this)); + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + MOZ_ASSERT(mPingInterval); + MOZ_ASSERT(!mPingTimer); + + nsresult rv; + rv = NS_NewTimerWithCallback(getter_AddRefs(mPingTimer), this, mPingInterval, + nsITimer::TYPE_ONE_SHOT); + if (NS_SUCCEEDED(rv)) { + LOG(("WebSocketChannel will generate ping after %d ms of receive silence\n", + mPingInterval)); + } else { + NS_WARNING("unable to create ping timer. Carrying on."); + } + + return NS_OK; +} + +void WebSocketChannel::ReportConnectionTelemetry(nsresult aStatusCode) { + // 3 bits are used. high bit is for wss, middle bit for failed, + // and low bit for proxy.. + // 0 - 7 : ws-ok-plain, ws-ok-proxy, ws-failed-plain, ws-failed-proxy, + // wss-ok-plain, wss-ok-proxy, wss-failed-plain, wss-failed-proxy + + bool didProxy = false; + + nsCOMPtr pi; + nsCOMPtr pc = do_QueryInterface(mChannel); + if (pc) pc->GetProxyInfo(getter_AddRefs(pi)); + if (pi) { + nsAutoCString proxyType; + pi->GetType(proxyType); + if (!proxyType.IsEmpty() && !proxyType.EqualsLiteral("direct")) + didProxy = true; + } + + uint8_t value = + (mEncrypted ? (1 << 2) : 0) | + (!(mGotUpgradeOK && NS_SUCCEEDED(aStatusCode)) ? (1 << 1) : 0) | + (didProxy ? (1 << 0) : 0); + + LOG(("WebSocketChannel::ReportConnectionTelemetry() %p %d", this, value)); + Telemetry::Accumulate(Telemetry::WEBSOCKETS_HANDSHAKE_TYPE, value); +} + +// nsIDNSListener + +NS_IMETHODIMP +WebSocketChannel::OnLookupComplete(nsICancelable* aRequest, + nsIDNSRecord* aRecord, nsresult aStatus) { + LOG(("WebSocketChannel::OnLookupComplete() %p [%p %p %" PRIx32 "]\n", this, + aRequest, aRecord, static_cast(aStatus))); + + MOZ_ASSERT(NS_IsMainThread(), "not main thread"); + + if (mStopped) { + LOG(("WebSocketChannel::OnLookupComplete: Request Already Stopped\n")); + mCancelable = nullptr; + return NS_OK; + } + + mCancelable = nullptr; + + // These failures are not fatal - we just use the hostname as the key + if (NS_FAILED(aStatus)) { + LOG(("WebSocketChannel::OnLookupComplete: No DNS Response\n")); + + // set host in case we got here without calling DoAdmissionDNS() + mURI->GetHost(mAddress); + } else { + nsCOMPtr record = do_QueryInterface(aRecord); + MOZ_ASSERT(record); + nsresult rv = record->GetNextAddrAsString(mAddress); + if (NS_FAILED(rv)) + LOG(("WebSocketChannel::OnLookupComplete: Failed GetNextAddr\n")); + } + + LOG(("WebSocket OnLookupComplete: Proceeding to ConditionallyConnect\n")); + nsWSAdmissionManager::ConditionallyConnect(this); + + return NS_OK; +} + +// nsIProtocolProxyCallback +NS_IMETHODIMP +WebSocketChannel::OnProxyAvailable(nsICancelable* aRequest, + nsIChannel* aChannel, nsIProxyInfo* pi, + nsresult status) { + if (mStopped) { + LOG(("WebSocketChannel::OnProxyAvailable: [%p] Request Already Stopped\n", + this)); + mCancelable = nullptr; + return NS_OK; + } + + MOZ_ASSERT(!mCancelable || (aRequest == mCancelable)); + mCancelable = nullptr; + + nsAutoCString type; + if (NS_SUCCEEDED(status) && pi && NS_SUCCEEDED(pi->GetType(type)) && + !type.EqualsLiteral("direct")) { + LOG(("WebSocket OnProxyAvailable [%p] Proxy found skip DNS lookup\n", + this)); + // call DNS callback directly without DNS resolver + OnLookupComplete(nullptr, nullptr, NS_ERROR_FAILURE); + } else { + LOG(("WebSocketChannel::OnProxyAvailable[%p] checking DNS resolution\n", + this)); + nsresult rv = DoAdmissionDNS(); + if (NS_FAILED(rv)) { + LOG(("WebSocket OnProxyAvailable [%p] DNS lookup failed\n", this)); + // call DNS callback directly without DNS resolver + OnLookupComplete(nullptr, nullptr, NS_ERROR_FAILURE); + } + } + + return NS_OK; +} + +// nsIInterfaceRequestor + +NS_IMETHODIMP +WebSocketChannel::GetInterface(const nsIID& iid, void** result) { + LOG(("WebSocketChannel::GetInterface() %p\n", this)); + + if (iid.Equals(NS_GET_IID(nsIChannelEventSink))) + return QueryInterface(iid, result); + + if (mCallbacks) return mCallbacks->GetInterface(iid, result); + + return NS_ERROR_NO_INTERFACE; +} + +// nsIChannelEventSink + +NS_IMETHODIMP +WebSocketChannel::AsyncOnChannelRedirect( + nsIChannel* oldChannel, nsIChannel* newChannel, uint32_t flags, + nsIAsyncVerifyRedirectCallback* callback) { + LOG(("WebSocketChannel::AsyncOnChannelRedirect() %p\n", this)); + + MOZ_ASSERT(NS_IsMainThread(), "not main thread"); + + nsresult rv; + + nsCOMPtr newuri; + rv = newChannel->GetURI(getter_AddRefs(newuri)); + NS_ENSURE_SUCCESS(rv, rv); + + // newuri is expected to be http or https + bool newuriIsHttps = newuri->SchemeIs("https"); + + if (!mAutoFollowRedirects) { + // Even if redirects configured off, still allow them for HTTP Strict + // Transport Security (from ws://FOO to https://FOO (mapped to wss://FOO) + + if (!(flags & (nsIChannelEventSink::REDIRECT_INTERNAL | + nsIChannelEventSink::REDIRECT_STS_UPGRADE))) { + nsAutoCString newSpec; + rv = newuri->GetSpec(newSpec); + NS_ENSURE_SUCCESS(rv, rv); + + LOG(("WebSocketChannel: Redirect to %s denied by configuration\n", + newSpec.get())); + return NS_ERROR_FAILURE; + } + } + + if (mEncrypted && !newuriIsHttps) { + nsAutoCString spec; + if (NS_SUCCEEDED(newuri->GetSpec(spec))) + LOG(("WebSocketChannel: Redirect to %s violates encryption rule\n", + spec.get())); + return NS_ERROR_FAILURE; + } + + nsCOMPtr newHttpChannel = do_QueryInterface(newChannel, &rv); + if (NS_FAILED(rv)) { + LOG(("WebSocketChannel: Redirect could not QI to HTTP\n")); + return rv; + } + + nsCOMPtr newUpgradeChannel = + do_QueryInterface(newChannel, &rv); + + if (NS_FAILED(rv)) { + LOG(("WebSocketChannel: Redirect could not QI to HTTP Upgrade\n")); + return rv; + } + + // The redirect is likely OK + + newChannel->SetNotificationCallbacks(this); + + mEncrypted = newuriIsHttps; + rv = NS_MutateURI(newuri) + .SetScheme(mEncrypted ? "wss"_ns : "ws"_ns) + .Finalize(mURI); + + if (NS_FAILED(rv)) { + LOG(("WebSocketChannel: Could not set the proper scheme\n")); + return rv; + } + + mHttpChannel = newHttpChannel; + mChannel = newUpgradeChannel; + rv = SetupRequest(); + if (NS_FAILED(rv)) { + LOG(("WebSocketChannel: Redirect could not SetupRequest()\n")); + return rv; + } + + // Redirected-to URI may need to be delayed by 1-connecting-per-host and + // delay-after-fail algorithms. So hold off calling OnRedirectVerifyCallback + // until BeginOpen, when we know it's OK to proceed with new channel. + mRedirectCallback = callback; + + // Mark old channel as successfully connected so we'll clear any FailDelay + // associated with the old URI. Note: no need to also call OnStopSession: + // it's a no-op for successful, already-connected channels. + nsWSAdmissionManager::OnConnected(this); + + // ApplyForAdmission as if we were starting from fresh... + mAddress.Truncate(); + mOpenedHttpChannel = false; + rv = ApplyForAdmission(); + if (NS_FAILED(rv)) { + LOG(("WebSocketChannel: Redirect failed due to DNS failure\n")); + mRedirectCallback = nullptr; + return rv; + } + + return NS_OK; +} + +// nsITimerCallback + +NS_IMETHODIMP +WebSocketChannel::Notify(nsITimer* timer) { + LOG(("WebSocketChannel::Notify() %p [%p]\n", this, timer)); + + if (timer == mCloseTimer) { + MOZ_ASSERT(mClientClosed, "Close Timeout without local close"); + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + mCloseTimer = nullptr; + if (mStopped || mServerClosed) /* no longer relevant */ + return NS_OK; + + LOG(("WebSocketChannel:: Expecting Server Close - Timed Out\n")); + AbortSession(NS_ERROR_NET_TIMEOUT); + } else if (timer == mOpenTimer) { + MOZ_ASSERT(NS_IsMainThread(), "not main thread"); + + mOpenTimer = nullptr; + LOG(("WebSocketChannel:: Connection Timed Out\n")); + if (mStopped || mServerClosed) /* no longer relevant */ + return NS_OK; + + AbortSession(NS_ERROR_NET_TIMEOUT); + } else if (timer == mReconnectDelayTimer) { + MOZ_ASSERT(mConnecting == CONNECTING_DELAYED, + "woke up from delay w/o being delayed?"); + MOZ_ASSERT(NS_IsMainThread(), "not main thread"); + + mReconnectDelayTimer = nullptr; + LOG(("WebSocketChannel: connecting [this=%p] after reconnect delay", this)); + BeginOpen(false); + } else if (timer == mPingTimer) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + if (mClientClosed || mServerClosed || mRequestedClose) { + // no point in worrying about ping now + mPingTimer = nullptr; + return NS_OK; + } + + if (!mPingOutstanding) { + // Ping interval must be non-null or PING was forced by OnNetworkChanged() + MOZ_ASSERT(mPingInterval || mPingForced); + LOG(("nsWebSocketChannel:: Generating Ping\n")); + mPingOutstanding = 1; + mPingForced = false; + mPingTimer->InitWithCallback(this, mPingResponseTimeout, + nsITimer::TYPE_ONE_SHOT); + GeneratePing(); + } else { + LOG(("nsWebSocketChannel:: Timed out Ping\n")); + mPingTimer = nullptr; + AbortSession(NS_ERROR_NET_TIMEOUT); + } + } else if (timer == mLingeringCloseTimer) { + LOG(("WebSocketChannel:: Lingering Close Timer")); + CleanupConnection(); + } else { + MOZ_ASSERT(0, "Unknown Timer"); + } + + return NS_OK; +} + +// nsINamed + +NS_IMETHODIMP +WebSocketChannel::GetName(nsACString& aName) { + aName.AssignLiteral("WebSocketChannel"); + return NS_OK; +} + +// nsIWebSocketChannel + +NS_IMETHODIMP +WebSocketChannel::GetSecurityInfo(nsISupports** aSecurityInfo) { + LOG(("WebSocketChannel::GetSecurityInfo() %p\n", this)); + MOZ_ASSERT(NS_IsMainThread(), "not main thread"); + + if (mTransport) { + if (NS_FAILED(mTransport->GetSecurityInfo(aSecurityInfo))) + *aSecurityInfo = nullptr; + } + return NS_OK; +} + +NS_IMETHODIMP +WebSocketChannel::AsyncOpen(nsIURI* aURI, const nsACString& aOrigin, + uint64_t aInnerWindowID, + nsIWebSocketListener* aListener, + nsISupports* aContext) { + LOG(("WebSocketChannel::AsyncOpen() %p\n", this)); + + if (!NS_IsMainThread()) { + MOZ_ASSERT(false, "not main thread"); + LOG(("WebSocketChannel::AsyncOpen() called off the main thread")); + return NS_ERROR_UNEXPECTED; + } + + if ((!aURI && !mIsServerSide) || !aListener) { + LOG(("WebSocketChannel::AsyncOpen() Uri or Listener null")); + return NS_ERROR_UNEXPECTED; + } + + if (mListenerMT || mWasOpened) return NS_ERROR_ALREADY_OPENED; + + nsresult rv; + + // Ensure target thread is set. + if (!mTargetThread) { + mTargetThread = GetMainThreadEventTarget(); + } + + mSocketThread = do_GetService(NS_SOCKETTRANSPORTSERVICE_CONTRACTID, &rv); + if (NS_FAILED(rv)) { + NS_WARNING("unable to continue without socket transport service"); + return rv; + } + + nsCOMPtr prefService; + prefService = do_GetService(NS_PREFSERVICE_CONTRACTID); + + if (prefService) { + int32_t intpref; + bool boolpref; + rv = + prefService->GetIntPref("network.websocket.max-message-size", &intpref); + if (NS_SUCCEEDED(rv)) { + mMaxMessageSize = clamped(intpref, 1024, INT32_MAX); + } + rv = prefService->GetIntPref("network.websocket.timeout.close", &intpref); + if (NS_SUCCEEDED(rv)) { + 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) && !mClientSetPingInterval) { + mPingInterval = clamped(intpref, 0, 86400) * 1000; + } + rv = prefService->GetIntPref("network.websocket.timeout.ping.response", + &intpref); + if (NS_SUCCEEDED(rv) && !mClientSetPingTimeout) { + mPingResponseTimeout = clamped(intpref, 1, 3600) * 1000; + } + rv = prefService->GetBoolPref( + "network.websocket.extensions.permessage-deflate", &boolpref); + if (NS_SUCCEEDED(rv)) { + mAllowPMCE = boolpref ? 1 : 0; + } + rv = prefService->GetBoolPref( + "network.websocket.auto-follow-http-redirects", &boolpref); + if (NS_SUCCEEDED(rv)) { + mAutoFollowRedirects = boolpref ? 1 : 0; + } + rv = prefService->GetIntPref("network.websocket.max-connections", &intpref); + if (NS_SUCCEEDED(rv)) { + mMaxConcurrentConnections = clamped(intpref, 1, 0xffff); + } + } + + int32_t sessionCount = -1; + nsWSAdmissionManager::GetSessionCount(sessionCount); + if (sessionCount >= 0) { + LOG(("WebSocketChannel::AsyncOpen %p sessionCount=%d max=%d\n", this, + sessionCount, mMaxConcurrentConnections)); + } + + if (sessionCount >= mMaxConcurrentConnections) { + LOG(("WebSocketChannel: max concurrency %d exceeded (%d)", + mMaxConcurrentConnections, sessionCount)); + + // WebSocket connections are expected to be long lived, so return + // an error here instead of queueing + return NS_ERROR_SOCKET_CREATE_FAILED; + } + + mInnerWindowID = aInnerWindowID; + mOriginalURI = aURI; + mURI = mOriginalURI; + mOrigin = aOrigin; + + if (mIsServerSide) { + // IncrementSessionCount(); + mWasOpened = 1; + mListenerMT = new ListenerAndContextContainer(aListener, aContext); + rv = mServerTransportProvider->SetListener(this); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + mServerTransportProvider = nullptr; + + return NS_OK; + } + + mURI->GetHostPort(mHost); + + mRandomGenerator = + do_GetService("@mozilla.org/security/random-generator;1", &rv); + if (NS_FAILED(rv)) { + NS_WARNING("unable to continue without random number generator"); + return rv; + } + + nsCOMPtr localURI; + nsCOMPtr localChannel; + + rv = NS_MutateURI(mURI) + .SetScheme(mEncrypted ? "https"_ns : "http"_ns) + .Finalize(localURI); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr ioService; + ioService = do_GetService(NS_IOSERVICE_CONTRACTID, &rv); + if (NS_FAILED(rv)) { + NS_WARNING("unable to continue without io service"); + return rv; + } + + // Ideally we'd call newChannelFromURIWithLoadInfo here, but that doesn't + // allow setting proxy uri/flags + rv = ioService->NewChannelFromURIWithProxyFlags( + localURI, mURI, + nsIProtocolProxyService::RESOLVE_PREFER_SOCKS_PROXY | + nsIProtocolProxyService::RESOLVE_PREFER_HTTPS_PROXY | + nsIProtocolProxyService::RESOLVE_ALWAYS_TUNNEL, + mLoadInfo->LoadingNode(), mLoadInfo->GetLoadingPrincipal(), + mLoadInfo->TriggeringPrincipal(), mLoadInfo->GetSecurityFlags(), + mLoadInfo->InternalContentPolicyType(), getter_AddRefs(localChannel)); + NS_ENSURE_SUCCESS(rv, rv); + + // Please note that we still call SetLoadInfo on the channel because + // we want the same instance of the loadInfo to be set on the channel. + rv = localChannel->SetLoadInfo(mLoadInfo); + NS_ENSURE_SUCCESS(rv, rv); + + // Pass most GetInterface() requests through to our instantiator, but handle + // nsIChannelEventSink in this object in order to deal with redirects + localChannel->SetNotificationCallbacks(this); + + class MOZ_STACK_CLASS CleanUpOnFailure { + public: + explicit CleanUpOnFailure(WebSocketChannel* aWebSocketChannel) + : mWebSocketChannel(aWebSocketChannel) {} + + ~CleanUpOnFailure() { + if (!mWebSocketChannel->mWasOpened) { + mWebSocketChannel->mChannel = nullptr; + mWebSocketChannel->mHttpChannel = nullptr; + } + } + + WebSocketChannel* mWebSocketChannel; + }; + + CleanUpOnFailure cuof(this); + + mChannel = do_QueryInterface(localChannel, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + mHttpChannel = do_QueryInterface(localChannel, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = SetupRequest(); + if (NS_FAILED(rv)) return rv; + + mPrivateBrowsing = NS_UsePrivateBrowsing(localChannel); + + if (mConnectionLogService && !mPrivateBrowsing) { + mConnectionLogService->AddHost(mHost, mSerial, + BaseWebSocketChannel::mEncrypted); + } + + rv = ApplyForAdmission(); + if (NS_FAILED(rv)) return rv; + + // Register for prefs change notifications + nsCOMPtr observerService = + mozilla::services::GetObserverService(); + if (!observerService) { + NS_WARNING("failed to get observer service"); + return NS_ERROR_FAILURE; + } + + rv = observerService->AddObserver(this, NS_NETWORK_LINK_TOPIC, false); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Only set these if the open was successful: + // + mWasOpened = 1; + mListenerMT = new ListenerAndContextContainer(aListener, aContext); + IncrementSessionCount(); + + return rv; +} + +NS_IMETHODIMP +WebSocketChannel::Close(uint16_t code, const nsACString& reason) { + LOG(("WebSocketChannel::Close() %p\n", this)); + MOZ_ASSERT(NS_IsMainThread(), "not main thread"); + + { + MutexAutoLock lock(mMutex); + + if (mRequestedClose) { + return NS_OK; + } + + if (mStopped) { + return NS_ERROR_NOT_AVAILABLE; + } + + // The API requires the UTF-8 string to be 123 or less bytes + if (reason.Length() > 123) return NS_ERROR_ILLEGAL_VALUE; + + mRequestedClose = true; + mScriptCloseReason = reason; + mScriptCloseCode = code; + + if (mDataStarted) { + return mSocketThread->Dispatch( + new OutboundEnqueuer(this, + new OutboundMessage(kMsgTypeFin, VoidCString())), + nsIEventTarget::DISPATCH_NORMAL); + } + + mStopped = true; + } + + nsresult rv; + if (code == CLOSE_GOING_AWAY) { + // Not an error: for example, tab has closed or navigated away + LOG(("WebSocketChannel::Close() GOING_AWAY without transport.")); + rv = NS_OK; + } else { + LOG(("WebSocketChannel::Close() without transport - error.")); + rv = NS_ERROR_NOT_CONNECTED; + } + + DoStopSession(rv); + return rv; +} + +NS_IMETHODIMP +WebSocketChannel::SendMsg(const nsACString& aMsg) { + LOG(("WebSocketChannel::SendMsg() %p\n", this)); + + return SendMsgCommon(aMsg, false, aMsg.Length()); +} + +NS_IMETHODIMP +WebSocketChannel::SendBinaryMsg(const nsACString& aMsg) { + LOG(("WebSocketChannel::SendBinaryMsg() %p len=%d\n", this, aMsg.Length())); + return SendMsgCommon(aMsg, true, aMsg.Length()); +} + +NS_IMETHODIMP +WebSocketChannel::SendBinaryStream(nsIInputStream* aStream, uint32_t aLength) { + LOG(("WebSocketChannel::SendBinaryStream() %p\n", this)); + + return SendMsgCommon(VoidCString(), true, aLength, aStream); +} + +nsresult WebSocketChannel::SendMsgCommon(const nsACString& aMsg, bool aIsBinary, + uint32_t aLength, + nsIInputStream* aStream) { + MOZ_ASSERT(IsOnTargetThread(), "not target thread"); + + if (!mDataStarted) { + LOG(("WebSocketChannel:: Error: data not started yet\n")); + return NS_ERROR_UNEXPECTED; + } + + if (mRequestedClose) { + LOG(("WebSocketChannel:: Error: send when closed\n")); + return NS_ERROR_UNEXPECTED; + } + + if (mStopped) { + LOG(("WebSocketChannel:: Error: send when stopped\n")); + return NS_ERROR_NOT_CONNECTED; + } + + MOZ_ASSERT(mMaxMessageSize >= 0, "max message size negative"); + if (aLength > static_cast(mMaxMessageSize)) { + LOG(("WebSocketChannel:: Error: message too big\n")); + return NS_ERROR_FILE_TOO_BIG; + } + + if (mConnectionLogService && !mPrivateBrowsing) { + mConnectionLogService->NewMsgSent(mHost, mSerial, aLength); + LOG(("Added new msg sent for %s", mHost.get())); + } + + return mSocketThread->Dispatch( + aStream + ? new OutboundEnqueuer(this, new OutboundMessage(aStream, aLength)) + : new OutboundEnqueuer( + this, + new OutboundMessage( + aIsBinary ? kMsgTypeBinaryString : kMsgTypeString, 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, aSocketIn, aSocketOut)); + } + + LOG(("WebSocketChannel::OnTransportAvailable %p [%p %p %p] rcvdonstart=%d\n", + this, aTransport, aSocketIn, aSocketOut, mGotUpgradeOK)); + + if (mStopped) { + LOG(("WebSocketChannel::OnTransportAvailable: Already stopped")); + return NS_OK; + } + + MOZ_ASSERT(NS_IsMainThread(), "not main thread"); + MOZ_ASSERT(!mRecvdHttpUpgradeTransport, "OTA duplicated"); + MOZ_ASSERT(aSocketIn, "OTA with invalid socketIn"); + + mTransport = aTransport; + mSocketIn = aSocketIn; + mSocketOut = aSocketOut; + + nsresult rv; + rv = mTransport->SetEventSink(nullptr, nullptr); + if (NS_FAILED(rv)) return rv; + rv = mTransport->SetSecurityCallbacks(this); + if (NS_FAILED(rv)) return rv; + + mRecvdHttpUpgradeTransport = 1; + if (mGotUpgradeOK) { + // We're now done CONNECTING, which means we can now open another, + // perhaps parallel, connection to the same host if one + // is pending + nsWSAdmissionManager::OnConnected(this); + + return CallStartWebsocketData(); + } + + if (mIsServerSide) { + if (!mNegotiatedExtensions.IsEmpty()) { + bool clientNoContextTakeover; + bool serverNoContextTakeover; + int32_t clientMaxWindowBits; + int32_t serverMaxWindowBits; + + rv = ParseWebSocketExtension( + mNegotiatedExtensions, eParseServerSide, clientNoContextTakeover, + serverNoContextTakeover, clientMaxWindowBits, serverMaxWindowBits); + MOZ_RELEASE_ASSERT(NS_SUCCEEDED(rv), "illegal value provided by server"); + + if (clientMaxWindowBits == -1) { + clientMaxWindowBits = 15; + } + if (serverMaxWindowBits == -1) { + serverMaxWindowBits = 15; + } + + mPMCECompressor = MakeUnique( + serverNoContextTakeover, serverMaxWindowBits, clientMaxWindowBits); + if (mPMCECompressor->Active()) { + LOG( + ("WebSocketChannel::OnTransportAvailable: PMCE negotiated, %susing " + "context takeover, serverMaxWindowBits=%d, " + "clientMaxWindowBits=%d\n", + serverNoContextTakeover ? "NOT " : "", serverMaxWindowBits, + clientMaxWindowBits)); + + mNegotiatedExtensions = "permessage-deflate"; + } else { + LOG( + ("WebSocketChannel::OnTransportAvailable: Cannot init PMCE " + "compression object\n")); + mPMCECompressor = nullptr; + AbortSession(NS_ERROR_UNEXPECTED); + return NS_ERROR_UNEXPECTED; + } + } + + return CallStartWebsocketData(); + } + + return NS_OK; +} + +NS_IMETHODIMP +WebSocketChannel::OnUpgradeFailed(nsresult aErrorCode) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + LOG(("WebSocketChannel::OnUpgradeFailed() %p [aErrorCode %" PRIx32 "]", this, + static_cast(aErrorCode))); + + if (mStopped) { + LOG(("WebSocketChannel::OnUpgradeFailed: Already stopped")); + return NS_OK; + } + + MOZ_ASSERT(!mRecvdHttpUpgradeTransport, "OTA already called"); + + AbortSession(aErrorCode); + return NS_OK; +} + +// nsIRequestObserver (from nsIStreamListener) + +NS_IMETHODIMP +WebSocketChannel::OnStartRequest(nsIRequest* aRequest) { + LOG(("WebSocketChannel::OnStartRequest(): %p [%p %p] recvdhttpupgrade=%d\n", + this, aRequest, mHttpChannel.get(), mRecvdHttpUpgradeTransport)); + MOZ_ASSERT(NS_IsMainThread(), "not main thread"); + MOZ_ASSERT(!mGotUpgradeOK, "OTA duplicated"); + + if (mStopped) { + LOG(("WebSocketChannel::OnStartRequest: Channel Already Done\n")); + AbortSession(NS_ERROR_CONNECTION_REFUSED); + return NS_ERROR_CONNECTION_REFUSED; + } + + nsresult rv; + uint32_t status; + char *val, *token; + + rv = mHttpChannel->GetResponseStatus(&status); + if (NS_FAILED(rv)) { + nsresult httpStatus; + rv = NS_ERROR_CONNECTION_REFUSED; + + // If we failed to connect due to unsuccessful TLS handshake, we must + // propagate a specific error to mozilla::dom::WebSocketImpl so it can set + // status code to 1015. Otherwise return NS_ERROR_CONNECTION_REFUSED. + if (NS_SUCCEEDED(mHttpChannel->GetStatus(&httpStatus))) { + uint32_t errorClass; + nsCOMPtr errSvc = + do_GetService("@mozilla.org/nss_errors_service;1"); + // If GetErrorClass succeeds httpStatus is TLS related failure. + if (errSvc && + NS_SUCCEEDED(errSvc->GetErrorClass(httpStatus, &errorClass))) { + rv = NS_ERROR_NET_INADEQUATE_SECURITY; + } + } + + LOG(("WebSocketChannel::OnStartRequest: No HTTP Response\n")); + AbortSession(rv); + return rv; + } + + LOG(("WebSocketChannel::OnStartRequest: HTTP status %d\n", status)); + nsCOMPtr internalChannel = + do_QueryInterface(mHttpChannel); + uint32_t versionMajor, versionMinor; + rv = internalChannel->GetResponseVersion(&versionMajor, &versionMinor); + if (NS_FAILED(rv) || + !((versionMajor == 1 && versionMinor != 0) || versionMajor == 2) || + (versionMajor == 1 && status != 101) || + (versionMajor == 2 && status != 200)) { + AbortSession(NS_ERROR_CONNECTION_REFUSED); + return NS_ERROR_CONNECTION_REFUSED; + } + + if (versionMajor == 1) { + // These are only present on http/1.x websocket upgrades + nsAutoCString respUpgrade; + rv = mHttpChannel->GetResponseHeader("Upgrade"_ns, respUpgrade); + + if (NS_SUCCEEDED(rv)) { + rv = NS_ERROR_ILLEGAL_VALUE; + if (!respUpgrade.IsEmpty()) { + val = respUpgrade.BeginWriting(); + while ((token = nsCRT::strtok(val, ", \t", &val))) { + if (PL_strcasecmp(token, "Websocket") == 0) { + rv = NS_OK; + break; + } + } + } + } + + if (NS_FAILED(rv)) { + LOG( + ("WebSocketChannel::OnStartRequest: " + "HTTP response header Upgrade: websocket not found\n")); + AbortSession(NS_ERROR_ILLEGAL_VALUE); + return rv; + } + + nsAutoCString respConnection; + rv = mHttpChannel->GetResponseHeader("Connection"_ns, respConnection); + + if (NS_SUCCEEDED(rv)) { + rv = NS_ERROR_ILLEGAL_VALUE; + if (!respConnection.IsEmpty()) { + val = respConnection.BeginWriting(); + while ((token = nsCRT::strtok(val, ", \t", &val))) { + if (PL_strcasecmp(token, "Upgrade") == 0) { + rv = NS_OK; + break; + } + } + } + } + + if (NS_FAILED(rv)) { + LOG( + ("WebSocketChannel::OnStartRequest: " + "HTTP response header 'Connection: Upgrade' not found\n")); + AbortSession(NS_ERROR_ILLEGAL_VALUE); + return rv; + } + + nsAutoCString respAccept; + rv = mHttpChannel->GetResponseHeader("Sec-WebSocket-Accept"_ns, respAccept); + + if (NS_FAILED(rv) || respAccept.IsEmpty() || + !respAccept.Equals(mHashedSecret)) { + LOG( + ("WebSocketChannel::OnStartRequest: " + "HTTP response header Sec-WebSocket-Accept check failed\n")); + LOG(("WebSocketChannel::OnStartRequest: Expected %s received %s\n", + mHashedSecret.get(), respAccept.get())); +#ifdef FUZZING + if (NS_FAILED(rv) || respAccept.IsEmpty()) { +#endif + AbortSession(NS_ERROR_ILLEGAL_VALUE); + return NS_ERROR_ILLEGAL_VALUE; +#ifdef FUZZING + } +#endif + } + } + + // If we sent a sub protocol header, verify the response matches. + // If response contains protocol that was not in request, fail. + // If response contained no protocol header, set to "" so the protocol + // attribute of the WebSocket JS object reflects that + if (!mProtocol.IsEmpty()) { + nsAutoCString respProtocol; + rv = mHttpChannel->GetResponseHeader("Sec-WebSocket-Protocol"_ns, + respProtocol); + if (NS_SUCCEEDED(rv)) { + rv = NS_ERROR_ILLEGAL_VALUE; + val = mProtocol.BeginWriting(); + while ((token = nsCRT::strtok(val, ", \t", &val))) { + if (PL_strcmp(token, respProtocol.get()) == 0) { + rv = NS_OK; + break; + } + } + + if (NS_SUCCEEDED(rv)) { + LOG(("WebsocketChannel::OnStartRequest: subprotocol %s confirmed", + respProtocol.get())); + mProtocol = respProtocol; + } else { + LOG( + ("WebsocketChannel::OnStartRequest: " + "Server replied with non-matching subprotocol [%s]: aborting", + respProtocol.get())); + mProtocol.Truncate(); + AbortSession(NS_ERROR_ILLEGAL_VALUE); + return NS_ERROR_ILLEGAL_VALUE; + } + } else { + LOG( + ("WebsocketChannel::OnStartRequest " + "subprotocol [%s] not found - none returned", + mProtocol.get())); + mProtocol.Truncate(); + } + } + + rv = HandleExtensions(); + if (NS_FAILED(rv)) return rv; + + // Update mEffectiveURL for off main thread URI access. + nsCOMPtr uri = mURI ? mURI : mOriginalURI; + nsAutoCString spec; + rv = uri->GetSpec(spec); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + CopyUTF8toUTF16(spec, mEffectiveURL); + + mGotUpgradeOK = 1; + if (mRecvdHttpUpgradeTransport) { + // We're now done CONNECTING, which means we can now open another, + // perhaps parallel, connection to the same host if one + // is pending + nsWSAdmissionManager::OnConnected(this); + + return CallStartWebsocketData(); + } + + return NS_OK; +} + +NS_IMETHODIMP +WebSocketChannel::OnStopRequest(nsIRequest* aRequest, nsresult aStatusCode) { + LOG(("WebSocketChannel::OnStopRequest() %p [%p %p %" PRIx32 "]\n", this, + aRequest, mHttpChannel.get(), static_cast(aStatusCode))); + MOZ_ASSERT(NS_IsMainThread(), "not main thread"); + + // OnTransportAvailable won't be called if the request is stopped with + // an error. Abort the session now instead of waiting for timeout. + if (NS_FAILED(aStatusCode) && !mRecvdHttpUpgradeTransport) { + AbortSession(aStatusCode); + } + + ReportConnectionTelemetry(aStatusCode); + + // This is the end of the HTTP upgrade transaction, the + // upgraded streams live on + + mChannel = nullptr; + mHttpChannel = nullptr; + mLoadGroup = nullptr; + mCallbacks = nullptr; + + return NS_OK; +} + +// nsIInputStreamCallback + +NS_IMETHODIMP +WebSocketChannel::OnInputStreamReady(nsIAsyncInputStream* aStream) { + LOG(("WebSocketChannel::OnInputStreamReady() %p\n", this)); + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + if (!mSocketIn) // did we we clean up the socket after scheduling InputReady? + return NS_OK; + + // this is after the http upgrade - so we are speaking websockets + char buffer[2048]; + uint32_t count; + nsresult rv; + + do { + rv = mSocketIn->Read((char*)buffer, 2048, &count); + LOG(("WebSocketChannel::OnInputStreamReady: read %u rv %" PRIx32 "\n", + count, static_cast(rv))); + + if (rv == NS_BASE_STREAM_WOULD_BLOCK) { + mSocketIn->AsyncWait(this, 0, 0, mSocketThread); + return NS_OK; + } + + if (NS_FAILED(rv)) { + AbortSession(rv); + return rv; + } + + if (count == 0) { + AbortSession(NS_BASE_STREAM_CLOSED); + return NS_OK; + } + + if (mStopped) { + continue; + } + + rv = ProcessInput((uint8_t*)buffer, count); + if (NS_FAILED(rv)) { + AbortSession(rv); + return rv; + } + } while (NS_SUCCEEDED(rv) && mSocketIn); + + return NS_OK; +} + +// nsIOutputStreamCallback + +NS_IMETHODIMP +WebSocketChannel::OnOutputStreamReady(nsIAsyncOutputStream* aStream) { + LOG(("WebSocketChannel::OnOutputStreamReady() %p\n", this)); + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + nsresult rv; + + if (!mCurrentOut) PrimeNewOutgoingMessage(); + + while (mCurrentOut && mSocketOut) { + const char* sndBuf; + uint32_t toSend; + uint32_t amtSent; + + if (mHdrOut) { + sndBuf = (const char*)mHdrOut; + toSend = mHdrOutToSend; + LOG( + ("WebSocketChannel::OnOutputStreamReady: " + "Try to send %u of hdr/copybreak\n", + toSend)); + } else { + sndBuf = (char*)mCurrentOut->BeginReading() + mCurrentOutSent; + toSend = mCurrentOut->Length() - mCurrentOutSent; + if (toSend > 0) { + LOG( + ("WebSocketChannel::OnOutputStreamReady: " + "Try to send %u of data\n", + toSend)); + } + } + + if (toSend == 0) { + amtSent = 0; + } else { + rv = mSocketOut->Write(sndBuf, toSend, &amtSent); + LOG(("WebSocketChannel::OnOutputStreamReady: write %u rv %" PRIx32 "\n", + amtSent, static_cast(rv))); + + if (rv == NS_BASE_STREAM_WOULD_BLOCK) { + mSocketOut->AsyncWait(this, 0, 0, mSocketThread); + return NS_OK; + } + + if (NS_FAILED(rv)) { + AbortSession(rv); + return NS_OK; + } + } + + if (mHdrOut) { + if (amtSent == toSend) { + mHdrOut = nullptr; + mHdrOutToSend = 0; + } else { + mHdrOut += amtSent; + mHdrOutToSend -= amtSent; + mSocketOut->AsyncWait(this, 0, 0, mSocketThread); + } + } else { + if (amtSent == toSend) { + if (!mStopped) { + mTargetThread->Dispatch( + new CallAcknowledge(this, mCurrentOut->OrigLength()), + NS_DISPATCH_NORMAL); + } + DeleteCurrentOutGoingMessage(); + PrimeNewOutgoingMessage(); + } else { + mCurrentOutSent += amtSent; + mSocketOut->AsyncWait(this, 0, 0, mSocketThread); + } + } + } + + if (mReleaseOnTransmit) ReleaseSession(); + return NS_OK; +} + +// nsIStreamListener + +NS_IMETHODIMP +WebSocketChannel::OnDataAvailable(nsIRequest* aRequest, + nsIInputStream* aInputStream, + uint64_t aOffset, uint32_t aCount) { + LOG(("WebSocketChannel::OnDataAvailable() %p [%p %p %p %" PRIu64 " %u]\n", + this, aRequest, mHttpChannel.get(), aInputStream, aOffset, aCount)); + + // This is the HTTP OnDataAvailable Method, which means this is http data in + // response to the upgrade request and there should be no http response body + // if the upgrade succeeded. This generally should be caught by a non 101 + // response code in OnStartRequest().. so we can ignore the data here + + LOG(("WebSocketChannel::OnDataAvailable: HTTP data unexpected len>=%u\n", + aCount)); + + return NS_OK; +} + +} // namespace net +} // namespace mozilla + +#undef CLOSE_GOING_AWAY diff --git a/netwerk/protocol/websocket/WebSocketChannel.h b/netwerk/protocol/websocket/WebSocketChannel.h new file mode 100644 index 0000000000..950f76d0b1 --- /dev/null +++ b/netwerk/protocol/websocket/WebSocketChannel.h @@ -0,0 +1,315 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et 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/. */ + +#ifndef mozilla_net_WebSocketChannel_h +#define mozilla_net_WebSocketChannel_h + +#include "nsISupports.h" +#include "nsIInterfaceRequestor.h" +#include "nsIStreamListener.h" +#include "nsIAsyncInputStream.h" +#include "nsIAsyncOutputStream.h" +#include "nsITimer.h" +#include "nsIDNSListener.h" +#include "nsINamed.h" +#include "nsIObserver.h" +#include "nsIProtocolProxyCallback.h" +#include "nsIChannelEventSink.h" +#include "nsIHttpChannelInternal.h" +#include "BaseWebSocketChannel.h" + +#include "nsCOMPtr.h" +#include "nsString.h" +#include "nsDeque.h" +#include "mozilla/Atomics.h" + +class nsIAsyncVerifyRedirectCallback; +class nsIDashboardEventNotifier; +class nsIEventTarget; +class nsIHttpChannel; +class nsIRandomGenerator; +class nsISocketTransport; +class nsIURI; + +namespace mozilla { +namespace net { + +class OutboundMessage; +class OutboundEnqueuer; +class nsWSAdmissionManager; +class PMCECompression; +class CallOnMessageAvailable; +class CallOnStop; +class CallOnServerClose; +class CallAcknowledge; +class WebSocketEventService; + +[[nodiscard]] extern nsresult CalculateWebSocketHashedSecret( + const nsACString& aKey, nsACString& aHash); +extern void ProcessServerWebSocketExtensions(const nsACString& aExtensions, + nsACString& aNegotiatedExtensions); + +// Used to enforce "1 connecting websocket per host" rule, and reconnect delays +enum wsConnectingState { + NOT_CONNECTING = 0, // Not yet (or no longer) trying to open connection + CONNECTING_QUEUED, // Waiting for other ws to same host to finish opening + CONNECTING_DELAYED, // Delayed by "reconnect after failure" algorithm + CONNECTING_IN_PROGRESS // Started connection: waiting for result +}; + +class WebSocketChannel : public BaseWebSocketChannel, + public nsIHttpUpgradeListener, + public nsIStreamListener, + public nsIInputStreamCallback, + public nsIOutputStreamCallback, + public nsITimerCallback, + public nsIDNSListener, + public nsIObserver, + public nsIProtocolProxyCallback, + public nsIInterfaceRequestor, + public nsIChannelEventSink, + public nsINamed { + friend class WebSocketFrame; + + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIHTTPUPGRADELISTENER + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMLISTENER + NS_DECL_NSIINPUTSTREAMCALLBACK + NS_DECL_NSIOUTPUTSTREAMCALLBACK + NS_DECL_NSITIMERCALLBACK + NS_DECL_NSIDNSLISTENER + NS_DECL_NSIPROTOCOLPROXYCALLBACK + NS_DECL_NSIINTERFACEREQUESTOR + NS_DECL_NSICHANNELEVENTSINK + NS_DECL_NSIOBSERVER + NS_DECL_NSINAMED + + // nsIWebSocketChannel methods BaseWebSocketChannel didn't implement for us + // + NS_IMETHOD AsyncOpen(nsIURI* aURI, const nsACString& aOrigin, + uint64_t aWindowID, nsIWebSocketListener* aListener, + nsISupports* aContext) override; + NS_IMETHOD Close(uint16_t aCode, const nsACString& aReason) override; + NS_IMETHOD SendMsg(const nsACString& aMsg) override; + NS_IMETHOD SendBinaryMsg(const nsACString& aMsg) override; + NS_IMETHOD SendBinaryStream(nsIInputStream* aStream, + uint32_t length) override; + NS_IMETHOD GetSecurityInfo(nsISupports** aSecurityInfo) override; + + WebSocketChannel(); + static void Shutdown(); + bool IsOnTargetThread(); + + // Off main thread URI access. + void GetEffectiveURL(nsAString& aEffectiveURL) const override; + bool IsEncrypted() const override; + + const static uint32_t kControlFrameMask = 0x8; + + // First byte of the header + const static uint8_t kFinalFragBit = 0x80; + const static uint8_t kRsvBitsMask = 0x70; + const static uint8_t kRsv1Bit = 0x40; + const static uint8_t kRsv2Bit = 0x20; + const static uint8_t kRsv3Bit = 0x10; + const static uint8_t kOpcodeBitsMask = 0x0F; + + // Second byte of the header + const static uint8_t kMaskBit = 0x80; + const static uint8_t kPayloadLengthBitsMask = 0x7F; + + protected: + virtual ~WebSocketChannel(); + + private: + friend class OutboundEnqueuer; + friend class nsWSAdmissionManager; + friend class FailDelayManager; + friend class CallOnMessageAvailable; + friend class CallOnStop; + friend class CallOnServerClose; + friend class CallAcknowledge; + + // Common send code for binary + text msgs + [[nodiscard]] nsresult SendMsgCommon(const nsACString& aMsg, bool isBinary, + uint32_t length, + nsIInputStream* aStream = nullptr); + + void EnqueueOutgoingMessage(nsDeque& aQueue, + OutboundMessage* aMsg); + + void PrimeNewOutgoingMessage(); + void DeleteCurrentOutGoingMessage(); + void GeneratePong(uint8_t* payload, uint32_t len); + void GeneratePing(); + + [[nodiscard]] nsresult OnNetworkChanged(); + [[nodiscard]] nsresult StartPinging(); + + void BeginOpen(bool aCalledFromAdmissionManager); + void BeginOpenInternal(); + [[nodiscard]] nsresult HandleExtensions(); + [[nodiscard]] nsresult SetupRequest(); + [[nodiscard]] nsresult ApplyForAdmission(); + [[nodiscard]] nsresult DoAdmissionDNS(); + [[nodiscard]] nsresult CallStartWebsocketData(); + [[nodiscard]] nsresult StartWebsocketData(); + uint16_t ResultToCloseCode(nsresult resultCode); + void ReportConnectionTelemetry(nsresult aStatusCode); + + void StopSession(nsresult reason); + void DoStopSession(nsresult reason); + void AbortSession(nsresult reason); + void ReleaseSession(); + void CleanupConnection(); + void IncrementSessionCount(); + void DecrementSessionCount(); + + void EnsureHdrOut(uint32_t size); + + static void ApplyMask(uint32_t mask, uint8_t* data, uint64_t len); + + bool IsPersistentFramePtr(); + [[nodiscard]] nsresult ProcessInput(uint8_t* buffer, uint32_t count); + [[nodiscard]] bool UpdateReadBuffer(uint8_t* buffer, uint32_t count, + uint32_t accumulatedFragments, + uint32_t* available); + + inline void ResetPingTimer() { + mPingOutstanding = 0; + if (mPingTimer) { + if (!mPingInterval) { + // The timer was created by forced ping and regular pinging is disabled, + // so cancel and null out mPingTimer. + mPingTimer->Cancel(); + mPingTimer = nullptr; + } else { + mPingTimer->SetDelay(mPingInterval); + } + } + } + + nsCOMPtr mSocketThread; + nsCOMPtr mChannel; + nsCOMPtr mHttpChannel; + nsCOMPtr mCancelable; + nsCOMPtr mRedirectCallback; + nsCOMPtr mRandomGenerator; + + nsCString mHashedSecret; + + // Used as key for connection managment: Initially set to hostname from URI, + // then to IP address (unless we're leaving DNS resolution to a proxy server) + nsCString mAddress; + int32_t mPort; // WS server port + + // Used for off main thread access to the URI string. + nsCString mHost; + nsString mEffectiveURL; + + nsCOMPtr mTransport; + nsCOMPtr mSocketIn; + nsCOMPtr mSocketOut; + + nsCOMPtr mCloseTimer; + uint32_t mCloseTimeout; /* milliseconds */ + + nsCOMPtr mOpenTimer; + uint32_t mOpenTimeout; /* milliseconds */ + wsConnectingState mConnecting; /* 0 if not connecting */ + nsCOMPtr mReconnectDelayTimer; + + nsCOMPtr mPingTimer; + + nsCOMPtr mLingeringCloseTimer; + const static int32_t kLingeringCloseTimeout = 1000; + const static int32_t kLingeringCloseThreshold = 50; + + RefPtr mService; + + int32_t mMaxConcurrentConnections; + + uint64_t mInnerWindowID; + + // following members are accessed only on the main thread + uint32_t mGotUpgradeOK : 1; + uint32_t mRecvdHttpUpgradeTransport : 1; + uint32_t mAutoFollowRedirects : 1; + uint32_t mAllowPMCE : 1; + uint32_t : 0; + + // following members are accessed only on the socket thread + uint32_t mPingOutstanding : 1; + uint32_t mReleaseOnTransmit : 1; + uint32_t : 0; + + Atomic mDataStarted; + Atomic mRequestedClose; + Atomic mClientClosed; + Atomic mServerClosed; + Atomic mStopped; + Atomic mCalledOnStop; + Atomic mTCPClosed; + Atomic mOpenedHttpChannel; + Atomic mIncrementedSessionCount; + Atomic mDecrementedSessionCount; + + int32_t mMaxMessageSize; + nsresult mStopOnClose; + uint16_t mServerCloseCode; + nsCString mServerCloseReason; + uint16_t mScriptCloseCode; + nsCString mScriptCloseReason; + + // These are for the read buffers + const static uint32_t kIncomingBufferInitialSize = 16 * 1024; + // We're ok with keeping a buffer this size or smaller around for the life of + // the websocket. If a particular message needs bigger than this we'll + // increase the buffer temporarily, then drop back down to this size. + const static uint32_t kIncomingBufferStableSize = 128 * 1024; + + uint8_t* mFramePtr; + uint8_t* mBuffer; + uint8_t mFragmentOpcode; + uint32_t mFragmentAccumulator; + uint32_t mBuffered; + uint32_t mBufferSize; + + // These are for the send buffers + const static int32_t kCopyBreak = 1000; + + OutboundMessage* mCurrentOut; + uint32_t mCurrentOutSent; + nsDeque mOutgoingMessages; + nsDeque mOutgoingPingMessages; + nsDeque mOutgoingPongMessages; + uint32_t mHdrOutToSend; + uint8_t* mHdrOut; + uint8_t mOutHeader[kCopyBreak + 16]; + UniquePtr mPMCECompressor; + uint32_t mDynamicOutputSize; + uint8_t* mDynamicOutput; + bool mPrivateBrowsing; + + nsCOMPtr mConnectionLogService; + + mozilla::Mutex mMutex; +}; + +class WebSocketSSLChannel : public WebSocketChannel { + public: + WebSocketSSLChannel() { BaseWebSocketChannel::mEncrypted = true; } + + protected: + virtual ~WebSocketSSLChannel() = default; +}; + +} // namespace net +} // namespace mozilla + +#endif // mozilla_net_WebSocketChannel_h diff --git a/netwerk/protocol/websocket/WebSocketChannelChild.cpp b/netwerk/protocol/websocket/WebSocketChannelChild.cpp new file mode 100644 index 0000000000..ae9da1028f --- /dev/null +++ b/netwerk/protocol/websocket/WebSocketChannelChild.cpp @@ -0,0 +1,729 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et 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 "WebSocketLog.h" +#include "base/compiler_specific.h" +#include "mozilla/dom/BrowserChild.h" +#include "mozilla/net/NeckoChild.h" +#include "WebSocketChannelChild.h" +#include "nsContentUtils.h" +#include "nsIBrowserChild.h" +#include "nsNetUtil.h" +#include "mozilla/ipc/IPCStreamUtils.h" +#include "mozilla/ipc/URIUtils.h" +#include "mozilla/ipc/BackgroundUtils.h" +#include "mozilla/net/ChannelEventQueue.h" +#include "SerializedLoadContext.h" + +using namespace mozilla::ipc; + +namespace mozilla { +namespace net { + +NS_IMPL_ADDREF(WebSocketChannelChild) + +NS_IMETHODIMP_(MozExternalRefCountType) WebSocketChannelChild::Release() { + MOZ_ASSERT(0 != mRefCnt, "dup release"); + --mRefCnt; + NS_LOG_RELEASE(this, mRefCnt, "WebSocketChannelChild"); + + if (mRefCnt == 1) { + MaybeReleaseIPCObject(); + return mRefCnt; + } + + if (mRefCnt == 0) { + mRefCnt = 1; /* stabilize */ + delete this; + return 0; + } + return mRefCnt; +} + +NS_INTERFACE_MAP_BEGIN(WebSocketChannelChild) + NS_INTERFACE_MAP_ENTRY(nsIWebSocketChannel) + NS_INTERFACE_MAP_ENTRY(nsIProtocolHandler) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIWebSocketChannel) + NS_INTERFACE_MAP_ENTRY(nsIThreadRetargetableRequest) +NS_INTERFACE_MAP_END + +WebSocketChannelChild::WebSocketChannelChild(bool aEncrypted) + : NeckoTargetHolder(nullptr), + mIPCState(Closed), + mMutex("WebSocketChannelChild::mMutex") { + MOZ_ASSERT(NS_IsMainThread(), "not main thread"); + + LOG(("WebSocketChannelChild::WebSocketChannelChild() %p\n", this)); + mEncrypted = aEncrypted; + mEventQ = new ChannelEventQueue(static_cast(this)); +} + +WebSocketChannelChild::~WebSocketChannelChild() { + LOG(("WebSocketChannelChild::~WebSocketChannelChild() %p\n", this)); +} + +void WebSocketChannelChild::AddIPDLReference() { + MOZ_ASSERT(NS_IsMainThread()); + + { + MutexAutoLock lock(mMutex); + MOZ_ASSERT(mIPCState == Closed, + "Attempt to retain more than one IPDL reference"); + mIPCState = Opened; + } + + AddRef(); +} + +void WebSocketChannelChild::ReleaseIPDLReference() { + MOZ_ASSERT(NS_IsMainThread()); + + { + MutexAutoLock lock(mMutex); + MOZ_ASSERT(mIPCState != Closed, + "Attempt to release nonexistent IPDL reference"); + mIPCState = Closed; + } + + Release(); +} + +void WebSocketChannelChild::MaybeReleaseIPCObject() { + { + MutexAutoLock lock(mMutex); + if (mIPCState != Opened) { + return; + } + + mIPCState = Closing; + } + + if (!NS_IsMainThread()) { + nsCOMPtr target = GetNeckoTarget(); + MOZ_ALWAYS_SUCCEEDS(target->Dispatch( + NewRunnableMethod("WebSocketChannelChild::MaybeReleaseIPCObject", this, + &WebSocketChannelChild::MaybeReleaseIPCObject), + NS_DISPATCH_NORMAL)); + return; + } + + SendDeleteSelf(); +} + +void WebSocketChannelChild::GetEffectiveURL(nsAString& aEffectiveURL) const { + aEffectiveURL = mEffectiveURL; +} + +bool WebSocketChannelChild::IsEncrypted() const { return mEncrypted; } + +class WebSocketEvent { + public: + MOZ_COUNTED_DEFAULT_CTOR(WebSocketEvent) + MOZ_COUNTED_DTOR_VIRTUAL(WebSocketEvent) + virtual void Run(WebSocketChannelChild* aChild) = 0; +}; + +class WrappedWebSocketEvent : public Runnable { + public: + WrappedWebSocketEvent(WebSocketChannelChild* aChild, + UniquePtr&& aWebSocketEvent) + : Runnable("net::WrappedWebSocketEvent"), + mChild(aChild), + mWebSocketEvent(std::move(aWebSocketEvent)) { + MOZ_RELEASE_ASSERT(!!mWebSocketEvent); + } + NS_IMETHOD Run() override { + mWebSocketEvent->Run(mChild); + return NS_OK; + } + + private: + RefPtr mChild; + UniquePtr mWebSocketEvent; +}; + +class EventTargetDispatcher : public ChannelEvent { + public: + EventTargetDispatcher(WebSocketChannelChild* aChild, + WebSocketEvent* aWebSocketEvent, + nsIEventTarget* aEventTarget) + : mChild(aChild), + mWebSocketEvent(aWebSocketEvent), + mEventTarget(aEventTarget) {} + + void Run() override { + if (mEventTarget) { + mEventTarget->Dispatch( + new WrappedWebSocketEvent(mChild, std::move(mWebSocketEvent)), + NS_DISPATCH_NORMAL); + return; + } + + mWebSocketEvent->Run(mChild); + } + + already_AddRefed GetEventTarget() override { + nsCOMPtr target = mEventTarget; + if (!target) { + target = GetMainThreadEventTarget(); + } + return target.forget(); + } + + private: + // The lifetime of the child is ensured by ChannelEventQueue. + WebSocketChannelChild* mChild; + UniquePtr mWebSocketEvent; + nsCOMPtr mEventTarget; +}; + +class StartEvent : public WebSocketEvent { + public: + StartEvent(const nsCString& aProtocol, const nsCString& aExtensions, + const nsString& aEffectiveURL, bool aEncrypted, + uint64_t aHttpChannelId) + : mProtocol(aProtocol), + mExtensions(aExtensions), + mEffectiveURL(aEffectiveURL), + mEncrypted(aEncrypted), + mHttpChannelId(aHttpChannelId) {} + + void Run(WebSocketChannelChild* aChild) override { + aChild->OnStart(mProtocol, mExtensions, mEffectiveURL, mEncrypted, + mHttpChannelId); + } + + private: + nsCString mProtocol; + nsCString mExtensions; + nsString mEffectiveURL; + bool mEncrypted; + uint64_t mHttpChannelId; +}; + +mozilla::ipc::IPCResult WebSocketChannelChild::RecvOnStart( + const nsCString& aProtocol, const nsCString& aExtensions, + const nsString& aEffectiveURL, const bool& aEncrypted, + const uint64_t& aHttpChannelId) { + mEventQ->RunOrEnqueue(new EventTargetDispatcher( + this, + new StartEvent(aProtocol, aExtensions, aEffectiveURL, aEncrypted, + aHttpChannelId), + mTargetThread)); + + return IPC_OK(); +} + +void WebSocketChannelChild::OnStart(const nsCString& aProtocol, + const nsCString& aExtensions, + const nsString& aEffectiveURL, + const bool& aEncrypted, + const uint64_t& aHttpChannelId) { + LOG(("WebSocketChannelChild::RecvOnStart() %p\n", this)); + SetProtocol(aProtocol); + mNegotiatedExtensions = aExtensions; + mEffectiveURL = aEffectiveURL; + mEncrypted = aEncrypted; + mHttpChannelId = aHttpChannelId; + + if (mListenerMT) { + AutoEventEnqueuer ensureSerialDispatch(mEventQ); + nsresult rv = mListenerMT->mListener->OnStart(mListenerMT->mContext); + if (NS_FAILED(rv)) { + LOG( + ("WebSocketChannelChild::OnStart " + "mListenerMT->mListener->OnStart() failed with error 0x%08" PRIx32, + static_cast(rv))); + } + } +} + +class StopEvent : public WebSocketEvent { + public: + explicit StopEvent(const nsresult& aStatusCode) : mStatusCode(aStatusCode) {} + + void Run(WebSocketChannelChild* aChild) override { + aChild->OnStop(mStatusCode); + } + + private: + nsresult mStatusCode; +}; + +mozilla::ipc::IPCResult WebSocketChannelChild::RecvOnStop( + const nsresult& aStatusCode) { + mEventQ->RunOrEnqueue(new EventTargetDispatcher( + this, new StopEvent(aStatusCode), mTargetThread)); + + return IPC_OK(); +} + +void WebSocketChannelChild::OnStop(const nsresult& aStatusCode) { + LOG(("WebSocketChannelChild::RecvOnStop() %p\n", this)); + if (mListenerMT) { + AutoEventEnqueuer ensureSerialDispatch(mEventQ); + nsresult rv = + mListenerMT->mListener->OnStop(mListenerMT->mContext, aStatusCode); + if (NS_FAILED(rv)) { + LOG( + ("WebSocketChannel::OnStop " + "mListenerMT->mListener->OnStop() failed with error 0x%08" PRIx32, + static_cast(rv))); + } + } +} + +class MessageEvent : public WebSocketEvent { + public: + MessageEvent(const nsCString& aMessage, bool aBinary) + : mMessage(aMessage), mBinary(aBinary) {} + + void Run(WebSocketChannelChild* aChild) override { + if (!mBinary) { + aChild->OnMessageAvailable(mMessage); + } else { + aChild->OnBinaryMessageAvailable(mMessage); + } + } + + private: + nsCString mMessage; + bool mBinary; +}; + +bool WebSocketChannelChild::RecvOnMessageAvailableInternal( + const nsDependentCSubstring& aMsg, bool aMoreData, bool aBinary) { + if (aMoreData) { + return mReceivedMsgBuffer.Append(aMsg, fallible); + } + + if (!mReceivedMsgBuffer.Append(aMsg, fallible)) { + return false; + } + + mEventQ->RunOrEnqueue(new EventTargetDispatcher( + this, new MessageEvent(mReceivedMsgBuffer, aBinary), mTargetThread)); + mReceivedMsgBuffer.Truncate(); + return true; +} + +class OnErrorEvent : public WebSocketEvent { + public: + OnErrorEvent() = default; + + void Run(WebSocketChannelChild* aChild) override { aChild->OnError(); } +}; + +void WebSocketChannelChild::OnError() { + LOG(("WebSocketChannelChild::OnError() %p", this)); + if (mListenerMT) { + AutoEventEnqueuer ensureSerialDispatch(mEventQ); + Unused << mListenerMT->mListener->OnError(); + } +} + +mozilla::ipc::IPCResult WebSocketChannelChild::RecvOnMessageAvailable( + const nsDependentCSubstring& aMsg, const bool& aMoreData) { + if (!RecvOnMessageAvailableInternal(aMsg, aMoreData, false)) { + LOG(("WebSocketChannelChild %p append message failed", this)); + mEventQ->RunOrEnqueue( + new EventTargetDispatcher(this, new OnErrorEvent(), mTargetThread)); + } + return IPC_OK(); +} + +void WebSocketChannelChild::OnMessageAvailable(const nsCString& aMsg) { + LOG(("WebSocketChannelChild::RecvOnMessageAvailable() %p\n", this)); + if (mListenerMT) { + AutoEventEnqueuer ensureSerialDispatch(mEventQ); + nsresult rv = + mListenerMT->mListener->OnMessageAvailable(mListenerMT->mContext, aMsg); + if (NS_FAILED(rv)) { + LOG( + ("WebSocketChannelChild::OnMessageAvailable " + "mListenerMT->mListener->OnMessageAvailable() " + "failed with error 0x%08" PRIx32, + static_cast(rv))); + } + } +} + +mozilla::ipc::IPCResult WebSocketChannelChild::RecvOnBinaryMessageAvailable( + const nsDependentCSubstring& aMsg, const bool& aMoreData) { + if (!RecvOnMessageAvailableInternal(aMsg, aMoreData, true)) { + LOG(("WebSocketChannelChild %p append message failed", this)); + mEventQ->RunOrEnqueue( + new EventTargetDispatcher(this, new OnErrorEvent(), mTargetThread)); + } + return IPC_OK(); +} + +void WebSocketChannelChild::OnBinaryMessageAvailable(const nsCString& aMsg) { + LOG(("WebSocketChannelChild::RecvOnBinaryMessageAvailable() %p\n", this)); + if (mListenerMT) { + AutoEventEnqueuer ensureSerialDispatch(mEventQ); + nsresult rv = mListenerMT->mListener->OnBinaryMessageAvailable( + mListenerMT->mContext, aMsg); + if (NS_FAILED(rv)) { + LOG( + ("WebSocketChannelChild::OnBinaryMessageAvailable " + "mListenerMT->mListener->OnBinaryMessageAvailable() " + "failed with error 0x%08" PRIx32, + static_cast(rv))); + } + } +} + +class AcknowledgeEvent : public WebSocketEvent { + public: + explicit AcknowledgeEvent(const uint32_t& aSize) : mSize(aSize) {} + + void Run(WebSocketChannelChild* aChild) override { + aChild->OnAcknowledge(mSize); + } + + private: + uint32_t mSize; +}; + +mozilla::ipc::IPCResult WebSocketChannelChild::RecvOnAcknowledge( + const uint32_t& aSize) { + mEventQ->RunOrEnqueue(new EventTargetDispatcher( + this, new AcknowledgeEvent(aSize), mTargetThread)); + + return IPC_OK(); +} + +void WebSocketChannelChild::OnAcknowledge(const uint32_t& aSize) { + LOG(("WebSocketChannelChild::RecvOnAcknowledge() %p\n", this)); + if (mListenerMT) { + AutoEventEnqueuer ensureSerialDispatch(mEventQ); + nsresult rv = + mListenerMT->mListener->OnAcknowledge(mListenerMT->mContext, aSize); + if (NS_FAILED(rv)) { + LOG( + ("WebSocketChannel::OnAcknowledge " + "mListenerMT->mListener->OnAcknowledge() " + "failed with error 0x%08" PRIx32, + static_cast(rv))); + } + } +} + +class ServerCloseEvent : public WebSocketEvent { + public: + ServerCloseEvent(const uint16_t aCode, const nsCString& aReason) + : mCode(aCode), mReason(aReason) {} + + void Run(WebSocketChannelChild* aChild) override { + aChild->OnServerClose(mCode, mReason); + } + + private: + uint16_t mCode; + nsCString mReason; +}; + +mozilla::ipc::IPCResult WebSocketChannelChild::RecvOnServerClose( + const uint16_t& aCode, const nsCString& aReason) { + mEventQ->RunOrEnqueue(new EventTargetDispatcher( + this, new ServerCloseEvent(aCode, aReason), mTargetThread)); + + return IPC_OK(); +} + +void WebSocketChannelChild::OnServerClose(const uint16_t& aCode, + const nsCString& aReason) { + LOG(("WebSocketChannelChild::RecvOnServerClose() %p\n", this)); + if (mListenerMT) { + AutoEventEnqueuer ensureSerialDispatch(mEventQ); + DebugOnly rv = mListenerMT->mListener->OnServerClose( + mListenerMT->mContext, aCode, aReason); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } +} + +void WebSocketChannelChild::SetupNeckoTarget() { + mNeckoTarget = nsContentUtils::GetEventTargetByLoadInfo( + mLoadInfo, TaskCategory::Network); + if (!mNeckoTarget) { + return; + } + + gNeckoChild->SetEventTargetForActor(this, mNeckoTarget); +} + +NS_IMETHODIMP +WebSocketChannelChild::AsyncOpen(nsIURI* aURI, const nsACString& aOrigin, + uint64_t aInnerWindowID, + nsIWebSocketListener* aListener, + nsISupports* aContext) { + LOG(("WebSocketChannelChild::AsyncOpen() %p\n", this)); + + MOZ_ASSERT(NS_IsMainThread(), "not main thread"); + MOZ_ASSERT((aURI && !mIsServerSide) || (!aURI && mIsServerSide), + "Invalid aURI for WebSocketChannelChild::AsyncOpen"); + MOZ_ASSERT(aListener && !mListenerMT, + "Invalid state for WebSocketChannelChild::AsyncOpen"); + + mozilla::dom::BrowserChild* browserChild = nullptr; + nsCOMPtr iBrowserChild; + NS_QueryNotificationCallbacks(mCallbacks, mLoadGroup, + NS_GET_IID(nsIBrowserChild), + getter_AddRefs(iBrowserChild)); + if (iBrowserChild) { + browserChild = + static_cast(iBrowserChild.get()); + } + + ContentChild* cc = static_cast(gNeckoChild->Manager()); + if (cc->IsShuttingDown()) { + return NS_ERROR_FAILURE; + } + + // Corresponding release in DeallocPWebSocket + AddIPDLReference(); + + nsCOMPtr uri; + Maybe loadInfoArgs; + Maybe transportProvider; + + if (!mIsServerSide) { + uri = aURI; + nsresult rv = LoadInfoToLoadInfoArgs(mLoadInfo, &loadInfoArgs); + NS_ENSURE_SUCCESS(rv, rv); + + transportProvider = Nothing(); + } else { + MOZ_ASSERT(mServerTransportProvider); + PTransportProviderChild* ipcChild; + nsresult rv = mServerTransportProvider->GetIPCChild(&ipcChild); + NS_ENSURE_SUCCESS(rv, rv); + + transportProvider = Some(ipcChild); + } + + // This must be called before sending constructor message. + SetupNeckoTarget(); + + gNeckoChild->SendPWebSocketConstructor( + this, browserChild, IPC::SerializedLoadContext(this), mSerial); + if (!SendAsyncOpen(uri, nsCString(aOrigin), aInnerWindowID, mProtocol, + mEncrypted, mPingInterval, mClientSetPingInterval, + mPingResponseTimeout, mClientSetPingTimeout, loadInfoArgs, + transportProvider, mNegotiatedExtensions)) { + return NS_ERROR_UNEXPECTED; + } + + if (mIsServerSide) { + mServerTransportProvider = nullptr; + } + + mOriginalURI = aURI; + mURI = mOriginalURI; + mListenerMT = new ListenerAndContextContainer(aListener, aContext); + mOrigin = aOrigin; + mWasOpened = 1; + + return NS_OK; +} + +class CloseEvent : public Runnable { + public: + CloseEvent(WebSocketChannelChild* aChild, uint16_t aCode, + const nsACString& aReason) + : Runnable("net::CloseEvent"), + mChild(aChild), + mCode(aCode), + mReason(aReason) { + MOZ_RELEASE_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(aChild); + } + NS_IMETHOD Run() override { + MOZ_RELEASE_ASSERT(NS_IsMainThread()); + mChild->Close(mCode, mReason); + return NS_OK; + } + + private: + RefPtr mChild; + uint16_t mCode; + nsCString mReason; +}; + +NS_IMETHODIMP +WebSocketChannelChild::Close(uint16_t code, const nsACString& reason) { + if (!NS_IsMainThread()) { + MOZ_RELEASE_ASSERT(mTargetThread->IsOnCurrentThread()); + nsCOMPtr target = GetNeckoTarget(); + return target->Dispatch(new CloseEvent(this, code, reason), + NS_DISPATCH_NORMAL); + } + LOG(("WebSocketChannelChild::Close() %p\n", this)); + + { + MutexAutoLock lock(mMutex); + if (mIPCState != Opened) { + return NS_ERROR_UNEXPECTED; + } + } + + if (!SendClose(code, nsCString(reason))) { + return NS_ERROR_UNEXPECTED; + } + + return NS_OK; +} + +class MsgEvent : public Runnable { + public: + MsgEvent(WebSocketChannelChild* aChild, const nsACString& aMsg, + bool aBinaryMsg) + : Runnable("net::MsgEvent"), + mChild(aChild), + mMsg(aMsg), + mBinaryMsg(aBinaryMsg) { + MOZ_RELEASE_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(aChild); + } + NS_IMETHOD Run() override { + MOZ_RELEASE_ASSERT(NS_IsMainThread()); + if (mBinaryMsg) { + mChild->SendBinaryMsg(mMsg); + } else { + mChild->SendMsg(mMsg); + } + return NS_OK; + } + + private: + RefPtr mChild; + nsCString mMsg; + bool mBinaryMsg; +}; + +NS_IMETHODIMP +WebSocketChannelChild::SendMsg(const nsACString& aMsg) { + if (!NS_IsMainThread()) { + MOZ_RELEASE_ASSERT(IsOnTargetThread()); + nsCOMPtr target = GetNeckoTarget(); + return target->Dispatch(new MsgEvent(this, aMsg, false), + NS_DISPATCH_NORMAL); + } + LOG(("WebSocketChannelChild::SendMsg() %p\n", this)); + + { + MutexAutoLock lock(mMutex); + if (mIPCState != Opened) { + return NS_ERROR_UNEXPECTED; + } + } + + if (!SendSendMsg(nsCString(aMsg))) { + return NS_ERROR_UNEXPECTED; + } + + return NS_OK; +} + +NS_IMETHODIMP +WebSocketChannelChild::SendBinaryMsg(const nsACString& aMsg) { + if (!NS_IsMainThread()) { + MOZ_RELEASE_ASSERT(IsOnTargetThread()); + nsCOMPtr target = GetNeckoTarget(); + return target->Dispatch(new MsgEvent(this, aMsg, true), NS_DISPATCH_NORMAL); + } + LOG(("WebSocketChannelChild::SendBinaryMsg() %p\n", this)); + + { + MutexAutoLock lock(mMutex); + if (mIPCState != Opened) { + return NS_ERROR_UNEXPECTED; + } + } + + if (!SendSendBinaryMsg(nsCString(aMsg))) { + return NS_ERROR_UNEXPECTED; + } + + return NS_OK; +} + +class BinaryStreamEvent : public Runnable { + public: + BinaryStreamEvent(WebSocketChannelChild* aChild, nsIInputStream* aStream, + uint32_t aLength) + : Runnable("net::BinaryStreamEvent"), + mChild(aChild), + mStream(aStream), + mLength(aLength) { + MOZ_RELEASE_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(aChild); + } + NS_IMETHOD Run() override { + MOZ_ASSERT(NS_IsMainThread()); + nsresult rv = mChild->SendBinaryStream(mStream, mLength); + if (NS_FAILED(rv)) { + LOG( + ("WebSocketChannelChild::BinaryStreamEvent %p " + "SendBinaryStream failed (%08" PRIx32 ")\n", + this, static_cast(rv))); + } + return NS_OK; + } + + private: + RefPtr mChild; + nsCOMPtr mStream; + uint32_t mLength; +}; + +NS_IMETHODIMP +WebSocketChannelChild::SendBinaryStream(nsIInputStream* aStream, + uint32_t aLength) { + if (!NS_IsMainThread()) { + MOZ_RELEASE_ASSERT(mTargetThread->IsOnCurrentThread()); + nsCOMPtr target = GetNeckoTarget(); + return target->Dispatch(new BinaryStreamEvent(this, aStream, aLength), + NS_DISPATCH_NORMAL); + } + + LOG(("WebSocketChannelChild::SendBinaryStream() %p\n", this)); + + AutoIPCStream autoStream; + autoStream.Serialize(aStream, static_cast( + gNeckoChild->Manager())); + + { + MutexAutoLock lock(mMutex); + if (mIPCState != Opened) { + return NS_ERROR_UNEXPECTED; + } + } + + if (!SendSendBinaryStream(autoStream.TakeValue(), aLength)) { + return NS_ERROR_UNEXPECTED; + } + + return NS_OK; +} + +NS_IMETHODIMP +WebSocketChannelChild::GetSecurityInfo(nsISupports** aSecurityInfo) { + LOG(("WebSocketChannelChild::GetSecurityInfo() %p\n", this)); + return NS_ERROR_NOT_AVAILABLE; +} + +bool WebSocketChannelChild::IsOnTargetThread() { + MOZ_ASSERT(mTargetThread); + bool isOnTargetThread = false; + nsresult rv = mTargetThread->IsOnCurrentThread(&isOnTargetThread); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + return NS_FAILED(rv) ? false : isOnTargetThread; +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/websocket/WebSocketChannelChild.h b/netwerk/protocol/websocket/WebSocketChannelChild.h new file mode 100644 index 0000000000..4be4ada669 --- /dev/null +++ b/netwerk/protocol/websocket/WebSocketChannelChild.h @@ -0,0 +1,111 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et 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/. */ + +#ifndef mozilla_net_WebSocketChannelChild_h +#define mozilla_net_WebSocketChannelChild_h + +#include "mozilla/net/NeckoTargetHolder.h" +#include "mozilla/net/PWebSocketChild.h" +#include "mozilla/net/BaseWebSocketChannel.h" +#include "nsString.h" + +namespace mozilla { + +namespace net { + +class ChannelEvent; +class ChannelEventQueue; + +class WebSocketChannelChild final : public BaseWebSocketChannel, + public PWebSocketChild, + public NeckoTargetHolder { + friend class PWebSocketChild; + + public: + explicit WebSocketChannelChild(bool aSecure); + + NS_DECL_THREADSAFE_ISUPPORTS + + // nsIWebSocketChannel methods BaseWebSocketChannel didn't implement for us + // + NS_IMETHOD AsyncOpen(nsIURI* aURI, const nsACString& aOrigin, + uint64_t aInnerWindowID, nsIWebSocketListener* aListener, + nsISupports* aContext) override; + NS_IMETHOD Close(uint16_t code, const nsACString& reason) override; + NS_IMETHOD SendMsg(const nsACString& aMsg) override; + NS_IMETHOD SendBinaryMsg(const nsACString& aMsg) override; + NS_IMETHOD SendBinaryStream(nsIInputStream* aStream, + uint32_t aLength) override; + NS_IMETHOD GetSecurityInfo(nsISupports** aSecurityInfo) override; + + void AddIPDLReference(); + void ReleaseIPDLReference(); + + // Off main thread URI access. + void GetEffectiveURL(nsAString& aEffectiveURL) const override; + bool IsEncrypted() const override; + + private: + ~WebSocketChannelChild(); + + mozilla::ipc::IPCResult RecvOnStart(const nsCString& aProtocol, + const nsCString& aExtensions, + const nsString& aEffectiveURL, + const bool& aSecure, + const uint64_t& aHttpChannelId); + mozilla::ipc::IPCResult RecvOnStop(const nsresult& aStatusCode); + mozilla::ipc::IPCResult RecvOnMessageAvailable( + const nsDependentCSubstring& aMsg, const bool& aMoreData); + mozilla::ipc::IPCResult RecvOnBinaryMessageAvailable( + const nsDependentCSubstring& aMsg, const bool& aMoreData); + mozilla::ipc::IPCResult RecvOnAcknowledge(const uint32_t& aSize); + mozilla::ipc::IPCResult RecvOnServerClose(const uint16_t& aCode, + const nsCString& aReason); + + void OnStart(const nsCString& aProtocol, const nsCString& aExtensions, + const nsString& aEffectiveURL, const bool& aSecure, + const uint64_t& aHttpChannelId); + void OnStop(const nsresult& aStatusCode); + void OnMessageAvailable(const nsCString& aMsg); + void OnBinaryMessageAvailable(const nsCString& aMsg); + void OnAcknowledge(const uint32_t& aSize); + void OnServerClose(const uint16_t& aCode, const nsCString& aReason); + void AsyncOpenFailed(); + + bool IsOnTargetThread(); + + void MaybeReleaseIPCObject(); + + // This function tries to get a labeled event target for |mNeckoTarget|. + void SetupNeckoTarget(); + + bool RecvOnMessageAvailableInternal(const nsDependentCSubstring& aMsg, + bool aMoreData, bool aBinary); + + void OnError(); + + RefPtr mEventQ; + nsString mEffectiveURL; + nsCString mReceivedMsgBuffer; + + // This variable is protected by mutex. + enum { Opened, Closing, Closed } mIPCState; + + mozilla::Mutex mMutex; + + friend class StartEvent; + friend class StopEvent; + friend class MessageEvent; + friend class AcknowledgeEvent; + friend class ServerCloseEvent; + friend class AsyncOpenFailedEvent; + friend class OnErrorEvent; +}; + +} // namespace net +} // namespace mozilla + +#endif // mozilla_net_WebSocketChannelChild_h diff --git a/netwerk/protocol/websocket/WebSocketChannelParent.cpp b/netwerk/protocol/websocket/WebSocketChannelParent.cpp new file mode 100644 index 0000000000..a1c3524471 --- /dev/null +++ b/netwerk/protocol/websocket/WebSocketChannelParent.cpp @@ -0,0 +1,351 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et 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 "WebSocketLog.h" +#include "WebSocketChannelParent.h" +#include "nsIAuthPromptProvider.h" +#include "mozilla/ipc/InputStreamUtils.h" +#include "mozilla/ipc/URIUtils.h" +#include "mozilla/ipc/BackgroundUtils.h" +#include "SerializedLoadContext.h" +#include "mozilla/net/NeckoCommon.h" +#include "mozilla/net/WebSocketChannel.h" + +using namespace mozilla::ipc; + +namespace mozilla { +namespace net { + +NS_IMPL_ISUPPORTS(WebSocketChannelParent, nsIWebSocketListener, + nsIInterfaceRequestor) + +WebSocketChannelParent::WebSocketChannelParent( + nsIAuthPromptProvider* aAuthProvider, nsILoadContext* aLoadContext, + PBOverrideStatus aOverrideStatus, uint32_t aSerial) + : mAuthProvider(aAuthProvider), + mLoadContext(aLoadContext), + mSerial(aSerial) { + // Websocket channels can't have a private browsing override + MOZ_ASSERT_IF(!aLoadContext, aOverrideStatus == kPBOverride_Unset); +} +//----------------------------------------------------------------------------- +// WebSocketChannelParent::PWebSocketChannelParent +//----------------------------------------------------------------------------- + +mozilla::ipc::IPCResult WebSocketChannelParent::RecvDeleteSelf() { + LOG(("WebSocketChannelParent::RecvDeleteSelf() %p\n", this)); + mChannel = nullptr; + mAuthProvider = nullptr; + IProtocol* mgr = Manager(); + if (CanRecv() && !Send__delete__(this)) { + return IPC_FAIL_NO_REASON(mgr); + } + return IPC_OK(); +} + +mozilla::ipc::IPCResult WebSocketChannelParent::RecvAsyncOpen( + nsIURI* aURI, const nsCString& aOrigin, const uint64_t& aInnerWindowID, + const nsCString& aProtocol, const bool& aSecure, + const uint32_t& aPingInterval, const bool& aClientSetPingInterval, + const uint32_t& aPingTimeout, const bool& aClientSetPingTimeout, + const Maybe& aLoadInfoArgs, + const Maybe& aTransportProvider, + const nsCString& aNegotiatedExtensions) { + LOG(("WebSocketChannelParent::RecvAsyncOpen() %p\n", this)); + + nsresult rv; + nsCOMPtr loadInfo; + nsCOMPtr uri; + + rv = LoadInfoArgsToLoadInfo(aLoadInfoArgs, getter_AddRefs(loadInfo)); + if (NS_FAILED(rv)) { + goto fail; + } + + if (aSecure) { + mChannel = + do_CreateInstance("@mozilla.org/network/protocol;1?name=wss", &rv); + } else { + mChannel = + do_CreateInstance("@mozilla.org/network/protocol;1?name=ws", &rv); + } + if (NS_FAILED(rv)) goto fail; + + rv = mChannel->SetSerial(mSerial); + if (NS_WARN_IF(NS_FAILED(rv))) { + goto fail; + } + + rv = mChannel->SetLoadInfo(loadInfo); + if (NS_FAILED(rv)) { + goto fail; + } + + rv = mChannel->SetNotificationCallbacks(this); + if (NS_FAILED(rv)) goto fail; + + rv = mChannel->SetProtocol(aProtocol); + if (NS_FAILED(rv)) goto fail; + + if (aTransportProvider.isSome()) { + RefPtr provider = + static_cast(aTransportProvider.value()); + rv = mChannel->SetServerParameters(provider, aNegotiatedExtensions); + if (NS_FAILED(rv)) { + goto fail; + } + } else { + uri = aURI; + if (!uri) { + rv = NS_ERROR_FAILURE; + goto fail; + } + } + + // only use ping values from child if they were overridden by client code. + if (aClientSetPingInterval) { + // IDL allows setting in seconds, so must be multiple of 1000 ms + MOZ_ASSERT(aPingInterval >= 1000 && !(aPingInterval % 1000)); + DebugOnly rv = mChannel->SetPingInterval(aPingInterval / 1000); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + if (aClientSetPingTimeout) { + MOZ_ASSERT(aPingTimeout >= 1000 && !(aPingTimeout % 1000)); + DebugOnly rv = mChannel->SetPingTimeout(aPingTimeout / 1000); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + + rv = mChannel->AsyncOpen(uri, aOrigin, aInnerWindowID, this, nullptr); + if (NS_FAILED(rv)) goto fail; + + return IPC_OK(); + +fail: + mChannel = nullptr; + if (!SendOnStop(rv)) { + return IPC_FAIL_NO_REASON(this); + } + return IPC_OK(); +} + +mozilla::ipc::IPCResult WebSocketChannelParent::RecvClose( + const uint16_t& code, const nsCString& reason) { + LOG(("WebSocketChannelParent::RecvClose() %p\n", this)); + if (mChannel) { + nsresult rv = mChannel->Close(code, reason); + NS_ENSURE_SUCCESS(rv, IPC_OK()); + } + + return IPC_OK(); +} + +mozilla::ipc::IPCResult WebSocketChannelParent::RecvSendMsg( + const nsCString& aMsg) { + LOG(("WebSocketChannelParent::RecvSendMsg() %p\n", this)); + if (mChannel) { + nsresult rv = mChannel->SendMsg(aMsg); + NS_ENSURE_SUCCESS(rv, IPC_OK()); + } + return IPC_OK(); +} + +mozilla::ipc::IPCResult WebSocketChannelParent::RecvSendBinaryMsg( + const nsCString& aMsg) { + LOG(("WebSocketChannelParent::RecvSendBinaryMsg() %p\n", this)); + if (mChannel) { + nsresult rv = mChannel->SendBinaryMsg(aMsg); + NS_ENSURE_SUCCESS(rv, IPC_OK()); + } + return IPC_OK(); +} + +mozilla::ipc::IPCResult WebSocketChannelParent::RecvSendBinaryStream( + const IPCStream& aStream, const uint32_t& aLength) { + LOG(("WebSocketChannelParent::RecvSendBinaryStream() %p\n", this)); + if (mChannel) { + nsCOMPtr stream = DeserializeIPCStream(aStream); + if (!stream) { + return IPC_FAIL_NO_REASON(this); + } + nsresult rv = mChannel->SendBinaryStream(stream, aLength); + NS_ENSURE_SUCCESS(rv, IPC_OK()); + } + return IPC_OK(); +} + +//----------------------------------------------------------------------------- +// WebSocketChannelParent::nsIRequestObserver +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +WebSocketChannelParent::OnStart(nsISupports* aContext) { + LOG(("WebSocketChannelParent::OnStart() %p\n", this)); + nsAutoCString protocol, extensions; + nsString effectiveURL; + bool encrypted = false; + uint64_t httpChannelId = 0; + if (mChannel) { + DebugOnly rv = mChannel->GetProtocol(protocol); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + rv = mChannel->GetExtensions(extensions); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + RefPtr channel; + channel = static_cast(mChannel.get()); + MOZ_ASSERT(channel); + + channel->GetEffectiveURL(effectiveURL); + encrypted = channel->IsEncrypted(); + httpChannelId = channel->HttpChannelId(); + } + if (!CanRecv() || !SendOnStart(protocol, extensions, effectiveURL, encrypted, + httpChannelId)) { + return NS_ERROR_FAILURE; + } + return NS_OK; +} + +NS_IMETHODIMP +WebSocketChannelParent::OnStop(nsISupports* aContext, nsresult aStatusCode) { + LOG(("WebSocketChannelParent::OnStop() %p\n", this)); + if (!CanRecv() || !SendOnStop(aStatusCode)) { + return NS_ERROR_FAILURE; + } + return NS_OK; +} + +static bool SendOnMessageAvailableHelper( + const nsACString& aMsg, + const std::function& aSendFunc) { + // To avoid the crash caused by too large IPC message, we have to split the + // data in small chunks and send them to child process. Note that the chunk + // size used here is the same as what we used for PHttpChannel. + static uint32_t const kCopyChunkSize = 128 * 1024; + uint32_t count = aMsg.Length(); + if (count <= kCopyChunkSize) { + return aSendFunc(nsDependentCSubstring(aMsg), false); + } + + uint32_t start = 0; + uint32_t toRead = std::min(count, kCopyChunkSize); + while (count) { + nsDependentCSubstring data(Substring(aMsg, start, toRead)); + + if (!aSendFunc(data, count > kCopyChunkSize)) { + return false; + } + + start += toRead; + count -= toRead; + toRead = std::min(count, kCopyChunkSize); + } + + return true; +} + +NS_IMETHODIMP +WebSocketChannelParent::OnMessageAvailable(nsISupports* aContext, + const nsACString& aMsg) { + LOG(("WebSocketChannelParent::OnMessageAvailable() %p\n", this)); + + if (!CanRecv()) { + return NS_ERROR_FAILURE; + } + + auto sendFunc = [self = UnsafePtr(this)]( + const nsDependentCSubstring& aMsg, bool aMoreData) { + return self->SendOnMessageAvailable(aMsg, aMoreData); + }; + + if (!SendOnMessageAvailableHelper(aMsg, sendFunc)) { + return NS_ERROR_FAILURE; + } + + return NS_OK; +} + +NS_IMETHODIMP +WebSocketChannelParent::OnBinaryMessageAvailable(nsISupports* aContext, + const nsACString& aMsg) { + LOG(("WebSocketChannelParent::OnBinaryMessageAvailable() %p\n", this)); + + if (!CanRecv()) { + return NS_ERROR_FAILURE; + } + + auto sendFunc = [self = UnsafePtr(this)]( + const nsDependentCSubstring& aMsg, bool aMoreData) { + return self->SendOnBinaryMessageAvailable(aMsg, aMoreData); + }; + + if (!SendOnMessageAvailableHelper(aMsg, sendFunc)) { + return NS_ERROR_FAILURE; + } + + return NS_OK; +} + +NS_IMETHODIMP +WebSocketChannelParent::OnAcknowledge(nsISupports* aContext, uint32_t aSize) { + LOG(("WebSocketChannelParent::OnAcknowledge() %p\n", this)); + if (!CanRecv() || !SendOnAcknowledge(aSize)) { + return NS_ERROR_FAILURE; + } + return NS_OK; +} + +NS_IMETHODIMP +WebSocketChannelParent::OnServerClose(nsISupports* aContext, uint16_t code, + const nsACString& reason) { + LOG(("WebSocketChannelParent::OnServerClose() %p\n", this)); + if (!CanRecv() || !SendOnServerClose(code, nsCString(reason))) { + return NS_ERROR_FAILURE; + } + return NS_OK; +} + +NS_IMETHODIMP +WebSocketChannelParent::OnError() { return NS_OK; } + +void WebSocketChannelParent::ActorDestroy(ActorDestroyReason why) { + LOG(("WebSocketChannelParent::ActorDestroy() %p\n", this)); + + // Make sure we close the channel if the content process dies without going + // through a clean shutdown. + if (mChannel) { + Unused << mChannel->Close(nsIWebSocketChannel::CLOSE_GOING_AWAY, + "Child was killed"_ns); + } +} + +//----------------------------------------------------------------------------- +// WebSocketChannelParent::nsIInterfaceRequestor +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +WebSocketChannelParent::GetInterface(const nsIID& iid, void** result) { + LOG(("WebSocketChannelParent::GetInterface() %p\n", this)); + if (mAuthProvider && iid.Equals(NS_GET_IID(nsIAuthPromptProvider))) { + nsresult rv = mAuthProvider->GetAuthPrompt( + nsIAuthPromptProvider::PROMPT_NORMAL, iid, result); + if (NS_FAILED(rv)) { + return NS_ERROR_NO_INTERFACE; + } + return NS_OK; + } + + // Only support nsILoadContext if child channel's callbacks did too + if (iid.Equals(NS_GET_IID(nsILoadContext)) && mLoadContext) { + nsCOMPtr copy = mLoadContext; + copy.forget(result); + return NS_OK; + } + + return QueryInterface(iid, result); +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/websocket/WebSocketChannelParent.h b/netwerk/protocol/websocket/WebSocketChannelParent.h new file mode 100644 index 0000000000..0d19b9ff6b --- /dev/null +++ b/netwerk/protocol/websocket/WebSocketChannelParent.h @@ -0,0 +1,69 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et 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/. */ + +#ifndef mozilla_net_WebSocketChannelParent_h +#define mozilla_net_WebSocketChannelParent_h + +#include "mozilla/net/PWebSocketParent.h" +#include "mozilla/net/NeckoParent.h" +#include "nsIInterfaceRequestor.h" +#include "nsIWebSocketListener.h" +#include "nsIWebSocketChannel.h" +#include "nsILoadContext.h" +#include "nsCOMPtr.h" +#include "nsString.h" + +class nsIAuthPromptProvider; + +namespace mozilla { +namespace net { + +class WebSocketChannelParent : public PWebSocketParent, + public nsIWebSocketListener, + public nsIInterfaceRequestor { + friend class PWebSocketParent; + + ~WebSocketChannelParent() = default; + + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIWEBSOCKETLISTENER + NS_DECL_NSIINTERFACEREQUESTOR + + WebSocketChannelParent(nsIAuthPromptProvider* aAuthProvider, + nsILoadContext* aLoadContext, + PBOverrideStatus aOverrideStatus, uint32_t aSerial); + + private: + mozilla::ipc::IPCResult RecvAsyncOpen( + nsIURI* aURI, const nsCString& aOrigin, const uint64_t& aInnerWindowID, + const nsCString& aProtocol, const bool& aSecure, + const uint32_t& aPingInterval, const bool& aClientSetPingInterval, + const uint32_t& aPingTimeout, const bool& aClientSetPingTimeout, + const Maybe& aLoadInfoArgs, + const Maybe& aTransportProvider, + const nsCString& aNegotiatedExtensions); + mozilla::ipc::IPCResult RecvClose(const uint16_t& code, + const nsCString& reason); + mozilla::ipc::IPCResult RecvSendMsg(const nsCString& aMsg); + mozilla::ipc::IPCResult RecvSendBinaryMsg(const nsCString& aMsg); + mozilla::ipc::IPCResult RecvSendBinaryStream(const IPCStream& aStream, + const uint32_t& aLength); + mozilla::ipc::IPCResult RecvDeleteSelf(); + + void ActorDestroy(ActorDestroyReason why) override; + + nsCOMPtr mAuthProvider; + nsCOMPtr mChannel; + nsCOMPtr mLoadContext; + + uint32_t mSerial; +}; + +} // namespace net +} // namespace mozilla + +#endif // mozilla_net_WebSocketChannelParent_h diff --git a/netwerk/protocol/websocket/WebSocketEventListenerChild.cpp b/netwerk/protocol/websocket/WebSocketEventListenerChild.cpp new file mode 100644 index 0000000000..2232475382 --- /dev/null +++ b/netwerk/protocol/websocket/WebSocketEventListenerChild.cpp @@ -0,0 +1,109 @@ +/* -*- 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 "WebSocketEventListenerChild.h" + +#include "WebSocketEventService.h" +#include "WebSocketFrame.h" + +namespace mozilla { +namespace net { + +WebSocketEventListenerChild::WebSocketEventListenerChild( + uint64_t aInnerWindowID, nsISerialEventTarget* aTarget) + : NeckoTargetHolder(aTarget), + mService(WebSocketEventService::GetOrCreate()), + mInnerWindowID(aInnerWindowID) {} + +WebSocketEventListenerChild::~WebSocketEventListenerChild() { + MOZ_ASSERT(!mService); +} + +mozilla::ipc::IPCResult WebSocketEventListenerChild::RecvWebSocketCreated( + const uint32_t& aWebSocketSerialID, const nsString& aURI, + const nsCString& aProtocols) { + if (mService) { + nsCOMPtr target = GetNeckoTarget(); + mService->WebSocketCreated(aWebSocketSerialID, mInnerWindowID, aURI, + aProtocols, target); + } + + return IPC_OK(); +} + +mozilla::ipc::IPCResult WebSocketEventListenerChild::RecvWebSocketOpened( + const uint32_t& aWebSocketSerialID, const nsString& aEffectiveURI, + const nsCString& aProtocols, const nsCString& aExtensions, + const uint64_t& aHttpChannelId) { + if (mService) { + nsCOMPtr target = GetNeckoTarget(); + mService->WebSocketOpened(aWebSocketSerialID, mInnerWindowID, aEffectiveURI, + aProtocols, aExtensions, aHttpChannelId, target); + } + + return IPC_OK(); +} + +mozilla::ipc::IPCResult +WebSocketEventListenerChild::RecvWebSocketMessageAvailable( + const uint32_t& aWebSocketSerialID, const nsCString& aData, + const uint16_t& aMessageType) { + if (mService) { + nsCOMPtr target = GetNeckoTarget(); + mService->WebSocketMessageAvailable(aWebSocketSerialID, mInnerWindowID, + aData, aMessageType, target); + } + + return IPC_OK(); +} + +mozilla::ipc::IPCResult WebSocketEventListenerChild::RecvWebSocketClosed( + const uint32_t& aWebSocketSerialID, const bool& aWasClean, + const uint16_t& aCode, const nsString& aReason) { + if (mService) { + nsCOMPtr target = GetNeckoTarget(); + mService->WebSocketClosed(aWebSocketSerialID, mInnerWindowID, aWasClean, + aCode, aReason, target); + } + + return IPC_OK(); +} + +mozilla::ipc::IPCResult WebSocketEventListenerChild::RecvFrameReceived( + const uint32_t& aWebSocketSerialID, const WebSocketFrameData& aFrameData) { + if (mService) { + nsCOMPtr target = GetNeckoTarget(); + RefPtr frame = new WebSocketFrame(aFrameData); + mService->FrameReceived(aWebSocketSerialID, mInnerWindowID, frame.forget(), + target); + } + + return IPC_OK(); +} + +mozilla::ipc::IPCResult WebSocketEventListenerChild::RecvFrameSent( + const uint32_t& aWebSocketSerialID, const WebSocketFrameData& aFrameData) { + if (mService) { + nsCOMPtr target = GetNeckoTarget(); + RefPtr frame = new WebSocketFrame(aFrameData); + mService->FrameSent(aWebSocketSerialID, mInnerWindowID, frame.forget(), + target); + } + + return IPC_OK(); +} + +void WebSocketEventListenerChild::Close() { + mService = nullptr; + SendClose(); +} + +void WebSocketEventListenerChild::ActorDestroy(ActorDestroyReason aWhy) { + mService = nullptr; +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/websocket/WebSocketEventListenerChild.h b/netwerk/protocol/websocket/WebSocketEventListenerChild.h new file mode 100644 index 0000000000..88dddb79a5 --- /dev/null +++ b/netwerk/protocol/websocket/WebSocketEventListenerChild.h @@ -0,0 +1,63 @@ +/* -*- 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/. */ + +#ifndef mozilla_net_WebSocketEventListenerChild_h +#define mozilla_net_WebSocketEventListenerChild_h + +#include "mozilla/net/NeckoTargetHolder.h" +#include "mozilla/net/PWebSocketEventListenerChild.h" + +namespace mozilla { +namespace net { + +class WebSocketEventService; + +class WebSocketEventListenerChild final : public PWebSocketEventListenerChild, + public NeckoTargetHolder { + public: + NS_INLINE_DECL_REFCOUNTING(WebSocketEventListenerChild) + + explicit WebSocketEventListenerChild(uint64_t aInnerWindowID, + nsISerialEventTarget* aTarget); + + mozilla::ipc::IPCResult RecvWebSocketCreated( + const uint32_t& aWebSocketSerialID, const nsString& aURI, + const nsCString& aProtocols); + + mozilla::ipc::IPCResult RecvWebSocketOpened( + const uint32_t& aWebSocketSerialID, const nsString& aEffectiveURI, + const nsCString& aProtocols, const nsCString& aExtensions, + const uint64_t& aHttpChannelId); + + mozilla::ipc::IPCResult RecvWebSocketMessageAvailable( + const uint32_t& aWebSocketSerialID, const nsCString& aData, + const uint16_t& aMessageType); + + mozilla::ipc::IPCResult RecvWebSocketClosed( + const uint32_t& aWebSocketSerialID, const bool& aWasClean, + const uint16_t& aCode, const nsString& aReason); + + mozilla::ipc::IPCResult RecvFrameReceived( + const uint32_t& aWebSocketSerialID, const WebSocketFrameData& aFrameData); + + mozilla::ipc::IPCResult RecvFrameSent(const uint32_t& aWebSocketSerialID, + const WebSocketFrameData& aFrameData); + + void Close(); + + private: + ~WebSocketEventListenerChild(); + + virtual void ActorDestroy(ActorDestroyReason aWhy) override; + + RefPtr mService; + uint64_t mInnerWindowID; +}; + +} // namespace net +} // namespace mozilla + +#endif // mozilla_net_WebSocketEventListenerChild_h diff --git a/netwerk/protocol/websocket/WebSocketEventListenerParent.cpp b/netwerk/protocol/websocket/WebSocketEventListenerParent.cpp new file mode 100644 index 0000000000..bb21483fb9 --- /dev/null +++ b/netwerk/protocol/websocket/WebSocketEventListenerParent.cpp @@ -0,0 +1,119 @@ +/* -*- 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 "WebSocketEventService.h" +#include "WebSocketEventListenerParent.h" +#include "mozilla/Unused.h" + +namespace mozilla { +namespace net { + +NS_INTERFACE_MAP_BEGIN(WebSocketEventListenerParent) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIWebSocketEventListener) + NS_INTERFACE_MAP_ENTRY(nsIWebSocketEventListener) +NS_INTERFACE_MAP_END + +NS_IMPL_ADDREF(WebSocketEventListenerParent) +NS_IMPL_RELEASE(WebSocketEventListenerParent) + +WebSocketEventListenerParent::WebSocketEventListenerParent( + uint64_t aInnerWindowID) + : mService(WebSocketEventService::GetOrCreate()), + mInnerWindowID(aInnerWindowID) { + DebugOnly rv = mService->AddListener(mInnerWindowID, this); + MOZ_ASSERT(NS_SUCCEEDED(rv)); +} + +WebSocketEventListenerParent::~WebSocketEventListenerParent() { + MOZ_ASSERT(!mService); +} + +mozilla::ipc::IPCResult WebSocketEventListenerParent::RecvClose() { + if (mService) { + UnregisterListener(); + Unused << Send__delete__(this); + } + + return IPC_OK(); +} + +void WebSocketEventListenerParent::ActorDestroy(ActorDestroyReason aWhy) { + UnregisterListener(); +} + +void WebSocketEventListenerParent::UnregisterListener() { + if (mService) { + DebugOnly rv = mService->RemoveListener(mInnerWindowID, this); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + mService = nullptr; + } +} + +NS_IMETHODIMP +WebSocketEventListenerParent::WebSocketCreated(uint32_t aWebSocketSerialID, + const nsAString& aURI, + const nsACString& aProtocols) { + Unused << SendWebSocketCreated(aWebSocketSerialID, nsString(aURI), + nsCString(aProtocols)); + return NS_OK; +} + +NS_IMETHODIMP +WebSocketEventListenerParent::WebSocketOpened(uint32_t aWebSocketSerialID, + const nsAString& aEffectiveURI, + const nsACString& aProtocols, + const nsACString& aExtensions, + uint64_t aHttpChannelId) { + Unused << SendWebSocketOpened(aWebSocketSerialID, nsString(aEffectiveURI), + nsCString(aProtocols), nsCString(aExtensions), + aHttpChannelId); + return NS_OK; +} + +NS_IMETHODIMP +WebSocketEventListenerParent::WebSocketClosed(uint32_t aWebSocketSerialID, + bool aWasClean, uint16_t aCode, + const nsAString& aReason) { + Unused << SendWebSocketClosed(aWebSocketSerialID, aWasClean, aCode, + nsString(aReason)); + return NS_OK; +} + +NS_IMETHODIMP +WebSocketEventListenerParent::WebSocketMessageAvailable( + uint32_t aWebSocketSerialID, const nsACString& aData, + uint16_t aMessageType) { + Unused << SendWebSocketMessageAvailable(aWebSocketSerialID, nsCString(aData), + aMessageType); + return NS_OK; +} + +NS_IMETHODIMP +WebSocketEventListenerParent::FrameReceived(uint32_t aWebSocketSerialID, + nsIWebSocketFrame* aFrame) { + if (!aFrame) { + return NS_ERROR_FAILURE; + } + + WebSocketFrame* frame = static_cast(aFrame); + Unused << SendFrameReceived(aWebSocketSerialID, frame->Data()); + return NS_OK; +} + +NS_IMETHODIMP +WebSocketEventListenerParent::FrameSent(uint32_t aWebSocketSerialID, + nsIWebSocketFrame* aFrame) { + if (!aFrame) { + return NS_ERROR_FAILURE; + } + + WebSocketFrame* frame = static_cast(aFrame); + Unused << SendFrameSent(aWebSocketSerialID, frame->Data()); + return NS_OK; +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/websocket/WebSocketEventListenerParent.h b/netwerk/protocol/websocket/WebSocketEventListenerParent.h new file mode 100644 index 0000000000..eb5e02679b --- /dev/null +++ b/netwerk/protocol/websocket/WebSocketEventListenerParent.h @@ -0,0 +1,44 @@ +/* -*- 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/. */ + +#ifndef mozilla_net_WebSocketEventListenerParent_h +#define mozilla_net_WebSocketEventListenerParent_h + +#include "mozilla/net/PWebSocketEventListenerParent.h" +#include "nsIWebSocketEventService.h" + +namespace mozilla { +namespace net { + +class WebSocketEventService; + +class WebSocketEventListenerParent final : public PWebSocketEventListenerParent, + public nsIWebSocketEventListener { + friend class PWebSocketEventListenerParent; + + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIWEBSOCKETEVENTLISTENER + + explicit WebSocketEventListenerParent(uint64_t aInnerWindowID); + + private: + ~WebSocketEventListenerParent(); + + mozilla::ipc::IPCResult RecvClose(); + + virtual void ActorDestroy(ActorDestroyReason aWhy) override; + + void UnregisterListener(); + + RefPtr mService; + uint64_t mInnerWindowID; +}; + +} // namespace net +} // namespace mozilla + +#endif // mozilla_net_WebSocketEventListenerParent_h diff --git a/netwerk/protocol/websocket/WebSocketEventService.cpp b/netwerk/protocol/websocket/WebSocketEventService.cpp new file mode 100644 index 0000000000..13322bad4e --- /dev/null +++ b/netwerk/protocol/websocket/WebSocketEventService.cpp @@ -0,0 +1,563 @@ +/* -*- 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 "WebSocketEventListenerChild.h" +#include "WebSocketEventService.h" +#include "WebSocketFrame.h" + +#include "mozilla/net/NeckoChild.h" +#include "mozilla/StaticPtr.h" +#include "nsISupportsPrimitives.h" +#include "nsIObserverService.h" +#include "nsXULAppAPI.h" +#include "nsSocketTransportService2.h" +#include "nsThreadUtils.h" +#include "mozilla/Services.h" +#include "nsIWebSocketImpl.h" + +namespace mozilla { +namespace net { + +namespace { + +StaticRefPtr gWebSocketEventService; + +bool IsChildProcess() { + return XRE_GetProcessType() != GeckoProcessType_Default; +} + +} // anonymous namespace + +class WebSocketBaseRunnable : public Runnable { + public: + WebSocketBaseRunnable(uint32_t aWebSocketSerialID, uint64_t aInnerWindowID) + : Runnable("net::WebSocketBaseRunnable"), + mWebSocketSerialID(aWebSocketSerialID), + mInnerWindowID(aInnerWindowID) {} + + NS_IMETHOD Run() override { + MOZ_ASSERT(NS_IsMainThread()); + + RefPtr service = + WebSocketEventService::GetOrCreate(); + MOZ_ASSERT(service); + + WebSocketEventService::WindowListeners listeners; + service->GetListeners(mInnerWindowID, listeners); + + for (uint32_t i = 0; i < listeners.Length(); ++i) { + DoWork(listeners[i]); + } + + return NS_OK; + } + + protected: + ~WebSocketBaseRunnable() = default; + + virtual void DoWork(nsIWebSocketEventListener* aListener) = 0; + + uint32_t mWebSocketSerialID; + uint64_t mInnerWindowID; +}; + +class WebSocketFrameRunnable final : public WebSocketBaseRunnable { + public: + WebSocketFrameRunnable(uint32_t aWebSocketSerialID, uint64_t aInnerWindowID, + already_AddRefed aFrame, + bool aFrameSent) + : WebSocketBaseRunnable(aWebSocketSerialID, aInnerWindowID), + mFrame(std::move(aFrame)), + mFrameSent(aFrameSent) {} + + private: + virtual void DoWork(nsIWebSocketEventListener* aListener) override { + DebugOnly rv; + if (mFrameSent) { + rv = aListener->FrameSent(mWebSocketSerialID, mFrame); + } else { + rv = aListener->FrameReceived(mWebSocketSerialID, mFrame); + } + + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Frame op failed"); + } + + RefPtr mFrame; + bool mFrameSent; +}; + +class WebSocketCreatedRunnable final : public WebSocketBaseRunnable { + public: + WebSocketCreatedRunnable(uint32_t aWebSocketSerialID, uint64_t aInnerWindowID, + const nsAString& aURI, const nsACString& aProtocols) + : WebSocketBaseRunnable(aWebSocketSerialID, aInnerWindowID), + mURI(aURI), + mProtocols(aProtocols) {} + + private: + virtual void DoWork(nsIWebSocketEventListener* aListener) override { + DebugOnly rv = + aListener->WebSocketCreated(mWebSocketSerialID, mURI, mProtocols); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "WebSocketCreated failed"); + } + + const nsString mURI; + const nsCString mProtocols; +}; + +class WebSocketOpenedRunnable final : public WebSocketBaseRunnable { + public: + WebSocketOpenedRunnable(uint32_t aWebSocketSerialID, uint64_t aInnerWindowID, + const nsAString& aEffectiveURI, + const nsACString& aProtocols, + const nsACString& aExtensions, + uint64_t aHttpChannelId) + : WebSocketBaseRunnable(aWebSocketSerialID, aInnerWindowID), + mEffectiveURI(aEffectiveURI), + mProtocols(aProtocols), + mExtensions(aExtensions), + mHttpChannelId(aHttpChannelId) {} + + private: + virtual void DoWork(nsIWebSocketEventListener* aListener) override { + DebugOnly rv = + aListener->WebSocketOpened(mWebSocketSerialID, mEffectiveURI, + mProtocols, mExtensions, mHttpChannelId); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "WebSocketOpened failed"); + } + + const nsString mEffectiveURI; + const nsCString mProtocols; + const nsCString mExtensions; + uint64_t mHttpChannelId; +}; + +class WebSocketMessageAvailableRunnable final : public WebSocketBaseRunnable { + public: + WebSocketMessageAvailableRunnable(uint32_t aWebSocketSerialID, + uint64_t aInnerWindowID, + const nsACString& aData, + uint16_t aMessageType) + : WebSocketBaseRunnable(aWebSocketSerialID, aInnerWindowID), + mData(aData), + mMessageType(aMessageType) {} + + private: + virtual void DoWork(nsIWebSocketEventListener* aListener) override { + DebugOnly rv = aListener->WebSocketMessageAvailable( + mWebSocketSerialID, mData, mMessageType); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "WebSocketMessageAvailable failed"); + } + + const nsCString mData; + uint16_t mMessageType; +}; + +class WebSocketClosedRunnable final : public WebSocketBaseRunnable { + public: + WebSocketClosedRunnable(uint32_t aWebSocketSerialID, uint64_t aInnerWindowID, + bool aWasClean, uint16_t aCode, + const nsAString& aReason) + : WebSocketBaseRunnable(aWebSocketSerialID, aInnerWindowID), + mWasClean(aWasClean), + mCode(aCode), + mReason(aReason) {} + + private: + virtual void DoWork(nsIWebSocketEventListener* aListener) override { + DebugOnly rv = aListener->WebSocketClosed( + mWebSocketSerialID, mWasClean, mCode, mReason); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "WebSocketClosed failed"); + } + + bool mWasClean; + uint16_t mCode; + const nsString mReason; +}; + +/* static */ +already_AddRefed WebSocketEventService::Get() { + MOZ_ASSERT(NS_IsMainThread()); + RefPtr service = gWebSocketEventService.get(); + return service.forget(); +} + +/* static */ +already_AddRefed WebSocketEventService::GetOrCreate() { + MOZ_ASSERT(NS_IsMainThread()); + + if (!gWebSocketEventService) { + gWebSocketEventService = new WebSocketEventService(); + } + + RefPtr service = gWebSocketEventService.get(); + return service.forget(); +} + +NS_INTERFACE_MAP_BEGIN(WebSocketEventService) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIWebSocketEventService) + NS_INTERFACE_MAP_ENTRY(nsIObserver) + NS_INTERFACE_MAP_ENTRY(nsIWebSocketEventService) +NS_INTERFACE_MAP_END + +NS_IMPL_ADDREF(WebSocketEventService) +NS_IMPL_RELEASE(WebSocketEventService) + +WebSocketEventService::WebSocketEventService() : mCountListeners(0) { + MOZ_ASSERT(NS_IsMainThread()); + + nsCOMPtr obs = mozilla::services::GetObserverService(); + if (obs) { + obs->AddObserver(this, "xpcom-shutdown", false); + obs->AddObserver(this, "inner-window-destroyed", false); + } +} + +WebSocketEventService::~WebSocketEventService() { + MOZ_ASSERT(NS_IsMainThread()); +} + +void WebSocketEventService::WebSocketCreated(uint32_t aWebSocketSerialID, + uint64_t aInnerWindowID, + const nsAString& aURI, + const nsACString& aProtocols, + nsIEventTarget* aTarget) { + // Let's continue only if we have some listeners. + if (!HasListeners()) { + return; + } + + RefPtr runnable = new WebSocketCreatedRunnable( + aWebSocketSerialID, aInnerWindowID, aURI, aProtocols); + DebugOnly rv = aTarget + ? aTarget->Dispatch(runnable, NS_DISPATCH_NORMAL) + : NS_DispatchToMainThread(runnable); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "NS_DispatchToMainThread failed"); +} + +void WebSocketEventService::WebSocketOpened(uint32_t aWebSocketSerialID, + uint64_t aInnerWindowID, + const nsAString& aEffectiveURI, + const nsACString& aProtocols, + const nsACString& aExtensions, + uint64_t aHttpChannelId, + nsIEventTarget* aTarget) { + // Let's continue only if we have some listeners. + if (!HasListeners()) { + return; + } + + RefPtr runnable = new WebSocketOpenedRunnable( + aWebSocketSerialID, aInnerWindowID, aEffectiveURI, aProtocols, + aExtensions, aHttpChannelId); + DebugOnly rv = aTarget + ? aTarget->Dispatch(runnable, NS_DISPATCH_NORMAL) + : NS_DispatchToMainThread(runnable); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "NS_DispatchToMainThread failed"); +} + +void WebSocketEventService::WebSocketMessageAvailable( + uint32_t aWebSocketSerialID, uint64_t aInnerWindowID, + const nsACString& aData, uint16_t aMessageType, nsIEventTarget* aTarget) { + // Let's continue only if we have some listeners. + if (!HasListeners()) { + return; + } + + RefPtr runnable = + new WebSocketMessageAvailableRunnable(aWebSocketSerialID, aInnerWindowID, + aData, aMessageType); + DebugOnly rv = aTarget + ? aTarget->Dispatch(runnable, NS_DISPATCH_NORMAL) + : NS_DispatchToMainThread(runnable); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "NS_DispatchToMainThread failed"); +} + +void WebSocketEventService::WebSocketClosed(uint32_t aWebSocketSerialID, + uint64_t aInnerWindowID, + bool aWasClean, uint16_t aCode, + const nsAString& aReason, + nsIEventTarget* aTarget) { + // Let's continue only if we have some listeners. + if (!HasListeners()) { + return; + } + + RefPtr runnable = new WebSocketClosedRunnable( + aWebSocketSerialID, aInnerWindowID, aWasClean, aCode, aReason); + DebugOnly rv = aTarget + ? aTarget->Dispatch(runnable, NS_DISPATCH_NORMAL) + : NS_DispatchToMainThread(runnable); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "NS_DispatchToMainThread failed"); +} + +void WebSocketEventService::FrameReceived( + uint32_t aWebSocketSerialID, uint64_t aInnerWindowID, + already_AddRefed aFrame, nsIEventTarget* aTarget) { + RefPtr frame(std::move(aFrame)); + MOZ_ASSERT(frame); + + // Let's continue only if we have some listeners. + if (!HasListeners()) { + return; + } + + RefPtr runnable = + new WebSocketFrameRunnable(aWebSocketSerialID, aInnerWindowID, + frame.forget(), false /* frameSent */); + DebugOnly rv = aTarget + ? aTarget->Dispatch(runnable, NS_DISPATCH_NORMAL) + : NS_DispatchToMainThread(runnable); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "NS_DispatchToMainThread failed"); +} + +void WebSocketEventService::FrameSent(uint32_t aWebSocketSerialID, + uint64_t aInnerWindowID, + already_AddRefed aFrame, + nsIEventTarget* aTarget) { + RefPtr frame(std::move(aFrame)); + MOZ_ASSERT(frame); + + // Let's continue only if we have some listeners. + if (!HasListeners()) { + return; + } + + RefPtr runnable = new WebSocketFrameRunnable( + aWebSocketSerialID, aInnerWindowID, frame.forget(), true /* frameSent */); + + DebugOnly rv = aTarget + ? aTarget->Dispatch(runnable, NS_DISPATCH_NORMAL) + : NS_DispatchToMainThread(runnable); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "NS_DispatchToMainThread failed"); +} + +void WebSocketEventService::AssociateWebSocketImplWithSerialID( + nsIWebSocketImpl* aWebSocketImpl, uint32_t aWebSocketSerialID) { + MOZ_ASSERT(NS_IsMainThread()); + + mWebSocketImplMap.Put(aWebSocketSerialID, + do_GetWeakReference(aWebSocketImpl)); +} + +NS_IMETHODIMP +WebSocketEventService::SendMessage(uint32_t aWebSocketSerialID, + const nsAString& aMessage) { + MOZ_ASSERT(NS_IsMainThread()); + + nsWeakPtr weakPtr = mWebSocketImplMap.Get(aWebSocketSerialID); + nsCOMPtr webSocketImpl = do_QueryReferent(weakPtr); + + if (!webSocketImpl) { + return NS_ERROR_NOT_AVAILABLE; + } + + return webSocketImpl->SendMessage(aMessage); +} + +NS_IMETHODIMP +WebSocketEventService::AddListener(uint64_t aInnerWindowID, + nsIWebSocketEventListener* aListener) { + MOZ_ASSERT(NS_IsMainThread()); + + if (!aListener) { + return NS_ERROR_FAILURE; + } + + ++mCountListeners; + + WindowListener* listener = mWindows.Get(aInnerWindowID); + if (!listener) { + listener = new WindowListener(); + + if (IsChildProcess()) { + PWebSocketEventListenerChild* actor = + gNeckoChild->SendPWebSocketEventListenerConstructor(aInnerWindowID); + + listener->mActor = static_cast(actor); + MOZ_ASSERT(listener->mActor); + } + + mWindows.Put(aInnerWindowID, listener); + } + + listener->mListeners.AppendElement(aListener); + + return NS_OK; +} + +NS_IMETHODIMP +WebSocketEventService::RemoveListener(uint64_t aInnerWindowID, + nsIWebSocketEventListener* aListener) { + MOZ_ASSERT(NS_IsMainThread()); + + if (!aListener) { + return NS_ERROR_FAILURE; + } + + WindowListener* listener = mWindows.Get(aInnerWindowID); + if (!listener) { + return NS_ERROR_FAILURE; + } + + if (!listener->mListeners.RemoveElement(aListener)) { + return NS_ERROR_FAILURE; + } + + // The last listener for this window. + if (listener->mListeners.IsEmpty()) { + if (IsChildProcess()) { + ShutdownActorListener(listener); + } + + mWindows.Remove(aInnerWindowID); + } + + MOZ_ASSERT(mCountListeners); + --mCountListeners; + + return NS_OK; +} + +NS_IMETHODIMP +WebSocketEventService::HasListenerFor(uint64_t aInnerWindowID, bool* aResult) { + MOZ_ASSERT(NS_IsMainThread()); + + *aResult = mWindows.Get(aInnerWindowID); + + return NS_OK; +} + +NS_IMETHODIMP +WebSocketEventService::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) { + MOZ_ASSERT(NS_IsMainThread()); + + if (!strcmp(aTopic, "xpcom-shutdown")) { + Shutdown(); + return NS_OK; + } + + if (!strcmp(aTopic, "inner-window-destroyed") && HasListeners()) { + nsCOMPtr wrapper = do_QueryInterface(aSubject); + NS_ENSURE_TRUE(wrapper, NS_ERROR_FAILURE); + + uint64_t innerID; + nsresult rv = wrapper->GetData(&innerID); + NS_ENSURE_SUCCESS(rv, rv); + + WindowListener* listener = mWindows.Get(innerID); + if (!listener) { + return NS_OK; + } + + MOZ_ASSERT(mCountListeners >= listener->mListeners.Length()); + mCountListeners -= listener->mListeners.Length(); + + if (IsChildProcess()) { + ShutdownActorListener(listener); + } + + mWindows.Remove(innerID); + } + + // This should not happen. + return NS_ERROR_FAILURE; +} + +void WebSocketEventService::Shutdown() { + MOZ_ASSERT(NS_IsMainThread()); + + if (gWebSocketEventService) { + nsCOMPtr obs = mozilla::services::GetObserverService(); + if (obs) { + obs->RemoveObserver(gWebSocketEventService, "xpcom-shutdown"); + obs->RemoveObserver(gWebSocketEventService, "inner-window-destroyed"); + } + + mWindows.Clear(); + gWebSocketEventService = nullptr; + } +} + +bool WebSocketEventService::HasListeners() const { return !!mCountListeners; } + +void WebSocketEventService::GetListeners( + uint64_t aInnerWindowID, + WebSocketEventService::WindowListeners& aListeners) const { + aListeners.Clear(); + + WindowListener* listener = mWindows.Get(aInnerWindowID); + if (!listener) { + return; + } + + aListeners.AppendElements(listener->mListeners); +} + +void WebSocketEventService::ShutdownActorListener(WindowListener* aListener) { + MOZ_ASSERT(aListener); + MOZ_ASSERT(aListener->mActor); + aListener->mActor->Close(); + aListener->mActor = nullptr; +} + +already_AddRefed WebSocketEventService::CreateFrameIfNeeded( + bool aFinBit, bool aRsvBit1, bool aRsvBit2, bool aRsvBit3, uint8_t aOpCode, + bool aMaskBit, uint32_t aMask, const nsCString& aPayload) { + if (!HasListeners()) { + return nullptr; + } + + return MakeAndAddRef(aFinBit, aRsvBit1, aRsvBit2, aRsvBit3, + aOpCode, aMaskBit, aMask, aPayload); +} + +already_AddRefed WebSocketEventService::CreateFrameIfNeeded( + bool aFinBit, bool aRsvBit1, bool aRsvBit2, bool aRsvBit3, uint8_t aOpCode, + bool aMaskBit, uint32_t aMask, uint8_t* aPayload, uint32_t aPayloadLength) { + if (!HasListeners()) { + return nullptr; + } + + nsAutoCString payloadStr; + if (NS_WARN_IF(!(payloadStr.Assign((const char*)aPayload, aPayloadLength, + mozilla::fallible)))) { + return nullptr; + } + + return MakeAndAddRef(aFinBit, aRsvBit1, aRsvBit2, aRsvBit3, + aOpCode, aMaskBit, aMask, payloadStr); +} + +already_AddRefed WebSocketEventService::CreateFrameIfNeeded( + bool aFinBit, bool aRsvBit1, bool aRsvBit2, bool aRsvBit3, uint8_t aOpCode, + bool aMaskBit, uint32_t aMask, uint8_t* aPayloadInHdr, + uint32_t aPayloadInHdrLength, uint8_t* aPayload, uint32_t aPayloadLength) { + if (!HasListeners()) { + return nullptr; + } + + uint32_t payloadLength = aPayloadLength + aPayloadInHdrLength; + + nsAutoCString payload; + if (NS_WARN_IF(!payload.SetLength(payloadLength, fallible))) { + return nullptr; + } + + char* payloadPtr = payload.BeginWriting(); + if (aPayloadInHdrLength) { + memcpy(payloadPtr, aPayloadInHdr, aPayloadInHdrLength); + } + + memcpy(payloadPtr + aPayloadInHdrLength, aPayload, aPayloadLength); + + return MakeAndAddRef(aFinBit, aRsvBit1, aRsvBit2, aRsvBit3, + aOpCode, aMaskBit, aMask, payload); +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/websocket/WebSocketEventService.h b/netwerk/protocol/websocket/WebSocketEventService.h new file mode 100644 index 0000000000..81e9ce8607 --- /dev/null +++ b/netwerk/protocol/websocket/WebSocketEventService.h @@ -0,0 +1,122 @@ +/* -*- 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/. */ + +#ifndef mozilla_net_WebSocketEventService_h +#define mozilla_net_WebSocketEventService_h + +#include "mozilla/AlreadyAddRefed.h" +#include "mozilla/Atomics.h" +#include "nsIWebSocketEventService.h" +#include "nsCOMPtr.h" +#include "nsClassHashtable.h" +#include "nsHashKeys.h" +#include "nsIObserver.h" +#include "nsISupportsImpl.h" +#include "nsTArray.h" + +class nsIWebSocketImpl; + +namespace mozilla { +namespace net { + +class WebSocketFrame; +class WebSocketEventListenerChild; + +class WebSocketEventService final : public nsIWebSocketEventService, + public nsIObserver { + friend class WebSocketBaseRunnable; + + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIOBSERVER + NS_DECL_NSIWEBSOCKETEVENTSERVICE + + static already_AddRefed Get(); + static already_AddRefed GetOrCreate(); + + void WebSocketCreated(uint32_t aWebSocketSerialID, uint64_t aInnerWindowID, + const nsAString& aURI, const nsACString& aProtocols, + nsIEventTarget* aTarget = nullptr); + + void WebSocketOpened(uint32_t aWebSocketSerialID, uint64_t aInnerWindowID, + const nsAString& aEffectiveURI, + const nsACString& aProtocols, + const nsACString& aExtensions, uint64_t aHttpChannelId, + nsIEventTarget* aTarget = nullptr); + + void WebSocketMessageAvailable(uint32_t aWebSocketSerialID, + uint64_t aInnerWindowID, + const nsACString& aData, uint16_t aMessageType, + nsIEventTarget* aTarget = nullptr); + + void WebSocketClosed(uint32_t aWebSocketSerialID, uint64_t aInnerWindowID, + bool aWasClean, uint16_t aCode, const nsAString& aReason, + nsIEventTarget* aTarget = nullptr); + + void FrameReceived(uint32_t aWebSocketSerialID, uint64_t aInnerWindowID, + already_AddRefed aFrame, + nsIEventTarget* aTarget = nullptr); + + void FrameSent(uint32_t aWebSocketSerialID, uint64_t aInnerWindowID, + already_AddRefed aFrame, + nsIEventTarget* aTarget = nullptr); + + void AssociateWebSocketImplWithSerialID(nsIWebSocketImpl* aWebSocketImpl, + uint32_t aWebSocketSerialID); + + already_AddRefed CreateFrameIfNeeded( + bool aFinBit, bool aRsvBit1, bool aRsvBit2, bool aRsvBit3, + uint8_t aOpCode, bool aMaskBit, uint32_t aMask, + const nsCString& aPayload); + + already_AddRefed CreateFrameIfNeeded( + bool aFinBit, bool aRsvBit1, bool aRsvBit2, bool aRsvBit3, + uint8_t aOpCode, bool aMaskBit, uint32_t aMask, uint8_t* aPayload, + uint32_t aPayloadLength); + + already_AddRefed CreateFrameIfNeeded( + bool aFinBit, bool aRsvBit1, bool aRsvBit2, bool aRsvBit3, + uint8_t aOpCode, bool aMaskBit, uint32_t aMask, uint8_t* aPayloadInHdr, + uint32_t aPayloadInHdrLength, uint8_t* aPayload, uint32_t aPayloadLength); + + private: + WebSocketEventService(); + ~WebSocketEventService(); + + bool HasListeners() const; + void Shutdown(); + + typedef nsTArray> WindowListeners; + + nsDataHashtable mWebSocketImplMap; + + struct WindowListener { + WindowListeners mListeners; + RefPtr mActor; + }; + + void GetListeners(uint64_t aInnerWindowID, WindowListeners& aListeners) const; + + void ShutdownActorListener(WindowListener* aListener); + + // Used only on the main-thread. + nsClassHashtable mWindows; + + Atomic mCountListeners; +}; + +} // namespace net +} // namespace mozilla + +/** + * Casting WebSocketEventService to nsISupports is ambiguous. + * This method handles that. + */ +inline nsISupports* ToSupports(mozilla::net::WebSocketEventService* p) { + return NS_ISUPPORTS_CAST(nsIWebSocketEventService*, p); +} + +#endif // mozilla_net_WebSocketEventService_h diff --git a/netwerk/protocol/websocket/WebSocketFrame.cpp b/netwerk/protocol/websocket/WebSocketFrame.cpp new file mode 100644 index 0000000000..c73c170c49 --- /dev/null +++ b/netwerk/protocol/websocket/WebSocketFrame.cpp @@ -0,0 +1,158 @@ +/* -*- 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 "WebSocketFrame.h" + +#include "ErrorList.h" +#include "MainThreadUtils.h" +#include "chrome/common/ipc_message_utils.h" +#include "mozilla/Assertions.h" +#include "nsDOMNavigationTiming.h" +#include "nsSocketTransportService2.h" +#include "nscore.h" +#include "prtime.h" + +namespace mozilla { +namespace net { + +NS_INTERFACE_MAP_BEGIN(WebSocketFrame) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIWebSocketFrame) + NS_INTERFACE_MAP_ENTRY(nsIWebSocketFrame) +NS_INTERFACE_MAP_END + +NS_IMPL_ADDREF(WebSocketFrame) +NS_IMPL_RELEASE(WebSocketFrame) + +WebSocketFrame::WebSocketFrame(const WebSocketFrameData& aData) + : mData(aData) {} + +WebSocketFrame::WebSocketFrame(bool aFinBit, bool aRsvBit1, bool aRsvBit2, + bool aRsvBit3, uint8_t aOpCode, bool aMaskBit, + uint32_t aMask, const nsCString& aPayload) + : mData(PR_Now(), aFinBit, aRsvBit1, aRsvBit2, aRsvBit3, aOpCode, aMaskBit, + aMask, aPayload) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + mData.mTimeStamp = PR_Now(); +} + +#define WSF_GETTER(method, value, type) \ + NS_IMETHODIMP \ + WebSocketFrame::method(type* aValue) { \ + MOZ_ASSERT(NS_IsMainThread()); \ + if (!aValue) { \ + return NS_ERROR_FAILURE; \ + } \ + *aValue = value; \ + return NS_OK; \ + } + +WSF_GETTER(GetTimeStamp, mData.mTimeStamp, DOMHighResTimeStamp); +WSF_GETTER(GetFinBit, mData.mFinBit, bool); +WSF_GETTER(GetRsvBit1, mData.mRsvBit1, bool); +WSF_GETTER(GetRsvBit2, mData.mRsvBit2, bool); +WSF_GETTER(GetRsvBit3, mData.mRsvBit3, bool); +WSF_GETTER(GetOpCode, mData.mOpCode, uint16_t); +WSF_GETTER(GetMaskBit, mData.mMaskBit, bool); +WSF_GETTER(GetMask, mData.mMask, uint32_t); + +#undef WSF_GETTER + +NS_IMETHODIMP +WebSocketFrame::GetPayload(nsACString& aValue) { + MOZ_ASSERT(NS_IsMainThread()); + aValue = mData.mPayload; + return NS_OK; +} + +WebSocketFrameData::WebSocketFrameData() + : mTimeStamp(0), + mFinBit(false), + mRsvBit1(false), + mRsvBit2(false), + mRsvBit3(false), + mMaskBit(false), + mOpCode(0), + mMask(0) { + MOZ_COUNT_CTOR(WebSocketFrameData); +} + +WebSocketFrameData::WebSocketFrameData(DOMHighResTimeStamp aTimeStamp, + bool aFinBit, bool aRsvBit1, + bool aRsvBit2, bool aRsvBit3, + uint8_t aOpCode, bool aMaskBit, + uint32_t aMask, + const nsCString& aPayload) + : mTimeStamp(aTimeStamp), + mFinBit(aFinBit), + mRsvBit1(aRsvBit1), + mRsvBit2(aRsvBit2), + mRsvBit3(aRsvBit3), + mMaskBit(aMaskBit), + mOpCode(aOpCode), + mMask(aMask), + mPayload(aPayload) { + MOZ_COUNT_CTOR(WebSocketFrameData); +} + +WebSocketFrameData::WebSocketFrameData(const WebSocketFrameData& aData) + : mTimeStamp(aData.mTimeStamp), + mFinBit(aData.mFinBit), + mRsvBit1(aData.mRsvBit1), + mRsvBit2(aData.mRsvBit2), + mRsvBit3(aData.mRsvBit3), + mMaskBit(aData.mMaskBit), + mOpCode(aData.mOpCode), + mMask(aData.mMask), + mPayload(aData.mPayload) { + MOZ_COUNT_CTOR(WebSocketFrameData); +} + +WebSocketFrameData::~WebSocketFrameData() { + MOZ_COUNT_DTOR(WebSocketFrameData); +} + +void WebSocketFrameData::WriteIPCParams(IPC::Message* aMessage) const { + WriteParam(aMessage, mTimeStamp); + WriteParam(aMessage, mFinBit); + WriteParam(aMessage, mRsvBit1); + WriteParam(aMessage, mRsvBit2); + WriteParam(aMessage, mRsvBit3); + WriteParam(aMessage, mMaskBit); + WriteParam(aMessage, mOpCode); + WriteParam(aMessage, mMask); + WriteParam(aMessage, mPayload); +} + +bool WebSocketFrameData::ReadIPCParams(const IPC::Message* aMessage, + PickleIterator* aIter) { + if (!ReadParam(aMessage, aIter, &mTimeStamp)) { + return false; + } + +#define ReadParamHelper(x) \ + { \ + bool bit; \ + if (!ReadParam(aMessage, aIter, &bit)) { \ + return false; \ + } \ + x = bit; \ + } + + ReadParamHelper(mFinBit); + ReadParamHelper(mRsvBit1); + ReadParamHelper(mRsvBit2); + ReadParamHelper(mRsvBit3); + ReadParamHelper(mMaskBit); + +#undef ReadParamHelper + + return ReadParam(aMessage, aIter, &mOpCode) && + ReadParam(aMessage, aIter, &mMask) && + ReadParam(aMessage, aIter, &mPayload); +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/websocket/WebSocketFrame.h b/netwerk/protocol/websocket/WebSocketFrame.h new file mode 100644 index 0000000000..3f96d9a131 --- /dev/null +++ b/netwerk/protocol/websocket/WebSocketFrame.h @@ -0,0 +1,100 @@ +/* -*- 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/. */ + +#ifndef mozilla_net_WebSocketFrame_h +#define mozilla_net_WebSocketFrame_h + +#include +#include "nsISupports.h" +#include "nsIWebSocketEventService.h" +#include "nsString.h" + +class PickleIterator; + +// Avoid including nsDOMNavigationTiming.h here, where the canonical definition +// of DOMHighResTimeStamp resides. +typedef double DOMHighResTimeStamp; + +namespace IPC { +class Message; +template +struct ParamTraits; +} // namespace IPC + +namespace mozilla { +namespace net { + +class WebSocketFrameData final { + public: + WebSocketFrameData(); + + explicit WebSocketFrameData(const WebSocketFrameData& aData); + + WebSocketFrameData(DOMHighResTimeStamp aTimeStamp, bool aFinBit, + bool aRsvBit1, bool aRsvBit2, bool aRsvBit3, + uint8_t aOpCode, bool aMaskBit, uint32_t aMask, + const nsCString& aPayload); + + ~WebSocketFrameData(); + + // For IPC serialization + void WriteIPCParams(IPC::Message* aMessage) const; + bool ReadIPCParams(const IPC::Message* aMessage, PickleIterator* aIter); + + DOMHighResTimeStamp mTimeStamp; + + bool mFinBit : 1; + bool mRsvBit1 : 1; + bool mRsvBit2 : 1; + bool mRsvBit3 : 1; + bool mMaskBit : 1; + uint8_t mOpCode; + + uint32_t mMask; + + nsCString mPayload; +}; + +class WebSocketFrame final : public nsIWebSocketFrame { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIWEBSOCKETFRAME + + explicit WebSocketFrame(const WebSocketFrameData& aData); + + WebSocketFrame(bool aFinBit, bool aRsvBit1, bool aRsvBit2, bool aRsvBit3, + uint8_t aOpCode, bool aMaskBit, uint32_t aMask, + const nsCString& aPayload); + + const WebSocketFrameData& Data() const { return mData; } + + private: + ~WebSocketFrame() = default; + + WebSocketFrameData mData; +}; + +} // namespace net +} // namespace mozilla + +namespace IPC { +template <> +struct ParamTraits { + typedef mozilla::net::WebSocketFrameData paramType; + + static void Write(Message* aMsg, const paramType& aParam) { + aParam.WriteIPCParams(aMsg); + } + + static bool Read(const Message* aMsg, PickleIterator* aIter, + paramType* aResult) { + return aResult->ReadIPCParams(aMsg, aIter); + } +}; + +} // namespace IPC + +#endif // mozilla_net_WebSocketFrame_h diff --git a/netwerk/protocol/websocket/WebSocketLog.h b/netwerk/protocol/websocket/WebSocketLog.h new file mode 100644 index 0000000000..80122249c2 --- /dev/null +++ b/netwerk/protocol/websocket/WebSocketLog.h @@ -0,0 +1,24 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et 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/. */ + +#ifndef WebSocketLog_h +#define WebSocketLog_h + +#include "base/basictypes.h" +#include "mozilla/Logging.h" +#include "mozilla/net/NeckoChild.h" + +namespace mozilla { +namespace net { +extern LazyLogModule webSocketLog; +} +} // namespace mozilla + +#undef LOG +#define LOG(args) \ + MOZ_LOG(mozilla::net::webSocketLog, mozilla::LogLevel::Debug, args) + +#endif diff --git a/netwerk/protocol/websocket/moz.build b/netwerk/protocol/websocket/moz.build new file mode 100644 index 0000000000..c650e390b6 --- /dev/null +++ b/netwerk/protocol/websocket/moz.build @@ -0,0 +1,62 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +with Files("**"): + BUG_COMPONENT = ("Core", "Networking: WebSockets") + +XPIDL_SOURCES += [ + "nsITransportProvider.idl", + "nsIWebSocketChannel.idl", + "nsIWebSocketEventService.idl", + "nsIWebSocketImpl.idl", + "nsIWebSocketListener.idl", +] + +XPIDL_MODULE = "necko_websocket" + +EXPORTS.mozilla.net += [ + "BaseWebSocketChannel.h", + "IPCTransportProvider.h", + "WebSocketChannel.h", + "WebSocketChannelChild.h", + "WebSocketChannelParent.h", + "WebSocketEventListenerChild.h", + "WebSocketEventListenerParent.h", + "WebSocketEventService.h", + "WebSocketFrame.h", +] + +UNIFIED_SOURCES += [ + "BaseWebSocketChannel.cpp", + "IPCTransportProvider.cpp", + "WebSocketChannel.cpp", + "WebSocketChannelChild.cpp", + "WebSocketChannelParent.cpp", + "WebSocketEventListenerChild.cpp", + "WebSocketEventListenerParent.cpp", + "WebSocketEventService.cpp", + "WebSocketFrame.cpp", +] + +IPDL_SOURCES += [ + "PTransportProvider.ipdl", + "PWebSocket.ipdl", + "PWebSocketEventListener.ipdl", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" + +LOCAL_INCLUDES += [ + "/dom/base", + "/netwerk/base", +] + +if CONFIG["CC_TYPE"] in ("clang", "gcc"): + CXXFLAGS += ["-Wno-error=shadow"] + +include("/tools/fuzzing/libfuzzer-config.mozbuild") diff --git a/netwerk/protocol/websocket/nsITransportProvider.idl b/netwerk/protocol/websocket/nsITransportProvider.idl new file mode 100644 index 0000000000..8b60eabd03 --- /dev/null +++ b/netwerk/protocol/websocket/nsITransportProvider.idl @@ -0,0 +1,36 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* vim: set sw=4 ts=4 et 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/. */ + +interface nsIHttpUpgradeListener; + +#include "nsISupports.idl" + +%{C++ +namespace mozilla { +namespace net { +class PTransportProviderChild; +} +} +%} + +[ptr] native PTransportProviderChild(mozilla::net::PTransportProviderChild); + +/** + * An interface which can be used to asynchronously request a nsITransport + * together with the input and output streams that go together with it. + */ +[scriptable, uuid(6fcec704-cfd2-46ef-a394-a64d5cb1475c)] +interface nsITransportProvider : nsISupports +{ + // This must not be called in a child process since transport + // objects are not accessible there. Call getIPCChild instead. + [must_use] void setListener(in nsIHttpUpgradeListener listener); + + // This must be implemented by nsITransportProvider objects running + // in the child process. It must return null when called in the parent + // process. + [noscript, must_use] PTransportProviderChild getIPCChild(); +}; diff --git a/netwerk/protocol/websocket/nsIWebSocketChannel.idl b/netwerk/protocol/websocket/nsIWebSocketChannel.idl new file mode 100644 index 0000000000..5c2137b01b --- /dev/null +++ b/netwerk/protocol/websocket/nsIWebSocketChannel.idl @@ -0,0 +1,252 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* vim: set sw=4 ts=4 et 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/. */ + +interface nsICookieJarSettings; +interface nsIURI; +interface nsIInterfaceRequestor; +interface nsILoadGroup; +interface nsIWebSocketListener; +interface nsIInputStream; +interface nsILoadInfo; +interface nsIPrincipal; +interface nsITransportProvider; + +webidl Node; + +#include "nsISupports.idl" +#include "nsIContentPolicy.idl" + +/** + * Low-level websocket API: handles network protocol. + * + * This is primarly intended for use by the higher-level nsIWebSocket.idl. + * We are also making it scriptable for now, but this may change once we have + * WebSockets for Workers. + */ +[scriptable, builtinclass, uuid(ce71d028-322a-4105-a947-a894689b52bf)] +interface nsIWebSocketChannel : nsISupports +{ + /** + * The original URI used to construct the protocol connection. This is used + * in the case of a redirect or URI "resolution" (e.g. resolving a + * resource: URI to a file: URI) so that the original pre-redirect + * URI can still be obtained. This is never null. + */ + [must_use] readonly attribute nsIURI originalURI; + + /** + * The readonly URI corresponding to the protocol connection after any + * redirections are completed. + */ + [must_use] readonly attribute nsIURI URI; + + /** + * The notification callbacks for authorization, etc.. + */ + [must_use] attribute nsIInterfaceRequestor notificationCallbacks; + + /** + * Transport-level security information (if any) + */ + [must_use] readonly attribute nsISupports securityInfo; + + /** + * The load group of of the websocket + */ + [must_use] attribute nsILoadGroup loadGroup; + + /** + * The load info of the websocket + */ + [must_use] attribute nsILoadInfo loadInfo; + + /** + * Sec-Websocket-Protocol value + */ + [must_use] attribute ACString protocol; + + /** + * Sec-Websocket-Extensions response header value + */ + [must_use] readonly attribute ACString extensions; + + /** + * The channelId of the underlying http channel. + * It's available only after nsIWebSocketListener::onStart + */ + [must_use] readonly attribute uint64_t httpChannelId; + + /** + * Init the WebSocketChannel with LoadInfo arguments. + * @param aLoadingNode + * @param aLoadingPrincipal + * @param aTriggeringPrincipal + * @param aCookieJarSettings + * @param aSecurityFlags + * @param aContentPolicyType + * These will be used as values for the nsILoadInfo object on the + * created channel. For details, see nsILoadInfo in nsILoadInfo.idl + * @return reference to the new nsIChannel object + * + * Keep in mind that URIs coming from a webpage should *never* use the + * systemPrincipal as the loadingPrincipal. + * + * Please note, if you provide both a loadingNode and a loadingPrincipal, + * then loadingPrincipal must be equal to loadingNode->NodePrincipal(). + * But less error prone is to just supply a loadingNode. + */ + [notxpcom] nsresult initLoadInfoNative(in Node aLoadingNode, + in nsIPrincipal aLoadingPrincipal, + in nsIPrincipal aTriggeringPrincipal, + in nsICookieJarSettings aCookieJarSettings, + in unsigned long aSecurityFlags, + in nsContentPolicyType aContentPolicyType, + in unsigned long aSandboxFlags); + + /** + * Similar to the previous one but without nsICookieJarSettings. + * This method is used by JS code where nsICookieJarSettings is not exposed. + */ + [must_use] void initLoadInfo(in Node aLoadingNode, + in nsIPrincipal aLoadingPrincipal, + in nsIPrincipal aTriggeringPrincipal, + in unsigned long aSecurityFlags, + in nsContentPolicyType aContentPolicyType); + + /** + * Asynchronously open the websocket connection. Received messages are fed + * to the socket listener as they arrive. The socket listener's methods + * are called on the thread that calls asyncOpen and are not called until + * after asyncOpen returns. If asyncOpen returns successfully, the + * protocol implementation promises to call at least onStop on the listener. + * + * NOTE: Implementations should throw NS_ERROR_ALREADY_OPENED if the + * websocket connection is reopened. + * + * @param aURI the uri of the websocket protocol - may be redirected + * @param aOrigin the uri of the originating resource + * @param aInnerWindowID the inner window ID + * @param aListener the nsIWebSocketListener implementation + * @param aContext an opaque parameter forwarded to aListener's methods + */ + [must_use] void asyncOpen(in nsIURI aURI, + in ACString aOrigin, + in unsigned long long aInnerWindowID, + in nsIWebSocketListener aListener, + in nsISupports aContext); + + /* + * Close the websocket connection for writing - no more calls to sendMsg + * or sendBinaryMsg should be made after calling this. The listener object + * may receive more messages if a server close has not yet been received. + * + * @param aCode the websocket closing handshake close code. Set to 0 if + * you are not providing a code. + * @param aReason the websocket closing handshake close reason + */ + [must_use] void close(in unsigned short aCode, in AUTF8String aReason); + + // section 7.4.1 defines these close codes + const unsigned short CLOSE_NORMAL = 1000; + const unsigned short CLOSE_GOING_AWAY = 1001; + const unsigned short CLOSE_PROTOCOL_ERROR = 1002; + const unsigned short CLOSE_UNSUPPORTED_DATATYPE = 1003; + // code 1004 is reserved + const unsigned short CLOSE_NO_STATUS = 1005; + const unsigned short CLOSE_ABNORMAL = 1006; + const unsigned short CLOSE_INVALID_PAYLOAD = 1007; + const unsigned short CLOSE_POLICY_VIOLATION = 1008; + const unsigned short CLOSE_TOO_LARGE = 1009; + const unsigned short CLOSE_EXTENSION_MISSING = 1010; + // Initially used just for server-side internal errors: adopted later for + // client-side errors too (not clear if will make into spec: see + // http://www.ietf.org/mail-archive/web/hybi/current/msg09372.html + const unsigned short CLOSE_INTERNAL_ERROR = 1011; + // MUST NOT be set as a status code in Close control frame by an endpoint: + // To be used if TLS handshake failed (ex: server certificate unverifiable) + const unsigned short CLOSE_TLS_FAILED = 1015; + + /** + * Use to send text message down the connection to WebSocket peer. + * + * @param aMsg the utf8 string to send + */ + [must_use] void sendMsg(in AUTF8String aMsg); + + /** + * Use to send binary message down the connection to WebSocket peer. + * + * @param aMsg the data to send + */ + [must_use] void sendBinaryMsg(in ACString aMsg); + + /** + * Use to send a binary stream (Blob) to Websocket peer. + * + * @param aStream The input stream to be sent. + */ + [must_use] void sendBinaryStream(in nsIInputStream aStream, + in unsigned long length); + + /** + * This value determines how often (in seconds) 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. + */ + [must_use] attribute unsigned long pingInterval; + + /** + * This value determines how long (in seconds) the websocket waits for + * the server to reply to a ping that has been sent before considering the + * connection broken. + * + * This value can currently only be set before asyncOpen is called, else + * NS_ERROR_IN_PROGRESS is thrown. + */ + [must_use] attribute unsigned long pingTimeout; + + /** + * Unique ID for this channel. It's not readonly because when the channel is + * created via IPC, the serial number is received from the child process. + */ + [must_use] attribute unsigned long serial; + + /** + * Set a nsITransportProvider and negotated extensions to be used by this + * channel. Calling this function also means that this channel will + * implement the server-side part of a websocket connection rather than the + * client-side part. + */ + [must_use] void setServerParameters(in nsITransportProvider aProvider, + in ACString aNegotiatedExtensions); + +%{C++ + inline uint32_t Serial() + { + uint32_t serial; + nsresult rv = GetSerial(&serial); + if (NS_WARN_IF(NS_FAILED(rv))) { + return 0; + } + return serial; + } + + inline uint64_t HttpChannelId() + { + uint64_t httpChannelId; + nsresult rv = GetHttpChannelId(&httpChannelId); + if (NS_WARN_IF(NS_FAILED(rv))) { + return 0; + } + return httpChannelId; + } +%} +}; diff --git a/netwerk/protocol/websocket/nsIWebSocketEventService.idl b/netwerk/protocol/websocket/nsIWebSocketEventService.idl new file mode 100644 index 0000000000..9763850609 --- /dev/null +++ b/netwerk/protocol/websocket/nsIWebSocketEventService.idl @@ -0,0 +1,87 @@ +/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 "domstubs.idl" +#include "nsISupports.idl" + +interface nsIWeakReference; + +[scriptable, builtinclass, uuid(6714a6be-2265-4f73-a988-d78a12416037)] +interface nsIWebSocketFrame : nsISupports +{ + [must_use] readonly attribute DOMHighResTimeStamp timeStamp; + + [must_use] readonly attribute boolean finBit; + + [must_use] readonly attribute boolean rsvBit1; + [must_use] readonly attribute boolean rsvBit2; + [must_use] readonly attribute boolean rsvBit3; + + [must_use] readonly attribute unsigned short opCode; + + [must_use] readonly attribute boolean maskBit; + + [must_use] readonly attribute unsigned long mask; + + [must_use] readonly attribute ACString payload; + + // Non-Control opCode values: + const unsigned short OPCODE_CONTINUATION = 0x0; + const unsigned short OPCODE_TEXT = 0x1; + const unsigned short OPCODE_BINARY = 0x2; + + // Control opCode values: + const unsigned short OPCODE_CLOSE = 0x8; + const unsigned short OPCODE_PING = 0x9; + const unsigned short OPCODE_PONG = 0xA; +}; + +[scriptable, uuid(e7c005ab-e694-489b-b741-96db43ffb16f)] +interface nsIWebSocketEventListener : nsISupports +{ + [must_use] void webSocketCreated(in unsigned long aWebSocketSerialID, + in AString aURI, + in ACString aProtocols); + + [must_use] void webSocketOpened(in unsigned long aWebSocketSerialID, + in AString aEffectiveURI, + in ACString aProtocols, + in ACString aExtensions, + in uint64_t aHttpChannelId); + + const unsigned short TYPE_STRING = 0x0; + const unsigned short TYPE_BLOB = 0x1; + const unsigned short TYPE_ARRAYBUFFER = 0x2; + + [must_use] void webSocketMessageAvailable(in unsigned long aWebSocketSerialID, + in ACString aMessage, + in unsigned short aType); + + [must_use] void webSocketClosed(in unsigned long aWebSocketSerialID, + in boolean aWasClean, + in unsigned short aCode, + in AString aReason); + + [must_use] void frameReceived(in unsigned long aWebSocketSerialID, + in nsIWebSocketFrame aFrame); + + [must_use] void frameSent(in unsigned long aWebSocketSerialID, + in nsIWebSocketFrame aFrame); +}; + +[scriptable, builtinclass, uuid(b89d1b90-2cf3-4d8f-ac21-5aedfb25c760)] +interface nsIWebSocketEventService : nsISupports +{ + [must_use] void sendMessage(in unsigned long aWebSocketSerialID, + in AString aMessage); + + [must_use] void addListener(in unsigned long long aInnerWindowID, + in nsIWebSocketEventListener aListener); + + [must_use] void removeListener(in unsigned long long aInnerWindowID, + in nsIWebSocketEventListener aListener); + + [must_use] bool hasListenerFor(in unsigned long long aInnerWindowID); +}; diff --git a/netwerk/protocol/websocket/nsIWebSocketImpl.idl b/netwerk/protocol/websocket/nsIWebSocketImpl.idl new file mode 100644 index 0000000000..ab4177a7e6 --- /dev/null +++ b/netwerk/protocol/websocket/nsIWebSocketImpl.idl @@ -0,0 +1,18 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* vim: set sw=4 ts=4 et 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 "nsISupports.idl" + +[scriptable, uuid(db1f4e2b-3cff-4615-a03c-341fda66c53d)] +interface nsIWebSocketImpl : nsISupports +{ + /** + * Called to send message of type string through web socket + * + * @param aMessage the message to send + */ + [must_use] void sendMessage(in AString aMessage); +}; diff --git a/netwerk/protocol/websocket/nsIWebSocketListener.idl b/netwerk/protocol/websocket/nsIWebSocketListener.idl new file mode 100644 index 0000000000..4a8416b758 --- /dev/null +++ b/netwerk/protocol/websocket/nsIWebSocketListener.idl @@ -0,0 +1,96 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* vim: set sw=4 ts=4 et 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 "nsISupports.idl" + +/** + * nsIWebSocketListener: passed to nsIWebSocketChannel::AsyncOpen. Receives + * websocket traffic events as they arrive. + */ +[scriptable, uuid(d74c96b2-65b3-4e39-9e39-c577de5d7a73)] +interface nsIWebSocketListener : nsISupports +{ + /** + * Called to signify the establishment of the message stream. + * + * Unlike most other networking channels (which use nsIRequestObserver + * instead of this class), we do not guarantee that OnStart is always + * called: OnStop is called without calling this function if errors occur + * during connection setup. If the websocket connection is successful, + * OnStart will be called before any other calls to this API. + * + * @param aContext user defined context + */ + [must_use] void onStart(in nsISupports aContext); + + /** + * Called to signify the completion of the message stream. + * OnStop is the final notification the listener will receive and it + * completes the WebSocket connection: after it returns the + * nsIWebSocketChannel will release its reference to the listener. + * + * Note: this event can be received in error cases even if + * nsIWebSocketChannel::Close() has not been called. + * + * @param aContext user defined context + * @param aStatusCode reason for stopping (NS_OK if completed successfully) + */ + [must_use] void onStop(in nsISupports aContext, + in nsresult aStatusCode); + + /** + * Called to deliver text message. + * + * @param aContext user defined context + * @param aMsg the message data + */ + [must_use] void onMessageAvailable(in nsISupports aContext, + in AUTF8String aMsg); + + /** + * Called to deliver binary message. + * + * @param aContext user defined context + * @param aMsg the message data + */ + [must_use] void onBinaryMessageAvailable(in nsISupports aContext, + in ACString aMsg); + + /** + * Called to acknowledge message sent via sendMsg() or sendBinaryMsg. + * + * @param aContext user defined context + * @param aSize number of bytes placed in OS send buffer + */ + [must_use] void onAcknowledge(in nsISupports aContext, in uint32_t aSize); + + /** + * Called to inform receipt of WebSocket Close message from server. + * In the case of errors onStop() can be called without ever + * receiving server close. + * + * No additional messages through onMessageAvailable(), + * onBinaryMessageAvailable() or onAcknowledge() will be delievered + * to the listener after onServerClose(), though outgoing messages can still + * be sent through the nsIWebSocketChannel connection. + * + * @param aContext user defined context + * @param aCode the websocket closing handshake close code. + * @param aReason the websocket closing handshake close reason + */ + [must_use] void onServerClose(in nsISupports aContext, + in unsigned short aCode, + in AUTF8String aReason); + + /** + * Called to inform an error is happened. The connection will be closed + * when this is called. + */ + [must_use] void OnError(); + +}; + + -- cgit v1.2.3