From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 02:47:55 +0200 Subject: Adding upstream version 124.0.1. Signed-off-by: Daniel Baumann --- netwerk/protocol/about/moz.build | 32 + netwerk/protocol/about/nsAboutBlank.cpp | 52 + netwerk/protocol/about/nsAboutBlank.h | 32 + netwerk/protocol/about/nsAboutCache.cpp | 533 + netwerk/protocol/about/nsAboutCache.h | 199 + netwerk/protocol/about/nsAboutCacheEntry.cpp | 559 + netwerk/protocol/about/nsAboutCacheEntry.h | 86 + netwerk/protocol/about/nsAboutProtocolHandler.cpp | 428 + netwerk/protocol/about/nsAboutProtocolHandler.h | 139 + netwerk/protocol/about/nsAboutProtocolUtils.h | 65 + netwerk/protocol/about/nsIAboutModule.idl | 113 + 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 | 146 + netwerk/protocol/data/nsDataChannel.h | 31 + netwerk/protocol/data/nsDataHandler.cpp | 272 + netwerk/protocol/data/nsDataHandler.h | 56 + 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 | 40 + netwerk/protocol/file/nsFileChannel.cpp | 569 + netwerk/protocol/file/nsFileChannel.h | 59 + netwerk/protocol/file/nsFileProtocolHandler.cpp | 266 + netwerk/protocol/file/nsFileProtocolHandler.h | 28 + netwerk/protocol/file/nsIFileChannel.idl | 17 + netwerk/protocol/file/nsIFileProtocolHandler.idl | 102 + netwerk/protocol/gio/GIOChannelChild.cpp | 458 + netwerk/protocol/gio/GIOChannelChild.h | 111 + netwerk/protocol/gio/GIOChannelParent.cpp | 324 + netwerk/protocol/gio/GIOChannelParent.h | 80 + netwerk/protocol/gio/PGIOChannel.ipdl | 51 + netwerk/protocol/gio/components.conf | 24 + netwerk/protocol/gio/moz.build | 42 + netwerk/protocol/gio/nsGIOProtocolHandler.cpp | 1027 ++ netwerk/protocol/gio/nsGIOProtocolHandler.h | 38 + netwerk/protocol/http/ASpdySession.cpp | 94 + netwerk/protocol/http/ASpdySession.h | 115 + netwerk/protocol/http/AltDataOutputStreamChild.cpp | 204 + netwerk/protocol/http/AltDataOutputStreamChild.h | 51 + .../protocol/http/AltDataOutputStreamParent.cpp | 87 + netwerk/protocol/http/AltDataOutputStreamParent.h | 52 + netwerk/protocol/http/AltServiceChild.cpp | 111 + netwerk/protocol/http/AltServiceChild.h | 41 + netwerk/protocol/http/AltServiceParent.cpp | 49 + netwerk/protocol/http/AltServiceParent.h | 39 + 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 | 1354 +++ netwerk/protocol/http/AlternateServices.h | 271 + .../protocol/http/BackgroundChannelRegistrar.cpp | 99 + netwerk/protocol/http/BackgroundChannelRegistrar.h | 55 + .../protocol/http/BackgroundDataBridgeChild.cpp | 60 + netwerk/protocol/http/BackgroundDataBridgeChild.h | 41 + .../protocol/http/BackgroundDataBridgeParent.cpp | 63 + netwerk/protocol/http/BackgroundDataBridgeParent.h | 36 + netwerk/protocol/http/BinaryHttpRequest.cpp | 51 + netwerk/protocol/http/BinaryHttpRequest.h | 48 + netwerk/protocol/http/CacheControlParser.cpp | 134 + netwerk/protocol/http/CacheControlParser.h | 52 + netwerk/protocol/http/CachePushChecker.cpp | 248 + netwerk/protocol/http/CachePushChecker.h | 45 + netwerk/protocol/http/ClassOfService.h | 76 + netwerk/protocol/http/ConnectionDiagnostics.cpp | 239 + netwerk/protocol/http/ConnectionEntry.cpp | 1102 ++ netwerk/protocol/http/ConnectionEntry.h | 231 + netwerk/protocol/http/ConnectionHandle.cpp | 98 + netwerk/protocol/http/ConnectionHandle.h | 41 + netwerk/protocol/http/DnsAndConnectSocket.cpp | 1391 +++ netwerk/protocol/http/DnsAndConnectSocket.h | 274 + netwerk/protocol/http/EarlyHintPreconnect.cpp | 106 + netwerk/protocol/http/EarlyHintPreconnect.h | 24 + netwerk/protocol/http/EarlyHintPreloader.cpp | 846 ++ netwerk/protocol/http/EarlyHintPreloader.h | 199 + netwerk/protocol/http/EarlyHintRegistrar.cpp | 130 + netwerk/protocol/http/EarlyHintRegistrar.h | 82 + netwerk/protocol/http/EarlyHintsService.cpp | 191 + netwerk/protocol/http/EarlyHintsService.h | 59 + netwerk/protocol/http/HPKEConfigManager.sys.mjs | 42 + netwerk/protocol/http/HTTPSRecordResolver.cpp | 117 + netwerk/protocol/http/HTTPSRecordResolver.h | 44 + netwerk/protocol/http/Http2Compression.cpp | 1458 +++ netwerk/protocol/http/Http2Compression.h | 207 + netwerk/protocol/http/Http2HuffmanIncoming.h | 709 ++ netwerk/protocol/http/Http2HuffmanOutgoing.h | 85 + netwerk/protocol/http/Http2Push.cpp | 557 + netwerk/protocol/http/Http2Push.h | 182 + netwerk/protocol/http/Http2Session.cpp | 4513 ++++++++ netwerk/protocol/http/Http2Session.h | 626 ++ netwerk/protocol/http/Http2Stream.cpp | 298 + netwerk/protocol/http/Http2Stream.h | 59 + netwerk/protocol/http/Http2StreamBase.cpp | 1324 +++ netwerk/protocol/http/Http2StreamBase.h | 377 + netwerk/protocol/http/Http2StreamTunnel.cpp | 764 ++ netwerk/protocol/http/Http2StreamTunnel.h | 151 + netwerk/protocol/http/Http3Session.cpp | 2522 +++++ netwerk/protocol/http/Http3Session.h | 388 + netwerk/protocol/http/Http3Stream.cpp | 533 + netwerk/protocol/http/Http3Stream.h | 165 + netwerk/protocol/http/Http3StreamBase.h | 70 + netwerk/protocol/http/Http3WebTransportSession.cpp | 518 + netwerk/protocol/http/Http3WebTransportSession.h | 123 + netwerk/protocol/http/Http3WebTransportStream.cpp | 667 ++ netwerk/protocol/http/Http3WebTransportStream.h | 136 + netwerk/protocol/http/HttpAuthUtils.cpp | 171 + netwerk/protocol/http/HttpAuthUtils.h | 33 + .../protocol/http/HttpBackgroundChannelChild.cpp | 499 + netwerk/protocol/http/HttpBackgroundChannelChild.h | 156 + .../protocol/http/HttpBackgroundChannelParent.cpp | 526 + .../protocol/http/HttpBackgroundChannelParent.h | 124 + netwerk/protocol/http/HttpBaseChannel.cpp | 6479 ++++++++++++ netwerk/protocol/http/HttpBaseChannel.h | 1194 +++ netwerk/protocol/http/HttpChannelChild.cpp | 3391 ++++++ netwerk/protocol/http/HttpChannelChild.h | 481 + netwerk/protocol/http/HttpChannelParams.ipdlh | 71 + netwerk/protocol/http/HttpChannelParent.cpp | 2202 ++++ netwerk/protocol/http/HttpChannelParent.h | 333 + netwerk/protocol/http/HttpConnectionBase.cpp | 101 + netwerk/protocol/http/HttpConnectionBase.h | 246 + netwerk/protocol/http/HttpConnectionMgrChild.cpp | 191 + netwerk/protocol/http/HttpConnectionMgrChild.h | 52 + netwerk/protocol/http/HttpConnectionMgrParent.cpp | 319 + netwerk/protocol/http/HttpConnectionMgrParent.h | 43 + netwerk/protocol/http/HttpConnectionMgrShell.h | 229 + netwerk/protocol/http/HttpConnectionUDP.cpp | 706 ++ netwerk/protocol/http/HttpConnectionUDP.h | 138 + netwerk/protocol/http/HttpInfo.cpp | 16 + netwerk/protocol/http/HttpInfo.h | 24 + netwerk/protocol/http/HttpLog.h | 73 + netwerk/protocol/http/HttpTrafficAnalyzer.cpp | 269 + netwerk/protocol/http/HttpTrafficAnalyzer.h | 51 + netwerk/protocol/http/HttpTrafficAnalyzer.inc | 106 + netwerk/protocol/http/HttpTransactionChild.cpp | 665 ++ netwerk/protocol/http/HttpTransactionChild.h | 127 + netwerk/protocol/http/HttpTransactionParent.cpp | 924 ++ netwerk/protocol/http/HttpTransactionParent.h | 197 + netwerk/protocol/http/HttpTransactionShell.h | 242 + netwerk/protocol/http/HttpWinUtils.cpp | 111 + netwerk/protocol/http/HttpWinUtils.h | 18 + netwerk/protocol/http/InterceptedHttpChannel.cpp | 1714 +++ netwerk/protocol/http/InterceptedHttpChannel.h | 316 + netwerk/protocol/http/MockHttpAuth.cpp | 46 + netwerk/protocol/http/MockHttpAuth.h | 31 + netwerk/protocol/http/NetworkMarker.cpp | 188 + netwerk/protocol/http/NetworkMarker.h | 40 + netwerk/protocol/http/NullHttpChannel.cpp | 865 ++ netwerk/protocol/http/NullHttpChannel.h | 64 + netwerk/protocol/http/NullHttpTransaction.cpp | 212 + netwerk/protocol/http/NullHttpTransaction.h | 94 + netwerk/protocol/http/ObliviousHttpChannel.cpp | 860 ++ netwerk/protocol/http/ObliviousHttpChannel.h | 72 + netwerk/protocol/http/ObliviousHttpService.cpp | 235 + netwerk/protocol/http/ObliviousHttpService.h | 46 + netwerk/protocol/http/OpaqueResponseUtils.cpp | 679 ++ netwerk/protocol/http/OpaqueResponseUtils.h | 216 + netwerk/protocol/http/PAltDataOutputStream.ipdl | 42 + netwerk/protocol/http/PAltService.ipdl | 38 + netwerk/protocol/http/PAltSvcTransaction.ipdl | 23 + netwerk/protocol/http/PBackgroundDataBridge.ipdl | 32 + netwerk/protocol/http/PHttpBackgroundChannel.ipdl | 80 + netwerk/protocol/http/PHttpChannel.ipdl | 147 + netwerk/protocol/http/PHttpChannelParams.h | 290 + netwerk/protocol/http/PHttpConnectionMgr.ipdl | 46 + netwerk/protocol/http/PHttpTransaction.ipdl | 119 + netwerk/protocol/http/PSpdyPush.h | 56 + netwerk/protocol/http/ParentChannelListener.cpp | 292 + netwerk/protocol/http/ParentChannelListener.h | 86 + netwerk/protocol/http/PendingTransactionInfo.cpp | 136 + netwerk/protocol/http/PendingTransactionInfo.h | 63 + netwerk/protocol/http/PendingTransactionQueue.cpp | 287 + netwerk/protocol/http/PendingTransactionQueue.h | 92 + netwerk/protocol/http/QuicSocketControl.cpp | 128 + netwerk/protocol/http/QuicSocketControl.h | 67 + netwerk/protocol/http/README | 119 + netwerk/protocol/http/SpeculativeTransaction.cpp | 89 + netwerk/protocol/http/SpeculativeTransaction.h | 70 + netwerk/protocol/http/TLSTransportLayer.cpp | 866 ++ netwerk/protocol/http/TLSTransportLayer.h | 170 + netwerk/protocol/http/TRRServiceChannel.cpp | 1581 +++ netwerk/protocol/http/TRRServiceChannel.h | 177 + netwerk/protocol/http/TimingStruct.h | 44 + netwerk/protocol/http/TlsHandshaker.cpp | 336 + netwerk/protocol/http/TlsHandshaker.h | 95 + .../http/WellKnownOpportunisticUtils.sys.mjs | 26 + netwerk/protocol/http/binary_http/Cargo.toml | 11 + .../protocol/http/binary_http/src/binary_http.h | 24 + netwerk/protocol/http/binary_http/src/lib.rs | 264 + netwerk/protocol/http/components.conf | 33 + 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/metrics.yaml | 275 + netwerk/protocol/http/moz.build | 219 + netwerk/protocol/http/nsAHttpConnection.h | 292 + netwerk/protocol/http/nsAHttpTransaction.h | 307 + netwerk/protocol/http/nsCORSListenerProxy.cpp | 1753 ++++ netwerk/protocol/http/nsCORSListenerProxy.h | 130 + netwerk/protocol/http/nsHttp.cpp | 1160 +++ netwerk/protocol/http/nsHttp.h | 527 + .../protocol/http/nsHttpActivityDistributor.cpp | 298 + netwerk/protocol/http/nsHttpActivityDistributor.h | 40 + netwerk/protocol/http/nsHttpAtomList.h | 111 + netwerk/protocol/http/nsHttpAuthCache.cpp | 349 + netwerk/protocol/http/nsHttpAuthCache.h | 219 + netwerk/protocol/http/nsHttpAuthManager.cpp | 105 + netwerk/protocol/http/nsHttpAuthManager.h | 34 + netwerk/protocol/http/nsHttpBasicAuth.cpp | 101 + netwerk/protocol/http/nsHttpBasicAuth.h | 39 + netwerk/protocol/http/nsHttpChannel.cpp | 10430 +++++++++++++++++++ netwerk/protocol/http/nsHttpChannel.h | 869 ++ .../protocol/http/nsHttpChannelAuthProvider.cpp | 1913 ++++ netwerk/protocol/http/nsHttpChannelAuthProvider.h | 185 + netwerk/protocol/http/nsHttpChunkedDecoder.cpp | 170 + netwerk/protocol/http/nsHttpChunkedDecoder.h | 50 + netwerk/protocol/http/nsHttpConnection.cpp | 2609 +++++ netwerk/protocol/http/nsHttpConnection.h | 388 + netwerk/protocol/http/nsHttpConnectionInfo.cpp | 570 + netwerk/protocol/http/nsHttpConnectionInfo.h | 317 + netwerk/protocol/http/nsHttpConnectionMgr.cpp | 3863 +++++++ netwerk/protocol/http/nsHttpConnectionMgr.h | 469 + netwerk/protocol/http/nsHttpDigestAuth.cpp | 743 ++ netwerk/protocol/http/nsHttpDigestAuth.h | 98 + netwerk/protocol/http/nsHttpHandler.cpp | 2812 +++++ netwerk/protocol/http/nsHttpHandler.h | 918 ++ netwerk/protocol/http/nsHttpHeaderArray.cpp | 475 + netwerk/protocol/http/nsHttpHeaderArray.h | 334 + netwerk/protocol/http/nsHttpNTLMAuth.cpp | 404 + netwerk/protocol/http/nsHttpNTLMAuth.h | 36 + netwerk/protocol/http/nsHttpRequestHead.cpp | 367 + netwerk/protocol/http/nsHttpRequestHead.h | 155 + netwerk/protocol/http/nsHttpResponseHead.cpp | 1220 +++ netwerk/protocol/http/nsHttpResponseHead.h | 239 + netwerk/protocol/http/nsHttpTransaction.cpp | 3628 +++++++ netwerk/protocol/http/nsHttpTransaction.h | 599 ++ .../http/nsIBackgroundChannelRegistrar.idl | 63 + netwerk/protocol/http/nsIBinaryHttp.idl | 40 + netwerk/protocol/http/nsICorsPreflightCallback.h | 31 + netwerk/protocol/http/nsIEarlyHintObserver.idl | 16 + netwerk/protocol/http/nsIHttpActivityObserver.idl | 214 + netwerk/protocol/http/nsIHttpAuthManager.idl | 115 + .../protocol/http/nsIHttpAuthenticableChannel.idl | 122 + netwerk/protocol/http/nsIHttpAuthenticator.idl | 221 + netwerk/protocol/http/nsIHttpChannel.idl | 497 + .../protocol/http/nsIHttpChannelAuthProvider.idl | 86 + netwerk/protocol/http/nsIHttpChannelChild.idl | 38 + netwerk/protocol/http/nsIHttpChannelInternal.idl | 522 + netwerk/protocol/http/nsIHttpHeaderVisitor.idl | 26 + netwerk/protocol/http/nsIHttpProtocolHandler.idl | 215 + netwerk/protocol/http/nsIObliviousHttp.idl | 78 + netwerk/protocol/http/nsIObliviousHttpChannel.idl | 26 + netwerk/protocol/http/nsIRaceCacheWithNetwork.idl | 57 + netwerk/protocol/http/nsITlsHandshakeListener.idl | 14 + .../http/nsIWellKnownOpportunisticUtils.idl | 23 + netwerk/protocol/http/nsServerTiming.cpp | 110 + netwerk/protocol/http/nsServerTiming.h | 54 + netwerk/protocol/http/oblivious_http/Cargo.toml | 11 + netwerk/protocol/http/oblivious_http/src/lib.rs | 188 + .../http/oblivious_http/src/oblivious_http.h | 24 + netwerk/protocol/moz.build | 10 + netwerk/protocol/res/ExtensionProtocolHandler.cpp | 1039 ++ netwerk/protocol/res/ExtensionProtocolHandler.h | 236 + netwerk/protocol/res/PageThumbProtocolHandler.cpp | 361 + netwerk/protocol/res/PageThumbProtocolHandler.h | 120 + netwerk/protocol/res/RemoteStreamGetter.cpp | 138 + netwerk/protocol/res/RemoteStreamGetter.h | 68 + netwerk/protocol/res/SubstitutingJARURI.h | 229 + .../protocol/res/SubstitutingProtocolHandler.cpp | 723 ++ netwerk/protocol/res/SubstitutingProtocolHandler.h | 129 + netwerk/protocol/res/SubstitutingURL.h | 66 + netwerk/protocol/res/moz.build | 42 + netwerk/protocol/res/nsIResProtocolHandler.idl | 15 + .../res/nsISubstitutingProtocolHandler.idl | 62 + netwerk/protocol/res/nsResProtocolHandler.cpp | 195 + netwerk/protocol/res/nsResProtocolHandler.h | 78 + netwerk/protocol/viewsource/moz.build | 29 + .../protocol/viewsource/nsIViewSourceChannel.idl | 41 + .../protocol/viewsource/nsViewSourceChannel.cpp | 1220 +++ netwerk/protocol/viewsource/nsViewSourceChannel.h | 98 + .../protocol/viewsource/nsViewSourceHandler.cpp | 141 + netwerk/protocol/viewsource/nsViewSourceHandler.h | 48 + .../protocol/websocket/BaseWebSocketChannel.cpp | 378 + netwerk/protocol/websocket/BaseWebSocketChannel.h | 137 + .../protocol/websocket/IPCTransportProvider.cpp | 89 + netwerk/protocol/websocket/IPCTransportProvider.h | 89 + netwerk/protocol/websocket/PTransportProvider.ipdl | 30 + netwerk/protocol/websocket/PWebSocket.ipdl | 67 + .../protocol/websocket/PWebSocketConnection.ipdl | 33 + .../websocket/PWebSocketEventListener.ipdl | 53 + netwerk/protocol/websocket/WebSocketChannel.cpp | 4336 ++++++++ netwerk/protocol/websocket/WebSocketChannel.h | 377 + .../protocol/websocket/WebSocketChannelChild.cpp | 732 ++ netwerk/protocol/websocket/WebSocketChannelChild.h | 116 + .../protocol/websocket/WebSocketChannelParent.cpp | 360 + .../protocol/websocket/WebSocketChannelParent.h | 70 + netwerk/protocol/websocket/WebSocketConnection.cpp | 261 + netwerk/protocol/websocket/WebSocketConnection.h | 79 + .../protocol/websocket/WebSocketConnectionBase.h | 86 + .../websocket/WebSocketConnectionChild.cpp | 221 + .../protocol/websocket/WebSocketConnectionChild.h | 54 + .../websocket/WebSocketConnectionListener.h | 23 + .../websocket/WebSocketConnectionParent.cpp | 210 + .../protocol/websocket/WebSocketConnectionParent.h | 68 + .../websocket/WebSocketEventListenerChild.cpp | 109 + .../websocket/WebSocketEventListenerChild.h | 63 + .../websocket/WebSocketEventListenerParent.cpp | 117 + .../websocket/WebSocketEventListenerParent.h | 44 + .../protocol/websocket/WebSocketEventService.cpp | 566 + netwerk/protocol/websocket/WebSocketEventService.h | 125 + netwerk/protocol/websocket/WebSocketFrame.cpp | 133 + netwerk/protocol/websocket/WebSocketFrame.h | 104 + netwerk/protocol/websocket/WebSocketLog.h | 24 + netwerk/protocol/websocket/moz.build | 67 + .../protocol/websocket/nsITransportProvider.idl | 36 + netwerk/protocol/websocket/nsIWebSocketChannel.idl | 268 + .../websocket/nsIWebSocketEventService.idl | 87 + netwerk/protocol/websocket/nsIWebSocketImpl.idl | 18 + .../protocol/websocket/nsIWebSocketListener.idl | 94 + netwerk/protocol/webtransport/WebTransportHash.cpp | 24 + netwerk/protocol/webtransport/WebTransportHash.h | 30 + netwerk/protocol/webtransport/WebTransportLog.h | 21 + .../webtransport/WebTransportSessionProxy.cpp | 1269 +++ .../webtransport/WebTransportSessionProxy.h | 195 + .../webtransport/WebTransportStreamProxy.cpp | 386 + .../webtransport/WebTransportStreamProxy.h | 84 + netwerk/protocol/webtransport/moz.build | 36 + netwerk/protocol/webtransport/nsIWebTransport.idl | 132 + .../webtransport/nsIWebTransportStream.idl | 89 + 334 files changed, 124171 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/gio/GIOChannelChild.cpp create mode 100644 netwerk/protocol/gio/GIOChannelChild.h create mode 100644 netwerk/protocol/gio/GIOChannelParent.cpp create mode 100644 netwerk/protocol/gio/GIOChannelParent.h create mode 100644 netwerk/protocol/gio/PGIOChannel.ipdl 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/BinaryHttpRequest.cpp create mode 100644 netwerk/protocol/http/BinaryHttpRequest.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/ClassOfService.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/DnsAndConnectSocket.cpp create mode 100644 netwerk/protocol/http/DnsAndConnectSocket.h create mode 100644 netwerk/protocol/http/EarlyHintPreconnect.cpp create mode 100644 netwerk/protocol/http/EarlyHintPreconnect.h create mode 100644 netwerk/protocol/http/EarlyHintPreloader.cpp create mode 100644 netwerk/protocol/http/EarlyHintPreloader.h create mode 100644 netwerk/protocol/http/EarlyHintRegistrar.cpp create mode 100644 netwerk/protocol/http/EarlyHintRegistrar.h create mode 100644 netwerk/protocol/http/EarlyHintsService.cpp create mode 100644 netwerk/protocol/http/EarlyHintsService.h create mode 100644 netwerk/protocol/http/HPKEConfigManager.sys.mjs create mode 100644 netwerk/protocol/http/HTTPSRecordResolver.cpp create mode 100644 netwerk/protocol/http/HTTPSRecordResolver.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/Http2StreamBase.cpp create mode 100644 netwerk/protocol/http/Http2StreamBase.h create mode 100644 netwerk/protocol/http/Http2StreamTunnel.cpp create mode 100644 netwerk/protocol/http/Http2StreamTunnel.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/Http3StreamBase.h create mode 100644 netwerk/protocol/http/Http3WebTransportSession.cpp create mode 100644 netwerk/protocol/http/Http3WebTransportSession.h create mode 100644 netwerk/protocol/http/Http3WebTransportStream.cpp create mode 100644 netwerk/protocol/http/Http3WebTransportStream.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/HttpWinUtils.cpp create mode 100644 netwerk/protocol/http/HttpWinUtils.h create mode 100644 netwerk/protocol/http/InterceptedHttpChannel.cpp create mode 100644 netwerk/protocol/http/InterceptedHttpChannel.h create mode 100644 netwerk/protocol/http/MockHttpAuth.cpp create mode 100644 netwerk/protocol/http/MockHttpAuth.h create mode 100644 netwerk/protocol/http/NetworkMarker.cpp create mode 100644 netwerk/protocol/http/NetworkMarker.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/ObliviousHttpChannel.cpp create mode 100644 netwerk/protocol/http/ObliviousHttpChannel.h create mode 100644 netwerk/protocol/http/ObliviousHttpService.cpp create mode 100644 netwerk/protocol/http/ObliviousHttpService.h create mode 100644 netwerk/protocol/http/OpaqueResponseUtils.cpp create mode 100644 netwerk/protocol/http/OpaqueResponseUtils.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/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/TLSTransportLayer.cpp create mode 100644 netwerk/protocol/http/TLSTransportLayer.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/TlsHandshaker.cpp create mode 100644 netwerk/protocol/http/TlsHandshaker.h create mode 100644 netwerk/protocol/http/WellKnownOpportunisticUtils.sys.mjs create mode 100644 netwerk/protocol/http/binary_http/Cargo.toml create mode 100644 netwerk/protocol/http/binary_http/src/binary_http.h create mode 100644 netwerk/protocol/http/binary_http/src/lib.rs 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/metrics.yaml 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/nsIBinaryHttp.idl create mode 100644 netwerk/protocol/http/nsICorsPreflightCallback.h create mode 100644 netwerk/protocol/http/nsIEarlyHintObserver.idl 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/nsIObliviousHttp.idl create mode 100644 netwerk/protocol/http/nsIObliviousHttpChannel.idl create mode 100644 netwerk/protocol/http/nsIRaceCacheWithNetwork.idl create mode 100644 netwerk/protocol/http/nsITlsHandshakeListener.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/http/oblivious_http/Cargo.toml create mode 100644 netwerk/protocol/http/oblivious_http/src/lib.rs create mode 100644 netwerk/protocol/http/oblivious_http/src/oblivious_http.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/RemoteStreamGetter.cpp create mode 100644 netwerk/protocol/res/RemoteStreamGetter.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/PWebSocketConnection.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/WebSocketConnection.cpp create mode 100644 netwerk/protocol/websocket/WebSocketConnection.h create mode 100644 netwerk/protocol/websocket/WebSocketConnectionBase.h create mode 100644 netwerk/protocol/websocket/WebSocketConnectionChild.cpp create mode 100644 netwerk/protocol/websocket/WebSocketConnectionChild.h create mode 100644 netwerk/protocol/websocket/WebSocketConnectionListener.h create mode 100644 netwerk/protocol/websocket/WebSocketConnectionParent.cpp create mode 100644 netwerk/protocol/websocket/WebSocketConnectionParent.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 create mode 100644 netwerk/protocol/webtransport/WebTransportHash.cpp create mode 100644 netwerk/protocol/webtransport/WebTransportHash.h create mode 100644 netwerk/protocol/webtransport/WebTransportLog.h create mode 100644 netwerk/protocol/webtransport/WebTransportSessionProxy.cpp create mode 100644 netwerk/protocol/webtransport/WebTransportSessionProxy.h create mode 100644 netwerk/protocol/webtransport/WebTransportStreamProxy.cpp create mode 100644 netwerk/protocol/webtransport/WebTransportStreamProxy.h create mode 100644 netwerk/protocol/webtransport/moz.build create mode 100644 netwerk/protocol/webtransport/nsIWebTransport.idl create mode 100644 netwerk/protocol/webtransport/nsIWebTransportStream.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..f130d9b07b --- /dev/null +++ b/netwerk/protocol/about/moz.build @@ -0,0 +1,32 @@ +# -*- 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", +] diff --git a/netwerk/protocol/about/nsAboutBlank.cpp b/netwerk/protocol/about/nsAboutBlank.cpp new file mode 100644 index 0000000000..88db0d2de3 --- /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" +#include "nsIChannel.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(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..6cb43507f5 --- /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(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..1873277a7a --- /dev/null +++ b/netwerk/protocol/about/nsAboutCache.cpp @@ -0,0 +1,533 @@ +/* -*- 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; + NS_NewPipe(getter_AddRefs(inputStream), getter_AddRefs(mStream), 16384, + (uint32_t)-1, + true, // non-blocking input + false // blocking output + ); + + 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); + } 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" + "\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 = mChannel->AsyncOpen(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, getter_AddRefs(cacheStorage)); + } else if (storageName == "memory") { + rv = cacheService->MemoryCacheStorage(loadInfo, + 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, + int64_t aAltDataSize, uint32_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" + " \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"); + + // Length of alternative content + 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 sizeAlternative Data 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(aAltDataSize); + 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 | + nsIAboutModule::IS_SECURE_CHROME_UI; + return NS_OK; +} + +// static +nsresult nsAboutCache::Create(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..65292b9aae --- /dev/null +++ b/netwerk/protocol/about/nsAboutCache.h @@ -0,0 +1,199 @@ +/* -*- 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(nsITransportSecurityInfo** 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(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 = false; + + // Flag initially false, that indicates the entries header has + // been added to the output HTML. + bool mEntriesHeaderAdded = false; + + // Cancelation flag + bool mCancel = false; + + // 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..08dfcf6ca0 --- /dev/null +++ b/netwerk/protocol/about/nsAboutCacheEntry.cpp @@ -0,0 +1,559 @@ +/* -*- 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 + +#include "nsAboutCacheEntry.h" + +#include "CacheFileUtils.h" +#include "CacheObserver.h" +#include "mozilla/Sprintf.h" +#include "nsAboutCache.h" +#include "nsAboutProtocolUtils.h" +#include "nsContentUtils.h" +#include "nsEscape.h" +#include "nsIAsyncInputStream.h" +#include "nsIAsyncOutputStream.h" +#include "nsICacheStorage.h" +#include "nsIPipe.h" +#include "nsITransportSecurityInfo.h" +#include "nsInputStreamPump.h" +#include "nsNetUtil.h" + +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; + NS_NewPipe2(getter_AddRefs(inputStream), getter_AddRefs(mOutputStream), true, + false, 256, UINT32_MAX); + + constexpr auto buffer = + "\n" + "\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, + uint32_t* result) { + *result = nsICacheEntryOpenCallback::ENTRY_WANTED; + return NS_OK; +} + +NS_IMETHODIMP +nsAboutCacheEntry::Channel::OnCacheEntryAvailable(nsICacheEntry* entry, + bool isNew, 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; + nsAutoCString s; + + // Fetch Count + s.Truncate(); + entry->GetFetchCount(&u); + s.AppendInt(u); + 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..76f8649061 --- /dev/null +++ b/netwerk/protocol/about/nsAboutCacheEntry.h @@ -0,0 +1,86 @@ +/* -*- 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" +#include "nsIChannel.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() = default; + + 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{nullptr}; + nsCOMPtr mOutputStream; + bool mWaitingForData{false}; + uint32_t mHexDumpState{0}; + + 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..5bbaa9010e --- /dev/null +++ b/netwerk/protocol/about/nsAboutProtocolHandler.cpp @@ -0,0 +1,428 @@ +/* -*- 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 "nsContentUtils.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(kNestedAboutURICID, NS_NESTEDABOUTURI_CID); + +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. + return path.EqualsLiteral("blank") || path.EqualsLiteral("logo") || + path.EqualsLiteral("srcdoc"); +} + +//////////////////////////////////////////////////////////////////////////////// + +NS_IMPL_ISUPPORTS(nsAboutProtocolHandler, nsIProtocolHandler, + nsIProtocolHandlerWithDynamicFlags, nsISupportsWeakReference) + +//////////////////////////////////////////////////////////////////////////////// +// nsIProtocolHandler methods: + +NS_IMETHODIMP +nsAboutProtocolHandler::GetScheme(nsACString& result) { + result.AssignLiteral("about"); + return NS_OK; +} + +NS_IMETHODIMP +nsAboutProtocolHandler::GetFlagsForURI(nsIURI* aURI, uint32_t* aFlags) { + // First use the default (which is "unsafe for content"): + *aFlags = URI_NORELATIVE | URI_NOAUTH | URI_DANGEROUS_TO_LOAD | + URI_SCHEME_NOT_SELF_LINKABLE; + + // 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); + + rv = NS_MutateURI(new nsNestedAboutURI::Mutator()) + .Apply(&nsINestedAboutURIMutator::InitWithBase, inner, aBaseURI) + .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; + if (NS_SUCCEEDED(NS_GetAboutModuleName(uri, path)) && + 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. + return NS_ERROR_MALFORMED_URI; + } + + if (NS_FAILED(rv)) { + if (rv == NS_ERROR_FACTORY_NOT_REGISTERED) { + // This looks like an about: we don't know about. Convert + // this to an invalid URI error. + return NS_ERROR_MALFORMED_URI; + } + + return rv; + } + + uint32_t flags = 0; + if (NS_FAILED(aboutMod->GetURIFlags(uri, &flags))) { + return NS_ERROR_FAILURE; + } + + bool safeForUntrustedContent = + (flags & nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT) != 0; + + MOZ_DIAGNOSTIC_ASSERT( + safeForUntrustedContent || + (flags & (nsIAboutModule::URI_CAN_LOAD_IN_CHILD | + nsIAboutModule::URI_MUST_LOAD_IN_CHILD)) == 0, + "Only unprivileged content should be loaded in child processes. (Did " + "you forget to add URI_SAFE_FOR_UNTRUSTED_CONTENT to your about: " + "page?)"); + + // The standard return case: + rv = aboutMod->NewChannel(uri, aLoadInfo, result); + if (NS_FAILED(rv)) { + if (rv == NS_ERROR_FACTORY_NOT_REGISTERED) { + // This looks like an about: we don't know about. Convert + // this to an invalid URI error. + return NS_ERROR_MALFORMED_URI; + } + + return 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 (safeForUntrustedContent) { + (*result)->SetOwner(nullptr); + } + + RefPtr aboutURI; + if (NS_SUCCEEDED( + uri->QueryInterface(kNestedAboutURICID, getter_AddRefs(aboutURI))) && + aboutURI->GetBaseURI()) { + nsCOMPtr writableBag = do_QueryInterface(*result); + if (writableBag) { + writableBag->SetPropertyAsInterface(u"baseURI"_ns, + aboutURI->GetBaseURI()); + } + } + + return NS_OK; +} + +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::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..e861f065b3 --- /dev/null +++ b/netwerk/protocol/about/nsAboutProtocolHandler.h @@ -0,0 +1,139 @@ +/* -*- 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() {} + 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.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; + } + + 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..fdc92620f3 --- /dev/null +++ b/netwerk/protocol/about/nsAboutProtocolUtils.h @@ -0,0 +1,65 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 "mozilla/Try.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"); + + MOZ_TRY(aAboutURI->GetPathQueryRef(aModule)); + + 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; +} + +[[nodiscard]] inline bool NS_IsContentAccessibleAboutURI(nsIURI* aURI) { + MOZ_ASSERT(aURI->SchemeIs("about"), "Should be used only on about: URIs"); + nsAutoCString name; + if (NS_WARN_IF(NS_FAILED(NS_GetAboutModuleName(aURI, name)))) { + return true; + } + return name.EqualsLiteral("blank") || name.EqualsLiteral("srcdoc"); +} + +inline nsresult NS_GetAboutModule(nsIURI* aAboutURI, nsIAboutModule** aModule) { + MOZ_ASSERT(aAboutURI, "Must have URI"); + + nsAutoCString contractID; + MOZ_TRY(NS_GetAboutModuleName(aAboutURI, contractID)); + + // 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..7750dd2d4b --- /dev/null +++ b/netwerk/protocol/about/nsIAboutModule.idl @@ -0,0 +1,113 @@ +/* -*- 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 is a secure chrome UI + */ + const unsigned long IS_SECURE_CHROME_UI = (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..e427120a55 --- /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" + +#ifdef FUZZING_SNAPSHOT +# define MOZ_ALWAYS_SUCCEEDS_FUZZING(...) (void)__VA_ARGS__ +#else +# define MOZ_ALWAYS_SUCCEEDS_FUZZING(...) MOZ_ALWAYS_SUCCEEDS(__VA_ARGS__) +#endif + +namespace mozilla { +namespace net { + +NS_IMPL_ISUPPORTS(DataChannelParent, nsIParentChannel, nsIStreamListener) + +bool DataChannelParent::Init(const uint64_t& aChannelId) { + nsCOMPtr channel; + + MOZ_ALWAYS_SUCCEEDS_FUZZING( + 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::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..87b00adea3 --- /dev/null +++ b/netwerk/protocol/data/nsDataChannel.cpp @@ -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/. */ + +// data implementation + +#include "nsDataChannel.h" + +#include "mozilla/Base64.h" +#include "nsDataHandler.h" +#include "nsIInputStream.h" +#include "nsEscape.h" +#include "nsStringStream.h" +#include "nsIObserverService.h" +#include "mozilla/dom/ContentParent.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, &mMimeType); + 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; + + // notify "data-channel-opened" observers + MaybeSendDataChannelOpenNotification(); + + bufInStream.forget(result); + + return NS_OK; +} + +nsresult nsDataChannel::MaybeSendDataChannelOpenNotification() { + nsCOMPtr obsService = services::GetObserverService(); + if (!obsService) { + return NS_OK; + } + + nsCOMPtr loadInfo; + nsresult rv = GetLoadInfo(getter_AddRefs(loadInfo)); + if (NS_FAILED(rv)) { + return rv; + } + + bool isTopLevel; + rv = loadInfo->GetIsTopLevelLoad(&isTopLevel); + if (NS_FAILED(rv)) { + return rv; + } + + uint64_t browsingContextID; + rv = loadInfo->GetBrowsingContextID(&browsingContextID); + if (NS_FAILED(rv)) { + return rv; + } + + if ((browsingContextID != 0 && isTopLevel) || + !loadInfo->TriggeringPrincipal()->IsSystemPrincipal()) { + obsService->NotifyObservers(static_cast(this), + "data-channel-opened", nullptr); + } + return NS_OK; +} diff --git a/netwerk/protocol/data/nsDataChannel.h b/netwerk/protocol/data/nsDataChannel.h new file mode 100644 index 0000000000..d7313d66a0 --- /dev/null +++ b/netwerk/protocol/data/nsDataChannel.h @@ -0,0 +1,31 @@ +/* -*- 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); } + + const nsACString& MimeType() const { return mMimeType; } + + protected: + [[nodiscard]] virtual nsresult OpenContentStream( + bool async, nsIInputStream** result, nsIChannel** channel) override; + + nsCString mMimeType; + + private: + nsresult MaybeSendDataChannelOpenNotification(); +}; + +#endif /* nsDataChannel_h___ */ diff --git a/netwerk/protocol/data/nsDataHandler.cpp b/netwerk/protocol/data/nsDataHandler.cpp new file mode 100644 index 0000000000..d3a5743097 --- /dev/null +++ b/netwerk/protocol/data/nsDataHandler.cpp @@ -0,0 +1,272 @@ +/* -*- 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 "nsNetUtil.h" +#include "nsSimpleURI.h" +#include "nsUnicharUtils.h" +#include "mozilla/dom/MimeType.h" +#include "mozilla/StaticPrefs_network.h" +#include "mozilla/Try.h" +#include "DefaultURI.h" + +using namespace mozilla; + +//////////////////////////////////////////////////////////////////////////////// + +NS_IMPL_ISUPPORTS(nsDataHandler, nsIProtocolHandler, nsISupportsWeakReference) + +nsresult nsDataHandler::Create(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; +} + +/* static */ nsresult nsDataHandler::CreateNewURI(const nsACString& aSpec, + const char* aCharset, + nsIURI* aBaseURI, + nsIURI** result) { + nsCOMPtr uri; + nsAutoCString contentType; + bool base64; + MOZ_TRY(ParseURI(aSpec, contentType, /* contentCharset = */ nullptr, base64, + /* dataBuffer = */ nullptr)); + + // Strip whitespace unless this is text, where whitespace is important + // Don't strip escaped whitespace though (bug 391951) + nsresult rv; + if (base64 || (StaticPrefs::network_url_strip_data_url_whitespace() && + strncmp(contentType.get(), "text/", 5) != 0 && + contentType.Find("xml") == kNotFound)) { + // it's ascii encoded binary, don't let any spaces in + rv = NS_MutateURI(new mozilla::net::nsSimpleURI::Mutator()) + .Apply(&nsISimpleURIMutator::SetSpecAndFilterWhitespace, aSpec, + nullptr) + .Finalize(uri); + } else { + rv = NS_MutateURI(new mozilla::net::nsSimpleURI::Mutator()) + .SetSpec(aSpec) + .Finalize(uri); + } + + if (NS_FAILED(rv)) return rv; + + // use DefaultURI to check for validity when we have possible hostnames + // since nsSimpleURI doesn't know about hostnames + auto pos = aSpec.Find("data:"); + if (pos != kNotFound) { + nsDependentCSubstring rest(aSpec, pos + sizeof("data:") - 1, -1); + if (StringBeginsWith(rest, "//"_ns)) { + nsCOMPtr uriWithHost; + rv = NS_MutateURI(new mozilla::net::DefaultURI::Mutator()) + .SetSpec(aSpec) + .Finalize(uriWithHost); + NS_ENSURE_SUCCESS(rv, 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; +} + +namespace { + +bool TrimSpacesAndBase64(nsACString& aMimeType) { + const char* beg = aMimeType.BeginReading(); + const char* end = aMimeType.EndReading(); + + // trim leading and trailing spaces + while (beg < end && NS_IsHTTPWhitespace(*beg)) { + ++beg; + } + if (beg == end) { + aMimeType.Truncate(); + return false; + } + while (end > beg && NS_IsHTTPWhitespace(*(end - 1))) { + --end; + } + if (beg == end) { + aMimeType.Truncate(); + return false; + } + + // trim trailing `; base64` (if any) and remember it + const char* pos = end - 1; + bool foundBase64 = false; + if (pos > beg && *pos == '4' && --pos > beg && *pos == '6' && --pos > beg && + ToLowerCaseASCII(*pos) == 'e' && --pos > beg && + ToLowerCaseASCII(*pos) == 's' && --pos > beg && + ToLowerCaseASCII(*pos) == 'a' && --pos > beg && + ToLowerCaseASCII(*pos) == 'b') { + while (--pos > beg && NS_IsHTTPWhitespace(*pos)) { + } + if (pos >= beg && *pos == ';') { + end = pos; + foundBase64 = true; + } + } + + // actually trim off the spaces and trailing base64, returning if we found it. + const char* s = aMimeType.BeginReading(); + aMimeType.Assign(Substring(aMimeType, beg - s, end - s)); + return foundBase64; +} + +} // namespace + +nsresult nsDataHandler::ParsePathWithoutRef(const nsACString& aPath, + nsCString& aContentType, + nsCString* aContentCharset, + bool& aIsBase64, + nsDependentCSubstring* aDataBuffer, + nsCString* aMimeType) { + static constexpr auto kCharset = "charset"_ns; + + // This implements https://fetch.spec.whatwg.org/#data-url-processor + // It also returns the full mimeType in aMimeType so fetch/XHR may access it + // for content-length headers. The contentType and charset parameters retain + // our legacy behavior, as much Gecko code generally expects GetContentType + // to yield only the MimeType's essence, not its full value with parameters. + + aIsBase64 = false; + + 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; + } + + // "Let mimeType be the result of collecting a sequence of code points that + // are not equal to U+002C (,), given position." + nsCString mimeType(Substring(aPath, 0, commaIdx)); + + // "Strip leading and trailing ASCII whitespace from mimeType." + // "If mimeType ends with U+003B (;), followed by zero or more U+0020 SPACE, + // followed by an ASCII case-insensitive match for "base64", then ..." + aIsBase64 = TrimSpacesAndBase64(mimeType); + + // "If mimeType starts with ";", then prepend "text/plain" to mimeType." + if (mimeType.Length() > 0 && mimeType.CharAt(0) == ';') { + mimeType = "text/plain"_ns + mimeType; + } + + // "Let mimeTypeRecord be the result of parsing mimeType." + // This also checks for instances of ;base64 in the middle of the MimeType. + // This is against the current spec, but we're doing it because we have + // historically seen webcompat issues relying on this (see bug 781693). + if (mozilla::UniquePtr parsed = CMimeType::Parse(mimeType)) { + parsed->GetEssence(aContentType); + if (aContentCharset) { + parsed->GetParameterValue(kCharset, *aContentCharset); + } + if (aMimeType) { + parsed->Serialize(*aMimeType); + } + if (parsed->IsBase64() && + !StaticPrefs::network_url_strict_data_url_base64_placement()) { + aIsBase64 = true; + } + } else { + // "If mimeTypeRecord is failure, then set mimeTypeRecord to + // text/plain;charset=US-ASCII." + aContentType.AssignLiteral("text/plain"); + if (aContentCharset) { + aContentCharset->AssignLiteral("US-ASCII"); + } + if (aMimeType) { + aMimeType->AssignLiteral("text/plain;charset=US-ASCII"); + } + } + + if (aDataBuffer) { + aDataBuffer->Rebind(aPath, commaIdx + 1); + } + + return NS_OK; +} + +static inline char ToLower(const char c) { + if (c >= 'A' && c <= 'Z') { + return char(c + ('a' - 'A')); + } + return c; +} + +nsresult nsDataHandler::ParseURI(const nsACString& spec, nsCString& contentType, + nsCString* contentCharset, bool& isBase64, + nsCString* dataBuffer) { + static constexpr auto kDataScheme = "data:"_ns; + + // move past "data:" + const char* pos = std::search( + spec.BeginReading(), spec.EndReading(), kDataScheme.BeginReading(), + kDataScheme.EndReading(), + [](const char a, const char b) { return ToLower(a) == ToLower(b); }); + if (pos == spec.EndReading()) { + return NS_ERROR_MALFORMED_URI; + } + + uint32_t scheme = pos - spec.BeginReading(); + 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..4796f0f453 --- /dev/null +++ b/netwerk/protocol/data/nsDataHandler.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/. */ + +#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(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(const nsACString& 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, nsCString* aMimeType = nullptr); +}; + +#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..e54fb11260 --- /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" + +#ifdef FUZZING_SNAPSHOT +# define MOZ_ALWAYS_SUCCEEDS_FUZZING(...) (void)__VA_ARGS__ +#else +# define MOZ_ALWAYS_SUCCEEDS_FUZZING(...) MOZ_ALWAYS_SUCCEEDS(__VA_ARGS__) +#endif + +namespace mozilla { +namespace net { + +NS_IMPL_ISUPPORTS(FileChannelParent, nsIParentChannel, nsIStreamListener) + +bool FileChannelParent::Init(const uint64_t& aChannelId) { + nsCOMPtr channel; + + MOZ_ALWAYS_SUCCEEDS_FUZZING( + 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::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..821e20e1c5 --- /dev/null +++ b/netwerk/protocol/file/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/. + +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", +] diff --git a/netwerk/protocol/file/nsFileChannel.cpp b/netwerk/protocol/file/nsFileChannel.cpp new file mode 100644 index 0000000000..096f8807ba --- /dev/null +++ b/netwerk/protocol/file/nsFileChannel.cpp @@ -0,0 +1,569 @@ +/* -*- 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 "mozilla/dom/ContentParent.h" +#include "../protocol/http/nsHttpHandler.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) { + // stopped prematurely (out of disk space) + rv = NS_ERROR_FILE_NO_DEVICE_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); + + RefPtr handler = nsHttpHandler::GetInstance(); + MOZ_ALWAYS_SUCCEEDS(handler->NewChannelId(mChannelId)); + + // 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)) { + if (rv == NS_ERROR_FILE_NOT_FOUND) { + CheckForBrokenChromeURL(mLoadInfo, OriginalURI()); + } + + 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); + } + } + + // notify "file-channel-opened" observers + MaybeSendFileOpenNotification(); + + *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 = TaskQueue::Create(sts.forget(), "FileChannel"); + 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) { + size = 0; + } else { + return rv; + } + } + mContentLength = size; + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsFileChannel::nsISupports + +NS_IMPL_ISUPPORTS_INHERITED(nsFileChannel, nsBaseChannel, nsIUploadChannel, + nsIFileChannel, nsIIdentChannel) + +//----------------------------------------------------------------------------- +// 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); +} + +nsresult nsFileChannel::MaybeSendFileOpenNotification() { + nsCOMPtr obsService = services::GetObserverService(); + if (!obsService) { + return NS_OK; + } + + nsCOMPtr loadInfo; + nsresult rv = GetLoadInfo(getter_AddRefs(loadInfo)); + if (NS_FAILED(rv)) { + return rv; + } + + bool isTopLevel; + rv = loadInfo->GetIsTopLevelLoad(&isTopLevel); + if (NS_FAILED(rv)) { + return rv; + } + + uint64_t browsingContextID; + rv = loadInfo->GetBrowsingContextID(&browsingContextID); + if (NS_FAILED(rv)) { + return rv; + } + + if ((browsingContextID != 0 && isTopLevel) || + !loadInfo->TriggeringPrincipal()->IsSystemPrincipal()) { + obsService->NotifyObservers(static_cast(this), + "file-channel-opened", nullptr); + } + return NS_OK; +} + +//----------------------------------------------------------------------------- +// 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) { + *result = do_AddRef(mUploadStream).take(); + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsFileChannel::nsIIdentChannel + +NS_IMETHODIMP +nsFileChannel::GetChannelId(uint64_t* aChannelId) { + *aChannelId = mChannelId; + return NS_OK; +} + +NS_IMETHODIMP +nsFileChannel::SetChannelId(uint64_t aChannelId) { + mChannelId = aChannelId; + return NS_OK; +} diff --git a/netwerk/protocol/file/nsFileChannel.h b/netwerk/protocol/file/nsFileChannel.h new file mode 100644 index 0000000000..781ce5b7af --- /dev/null +++ b/netwerk/protocol/file/nsFileChannel.h @@ -0,0 +1,59 @@ +/* -*- 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 nsIIdentChannel { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_NSIFILECHANNEL + NS_DECL_NSIUPLOADCHANNEL + NS_FORWARD_NSIREQUEST(nsBaseChannel::) + NS_FORWARD_NSICHANNEL(nsBaseChannel::) + NS_DECL_NSIIDENTCHANNEL + + 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); + nsresult MaybeSendFileOpenNotification(); + + nsCOMPtr mUploadStream; + int64_t mUploadLength; + nsCOMPtr mFileURI; + uint64_t mChannelId = 0; +}; + +#endif // !nsFileChannel_h__ diff --git a/netwerk/protocol/file/nsFileProtocolHandler.cpp b/netwerk/protocol/file/nsFileProtocolHandler.cpp new file mode 100644 index 0000000000..10a1b9c029 --- /dev/null +++ b/netwerk/protocol/file/nsFileProtocolHandler.cpp @@ -0,0 +1,266 @@ +/* -*- 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" +#include "mozilla/net/NeckoCommon.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::NewChannel(nsIURI* uri, nsILoadInfo* aLoadInfo, + nsIChannel** result) { + nsresult rv; + + RefPtr chan; + if (mozilla::net::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; + } + + *result = chan.forget().downcast().take(); + 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 mozilla::net::nsStandardURL::Mutator()) + .Apply(&nsIFileURLMutator::SetFile, file) + .Finalize(aResult); +} + +NS_IMETHODIMP +nsFileProtocolHandler::NewFileURIMutator(nsIFile* aFile, + nsIURIMutator** aResult) { + NS_ENSURE_ARG_POINTER(aFile); + nsresult rv; + + nsCOMPtr mutator = new mozilla::net::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..d470dcee2a --- /dev/null +++ b/netwerk/protocol/file/nsIFileProtocolHandler.idl @@ -0,0 +1,102 @@ +/* -*- 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); + + /** + * DEPRECATED, AVOID IF AT ALL POSSIBLE. + * + * Calling this will cause IO on the calling thread, to determine + * if the file is a directory or file, and based on that behaves as + * if you called getURLSpecFromDir or getURLSpecFromActualFile, + * respectively. This IO may take multiple seconds (e.g. for network + * paths, slow external drives that need to be woken up, etc.). + * + * Usually, the caller should *know* that the `file` argument is + * either a directory (in which case it should call getURLSpecFromDir) + * or a non-directory file (in which case it should call + * getURLSpecFromActualFile), and not need to call this method. + */ + [noscript] AUTF8String getURLSpecFromFile(in nsIFile file); + + /** + * Converts a non-directory 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. + * + * Should only be called on files which are not directories. If + * called on directories, the resulting URL may lack a trailing slash + * and cause relative URLs in such a document to misbehave. + */ + AUTF8String getURLSpecFromActualFile(in nsIFile file); + + /** + * Converts a directory 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. + * + * Should only be called on files which are directories (will enforce + * the URL ends with a slash). + */ + 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/gio/GIOChannelChild.cpp b/netwerk/protocol/gio/GIOChannelChild.cpp new file mode 100644 index 0000000000..2adb611666 --- /dev/null +++ b/netwerk/protocol/gio/GIOChannelChild.cpp @@ -0,0 +1,458 @@ +/* -*- 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 "mozilla/net/NeckoChild.h" +#include "GIOChannelChild.h" +#include "nsGIOProtocolHandler.h" +#include "mozilla/dom/ContentChild.h" +#include "mozilla/dom/BrowserChild.h" +#include "nsContentUtils.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 "SerializedLoadContext.h" +#include "mozilla/Logging.h" + +using mozilla::dom::ContentChild; + +namespace mozilla { +#undef LOG +#define LOG(args) MOZ_LOG(gGIOLog, mozilla::LogLevel::Debug, args) +namespace net { + +GIOChannelChild::GIOChannelChild(nsIURI* aUri) + : mEventQ(new ChannelEventQueue(static_cast(this))) { + 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(); +} + +void GIOChannelChild::AddIPDLReference() { + MOZ_ASSERT(!mIPCOpen, "Attempt to retain more than one IPDL reference"); + mIPCOpen = true; + AddRef(); +} + +void GIOChannelChild::ReleaseIPDLReference() { + MOZ_ASSERT(mIPCOpen, "Attempt to release nonexistent IPDL reference"); + mIPCOpen = false; + Release(); +} + +//----------------------------------------------------------------------------- +// GIOChannelChild::nsISupports +//----------------------------------------------------------------------------- + +NS_IMPL_ISUPPORTS_INHERITED(GIOChannelChild, nsBaseChannel, nsIChildChannel) + +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +GIOChannelChild::AsyncOpen(nsIStreamListener* aListener) { + nsCOMPtr listener = aListener; + nsresult rv = + nsContentSecurityManager::doContentSecurityCheck(this, listener); + NS_ENSURE_SUCCESS(rv, rv); + + LOG(("GIOChannelChild::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); + } + + Maybe ipcStream; + mozilla::ipc::SerializeIPCStream(do_AddRef(mUploadStream), ipcStream, + /* aAllowLazy */ false); + + uint32_t loadFlags = 0; + GetLoadFlags(&loadFlags); + + GIOChannelOpenArgs openArgs; + SerializeURI(nsBaseChannel::URI(), openArgs.uri()); + openArgs.startPos() = mStartPos; + openArgs.entityID() = mEntityID; + openArgs.uploadStream() = ipcStream; + 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(); + + // The socket transport layer in the chrome process now has a logical ref to + // us until OnStopRequest is called. + AddIPDLReference(); + + if (!gNeckoChild->SendPGIOChannelConstructor( + this, browserChild, IPC::SerializedLoadContext(this), openArgs)) { + return NS_ERROR_FAILURE; + } + + mIsPending = true; + mWasOpened = true; + + return rv; +} + +NS_IMETHODIMP +GIOChannelChild::IsPending(bool* aResult) { + *aResult = mIsPending; + return NS_OK; +} + +nsresult GIOChannelChild::OpenContentStream(bool aAsync, + nsIInputStream** aStream, + nsIChannel** aChannel) { + MOZ_CRASH("GIOChannel*Child* should never have OpenContentStream called!"); + return NS_OK; +} + +mozilla::ipc::IPCResult GIOChannelChild::RecvOnStartRequest( + const nsresult& aChannelStatus, const int64_t& aContentLength, + const nsACString& aContentType, const nsACString& aEntityID, + const URIParams& aURI) { + LOG(("GIOChannelChild::RecvOnStartRequest [this=%p]\n", this)); + mEventQ->RunOrEnqueue(new NeckoTargetChannelFunctionEvent( + this, [self = UnsafePtr(this), aChannelStatus, + aContentLength, aContentType = nsCString(aContentType), + aEntityID = nsCString(aEntityID), aURI]() { + self->DoOnStartRequest(aChannelStatus, aContentLength, aContentType, + aEntityID, aURI); + })); + return IPC_OK(); +} + +void GIOChannelChild::DoOnStartRequest(const nsresult& aChannelStatus, + const int64_t& aContentLength, + const nsACString& aContentType, + const nsACString& aEntityID, + const URIParams& aURI) { + LOG(("GIOChannelChild::DoOnStartRequest [this=%p]\n", this)); + if (!mCanceled && NS_SUCCEEDED(mStatus)) { + mStatus = aChannelStatus; + } + + mContentLength = aContentLength; + SetContentType(aContentType); + 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); + } + + AutoEventEnqueuer ensureSerialDispatch(mEventQ); + rv = mListener->OnStartRequest(this); + if (NS_FAILED(rv)) { + Cancel(rv); + } +} + +mozilla::ipc::IPCResult GIOChannelChild::RecvOnDataAvailable( + const nsresult& aChannelStatus, const nsACString& aData, + const uint64_t& aOffset, const uint32_t& aCount) { + LOG(("GIOChannelChild::RecvOnDataAvailable [this=%p]\n", this)); + mEventQ->RunOrEnqueue(new NeckoTargetChannelFunctionEvent( + this, [self = UnsafePtr(this), aChannelStatus, + aData = nsCString(aData), aOffset, aCount]() { + self->DoOnDataAvailable(aChannelStatus, aData, aOffset, aCount); + })); + + return IPC_OK(); +} + +void GIOChannelChild::DoOnDataAvailable(const nsresult& aChannelStatus, + const nsACString& aData, + const uint64_t& aOffset, + const uint32_t& aCount) { + LOG(("GIOChannelChild::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 GIOChannelChild::RecvOnStopRequest( + const nsresult& aChannelStatus) { + LOG(("GIOChannelChild::RecvOnStopRequest [this=%p status=%" PRIx32 "]\n", + this, static_cast(aChannelStatus))); + + mEventQ->RunOrEnqueue(new NeckoTargetChannelFunctionEvent( + this, [self = UnsafePtr(this), aChannelStatus]() { + self->DoOnStopRequest(aChannelStatus); + })); + return IPC_OK(); +} + +void GIOChannelChild::DoOnStopRequest(const nsresult& aChannelStatus) { + LOG(("GIOChannelChild::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::DeallocPGIOChannelChild(), which deletes |this| if + // IPDL holds the last reference. Don't rely on |this| existing after here! + Send__delete__(this); +} + +mozilla::ipc::IPCResult GIOChannelChild::RecvFailedAsyncOpen( + const nsresult& aStatusCode) { + LOG(("GIOChannelChild::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 GIOChannelChild::DoFailedAsyncOpen(const nsresult& aStatusCode) { + LOG(("GIOChannelChild::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 GIOChannelChild::RecvDeleteSelf() { + mEventQ->RunOrEnqueue(new NeckoTargetChannelFunctionEvent( + this, + [self = UnsafePtr(this)]() { self->DoDeleteSelf(); })); + return IPC_OK(); +} + +void GIOChannelChild::DoDeleteSelf() { + if (mIPCOpen) { + Send__delete__(this); + } +} + +//----------------------------------------------------------------------------- +// GIOChannelChild::nsIResumableChannel +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +GIOChannelChild::Cancel(nsresult aStatus) { + LOG(("GIOChannelChild::Cancel [this=%p]\n", this)); + + if (mCanceled) { + return NS_OK; + } + + mCanceled = true; + mStatus = aStatus; + if (mIPCOpen) { + SendCancel(aStatus); + } + return NS_OK; +} + +NS_IMETHODIMP +GIOChannelChild::Suspend() { + NS_ENSURE_TRUE(mIPCOpen, NS_ERROR_NOT_AVAILABLE); + + LOG(("GIOChannelChild::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 +GIOChannelChild::Resume() { + NS_ENSURE_TRUE(mIPCOpen, NS_ERROR_NOT_AVAILABLE); + + LOG(("GIOChannelChild::Resume [this=%p]\n", this)); + + // SendResume only once, when suspend count drops to 0. + if (!--mSuspendCount && mSuspendSent) { + SendResume(); + } + mEventQ->Resume(); + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// GIOChannelChild::nsIChildChannel +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +GIOChannelChild::ConnectParent(uint32_t aId) { + NS_ENSURE_TRUE((gNeckoChild), NS_ERROR_FAILURE); + NS_ENSURE_TRUE( + !static_cast(gNeckoChild->Manager())->IsShuttingDown(), + NS_ERROR_FAILURE); + + LOG(("GIOChannelChild::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(); + + GIOChannelConnectArgs connectArgs(aId); + + if (!gNeckoChild->SendPGIOChannelConstructor( + this, browserChild, IPC::SerializedLoadContext(this), connectArgs)) { + return NS_ERROR_FAILURE; + } + + return NS_OK; +} + +NS_IMETHODIMP +GIOChannelChild::CompleteRedirectSetup(nsIStreamListener* aListener) { + LOG(("GIOChannelChild::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 GIOChannelChild::SetupNeckoTarget() { + if (mNeckoTarget) { + return; + } + mNeckoTarget = GetMainThreadSerialEventTarget(); +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/gio/GIOChannelChild.h b/netwerk/protocol/gio/GIOChannelChild.h new file mode 100644 index 0000000000..158ab6804f --- /dev/null +++ b/netwerk/protocol/gio/GIOChannelChild.h @@ -0,0 +1,111 @@ +/* -*- 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_GIOCHANNELCHILD_H +#define NS_GIOCHANNELCHILD_H + +#include "mozilla/net/PGIOChannelChild.h" +#include "mozilla/net/ChannelEventQueue.h" +#include "nsBaseChannel.h" +#include "nsIUploadChannel.h" +#include "nsIProxiedChannel.h" +#include "nsIResumableChannel.h" +#include "nsIChildChannel.h" +#include "nsIEventTarget.h" +#include "nsIStreamListener.h" + +class nsIEventTarget; + +namespace mozilla { +namespace net { + +class GIOChannelChild final : public PGIOChannelChild, + public nsBaseChannel, + public nsIChildChannel { + public: + using nsIStreamListener = ::nsIStreamListener; + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_NSICHILDCHANNEL + + NS_IMETHOD Cancel(nsresult aStatus) override; + NS_IMETHOD Suspend() override; + NS_IMETHOD Resume() override; + + explicit GIOChannelChild(nsIURI* uri); + + 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 ~GIOChannelChild() = default; + + mozilla::ipc::IPCResult RecvOnStartRequest(const nsresult& aChannelStatus, + const int64_t& aContentLength, + const nsACString& aContentType, + const nsACString& aEntityID, + const URIParams& aURI) override; + mozilla::ipc::IPCResult RecvOnDataAvailable(const nsresult& aChannelStatus, + const nsACString& aData, + const uint64_t& aOffset, + const uint32_t& aCount) override; + mozilla::ipc::IPCResult RecvOnStopRequest( + const nsresult& aChannelStatus) override; + mozilla::ipc::IPCResult RecvFailedAsyncOpen( + const nsresult& aStatusCode) override; + mozilla::ipc::IPCResult RecvDeleteSelf() override; + + void DoOnStartRequest(const nsresult& aChannelStatus, + const int64_t& aContentLength, + const nsACString& aContentType, + const nsACString& aEntityID, const URIParams& aURI); + void DoOnDataAvailable(const nsresult& aChannelStatus, + const nsACString& aData, const uint64_t& aOffset, + const uint32_t& aCount); + void DoOnStopRequest(const nsresult& aChannelStatus); + void DoFailedAsyncOpen(const nsresult& aStatusCode); + void DoDeleteSelf(); + + void SetupNeckoTarget() override; + + friend class NeckoTargetChannelFunctionEvent; + + private: + nsCOMPtr mUploadStream; + + bool mIPCOpen = false; + const RefPtr mEventQ; + + bool mCanceled = false; + uint32_t mSuspendCount = 0; + ; + bool mIsPending = false; + + uint64_t mStartPos = 0; + nsCString mEntityID; + + // Set if SendSuspend is called. Determines if SendResume is needed when + // diverting callbacks to parent. + bool mSuspendSent = false; +}; + +inline bool GIOChannelChild::IsSuspended() const { return mSuspendCount != 0; } + +} // namespace net +} // namespace mozilla + +#endif /* NS_GIOCHANNELCHILD_H */ diff --git a/netwerk/protocol/gio/GIOChannelParent.cpp b/netwerk/protocol/gio/GIOChannelParent.cpp new file mode 100644 index 0000000000..a19829a81d --- /dev/null +++ b/netwerk/protocol/gio/GIOChannelParent.cpp @@ -0,0 +1,324 @@ +/* -*- 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 "GIOChannelParent.h" +#include "nsGIOProtocolHandler.h" +#include "mozilla/Assertions.h" +#include "mozilla/dom/ContentParent.h" +#include "mozilla/dom/BrowserParent.h" +#include "mozilla/net/NeckoParent.h" +#include "nsNetUtil.h" +#include "nsIChannel.h" +#include "mozilla/net/NeckoChannelParams.h" +#include "nsIAuthPrompt.h" +#include "nsIAuthPromptProvider.h" +#include "nsISecureBrowserUI.h" +#include "nsQueryObject.h" +#include "mozilla/Logging.h" +#include "mozilla/net/ChannelEventQueue.h" +#include "mozilla/ipc/URIUtils.h" + +using namespace mozilla::dom; +using namespace mozilla::ipc; + +namespace mozilla { +#undef LOG +#define LOG(args) MOZ_LOG(gGIOLog, mozilla::LogLevel::Debug, args) +namespace net { + +GIOChannelParent::GIOChannelParent(dom::BrowserParent* aIframeEmbedding, + nsILoadContext* aLoadContext, + PBOverrideStatus aOverrideStatus) + : mLoadContext(aLoadContext), + mPBOverride(aOverrideStatus), + mBrowserParent(aIframeEmbedding) { + mEventQ = new ChannelEventQueue(static_cast(this)); +} + +void GIOChannelParent::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; +} + +//----------------------------------------------------------------------------- +// GIOChannelParent::nsISupports +//----------------------------------------------------------------------------- +NS_IMPL_ISUPPORTS(GIOChannelParent, nsIStreamListener, nsIParentChannel, + nsIInterfaceRequestor, nsIRequestObserver) + +//----------------------------------------------------------------------------- +// GIOChannelParent methods +//----------------------------------------------------------------------------- + +bool GIOChannelParent::Init(const GIOChannelCreationArgs& aOpenArgs) { + switch (aOpenArgs.type()) { + case GIOChannelCreationArgs::TGIOChannelOpenArgs: { + const GIOChannelOpenArgs& a = aOpenArgs.get_GIOChannelOpenArgs(); + return DoAsyncOpen(a.uri(), a.startPos(), a.entityID(), a.uploadStream(), + a.loadInfo(), a.loadFlags()); + } + case GIOChannelCreationArgs::TGIOChannelConnectArgs: { + const GIOChannelConnectArgs& cArgs = + aOpenArgs.get_GIOChannelConnectArgs(); + return ConnectChannel(cArgs.channelId()); + } + default: + MOZ_ASSERT_UNREACHABLE("unknown open type"); + return false; + } +} + +bool GIOChannelParent::DoAsyncOpen(const URIParams& aURI, + const uint64_t& aStartPos, + const nsCString& aEntityID, + const Maybe& aUploadStream, + const LoadInfoArgs& aLoadInfoArgs, + const uint32_t& aLoadFlags) { + nsresult rv; + + nsCOMPtr uri = DeserializeURI(aURI); + if (!uri) { + return false; + } + +#ifdef DEBUG + LOG(("GIOChannelParent DoAsyncOpen [this=%p uri=%s]\n", this, + uri->GetSpecOrDefault().get())); +#endif + + nsCOMPtr ios(do_GetIOService(&rv)); + if (NS_FAILED(rv)) { + return SendFailedAsyncOpen(rv); + } + + nsAutoCString remoteType; + rv = GetRemoteType(remoteType); + if (NS_FAILED(rv)) { + return SendFailedAsyncOpen(rv); + } + + nsCOMPtr loadInfo; + rv = mozilla::ipc::LoadInfoArgsToLoadInfo(aLoadInfoArgs, remoteType, + 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; + + nsIChannel* gioChan = static_cast(mChannel.get()); + + rv = gioChan->AsyncOpen(this); + if (NS_FAILED(rv)) { + return SendFailedAsyncOpen(rv); + } + + return true; +} + +bool GIOChannelParent::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 GIOChannelParent::RecvCancel(const nsresult& status) { + if (mChannel) { + mChannel->Cancel(status); + } + + return IPC_OK(); +} + +mozilla::ipc::IPCResult GIOChannelParent::RecvSuspend() { + if (mChannel) { + mChannel->Suspend(); + } + return IPC_OK(); +} + +mozilla::ipc::IPCResult GIOChannelParent::RecvResume() { + if (mChannel) { + mChannel->Resume(); + } + return IPC_OK(); +} + +//----------------------------------------------------------------------------- +// GIOChannelParent::nsIRequestObserver +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +GIOChannelParent::OnStartRequest(nsIRequest* aRequest) { + LOG(("GIOChannelParent::OnStartRequest [this=%p]\n", this)); + nsCOMPtr chan = do_QueryInterface(aRequest); + MOZ_ASSERT(chan); + NS_ENSURE_TRUE(chan, NS_ERROR_UNEXPECTED); + + int64_t contentLength; + chan->GetContentLength(&contentLength); + nsCString contentType; + chan->GetContentType(contentType); + nsresult channelStatus = NS_OK; + chan->GetStatus(&channelStatus); + + nsCString entityID; + URIParams uriparam; + nsCOMPtr uri; + chan->GetURI(getter_AddRefs(uri)); + SerializeURI(uri, uriparam); + + if (mIPCClosed || !SendOnStartRequest(channelStatus, contentLength, + contentType, entityID, uriparam)) { + return NS_ERROR_UNEXPECTED; + } + + return NS_OK; +} + +NS_IMETHODIMP +GIOChannelParent::OnStopRequest(nsIRequest* aRequest, nsresult aStatusCode) { + LOG(("GIOChannelParent::OnStopRequest: [this=%p status=%" PRIu32 "]\n", this, + static_cast(aStatusCode))); + + if (mIPCClosed || !SendOnStopRequest(aStatusCode)) { + return NS_ERROR_UNEXPECTED; + } + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// GIOChannelParent::nsIStreamListener +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +GIOChannelParent::OnDataAvailable(nsIRequest* aRequest, + nsIInputStream* aInputStream, + uint64_t aOffset, uint32_t aCount) { + LOG(("GIOChannelParent::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; +} + +//----------------------------------------------------------------------------- +// GIOChannelParent::nsIParentChannel +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +GIOChannelParent::SetParentListener(ParentChannelListener* aListener) { + // Do not need ptr to ParentChannelListener. + return NS_OK; +} + +NS_IMETHODIMP +GIOChannelParent::NotifyClassificationFlags(uint32_t aClassificationFlags, + bool aIsThirdParty) { + // Nothing to do. + return NS_OK; +} + +NS_IMETHODIMP +GIOChannelParent::SetClassifierMatchedInfo(const nsACString& aList, + const nsACString& aProvider, + const nsACString& aFullHash) { + // nothing to do + return NS_OK; +} + +NS_IMETHODIMP +GIOChannelParent::SetClassifierMatchedTrackingInfo( + const nsACString& aLists, const nsACString& aFullHashes) { + // nothing to do + return NS_OK; +} + +NS_IMETHODIMP +GIOChannelParent::Delete() { + if (mIPCClosed || !SendDeleteSelf()) { + return NS_ERROR_UNEXPECTED; + } + + return NS_OK; +} + +NS_IMETHODIMP +GIOChannelParent::GetRemoteType(nsACString& aRemoteType) { + if (!CanSend()) { + return NS_ERROR_UNEXPECTED; + } + + dom::PContentParent* pcp = Manager()->Manager(); + aRemoteType = static_cast(pcp)->GetRemoteType(); + return NS_OK; +} + +//----------------------------------------------------------------------------- +// GIOChannelParent::nsIInterfaceRequestor +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +GIOChannelParent::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); + } + } + + // 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); +} + +//--------------------- +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/gio/GIOChannelParent.h b/netwerk/protocol/gio/GIOChannelParent.h new file mode 100644 index 0000000000..32d3bd0555 --- /dev/null +++ b/netwerk/protocol/gio/GIOChannelParent.h @@ -0,0 +1,80 @@ +/* -*- 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_GIOCHANNELPARENT_H +#define NS_GIOCHANNELPARENT_H + +#include "mozilla/net/PGIOChannelParent.h" +#include "mozilla/net/NeckoParent.h" +#include "nsIParentChannel.h" +#include "nsIInterfaceRequestor.h" + +class nsILoadContext; + +namespace mozilla { + +namespace dom { +class BrowserParent; +} // namespace dom + +namespace net { +class ChannelEventQueue; + +class GIOChannelParent final : public PGIOChannelParent, + public nsIParentChannel, + public nsIInterfaceRequestor { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMLISTENER + NS_DECL_NSIPARENTCHANNEL + NS_DECL_NSIINTERFACEREQUESTOR + + GIOChannelParent(dom::BrowserParent* aIframeEmbedding, + nsILoadContext* aLoadContext, + PBOverrideStatus aOverrideStatus); + + bool Init(const GIOChannelCreationArgs& aOpenArgs); + + protected: + virtual ~GIOChannelParent() = default; + + bool DoAsyncOpen(const URIParams& aURI, const uint64_t& aStartPos, + const nsCString& aEntityID, + const Maybe& aUploadStream, + const LoadInfoArgs& 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; + + nsCOMPtr mChannel; + + bool mIPCClosed = false; + + nsCOMPtr mLoadContext; + + PBOverrideStatus mPBOverride; + + // Set to the canceled status value if the main channel was canceled. + nsresult mStatus = NS_OK; + + RefPtr mBrowserParent; + + RefPtr mEventQ; +}; + +} // namespace net +} // namespace mozilla + +#endif /* NS_GIOCHANNELPARENT_H */ diff --git a/netwerk/protocol/gio/PGIOChannel.ipdl b/netwerk/protocol/gio/PGIOChannel.ipdl new file mode 100644 index 0000000000..e4f1f6380b --- /dev/null +++ b/netwerk/protocol/gio/PGIOChannel.ipdl @@ -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 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 { + +[ManualDealloc, ChildImpl=virtual, ParentImpl=virtual] +async protocol PGIOChannel +{ + 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, + nsCString aEntityID, + URIParams aURI); + async OnDataAvailable(nsresult channelStatus, + nsCString data, + uint64_t offset, + uint32_t count); + async OnStopRequest(nsresult channelStatus); + async FailedAsyncOpen(nsresult statusCode); + + async DeleteSelf(); +}; + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/gio/components.conf b/netwerk/protocol/gio/components.conf new file mode 100644 index 0000000000..a2bbfd8f19 --- /dev/null +++ b/netwerk/protocol/gio/components.conf @@ -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/. + +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', + 'categories': { 'xpcom-startup': 'nsGIOProtocolHandler' }, + 'protocol_config': { + 'scheme': 'moz-gio', + 'flags': [ + 'URI_STD', + 'URI_DANGEROUS_TO_LOAD', + ], + }, + }, +] diff --git a/netwerk/protocol/gio/moz.build b/netwerk/protocol/gio/moz.build new file mode 100644 index 0000000000..4078e5605c --- /dev/null +++ b/netwerk/protocol/gio/moz.build @@ -0,0 +1,42 @@ +# -*- 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/. + +XPCOM_MANIFESTS += [ + "components.conf", +] + +EXPORTS += [ + "nsGIOProtocolHandler.h", +] + +EXPORTS.mozilla.net += [ + "GIOChannelChild.h", + "GIOChannelParent.h", +] + +UNIFIED_SOURCES += [ + "GIOChannelChild.cpp", + "GIOChannelParent.cpp", + "nsGIOProtocolHandler.cpp", +] + +IPDL_SOURCES = [ + "PGIOChannel.ipdl", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +LOCAL_INCLUDES += [ + "/netwerk/base", +] + +FINAL_LIBRARY = "xul" + +if CONFIG["MOZ_WIDGET_TOOLKIT"] == "gtk": + CXXFLAGS += CONFIG["MOZ_GTK3_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..469d7982d4 --- /dev/null +++ b/netwerk/protocol/gio/nsGIOProtocolHandler.cpp @@ -0,0 +1,1027 @@ +/* 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 "GIOChannelChild.h" +#include "mozilla/Components.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/Logging.h" +#include "mozilla/net/NeckoChild.h" +#include "mozilla/NullPrincipal.h" +#include "nsIPrefBranch.h" +#include "nsIPrefService.h" +#include "nsIObserver.h" +#include "nsCRT.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 "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 +LazyLogModule gGIOLog("gio"); +#undef LOG +#define LOG(args) MOZ_LOG(gGIOLog, 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. + */ +enum class MountOperationResult { + MOUNT_OPERATION_IN_PROGRESS, /** \enum operation in progress */ + MOUNT_OPERATION_SUCCESS, /** \enum operation successful */ + MOUNT_OPERATION_FAILED /** \enum operation not successful */ +}; + +//----------------------------------------------------------------------------- +/** + * 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 nsCRT::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 { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIINPUTSTREAM + + explicit nsGIOInputStream(const nsCString& uriSpec) : mSpec(uriSpec) {} + + 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. + + mChannel = do_AddRef(channel).take(); + } + void SetMountResult(MountOperationResult result, gint error_code); + + private: + ~nsGIOInputStream() { Close(); } + 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{nullptr}; // manually refcounted + GFile* mHandle{nullptr}; + GFileInputStream* mStream{nullptr}; + uint64_t mBytesRemaining{UINT64_MAX}; + nsresult mStatus{NS_OK}; + GList* mDirList{nullptr}; + GList* mDirListPtr{nullptr}; + nsCString mDirBuf; + uint32_t mDirBufCursor{0}; + bool mDirOpen{false}; + MountOperationResult mMountRes = + MountOperationResult::MOUNT_OPERATION_SUCCESS; + mozilla::Monitor mMonitorMountInProgress MOZ_UNANNOTATED{ + "GIOInputStream::MountFinished"}; + 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 = MountOperationResult::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 == MountOperationResult::MOUNT_OPERATION_IN_PROGRESS) { + mon.Wait(); + } + + g_object_unref(mount_op); + + if (mMountRes == MountOperationResult::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 column names + mDirBuf.AppendLiteral( + "200: filename content-length last-modified file-type\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; +} + +/** + * Return the status of the input stream + */ +NS_IMETHODIMP +nsGIOInputStream::StreamStatus() { return mStatus; } + +/** + * 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(MountOperationResult::MOUNT_OPERATION_FAILED, + error->code); + g_error_free(error); + } else { + istream->SetMountResult(MountOperationResult::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() { + if (net::IsNeckoChild()) { + net::NeckoChild::InitNeckoChild(); + } + 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) { + nsCOMPtr ioService = components::IO::Service(); + if (NS_WARN_IF(!ioService)) { + LOG(("gio: ioservice not available\n")); + return; + } + + // 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 sftp protocol so + // far. + nsAutoCString prefValue; + nsresult rv = prefs->GetCharPref(MOZ_GIO_SUPPORTED_PROTOCOLS, prefValue); + if (NS_SUCCEEDED(rv)) { + prefValue.StripWhitespace(); + ToLowerCase(prefValue); + } else { + prefValue.AssignLiteral("" // use none by default + ); + } + LOG(("gio: supported protocols \"%s\"\n", prefValue.get())); + + // Unregister any previously registered dynamic protocols. + for (const nsCString& scheme : mSupportedProtocols) { + LOG(("gio: unregistering handler for \"%s\"", scheme.get())); + ioService->UnregisterProtocolHandler(scheme); + } + mSupportedProtocols.Clear(); + + // Register each protocol from the pref branch to reference + // nsGIOProtocolHandler. + for (const nsDependentCSubstring& protocol : prefValue.Split(',')) { + if (NS_WARN_IF(!StringEndsWith(protocol, ":"_ns))) { + continue; // each protocol must end with a `:` character to be recognized + } + + nsCString scheme(Substring(protocol, 0, protocol.Length() - 1)); + if (NS_SUCCEEDED(ioService->RegisterProtocolHandler( + scheme, this, + nsIProtocolHandler::URI_STD | + nsIProtocolHandler::URI_DANGEROUS_TO_LOAD, + /* aDefaultPort */ -1))) { + LOG(("gio: successfully registered handler for \"%s\"", scheme.get())); + mSupportedProtocols.AppendElement(scheme); + } else { + LOG(("gio: failed to register handler for \"%s\"", scheme.get())); + } + } +} + +bool nsGIOProtocolHandler::IsSupportedProtocol(const nsCString& aScheme) { + for (const auto& protocol : mSupportedProtocols) { + if (aScheme.EqualsIgnoreCase(protocol)) { + return true; + } + } + return false; +} + +NS_IMETHODIMP +nsGIOProtocolHandler::GetScheme(nsACString& aScheme) { + aScheme.AssignLiteral(MOZ_GIO_SCHEME); + 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; + } + + nsAutoCString scheme; + rv = aURI->GetScheme(scheme); + if (NS_FAILED(rv)) { + return rv; + } + + if (!IsSupportedProtocol(scheme)) { + return NS_ERROR_UNKNOWN_PROTOCOL; + } + + // g_vfs_get_supported_uri_schemes() returns a very limited list in the + // child due to the sandbox, so we only check if its valid for the parent. + if (XRE_IsParentProcess() && !IsValidGIOScheme(scheme)) { + return NS_ERROR_UNKNOWN_PROTOCOL; + } + + RefPtr channel; + if (net::IsNeckoChild()) { + channel = new mozilla::net::GIOChannelChild(aURI); + // set the loadInfo on the new channel + channel->SetLoadInfo(aLoadInfo); + + rv = channel->SetContentType(nsLiteralCString(UNKNOWN_CONTENT_TYPE)); + NS_ENSURE_SUCCESS(rv, rv); + + channel.forget(aResult); + return NS_OK; + } + + 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..08e7c01bed --- /dev/null +++ b/netwerk/protocol/gio/nsGIOProtocolHandler.h @@ -0,0 +1,38 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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" + +#include "mozilla/Logging.h" +extern mozilla::LazyLogModule gGIOLog; + +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& aScheme); + + protected: + ~nsGIOProtocolHandler() = default; + + private: + nsresult Init(); + + void InitSupportedProtocolsPref(nsIPrefBranch* prefs); + + static mozilla::StaticRefPtr sSingleton; + nsTArray mSupportedProtocols; +}; + +#endif // nsGIOProtocolHandler_h___ diff --git a/netwerk/protocol/http/ASpdySession.cpp b/netwerk/protocol/http/ASpdySession.cpp new file mode 100644 index 0000000000..40df46b739 --- /dev/null +++ b/netwerk/protocol/http/ASpdySession.cpp @@ -0,0 +1,94 @@ +/* -*- 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/StaticPrefs_network.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 = SpdyVersion::HTTP_2; + VersionString = "h2"_ns; + ALPNCallbacks = Http2Session::ALPNCallback; +} + +////////////////////////////////////////// +// 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.InsertOrUpdate(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..45831a140c --- /dev/null +++ b/netwerk/protocol/http/ASpdySession.h @@ -0,0 +1,115 @@ +/* -*- 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 "nsHttp.h" +#include "nsString.h" + +class nsISocketTransport; + +namespace mozilla { +namespace net { + +class nsHttpConnection; + +class ASpdySession : public nsAHttpTransaction { + public: + ASpdySession() = default; + virtual ~ASpdySession() = default; + + [[nodiscard]] virtual bool AddStream(nsAHttpTransaction*, int32_t, + 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; + + virtual void PrintDiagnostics(nsCString& log) = 0; + + bool ResponseTimeoutEnabled() const final { return true; } + + virtual void SendPing() = 0; + + const static uint32_t kSendingChunkSize = 16000; + const static uint32_t kTCPSendBufferSize = 131072; + const static uint32_t kInitialPushAllowance = 131072; // match default pref + + // 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 WebSocketSupport GetWebSocketSupport() = 0; + + virtual already_AddRefed CreateTunnelStream( + nsAHttpTransaction* aHttpTransaction, nsIInterfaceRequestor* aCallbacks, + PRIntervalTime aRtt, bool aIsWebSocket = false) = 0; +}; + +using ALPNCallback = bool (*)(nsITLSSocketControl*); + +// 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; + + SpdyVersion Version; // telemetry enum e.g. SPDY_VERSION_31 + nsCString VersionString; // 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; +}; + +} // 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..89e34a4694 --- /dev/null +++ b/netwerk/protocol/http/AltDataOutputStreamChild.cpp @@ -0,0 +1,204 @@ +/* -*- 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 size_t kChunkSize = 128 * 1024; + size_t next = std::min(data.Length(), kChunkSize); + for (size_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::StreamStatus() { + if (!mIPCOpen) { + return NS_ERROR_NOT_AVAILABLE; + } + return mError; +} + +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 = GetMainThreadSerialEventTarget(); + } + + 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..252e313358 --- /dev/null +++ b/netwerk/protocol/http/AltDataOutputStreamParent.cpp @@ -0,0 +1,87 @@ +/* -*- 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/PerfStats.h" +#include "mozilla/Unused.h" +#include "nsIAsyncOutputStream.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"); + PerfStats::RecordMeasurementStart(PerfStats::Metric::JSBC_IO_Write); +} + +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) { + PerfStats::RecordMeasurementEnd(PerfStats::Metric::JSBC_IO_Write); + + 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..79959f2161 --- /dev/null +++ b/netwerk/protocol/http/AltServiceChild.cpp @@ -0,0 +1,111 @@ +/* -*- 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()); + } + }; + + 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, bool aPrivateBrowsing, + 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, + aPrivateBrowsing, 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..cc70a0e048 --- /dev/null +++ b/netwerk/protocol/http/AltServiceChild.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 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, bool aPrivateBrowsing, + 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..468925c2ad --- /dev/null +++ b/netwerk/protocol/http/AltServiceParent.cpp @@ -0,0 +1,49 @@ +/* -*- 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) { + LOG(("AltServiceParent::RecvClearHostMapping [this=%p]\n", this)); + if (gHttpHandler) { + gHttpHandler->AltServiceCache()->ClearHostMapping(aHost, aPort, + aOriginAttributes); + } + 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 bool& aPrivateBrowsing, + 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, aPrivateBrowsing, 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..471b7049b2 --- /dev/null +++ b/netwerk/protocol/http/AltServiceParent.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 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); + + mozilla::ipc::IPCResult RecvProcessHeader( + const nsCString& aBuf, const nsCString& aOriginScheme, + const nsCString& aOriginHost, const int32_t& aOriginPort, + const nsACString& aUsername, const bool& aPrivateBrowsing, + 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..a386206b6d --- /dev/null +++ b/netwerk/protocol/http/AlternateServices.cpp @@ -0,0 +1,1354 @@ +/* -*- 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 "mozilla/Atomics.h" +#include "mozilla/StaticPrefs_network.h" +#include "mozilla/SyncRunnable.h" +#include "mozilla/dom/PContent.h" +#include "mozilla/net/AltSvcTransactionChild.h" +#include "mozilla/net/AltSvcTransactionParent.h" +#include "nsComponentManagerUtils.h" +#include "nsEscape.h" +#include "nsHttpChannel.h" +#include "nsHttpConnectionInfo.h" +#include "nsHttpHandler.h" +#include "nsHttpTransaction.h" +#include "nsIOService.h" +#include "nsITLSSocketControl.h" +#include "nsIWellKnownOpportunisticUtils.h" +#include "nsThreadUtils.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, + bool privateBrowsing, 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); + 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; + } + + SpdyInformation* spdyInfo = gHttpHandler->SpdyInfo(); + if (!(npnToken.Equals(spdyInfo->VersionString) && + StaticPrefs::network_http_http2_enabled()) && + !(isHttp3 && nsHttpHandler::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, + privateBrowsing, 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); + } 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(nsIDataStorage* storage, int32_t epoch, + const nsACString& originScheme, + const nsACString& originHost, int32_t originPort, + const nsACString& username, bool privateBrowsing, + 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), + mPrivate(privateBrowsing), + mExpiresAt(expiresAt), + mNPNToken(npnToken), + mOriginAttributes(originAttributes), + 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, + mOriginAttributes, mIsHttp3); + } +} + +void AltSvcMapping::MakeHashKey(nsCString& outKey, + const nsACString& originScheme, + const nsACString& originHost, + int32_t originPort, bool privateBrowsing, + 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(':'); + + outKey.Append(aHttp3 ? '3' : '.'); +} + +int32_t AltSvcMapping::TTL() { return mExpiresAt - NowInSeconds(); } + +void AltSvcMapping::SyncString(const nsCString& str) { + MOZ_ASSERT(NS_IsMainThread()); + (void)mStorage->Put(HashKey(), str, + mPrivate ? nsIDataStorage::DataType::Private + : nsIDataStorage::DataType::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; + } + + (void)mStorage->Put(HashKey(), value, + mPrivate ? nsIDataStorage::DataType::Private + : nsIDataStorage::DataType::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, pi, originAttributes, + mAlternateHost, mAlternatePort, mIsHttp3, false); + + // 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.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(""_ns); // Formerly topWindowOrigin. Now unused empty string. + out.Append('|'); // Be careful, the top window origin may contain colons! + out.Append('n'); // Formerly mIsolated. Now always 'n'. Should remove someday + out.Append(':'); + out.Append(mIsHttp3 ? 'y' : 'n'); + out.Append(':'); + // Add code to serialize new members here! +} + +AltSvcMapping::AltSvcMapping(nsIDataStorage* storage, int32_t epoch, + const nsCString& str) + : mStorage(storage), mStorageEpoch(epoch) { + 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; + // TopWindowOrigin used to be encoded here. Now it's unused. + separator = ':'; + _NS_NEXT_TOKEN; + // mIsolated used to be encoded here. Now it's unused. + _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, 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), + 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()); + // We don't want to let this transaction use consistent connection. + mCaps &= ~NS_HTTP_ALLOW_KEEPALIVE; +} + +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 socketControl; + mConnection->GetTLSSocketControl(getter_AddRefs(socketControl)); + + 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, loadInfo)) || + NS_FAILED(chan->SetAllowAltSvc(false)) || + NS_FAILED(chan->SetRedirectMode( + nsIHttpChannelInternal::REDIRECT_MODE_ERROR)) || + 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()); + uint64_t oldLen = static_cast(mWKResponse.Length()); + uint64_t newLen = static_cast(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 [%zd]\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() { + static Atomic initialized(false); + + if (initialized) { + return; + } + + auto initTask = [&]() { + MOZ_ASSERT(NS_IsMainThread()); + + // nsIDataStorage gives synchronous access to a memory based hash table + // that is backed by disk where those writes are done asynchronously + // on another thread + nsCOMPtr dataStorageManager( + do_GetService("@mozilla.org/security/datastoragemanager;1")); + if (!dataStorageManager) { + LOG(("AltSvcCache::EnsureStorageInited WARN NO STORAGE MANAGER\n")); + return; + } + nsresult rv = dataStorageManager->Get( + nsIDataStorageManager::AlternateServices, getter_AddRefs(mStorage)); + if (NS_FAILED(rv) || !mStorage) { + LOG(("AltSvcCache::EnsureStorageInited WARN NO STORAGE\n")); + return; + } + initialized = true; + + mStorageEpoch = NowInSeconds(); + }; + + if (NS_IsMainThread()) { + initTask(); + return; + } + + nsCOMPtr main = GetMainThreadSerialEventTarget(); + if (!main) { + return; + } + + SyncRunnable::DispatchToThread( + main, + 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()) { + bool isReady; + nsresult rv = mStorage->IsReady(&isReady); + if (NS_FAILED(rv)) { + LOG(("AltSvcCache::LookupMapping %p mStorage->IsReady failed\n", this)); + return nullptr; + } + if (!isReady) { + LOG(("AltSvcCache::LookupMapping %p skip when storage is not ready\n", + this)); + return nullptr; + } + } + + nsAutoCString val; + nsresult rv = + mStorage->Get(key, + privateBrowsing ? nsIDataStorage::DataType::Private + : nsIDataStorage::DataType::Persistent, + val); + if (NS_FAILED(rv) && rv != NS_ERROR_NOT_AVAILABLE) { + LOG(("AltSvcCache::LookupMapping %p mStorage->Get failed \n", this)); + return nullptr; + } + if (rv == NS_ERROR_NOT_AVAILABLE || val.IsEmpty()) { + LOG(("AltSvcCache::LookupMapping %p MISS\n", this)); + return nullptr; + } + RefPtr mapping = + new AltSvcMapping(mStorage, mStorageEpoch, val); + if (!mapping->Validated() && (mapping->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)); + (void)mStorage->Remove(key, mapping->Private() + ? nsIDataStorage::DataType::Private + : nsIDataStorage::DataType::Persistent); + return nullptr; + } + + if (mapping->IsHttp3() && + (!nsHttpHandler::IsHttp3Enabled() || + !gHttpHandler->IsHttp3VersionSupported(mapping->NPNToken()) || + gHttpHandler->IsHttp3Excluded(mapping->AlternateHost()))) { + // If Http3 is disabled or the version not supported anymore, remove the + // mapping. + (void)mStorage->Remove(key, mapping->Private() + ? nsIDataStorage::DataType::Private + : nsIDataStorage::DataType::Persistent); + return nullptr; + } + + if (mapping->TTL() <= 0) { + LOG(("AltSvcCache::LookupMapping %p expired hit - MISS\n", this)); + (void)mStorage->Remove(key, mapping->Private() + ? nsIDataStorage::DataType::Private + : nsIDataStorage::DataType::Persistent); + return nullptr; + } + + MOZ_ASSERT(mapping->Private() == privateBrowsing); + LOG(("AltSvcCache::LookupMapping %p HIT %p\n", this, mapping.get())); + return mapping.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; + } + + if (map->IsHttp3()) { + bool isDirectOrNoProxy = pi ? pi->IsDirect() : true; + if (!isDirectOrNoProxy) { + LOG( + ("AltSvcCache::UpdateAltServiceMapping %p map %p ignored h3 because " + "proxy is in use %p\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, 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, + 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, + 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) + : Runnable("net::ProxyClearHostMapping"), + mHost(host), + mPort(port), + mOriginAttributes(originAttributes) {} + + NS_IMETHOD Run() override { + MOZ_ASSERT(NS_IsMainThread()); + gHttpHandler->AltServiceCache()->ClearHostMapping(mHost, mPort, + mOriginAttributes); + return NS_OK; + } + + private: + nsCString mHost; + int32_t mPort; + OriginAttributes mOriginAttributes; +}; + +void AltSvcCache::ClearHostMapping(const nsACString& host, int32_t port, + const OriginAttributes& originAttributes) { + MOZ_ASSERT(XRE_IsParentProcess()); + + if (!NS_IsMainThread()) { + nsCOMPtr event = + new ProxyClearHostMapping(host, port, originAttributes); + 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) { + AltSvcMapping::MakeHashKey(key, scheme, host, port, bool(pb), + originAttributes, false); + RefPtr existing = LookupMapping(key, bool(pb)); + if (existing) { + existing->SetExpired(); + } + AltSvcMapping::MakeHashKey(key, scheme, host, port, bool(pb), + 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()); + } +} + +void AltSvcCache::ClearAltServiceMappings() { + MOZ_ASSERT(NS_IsMainThread()); + if (mStorage) { + (void)mStorage->Clear(); + } +} + +nsresult AltSvcCache::GetAltSvcCacheKeys(nsTArray& value) { + MOZ_ASSERT(NS_IsMainThread()); + if (gHttpHandler->AllowAltSvc() && mStorage) { + nsTArray> items; + nsresult rv = mStorage->GetAll(items); + if (NS_FAILED(rv)) { + return rv; + } + + for (const auto& item : items) { + nsAutoCString key; + rv = item->GetKey(key); + if (NS_FAILED(rv)) { + return rv; + } + value.AppendElement(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; +} + +template class AltSvcTransaction; + +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..4de5a281c0 --- /dev/null +++ b/netwerk/protocol/http/AlternateServices.h @@ -0,0 +1,271 @@ +/* -*- 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 "nsRefPtrHashtable.h" +#include "nsString.h" +#include "nsIDataStorage.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(nsIDataStorage* storage, int32_t storageEpoch, + const nsACString& originScheme, const nsACString& originHost, + int32_t originPort, const nsACString& username, + bool privateBrowsing, uint32_t expiresAt, + const nsACString& alternateHost, int32_t alternatePort, + const nsACString& npnToken, + const OriginAttributes& originAttributes, bool aIsHttp3); + + public: + AltSvcMapping(nsIDataStorage* storage, int32_t storageEpoch, + const nsCString& str); + + static void ProcessHeader( + const nsCString& buf, const nsCString& originScheme, + const nsCString& originHost, int32_t originPort, + const nsACString& username, bool privateBrowsing, + 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* proxyInfo); + + 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; } + + 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, + const OriginAttributes& originAttributes, + bool aHttp3); + + bool IsHttp3() { return mIsHttp3; } + const nsCString& NPNToken() const { return mNPNToken; } + + private: + virtual ~AltSvcMapping() = default; + void SyncString(const nsCString& str); + nsCOMPtr mStorage; + int32_t mStorageEpoch; + void Serialize(nsCString& out); + + nsCString mHashKey; + + // If you change any of these members, update Serialize() + nsCString mAlternateHost; + int32_t mAlternatePort{-1}; + + nsCString mOriginHost; + int32_t mOriginPort{-1}; + + nsCString mUsername; + bool mPrivate{false}; + + // alt-svc mappping + uint32_t mExpiresAt{0}; + + bool mValidated{false}; + // origin is https:// + MOZ_INIT_OUTSIDE_CTOR bool mHttps{false}; + // .wk allows http and https on same con + MOZ_INIT_OUTSIDE_CTOR bool mMixedScheme{false}; + + nsCString mNPNToken; + + OriginAttributes mOriginAttributes; + + bool mSyncOnlyOnSuccess{false}; + bool mIsHttp3{false}; +}; + +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() = default; + 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 privateBrowsing, const OriginAttributes& originAttributes, + bool aHttp2Allowed, bool aHttp3Allowed); + void ClearAltServiceMappings(); + void ClearHostMapping(const nsACString& host, int32_t port, + const OriginAttributes& originAttributes); + void ClearHostMapping(nsHttpConnectionInfo* ci); + nsIDataStorage* GetStoragePtr() { return mStorage.get(); } + int32_t StorageEpoch() { return mStorageEpoch; } + nsresult GetAltSvcCacheKeys(nsTArray& value); + + private: + void EnsureStorageInited(); + already_AddRefed LookupMapping(const nsCString& key, + bool privateBrowsing); + nsCOMPtr mStorage; + int32_t mStorageEpoch{0}; +}; + +// 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..13f1ca88f0 --- /dev/null +++ b/netwerk/protocol/http/BackgroundChannelRegistrar.cpp @@ -0,0 +1,99 @@ +/* -*- 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 "mozilla/ClearOnShutdown.h" +#include "mozilla/StaticPtr.h" +#include "HttpBackgroundChannelParent.h" +#include "HttpChannelParent.h" +#include "nsXULAppAPI.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(); + ClearOnShutdown(&gSingleton); + } + return do_AddRef(gSingleton); +} + +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.InsertOrUpdate(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.InsertOrUpdate(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..8f926103a2 --- /dev/null +++ b/netwerk/protocol/http/BackgroundChannelRegistrar.h @@ -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/. */ + +#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 { + using ChannelHashtable = + nsRefPtrHashtable; + using BackgroundChannelHashtable = + nsRefPtrHashtable; + + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIBACKGROUNDCHANNELREGISTRAR + + explicit BackgroundChannelRegistrar(); + + // Singleton accessor + static already_AddRefed GetOrCreate(); + + 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..481bec5985 --- /dev/null +++ b/netwerk/protocol/http/BackgroundDataBridgeChild.cpp @@ -0,0 +1,60 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 nsACString& data, + const TimeStamp& aOnDataAvailableStartTime) { + if (!mBgChild) { + return IPC_OK(); + } + + if (mBgChild->ChannelClosed()) { + Close(); + return IPC_OK(); + } + + return mBgChild->RecvOnTransportAndData(NS_OK, NS_NET_STATUS_RECEIVING_FROM, + offset, count, data, true, + aOnDataAvailableStartTime); +} + +mozilla::ipc::IPCResult BackgroundDataBridgeChild::RecvOnStopRequest( + nsresult aStatus, const ResourceTimingStructArgs& aTiming, + const TimeStamp& aLastActiveTabOptHit, + const nsHttpHeaderArray& aResponseTrailers, + const TimeStamp& aOnStopRequestStartTime) { + if (!mBgChild) { + return IPC_OK(); + } + + if (mBgChild->ChannelClosed()) { + Close(); + return IPC_OK(); + } + + return mBgChild->RecvOnStopRequest( + aStatus, aTiming, aLastActiveTabOptHit, aResponseTrailers, + nsTArray(), true, aOnStopRequestStartTime); +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/BackgroundDataBridgeChild.h b/netwerk/protocol/http/BackgroundDataBridgeChild.h new file mode 100644 index 0000000000..13c890e005 --- /dev/null +++ b/netwerk/protocol/http/BackgroundDataBridgeChild.h @@ -0,0 +1,41 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 nsACString& data, + const TimeStamp& aOnDataAvailableStartTime); + mozilla::ipc::IPCResult RecvOnStopRequest( + nsresult aStatus, const ResourceTimingStructArgs& aTiming, + const TimeStamp& aLastActiveTabOptHit, + const nsHttpHeaderArray& aResponseTrailers, + const TimeStamp& aOnStopRequestStartTime); +}; + +} // 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..843c64023e --- /dev/null +++ b/netwerk/protocol/http/BackgroundDataBridgeParent.cpp @@ -0,0 +1,63 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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(GetCurrentSerialEventTarget()) { + if (SocketProcessChild* child = SocketProcessChild::GetSingleton()) { + child->AddDataBridgeToMap(aChannelID, this); + } +} + +void BackgroundDataBridgeParent::ActorDestroy(ActorDestroyReason aWhy) { + if (SocketProcessChild* child = SocketProcessChild::GetSingleton()) { + child->RemoveDataBridgeFromMap(mChannelID); + } +} + +already_AddRefed +BackgroundDataBridgeParent::GetBackgroundThread() { + return do_AddRef(mBackgroundThread); +} + +void BackgroundDataBridgeParent::Destroy() { + RefPtr self = this; + MOZ_ALWAYS_SUCCEEDS(mBackgroundThread->Dispatch( + NS_NewRunnableFunction("BackgroundDataBridgeParent::Destroy", + [self]() { + if (self->CanSend()) { + self->Close(); + } + }), + NS_DISPATCH_NORMAL)); +} + +void BackgroundDataBridgeParent::OnStopRequest( + nsresult aStatus, const ResourceTimingStructArgs& aTiming, + const TimeStamp& aLastActiveTabOptHit, + const nsHttpHeaderArray& aResponseTrailers, + const TimeStamp& aOnStopRequestStart) { + RefPtr self = this; + MOZ_ALWAYS_SUCCEEDS(mBackgroundThread->Dispatch( + NS_NewRunnableFunction("BackgroundDataBridgeParent::OnStopRequest", + [self, aStatus, aTiming, aLastActiveTabOptHit, + aResponseTrailers, aOnStopRequestStart]() { + if (self->CanSend()) { + Unused << self->SendOnStopRequest( + aStatus, aTiming, aLastActiveTabOptHit, + aResponseTrailers, aOnStopRequestStart); + self->Close(); + } + }), + 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..9bda9f3aa9 --- /dev/null +++ b/netwerk/protocol/http/BackgroundDataBridgeParent.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 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, + const TimeStamp& aOnStopRequestStart); + + private: + virtual ~BackgroundDataBridgeParent() = default; + + uint64_t mChannelID; + nsCOMPtr mBackgroundThread; +}; + +} // namespace net +} // namespace mozilla + +#endif // mozilla_net_BackgroundDataBridgeParent_h diff --git a/netwerk/protocol/http/BinaryHttpRequest.cpp b/netwerk/protocol/http/BinaryHttpRequest.cpp new file mode 100644 index 0000000000..fa28f4c5c9 --- /dev/null +++ b/netwerk/protocol/http/BinaryHttpRequest.cpp @@ -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/. */ + +#include "BinaryHttpRequest.h" + +namespace mozilla::net { + +NS_IMPL_ISUPPORTS(BinaryHttpRequest, nsIBinaryHttpRequest) + +NS_IMETHODIMP BinaryHttpRequest::GetMethod(nsACString& aMethod) { + aMethod.Assign(mMethod); + return NS_OK; +} + +NS_IMETHODIMP BinaryHttpRequest::GetScheme(nsACString& aScheme) { + aScheme.Assign(mScheme); + return NS_OK; +} + +NS_IMETHODIMP BinaryHttpRequest::GetAuthority(nsACString& aAuthority) { + aAuthority.Assign(mAuthority); + return NS_OK; +} + +NS_IMETHODIMP BinaryHttpRequest::GetPath(nsACString& aPath) { + aPath.Assign(mPath); + return NS_OK; +} + +NS_IMETHODIMP BinaryHttpRequest::GetHeaderNames( + nsTArray& aHeaderNames) { + aHeaderNames.Assign(mHeaderNames); + return NS_OK; +} + +NS_IMETHODIMP BinaryHttpRequest::GetHeaderValues( + nsTArray& aHeaderValues) { + aHeaderValues.Assign(mHeaderValues); + return NS_OK; +} + +NS_IMETHODIMP BinaryHttpRequest::GetContent(nsTArray& aContent) { + aContent.Assign(mContent); + return NS_OK; +} + +} // namespace mozilla::net diff --git a/netwerk/protocol/http/BinaryHttpRequest.h b/netwerk/protocol/http/BinaryHttpRequest.h new file mode 100644 index 0000000000..f8803f511c --- /dev/null +++ b/netwerk/protocol/http/BinaryHttpRequest.h @@ -0,0 +1,48 @@ +/* -*- 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_BinaryHttpRequest_h +#define mozilla_net_BinaryHttpRequest_h + +#include "nsIBinaryHttp.h" +#include "nsString.h" + +namespace mozilla::net { + +class BinaryHttpRequest final : public nsIBinaryHttpRequest { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIBINARYHTTPREQUEST + + BinaryHttpRequest(const nsACString& aMethod, const nsACString& aScheme, + const nsACString& aAuthority, const nsACString& aPath, + nsTArray&& aHeaderNames, + nsTArray&& aHeaderValues, + nsTArray&& aContent) + : mMethod(aMethod), + mScheme(aScheme), + mAuthority(aAuthority), + mPath(aPath), + mHeaderNames(std::move(aHeaderNames)), + mHeaderValues(std::move(aHeaderValues)), + mContent(std::move(aContent)) {} + + private: + ~BinaryHttpRequest() = default; + + const nsAutoCString mMethod; + const nsAutoCString mScheme; + const nsAutoCString mAuthority; + const nsAutoCString mPath; + const nsTArray mHeaderNames; + const nsTArray mHeaderValues; + const nsTArray mContent; +}; + +} // namespace mozilla::net + +#endif // mozilla_net_BinaryHttpRequest_h diff --git a/netwerk/protocol/http/CacheControlParser.cpp b/netwerk/protocol/http/CacheControlParser.cpp new file mode 100644 index 0000000000..79cdef5439 --- /dev/null +++ b/netwerk/protocol/http/CacheControlParser.cpp @@ -0,0 +1,134 @@ +/* -*- 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() { + do { + SkipWhites(); + 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; + } + + } while (CheckChar(',')); + + 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..9f48aa7eda --- /dev/null +++ b/netwerk/protocol/http/CachePushChecker.cpp @@ -0,0 +1,248 @@ +/* -*- 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" +#include "CacheControlParser.h" +#include "nsHttpHandler.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(GetCurrentSerialEventTarget()) {} + +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, 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, 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, + false /* forceValidateCacheContent */, 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, + 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/ClassOfService.h b/netwerk/protocol/http/ClassOfService.h new file mode 100644 index 0000000000..bf971d41df --- /dev/null +++ b/netwerk/protocol/http/ClassOfService.h @@ -0,0 +1,76 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 __ClassOfService_h__ +#define __ClassOfService_h__ + +#include "nsIClassOfService.h" +#include "nsPrintfCString.h" +#include "ipc/IPCMessageUtils.h" + +namespace mozilla::net { + +class ClassOfService { + public: + ClassOfService() : mClassFlags(0), mIncremental(false) {} + ClassOfService(unsigned long flags, bool incremental) + : mClassFlags(flags), mIncremental(incremental) {} + + // class flags (priority) + unsigned long Flags() const { return mClassFlags; } + void SetFlags(unsigned long flags) { mClassFlags = flags; } + + // incremental flags + bool Incremental() const { return mIncremental; } + void SetIncremental(bool incremental) { mIncremental = incremental; } + + static void ToString(const ClassOfService aCos, nsACString& aOut) { + return ToString(aCos.Flags(), aOut); + } + + static void ToString(unsigned long aFlags, nsACString& aOut) { + aOut = nsPrintfCString("%lX", aFlags); + } + + private: + unsigned long mClassFlags; + bool mIncremental; + friend IPC::ParamTraits; + friend bool operator==(const ClassOfService& lhs, const ClassOfService& rhs); + friend bool operator!=(const ClassOfService& lhs, const ClassOfService& rhs); +}; + +inline bool operator==(const ClassOfService& lhs, const ClassOfService& rhs) { + return lhs.mClassFlags == rhs.mClassFlags && + lhs.mIncremental == rhs.mIncremental; +} + +inline bool operator!=(const ClassOfService& lhs, const ClassOfService& rhs) { + return !(lhs == rhs); +} + +} // namespace mozilla::net + +namespace IPC { +template <> +struct ParamTraits { + typedef mozilla::net::ClassOfService paramType; + + static void Write(MessageWriter* aWriter, const paramType& aParam) { + WriteParam(aWriter, aParam.mClassFlags); + WriteParam(aWriter, aParam.mIncremental); + } + + static bool Read(MessageReader* aReader, paramType* aResult) { + if (!ReadParam(aReader, &aResult->mClassFlags) || + !ReadParam(aReader, &aResult->mIncremental)) + return false; + + return true; + } +}; + +} // namespace IPC + +#endif diff --git a/netwerk/protocol/http/ConnectionDiagnostics.cpp b/netwerk/protocol/http/ConnectionDiagnostics.cpp new file mode 100644 index 0000000000..aa1358538d --- /dev/null +++ b/netwerk/protocol/http/ConnectionDiagnostics.cpp @@ -0,0 +1,239 @@ +/* -*- 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" +#include "mozilla/StaticPrefs_network.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", + StaticPrefs::network_http_http2_enabled()); + mLogData.AppendPrintf("MaxSocketCount() = %d\n", + gHttpHandler->MaxSocketCount()); + mLogData.AppendPrintf("mNumActiveConns = %d\n", mNumActiveConns); + mLogData.AppendPrintf("mNumIdleConns = %d\n", mNumIdleConns); + + for (const RefPtr& ent : mCT.Values()) { + 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(" DnsAndSock Length = %zu\n", + mDnsAndConnectSockets.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 < mDnsAndConnectSockets.Length(); ++i) { + log.AppendPrintf(" :: Half Open #%u\n", i); + mDnsAndConnectSockets[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 (const auto& entry : mPendingTransactionTable) { + log.AppendPrintf(" :: Pending Transactions with Window ID = %" PRIu64 + "\n", + entry.GetKey()); + for (uint32_t j = 0; j < entry.GetData()->Length(); ++j) { + log.AppendPrintf(" ::: Pending Transaction #%u\n", i); + entry.GetData()->ElementAt(j)->PrintDiagnostics(log); + ++i; + } + } +} + +void DnsAndConnectSocket::PrintDiagnostics(nsCString& log) { + log.AppendPrintf(" has connected = %d, isSpeculative = %d\n", + HasConnected(), IsSpeculative()); + + TimeStamp now = TimeStamp::Now(); + + if (mPrimaryTransport.mSynStarted.IsNull()) { + log.AppendPrintf(" primary not started\n"); + } else { + log.AppendPrintf(" primary started %.2fms ago\n", + (now - mPrimaryTransport.mSynStarted).ToMilliseconds()); + } + + if (mBackupTransport.mSynStarted.IsNull()) { + log.AppendPrintf(" backup not started\n"); + } else { + log.AppendPrintf(" backup started %.2f ago\n", + (now - mBackupTransport.mSynStarted).ToMilliseconds()); + } + + log.AppendPrintf(" primary transport %d, backup transport %d\n", + !!mPrimaryTransport.mSocketTransport, + !!mBackupTransport.mSocketTransport); +} + +void nsHttpConnection::PrintDiagnostics(nsCString& log) { + log.AppendPrintf(" CanDirectlyActivate = %d\n", CanDirectlyActivate()); + + log.AppendPrintf(" npncomplete = %d setupSSLCalled = %d\n", + mTlsHandshaker->NPNComplete(), + mTlsHandshaker->SetupSSLCalled()); + + 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, + !!mSpdySession); + + 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); + + log.AppendPrintf(" read/written %" PRId64 "/%" PRId64 "\n", + mHttp3Session ? mHttp3Session->BytesRead() : 0, + mHttp3Session ? mHttp3Session->GetBytesWritten() : 0); + + 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.Length()); + + 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", static_cast(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 dnsAndSock = do_QueryReferent(mDnsAndSock); + log.AppendPrintf(" Waiting for half open sock: %p or connection: %p\n", + dnsAndSock.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..473f0ab9fc --- /dev/null +++ b/netwerk/protocol/http/ConnectionEntry.cpp @@ -0,0 +1,1102 @@ +/* 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" +#include "mozilla/StaticPrefs_network.h" +#include "nsHttpHandler.h" + +namespace mozilla { +namespace net { + +// ConnectionEntry +ConnectionEntry::~ConnectionEntry() { + LOG(("ConnectionEntry::~ConnectionEntry this=%p", this)); + + MOZ_ASSERT(!mIdleConns.Length()); + MOZ_ASSERT(!mActiveConns.Length()); + MOZ_DIAGNOSTIC_ASSERT(!mDnsAndConnectSockets.Length()); + MOZ_ASSERT(!PendingQueueLength()); + MOZ_ASSERT(!UrgentStartQueueLength()); + MOZ_ASSERT(!mDoNotDestroy); +} + +ConnectionEntry::ConnectionEntry(nsHttpConnectionInfo* ci) + : mConnInfo(ci), + mUsingSpdy(false), + mCanUseSpdy(true), + mPreferIPv4(false), + mPreferIPv6(false), + mUsedForConnection(false), + mDoNotDestroy(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) != + nullptr; +} + +uint32_t ConnectionEntry::UnconnectedDnsAndConnectSockets() const { + uint32_t unconnectedDnsAndConnectSockets = 0; + for (uint32_t i = 0; i < mDnsAndConnectSockets.Length(); ++i) { + if (!mDnsAndConnectSockets[i]->HasConnected()) { + ++unconnectedDnsAndConnectSockets; + } + } + return unconnectedDnsAndConnectSockets; +} + +void ConnectionEntry::InsertIntoDnsAndConnectSockets( + DnsAndConnectSocket* sock) { + mDnsAndConnectSockets.AppendElement(sock); + gHttpHandler->ConnMgr()->IncreaseNumDnsAndConnectSockets(); +} + +void ConnectionEntry::RemoveDnsAndConnectSocket(DnsAndConnectSocket* dnsAndSock, + bool abandon) { + if (abandon) { + dnsAndSock->Abandon(); + } + if (mDnsAndConnectSockets.RemoveElement(dnsAndSock)) { + gHttpHandler->ConnMgr()->DecreaseNumDnsAndConnectSockets(); + } + + if (!UnconnectedDnsAndConnectSockets()) { + // 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::RemoveDnsAndConnectSocket\n" + " failed to process pending queue\n")); + } + } +} + +void ConnectionEntry::CloseAllDnsAndConnectSockets() { + for (const auto& dnsAndSock : mDnsAndConnectSockets) { + dnsAndSock->Abandon(); + gHttpHandler->ConnMgr()->DecreaseNumDnsAndConnectSockets(); + } + mDnsAndConnectSockets.Clear(); + nsresult rv = gHttpHandler->ConnMgr()->ProcessPendingQ(mConnInfo); + if (NS_FAILED(rv)) { + LOG( + ("ConnectionEntry::CloseAllDnsAndConnectSockets\n" + " failed to process pending queue\n")); + } +} + +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(mConnInfo->HashKey()); +} + +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() && + StaticPrefs::network_http_http2_enabled() && mUsingSpdy && + (mDnsAndConnectSockets.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 (UnconnectedDnsAndConnectSockets()) { + 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 DnsAndConnectSocket'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() + UnconnectedDnsAndConnectSockets(); +} + +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++; + } +} + +void ConnectionEntry::CloseH2WebsocketConnections() { + while (mH2WebsocketConns.Length()) { + RefPtr conn(mH2WebsocketConns[0]); + mH2WebsocketConns.RemoveElementAt(0); + + // safe to close connection since we are on the socket thread + // closing via transaction to break connection/transaction bond + conn->CloseTransaction(conn->Transaction(), NS_ERROR_ABORT, true); + } +} + +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; +} + +nsresult ConnectionEntry::RemovePendingConnection(HttpConnectionBase* conn) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + if (!mPendingConns.RemoveElement(conn)) { + return NS_ERROR_UNEXPECTED; + } + + 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(); + } + + mCoalescingKeys.Clear(); +} + +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()) { + for (uint32_t index = 0; index < mPendingConns.Length(); ++index) { + RefPtr conn = do_QueryObject(mPendingConns[index]); + if (conn) { + conn->CheckForTraffic(true); + } + } + + 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->CheckForTraffic(true); + if (conn->EverUsedSpdy() && + StaticPrefs:: + network_http_http2_move_to_pending_list_after_network_change()) { + mActiveConns.RemoveElementAt(index); + gHttpHandler->ConnMgr()->DecrementActiveConnCount(conn); + mPendingConns.AppendElement(conn); + LOG(("Move active connection to pending list [conn=%p]\n", + conn.get())); + } + } + } + } + + // 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(); +} + +bool ConnectionEntry::IsInH2WebsocketConns(HttpConnectionBase* conn) { + return mH2WebsocketConns.Contains(conn); +} + +void ConnectionEntry::InsertIntoH2WebsocketConns(HttpConnectionBase* conn) { + // no incrementing of connection count since it is just a "fake" connection + mH2WebsocketConns.AppendElement(conn); +} + +void ConnectionEntry::RemoveH2WebsocketConns(HttpConnectionBase* conn) { + mH2WebsocketConns.RemoveElement(conn); +} + +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->SetCloseReason( + ConnectionCloseReason::CLOSE_EXISTING_CONN_FOR_COALESCING); + otherConn->DontReuse(); + } + } + + // Cancel any other pending connections - their associated transactions + // are in the pending queue and will be dispatched onto this new connection + CloseAllDnsAndConnectSockets(); +} + +bool ConnectionEntry::FindConnToClaim( + PendingTransactionInfo* pendingTransInfo) { + nsHttpTransaction* trans = pendingTransInfo->Transaction(); + + for (const auto& dnsAndSock : mDnsAndConnectSockets) { + if (dnsAndSock->AcceptsTransaction(trans) && dnsAndSock->Claim()) { + pendingTransInfo->RememberDnsAndConnectSocket(dnsAndSock); + // We've found a speculative connection or a connection that + // is free to be used in the DnsAndConnectSockets 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 DnsAndConnectSocket\n", + mConnInfo->HashKey().get())); + + // return OK because we have essentially opened a new connection + // by converting a speculative DnsAndConnectSockets 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(); + } + } + + 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::ClosePendingConnections() { + while (mPendingConns.Length()) { + RefPtr conn(mPendingConns[0]); + mPendingConns.RemoveElementAt(0); + + // 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::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" + " dnsAndSock-len=%zu pending=%zu" + " urgentStart pending=%zu\n", + this, mConnInfo->Origin(), IdleConnectionsLength(), ActiveConnsLength(), + mDnsAndConnectSockets.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 DnsAndConnectSockets. + if (mDnsAndConnectSockets.Length()) { + TimeStamp currentTime = TimeStamp::Now(); + double maxConnectTime_ms = gHttpHandler->ConnectTimeout(); + + for (const auto& dnsAndSock : Reversed(mDnsAndConnectSockets)) { + double delta = dnsAndSock->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 DnsAndConnectSocket to %s after %.2fms.\n", + mConnInfo->HashKey().get(), delta)); + dnsAndSock->CloseTransports(NS_ERROR_NET_TIMEOUT); + } + + // If this DnsAndConnectSocket hangs around for 5 seconds after we've + // closed() it then just abandon the socket. + if (delta > maxConnectTime_ms + 5000) { + LOG(("Abandon DnsAndConnectSocket to %s after %.2fms.\n", + mConnInfo->HashKey().get(), delta)); + RemoveDnsAndConnectSocket(dnsAndSock, true); + } + } + } + if (mDnsAndConnectSockets.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; + } + } +} + +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 < mDnsAndConnectSockets.Length(); i++) { + DnsAndConnectSockets dnsAndSock{}; + dnsAndSock.speculative = mDnsAndConnectSockets[i]->IsSpeculative(); + data.dnsAndSocks.AppendElement(dnsAndSock); + } + 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 DnsAndConnectSockets 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 DnsAndConnectSockets belonging to the given transaction. + nsWeakPtr tmp = pendingTransInfo->ForgetDnsAndConnectSocketAndActiveConn(); + RefPtr dnsAndSock = do_QueryReferent(tmp); + if (dnsAndSock) { + RemoveDnsAndConnectSocket(dnsAndSock, true); + } + 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 DnsAndConnectSockets and idle + // connections. This is to make sure the new echConfig will be used for the + // next connection. + CloseAllDnsAndConnectSockets(); + CloseIdleConnections(); +} + +bool ConnectionEntry::MaybeProcessCoalescingKeys(nsIDNSAddrRecord* dnsRecord, + bool aIsHttp3) { + if (!mConnInfo || !mConnInfo->EndToEndSSL() || (!aIsHttp3 && !AllowHttp2()) || + mConnInfo->UsingProxy() || !mCoalescingKeys.IsEmpty() || !dnsRecord) { + return false; + } + + nsTArray addressSet; + nsresult rv = dnsRecord->GetAddresses(addressSet); + + if (NS_FAILED(rv) || addressSet.IsEmpty()) { + return false; + } + + 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( + ("ConnectionEntry::MaybeProcessCoalescingKeys skip creating " + "Coalescing Key for host [%s]", + mConnInfo->Origin())); + continue; + } + nsCString* newKey = mCoalescingKeys.AppendElement(nsCString()); + newKey->SetLength(kIPv6CStrBufSize + 26); + addressSet[i].ToStringBuffer(newKey->BeginWriting(), kIPv6CStrBufSize); + newKey->SetLength(strlen(newKey->BeginReading())); + if (mConnInfo->GetAnonymous()) { + newKey->AppendLiteral("~A:"); + } else { + newKey->AppendLiteral("~.:"); + } + if (mConnInfo->GetFallbackConnection()) { + newKey->AppendLiteral("~F:"); + } else { + newKey->AppendLiteral("~.:"); + } + newKey->AppendInt(mConnInfo->OriginPort()); + newKey->AppendLiteral("/["); + nsAutoCString suffix; + mConnInfo->GetOriginAttributes().CreateSuffix(suffix); + newKey->Append(suffix); + newKey->AppendLiteral("]viaDNS"); + LOG( + ("ConnectionEntry::MaybeProcessCoalescingKeys " + "Established New Coalescing Key # %d for host " + "%s [%s]", + i, mConnInfo->Origin(), newKey->get())); + } + return true; +} + +nsresult ConnectionEntry::CreateDnsAndConnectSocket( + 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 DnsAndConnectSocket( + mConnInfo, trans, caps, speculative, isFromPredictor, urgentStart); + + if (speculative) { + sock->SetAllow1918(allow1918); + } + + nsresult rv = sock->Init(this); + if (NS_FAILED(rv)) { + sock->Abandon(); + return rv; + } + + InsertIntoDnsAndConnectSockets(sock); + + if (pendingTransInfo && sock->Claim()) { + pendingTransInfo->RememberDnsAndConnectSocket(sock); + } + + return NS_OK; +} + +bool ConnectionEntry::AllowToRetryDifferentIPFamilyForHttp3(nsresult aError) { + LOG( + ("ConnectionEntry::AllowToRetryDifferentIPFamilyForHttp3 %p " + "error=%" PRIx32, + this, static_cast(aError))); + if (!IsHttp3()) { + MOZ_ASSERT(false, "Should not be called for non Http/3 connection"); + return false; + } + + if (!StaticPrefs::network_http_http3_retry_different_ip_family()) { + return false; + } + + // Only allow to retry with these two errors. + if (aError != NS_ERROR_CONNECTION_REFUSED && + aError != NS_ERROR_PROXY_CONNECTION_REFUSED) { + return false; + } + + // Already retried once. + if (mRetriedDifferentIPFamilyForHttp3) { + return false; + } + + return true; +} + +void ConnectionEntry::SetRetryDifferentIPFamilyForHttp3(uint16_t aIPFamily) { + LOG(("ConnectionEntry::SetRetryDifferentIPFamilyForHttp3 %p, af=%u", this, + aIPFamily)); + + mPreferIPv4 = false; + mPreferIPv6 = false; + + if (aIPFamily == AF_INET) { + mPreferIPv6 = true; + } + + if (aIPFamily == AF_INET6) { + mPreferIPv4 = true; + } + + mRetriedDifferentIPFamilyForHttp3 = true; + + LOG((" %p prefer ipv4=%d, ipv6=%d", this, (bool)mPreferIPv4, + (bool)mPreferIPv6)); + MOZ_DIAGNOSTIC_ASSERT(mPreferIPv4 ^ mPreferIPv6); +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/ConnectionEntry.h b/netwerk/protocol/http/ConnectionEntry.h new file mode 100644 index 0000000000..811c5481de --- /dev/null +++ b/netwerk/protocol/http/ConnectionEntry.h @@ -0,0 +1,231 @@ +/* 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 "DnsAndConnectSocket.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); + void CloseH2WebsocketConnections(); + void ClosePendingConnections(); + 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); + nsresult RemovePendingConnection(HttpConnectionBase* conn); + void MakeAllDontReuseExcept(HttpConnectionBase* conn); + bool FindConnToClaim(PendingTransactionInfo* pendingTransInfo); + void CloseActiveConnections(); + void CloseAllActiveConnsWithNullTransactcion(nsresult aCloseCode); + + bool IsInH2WebsocketConns(HttpConnectionBase* conn); + void InsertIntoH2WebsocketConns(HttpConnectionBase* conn); + void RemoveH2WebsocketConns(HttpConnectionBase* conn); + + 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 DnsAndConnectSocketsLength() const { + return mDnsAndConnectSockets.Length(); + } + + void InsertIntoDnsAndConnectSockets(DnsAndConnectSocket* sock); + void RemoveDnsAndConnectSocket(DnsAndConnectSocket* dnsAndSock, bool abandon); + void CloseAllDnsAndConnectSockets(); + + HttpRetParams GetConnectionData(); + void LogConnections(); + + const RefPtr mConnInfo; + + bool AvailableForDispatchNow(); + + // calculate the number of half open sockets that have not had at least 1 + // connection complete + uint32_t UnconnectedDnsAndConnectSockets() const; + + // Remove a particular DnsAndConnectSocket from the mDnsAndConnectSocket array + bool RemoveDnsAndConnectSocket(DnsAndConnectSocket*); + + bool MaybeProcessCoalescingKeys(nsIDNSAddrRecord* dnsRecord, + bool aIsHttp3 = false); + + nsresult CreateDnsAndConnectSocket(nsAHttpTransaction* trans, uint32_t caps, + bool speculative, bool isFromPredictor, + bool urgentStart, bool allow1918, + PendingTransactionInfo* pendingTransInfo); + + // 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; + + 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); + + bool AllowToRetryDifferentIPFamilyForHttp3(nsresult aError); + void SetRetryDifferentIPFamilyForHttp3(uint16_t aIPFamily); + + private: + void InsertIntoIdleConnections_internal(nsHttpConnection* conn); + void RemoveFromIdleConnectionsIndex(size_t inx); + bool RemoveFromIdleConnections(nsHttpConnection* conn); + + nsTArray> mIdleConns; // idle persistent connections + nsTArray> mActiveConns; // active connections + // When a connection is added to this mPendingConns list, it is primarily + // to keep the connection alive and to continue serving its ongoing + // transaction. While in this list, the connection will not be available to + // serve any new transactions and will remain here until its current + // transaction is complete. + nsTArray> mPendingConns; + // "fake" http2 websocket connections that needs to be cleaned up on shutdown + nsTArray> mH2WebsocketConns; + + nsTArray> + mDnsAndConnectSockets; // dns resolution and half open connections + + PendingTransactionQueue mPendingQ; + ~ConnectionEntry(); + + bool mRetriedDifferentIPFamilyForHttp3 = false; +}; + +} // 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..621c094112 --- /dev/null +++ b/netwerk/protocol/http/ConnectionHandle.cpp @@ -0,0 +1,98 @@ +/* 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" +#include "nsHttpHandler.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 %p\n", + mConn.get())); + } + } +} + +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); +} + +Http3WebTransportSession* ConnectionHandle::GetWebTransportSession( + nsAHttpTransaction* aTransaction) { + return mConn->GetWebTransportSession(aTransaction); +} + +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::CurrentBrowserIdChanged(uint64_t id) { + // Do nothing. +} + +PRIntervalTime ConnectionHandle::LastWriteTime() { + return mConn->LastWriteTime(); +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/ConnectionHandle.h b/netwerk/protocol/http/ConnectionHandle.h new file mode 100644 index 0000000000..79b627f28a --- /dev/null +++ b/netwerk/protocol/http/ConnectionHandle.h @@ -0,0 +1,41 @@ +/* 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__ + +#include "nsAHttpConnection.h" +#include "HttpConnectionBase.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/DnsAndConnectSocket.cpp b/netwerk/protocol/http/DnsAndConnectSocket.cpp new file mode 100644 index 0000000000..8895ac97e1 --- /dev/null +++ b/netwerk/protocol/http/DnsAndConnectSocket.cpp @@ -0,0 +1,1391 @@ +/* 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 "DnsAndConnectSocket.h" +#include "nsHttpConnection.h" +#include "nsIClassOfService.h" +#include "nsIDNSRecord.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsIHttpActivityObserver.h" +#include "nsSocketTransportService2.h" +#include "nsDNSService2.h" +#include "nsQueryObject.h" +#include "nsURLHelper.h" +#include "mozilla/Components.h" +#include "mozilla/StaticPrefs_network.h" +#include "mozilla/SyncRunnable.h" +#include "nsHttpHandler.h" +#include "ConnectionEntry.h" +#include "HttpConnectionUDP.h" +#include "nsServiceManagerUtils.h" +#include "mozilla/net/NeckoChannelParams.h" // For HttpActivityArgs. + +// 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 { + +//////////////////////// DnsAndConnectSocket +NS_IMPL_ADDREF(DnsAndConnectSocket) +NS_IMPL_RELEASE(DnsAndConnectSocket) + +NS_INTERFACE_MAP_BEGIN(DnsAndConnectSocket) + 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(nsIDNSListener) + NS_INTERFACE_MAP_ENTRY_CONCRETE(DnsAndConnectSocket) +NS_INTERFACE_MAP_END + +static void NotifyActivity(nsHttpConnectionInfo* aConnInfo, uint32_t aSubtype) { + HttpConnectionActivity activity( + aConnInfo->HashKey(), aConnInfo->GetOrigin(), aConnInfo->OriginPort(), + aConnInfo->EndToEndSSL(), !aConnInfo->GetEchConfig().IsEmpty(), + aConnInfo->IsHttp3()); + gHttpHandler->ObserveHttpActivityWithArgs( + activity, NS_ACTIVITY_TYPE_HTTP_CONNECTION, aSubtype, PR_Now(), 0, ""_ns); +} + +DnsAndConnectSocket::DnsAndConnectSocket(nsHttpConnectionInfo* ci, + nsAHttpTransaction* trans, + uint32_t caps, bool speculative, + bool isFromPredictor, bool urgentStart) + : mTransaction(trans), + mCaps(caps), + mSpeculative(speculative), + mUrgentStart(urgentStart), + mIsFromPredictor(isFromPredictor), + mConnInfo(ci) { + MOZ_ASSERT(ci && trans, "constructor with null arguments"); + LOG(("Creating DnsAndConnectSocket [this=%p trans=%p ent=%s key=%s]\n", this, + trans, mConnInfo->Origin(), mConnInfo->HashKey().get())); + + mIsHttp3 = mConnInfo->IsHttp3(); + if (speculative) { + Telemetry::AutoCounter + totalSpeculativeConn; + ++totalSpeculativeConn; + + if (isFromPredictor) { + Telemetry::AutoCounter + totalPreconnectsCreated; + ++totalPreconnectsCreated; + } + } + + MOZ_ASSERT(mConnInfo); + NotifyActivity(mConnInfo, + mSpeculative + ? NS_HTTP_ACTIVITY_SUBTYPE_SPECULATIVE_DNSANDSOCKET_CREATED + : NS_HTTP_ACTIVITY_SUBTYPE_DNSANDSOCKET_CREATED); +} + +void DnsAndConnectSocket::CheckIsDone() { + MOZ_DIAGNOSTIC_ASSERT(!mPrimaryTransport.mSocketTransport); + MOZ_DIAGNOSTIC_ASSERT(!mPrimaryTransport.mStreamOut); + MOZ_DIAGNOSTIC_ASSERT(!mPrimaryTransport.mDNSRequest); + MOZ_DIAGNOSTIC_ASSERT(!mBackupTransport.mSocketTransport); + MOZ_DIAGNOSTIC_ASSERT(!mBackupTransport.mStreamOut); + MOZ_DIAGNOSTIC_ASSERT(!mBackupTransport.mDNSRequest); +} + +DnsAndConnectSocket::~DnsAndConnectSocket() { + LOG(("Destroying DnsAndConnectSocket [this=%p]\n", this)); + MOZ_ASSERT(mState == DnsAndSocketState::DONE); + CheckIsDone(); + // Check in case something goes wrong that we decrease + // the nsHttpConnectionMgr active connection number. + mPrimaryTransport.MaybeSetConnectingDone(); + mBackupTransport.MaybeSetConnectingDone(); + + if (mSpeculative) { + Telemetry::AutoCounter + unusedSpeculativeConn; + ++unusedSpeculativeConn; + + if (mIsFromPredictor) { + Telemetry::AutoCounter + totalPreconnectsUnused; + ++totalPreconnectsUnused; + } + } +} + +nsresult DnsAndConnectSocket::Init(ConnectionEntry* ent) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + MOZ_ASSERT(mState == DnsAndSocketState::INIT); + + if (mConnInfo->GetRoutedHost().IsEmpty()) { + mPrimaryTransport.mHost = mConnInfo->GetOrigin(); + mBackupTransport.mHost = mConnInfo->GetOrigin(); + } else { + mPrimaryTransport.mHost = mConnInfo->GetRoutedHost(); + mBackupTransport.mHost = mConnInfo->GetRoutedHost(); + } + + CheckProxyConfig(); + + if (!mSkipDnsResolution) { + nsresult rv = SetupDnsFlags(ent); + NS_ENSURE_SUCCESS(rv, rv); + } + return SetupEvent(SetupEvents::INIT_EVENT); +} + +void DnsAndConnectSocket::CheckProxyConfig() { + if (nsCOMPtr proxyInfo = mConnInfo->ProxyInfo()) { + nsAutoCString proxyType(proxyInfo->Type()); + + bool proxyTransparent = false; + if (proxyType.EqualsLiteral("socks") || proxyType.EqualsLiteral("socks4")) { + proxyTransparent = true; + if (proxyInfo->Flags() & nsIProxyInfo::TRANSPARENT_PROXY_RESOLVES_HOST) { + mProxyTransparentResolvesHost = true; + } + } + + if (mProxyTransparentResolvesHost) { + // Name resolution is done on the server side. Just pretend + // client resolution is complete, this will get picked up later. + // since we don't need to do DNS now, we bypass the resolving + // step by initializing mNetAddr to an empty address, but we + // must keep the port. The SOCKS IO layer will use the hostname + // we send it when it's created, rather than the empty address + // we send with the connect call. + mPrimaryTransport.mSkipDnsResolution = true; + mBackupTransport.mSkipDnsResolution = true; + mSkipDnsResolution = true; + } + + if (!proxyTransparent && !proxyInfo->Host().IsEmpty()) { + mProxyNotTransparent = true; + mPrimaryTransport.mHost = proxyInfo->Host(); + mBackupTransport.mHost = proxyInfo->Host(); + } + } +} + +nsresult DnsAndConnectSocket::SetupDnsFlags(ConnectionEntry* ent) { + LOG(("DnsAndConnectSocket::SetupDnsFlags [this=%p] ", this)); + + nsIDNSService::DNSFlags dnsFlags = nsIDNSService::RESOLVE_DEFAULT_FLAGS; + bool disableIpv6ForBackup = false; + if (mCaps & NS_HTTP_REFRESH_DNS) { + dnsFlags = nsIDNSService::RESOLVE_BYPASS_CACHE; + } + if (mCaps & NS_HTTP_DISABLE_IPV4) { + dnsFlags |= nsIDNSService::RESOLVE_DISABLE_IPV4; + } else if (mCaps & NS_HTTP_DISABLE_IPV6) { + dnsFlags |= nsIDNSService::RESOLVE_DISABLE_IPV6; + } else if (ent->PreferenceKnown()) { + if (ent->mPreferIPv6) { + dnsFlags |= nsIDNSService::RESOLVE_DISABLE_IPV4; + } else if (ent->mPreferIPv4) { + dnsFlags |= nsIDNSService::RESOLVE_DISABLE_IPV6; + } + mPrimaryTransport.mRetryWithDifferentIPFamily = true; + mBackupTransport.mRetryWithDifferentIPFamily = true; + } else if (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". + disableIpv6ForBackup = true; + } + + if (ent->mConnInfo->HasIPHintAddress()) { + nsresult rv; + 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( + mPrimaryTransport.mHost, nsIDNSService::RESOLVE_OFFLINE, + mConnInfo->GetOriginAttributes(), getter_AddRefs(record)); + if (NS_FAILED(rv) || !record) { + LOG(("Setting Socket to use IP hint address")); + dnsFlags |= nsIDNSService::RESOLVE_IP_HINT; + } + } + + dnsFlags |= + nsIDNSService::GetFlagsFromTRRMode(NS_HTTP_TRR_MODE_FROM_FLAGS(mCaps)); + + // When we get here, we are not resolving using any configured proxy likely + // because of individual proxy setting on the request or because the host is + // excluded from proxying. Hence, force resolution despite global proxy-DNS + // configuration. + dnsFlags |= nsIDNSService::RESOLVE_IGNORE_SOCKS_DNS; + + NS_ASSERTION(!(dnsFlags & nsIDNSService::RESOLVE_DISABLE_IPV6) || + !(dnsFlags & nsIDNSService::RESOLVE_DISABLE_IPV4), + "Setting both RESOLVE_DISABLE_IPV6 and RESOLVE_DISABLE_IPV4"); + + mPrimaryTransport.mDnsFlags = dnsFlags; + mBackupTransport.mDnsFlags = dnsFlags; + if (disableIpv6ForBackup) { + mBackupTransport.mDnsFlags |= nsIDNSService::RESOLVE_DISABLE_IPV6; + } + LOG(("DnsAndConnectSocket::SetupDnsFlags flags=%u flagsBackup=%u [this=%p]", + mPrimaryTransport.mDnsFlags, mBackupTransport.mDnsFlags, this)); + NS_ASSERTION( + !(mBackupTransport.mDnsFlags & nsIDNSService::RESOLVE_DISABLE_IPV6) || + !(mBackupTransport.mDnsFlags & nsIDNSService::RESOLVE_DISABLE_IPV4), + "Setting both RESOLVE_DISABLE_IPV6 and RESOLVE_DISABLE_IPV4"); + return NS_OK; +} + +nsresult DnsAndConnectSocket::SetupEvent(SetupEvents event) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG(("DnsAndConnectSocket::SetupEvent state=%d event=%d this=%p", mState, + event, this)); + nsresult rv = NS_OK; + switch (event) { + case SetupEvents::INIT_EVENT: + MOZ_ASSERT(mState == DnsAndSocketState::INIT); + rv = mPrimaryTransport.Init(this); + if (NS_FAILED(rv)) { + mState = DnsAndSocketState::DONE; + } else if (mPrimaryTransport.FirstResolving()) { + mState = DnsAndSocketState::RESOLVING; + } else if (!mIsHttp3 && mPrimaryTransport.ConnectingOrRetry()) { + mState = DnsAndSocketState::CONNECTING; + SetupBackupTimer(); + } else { + MOZ_ASSERT(false); + mState = DnsAndSocketState::DONE; + Abandon(); + rv = NS_ERROR_UNEXPECTED; + } + break; + case SetupEvents::RESOLVED_PRIMARY_EVENT: + // This event may be posted multiple times if a DNS lookup is + // retriggered, e.g with different parameter. + if (!mIsHttp3 && (mState == DnsAndSocketState::RESOLVING)) { + mState = DnsAndSocketState::CONNECTING; + SetupBackupTimer(); + } + break; + case SetupEvents::PRIMARY_DONE_EVENT: + MOZ_ASSERT((mState == DnsAndSocketState::RESOLVING) || + (mState == DnsAndSocketState::CONNECTING) || + (mState == DnsAndSocketState::ONE_CONNECTED)); + CancelBackupTimer(); + mBackupTransport.CancelDnsResolution(); + if (mBackupTransport.ConnectingOrRetry()) { + mState = DnsAndSocketState::ONE_CONNECTED; + } else { + mState = DnsAndSocketState::DONE; + } + break; + case SetupEvents::BACKUP_DONE_EVENT: + MOZ_ASSERT((mState == DnsAndSocketState::CONNECTING) || + (mState == DnsAndSocketState::ONE_CONNECTED)); + if (mPrimaryTransport.ConnectingOrRetry()) { + mState = DnsAndSocketState::ONE_CONNECTED; + } else { + mState = DnsAndSocketState::DONE; + } + break; + case SetupEvents::BACKUP_TIMER_FIRED_EVENT: + MOZ_ASSERT(mState == DnsAndSocketState::CONNECTING); + mBackupTransport.Init(this); + } + LOG(("DnsAndConnectSocket::SetupEvent state=%d", mState)); + + if (mState == DnsAndSocketState::DONE) { + CheckIsDone(); + RefPtr self(this); + RefPtr ent = + gHttpHandler->ConnMgr()->FindConnectionEntry(mConnInfo); + if (ent) { + ent->RemoveDnsAndConnectSocket(this, false); + } + return rv; + } + return NS_OK; +} + +void DnsAndConnectSocket::SetupBackupTimer() { + uint16_t timeout = gHttpHandler->GetIdleSynTimeout(); + MOZ_ASSERT(!mSynTimer, "timer already initd"); + + // 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 (timeout && (!mSpeculative || mConnInfo->GetFallbackConnection()) && + !mIsHttp3) { + // 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(("DnsAndConnectSocket::SetupBackupTimer() [this=%p]", this)); + } else if (timeout) { + LOG(("DnsAndConnectSocket::SetupBackupTimer() [this=%p], did not arm\n", + this)); + } +} + +void DnsAndConnectSocket::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(("DnsAndConnectSocket::CancelBackupTimer()")); + mSynTimer->Cancel(); + + // Keeping the reference to the timer to remember we have already + // performed the backup connection. +} + +void DnsAndConnectSocket::Abandon() { + LOG(("DnsAndConnectSocket::Abandon [this=%p ent=%s] %p %p %p %p", this, + mConnInfo->Origin(), mPrimaryTransport.mSocketTransport.get(), + mBackupTransport.mSocketTransport.get(), + mPrimaryTransport.mStreamOut.get(), mBackupTransport.mStreamOut.get())); + + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + // Tell socket (and backup socket) to forget the half open socket. + mPrimaryTransport.Abandon(); + mBackupTransport.Abandon(); + + // Stop the timer - we don't want any new backups. + CancelBackupTimer(); + + mState = DnsAndSocketState::DONE; +} + +double DnsAndConnectSocket::Duration(TimeStamp epoch) { + if (mPrimaryTransport.mSynStarted.IsNull()) { + return 0; + } + + return (epoch - mPrimaryTransport.mSynStarted).ToMilliseconds(); +} + +NS_IMETHODIMP // method for nsITimerCallback +DnsAndConnectSocket::Notify(nsITimer* timer) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + MOZ_ASSERT(timer == mSynTimer, "wrong timer"); + + MOZ_ASSERT(!mBackupTransport.mDNSRequest); + MOZ_ASSERT(!mBackupTransport.mSocketTransport); + MOZ_ASSERT(mSynTimer); + + DebugOnly rv = SetupEvent(BACKUP_TIMER_FIRED_EVENT); + 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 +DnsAndConnectSocket::GetName(nsACString& aName) { + aName.AssignLiteral("DnsAndConnectSocket"); + return NS_OK; +} + +NS_IMETHODIMP +DnsAndConnectSocket::OnLookupComplete(nsICancelable* request, nsIDNSRecord* rec, + nsresult status) { + LOG(( + "DnsAndConnectSocket::OnLookupComplete: this=%p mState=%d status %" PRIx32 + ".", + this, mState, static_cast(status))); + + if (nsCOMPtr addrRecord = do_QueryInterface((rec))) { + nsIRequest::TRRMode effectivemode = nsIRequest::TRR_DEFAULT_MODE; + addrRecord->GetEffectiveTRRMode(&effectivemode); + nsITRRSkipReason::value skipReason = nsITRRSkipReason::TRR_UNSET; + addrRecord->GetTrrSkipReason(&skipReason); + if (mTransaction) { + mTransaction->SetTRRInfo(effectivemode, skipReason); + } + } + + MOZ_DIAGNOSTIC_ASSERT(request); + RefPtr deleteProtector(this); + + if (!request || (!IsPrimary(request) && !IsBackup(request))) { + return NS_OK; + } + + if (IsPrimary(request) && NS_SUCCEEDED(status)) { + mTransaction->OnTransportStatus(nullptr, NS_NET_STATUS_RESOLVED_HOST, 0); + } + + // When using a HTTP proxy, NS_ERROR_UNKNOWN_HOST means the HTTP + // proxy host is not found, so we fixup the error code. + // For SOCKS proxies (mProxyTransparent == true), the socket + // transport resolves the real host here, so there's no fixup + // (see bug 226943). + if (mProxyNotTransparent && (status == NS_ERROR_UNKNOWN_HOST)) { + status = NS_ERROR_UNKNOWN_PROXY_HOST; + } + + nsresult rv; + // remember if it was primary because TransportSetups will delete the ref to + // the DNS request and check cannot be performed later. + bool isPrimary = IsPrimary(request); + if (isPrimary) { + rv = mPrimaryTransport.OnLookupComplete(this, rec, status); + if ((!mIsHttp3 && mPrimaryTransport.ConnectingOrRetry()) || + (mIsHttp3 && mPrimaryTransport.Resolved())) { + SetupEvent(SetupEvents::RESOLVED_PRIMARY_EVENT); + } + } else { + rv = mBackupTransport.OnLookupComplete(this, rec, status); + } + + if (NS_FAILED(rv) || mIsHttp3) { + // If we are retrying DNS, we should not setup the connection. + if (mIsHttp3 && mPrimaryTransport.mState == + TransportSetup::TransportSetupState::RETRY_RESOLVING) { + LOG(("Retry DNS for Http3")); + return NS_OK; + } + + // Before calling SetupConn we need to hold reference to this, i.e. a + // delete protector, because the corresponding ConnectionEntry may be + // abandoned and that will abandon this DnsAndConnectSocket. + SetupConn(isPrimary, rv); + // During a connection dispatch that will happen in SetupConn, + // a ConnectionEntry may be abandon and that will abandon this + // DnsAndConnectSocket. In that case mState will already be + // DnsAndSocketState::DONE and we do not need to set it again. + if (mState != DnsAndSocketState::DONE) { + if (isPrimary) { + SetupEvent(SetupEvents::PRIMARY_DONE_EVENT); + } else { + SetupEvent(SetupEvents::BACKUP_DONE_EVENT); + } + } + } + return NS_OK; +} + +// method for nsIAsyncOutputStreamCallback +NS_IMETHODIMP +DnsAndConnectSocket::OnOutputStreamReady(nsIAsyncOutputStream* out) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + MOZ_DIAGNOSTIC_ASSERT(mPrimaryTransport.mSocketTransport || + mBackupTransport.mSocketTransport); + MOZ_DIAGNOSTIC_ASSERT(IsPrimary(out) || IsBackup(out), "stream mismatch"); + + RefPtr ent = + gHttpHandler->ConnMgr()->FindConnectionEntry(mConnInfo); + MOZ_DIAGNOSTIC_ASSERT(ent); + Unused << ent; + + RefPtr deleteProtector(this); + + LOG(("DnsAndConnectSocket::OnOutputStreamReady [this=%p ent=%s %s]\n", this, + mConnInfo->Origin(), IsPrimary(out) ? "primary" : "backup")); + + // Remember if it was primary or backup reuest. + bool isPrimary = IsPrimary(out); + nsresult rv = NS_OK; + if (isPrimary) { + rv = mPrimaryTransport.CheckConnectedResult(this); + if (!mPrimaryTransport.DoneConnecting()) { + return NS_OK; + } + MOZ_ASSERT((NS_SUCCEEDED(rv) && + (mPrimaryTransport.mState == + TransportSetup::TransportSetupState::CONNECTING_DONE) && + mPrimaryTransport.mSocketTransport) || + (NS_FAILED(rv) && + (mPrimaryTransport.mState == + TransportSetup::TransportSetupState::DONE) && + !mPrimaryTransport.mSocketTransport)); + } else if (IsBackup(out)) { + rv = mBackupTransport.CheckConnectedResult(this); + if (!mBackupTransport.DoneConnecting()) { + return NS_OK; + } + MOZ_ASSERT((NS_SUCCEEDED(rv) && + (mBackupTransport.mState == + TransportSetup::TransportSetupState::CONNECTING_DONE) && + mBackupTransport.mSocketTransport) || + (NS_FAILED(rv) && + (mBackupTransport.mState == + TransportSetup::TransportSetupState::DONE) && + !mBackupTransport.mSocketTransport)); + } else { + MOZ_ASSERT(false, "unexpected stream"); + return NS_ERROR_UNEXPECTED; + } + + // Before calling SetupConn we need to hold a reference to this, i.e. a + // delete protector, because the corresponding ConnectionEntry may be + // abandoned and that will abandon this DnsAndConnectSocket. + rv = SetupConn(isPrimary, rv); + if (mState != DnsAndSocketState::DONE) { + // During a connection dispatch that will happen in SetupConn, + // a ConnectionEntry may be abandon and that will abandon this + // DnsAndConnectSocket. In that case mState will already be + // DnsAndSocketState::DONE and we do not need to set it again. + if (isPrimary) { + SetupEvent(SetupEvents::PRIMARY_DONE_EVENT); + } else { + SetupEvent(SetupEvents::BACKUP_DONE_EVENT); + } + } + return rv; +} + +nsresult DnsAndConnectSocket::SetupConn(bool isPrimary, nsresult status) { + // assign the new socket to the http connection + + RefPtr ent = + gHttpHandler->ConnMgr()->FindConnectionEntry(mConnInfo); + MOZ_DIAGNOSTIC_ASSERT(ent); + if (!ent) { + Abandon(); + return NS_OK; + } + + RefPtr conn; + + nsresult rv = NS_OK; + if (isPrimary) { + rv = mPrimaryTransport.SetupConn(mTransaction, ent, status, mCaps, + getter_AddRefs(conn)); + } else { + rv = mBackupTransport.SetupConn(mTransaction, ent, status, mCaps, + getter_AddRefs(conn)); + } + + nsCOMPtr callbacks; + mTransaction->GetSecurityCallbacks(getter_AddRefs(callbacks)); + + if (NS_FAILED(rv)) { + LOG( + ("DnsAndConnectSocket::SetupConn " + "conn->init (%p) failed %" PRIx32 "\n", + conn.get(), static_cast(rv))); + + if (nsHttpTransaction* trans = mTransaction->QueryHttpTransaction()) { + if (mIsHttp3 && !mConnInfo->GetWebTransport()) { + trans->DisableHttp3(true); + gHttpHandler->ExcludeHttp3(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. + ent->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. + mHasConnected = true; + + // if this is still in the pending list, remove it and dispatch it + RefPtr pendingTransInfo = + gHttpHandler->ConnMgr()->FindTransactionHelper(true, ent, mTransaction); + if (pendingTransInfo) { + MOZ_ASSERT(!mSpeculative, "Speculative Half Open found mTransaction"); + + ent->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( + ent, 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 || + (ent->mConnInfo->FirstHopSSL() && !ent->UrgentStartQueueLength() && + !ent->PendingQueueLength() && !ent->mConnInfo->UsingConnect())) { + LOG( + ("DnsAndConnectSocket::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(mConnInfo, callbacks, mCaps); + } + + ent->InsertIntoActiveConns(conn); + rv = gHttpHandler->ConnMgr()->DispatchAbstractTransaction(ent, trans, + mCaps, conn, 0); + } else { + // otherwise just put this in the persistent connection pool + LOG( + ("DnsAndConnectSocket::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! + if (ent->mConnInfo->FirstHopSSL() && !ent->mConnInfo->UsingConnect()) { + RefPtr connTCP = do_QueryObject(conn); + // If RemoveIdleConnection succeeds that means that conn is in the + // idle queue. + if (connTCP && NS_SUCCEEDED(ent->RemoveIdleConnection(connTCP))) { + RefPtr trans; + if (mTransaction->IsNullTransaction() && !mDispatchedMTransaction) { + mDispatchedMTransaction = true; + trans = mTransaction; + } else { + trans = new NullHttpTransaction(ent->mConnInfo, callbacks, mCaps); + } + ent->InsertIntoActiveConns(conn); + rv = gHttpHandler->ConnMgr()->DispatchAbstractTransaction( + ent, trans, mCaps, conn, 0); + } + } + } + } + + // 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 +DnsAndConnectSocket::OnTransportStatus(nsITransport* trans, nsresult status, + int64_t progress, int64_t progressMax) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + MOZ_ASSERT(IsPrimary(trans) || IsBackup(trans)); + if (mTransaction) { + if (IsPrimary(trans) || + (IsBackup(trans) && (status == NS_NET_STATUS_CONNECTED_TO) && + mPrimaryTransport.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_CONNECTING_TO and NS_NET_STATUS_CONNECTED_TO. + // mBackupTransport is only started after + // NS_NET_STATUS_CONNECTING_TO of mSocketTransport, so ignore + // NS_NET_STATUS_CONNECTING_TO event for mBackupTransport and + // send NS_NET_STATUS_CONNECTED_TO. + // mBackupTransport must be connected before mSocketTransport(e.g. + // mPrimaryTransport.mSocketTransport != nullpttr). + mTransaction->OnTransportStatus(trans, status, progress); + } + } + + if (status == NS_NET_STATUS_CONNECTED_TO) { + if (IsPrimary(trans)) { + mPrimaryTransport.mConnectedOK = true; + } else { + mBackupTransport.mConnectedOK = true; + } + } + + // The rest of this method only applies to the primary transport + if (!IsPrimary(trans)) { + return NS_OK; + } + + // 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. + + nsCOMPtr dnsRecord( + do_GetInterface(mPrimaryTransport.mSocketTransport)); + if (status == NS_NET_STATUS_CONNECTING_TO && + StaticPrefs::network_http_http2_enabled() && + StaticPrefs::network_http_http2_coalesce_hostnames()) { + RefPtr ent = + gHttpHandler->ConnMgr()->FindConnectionEntry(mConnInfo); + MOZ_DIAGNOSTIC_ASSERT(ent); + if (ent) { + if (ent->MaybeProcessCoalescingKeys(dnsRecord)) { + gHttpHandler->ConnMgr()->ProcessSpdyPendingQ(ent); + } + } + } + + return NS_OK; +} + +bool DnsAndConnectSocket::IsPrimary(nsITransport* trans) { + return trans == mPrimaryTransport.mSocketTransport; +} + +bool DnsAndConnectSocket::IsPrimary(nsIAsyncOutputStream* out) { + return out == mPrimaryTransport.mStreamOut; +} + +bool DnsAndConnectSocket::IsPrimary(nsICancelable* dnsRequest) { + return dnsRequest == mPrimaryTransport.mDNSRequest; +} + +bool DnsAndConnectSocket::IsBackup(nsITransport* trans) { + return trans == mBackupTransport.mSocketTransport; +} + +bool DnsAndConnectSocket::IsBackup(nsIAsyncOutputStream* out) { + return out == mBackupTransport.mStreamOut; +} + +bool DnsAndConnectSocket::IsBackup(nsICancelable* dnsRequest) { + return dnsRequest == mBackupTransport.mDNSRequest; +} + +// method for nsIInterfaceRequestor +NS_IMETHODIMP +DnsAndConnectSocket::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 DnsAndConnectSocket::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 DnsAndConnectSocket::Claim() { + if (mSpeculative) { + mSpeculative = false; + mAllow1918 = true; + auto resetFlag = [](TransportSetup& transport) { + uint32_t flags; + if (transport.mSocketTransport && + NS_SUCCEEDED( + transport.mSocketTransport->GetConnectionFlags(&flags))) { + flags &= ~nsISocketTransport::DISABLE_RFC1918; + flags &= ~nsISocketTransport::IS_SPECULATIVE_CONNECTION; + transport.mSocketTransport->SetConnectionFlags(flags); + } + }; + resetFlag(mPrimaryTransport); + resetFlag(mBackupTransport); + + 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 (mPrimaryTransport.ConnectingOrRetry() && + !mBackupTransport.mSocketTransport && !mSynTimer && !mIsHttp3) { + SetupBackupTimer(); + } + } + + if (mFreeToUse) { + mFreeToUse = false; + + if (mPrimaryTransport.mSocketTransport) { + nsCOMPtr tlsSocketControl; + if (NS_SUCCEEDED(mPrimaryTransport.mSocketTransport->GetTlsSocketControl( + getter_AddRefs(tlsSocketControl))) && + tlsSocketControl) { + Unused << tlsSocketControl->Claim(); + } + } + + return true; + } + + return false; +} + +void DnsAndConnectSocket::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; +} + +void DnsAndConnectSocket::CloseTransports(nsresult error) { + if (mPrimaryTransport.mSocketTransport) { + mPrimaryTransport.mSocketTransport->Close(error); + } + if (mBackupTransport.mSocketTransport) { + mBackupTransport.mSocketTransport->Close(error); + } +} + +DnsAndConnectSocket::TransportSetup::TransportSetup(bool isBackup) + : mState(TransportSetup::TransportSetupState::INIT), mIsBackup(isBackup) {} + +nsresult DnsAndConnectSocket::TransportSetup::Init( + DnsAndConnectSocket* dnsAndSock) { + nsresult rv; + mSynStarted = TimeStamp::Now(); + if (mSkipDnsResolution) { + mState = TransportSetup::TransportSetupState::CONNECTING; + rv = SetupStreams(dnsAndSock); + } else { + mState = TransportSetup::TransportSetupState::RESOLVING; + rv = ResolveHost(dnsAndSock); + } + if (NS_FAILED(rv)) { + CloseAll(); + mState = TransportSetup::TransportSetupState::DONE; + } + return rv; +} + +void DnsAndConnectSocket::TransportSetup::CancelDnsResolution() { + if (mDNSRequest) { + mDNSRequest->Cancel(NS_ERROR_ABORT); + mDNSRequest = nullptr; + } + if (mState == TransportSetup::TransportSetupState::RESOLVING) { + mState = TransportSetup::TransportSetupState::INIT; + } +} + +void DnsAndConnectSocket::TransportSetup::Abandon() { + CloseAll(); + mState = TransportSetup::TransportSetupState::DONE; +} + +void DnsAndConnectSocket::TransportSetup::SetConnecting() { + MOZ_ASSERT(!mWaitingForConnect); + mWaitingForConnect = true; + gHttpHandler->ConnMgr()->StartedConnect(); +} + +void DnsAndConnectSocket::TransportSetup::MaybeSetConnectingDone() { + if (mWaitingForConnect) { + mWaitingForConnect = false; + gHttpHandler->ConnMgr()->RecvdConnect(); + } +} + +void DnsAndConnectSocket::TransportSetup::CloseAll() { + MaybeSetConnectingDone(); + + // Tell socket (and backup socket) to forget the half open socket. + if (mSocketTransport) { + mSocketTransport->SetEventSink(nullptr, nullptr); + mSocketTransport->SetSecurityCallbacks(nullptr); + mSocketTransport = nullptr; + } + + // Tell output stream (and backup) to forget the half open socket. + if (mStreamOut) { + mStreamOut->AsyncWait(nullptr, 0, 0, nullptr); + mStreamOut = nullptr; + } + + // Lose references to input stream (and backup). + if (mStreamIn) { + mStreamIn->AsyncWait(nullptr, 0, 0, nullptr); + mStreamIn = nullptr; + } + + if (mDNSRequest) { + mDNSRequest->Cancel(NS_ERROR_ABORT); + mDNSRequest = nullptr; + } + + mConnectedOK = false; +} + +nsresult DnsAndConnectSocket::TransportSetup::CheckConnectedResult( + DnsAndConnectSocket* dnsAndSock) { + mState = TransportSetup::TransportSetupState::CONNECTING_DONE; + MaybeSetConnectingDone(); + + if (mSkipDnsResolution) { + return NS_OK; + } + bool retryDns = false; + mSocketTransport->GetRetryDnsIfPossible(&retryDns); + if (!retryDns) { + return NS_OK; + } + + bool retry = false; + if (mRetryWithDifferentIPFamily) { + mRetryWithDifferentIPFamily = false; + mDnsFlags ^= (nsIDNSService::RESOLVE_DISABLE_IPV6 | + nsIDNSService::RESOLVE_DISABLE_IPV4); + mResetFamilyPreference = true; + retry = true; + } else if (!(mDnsFlags & nsIDNSService::RESOLVE_DISABLE_TRR)) { + bool trrEnabled; + mDNSRecord->IsTRR(&trrEnabled); + if (trrEnabled) { + nsIRequest::TRRMode trrMode = nsIRequest::TRR_DEFAULT_MODE; + mDNSRecord->GetEffectiveTRRMode(&trrMode); + // If current trr mode is trr only, we should not retry. + if (trrMode != nsIRequest::TRR_ONLY_MODE) { + // Drop state to closed. This will trigger a new round of + // DNS resolving. Bypass the cache this time since the + // cached data came from TRR and failed already! + LOG((" failed to connect with TRR enabled, try w/o\n")); + mDnsFlags |= nsIDNSService::RESOLVE_DISABLE_TRR | + nsIDNSService::RESOLVE_BYPASS_CACHE | + nsIDNSService::RESOLVE_REFRESH_CACHE; + retry = true; + } + } + } + + if (retry) { + CloseAll(); + mState = TransportSetup::TransportSetupState::RETRY_RESOLVING; + nsresult rv = ResolveHost(dnsAndSock); + if (NS_FAILED(rv)) { + CloseAll(); + mState = TransportSetup::TransportSetupState::DONE; + } + return rv; + } + + return NS_OK; +} + +nsresult DnsAndConnectSocket::TransportSetup::SetupConn( + nsAHttpTransaction* transaction, ConnectionEntry* ent, nsresult status, + uint32_t cap, HttpConnectionBase** connection) { + RefPtr conn; + if (!ent->mConnInfo->IsHttp3()) { + conn = new nsHttpConnection(); + } else { + conn = new HttpConnectionUDP(); + } + + NotifyActivity(ent->mConnInfo, NS_HTTP_ACTIVITY_SUBTYPE_CONNECTION_CREATED); + + LOG( + ("DnsAndConnectSocket::SocketTransport::SetupConn " + "Created new nshttpconnection %p %s\n", + conn.get(), ent->mConnInfo->IsHttp3() ? "using http3" : "")); + + NullHttpTransaction* nullTrans = transaction->QueryNullTransaction(); + if (nullTrans) { + conn->BootstrapTimings(nullTrans->Timings()); + } + + // Some capabilities are needed before a transaction actually gets + // scheduled (e.g. how to negotiate false start) + conn->SetTransactionCaps(transaction->Caps()); + + nsCOMPtr callbacks; + transaction->GetSecurityCallbacks(getter_AddRefs(callbacks)); + nsresult rv = NS_OK; + if (!ent->mConnInfo->IsHttp3()) { + RefPtr connTCP = do_QueryObject(conn); + rv = + connTCP->Init(ent->mConnInfo, gHttpHandler->ConnMgr()->mMaxRequestDelay, + mSocketTransport, mStreamIn, mStreamOut, mConnectedOK, + status, callbacks, + PR_MillisecondsToInterval(static_cast( + (TimeStamp::Now() - mSynStarted).ToMilliseconds())), + cap & NS_HTTP_ALLOW_SPDY_WITHOUT_KEEPALIVE); + } else { + RefPtr connUDP = do_QueryObject(conn); + rv = connUDP->Init(ent->mConnInfo, mDNSRecord, status, callbacks, cap); + if (NS_SUCCEEDED(rv)) { + if (nsHttpHandler::IsHttp3Enabled() && + StaticPrefs::network_http_http2_coalesce_hostnames()) { + if (ent->MaybeProcessCoalescingKeys(mDNSRecord, true)) { + gHttpHandler->ConnMgr()->ProcessSpdyPendingQ(ent); + } + } + } + } + + bool resetPreference = false; + if (mResetFamilyPreference || + (mSocketTransport && + NS_SUCCEEDED( + mSocketTransport->GetResetIPFamilyPreference(&resetPreference)) && + resetPreference)) { + ent->ResetIPFamilyPreference(); + } + + NetAddr peeraddr; + if (mSocketTransport && + NS_SUCCEEDED(mSocketTransport->GetPeerAddr(&peeraddr))) { + ent->RecordIPFamilyPreference(peeraddr.raw.family); + } + + conn.forget(connection); + mSocketTransport = nullptr; + mStreamOut = nullptr; + mStreamIn = nullptr; + mState = TransportSetup::TransportSetupState::DONE; + return rv; +} + +nsresult DnsAndConnectSocket::TransportSetup::SetupStreams( + DnsAndConnectSocket* dnsAndSock) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + MOZ_DIAGNOSTIC_ASSERT(!mSocketTransport); + MOZ_DIAGNOSTIC_ASSERT(!mStreamOut); + MOZ_DIAGNOSTIC_ASSERT(!mDNSRequest); + + nsresult rv; + nsTArray socketTypes; + const nsHttpConnectionInfo* ci = dnsAndSock->mConnInfo; + if (dnsAndSock->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 = components::SocketTransport::Service(); + if (!sts) { + return NS_ERROR_NOT_AVAILABLE; + } + + LOG( + ("DnsAndConnectSocket::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(), mDNSRecord, + 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( + ("DnsAndConnectSocket 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(), mDNSRecord, + getter_AddRefs(socketTransport)); + } + NS_ENSURE_SUCCESS(rv, rv); + + uint32_t tmpFlags = 0; + if (dnsAndSock->mCaps & NS_HTTP_REFRESH_DNS) { + tmpFlags = nsISocketTransport::BYPASS_CACHE; + } + + tmpFlags |= nsISocketTransport::GetFlagsFromTRRMode( + NS_HTTP_TRR_MODE_FROM_FLAGS(dnsAndSock->mCaps)); + + if (dnsAndSock->mCaps & NS_HTTP_LOAD_ANONYMOUS) { + tmpFlags |= nsISocketTransport::ANONYMOUS_CONNECT; + } + + // When we are making a speculative connection we do not propagate all flags + // in mCaps, so we need to query nsHttpConnectionInfo directly as well. + if ((dnsAndSock->mCaps & NS_HTTP_LOAD_ANONYMOUS_CONNECT_ALLOW_CLIENT_CERT) || + ci->GetAnonymousAllowClientCert()) { + tmpFlags |= nsISocketTransport::ANONYMOUS_CONNECT_ALLOW_CLIENT_CERT; + } + + if (ci->GetPrivate()) { + tmpFlags |= nsISocketTransport::NO_PERMANENT_STORAGE; + } + + Unused << socketTransport->SetIsPrivate(ci->GetPrivate()); + + if (dnsAndSock->mCaps & NS_HTTP_DISALLOW_ECH) { + tmpFlags |= nsISocketTransport::DONT_TRY_ECH; + } + + if (dnsAndSock->mCaps & NS_HTTP_IS_RETRY) { + tmpFlags |= nsISocketTransport::IS_RETRY; + } + + if (((dnsAndSock->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); + NS_ENSURE_SUCCESS(rv, 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(mHost, nsIDNSService::RESOLVE_OFFLINE, + dnsAndSock->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 (mRetryWithDifferentIPFamily) { + // From the same reason, let the backup socket fail faster to try the other + // family. + uint16_t fallbackTimeout = + mIsBackup ? gHttpHandler->GetFallbackSynTimeout() : 0; + if (fallbackTimeout) { + socketTransport->SetTimeout(nsISocketTransport::TIMEOUT_CONNECT, + fallbackTimeout); + } + } + + if (!dnsAndSock->Allow1918()) { + tmpFlags |= nsISocketTransport::DISABLE_RFC1918; + } + + if (dnsAndSock->mSpeculative) { + tmpFlags |= nsISocketTransport::IS_SPECULATIVE_CONNECTION; + } + + socketTransport->SetConnectionFlags(tmpFlags); + socketTransport->SetTlsFlags(ci->GetTlsFlags()); + + const OriginAttributes& originAttributes = + dnsAndSock->mConnInfo->GetOriginAttributes(); + if (originAttributes != OriginAttributes()) { + socketTransport->SetOriginAttributes(originAttributes); + } + + socketTransport->SetQoSBits(gHttpHandler->GetQoSBits()); + + rv = socketTransport->SetEventSink(dnsAndSock, nullptr); + NS_ENSURE_SUCCESS(rv, rv); + + rv = socketTransport->SetSecurityCallbacks(dnsAndSock); + NS_ENSURE_SUCCESS(rv, rv); + + if (gHttpHandler->EchConfigEnabled() && !ci->GetEchConfig().IsEmpty()) { + MOZ_ASSERT(!ci->IsHttp3()); + LOG(("Setting ECH")); + rv = socketTransport->SetEchConfig(ci->GetEchConfig()); + NS_ENSURE_SUCCESS(rv, rv); + + NotifyActivity(dnsAndSock->mConnInfo, NS_HTTP_ACTIVITY_SUBTYPE_ECH_SET); + } + + RefPtr ent = + gHttpHandler->ConnMgr()->FindConnectionEntry(ci); + MOZ_DIAGNOSTIC_ASSERT(ent); + if (ent) { + Telemetry::Accumulate(Telemetry::HTTP_CONNECTION_ENTRY_CACHE_HIT_1, + ent->mUsedForConnection); + ent->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); + + mSocketTransport = socketTransport.forget(); + mStreamIn = do_QueryInterface(sin); + mStreamOut = do_QueryInterface(sout); + + rv = mStreamOut->AsyncWait(dnsAndSock, 0, 0, nullptr); + if (NS_SUCCEEDED(rv)) { + SetConnecting(); + } + + return rv; +} + +nsresult DnsAndConnectSocket::TransportSetup::ResolveHost( + DnsAndConnectSocket* dnsAndSock) { + MOZ_DIAGNOSTIC_ASSERT(!mSocketTransport); + MOZ_DIAGNOSTIC_ASSERT(!mStreamOut); + MOZ_DIAGNOSTIC_ASSERT(!mDNSRequest); + LOG(("DnsAndConnectSocket::TransportSetup::ResolveHost [this=%p %s%s]", this, + PromiseFlatCString(mHost).get(), + (mDnsFlags & nsIDNSService::RESOLVE_BYPASS_CACHE) ? " bypass cache" + : "")); + nsCOMPtr dns = GetOrInitDNSService(); + if (!dns) { + return NS_ERROR_FAILURE; + } + + if (!mIsBackup) { + dnsAndSock->mTransaction->OnTransportStatus( + nullptr, NS_NET_STATUS_RESOLVING_HOST, 0); + } + + nsresult rv = NS_OK; + do { + rv = dns->AsyncResolveNative( + mHost, nsIDNSService::RESOLVE_TYPE_DEFAULT, + mDnsFlags | nsIDNSService::RESOLVE_WANT_RECORD_ON_ERROR, nullptr, + dnsAndSock, gSocketTransportService, + dnsAndSock->mConnInfo->GetOriginAttributes(), + getter_AddRefs(mDNSRequest)); + } while (NS_FAILED(rv) && ShouldRetryDNS()); + + if (NS_FAILED(rv)) { + mDNSRequest = nullptr; + } + return rv; +} + +bool DnsAndConnectSocket::TransportSetup::ShouldRetryDNS() { + if (mDnsFlags & nsIDNSService::RESOLVE_IP_HINT) { + mDnsFlags &= ~nsIDNSService::RESOLVE_IP_HINT; + return true; + } + + if (mRetryWithDifferentIPFamily) { + mRetryWithDifferentIPFamily = false; + mDnsFlags ^= (nsIDNSService::RESOLVE_DISABLE_IPV6 | + nsIDNSService::RESOLVE_DISABLE_IPV4); + mResetFamilyPreference = true; + return true; + } + return false; +} + +nsresult DnsAndConnectSocket::TransportSetup::OnLookupComplete( + DnsAndConnectSocket* dnsAndSock, nsIDNSRecord* rec, nsresult status) { + mDNSRequest = nullptr; + if (NS_SUCCEEDED(status)) { + mDNSRecord = do_QueryInterface(rec); + MOZ_ASSERT(mDNSRecord); + + if (dnsAndSock->mConnInfo->IsHttp3()) { + mState = TransportSetup::TransportSetupState::RESOLVED; + return status; + } + nsresult rv = SetupStreams(dnsAndSock); + if (NS_SUCCEEDED(rv)) { + mState = TransportSetup::TransportSetupState::CONNECTING; + } else { + CloseAll(); + mState = TransportSetup::TransportSetupState::DONE; + } + return rv; + } + + // DNS lookup status failed + + if (ShouldRetryDNS()) { + mState = TransportSetup::TransportSetupState::RETRY_RESOLVING; + nsresult rv = ResolveHost(dnsAndSock); + if (NS_FAILED(rv)) { + CloseAll(); + mState = TransportSetup::TransportSetupState::DONE; + } + return rv; + } + + mState = TransportSetup::TransportSetupState::DONE; + return status; +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/DnsAndConnectSocket.h b/netwerk/protocol/http/DnsAndConnectSocket.h new file mode 100644 index 0000000000..8409de3d08 --- /dev/null +++ b/netwerk/protocol/http/DnsAndConnectSocket.h @@ -0,0 +1,274 @@ +/* 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 DnsAndConnectSocket_h__ +#define DnsAndConnectSocket_h__ + +#include "mozilla/TimeStamp.h" +#include "mozilla/UniquePtr.h" +#include "nsAHttpConnection.h" +#include "nsHttpConnection.h" +#include "nsHttpTransaction.h" +#include "nsIAsyncOutputStream.h" +#include "nsICancelable.h" +#include "nsIDNSListener.h" +#include "nsIDNSRecord.h" +#include "nsIDNSService.h" +#include "nsINamed.h" +#include "nsITransport.h" +#include "nsWeakReference.h" + +namespace mozilla { +namespace net { + +// 8d411b53-54bc-4a99-8b78-ff125eab1564 +#define NS_DNSANDCONNECTSOCKET_IID \ + { \ + 0x8d411b53, 0x54bc, 0x4a99, { \ + 0x8b, 0x78, 0xff, 0x12, 0x5e, 0xab, 0x15, 0x64 \ + } \ + } + +class PendingTransactionInfo; +class ConnectionEntry; + +class DnsAndConnectSocket final : public nsIOutputStreamCallback, + public nsITransportEventSink, + public nsIInterfaceRequestor, + public nsITimerCallback, + public nsINamed, + public nsSupportsWeakReference, + public nsIDNSListener { + ~DnsAndConnectSocket(); + + public: + NS_DECLARE_STATIC_IID_ACCESSOR(NS_DNSANDCONNECTSOCKET_IID) + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIOUTPUTSTREAMCALLBACK + NS_DECL_NSITRANSPORTEVENTSINK + NS_DECL_NSIINTERFACEREQUESTOR + NS_DECL_NSITIMERCALLBACK + NS_DECL_NSINAMED + NS_DECL_NSIDNSLISTENER + + DnsAndConnectSocket(nsHttpConnectionInfo* ci, nsAHttpTransaction* trans, + uint32_t caps, bool speculative, bool isFromPredictor, + bool urgentStart); + + [[nodiscard]] nsresult Init(ConnectionEntry* ent); + void Abandon(); + double Duration(TimeStamp epoch); + void CloseTransports(nsresult error); + + bool IsSpeculative() { return mSpeculative; } + + 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(); + + private: + // This performs checks that the DnsAndConnectSocket has been properly cleand + // up. + void CheckIsDone(); + + /** + * State: + * INIT: initial state. From this state: + * 1) change the state to RESOLVING and start the primary DNS lookup + * if mSkipDnsResolution is false, + * 2) or the lookup is skip and the state changes to CONNECTING and + * start the backup timer. + * 3) or changes to DONE in case of an error. + * RESOLVING: the primary DNS resolution is in progress. From this state + * we transition into CONNECTING or DONE. + * CONNECTING: We change to this state when the primary connection has + * started. At that point the backup timer is started. + * ONE_CONNECTED: We change into this state when one of the connections + * is connected and the second is in progres. + * DONE + * + * Events: + * INIT_EVENT: Start the primary dns resolution (if mSkipDnsResolution is + * false), otherwise start the primary connection. + * RESOLVED_PRIMARY_EVENT: the primary DNS resolution is done. This event + * may be resent due to DNS retries + * CONNECTED_EVENT: A connecion (primary or backup) is done + */ + enum DnsAndSocketState { + INIT, + RESOLVING, + CONNECTING, + ONE_CONNECTED, + DONE + } mState = INIT; + + enum SetupEvents { + INIT_EVENT, + RESOLVED_PRIMARY_EVENT, + PRIMARY_DONE_EVENT, + BACKUP_DONE_EVENT, + BACKUP_TIMER_FIRED_EVENT + }; + + // This structure is responsible for performing DNS lookup, creating socket + // and connecting the socket. + struct TransportSetup { + enum TransportSetupState { + INIT, + RESOLVING, + RESOLVED, + RETRY_RESOLVING, + CONNECTING, + CONNECTING_DONE, + DONE + } mState; + + bool FirstResolving() { + return mState == TransportSetup::TransportSetupState::RESOLVING; + } + bool ConnectingOrRetry() { + return (mState == TransportSetup::TransportSetupState::CONNECTING) || + (mState == TransportSetup::TransportSetupState::RETRY_RESOLVING) || + (mState == TransportSetup::TransportSetupState::CONNECTING_DONE); + } + bool Resolved() { + return mState == TransportSetup::TransportSetupState::RESOLVED; + } + bool DoneConnecting() { + return (mState == TransportSetup::TransportSetupState::CONNECTING_DONE) || + (mState == TransportSetup::TransportSetupState::DONE); + } + + nsCString mHost; + nsCOMPtr mDNSRequest; + nsCOMPtr mDNSRecord; + nsIDNSService::DNSFlags mDnsFlags = nsIDNSService::RESOLVE_DEFAULT_FLAGS; + bool mRetryWithDifferentIPFamily = false; + bool mResetFamilyPreference = false; + bool mSkipDnsResolution = false; + + nsCOMPtr mSocketTransport; + nsCOMPtr mStreamOut; + nsCOMPtr mStreamIn; + TimeStamp mSynStarted; + bool mConnectedOK = false; + bool mIsBackup; + + bool mWaitingForConnect = false; + void SetConnecting(); + void MaybeSetConnectingDone(); + + nsresult Init(DnsAndConnectSocket* dnsAndSock); + void CancelDnsResolution(); + void Abandon(); + void CloseAll(); + nsresult SetupConn(nsAHttpTransaction* transaction, ConnectionEntry* ent, + nsresult status, uint32_t cap, + HttpConnectionBase** connection); + [[nodiscard]] nsresult SetupStreams(DnsAndConnectSocket* dnsAndSock); + nsresult ResolveHost(DnsAndConnectSocket* dnsAndSock); + bool ShouldRetryDNS(); + nsresult OnLookupComplete(DnsAndConnectSocket* dnsAndSock, + nsIDNSRecord* rec, nsresult status); + nsresult CheckConnectedResult(DnsAndConnectSocket* dnsAndSock); + + protected: + explicit TransportSetup(bool isBackup); + }; + + struct PrimaryTransportSetup final : TransportSetup { + PrimaryTransportSetup() : TransportSetup(false) {} + }; + + struct BackupTransportSetup final : TransportSetup { + BackupTransportSetup() : TransportSetup(true) {} + }; + + nsresult SetupConn(bool isPrimary, nsresult status); + void SetupBackupTimer(); + void CancelBackupTimer(); + + bool IsPrimary(nsITransport* trans); + bool IsPrimary(nsIAsyncOutputStream* out); + bool IsPrimary(nsICancelable* dnsRequest); + bool IsBackup(nsITransport* trans); + bool IsBackup(nsIAsyncOutputStream* out); + bool IsBackup(nsICancelable* dnsRequest); + + // 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); + + void CheckProxyConfig(); + nsresult SetupDnsFlags(ConnectionEntry* ent); + nsresult SetupEvent(SetupEvents event); + + RefPtr mTransaction; + bool mDispatchedMTransaction = false; + + PrimaryTransportSetup mPrimaryTransport; + 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 = true; + + // mHasConnected tracks whether one of the sockets has completed the + // connection process. It may have completed unsuccessfully. + bool mHasConnected = false; + + bool mBackupConnStatsSet = false; + + // A DnsAndConnectSocket 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 = true; + + RefPtr mConnInfo; + nsCOMPtr mSynTimer; + BackupTransportSetup mBackupTransport; + + bool mIsHttp3; + + bool mSkipDnsResolution = false; + bool mProxyNotTransparent = false; + bool mProxyTransparentResolvesHost = false; +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(DnsAndConnectSocket, NS_DNSANDCONNECTSOCKET_IID) + +} // namespace net +} // namespace mozilla + +#endif // DnsAndConnectSocket_h__ diff --git a/netwerk/protocol/http/EarlyHintPreconnect.cpp b/netwerk/protocol/http/EarlyHintPreconnect.cpp new file mode 100644 index 0000000000..4282ae554e --- /dev/null +++ b/netwerk/protocol/http/EarlyHintPreconnect.cpp @@ -0,0 +1,106 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 "EarlyHintPreconnect.h" + +#include "mozilla/CORSMode.h" +#include "mozilla/dom/Element.h" +#include "mozilla/StaticPrefs_network.h" +#include "nsIOService.h" +#include "nsIURI.h" +#include "SpeculativeTransaction.h" + +namespace mozilla::net { + +namespace { +class EarlyHintsPreConnectOverride : public nsIInterfaceRequestor, + public nsISpeculativeConnectionOverrider { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSISPECULATIVECONNECTIONOVERRIDER + NS_DECL_NSIINTERFACEREQUESTOR + + explicit EarlyHintsPreConnectOverride(uint32_t aConnectionLimit) + : mConnectionLimit(aConnectionLimit) {} + + private: + virtual ~EarlyHintsPreConnectOverride() = default; + + // Only set once on main thread and can be read on multiple threads. + const uint32_t mConnectionLimit; +}; + +NS_IMPL_ISUPPORTS(EarlyHintsPreConnectOverride, nsIInterfaceRequestor, + nsISpeculativeConnectionOverrider) + +NS_IMETHODIMP +EarlyHintsPreConnectOverride::GetInterface(const nsIID& iid, void** result) { + if (NS_SUCCEEDED(QueryInterface(iid, result)) && *result) { + return NS_OK; + } + + return NS_ERROR_NO_INTERFACE; +} + +NS_IMETHODIMP +EarlyHintsPreConnectOverride::GetIgnoreIdle(bool* ignoreIdle) { + *ignoreIdle = true; + return NS_OK; +} + +NS_IMETHODIMP +EarlyHintsPreConnectOverride::GetParallelSpeculativeConnectLimit( + uint32_t* parallelSpeculativeConnectLimit) { + *parallelSpeculativeConnectLimit = mConnectionLimit; + return NS_OK; +} + +NS_IMETHODIMP +EarlyHintsPreConnectOverride::GetIsFromPredictor(bool* isFromPredictor) { + *isFromPredictor = false; + return NS_OK; +} + +NS_IMETHODIMP +EarlyHintsPreConnectOverride::GetAllow1918(bool* allow) { + *allow = false; + return NS_OK; +} + +} // namespace + +void EarlyHintPreconnect::MaybePreconnect( + const LinkHeader& aHeader, nsIURI* aBaseURI, + OriginAttributes&& aOriginAttributes) { + if (!StaticPrefs::network_early_hints_preconnect_enabled()) { + return; + } + + if (!gIOService) { + return; + } + + nsCOMPtr uri; + if (NS_FAILED(aHeader.NewResolveHref(getter_AddRefs(uri), aBaseURI))) { + return; + } + + // only preconnect secure context urls + if (!uri->SchemeIs("https")) { + return; + } + + uint32_t maxPreconnectCount = + StaticPrefs::network_early_hints_preconnect_max_connections(); + nsCOMPtr callbacks = + new EarlyHintsPreConnectOverride(maxPreconnectCount); + // Note that the http connection manager will limit the number of + // connections we can make, so it should be fine we don't check duplicate + // preconnect attempts here. + CORSMode corsMode = dom::Element::StringToCORSMode(aHeader.mCrossOrigin); + gIOService->SpeculativeConnectWithOriginAttributesNative( + uri, std::move(aOriginAttributes), callbacks, corsMode == CORS_ANONYMOUS); +} + +} // namespace mozilla::net diff --git a/netwerk/protocol/http/EarlyHintPreconnect.h b/netwerk/protocol/http/EarlyHintPreconnect.h new file mode 100644 index 0000000000..119d425c06 --- /dev/null +++ b/netwerk/protocol/http/EarlyHintPreconnect.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 mozilla_net_EarlyHintPreconnect_h +#define mozilla_net_EarlyHintPreconnect_h + +#include "nsNetUtil.h" + +namespace mozilla::net { + +class EarlyHintPreconnect final { + public: + static void MaybePreconnect(const LinkHeader& aHeader, nsIURI* aBaseURI, + OriginAttributes&& aOriginAttributes); + + EarlyHintPreconnect() = delete; + EarlyHintPreconnect(const EarlyHintPreconnect&) = delete; + EarlyHintPreconnect& operator=(const EarlyHintPreconnect&) = delete; +}; + +} // namespace mozilla::net + +#endif // mozilla_net_EarlyHintPreconnect_h diff --git a/netwerk/protocol/http/EarlyHintPreloader.cpp b/netwerk/protocol/http/EarlyHintPreloader.cpp new file mode 100644 index 0000000000..920fea7fcd --- /dev/null +++ b/netwerk/protocol/http/EarlyHintPreloader.cpp @@ -0,0 +1,846 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 "EarlyHintPreloader.h" + +#include "EarlyHintRegistrar.h" +#include "EarlyHintsService.h" +#include "ErrorList.h" +#include "HttpChannelParent.h" +#include "MainThreadUtils.h" +#include "NeckoCommon.h" +#include "gfxPlatform.h" +#include "mozilla/CORSMode.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/nsCSPContext.h" +#include "mozilla/dom/nsMixedContentBlocker.h" +#include "mozilla/dom/ReferrerInfo.h" +#include "mozilla/glean/GleanMetrics.h" +#include "mozilla/ipc/BackgroundUtils.h" +#include "mozilla/LoadInfo.h" +#include "mozilla/Logging.h" +#include "mozilla/net/EarlyHintRegistrar.h" +#include "mozilla/net/NeckoChannelParams.h" +#include "mozilla/StaticPrefs_network.h" +#include "mozilla/Telemetry.h" +#include "nsAttrValue.h" +#include "nsCOMPtr.h" +#include "nsContentPolicyUtils.h" +#include "nsContentSecurityManager.h" +#include "nsContentUtils.h" +#include "nsDebug.h" +#include "nsHttpChannel.h" +#include "nsIAsyncVerifyRedirectCallback.h" +#include "nsIChannel.h" +#include "nsIContentSecurityPolicy.h" +#include "nsIHttpChannel.h" +#include "nsIInputStream.h" +#include "nsILoadContext.h" +#include "nsILoadInfo.h" +#include "nsIParentChannel.h" +#include "nsIReferrerInfo.h" +#include "nsITimer.h" +#include "nsIURI.h" +#include "nsNetUtil.h" +#include "nsQueryObject.h" +#include "ParentChannelListener.h" +#include "nsIChannel.h" +#include "nsInterfaceRequestorAgg.h" + +// +// To enable logging (see mozilla/Logging.h for full details): +// +// set MOZ_LOG=EarlyHint:5 +// set MOZ_LOG_FILE=earlyhint.log +// +// this enables LogLevel::Debug level information and places all output in +// the file earlyhint.log +// +static mozilla::LazyLogModule gEarlyHintLog("EarlyHint"); + +#undef LOG +#define LOG(args) MOZ_LOG(gEarlyHintLog, mozilla::LogLevel::Debug, args) + +#undef LOG_ENABLED +#define LOG_ENABLED() MOZ_LOG_TEST(gEarlyHintLog, mozilla::LogLevel::Debug) + +namespace mozilla::net { + +namespace { +// This id uniquely identifies each early hint preloader in the +// EarlyHintRegistrar. Must only be accessed from main thread. +static uint64_t gEarlyHintPreloaderId{0}; +} // namespace + +//============================================================================= +// OngoingEarlyHints +//============================================================================= + +void OngoingEarlyHints::CancelAll(const nsACString& aReason) { + for (auto& preloader : mPreloaders) { + preloader->CancelChannel(NS_ERROR_ABORT, aReason, /* aDeleteEntry */ true); + } + mPreloaders.Clear(); + mStartedPreloads.Clear(); +} + +bool OngoingEarlyHints::Contains(const PreloadHashKey& aKey) { + return mStartedPreloads.Contains(aKey); +} + +bool OngoingEarlyHints::Add(const PreloadHashKey& aKey, + RefPtr aPreloader) { + if (!mStartedPreloads.Contains(aKey)) { + mStartedPreloads.Insert(aKey); + mPreloaders.AppendElement(aPreloader); + return true; + } + return false; +} + +void OngoingEarlyHints::RegisterLinksAndGetConnectArgs( + dom::ContentParentId aCpId, nsTArray& aOutLinks) { + // register all channels before returning + for (auto& preload : mPreloaders) { + EarlyHintConnectArgs args; + if (preload->Register(aCpId, args)) { + aOutLinks.AppendElement(std::move(args)); + } + } +} + +//============================================================================= +// EarlyHintPreloader +//============================================================================= + +EarlyHintPreloader::EarlyHintPreloader() { + AssertIsOnMainThread(); + mConnectArgs.earlyHintPreloaderId() = ++gEarlyHintPreloaderId; +}; + +EarlyHintPreloader::~EarlyHintPreloader() { + if (mTimer) { + mTimer->Cancel(); + mTimer = nullptr; + } + Telemetry::Accumulate(Telemetry::EH_STATE_OF_PRELOAD_REQUEST, mState); +} + +/* static */ +Maybe EarlyHintPreloader::GenerateHashKey( + ASDestination aAs, nsIURI* aURI, nsIPrincipal* aPrincipal, + CORSMode aCorsMode, bool aIsModulepreload) { + if (aIsModulepreload) { + return Some(PreloadHashKey::CreateAsScript( + aURI, aCorsMode, JS::loader::ScriptKind::eModule)); + } + if (aAs == ASDestination::DESTINATION_FONT && aCorsMode != CORS_NONE) { + return Some(PreloadHashKey::CreateAsFont(aURI, aCorsMode)); + } + if (aAs == ASDestination::DESTINATION_IMAGE) { + return Some(PreloadHashKey::CreateAsImage(aURI, aPrincipal, aCorsMode)); + } + if (aAs == ASDestination::DESTINATION_SCRIPT) { + return Some(PreloadHashKey::CreateAsScript( + aURI, aCorsMode, JS::loader::ScriptKind::eClassic)); + } + if (aAs == ASDestination::DESTINATION_STYLE) { + return Some(PreloadHashKey::CreateAsStyle( + aURI, aPrincipal, aCorsMode, + css::SheetParsingMode::eAuthorSheetFeatures)); + } + if (aAs == ASDestination::DESTINATION_FETCH && aCorsMode != CORS_NONE) { + return Some(PreloadHashKey::CreateAsFetch(aURI, aCorsMode)); + } + return Nothing(); +} + +/* static */ +nsSecurityFlags EarlyHintPreloader::ComputeSecurityFlags(CORSMode aCORSMode, + ASDestination aAs) { + if (aAs == ASDestination::DESTINATION_FONT) { + return nsContentSecurityManager::ComputeSecurityFlags( + CORSMode::CORS_NONE, + nsContentSecurityManager::CORSSecurityMapping::REQUIRE_CORS_CHECKS); + } + if (aAs == ASDestination::DESTINATION_IMAGE) { + return nsContentSecurityManager::ComputeSecurityFlags( + aCORSMode, nsContentSecurityManager::CORSSecurityMapping:: + CORS_NONE_MAPS_TO_INHERITED_CONTEXT) | + nsILoadInfo::SEC_ALLOW_CHROME; + } + if (aAs == ASDestination::DESTINATION_SCRIPT) { + return nsContentSecurityManager::ComputeSecurityFlags( + aCORSMode, nsContentSecurityManager::CORSSecurityMapping:: + CORS_NONE_MAPS_TO_DISABLED_CORS_CHECKS) | + nsILoadInfo::SEC_ALLOW_CHROME; + } + if (aAs == ASDestination::DESTINATION_STYLE) { + return nsContentSecurityManager::ComputeSecurityFlags( + aCORSMode, nsContentSecurityManager::CORSSecurityMapping:: + CORS_NONE_MAPS_TO_INHERITED_CONTEXT) | + nsILoadInfo::SEC_ALLOW_CHROME; + ; + } + if (aAs == ASDestination::DESTINATION_FETCH) { + return nsContentSecurityManager::ComputeSecurityFlags( + aCORSMode, nsContentSecurityManager::CORSSecurityMapping:: + CORS_NONE_MAPS_TO_DISABLED_CORS_CHECKS); + } + MOZ_ASSERT(false, "Unexpected ASDestination"); + return nsContentSecurityManager::ComputeSecurityFlags( + CORSMode::CORS_NONE, + nsContentSecurityManager::CORSSecurityMapping::REQUIRE_CORS_CHECKS); +} + +// static +void EarlyHintPreloader::MaybeCreateAndInsertPreload( + OngoingEarlyHints* aOngoingEarlyHints, const LinkHeader& aLinkHeader, + nsIURI* aBaseURI, nsIPrincipal* aPrincipal, + nsICookieJarSettings* aCookieJarSettings, + const nsACString& aResponseReferrerPolicy, const nsACString& aCSPHeader, + uint64_t aBrowsingContextID, + dom::CanonicalBrowsingContext* aLoadingBrowsingContext, + bool aIsModulepreload) { + nsAttrValue as; + ParseAsValue(aLinkHeader.mAs, as); + + ASDestination destination = static_cast(as.GetEnumValue()); + CollectResourcesTypeTelemetry(destination); + + if (!StaticPrefs::network_early_hints_enabled()) { + return; + } + + if (destination == ASDestination::DESTINATION_INVALID && !aIsModulepreload) { + // return early when it's definitly not an asset type we preload + // would be caught later as well, e.g. when creating the PreloadHashKey + return; + } + + if (destination == ASDestination::DESTINATION_FONT && + !gfxPlatform::GetPlatform()->DownloadableFontsEnabled()) { + return; + } + + nsCOMPtr uri; + NS_ENSURE_SUCCESS_VOID( + NS_NewURI(getter_AddRefs(uri), aLinkHeader.mHref, nullptr, aBaseURI)); + // The link relation may apply to a different resource, specified + // in the anchor parameter. For the link relations supported so far, + // we simply abort if the link applies to a resource different to the + // one we've loaded + if (!nsContentUtils::LinkContextIsURI(aLinkHeader.mAnchor, uri)) { + return; + } + + // only preload secure context urls + if (!nsMixedContentBlocker::IsPotentiallyTrustworthyOrigin(uri)) { + return; + } + + CORSMode corsMode = dom::Element::StringToCORSMode(aLinkHeader.mCrossOrigin); + + Maybe hashKey = + GenerateHashKey(destination, uri, aPrincipal, corsMode, aIsModulepreload); + if (!hashKey) { + return; + } + + if (aOngoingEarlyHints->Contains(*hashKey)) { + return; + } + + nsContentPolicyType contentPolicyType = + aIsModulepreload ? (IsScriptLikeOrInvalid(aLinkHeader.mAs) + ? nsContentPolicyType::TYPE_SCRIPT + : nsContentPolicyType::TYPE_INVALID) + : AsValueToContentPolicy(as); + + if (contentPolicyType == nsContentPolicyType::TYPE_INVALID) { + return; + } + + dom::ReferrerPolicy linkReferrerPolicy = + dom::ReferrerInfo::ReferrerPolicyAttributeFromString( + aLinkHeader.mReferrerPolicy); + + dom::ReferrerPolicy responseReferrerPolicy = + dom::ReferrerInfo::ReferrerPolicyAttributeFromString( + NS_ConvertUTF8toUTF16(aResponseReferrerPolicy)); + + // The early hint may have two referrer policies, one from the response header + // and one from the link element. + // + // For example, in this server response: + // HTTP/1.1 103 Early Hints + // Referrer-Policy : origin + // Link: ; rel=preload; as=style referrerpolicy=no-referrer + // + // The link header referrer policy, if present, will take precedence over + // the response referrer policy + dom::ReferrerPolicy finalReferrerPolicy = responseReferrerPolicy; + if (linkReferrerPolicy != dom::ReferrerPolicy::_empty) { + finalReferrerPolicy = linkReferrerPolicy; + } + nsCOMPtr referrerInfo = + new dom::ReferrerInfo(aBaseURI, finalReferrerPolicy); + + RefPtr earlyHintPreloader = new EarlyHintPreloader(); + + earlyHintPreloader->mLoadContext = aLoadingBrowsingContext; + + // Security flags for modulepreload's request mode are computed here directly + // until full support for worker destinations can be added. + // + // Implements "To fetch a single module script," + // Step 9. If destination is "worker", "sharedworker", or "serviceworker", + // and the top-level module fetch flag is set, then set request's + // mode to "same-origin". + nsSecurityFlags securityFlags = + aIsModulepreload + ? ((aLinkHeader.mAs.LowerCaseEqualsASCII("worker") || + aLinkHeader.mAs.LowerCaseEqualsASCII("sharedworker") || + aLinkHeader.mAs.LowerCaseEqualsASCII("serviceworker")) + ? nsILoadInfo::SEC_REQUIRE_SAME_ORIGIN_DATA_IS_BLOCKED + : nsILoadInfo::SEC_REQUIRE_CORS_INHERITS_SEC_CONTEXT) | + (corsMode == CORS_USE_CREDENTIALS + ? nsILoadInfo::SEC_COOKIES_INCLUDE + : nsILoadInfo::SEC_COOKIES_SAME_ORIGIN) | + nsILoadInfo::SEC_ALLOW_CHROME + : EarlyHintPreloader::ComputeSecurityFlags(corsMode, destination); + + // Verify that the resource should be loaded. + // This isn't the ideal way to test the resource against the CSP. + // The problem comes from the fact that at the stage of Early Hint + // processing we have not yet created a document where we would normally store + // the CSP. + + // First we will create a load info, + // nsILoadInfo::SEC_ONLY_FOR_EXPLICIT_CONTENTSEC_CHECK + nsCOMPtr secCheckLoadInfo = new LoadInfo( + aPrincipal, // loading principal + aPrincipal, // triggering principal + nullptr /* aLoadingContext node */, + nsILoadInfo::SEC_ONLY_FOR_EXPLICIT_CONTENTSEC_CHECK, contentPolicyType); + + if (aCSPHeader.Length() != 0) { + // If the CSP header is present then create a new CSP and apply the header + // directives to it + nsCOMPtr csp = new nsCSPContext(); + nsresult rv = csp->SetRequestContextWithPrincipal( + aPrincipal, aBaseURI, u""_ns, 0 /* aInnerWindowId */); + NS_ENSURE_SUCCESS_VOID(rv); + rv = CSP_AppendCSPFromHeader(csp, NS_ConvertUTF8toUTF16(aCSPHeader), + false /* report only */); + NS_ENSURE_SUCCESS_VOID(rv); + + // We create a temporary ClientInfo. This is required on the loadInfo as + // that is how the CSP is queried. More specificially, as a hack to be able + // to call NS_CheckContentLoadPolicy on nsILoadInfo which exclusively + // accesses the CSP from the ClientInfo, we create a synthetic ClientInfo to + // hold the CSP we are creating. This is not a safe thing to do in any other + // circumstance because ClientInfos are always describing a ClientSource + // that corresponds to a global or potential global, so creating an info + // without a source is unsound. For the purposes of doing things before a + // global exists, fetch has the concept of a + // https://fetch.spec.whatwg.org/#concept-request-reserved-client and + // nsILoadInfo explicity has methods around GiveReservedClientSource which + // are primarily used by ClientChannelHelper. If you are trying to do real + // CSP stuff and the ClientInfo is not there yet, please enhance the logic + // around ClientChannelHelper. + + mozilla::ipc::PrincipalInfo principalInfo; + rv = PrincipalToPrincipalInfo(aPrincipal, &principalInfo); + NS_ENSURE_SUCCESS_VOID(rv); + dom::ClientInfo clientInfo(nsID::GenerateUUID(), dom::ClientType::Window, + principalInfo, TimeStamp::Now()); + + // Our newly-created CSP is set on the ClientInfo via the indirect route of + // first serializing to CSPInfo + ipc::CSPInfo cspInfo; + rv = CSPToCSPInfo(csp, &cspInfo); + NS_ENSURE_SUCCESS_VOID(rv); + clientInfo.SetCspInfo(cspInfo); + + // This ClientInfo is then set on the new loadInfo. + // It can now be used to test the resource against the policy + secCheckLoadInfo->SetClientInfo(clientInfo); + } + + int16_t shouldLoad = nsIContentPolicy::ACCEPT; + nsresult rv = NS_CheckContentLoadPolicy(uri, secCheckLoadInfo, &shouldLoad, + nsContentUtils::GetContentPolicy()); + + if (NS_FAILED(rv) || NS_CP_REJECTED(shouldLoad)) { + return; + } + + NS_ENSURE_SUCCESS_VOID(earlyHintPreloader->OpenChannel( + uri, aPrincipal, securityFlags, contentPolicyType, referrerInfo, + aCookieJarSettings, aBrowsingContextID)); + + earlyHintPreloader->SetLinkHeader(aLinkHeader); + + DebugOnly result = + aOngoingEarlyHints->Add(*hashKey, earlyHintPreloader); + MOZ_ASSERT(result); +} + +nsresult EarlyHintPreloader::OpenChannel( + nsIURI* aURI, nsIPrincipal* aPrincipal, nsSecurityFlags aSecurityFlags, + nsContentPolicyType aContentPolicyType, nsIReferrerInfo* aReferrerInfo, + nsICookieJarSettings* aCookieJarSettings, uint64_t aBrowsingContextID) { + MOZ_ASSERT(aContentPolicyType == nsContentPolicyType::TYPE_IMAGE || + aContentPolicyType == + nsContentPolicyType::TYPE_INTERNAL_FETCH_PRELOAD || + aContentPolicyType == nsContentPolicyType::TYPE_SCRIPT || + aContentPolicyType == nsContentPolicyType::TYPE_STYLESHEET || + aContentPolicyType == nsContentPolicyType::TYPE_FONT); + + nsresult rv = + NS_NewChannel(getter_AddRefs(mChannel), aURI, aPrincipal, aSecurityFlags, + aContentPolicyType, aCookieJarSettings, + /* aPerformanceStorage */ nullptr, + /* aLoadGroup */ nullptr, + /* aCallbacks */ this, nsIRequest::LOAD_NORMAL); + + NS_ENSURE_SUCCESS(rv, rv); + + RefPtr httpChannelObject = do_QueryObject(mChannel); + if (!httpChannelObject) { + mChannel = nullptr; + return NS_ERROR_ABORT; + } + + // configure HTTP specific stuff + nsCOMPtr httpChannel = do_QueryInterface(mChannel); + if (!httpChannel) { + mChannel = nullptr; + return NS_ERROR_ABORT; + } + DebugOnly success = httpChannel->SetReferrerInfo(aReferrerInfo); + MOZ_ASSERT(NS_SUCCEEDED(success)); + success = httpChannel->SetRequestHeader("X-Moz"_ns, "early hint"_ns, false); + MOZ_ASSERT(NS_SUCCEEDED(success)); + + mParentListener = new ParentChannelListener(this, nullptr); + + PriorizeAsPreload(); + + rv = mChannel->AsyncOpen(mParentListener); + if (NS_FAILED(rv)) { + mParentListener = nullptr; + return rv; + } + + SetState(ePreloaderOpened); + + // Setting the BrowsingContextID here to let Early Hint requests show up in + // devtools. Normally that would automatically happen if we would pass the + // nsILoadGroup in ns_NewChannel above, but the nsILoadGroup is inaccessible + // here in the ParentProcess. The nsILoadGroup only exists in ContentProcess + // as part of the document and nsDocShell. It is also not yet determined which + // ContentProcess this load belongs to. + nsCOMPtr loadInfo = mChannel->LoadInfo(); + static_cast(loadInfo.get()) + ->UpdateBrowsingContextID(aBrowsingContextID); + + return NS_OK; +} + +void EarlyHintPreloader::PriorizeAsPreload() { + nsLoadFlags loadFlags = nsIRequest::LOAD_NORMAL; + Unused << mChannel->GetLoadFlags(&loadFlags); + Unused << mChannel->SetLoadFlags(loadFlags | nsIRequest::LOAD_BACKGROUND); + + if (nsCOMPtr cos = do_QueryInterface(mChannel)) { + Unused << cos->AddClassFlags(nsIClassOfService::Unblocked); + } +} + +void EarlyHintPreloader::SetLinkHeader(const LinkHeader& aLinkHeader) { + mConnectArgs.link() = aLinkHeader; +} + +bool EarlyHintPreloader::IsFromContentParent(dom::ContentParentId aCpId) const { + return aCpId == mCpId; +} + +bool EarlyHintPreloader::Register(dom::ContentParentId aCpId, + EarlyHintConnectArgs& aOut) { + mCpId = aCpId; + + // Set minimum delay of 1ms to always start the timer after the function call + // completed. + nsresult rv = NS_NewTimerWithCallback( + getter_AddRefs(mTimer), this, + std::max(StaticPrefs::network_early_hints_parent_connect_timeout(), + (uint32_t)1), + nsITimer::TYPE_ONE_SHOT); + if (NS_FAILED(rv)) { + MOZ_ASSERT(!mTimer); + CancelChannel(NS_ERROR_ABORT, "new-timer-failed"_ns, + /* aDeleteEntry */ false); + return false; + } + + // Create an entry in the redirect channel registrar + RefPtr registrar = EarlyHintRegistrar::GetOrCreate(); + registrar->RegisterEarlyHint(mConnectArgs.earlyHintPreloaderId(), this); + + aOut = mConnectArgs; + return true; +} + +nsresult EarlyHintPreloader::CancelChannel(nsresult aStatus, + const nsACString& aReason, + bool aDeleteEntry) { + LOG(("EarlyHintPreloader::CancelChannel [this=%p]\n", this)); + + if (mTimer) { + mTimer->Cancel(); + mTimer = nullptr; + } + if (aDeleteEntry) { + RefPtr registrar = EarlyHintRegistrar::GetOrCreate(); + registrar->DeleteEntry(mCpId, mConnectArgs.earlyHintPreloaderId()); + } + // clear redirect channel in case this channel is cleared between the call of + // EarlyHintPreloader::AsyncOnChannelRedirect and + // EarlyHintPreloader::OnRedirectResult + mRedirectChannel = nullptr; + if (mChannel) { + if (mSuspended) { + mChannel->Resume(); + } + mChannel->CancelWithReason(aStatus, aReason); + // Clearing mChannel is safe, because this EarlyHintPreloader is not in the + // EarlyHintRegistrar after this function call and we won't call + // SetHttpChannelFromEarlyHintPreloader nor OnStartRequest on mParent. + mChannel = nullptr; + SetState(ePreloaderCancelled); + } + return NS_OK; +} + +void EarlyHintPreloader::OnParentReady(nsIParentChannel* aParent) { + AssertIsOnMainThread(); + MOZ_ASSERT(aParent); + LOG(("EarlyHintPreloader::OnParentReady [this=%p]\n", this)); + + nsCOMPtr obs = services::GetObserverService(); + if (obs) { + obs->NotifyObservers(mChannel, "earlyhints-connectback", nullptr); + } + + mParent = aParent; + + if (mTimer) { + mTimer->Cancel(); + mTimer = nullptr; + } + + RefPtr registrar = EarlyHintRegistrar::GetOrCreate(); + registrar->DeleteEntry(mCpId, mConnectArgs.earlyHintPreloaderId()); + + if (mOnStartRequestCalled) { + SetParentChannel(); + InvokeStreamListenerFunctions(); + } +} + +void EarlyHintPreloader::SetParentChannel() { + RefPtr channel = do_QueryObject(mChannel); + RefPtr parent = do_QueryObject(mParent); + parent->SetHttpChannelFromEarlyHintPreloader(channel); +} + +// Adapted from +// https://searchfox.org/mozilla-central/rev/b4150d1c6fae0c51c522df2d2c939cf5ad331d4c/netwerk/ipc/DocumentLoadListener.cpp#1311 +void EarlyHintPreloader::InvokeStreamListenerFunctions() { + AssertIsOnMainThread(); + RefPtr self(this); + + LOG(( + "EarlyHintPreloader::InvokeStreamListenerFunctions [this=%p parent=%p]\n", + this, mParent.get())); + + // If we failed to suspend the channel, then we might have received + // some messages while the redirected was being handled. + // Manually send them on now. + if (!mIsFinished) { + // This is safe to do, because OnStartRequest/OnStopRequest/OnDataAvailable + // are all called on the main thread. They can't be called until we worked + // through all functions in the streamListnerFunctions array. + mParentListener->SetListenerAfterRedirect(mParent); + } + nsTArray streamListenerFunctions = + std::move(mStreamListenerFunctions); + + ForwardStreamListenerFunctions(std::move(streamListenerFunctions), mParent); + + // We don't expect to get new stream listener functions added + // via re-entrancy. If this ever happens, we should understand + // exactly why before allowing it. + NS_ASSERTION(mStreamListenerFunctions.IsEmpty(), + "Should not have added new stream listener function!"); + + if (mChannel && mSuspended) { + mChannel->Resume(); + } + mChannel = nullptr; + mParent = nullptr; + mParentListener = nullptr; + + SetState(ePreloaderUsed); +} + +//----------------------------------------------------------------------------- +// EarlyHintPreloader::nsISupports +//----------------------------------------------------------------------------- + +NS_IMPL_ISUPPORTS(EarlyHintPreloader, nsIRequestObserver, nsIStreamListener, + nsIChannelEventSink, nsIInterfaceRequestor, + nsIRedirectResultListener, nsIMultiPartChannelListener, + nsINamed, nsITimerCallback); + +//----------------------------------------------------------------------------- +// EarlyHintPreloader::nsIStreamListener +//----------------------------------------------------------------------------- + +// Implementation copied and adapted from DocumentLoadListener::OnStartRequest +// https://searchfox.org/mozilla-central/rev/380fc5571b039fd453b45bbb64ed13146fe9b066/netwerk/ipc/DocumentLoadListener.cpp#2317-2508 +NS_IMETHODIMP +EarlyHintPreloader::OnStartRequest(nsIRequest* aRequest) { + LOG(("EarlyHintPreloader::OnStartRequest [this=%p]\n", this)); + AssertIsOnMainThread(); + + mOnStartRequestCalled = true; + + nsCOMPtr multiPartChannel = do_QueryInterface(aRequest); + if (multiPartChannel) { + multiPartChannel->GetBaseChannel(getter_AddRefs(mChannel)); + } else { + mChannel = do_QueryInterface(aRequest); + } + MOZ_DIAGNOSTIC_ASSERT(mChannel); + + nsresult status = NS_OK; + Unused << aRequest->GetStatus(&status); + + if (mParent) { + SetParentChannel(); + mParent->OnStartRequest(aRequest); + InvokeStreamListenerFunctions(); + } else { + // Don't suspend the chanel when the channel got cancelled with + // CancelChannel, because then OnStopRequest wouldn't get called and we + // wouldn't clean up the channel. + if (NS_SUCCEEDED(status)) { + mChannel->Suspend(); + mSuspended = true; + } + mStreamListenerFunctions.AppendElement( + AsVariant(OnStartRequestParams{aRequest})); + } + + // return error after adding the OnStartRequest forward. The OnStartRequest + // failure has to be forwarded to listener, because they called AsyncOpen on + // this channel + return status; +} + +// Implementation copied from DocumentLoadListener::OnStopRequest +// https://searchfox.org/mozilla-central/rev/380fc5571b039fd453b45bbb64ed13146fe9b066/netwerk/ipc/DocumentLoadListener.cpp#2510-2528 +NS_IMETHODIMP +EarlyHintPreloader::OnStopRequest(nsIRequest* aRequest, nsresult aStatusCode) { + AssertIsOnMainThread(); + LOG(("EarlyHintPreloader::OnStopRequest [this=%p]\n", this)); + mStreamListenerFunctions.AppendElement( + AsVariant(OnStopRequestParams{aRequest, aStatusCode})); + + // If we're not a multi-part channel, then we're finished and we don't + // expect any further events. If we are, then this might be called again, + // so wait for OnAfterLastPart instead. + nsCOMPtr multiPartChannel = do_QueryInterface(aRequest); + if (!multiPartChannel) { + mIsFinished = true; + } + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// EarlyHintPreloader::nsIStreamListener +//----------------------------------------------------------------------------- + +// Implementation copied from DocumentLoadListener::OnDataAvailable +// https://searchfox.org/mozilla-central/rev/380fc5571b039fd453b45bbb64ed13146fe9b066/netwerk/ipc/DocumentLoadListener.cpp#2530-2549 +NS_IMETHODIMP +EarlyHintPreloader::OnDataAvailable(nsIRequest* aRequest, + nsIInputStream* aInputStream, + uint64_t aOffset, uint32_t aCount) { + AssertIsOnMainThread(); + LOG(("EarlyHintPreloader::OnDataAvailable [this=%p]\n", this)); + // This isn't supposed to happen, since we suspended the channel, but + // sometimes Suspend just doesn't work. This can happen when we're routing + // through nsUnknownDecoder to sniff the content type, and it doesn't handle + // being suspended. Let's just store the data and manually forward it to our + // redirected channel when it's ready. + nsCString data; + nsresult rv = NS_ReadInputStreamToString(aInputStream, data, aCount); + NS_ENSURE_SUCCESS(rv, rv); + + mStreamListenerFunctions.AppendElement(AsVariant( + OnDataAvailableParams{aRequest, std::move(data), aOffset, aCount})); + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// EarlyHintPreloader::nsIMultiPartChannelListener +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +EarlyHintPreloader::OnAfterLastPart(nsresult aStatus) { + LOG(("EarlyHintPreloader::OnAfterLastPart [this=%p]", this)); + mStreamListenerFunctions.AppendElement( + AsVariant(OnAfterLastPartParams{aStatus})); + mIsFinished = true; + return NS_OK; +} + +//----------------------------------------------------------------------------- +// EarlyHintPreloader::nsIChannelEventSink +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +EarlyHintPreloader::AsyncOnChannelRedirect( + nsIChannel* aOldChannel, nsIChannel* aNewChannel, uint32_t aFlags, + nsIAsyncVerifyRedirectCallback* callback) { + LOG(("EarlyHintPreloader::AsyncOnChannelRedirect [this=%p]", this)); + nsCOMPtr newURI; + nsresult rv = NS_GetFinalChannelURI(aNewChannel, getter_AddRefs(newURI)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = aNewChannel->GetURI(getter_AddRefs(newURI)); + if (NS_FAILED(rv)) { + callback->OnRedirectVerifyCallback(rv); + return NS_OK; + } + + // HTTP request headers are not automatically forwarded to the new channel. + nsCOMPtr httpChannel = do_QueryInterface(aNewChannel); + NS_ENSURE_STATE(httpChannel); + + rv = httpChannel->SetRequestHeader("X-Moz"_ns, "early hint"_ns, false); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + // Assign to mChannel after we get notification about success of the + // redirect in OnRedirectResult. + mRedirectChannel = aNewChannel; + + callback->OnRedirectVerifyCallback(NS_OK); + return NS_OK; +} + +//----------------------------------------------------------------------------- +// EarlyHintPreloader::nsIRedirectResultListener +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +EarlyHintPreloader::OnRedirectResult(nsresult aStatus) { + LOG(("EarlyHintPreloader::OnRedirectResult [this=%p] aProceeding=0x%" PRIx32, + this, static_cast(aStatus))); + if (NS_SUCCEEDED(aStatus) && mRedirectChannel) { + mChannel = mRedirectChannel; + } + + mRedirectChannel = nullptr; + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// EarlyHintPreloader::nsINamed +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +EarlyHintPreloader::GetName(nsACString& aName) { + aName.AssignLiteral("EarlyHintPreloader"); + return NS_OK; +} + +//----------------------------------------------------------------------------- +// EarlyHintPreloader::nsITimerCallback +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +EarlyHintPreloader::Notify(nsITimer* timer) { + // Death grip, because we will most likely remove the last reference when + // deleting us from the EarlyHintRegistrar + RefPtr deathGrip(this); + + RefPtr registrar = EarlyHintRegistrar::GetOrCreate(); + registrar->DeleteEntry(mCpId, mConnectArgs.earlyHintPreloaderId()); + + mTimer = nullptr; + mRedirectChannel = nullptr; + if (mChannel) { + if (mSuspended) { + mChannel->Resume(); + } + mChannel->CancelWithReason(NS_ERROR_ABORT, "parent-connect-timeout"_ns); + mChannel = nullptr; + } + SetState(ePreloaderTimeout); + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// EarlyHintPreloader::nsIInterfaceRequestor +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +EarlyHintPreloader::GetInterface(const nsIID& aIID, void** aResult) { + if (aIID.Equals(NS_GET_IID(nsIChannelEventSink))) { + NS_ADDREF_THIS(); + *aResult = static_cast(this); + return NS_OK; + } + + if (aIID.Equals(NS_GET_IID(nsIRedirectResultListener))) { + NS_ADDREF_THIS(); + *aResult = static_cast(this); + return NS_OK; + } + + if (aIID.Equals(NS_GET_IID(nsILoadContext)) && mLoadContext != nullptr) { + nsCOMPtr loadContext = mLoadContext; + loadContext.forget(aResult); + return NS_OK; + } + + return NS_ERROR_NO_INTERFACE; +} + +void EarlyHintPreloader::CollectResourcesTypeTelemetry( + ASDestination aASDestination) { + if (aASDestination == ASDestination::DESTINATION_FONT) { + glean::netwerk::early_hints.Get("font"_ns).Add(1); + } else if (aASDestination == ASDestination::DESTINATION_SCRIPT) { + glean::netwerk::early_hints.Get("script"_ns).Add(1); + } else if (aASDestination == ASDestination::DESTINATION_STYLE) { + glean::netwerk::early_hints.Get("stylesheet"_ns).Add(1); + } else if (aASDestination == ASDestination::DESTINATION_IMAGE) { + glean::netwerk::early_hints.Get("image"_ns).Add(1); + } else if (aASDestination == ASDestination::DESTINATION_FETCH) { + glean::netwerk::early_hints.Get("fetch"_ns).Add(1); + } else { + glean::netwerk::early_hints.Get("other"_ns).Add(1); + } +} +} // namespace mozilla::net diff --git a/netwerk/protocol/http/EarlyHintPreloader.h b/netwerk/protocol/http/EarlyHintPreloader.h new file mode 100644 index 0000000000..782e189731 --- /dev/null +++ b/netwerk/protocol/http/EarlyHintPreloader.h @@ -0,0 +1,199 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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_EarlyHintPreloader_h +#define mozilla_net_EarlyHintPreloader_h + +#include "mozilla/dom/ipc/IdType.h" +#include "mozilla/Maybe.h" +#include "mozilla/PreloadHashKey.h" +#include "NeckoCommon.h" +#include "mozilla/net/NeckoChannelParams.h" +#include "nsHashtablesFwd.h" +#include "nsIChannelEventSink.h" +#include "nsIInterfaceRequestor.h" +#include "nsIMultiPartChannel.h" +#include "nsIRedirectResultListener.h" +#include "nsIStreamListener.h" +#include "nsITimer.h" +#include "nsNetUtil.h" + +class nsAttrValue; +class nsICookieJarSettings; +class nsILoadContext; +class nsIPrincipal; +class nsIReferrerInfo; + +namespace mozilla::dom { +class CanonicalBrowsingContext; +} + +namespace mozilla::net { + +class EarlyHintPreloader; +class EarlyHintConnectArgs; +class ParentChannelListener; +struct LinkHeader; + +// class keeping track of all ongoing early hints +class OngoingEarlyHints final { + public: + NS_INLINE_DECL_REFCOUNTING(OngoingEarlyHints) + + OngoingEarlyHints() = default; + + // returns whether a preload with that key already existed + bool Contains(const PreloadHashKey& aKey); + bool Add(const PreloadHashKey& aKey, RefPtr aPreloader); + + void CancelAll(const nsACString& aReason); + + // registers all channels and returns the ids + void RegisterLinksAndGetConnectArgs( + dom::ContentParentId aCpId, nsTArray& aOutLinks); + + private: + ~OngoingEarlyHints() = default; + + // We need to do two things requiring two separate variables to keep track of + // preloads: + // - deduplicate Link headers when starting preloads, therefore we store them + // hashset with PreloadHashKey to look up whether we started the preload + // already + // - pass link headers in order they were received when passing all started + // preloads to the content process, therefore we store them in a nsTArray + nsTHashSet mStartedPreloads; + nsTArray> mPreloaders; +}; + +class EarlyHintPreloader final : public nsIStreamListener, + public nsIChannelEventSink, + public nsIRedirectResultListener, + public nsIInterfaceRequestor, + public nsIMultiPartChannelListener, + public nsINamed, + public nsITimerCallback { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMLISTENER + NS_DECL_NSICHANNELEVENTSINK + NS_DECL_NSIREDIRECTRESULTLISTENER + NS_DECL_NSIINTERFACEREQUESTOR + NS_DECL_NSIMULTIPARTCHANNELLISTENER + // required by NS_DECL_NSITIMERCALLBACK + NS_DECL_NSINAMED + NS_DECL_NSITIMERCALLBACK + + public: + // Create and insert a preload into OngoingEarlyHints if the same preload + // wasn't already issued and the LinkHeader can be parsed correctly. + static void MaybeCreateAndInsertPreload( + OngoingEarlyHints* aOngoingEarlyHints, const LinkHeader& aHeader, + nsIURI* aBaseURI, nsIPrincipal* aPrincipal, + nsICookieJarSettings* aCookieJarSettings, + const nsACString& aReferrerPolicy, const nsACString& aCSPHeader, + uint64_t aBrowsingContextID, + dom::CanonicalBrowsingContext* aLoadingBrowsingContext, + bool aIsModulepreload); + + // register Channel to EarlyHintRegistrar. Returns true and sets connect args + // if successful + bool Register(dom::ContentParentId aCpId, EarlyHintConnectArgs& aOut); + + // Allows EarlyHintRegistrar to check if the correct content process accesses + // this preload. Preventing compromised content processes to access Early Hint + // preloads from other origins + bool IsFromContentParent(dom::ContentParentId aCpId) const; + + // Should be called by the preloader service when the preload is not + // needed after all, because the final response returns a non-2xx status + // code. If aDeleteEntry is false, the calling function MUST make sure that + // the EarlyHintPreloader is not in the EarlyHintRegistrar anymore. Because + // after this function, the EarlyHintPreloader can't connect back to the + // parent anymore. + nsresult CancelChannel(nsresult aStatus, const nsACString& aReason, + bool aDeleteEntry); + + void OnParentReady(nsIParentChannel* aParent); + + private: + void SetParentChannel(); + void InvokeStreamListenerFunctions(); + + EarlyHintPreloader(); + ~EarlyHintPreloader(); + + static Maybe GenerateHashKey(ASDestination aAs, nsIURI* aURI, + nsIPrincipal* aPrincipal, + CORSMode corsMode, + bool aIsModulepreload); + + static nsSecurityFlags ComputeSecurityFlags(CORSMode aCORSMode, + ASDestination aAs); + + // call to start the preload + nsresult OpenChannel(nsIURI* aURI, nsIPrincipal* aPrincipal, + nsSecurityFlags aSecurityFlags, + nsContentPolicyType aContentPolicyType, + nsIReferrerInfo* aReferrerInfo, + nsICookieJarSettings* aCookieJarSettings, + uint64_t aBrowsingContextID); + void PriorizeAsPreload(); + void SetLinkHeader(const LinkHeader& aLinkHeader); + + static void CollectResourcesTypeTelemetry(ASDestination aASDestination); + nsCOMPtr mChannel; + nsCOMPtr mRedirectChannel; + + dom::ContentParentId mCpId; + EarlyHintConnectArgs mConnectArgs; + + // Copy behavior from DocumentLoadListener.h: + // https://searchfox.org/mozilla-central/rev/c0bed29d643393af6ebe77aa31455f283f169202/netwerk/ipc/DocumentLoadListener.h#487-512 + // The set of nsIStreamListener functions that got called on this + // listener, so that we can replay them onto the replacement channel's + // listener. This should generally only be OnStartRequest, since we + // Suspend() the channel at that point, but it can fail sometimes + // so we have to support holding a list. + nsTArray mStreamListenerFunctions; + + // Set to true once OnStartRequest is called + bool mOnStartRequestCalled = false; + // Set to true if we suspended mChannel in the OnStartRequest call + bool mSuspended = false; + nsCOMPtr mParent; + // Set to true after we've received the last OnStopRequest, and shouldn't + // setup a reference from the ParentChannelListener to the replacement + // channel. + bool mIsFinished = false; + + RefPtr mParentListener; + nsCOMPtr mTimer; + + // Hold the load context to provide data to web extension and anti tracking. + // See Bug 1836289 and Bug 1875268 + nsCOMPtr mLoadContext; + + private: + // IMPORTANT: when adding new values, always add them to the end, otherwise + // it will mess up telemetry. + enum EHPreloaderState : uint32_t { + ePreloaderCreated = 0, + ePreloaderOpened, + ePreloaderUsed, + ePreloaderCancelled, + ePreloaderTimeout, + }; + EHPreloaderState mState = ePreloaderCreated; + void SetState(EHPreloaderState aState) { mState = aState; } +}; + +inline nsISupports* ToSupports(EarlyHintPreloader* aObj) { + return static_cast(aObj); +} + +} // namespace mozilla::net + +#endif // mozilla_net_EarlyHintPreloader_h diff --git a/netwerk/protocol/http/EarlyHintRegistrar.cpp b/netwerk/protocol/http/EarlyHintRegistrar.cpp new file mode 100644 index 0000000000..d77c430819 --- /dev/null +++ b/netwerk/protocol/http/EarlyHintRegistrar.cpp @@ -0,0 +1,130 @@ +/* -*- 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 "EarlyHintRegistrar.h" + +#include "EarlyHintPreloader.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/StaticPtr.h" +#include "nsIObserver.h" +#include "nsIObserverService.h" +#include "nsXULAppAPI.h" + +namespace { +mozilla::StaticRefPtr gSingleton; +} // namespace + +namespace mozilla::net { + +namespace { + +class EHShutdownObserver final : public nsIObserver { + public: + EHShutdownObserver() = default; + + NS_DECL_ISUPPORTS + NS_DECL_NSIOBSERVER + + private: + ~EHShutdownObserver() = default; +}; + +NS_IMPL_ISUPPORTS(EHShutdownObserver, nsIObserver) + +NS_IMETHODIMP +EHShutdownObserver::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) { + EarlyHintRegistrar::CleanUp(); + return NS_OK; +} + +} // namespace + +EarlyHintRegistrar::EarlyHintRegistrar() { + // EarlyHintRegistrar 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()); +} + +EarlyHintRegistrar::~EarlyHintRegistrar() { MOZ_ASSERT(NS_IsMainThread()); } + +// static +void EarlyHintRegistrar::CleanUp() { + MOZ_ASSERT(NS_IsMainThread()); + + if (!gSingleton) { + return; + } + + for (auto& preloader : gSingleton->mEarlyHint) { + if (auto p = preloader.GetData()) { + // Don't delete entry from EarlyHintPreloader, because that would + // invalidate the iterator. + + p->CancelChannel(NS_ERROR_ABORT, "EarlyHintRegistrar::CleanUp"_ns, + /* aDeleteEntry */ false); + } + } + gSingleton->mEarlyHint.Clear(); +} + +// static +already_AddRefed EarlyHintRegistrar::GetOrCreate() { + MOZ_ASSERT(NS_IsMainThread()); + + if (!gSingleton) { + gSingleton = new EarlyHintRegistrar(); + nsCOMPtr obs = services::GetObserverService(); + if (NS_WARN_IF(!obs)) { + return nullptr; + } + nsCOMPtr observer = new EHShutdownObserver(); + nsresult rv = + obs->AddObserver(observer, "profile-change-net-teardown", false); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + mozilla::ClearOnShutdown(&gSingleton); + } + return do_AddRef(gSingleton); +} + +void EarlyHintRegistrar::DeleteEntry(dom::ContentParentId aCpId, + uint64_t aEarlyHintPreloaderId) { + MOZ_ASSERT(NS_IsMainThread()); + + RefPtr ehp = mEarlyHint.Get(aEarlyHintPreloaderId); + if (ehp && ehp->IsFromContentParent(aCpId)) { + mEarlyHint.Remove(aEarlyHintPreloaderId); + } +} + +void EarlyHintRegistrar::RegisterEarlyHint(uint64_t aEarlyHintPreloaderId, + EarlyHintPreloader* aEhp) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aEhp); + + mEarlyHint.InsertOrUpdate(aEarlyHintPreloaderId, RefPtr{aEhp}); +} + +bool EarlyHintRegistrar::LinkParentChannel(dom::ContentParentId aCpId, + uint64_t aEarlyHintPreloaderId, + nsIParentChannel* aParent) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aParent); + + RefPtr ehp; + bool found = mEarlyHint.Get(aEarlyHintPreloaderId, getter_AddRefs(ehp)); + if (ehp && ehp->IsFromContentParent(aCpId)) { + ehp->OnParentReady(aParent); + } + MOZ_ASSERT(ehp || !found); + return found; +} + +} // namespace mozilla::net diff --git a/netwerk/protocol/http/EarlyHintRegistrar.h b/netwerk/protocol/http/EarlyHintRegistrar.h new file mode 100644 index 0000000000..2684301090 --- /dev/null +++ b/netwerk/protocol/http/EarlyHintRegistrar.h @@ -0,0 +1,82 @@ +/* -*- 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_EarlyHintRegistrar_h__ +#define mozilla_net_EarlyHintRegistrar_h__ + +#include "mozilla/RefCounted.h" +#include "mozilla/dom/ipc/IdType.h" +#include "nsRefPtrHashtable.h" +#include "mozilla/AlreadyAddRefed.h" + +class nsIParentChannel; + +namespace mozilla::net { + +class EarlyHintPreloader; + +/* + * Registrar for pairing EarlyHintPreloader and HttpChannelParent via + * earlyHintPreloaderId. EarlyHintPreloader has to be registered first. + * EarlyHintPreloader::OnParentReady will be invoked to notify the + * EarlyHintpreloader about the existence of the associated HttpChannelParent. + */ +class EarlyHintRegistrar final : public RefCounted { + using EarlyHintHashtable = + nsRefPtrHashtable; + + public: + MOZ_DECLARE_REFCOUNTED_TYPENAME(EarlyHintRegistrar) + + EarlyHintRegistrar(); + ~EarlyHintRegistrar(); + + /* + * Store EarlyHintPreloader for the HttpChannelParent to connect back to. + * + * @param aEarlyHintPreloaderId identifying the EarlyHintPreloader + * @param aEhp the EarlyHintPreloader to store + */ + void RegisterEarlyHint(uint64_t aEarlyHintPreloaderId, + EarlyHintPreloader* aEhp); + + /* + * Link the provided nsIParentChannel with the associated EarlyHintPreloader. + * + * @param aEarlyHintPreloaderId identifying the EarlyHintPreloader + * @param aParent the nsIParentChannel to be paired + * @param aChannelId the id of the nsIChannel connecting to the + * EarlyHintPreloader. + */ + bool LinkParentChannel(dom::ContentParentId aCpId, + uint64_t aEarlyHintPreloaderId, + nsIParentChannel* aParent); + + /* + * Delete previous stored EarlyHintPreloader + * + * @param aEarlyHintPreloaderId identifying the EarlyHintPreloader + */ + void DeleteEntry(dom::ContentParentId aCpId, uint64_t aEarlyHintPreloaderId); + + /* + * This is called when "profile-change-net-teardown" is observed. We use this + * to cancel the ongoing preload and clear the linked EarlyHintPreloader + * objects. + */ + static void CleanUp(); + + // Singleton accessor + static already_AddRefed GetOrCreate(); + + private: + // Store unlinked EarlyHintPreloader objects. + EarlyHintHashtable mEarlyHint; +}; + +} // namespace mozilla::net + +#endif // mozilla_net_EarlyHintRegistrar_h__ diff --git a/netwerk/protocol/http/EarlyHintsService.cpp b/netwerk/protocol/http/EarlyHintsService.cpp new file mode 100644 index 0000000000..d8057259b8 --- /dev/null +++ b/netwerk/protocol/http/EarlyHintsService.cpp @@ -0,0 +1,191 @@ +/* -*- 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 "EarlyHintsService.h" +#include "EarlyHintPreconnect.h" +#include "EarlyHintPreloader.h" +#include "mozilla/dom/LinkStyle.h" +#include "mozilla/PreloadHashKey.h" +#include "mozilla/Telemetry.h" +#include "mozilla/glean/GleanMetrics.h" +#include "mozilla/StoragePrincipalHelper.h" +#include "nsContentUtils.h" +#include "nsIChannel.h" +#include "nsICookieJarSettings.h" +#include "nsILoadInfo.h" +#include "nsIPrincipal.h" +#include "nsNetUtil.h" +#include "nsString.h" + +namespace mozilla::net { + +EarlyHintsService::EarlyHintsService() + : mOngoingEarlyHints(new OngoingEarlyHints()) {} + +// implementing the destructor in the .cpp file to allow EarlyHintsService.h +// not to include EarlyHintPreloader.h, decoupling the two files and hopefully +// allow faster compile times +EarlyHintsService::~EarlyHintsService() = default; + +void EarlyHintsService::EarlyHint( + const nsACString& aLinkHeader, nsIURI* aBaseURI, nsIChannel* aChannel, + const nsACString& aReferrerPolicy, const nsACString& aCSPHeader, + dom::CanonicalBrowsingContext* aLoadingBrowsingContext) { + mEarlyHintsCount++; + if (mFirstEarlyHint.isNothing()) { + mFirstEarlyHint.emplace(TimeStamp::NowLoRes()); + } else { + // Only allow one early hint response with link headers. See + // https://html.spec.whatwg.org/multipage/semantics.html#early-hints + // > Note: Only the first early hint response served during the navigation + // > is handled, and it is discarded if it is succeeded by a cross-origin + // > redirect. + return; + } + + nsCOMPtr loadInfo = aChannel->LoadInfo(); + // We only follow Early Hints sent on the main document. Make sure that we got + // the main document channel here. + if (loadInfo->GetExternalContentPolicyType() != + ExtContentPolicy::TYPE_DOCUMENT) { + MOZ_ASSERT(false, "Early Hint on non-document channel"); + return; + } + nsCOMPtr principal; + // We want to set the top-level document as the triggeringPrincipal for the + // load of the sub-resources (image, font, fetch, script, style, fetch and in + // the future maybe more). We can't use the `triggeringPrincipal` of the main + // document channel, because it is the `systemPrincipal` for user initiated + // loads. Same for the `LoadInfo::FindPrincipalToInherit(aChannel)`. + // + // On 3xx redirects of the main document to cross site locations, all Early + // Hint preloads get cancelled as specified in the whatwg spec: + // + // Note: Only the first early hint response served during the navigation is + // handled, and it is discarded if it is succeeded by a cross-origin + // redirect. [1] + // + // Therefore the channel doesn't need to change the principal for any reason + // and has the correct principal for the whole lifetime. + // + // [1]: https://html.spec.whatwg.org/multipage/semantics.html#early-hints + nsresult rv = nsContentUtils::GetSecurityManager()->GetChannelResultPrincipal( + aChannel, getter_AddRefs(principal)); + NS_ENSURE_SUCCESS_VOID(rv); + + nsCOMPtr cookieJarSettings; + if (NS_FAILED( + loadInfo->GetCookieJarSettings(getter_AddRefs(cookieJarSettings)))) { + return; + } + + // TODO: find out why LinkHeaderParser uses utf16 and check if it can be + // changed to utf8 + auto linkHeaders = ParseLinkHeader(NS_ConvertUTF8toUTF16(aLinkHeader)); + + for (auto& linkHeader : linkHeaders) { + CollectLinkTypeTelemetry(linkHeader.mRel); + if (linkHeader.mRel.LowerCaseEqualsLiteral("preconnect")) { + mLinkType |= dom::LinkStyle::ePRECONNECT; + OriginAttributes originAttributes; + StoragePrincipalHelper::GetOriginAttributesForNetworkState( + aChannel, originAttributes); + EarlyHintPreconnect::MaybePreconnect(linkHeader, aBaseURI, + std::move(originAttributes)); + } else if (linkHeader.mRel.LowerCaseEqualsLiteral("preload")) { + mLinkType |= dom::LinkStyle::ePRELOAD; + EarlyHintPreloader::MaybeCreateAndInsertPreload( + mOngoingEarlyHints, linkHeader, aBaseURI, principal, + cookieJarSettings, aReferrerPolicy, aCSPHeader, + loadInfo->GetBrowsingContextID(), aLoadingBrowsingContext, false); + } else if (linkHeader.mRel.LowerCaseEqualsLiteral("modulepreload")) { + mLinkType |= dom::LinkStyle::eMODULE_PRELOAD; + EarlyHintPreloader::MaybeCreateAndInsertPreload( + mOngoingEarlyHints, linkHeader, aBaseURI, principal, + cookieJarSettings, aReferrerPolicy, aCSPHeader, + loadInfo->GetBrowsingContextID(), aLoadingBrowsingContext, true); + } + } +} + +void EarlyHintsService::FinalResponse(uint32_t aResponseStatus) { + // We will collect telemetry mosly once for a document. + // In case of a reddirect this will be called multiple times. + CollectTelemetry(Some(aResponseStatus)); +} + +void EarlyHintsService::Cancel(const nsACString& aReason) { + CollectTelemetry(Nothing()); + mOngoingEarlyHints->CancelAll(aReason); +} + +void EarlyHintsService::RegisterLinksAndGetConnectArgs( + dom::ContentParentId aCpId, nsTArray& aOutLinks) { + mOngoingEarlyHints->RegisterLinksAndGetConnectArgs(aCpId, aOutLinks); +} + +void EarlyHintsService::CollectTelemetry(Maybe aResponseStatus) { + // EH_NUM_OF_HINTS_PER_PAGE is only collected for the 2xx responses, + // regardless of the number of received mEarlyHintsCount. + // Other telemetry probes are only collected if there was at least one + // EarlyHins response. + if (aResponseStatus && (*aResponseStatus <= 299)) { + Telemetry::Accumulate(Telemetry::EH_NUM_OF_HINTS_PER_PAGE, + mEarlyHintsCount); + } + if (mEarlyHintsCount == 0) { + return; + } + + Telemetry::LABELS_EH_FINAL_RESPONSE label = + Telemetry::LABELS_EH_FINAL_RESPONSE::Cancel; + if (aResponseStatus) { + if (*aResponseStatus <= 299) { + label = Telemetry::LABELS_EH_FINAL_RESPONSE::R2xx; + + MOZ_ASSERT(mFirstEarlyHint); + Telemetry::AccumulateTimeDelta(Telemetry::EH_TIME_TO_FINAL_RESPONSE, + *mFirstEarlyHint, TimeStamp::NowLoRes()); + } else if (*aResponseStatus <= 399) { + label = Telemetry::LABELS_EH_FINAL_RESPONSE::R3xx; + } else if (*aResponseStatus <= 499) { + label = Telemetry::LABELS_EH_FINAL_RESPONSE::R4xx; + } else { + label = Telemetry::LABELS_EH_FINAL_RESPONSE::Other; + } + } + + Telemetry::AccumulateCategorical(label); + + // Reset telemetry counters and timestamps. + mEarlyHintsCount = 0; + mFirstEarlyHint = Nothing(); +} + +void EarlyHintsService::CollectLinkTypeTelemetry(const nsAString& aRel) { + if (aRel.LowerCaseEqualsLiteral("dns-prefetch")) { + glean::netwerk::eh_link_type.Get("dns-prefetch"_ns).Add(1); + } else if (aRel.LowerCaseEqualsLiteral("icon")) { + glean::netwerk::eh_link_type.Get("icon"_ns).Add(1); + } else if (aRel.LowerCaseEqualsLiteral("modulepreload")) { + glean::netwerk::eh_link_type.Get("modulepreload"_ns).Add(1); + } else if (aRel.LowerCaseEqualsLiteral("preconnect")) { + glean::netwerk::eh_link_type.Get("preconnect"_ns).Add(1); + } else if (aRel.LowerCaseEqualsLiteral("prefetch")) { + glean::netwerk::eh_link_type.Get("prefetch"_ns).Add(1); + } else if (aRel.LowerCaseEqualsLiteral("preload")) { + glean::netwerk::eh_link_type.Get("preload"_ns).Add(1); + } else if (aRel.LowerCaseEqualsLiteral("prerender")) { + glean::netwerk::eh_link_type.Get("prerender"_ns).Add(1); + } else if (aRel.LowerCaseEqualsLiteral("stylesheet")) { + glean::netwerk::eh_link_type.Get("stylesheet"_ns).Add(1); + } else { + glean::netwerk::eh_link_type.Get("other"_ns).Add(1); + } +} + +} // namespace mozilla::net diff --git a/netwerk/protocol/http/EarlyHintsService.h b/netwerk/protocol/http/EarlyHintsService.h new file mode 100644 index 0000000000..b256759add --- /dev/null +++ b/netwerk/protocol/http/EarlyHintsService.h @@ -0,0 +1,59 @@ +/* -*- 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_EarlyHintsService_h +#define mozilla_net_EarlyHintsService_h + +#include "mozilla/dom/ipc/IdType.h" +#include "mozilla/Maybe.h" +#include "mozilla/RefPtr.h" +#include "mozilla/TimeStamp.h" +#include "nsStringFwd.h" +#include "nsTArray.h" + +class nsIChannel; +class nsIURI; +class nsIInterfaceRequestor; +namespace mozilla::dom { +class CanonicalBrowsingContext; +} + +namespace mozilla::net { + +class EarlyHintConnectArgs; +class OngoingEarlyHints; + +class EarlyHintsService { + public: + EarlyHintsService(); + ~EarlyHintsService(); + void EarlyHint(const nsACString& aLinkHeader, nsIURI* aBaseURI, + nsIChannel* aChannel, const nsACString& aReferrerPolicy, + const nsACString& aCSPHeader, + dom::CanonicalBrowsingContext* aLoadingBrowsingContext); + void FinalResponse(uint32_t aResponseStatus); + void Cancel(const nsACString& aReason); + + void RegisterLinksAndGetConnectArgs( + dom::ContentParentId aCpId, nsTArray& aOutLinks); + + uint32_t LinkType() const { return mLinkType; } + + private: + void CollectTelemetry(Maybe aResponseStatus); + void CollectLinkTypeTelemetry(const nsAString& aRel); + + Maybe mFirstEarlyHint; + uint32_t mEarlyHintsCount{0}; + + RefPtr mOngoingEarlyHints; + uint32_t mLinkType = 0; +}; + +} // namespace mozilla::net + +#endif // mozilla_net_EarlyHintsService_h diff --git a/netwerk/protocol/http/HPKEConfigManager.sys.mjs b/netwerk/protocol/http/HPKEConfigManager.sys.mjs new file mode 100644 index 0000000000..b08cc892c5 --- /dev/null +++ b/netwerk/protocol/http/HPKEConfigManager.sys.mjs @@ -0,0 +1,42 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +let knownConfigs = new Map(); + +export class HPKEConfigManager { + static async get(aURL, aOptions = {}) { + try { + let config = await this.#getInternal(aURL, aOptions); + return new Uint8Array(config); + } catch (ex) { + console.error(ex); + return null; + } + } + + static async #getInternal(aURL, aOptions = {}) { + let { maxAge = -1 } = aOptions; + let knownConfig = knownConfigs.get(aURL); + if ( + knownConfig && + (maxAge < 0 || Date.now() - knownConfig.fetchDate < maxAge) + ) { + return knownConfig.config; + } + return this.fetchAndStore(aURL, aOptions); + } + + static async fetchAndStore(aURL, aOptions = {}) { + let fetchDate = Date.now(); + let resp = await fetch(aURL, { signal: aOptions.abortSignal }); + if (!resp?.ok) { + throw new Error( + `Fetching HPKE config from ${aURL} failed with error ${resp.status}` + ); + } + let config = await resp.blob().then(b => b.arrayBuffer()); + knownConfigs.set(aURL, { config, fetchDate }); + return config; + } +} diff --git a/netwerk/protocol/http/HTTPSRecordResolver.cpp b/netwerk/protocol/http/HTTPSRecordResolver.cpp new file mode 100644 index 0000000000..79fd92df7f --- /dev/null +++ b/netwerk/protocol/http/HTTPSRecordResolver.cpp @@ -0,0 +1,117 @@ +/* -*- 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 "nsIDNSAdditionalInfo.h" +#include "nsIDNSService.h" +#include "nsHttpConnectionInfo.h" +#include "nsNetCID.h" +#include "nsAHttpTransaction.h" +#include "nsServiceManagerUtils.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; + } + + nsIDNSService::DNSFlags flags = + nsIDNSService::GetFlagsFromTRRMode(mConnInfo->GetTRRMode()); + if (mCaps & NS_HTTP_REFRESH_DNS) { + flags |= nsIDNSService::RESOLVE_BYPASS_CACHE; + } + + nsCOMPtr info; + if (mConnInfo->OriginPort() != NS_HTTPS_DEFAULT_PORT) { + dns->NewAdditionalInfo(""_ns, mConnInfo->OriginPort(), + getter_AddRefs(info)); + } + return dns->AsyncResolveNative( + mConnInfo->GetOrigin(), nsIDNSService::RESOLVE_TYPE_HTTPSSVC, flags, info, + 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; + } + + if (!mTransaction) { + // The connecttion is not interesed in a response anymore. + 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) { + MOZ_ASSERT(mTransaction); + nsCOMPtr dns = do_GetService(NS_DNSSERVICE_CONTRACTID); + if (!dns) { + return; + } + + nsIDNSService::DNSFlags 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, + GetCurrentSerialEventTarget(), + mTransaction->ConnectionInfo()->GetOriginAttributes(), + getter_AddRefs(tmpOutstanding)); +} + +void HTTPSRecordResolver::Close() { mTransaction = nullptr; } + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/HTTPSRecordResolver.h b/netwerk/protocol/http/HTTPSRecordResolver.h new file mode 100644 index 0000000000..2aa5ae55d7 --- /dev/null +++ b/netwerk/protocol/http/HTTPSRecordResolver.h @@ -0,0 +1,44 @@ +/* -*- 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" +#include "nsHttpConnectionInfo.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); + + void Close(); + + protected: + virtual ~HTTPSRecordResolver(); + + private: + RefPtr mTransaction; + RefPtr mConnInfo; + uint32_t mCaps; +}; + +} // namespace net +} // namespace mozilla + +#endif // HTTPSRecordResolver_h__ diff --git a/netwerk/protocol/http/Http2Compression.cpp b/netwerk/protocol/http/Http2Compression.cpp new file mode 100644 index 0000000000..b95883f13f --- /dev/null +++ b/netwerk/protocol/http/Http2Compression.cpp @@ -0,0 +1,1458 @@ +/* -*- 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 { + MutexAutoLock lock(mMutex); + 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; + + Mutex mMutex{"HpackDynamicTableReporter"}; + Http2BaseCompressor* mCompressor MOZ_GUARDED_BY(mMutex); + + 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() { InitializeStaticHeaders(); } + +nvFIFO::~nvFIFO() { Clear(); } + +void nvFIFO::AddElement(const nsCString& name, const nsCString& value) { + nvPair* pair = new nvPair(name, value); + mByteCount += pair->Size(); + MutexAutoLock lock(mMutex); + mTable.PushFront(pair); +} + +void nvFIFO::AddElement(const nsCString& name) { AddElement(name, ""_ns); } + +void nvFIFO::RemoveElement() { + nvPair* pair = nullptr; + { + MutexAutoLock lock(mMutex); + 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; + MutexAutoLock lock(mMutex); + 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() { + mDynamicReporter = new HpackDynamicTableReporter(this); + RegisterStrongMemoryReporter(mDynamicReporter); +} + +Http2BaseCompressor::~Http2BaseCompressor() { + if (mPeakSize) { + Telemetry::Accumulate(mPeakSizeID, mPeakSize); + } + if (mPeakCount) { + Telemetry::Accumulate(mPeakCountID, mPeakCount); + } + UnregisterStrongMemoryReporter(mDynamicReporter); + { + MutexAutoLock lock(mDynamicReporter->mMutex); + mDynamicReporter->mCompressor = nullptr; + } + mDynamicReporter = nullptr; +} + +size_t nvFIFO::SizeOfDynamicTable(mozilla::MallocSizeOf aMallocSizeOf) const { + size_t size = 0; + MutexAutoLock lock(mMutex); + for (const auto elem : mTable) { + size += elem->SizeOfIncludingThis(aMallocSizeOf); + } + return size; +} + +void Http2BaseCompressor::ClearHeaderTable() { mHeaderTable.Clear(); } + +size_t Http2BaseCompressor::SizeOfExcludingThis( + mozilla::MallocSizeOf aMallocSizeOf) const { + return mHeaderTable.SizeOfDynamicTable(aMallocSizeOf); +} + +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); + + LOG(("Http2BaseCompressor::SetMaxBufferSizeInternal %u called", + maxBufferSize)); + + while (mHeaderTable.VariableLength() && + (mHeaderTable.ByteCount() > maxBufferSize)) { + mHeaderTable.RemoveElement(); + } + + 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; + } + + // Bug 1663836: reject invalid HTTP response header names - RFC7540 Sec 10.3 + const char* cFirst = name.BeginReading(); + if (cFirst != nullptr && *cFirst == ':') { + ++cFirst; + } + if (!nsHttp::IsValidToken(cFirst, name.EndReading())) { + nsCString toLog(name); + LOG(("HTTP Decompressor invalid response header found. [%s]\n", + toLog.get())); + return NS_ERROR_ILLEGAL_VALUE; + } + + // 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, LF or NUL in value - could be smuggling (RFC7540 Sec 10.3) + // treat as malformed + if (!nsHttp::IsReasonableHeaderValue(value)) { + return NS_ERROR_ILLEGAL_VALUE; + } + + // 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; + } + 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", startIndex); + if (crlfIndex == -1) { + break; + } + + int32_t colonIndex = Substring(nvInput, 0, crlfIndex).Find(":", 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; + } + 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 = + Substring(nvInput, 0, crlfIndex).Find("; ", 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..ad47949938 --- /dev/null +++ b/netwerk/protocol/http/Http2Compression.h @@ -0,0 +1,207 @@ +/* -*- 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" +#include "mozilla/Mutex.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; + size_t SizeOfDynamicTable(mozilla::MallocSizeOf aMallocSizeOf) const; + + private: + uint32_t mByteCount{0}; + nsDeque mTable; + + // This mutex is held when adding or removing elements in the table + // and when accessing the table from the main thread (in SizeOfDynamicTable) + // Since the operator[] and other const methods are always called + // on the socket thread, they don't need to lock the mutex. + // Mutable so it can be locked in SizeOfDynamicTable which is const + mutable Mutex mMutex MOZ_UNANNOTATED{"nvFIFO"}; +}; + +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{nullptr}; + nvFIFO mHeaderTable; + + uint32_t mMaxBuffer{kDefaultMaxBuffer}; + uint32_t mMaxBufferSetting{kDefaultMaxBuffer}; + bool mSetInitialMaxBufferSizeAllowed{true}; + + uint32_t mPeakSize{0}; + uint32_t mPeakCount{0}; + MOZ_INIT_OUTSIDE_CTOR + Telemetry::HistogramID mPeakSizeID; + MOZ_INIT_OUTSIDE_CTOR + Telemetry::HistogramID mPeakCountID; + + bool mDumpTables{false}; + + private: + RefPtr mDynamicReporter; +}; + +class Http2Compressor; + +class Http2Decompressor final : public Http2BaseCompressor { + public: + Http2Decompressor() { + 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& accum); + [[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 bytes, nsACString& val); + uint8_t ExtractByte(uint8_t bitsLeft, uint32_t& bytesConsumed); + [[nodiscard]] nsresult CopyHuffmanStringFromInput(uint32_t bytes, + 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{0}; + const uint8_t* mData{nullptr}; + uint32_t mDataLen{0}; + bool mSeenNonColonHeader{false}; + bool mIsPush{false}; +}; + +class Http2Compressor final : public Http2BaseCompressor { + public: + Http2Compressor() { + 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{-1}; + bool mBufferSizeChangeWaiting{false}; + uint32_t mLowestBufferSizeWaiting{0}; +}; + +} // 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..35cee9cd2a --- /dev/null +++ b/netwerk/protocol/http/Http2Push.cpp @@ -0,0 +1,557 @@ +/* -*- 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 "nsHttp.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) { + Http2StreamBase* stream = mStream; + return static_cast(stream); + } + return nullptr; +} + +void Http2PushedStreamWrapper::OnPushFailed() { + if (OnSocketThread()) { + if (mStream) { + Http2StreamBase* 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, + Http2StreamBase* aAssociatedStream, uint32_t aID, + uint64_t aCurrentForegroundTabOuterContentWindowId) + : Http2StreamBase((aTransaction->QueryHttpTransaction()) + ? aTransaction->QueryHttpTransaction()->BrowserId() + : 0, + aSession, 0, aCurrentForegroundTabOuterContentWindowId), + mAssociatedTransaction(aAssociatedStream->Transaction()), + mBufferedPush(aTransaction), + mTransaction(aTransaction) { + 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. + mTransactionBrowserId = aAssociatedStream->TransactionBrowserId(); +} + +bool Http2PushedStream::GetPushComplete() { return mPushCompleted; } + +nsresult Http2PushedStream::WriteSegments(nsAHttpSegmentWriter* writer, + uint32_t count, + uint32_t* countWritten) { + nsresult rv = Http2StreamBase::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 Http2StreamBase implements +// nsIHttpPushListener +bool Http2PushedStream::TestOnPush(Http2StreamBase* 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); + RefPtr session = Session(); + CreatePushHashKey(mHeaderScheme, mHeaderHost, originAttributes, + session->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); + Http2StreamBase::mRequestHeadersDone = 1; + Http2StreamBase::mOpenGenerated = 1; + Http2StreamBase::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())); + Http2StreamBase::AdjustInitialWindow(); + // Http2PushedStream::ReadSegments is needed to call TransmitFrame() + // and actually get this information into the session bytestream + RefPtr session = Session(); + session->TransactionHasDataToWrite(this); + } + // Otherwise, when we get hooked up, the initial window will get bumped + // anyway, so we're good to go. +} + +void Http2PushedStream::SetConsumerStream(Http2StreamBase* 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(Http2StreamBase* stream) { + RefPtr session = Session(); + session->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::CurrentBrowserIdChanged(uint64_t id) { + if (mConsumerStream) { + // Pass through to our sink, who will handle things appropriately. + mConsumerStream->CurrentBrowserIdChanged(id); + return; + } + + MOZ_ASSERT(gHttpHandler->ActiveTabPriority()); + + mCurrentBrowserId = id; + RefPtr session = Session(); + if (!session->UseH2Deps()) { + return; + } + + uint32_t oldDependency = mPriorityDependency; + if (mTransactionBrowserId != mCurrentBrowserId) { + mPriorityDependency = Http2Session::kBackgroundGroupID; + nsHttp::NotifyActiveTabLoadOptimization(); + } else { + mPriorityDependency = mDefaultPriorityDependency; + } + + if (mPriorityDependency != oldDependency) { + session->SendPriorityFrame(mStreamID, mPriorityDependency, mPriorityWeight); + } +} + +// ConvertPushHeaders is used to convert the pushed request headers +// into HTTP/1 format and report some telemetry +nsresult Http2PushedStream::ConvertPushHeaders(Http2Decompressor* decompressor, + nsACString& aHeadersIn, + nsACString& aHeadersOut) { + nsresult rv = decompressor->DecodeHeaderBlock( + reinterpret_cast(aHeadersIn.BeginReading()), + aHeadersIn.Length(), aHeadersOut, true); + if (NS_FAILED(rv)) { + LOG3(("Http2PushedStream::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( + ("Http2PushedStream::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( + ("Http2PushedStream::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 Http2PushedStream::CallToReadData(uint32_t count, + uint32_t* countRead) { + return mTransaction->ReadSegments(this, count, countRead); +} + +nsresult Http2PushedStream::CallToWriteData(uint32_t count, + uint32_t* countWritten) { + return mTransaction->WriteSegments(this, count, countWritten); +} + +nsresult Http2PushedStream::GenerateHeaders(nsCString& aCompressedData, + uint8_t& firstFrameFlags) { + MOZ_ASSERT(false); + return NS_ERROR_NOT_IMPLEMENTED; +} + +void Http2PushedStream::CloseStream(nsresult reason) { + mTransaction->Close(reason); + mSession = nullptr; +} + +////////////////////////////////////////// +// 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() { + 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) { + Http2StreamBase* 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..b7e25c2028 --- /dev/null +++ b/netwerk/protocol/http/Http2Push.h @@ -0,0 +1,182 @@ +/* -*- 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 "Http2StreamBase.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 Http2StreamBase { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(Http2PushedStream, override) + + Http2PushedStream(Http2PushTransactionBuffer* aTransaction, + Http2Session* aSession, Http2StreamBase* aAssociatedStream, + uint32_t aID, + uint64_t aCurrentForegroundTabOuterContentWindowId); + + Http2PushedStream* GetHttp2PushedStream() override { return this; } + bool GetPushComplete(); + + // The consumer stream is the synthetic pull stream hooked up to this push + Http2StreamBase* GetConsumerStream() { return mConsumerStream; }; + + void SetConsumerStream(Http2StreamBase* consumer); + [[nodiscard]] bool GetHashKey(nsCString& key); + + // override of Http2StreamBase + [[nodiscard]] nsresult ReadSegments(nsAHttpSegmentReader*, uint32_t, + uint32_t*) override; + [[nodiscard]] nsresult WriteSegments(nsAHttpSegmentWriter*, uint32_t, + uint32_t*) override; + void AdjustInitialWindow() override; + + nsAHttpTransaction* Transaction() override { return mTransaction; } + nsIRequestContext* RequestContext() override { return mRequestContext; }; + void ConnectPushedStream(Http2StreamBase* stream); + + [[nodiscard]] bool TryOnPush(); + [[nodiscard]] static bool TestOnPush(Http2StreamBase* stream); + + 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 Http2StreamBase + virtual bool HasSink() override { return !!mConsumerStream; } + void SetPushComplete() { mPushCompleted = true; } + virtual void CurrentBrowserIdChanged(uint64_t) override; + + nsCString& GetRequestString() { return mRequestString; } + nsCString& GetResourceUrl() { return mResourceUrl; } + + nsresult ConvertPushHeaders(Http2Decompressor* decompressor, + nsACString& aHeadersIn, nsACString& aHeadersOut); + + void CloseStream(nsresult reason) override; + + protected: + nsresult CallToReadData(uint32_t count, uint32_t* countRead) override; + nsresult CallToWriteData(uint32_t count, uint32_t* countWritten) override; + nsresult GenerateHeaders(nsCString& aCompressedData, + uint8_t& firstFrameFlags) override; + + private: + virtual ~Http2PushedStream() = default; + // paired request stream that consumes from real http/2 one.. null until a + // match is made. + Http2StreamBase* mConsumerStream{nullptr}; + + nsCOMPtr mRequestContext; + + nsAHttpTransaction* mAssociatedTransaction; + + Http2PushTransactionBuffer* mBufferedPush; + mozilla::TimeStamp mLastRead; + + nsCString mHashKey; + nsresult mStatus{NS_OK}; + bool mPushCompleted{false}; // server push FIN received + bool mDeferCleanupOnSuccess{true}; + + // 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{false}; + bool mOnPushFailed{false}; + nsCString mRequestString; + nsCString mResourceUrl; + + uint32_t mDefaultPriorityDependency; + + // 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 const mTransaction; +}; + +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{NS_OK}; + nsHttpRequestHead* mRequestHead{nullptr}; + Http2PushedStream* mPushStream{nullptr}; + bool mIsDone{false}; + + UniquePtr mBufferedHTTP1; + uint32_t mBufferedHTTP1Size{kDefaultBufferSize}; + uint32_t mBufferedHTTP1Used{0}; + uint32_t mBufferedHTTP1Consumed{0}; +}; + +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..e969d60c4d --- /dev/null +++ b/netwerk/protocol/http/Http2Session.cpp @@ -0,0 +1,4513 @@ +/* -*- 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 "CacheControlParser.h" +#include "CachePushChecker.h" +#include "Http2Push.h" +#include "Http2Session.h" +#include "Http2Stream.h" +#include "Http2StreamBase.h" +#include "Http2StreamTunnel.h" +#include "LoadContextInfo.h" +#include "mozilla/EndianUtils.h" +#include "mozilla/Preferences.h" +#include "mozilla/Sprintf.h" +#include "mozilla/StaticPrefs_network.h" +#include "mozilla/Telemetry.h" +#include "nsHttp.h" +#include "nsHttpConnection.h" +#include "nsHttpHandler.h" +#include "nsIRequestContext.h" +#include "nsISupportsPriority.h" +#include "nsITLSSocketControl.h" +#include "nsNetUtil.h" +#include "nsQueryObject.h" +#include "nsSocketTransportService2.h" +#include "nsStandardURL.h" +#include "nsURLHelper.h" +#include "prnetdb.h" +#include "sslerr.h" +#include "sslt.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(nsISupportsWeakReference) + NS_INTERFACE_MAP_ENTRY_CONCRETE(Http2Session) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsAHttpConnection) +NS_INTERFACE_MAP_END + +static void RemoveStreamFromQueue(Http2StreamBase* aStream, + nsTArray>& queue) { + for (const auto& stream : Reversed(queue)) { + if (stream == aStream) { + queue.RemoveElement(stream); + } + } +} + +static void AddStreamToQueue(Http2StreamBase* aStream, + nsTArray>& queue) { + if (!queue.Contains(aStream)) { + queue.AppendElement(aStream); + } +} + +static already_AddRefed GetNextStreamFromQueue( + nsTArray>& queue) { + while (!queue.IsEmpty() && !queue[0]) { + MOZ_ASSERT(false); + queue.RemoveElementAt(0); + } + if (queue.IsEmpty()) { + return nullptr; + } + + RefPtr stream = queue[0].get(); + queue.RemoveElementAt(0); + return stream.forget(); +} + +// "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), + 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; + mCurrentBrowserId = gHttpHandler->ConnMgr()->CurrentBrowserId(); + + mEnableWebsockets = StaticPrefs::network_http_http2_websockets(); + + bool dumpHpackTables = StaticPrefs::network_http_http2_enable_hpack_dump(); + mCompressor.SetDumpTables(dumpHpackTables); + mDecompressor.SetDumpTables(dumpHpackTables); +} + +void Http2Session::Shutdown(nsresult aReason) { + for (const auto& stream : mStreamTransactionHash.Values()) { + ShutdownStream(stream, aReason); + } + + for (auto& stream : mTunnelStreams) { + ShutdownStream(stream, aReason); + } +} + +void Http2Session::ShutdownStream(Http2StreamBase* aStream, nsresult aReason) { + // 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 && + (aStream->StreamID() > mGoAwayID || !aStream->HasRegisteredID())) { + CloseStream(aStream, NS_ERROR_NET_RESET); // can be restarted + } else if (aStream->RecvdData()) { + CloseStream(aStream, NS_ERROR_NET_PARTIAL_TRANSFER); + } else if (mGoAwayReason == INADEQUATE_SECURITY) { + CloseStream(aStream, NS_ERROR_NET_INADEQUATE_SECURITY); + } else if (!mCleanShutdown && (mGoAwayReason != NO_HTTP_ERROR)) { + CloseStream(aStream, NS_ERROR_NET_HTTP2_SENT_GOAWAY); + } else if (!mCleanShutdown && SecurityErrorThatMayNeedRestart(aReason)) { + CloseStream(aStream, aReason); + } else { + CloseStream(aStream, NS_ERROR_ABORT); + } +} + +Http2Session::~Http2Session() { + MOZ_DIAGNOSTIC_ASSERT(OnSocketThread()); + LOG3(("Http2Session::~Http2Session %p mDownstreamState=%X", this, + mDownstreamState)); + + Shutdown(NS_OK); + + 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, Http2StreamBase* 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)); + } +} + +using Http2ControlFx = nsresult (*)(Http2Session*); +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))); + + if (!mPingThreshold) { + return UINT32_MAX; + } + + 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 PR_IntervalToSeconds(mPingThreshold) - + PR_IntervalToSeconds(now - mLastReadEpoch); + } + + if (mPingSentEpoch) { + bool isTrr = (mTrrStreams > 0); + uint32_t pingTimeout = isTrr ? StaticPrefs::network_trr_ping_timeout() + : gHttpHandler->SpdyPingTimeout(); + LOG3( + ("Http2Session::ReadTimeoutTick %p handle outstanding ping, " + "timeout=%d\n", + this, pingTimeout)); + if ((now - mPingSentEpoch) >= pingTimeout) { + LOG3(("Http2Session::ReadTimeoutTick %p Ping Timer Exhaustion\n", this)); + if (mConnection) { + mConnection->SetCloseReason(ConnectionCloseReason::IDLE_TIMEOUT); + } + mPingSentEpoch = 0; + if (isTrr) { + // These must be set this way to ensure we gracefully restart all + // streams + mGoAwayID = 0; + mCleanShutdown = true; + // If TRR is mode 2, this Http2Session will be closed due to TRR request + // timeout, so we won't reach this code. If we are in mode 3, the + // request timeout is usually larger than the ping timeout. We close the + // stream with NS_ERROR_NET_RESET, so the transactions can be restarted. + Close(NS_ERROR_NET_RESET); + } else { + 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(Http2StreamBase* 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.InsertOrUpdate(aNewID, stream); + + if (aNewID & 1) { + // don't count push streams here + RefPtr ci(stream->ConnectionInfo()); + if (ci && ci->GetIsTrrServiceChannel()) { + IncrementTrrCounter(); + } + } + return aNewID; +} + +bool Http2Session::AddStream(nsAHttpTransaction* aHttpTransaction, + int32_t aPriority, + 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(); + + CreateStream(aHttpTransaction, aPriority, Http2StreamBaseType::Normal); + return true; +} + +void Http2Session::CreateStream(nsAHttpTransaction* aHttpTransaction, + int32_t aPriority, + Http2StreamBaseType streamType) { + RefPtr refStream; + switch (streamType) { + case Http2StreamBaseType::Normal: + refStream = + new Http2Stream(aHttpTransaction, this, aPriority, mCurrentBrowserId); + break; + case Http2StreamBaseType::WebSocket: + case Http2StreamBaseType::Tunnel: + case Http2StreamBaseType::ServerPush: + MOZ_RELEASE_ASSERT(false); + return; + } + + LOG3(("Http2Session::AddStream session=%p stream=%p serial=%" PRIu64 " " + "NextID=0x%X (tentative)", + this, refStream.get(), mSerial, mNextStreamID)); + + RefPtr stream = refStream; + mStreamTransactionHash.InsertOrUpdate(aHttpTransaction, std::move(refStream)); + + AddStreamToQueue(stream, mReadyForWrite); + 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(); + } +} + +already_AddRefed Http2Session::CreateTunnelStream( + nsAHttpTransaction* aHttpTransaction, nsIInterfaceRequestor* aCallbacks, + PRIntervalTime aRtt, bool aIsWebSocket) { + RefPtr refStream = CreateTunnelStreamFromConnInfo( + this, mCurrentBrowserId, aHttpTransaction->ConnectionInfo(), + aIsWebSocket); + + RefPtr newConn = refStream->CreateHttpConnection( + aHttpTransaction, aCallbacks, aRtt, aIsWebSocket); + + mTunnelStreams.AppendElement(std::move(refStream)); + return newConn.forget(); +} + +void Http2Session::QueueStream(Http2StreamBase* 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 + for (const auto& qStream : mQueuedStreams) { + MOZ_ASSERT(qStream != stream); + MOZ_ASSERT(qStream->Queued()); + } +#endif + + stream->SetQueued(true); + AddStreamToQueue(stream, mQueuedStreams); +} + +void Http2Session::ProcessPending() { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + RefPtr stream; + while (RoomForMoreConcurrent() && + (stream = GetNextStreamFromQueue(mQueuedStreams))) { + LOG3(("Http2Session::ProcessPending %p stream %p woken from queue.", this, + stream.get())); + MOZ_ASSERT(!stream->CountAsActive()); + MOZ_ASSERT(stream->Queued()); + stream->SetQueued(false); + AddStreamToQueue(stream, mReadyForWrite); + 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(); + } + 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.Length(); +} + +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(Http2StreamBase* 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(Http2StreamBase* 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()) { + 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(Http2StreamBase* 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) + Http2StreamBase* 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 (!StaticPrefs::network_http_http2_allow_push()) { + // 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 (StaticPrefs::network_http_http2_enabled_deps() && + 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(Http2StreamBase* 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; + + test++; + if (aStream->StreamID()) { + Http2StreamBase* idStream = mStreamIDHash.Get(aStream->StreamID()); + + test++; + if (idStream != aStream) break; + + if (aOptionalID) { + test++; + if (idStream->StreamID() != aOptionalID) break; + } + } + + if (aStream->IsTunnel()) { + return true; + } + + nsAHttpTransaction* trans = aStream->Transaction(); + + test++; + if (!trans) break; + + test++; + if (mStreamTransactionHash.GetWeak(trans) != aStream) 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 +} + +// static +Http2StreamTunnel* Http2Session::CreateTunnelStreamFromConnInfo( + Http2Session* session, uint64_t bcId, nsHttpConnectionInfo* info, + bool isWebSocket) { + MOZ_ASSERT(info); + MOZ_ASSERT(session); + + if (isWebSocket) { + LOG(("Http2Session creating Http2StreamWebSocket")); + MOZ_ASSERT(session->GetWebSocketSupport() == WebSocketSupport::SUPPORTED); + return new Http2StreamWebSocket( + session, nsISupportsPriority::PRIORITY_NORMAL, bcId, info); + } + + MOZ_ASSERT(info->UsingHttpProxy() && info->UsingConnect()); + LOG(("Http2Session creating Http2StreamTunnel")); + return new Http2StreamTunnel(session, nsISupportsPriority::PRIORITY_NORMAL, + bcId, info); +} + +void Http2Session::CleanupStream(Http2StreamBase* 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 = nullptr; + Http2Stream* h2Stream = aStream->GetHttp2Stream(); + if (h2Stream) { + pushSource = h2Stream->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)); + h2Stream->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 Http2StreamBase and drop the reference to + // its transaction + mStreamTransactionHash.Remove(aStream->Transaction()); + mTunnelStreams.RemoveElement(aStream); + + 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"); + Http2StreamBase* 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); +} + +void Http2Session::RemoveStreamFromQueues(Http2StreamBase* aStream) { + RemoveStreamFromQueue(aStream, mReadyForWrite); + RemoveStreamFromQueue(aStream, mQueuedStreams); + RemoveStreamFromQueue(aStream, mPushesReadyForRead); + RemoveStreamFromQueue(aStream, mSlowConsumersReadyForRead); +} + +void Http2Session::CloseStream(Http2StreamBase* aStream, nsresult aResult, + bool aRemoveFromQueue) { + 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; + } + + if (aRemoveFromQueue) { + RemoveStreamFromQueues(aStream); + } + + // Send the stream the close() indication + aStream->CloseStream(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 + // Http2StreamBase::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->DisableSpdy(); + CleanupStream(mInputFrameDataStream, NS_ERROR_NET_RESET, CANCEL_ERROR); + ResetDownstreamState(); + return NS_OK; + } + 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 (const auto& stream : self->mStreamTransactionHash.Values()) { + stream->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); + } + self->mHasTransactionWaitingForWebsockets = true; + } 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->mProcessedWaitingWebsockets = true; + } + + if (self->mHasTransactionWaitingForWebsockets) { + // trigger a queued websockets transaction -- enabled or not + LOG3(("Http2Sesssion::RecvSettings triggering queued websocket")); + RefPtr ci; + self->GetConnectionInfo(getter_AddRefs(ci)); + gHttpHandler->ConnMgr()->ProcessPendingQ(ci); + self->mHasTransactionWaitingForWebsockets = false; + } + + return NS_OK; +} + +nsresult Http2Session::RecvPushPromise(Http2Session* self) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + 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; + + Http2StreamBase* 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 (!StaticPrefs::network_http_http2_allow_push()) { + // 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->mCurrentBrowserId)); + + 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; + } + 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.InsertOrUpdate(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 = 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(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(Http2StreamBase::RESERVED_BY_REMOTE); + static_assert(Http2StreamBase::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; + // We need to reset mPreviousUsed. If we don't, the next time + // Http2Session::SendPing is called, it will have no effect. + self->mPreviousUsed = false; + } else { + // reply with a ack'd ping + self->GeneratePing(true); + } + + self->ResetDownstreamState(); + return NS_OK; +} + +nsresult Http2Session::RecvGoAway(Http2Session* self) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + 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->mConnection->SetCloseReason(ConnectionCloseReason::GO_AWAY); + 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 (const auto& stream : self->mStreamTransactionHash.Values()) { + // 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 + 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) { + Http2StreamBase* stream = + static_cast(self->mGoAwayStreamsToRestart.PopFront()); + + if (self->mPeerGoAwayReason == HTTP_1_1_REQUIRED) { + stream->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. + for (const auto& stream : self->mQueuedStreams) { + MOZ_ASSERT(stream->Queued()); + stream->SetQueued(false); + if (self->mPeerGoAwayReason == HTTP_1_1_REQUIRED) { + stream->DisableSpdy(); + } + self->CloseStream(stream, NS_ERROR_NET_RESET, false); + self->mStreamTransactionHash.Remove(stream->Transaction()); + } + self->mQueuedStreams.Clear(); + + 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(OnSocketThread(), "not on socket thread"); + 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 reset 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 (const auto& stream : self->mStreamTransactionHash.Values()) { + MOZ_ASSERT(self->mServerSessionWindow > 0); + + if (!stream->BlockedOnRwin() || stream->ServerReceiveWindow() <= 0) { + continue; + } + + AddStreamToQueue(stream, self->mReadyForWrite); + 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) + : Runnable("net::UpdateAltSvcEvent"), + mHeader(header), + mOrigin(aOrigin), + mCI(aCI) {} + + 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->GetPrivate(), nullptr, mCI->ProxyInfo(), 0, + mCI->GetOriginAttributes()); + return NS_OK; + } + + AltSvcMapping::ProcessHeader(mHeader, originScheme, originHost, originPort, + mCI->GetUsername(), mCI->GetPrivate(), 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 ssl; + self->mConnection->GetTLSSocketControl(getter_AddRefs(ssl)); + 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 (StringBeginsWith(origin, "https://"_ns, + nsCaseInsensitiveCStringComparator)) { + specifiedOriginHost.Assign(origin.get() + 8, origin.Length() - 8); + } else if (StringBeginsWith(origin, "http://"_ns, + nsCaseInsensitiveCStringComparator)) { + 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; + } + } + + RefPtr event = + new UpdateAltSvcEvent(altSvcFieldValue, origin, ci); + 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(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); + self->mOriginFrame.WithEntryHandle(key, [&](auto&& entry) { + if (!entry) { + entry.Insert(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 Http2StreamBase::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 Http2StreamBase 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_DIAGNOSTIC_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)); + + RefPtr stream = GetNextStreamFromQueue(mReadyForWrite); + + 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; + LOG(("Http2Session %p nothing to send because of 0RTT failed", this)); + Unused << ResumeRecv(); + } + 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 Http2StreamBase %p " + "0x%X", + this, stream.get(), 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 Http2StreamBase %p 0x%X " + "block-input=%d block-output=%d\n", + this, stream.get(), 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.get(), *countRead)); + AddStreamToQueue(stream, mReadyForWrite); + SetWriteCallbacks(); + return rv; + } + + if (stream->BlockedOnRwin()) { + LOG3(("Http2Session %p will stream %p 0x%X suspended for flow control\n", + this, stream.get(), stream->StreamID())); + return NS_BASE_STREAM_WOULD_BLOCK; + } + + LOG3(("Http2Session::ReadSegments %p stream=%p stream send complete", this, + stream.get())); + + // 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); + } else if (newState == PROCESSING_DATA_FRAME && + !mInputFrameDataStream->AllHeadersReceived()) { + LOG3( + ("Http2Session::ReadyToProcessDataFrame %p streamID 0x%X " + "Receiving data frame without having headers.", + this, mInputFrameID)); + CleanupStream(mInputFrameDataStream, NS_ERROR_NET_HTTP2_SENT_GOAWAY, + PROTOCOL_ERROR); + return NS_OK; + } + + 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) { + LOG(("Http2Session::WriteSegments %p already closed", this)); + // We return NS_ERROR_ABORT (a "soft" error) here, so when this error is + // propagated to another Http2Session, the Http2Session will not be closed + // due to this error code. + return NS_ERROR_ABORT; + } + + 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. + RefPtr pushConnectedStream = + GetNextStreamFromQueue(mPushesReadyForRead); + if (pushConnectedStream) { + return ProcessConnectedPush(pushConnectedStream, writer, count, + countWritten); + } + + // feed gecko channels that previously stopped consuming data + // only take data from stored buffers + RefPtr slowConsumer = + GetNextStreamFromQueue(mSlowConsumersReadyForRead); + 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 (const auto& stream : mStreamTransactionHash.Values()) { + stream->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); + } + 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->ReuseConnectionOnRestartOK(true); + } else if (mDownstreamRstReason == HTTP_1_1_REQUIRED) { + streamCleanupCode = NS_ERROR_NET_RESET; + mInputFrameDataStream->ReuseConnectionOnRestartOK(true); + mInputFrameDataStream->DisableSpdy(); + // actually allow restart by unsticking + mInputFrameDataStream->MakeNonSticky(); + } else { + streamCleanupCode = mInputFrameDataStream->RecvdData() + ? NS_ERROR_NET_PARTIAL_TRANSFER + : NS_ERROR_NET_INTERRUPT; + } + + if (mDownstreamRstReason == COMPRESSION_ERROR) { + mShouldGoAway = true; + } + + // mInputFrameDataStream is reset by ChangeDownstreamState + Http2StreamBase* 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 */ + + if (!mInputFrameDataStream) { + return NS_ERROR_UNEXPECTED; + } + 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) { + Http2StreamBase* streamToCleanup = nullptr; + if (mInputFrameFinal) { + streamToCleanup = mInputFrameDataStream; + } + + bool discardedPadding = + (mDownstreamState == DISCARDING_DATA_FRAME_PADDING); + ResetDownstreamState(); + + if (streamToCleanup) { + Http2PushedStream* pushed = streamToCleanup->GetHttp2PushedStream(); + if (discardedPadding && pushed) { + // Pushed streams are special on padding-only final data frames. + // See bug 1409570 comments 6-8 for details. + pushed->SetPushComplete(); + Http2StreamBase* pushSink = pushed->GetConsumerStream(); + if (pushSink) { + bool enqueueSink = true; + for (const auto& s : mPushesReadyForRead) { + if (s == pushSink) { + enqueueSink = false; + break; + } + } + if (enqueueSink) { + AddStreamToQueue(pushSink, mPushesReadyForRead); + // 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; +} + +nsresult Http2Session::ProcessConnectedPush( + Http2StreamBase* 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. + Http2Stream* h2Stream = pushConnectedStream->GetHttp2Stream(); + MOZ_ASSERT(h2Stream); + if (NS_SUCCEEDED(rv) && !*countWritten && h2Stream && + h2Stream->PushSource() && h2Stream->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(Http2StreamBase* 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(Http2StreamBase* 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(Http2StreamBase* 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(aReason); + + mStreamIDHash.Clear(); + mStreamTransactionHash.Clear(); + mTunnelStreams.Clear(); + mProcessedWaitingWebsockets = true; + + 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) { + // Since mInputFrameFinal can be reset, we need to also check RecvdFin to + // see if a stream doesn’t expect more frames. + if (!mInputFrameFinal && !mInputFrameDataStream->RecvdFin()) { + // 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(Http2StreamBase* stream) { + AddStreamToQueue(stream, mPushesReadyForRead); + Unused << ForceRecv(); +} + +void Http2Session::ConnectSlowConsumer(Http2StreamBase* stream) { + LOG3(("Http2Session::ConnectSlowConsumer %p 0x%X\n", this, + stream->StreamID())); + AddStreamToQueue(stream, mSlowConsumersReadyForRead); + Unused << ForceRecv(); +} + +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(nsITLSSocketControl* tlsSocketControl) { + LOG3(("Http2Session::ALPNCallback sslsocketcontrol=%p\n", tlsSocketControl)); + if (tlsSocketControl) { + int16_t version = tlsSocketControl->GetSSLVersionOffered(); + LOG3(("Http2Session::ALPNCallback version=%x\n", version)); + + if (version == nsITLSSocketControl::TLS_VERSION_1_2 && + !gHttpHandler->IsH2MandatorySuiteEnabled()) { + LOG3(("Http2Session::ALPNCallback Mandatory Cipher Suite Unavailable\n")); + return false; + } + + if (version >= nsITLSSocketControl::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 (!StaticPrefs::network_http_http2_enforce_tls_profile()) { + LOG3( + ("Http2Session::ConfirmTLSProfile %p passed due to configuration " + "bypass\n", + this)); + mTLSProfileConfirmed = true; + return NS_OK; + } + + if (!mConnection) return NS_ERROR_FAILURE; + + nsCOMPtr ssl; + mConnection->GetTLSSocketControl(getter_AddRefs(ssl)); + 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 < nsITLSSocketControl::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_ecdh_hybrid && !StaticPrefs::security_tls_enable_kyber()) { + LOG3(("Http2Session::ConfirmTLSProfile %p FAILED due to disabled KEA %d\n", + this, kea)); + return SessionError(INADEQUATE_SECURITY); + } + + if (kea != ssl_kea_dh && kea != ssl_kea_ecdh && kea != ssl_kea_ecdh_hybrid) { + 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); + } + 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 != nsITLSSocketControl::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) { + AddStreamToQueue(stream, mReadyForWrite); + 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())); + TransactionHasDataToRecv(stream); +} + +void Http2Session::TransactionHasDataToWrite(Http2StreamBase* stream) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG3(("Http2Session::TransactionHasDataToWrite %p stream=%p ID=0x%x", this, + stream, stream->StreamID())); + + AddStreamToQueue(stream, mReadyForWrite); + SetWriteCallbacks(); + Unused << ForceSend(); +} + +void Http2Session::TransactionHasDataToRecv(Http2StreamBase* caller) { + ConnectSlowConsumer(caller); +} + +bool Http2Session::IsPersistent() { return true; } + +nsresult Http2Session::TakeTransport(nsISocketTransport**, + nsIAsyncInputStream**, + nsIAsyncOutputStream**) { + MOZ_ASSERT(false, "TakeTransport of Http2Session"); + return NS_ERROR_UNEXPECTED; +} + +Http3WebTransportSession* Http2Session::GetWebTransportSession( + nsAHttpTransaction* aTransaction) { + MOZ_ASSERT(false, "GetWebTransportSession of Http2Session"); + return nullptr; +} + +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; +} + +void Http2Session::SetConnection(nsAHttpConnection* aConn) { + mConnection = aConn; +} + +//----------------------------------------------------------------------------- +// 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::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) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + // 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 NS_OK; +} + +bool Http2Session::IsReused() { + if (!mConnection) { + return false; + } + + 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"); + LOG(("Http2Session::SendPing %p mPreviousUsed=%d", this, mPreviousUsed)); + + 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(); + // Reset mLastReadEpoch, so we can really check when do we got pong from the + // server. + mLastReadEpoch = 0; + } + 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 sslSocketControl; + mConnection->GetTLSSocketControl(getter_AddRefs(sslSocketControl)); + if (!sslSocketControl) { + return false; + } + + // try all the coalescable versions we support. + const SpdyInformation* info = gHttpHandler->SpdyInfo(); + bool joinedReturn = false; + if (StaticPrefs::network_http_http2_enabled()) { + if (justKidding) { + rv = sslSocketControl->TestJoinConnection(info->VersionString, hostname, + port, &isJoined); + } else { + rv = sslSocketControl->JoinConnection(info->VersionString, 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.InsertOrUpdate(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.InsertOrUpdate(key2, joinedReturn); + } + } + return joinedReturn; +} + +void Http2Session::CurrentBrowserIdChanged(uint64_t id) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + mCurrentBrowserId = id; + + for (const auto& stream : mStreamTransactionHash.Values()) { + stream->CurrentBrowserIdChanged(id); + } +} + +void Http2Session::SetCleanShutdown(bool aCleanShutdown) { + mCleanShutdown = aCleanShutdown; +} + +WebSocketSupport Http2Session::GetWebSocketSupport() { + LOG3(("Http2Session::GetWebSocketSupport %p enable=%d allow=%d processed=%d", + this, mEnableWebsockets, mPeerAllowsWebsockets, + mProcessedWaitingWebsockets)); + + if (!mEnableWebsockets) { + return WebSocketSupport::NO_SUPPORT; + } + + if (mPeerAllowsWebsockets) { + return WebSocketSupport::SUPPORTED; + } + + if (!mProcessedWaitingWebsockets) { + mHasTransactionWaitingForWebsockets = true; + return WebSocketSupport::UNSURE; + } + + return WebSocketSupport::NO_SUPPORT; +} + +PRIntervalTime Http2Session::LastWriteTime() { + return mConnection->LastWriteTime(); +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/Http2Session.h b/netwerk/protocol/http/Http2Session.h new file mode 100644 index 0000000000..767e5cf9f7 --- /dev/null +++ b/netwerk/protocol/http/Http2Session.h @@ -0,0 +1,626 @@ +/* -*- 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 "nsTHashMap.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 Http2StreamBase; +class Http2StreamTunnel; +class nsHttpTransaction; +class nsHttpConnection; + +enum Http2StreamBaseType { Normal, WebSocket, Tunnel, ServerPush }; + +// b23b147c-c4f8-4d6e-841a-09f29a010de7 +#define NS_HTTP2SESSION_IID \ + { \ + 0xb23b147c, 0xc4f8, 0x4d6e, { \ + 0x84, 0x1a, 0x09, 0xf2, 0x9a, 0x01, 0x0d, 0xe7 \ + } \ + } + +class Http2Session final : public ASpdySession, + public nsAHttpConnection, + public nsAHttpSegmentReader, + public nsAHttpSegmentWriter { + ~Http2Session(); + + public: + NS_DECLARE_STATIC_IID_ACCESSOR(NS_HTTP2SESSION_IID) + + 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, + 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(Http2StreamBase*, 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*, Http2StreamBase*, const char*, const char*, + uint32_t); + + // overload of nsAHttpConnection + void TransactionHasDataToWrite(nsAHttpTransaction*) override; + void TransactionHasDataToRecv(nsAHttpTransaction*) override; + + // a similar version for Http2StreamBase + void TransactionHasDataToWrite(Http2StreamBase*); + void TransactionHasDataToRecv(Http2StreamBase* caller); + + // an overload of nsAHttpSegementReader + [[nodiscard]] virtual nsresult CommitToSegmentSize( + uint32_t count, 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(Http2StreamBase* stream); + void ConnectPushedStream(Http2StreamBase* stream); + void ConnectSlowConsumer(Http2StreamBase* stream); + + [[nodiscard]] nsresult ConfirmTLSProfile(); + [[nodiscard]] static bool ALPNCallback(nsITLSSocketControl* tlsSocketControl); + + 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; + 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; + + // For use by an HTTP2Stream + void Received421(nsHttpConnectionInfo* ci); + + void SendPriorityFrame(uint32_t streamID, uint32_t dependsOn, uint8_t weight); + void IncrementTrrCounter() { mTrrStreams++; } + + WebSocketSupport GetWebSocketSupport() override; + + already_AddRefed CreateTunnelStream( + nsAHttpTransaction* aHttpTransaction, nsIInterfaceRequestor* aCallbacks, + PRIntervalTime aRtt, bool aIsWebSocket = false) override; + + void CleanupStream(Http2StreamBase*, nsresult, errorType); + + private: + Http2Session(nsISocketTransport*, enum SpdyVersion version, + bool attemptingEarlyData); + + static Http2StreamTunnel* CreateTunnelStreamFromConnInfo( + Http2Session* session, uint64_t bcId, nsHttpConnectionInfo* connInfo, + bool isWebSocket); + + // 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]; + + void CreateStream(nsAHttpTransaction* aHttpTransaction, int32_t aPriority, + Http2StreamBaseType streamType); + + [[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(uint32_t, nsresult, errorType); + void CloseStream(Http2StreamBase* aStream, nsresult aResult, + bool aRemoveFromQueue = true); + void SendHello(); + void RemoveStreamFromQueues(Http2StreamBase*); + [[nodiscard]] nsresult ParsePadding(uint8_t&, uint16_t&); + + void SetWriteCallbacks(); + void RealignOutputQueue(); + + void ProcessPending(); + [[nodiscard]] nsresult ProcessConnectedPush(Http2StreamBase*, + nsAHttpSegmentWriter*, uint32_t, + uint32_t*); + [[nodiscard]] nsresult ProcessSlowConsumer(Http2StreamBase*, + 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(Http2StreamBase*, uint32_t); + void SetNeedsCleanup(); + + void UpdateLocalRwin(Http2StreamBase* stream, uint32_t bytes); + void UpdateLocalStreamWindow(Http2StreamBase* stream, uint32_t bytes); + void UpdateLocalSessionWindow(uint32_t bytes); + + void MaybeDecrementConcurrent(Http2StreamBase* stream); + bool RoomForMoreConcurrent(); + void IncrementConcurrent(Http2StreamBase* stream); + void QueueStream(Http2StreamBase* 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 aReason); + void ShutdownStream(Http2StreamBase* aStream, nsresult aResult); + + 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. + nsTHashMap mStreamIDHash; + nsRefPtrHashtable, Http2StreamBase> + mStreamTransactionHash; + nsTArray> mTunnelStreams; + + nsTArray> mReadyForWrite; + nsTArray> mQueuedStreams; + nsTArray> mPushesReadyForRead; + nsTArray> 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. + Http2StreamBase* 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. + Http2StreamBase* 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 justKidding); + bool TestOriginFrame(const nsACString& name, int32_t port); + bool mOriginFrameActivated; + nsTHashMap mOriginFrame; + + nsTHashMap mJoinConnectionCache; + + uint64_t mCurrentBrowserId; + + uint32_t mCntActivated; + + // A h2 session will be created before all socket events are trigered, + // e.g. NS_NET_STATUS_TLS_HANDSHAKE_ENDED. + // We should propagate this events to the first nsHttpTransaction. + RefPtr mFirstHttpTransaction; + bool mTlsHandshakeFinished; + + bool mPeerFailedHandshake; + + private: + uint32_t mTrrStreams; + + // websockets + 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 + // Setting this to true means there is a transaction waiting for the result of + // WebSocket support. We'll need to process the pending queue once we've + // received the settings. + bool mHasTransactionWaitingForWebsockets = false; +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(Http2Session, NS_HTTP2SESSION_IID); + +} // 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..cd758f694d --- /dev/null +++ b/netwerk/protocol/http/Http2Stream.cpp @@ -0,0 +1,298 @@ +/* -*- 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 "Http2Push.h" +#include "Http2Stream.h" +#include "nsHttp.h" +#include "nsHttpConnectionInfo.h" +#include "nsHttpRequestHead.h" +#include "nsISocketTransport.h" +#include "Http2Session.h" +#include "PSpdyPush.h" +#include "nsIRequestContext.h" +#include "nsHttpTransaction.h" +#include "nsSocketTransportService2.h" + +namespace mozilla::net { + +Http2Stream::Http2Stream(nsAHttpTransaction* httpTransaction, + Http2Session* session, int32_t priority, uint64_t bcId) + : Http2StreamBase((httpTransaction->QueryHttpTransaction()) + ? httpTransaction->QueryHttpTransaction()->BrowserId() + : 0, + session, priority, bcId), + mTransaction(httpTransaction) { + LOG1(("Http2Stream::Http2Stream %p trans=%p", this, httpTransaction)); +} + +Http2Stream::~Http2Stream() { ClearPushSource(); } + +void Http2Stream::CloseStream(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); + mSession = nullptr; +} + +void Http2Stream::ClearPushSource() { + if (mPushSource) { + mPushSource->SetConsumerStream(nullptr); + mPushSource = nullptr; + } +} + +nsresult Http2Stream::CheckPushCache() { + nsHttpRequestHead* head = mTransaction->RequestHead(); + + // check the push cache for GET + if (!head->IsGet()) { + return NS_OK; + } + + RefPtr session = Session(); + + 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, session->Serial(), + requestURI, mOrigin, hashkey); + + // 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())) { + RefPtr pushSession = pushedStream->Session(); + if (pushSession == session) { + 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, pushSession->Serial(), session->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", + session.get(), 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 + session->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()) { + session->IncrementTrrCounter(); + } + } + + return NS_OK; +} + +uint32_t Http2Stream::GetWireStreamId() { + // >0 even numbered IDs are pushed streams. + // odd numbered IDs are pulled streams. + // 0 is the sink for a pushed stream. + if (!mStreamID) { + MOZ_ASSERT(mPushSource); + if (!mPushSource) { + return 0; + } + + MOZ_ASSERT(mPushSource->StreamID()); + MOZ_ASSERT(!(mPushSource->StreamID() & 1)); // is a push stream + + // If the pushed stream has recvd a FIN, there is no reason to update + // the window + if (mPushSource->RecvdFin() || mPushSource->RecvdReset() || + (mPushSource->HTTPState() == RESERVED_BY_REMOTE)) { + return 0; + } + return mPushSource->StreamID(); + } + + if (mState == RESERVED_BY_REMOTE) { + // h2-14 prevents sending a window update in this state + return 0; + } + return mStreamID; +} + +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->StreamID() && !(mPushSource->StreamID() & 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; + + RefPtr session = Session(); + session->CreateFrameHeader(packet, 5, Http2Session::FRAME_TYPE_PRIORITY, 0, + mPushSource->StreamID()); + + 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->StreamID(), mPriorityDependency, mPriorityWeight)); +} + +bool Http2Stream::IsReadingFromPushStream() { return !!mPushSource; } + +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; + + RefPtr session = Session(); + session->ConnectPushedStream(this); + return NS_OK; + } + + return Http2StreamBase::OnWriteSegment(buf, count, countWritten); +} + +nsresult Http2Stream::CallToReadData(uint32_t count, uint32_t* countRead) { + return mTransaction->ReadSegments(this, count, countRead); +} + +nsresult Http2Stream::CallToWriteData(uint32_t count, uint32_t* countWritten) { + return mTransaction->WriteSegments(this, count, countWritten); +} + +// This is really a headers frame, but open is pretty clear from a workflow pov +nsresult Http2Stream::GenerateHeaders(nsCString& aCompressedData, + uint8_t& firstFrameFlags) { + nsHttpRequestHead* head = mTransaction->RequestHead(); + nsAutoCString requestURI; + head->RequestURI(requestURI); + RefPtr session = Session(); + LOG3(("Http2Stream %p Stream ID 0x%X [session=%p] for URI %s\n", this, + mStreamID, session.get(), requestURI.get())); + + 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); + + rv = session->Compressor()->EncodeHeaderBlock( + mFlatHttpRequestHeaders, method, path, authorityHeader, scheme, + EmptyCString(), false, aCompressedData); + NS_ENSURE_SUCCESS(rv, rv); + + int64_t clVal = session->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. + + if (head->IsGet() || head->IsHead()) { + // for GET and HEAD place the fin bit right on the + // header packet + 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 + firstFrameFlags |= Http2Session::kFlag_END_STREAM; + } + + // The size of the input headers is approximate + uint32_t ratio = + aCompressedData.Length() * 100 / + (11 + requestURI.Length() + mFlatHttpRequestHeaders.Length()); + + Telemetry::Accumulate(Telemetry::SPDY_SYN_RATIO, ratio); + + return NS_OK; +} + +} // namespace mozilla::net diff --git a/netwerk/protocol/http/Http2Stream.h b/netwerk/protocol/http/Http2Stream.h new file mode 100644 index 0000000000..aae1ca1b5d --- /dev/null +++ b/netwerk/protocol/http/Http2Stream.h @@ -0,0 +1,59 @@ +/* -*- 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_Http2Stream_h +#define mozilla_net_Http2Stream_h + +#include "Http2StreamBase.h" + +namespace mozilla::net { + +class Http2Stream : public Http2StreamBase { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(Http2Stream, override) + + Http2Stream(nsAHttpTransaction* httpTransaction, Http2Session* session, + int32_t priority, uint64_t bcId); + + void CloseStream(nsresult reason) override; + Http2Stream* GetHttp2Stream() override { return this; } + uint32_t GetWireStreamId() override; + + nsresult OnWriteSegment(char* buf, uint32_t count, + uint32_t* countWritten) override; + + nsresult CheckPushCache(); + Http2PushedStream* PushSource() { return mPushSource; } + bool IsReadingFromPushStream(); + void ClearPushSource(); + + nsAHttpTransaction* Transaction() override { return mTransaction; } + nsIRequestContext* RequestContext() override { + return mTransaction ? mTransaction->RequestContext() : nullptr; + } + + protected: + ~Http2Stream(); + nsresult CallToReadData(uint32_t count, uint32_t* countRead) override; + nsresult CallToWriteData(uint32_t count, uint32_t* countWritten) override; + nsresult GenerateHeaders(nsCString& aCompressedData, + uint8_t& firstFrameFlags) override; + + private: + // For Http2Push + void AdjustPushedPriority(); + Http2PushedStream* mPushSource{nullptr}; + + // 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; +}; + +} // namespace mozilla::net + +#endif // mozilla_net_Http2Stream_h diff --git a/netwerk/protocol/http/Http2StreamBase.cpp b/netwerk/protocol/http/Http2StreamBase.cpp new file mode 100644 index 0000000000..bb135a59d9 --- /dev/null +++ b/netwerk/protocol/http/Http2StreamBase.cpp @@ -0,0 +1,1324 @@ +/* -*- 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 "Http2StreamBase.h" +#include "Http2Stream.h" + +#include "mozilla/BasePrincipal.h" +#include "mozilla/StaticPrefs_network.h" +#include "mozilla/Telemetry.h" +#include "nsAlgorithm.h" +#include "nsHttp.h" +#include "nsHttpHandler.h" +#include "nsHttpRequestHead.h" +#include "nsIClassOfService.h" +#include "prnetdb.h" + +namespace mozilla::net { + +Http2StreamBase::Http2StreamBase(uint64_t aTransactionBrowserId, + Http2Session* session, int32_t priority, + uint64_t currentBrowserId) + : mSession( + do_GetWeakReference(static_cast(session))), + mRequestHeadersDone(0), + mOpenGenerated(0), + mAllHeadersReceived(0), + mQueued(0), + mSocketTransport(session->SocketTransport()), + mCurrentBrowserId(currentBrowserId), + mTransactionBrowserId(aTransactionBrowserId), + mTxInlineFrameSize(Http2Session::kDefaultBufferSize), + mChunkSize(session->SendingChunkSize()), + mRequestBlockedOnRead(0), + mRecvdFin(0), + mReceivedData(0), + mRecvdReset(0), + mSentReset(0), + mCountAsActive(0), + mSentFin(0), + mSentWaitingFor(0), + mSetTCPSocketBuffer(0), + mBypassInputBuffer(0) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + LOG1(("Http2StreamBase::Http2StreamBase %p", this)); + + 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)); +} + +Http2StreamBase::~Http2StreamBase() { + MOZ_DIAGNOSTIC_ASSERT(OnSocketThread()); + + mStreamID = Http2Session::kDeadStreamID; + + LOG3(("Http2StreamBase::~Http2StreamBase %p", this)); +} + +already_AddRefed Http2StreamBase::Session() { + RefPtr session = do_QueryReferent(mSession); + return session.forget(); +} + +// 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 Http2StreamBase::ReadSegments(nsAHttpSegmentReader* reader, + uint32_t count, uint32_t* countRead) { + LOG3(("Http2StreamBase %p ReadSegments reader=%p count=%d state=%x", this, + reader, count, mUpstreamState)); + RefPtr session = Session(); + // Reader is nullptr when this is a push stream. + MOZ_DIAGNOSTIC_ASSERT(!reader || (reader == session) || + (IsTunnel() && NS_FAILED(Condition()))); + + if (NS_FAILED(Condition())) { + return Condition(); + } + + 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( + ("Http2StreamBase %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 = CallToReadData(count, countRead); + mSegmentReader = nullptr; + + LOG3(("Http2StreamBase::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) { + session->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(("Http2StreamBase %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(("Http2StreamBase %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) && CloseSendStreamWhenDone()) { + MOZ_ASSERT(!mQueued); + MOZ_ASSERT(mRequestHeadersDone); + LOG3( + ("Http2StreamBase::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); + session->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, "Http2StreamBase::ReadSegments unknown state"); + break; + } + + return rv; +} + +uint64_t Http2StreamBase::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 Http2StreamBase::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 Http2StreamBase::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 Http2StreamBase::WriteSegments(nsAHttpSegmentWriter* writer, + uint32_t count, + uint32_t* countWritten) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + MOZ_ASSERT(!mSegmentWriter, "segment writer in progress"); + + LOG3(("Http2StreamBase::WriteSegments %p count=%d state=%x", this, count, + mUpstreamState)); + + mSegmentWriter = writer; + nsresult rv = CallToWriteData(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. + rv = BufferInput(count, countWritten); + LOG3(("Http2StreamBase::WriteSegments %p Buffered %" PRIX32 " %d\n", this, + static_cast(rv), *countWritten)); + } + + LOG3(("Http2StreamBase::WriteSegments %" PRIX32 "", + static_cast(rv))); + mSegmentWriter = nullptr; + return rv; +} + +nsresult Http2StreamBase::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(("Http2StreamBase::ParseHttpRequestHeaders %p avail=%d state=%x", this, + avail, mUpstreamState)); + + 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( + ("Http2StreamBase::ParseHttpRequestHeaders %p " + "Need more header bytes. Len = %zd", + 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; + + Http2Stream* selfRegularStream = this->GetHttp2Stream(); + if (selfRegularStream) { + return selfRegularStream->CheckPushCache(); + } + + return NS_OK; +} + +// This is really a headers frame, but open is pretty clear from a workflow pov +nsresult Http2StreamBase::GenerateOpen() { + // It is now OK to assign a streamID that we are assured will + // be monotonically increasing amongst new streams on this + // session + RefPtr session = Session(); + mStreamID = session->RegisterStreamID(this); + MOZ_ASSERT(mStreamID & 1, "Http2 Stream Channel ID must be odd"); + MOZ_ASSERT(!mOpenGenerated); + + mOpenGenerated = 1; + + LOG3(("Http2StreamBase %p Stream ID 0x%X [session=%p]\n", this, mStreamID, + session.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} + + nsCString compressedData; + uint8_t firstFrameFlags = Http2Session::kFlag_PRIORITY; + + nsresult rv = GenerateHeaders(compressedData, firstFrameFlags); + if (NS_FAILED(rv)) { + return rv; + } + + if (firstFrameFlags & Http2Session::kFlag_END_STREAM) { + SetSentFin(true); + } + + // 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( + ("Http2StreamBase %p Generating %d bytes of HEADERS for stream 0x%X with " + "priority weight %u dep 0x%X frames %u\n", + this, mTxInlineFrameUsed, mStreamID, mPriorityWeight, + mPriorityDependency, numFrames)); + + 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; + + session->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()); + + mFlatHttpRequestHeaders.Truncate(); + + return NS_OK; +} + +void Http2StreamBase::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. + + uint32_t wireStreamId = GetWireStreamId(); + if (wireStreamId == 0) { + 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; + RefPtr session = Session(); + nsHttpTransaction* trans = HttpTransaction(); + if (trans && trans->InitialRwin()) { + bump = (trans->InitialRwin() > mClientReceiveWindow) + ? (trans->InitialRwin() - mClientReceiveWindow) + : 0; + } else { + MOZ_ASSERT(session->InitialRwin() >= mClientReceiveWindow); + bump = session->InitialRwin() - mClientReceiveWindow; + } + + LOG3(("AdjustInitialwindow increased flow control window %p 0x%X %u\n", this, + wireStreamId, 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; + + session->CreateFrameHeader(packet, 4, Http2Session::FRAME_TYPE_WINDOW_UPDATE, + 0, wireStreamId); + + mClientReceiveWindow += bump; + bump = PR_htonl(bump); + memcpy(packet + Http2Session::kFrameHeaderBytes, &bump, 4); +} + +void Http2StreamBase::UpdateTransportReadEvents(uint32_t count) { + mTotalRead += count; + if (!mSocketTransport) { + return; + } + + if (Transaction()) { + Transaction()->OnTransportStatus(mSocketTransport, + NS_NET_STATUS_RECEIVING_FROM, mTotalRead); + } +} + +void Http2StreamBase::UpdateTransportSendEvents(uint32_t count) { + mTotalSent += count; + + // Setting the TCP send buffer, introduced in + // https://bugzilla.mozilla.org/show_bug.cgi?id=790184, which the following + // comment refers to, is being removed once we verify no increases in error + // rate. + // + // 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 (StaticPrefs::network_http_http2_send_buffer_size() > 0 && + (mTotalSent > bufferSize) && !mSetTCPSocketBuffer) { + mSetTCPSocketBuffer = 1; + mSocketTransport->SetSendBufferSize(bufferSize); + } + + if ((mUpstreamState != SENDING_FIN_STREAM) && Transaction()) { + Transaction()->OnTransportStatus(mSocketTransport, NS_NET_STATUS_SENDING_TO, + mTotalSent); + } + + if (!mSentWaitingFor && !mRequestBodyLenRemaining) { + mSentWaitingFor = 1; + if (Transaction()) { + Transaction()->OnTransportStatus(mSocketTransport, + NS_NET_STATUS_WAITING_FOR, 0); + } + } +} + +nsresult Http2StreamBase::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(("Http2StreamBase::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; + RefPtr session = Session(); + + // 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"); + session->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 = session->BufferOutput(reinterpret_cast(mTxInlineFrame.get()), + mTxInlineFrameUsed, &transmittedCount); + LOG3( + ("Http2StreamBase::TransmitFrame for inline BufferOutput session=%p " + "stream=%p result %" PRIx32 " len=%d", + session.get(), 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(session, 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 (session->AmountOfOutputBuffered()) { + rv = session->BufferOutput(buf, mTxStreamFrameSize, &transmittedCount); + } else { + rv = session->OnReadSegment(buf, mTxStreamFrameSize, &transmittedCount); + } + + LOG3( + ("Http2StreamBase::TransmitFrame for regular session=%p " + "stream=%p result %" PRIx32 " len=%d", + session.get(), 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(session, this, "Writing from Transaction Buffer", buf, + transmittedCount); + + *countUsed += mTxStreamFrameSize; + } + + if (!mAttempting0RTT) { + session->FlushOutputQueue(); + } + + // calling this will trigger waiting_for if mRequestBodyLenRemaining is 0 + UpdateTransportSendEvents(mTxInlineFrameUsed + mTxStreamFrameSize); + + mTxInlineFrameUsed = 0; + mTxStreamFrameSize = 0; + + return NS_OK; +} + +void Http2StreamBase::ChangeState(enum upstreamStateType newState) { + LOG3(("Http2StreamBase::ChangeState() %p from %X to %X", this, mUpstreamState, + newState)); + mUpstreamState = newState; +} + +void Http2StreamBase::GenerateDataFrameHeader(uint32_t dataLength, + bool lastFrame) { + LOG3(("Http2StreamBase::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); + } + + RefPtr session = Session(); + session->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 Http2StreamBase::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(("Http2StreamBase::ConvertResponseHeaders %p decode Error\n", this)); + return rv; + } + + nsAutoCString statusString; + decompressor->GetStatus(statusString); + if (statusString.IsEmpty()) { + LOG3(("Http2StreamBase::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( + ("Http2StreamBase::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(("Http2StreamBase::ConvertResponseHeaders %p response code %d\n", this, + httpResponseCode)); + + if (httpResponseCode == 421) { + // Origin Frame requires 421 to remove this origin from the origin set + RefPtr session = Session(); + session->Received421(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())); + HandleResponseHeaders(aHeadersOut, httpResponseCode); + + return NS_OK; +} + +nsresult Http2StreamBase::ConvertResponseTrailers( + Http2Decompressor* decompressor, nsACString& aTrailersIn) { + LOG3(("Http2StreamBase::ConvertResponseTrailers %p", this)); + nsAutoCString flatTrailers; + + nsresult rv = decompressor->DecodeHeaderBlock( + reinterpret_cast(aTrailersIn.BeginReading()), + aTrailersIn.Length(), flatTrailers, false); + if (NS_FAILED(rv)) { + LOG3(("Http2StreamBase::ConvertResponseTrailers %p decode Error", this)); + return rv; + } + + nsHttpTransaction* trans = HttpTransaction(); + if (trans) { + trans->SetHttpTrailers(flatTrailers); + } else { + LOG3(("Http2StreamBase::ConvertResponseTrailers %p no trans", this)); + } + + return NS_OK; +} + +void Http2StreamBase::SetResponseIsComplete() { + nsHttpTransaction* trans = HttpTransaction(); + if (trans) { + trans->SetResponseIsComplete(); + } +} + +void Http2StreamBase::SetAllHeadersReceived() { + if (mAllHeadersReceived) { + return; + } + + if (mState == RESERVED_BY_REMOTE) { + // pushed streams needs to wait until headers have + // arrived to open up their window + LOG3( + ("Http2StreamBase::SetAllHeadersReceived %p state OPEN from reserved\n", + this)); + mState = OPEN; + AdjustInitialWindow(); + } + + mAllHeadersReceived = 1; +} + +bool Http2StreamBase::AllowFlowControlledWrite() { + RefPtr session = Session(); + return (session->ServerSessionWindow() > 0) && (mServerReceiveWindow > 0); +} + +void Http2StreamBase::UpdateServerReceiveWindow(int32_t delta) { + mServerReceiveWindow += delta; + + if (mBlockedOnRwin && AllowFlowControlledWrite()) { + LOG3( + ("Http2StreamBase::UpdateServerReceived UnPause %p 0x%X " + "Open stream window\n", + this, mStreamID)); + RefPtr session = Session(); + session->TransactionHasDataToWrite(this); + } +} + +void Http2StreamBase::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 Http2StreamBase::SetPriorityDependency(uint32_t newPriority, + uint32_t newDependency) { + SetPriority(newPriority); + mPriorityDependency = newDependency; +} + +static uint32_t GetPriorityDependencyFromTransaction(nsHttpTransaction* trans) { + MOZ_ASSERT(trans); + + uint32_t classFlags = trans->GetClassOfService().Flags(); + + 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 Http2StreamBase::UpdatePriorityDependency() { + RefPtr session = Session(); + if (!session->UseH2Deps()) { + return; + } + + nsHttpTransaction* trans = HttpTransaction(); + 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() && + mTransactionBrowserId != mCurrentBrowserId && + mPriorityDependency != Http2Session::kUrgentStartGroupID) { + LOG3( + ("Http2StreamBase::UpdatePriorityDependency %p " + " depends on background group for trans %p\n", + this, trans)); + mPriorityDependency = Http2Session::kBackgroundGroupID; + + nsHttp::NotifyActiveTabLoadOptimization(); + } + + LOG1( + ("Http2StreamBase::UpdatePriorityDependency %p " + "depends on stream 0x%X\n", + this, mPriorityDependency)); +} + +void Http2StreamBase::CurrentBrowserIdChanged(uint64_t id) { + 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; + } + + CurrentBrowserIdChangedInternal(id); +} + +void Http2StreamBase::CurrentBrowserIdChangedInternal(uint64_t id) { + MOZ_ASSERT(gHttpHandler->ActiveTabPriority()); + RefPtr session = Session(); + LOG3( + ("Http2StreamBase::CurrentBrowserIdChangedInternal " + "%p browserId=%" PRIx64 "\n", + this, id)); + + mCurrentBrowserId = id; + + if (!session->UseH2Deps()) { + return; + } + + // Urgent start takes an absolute precedence, so don't + // change mPriorityDependency here. + if (mPriorityDependency == Http2Session::kUrgentStartGroupID) { + return; + } + + if (mTransactionBrowserId != mCurrentBrowserId) { + mPriorityDependency = Http2Session::kBackgroundGroupID; + LOG3( + ("Http2StreamBase::CurrentBrowserIdChangedInternal %p " + "move into background group.\n", + this)); + + nsHttp::NotifyActiveTabLoadOptimization(); + } else { + nsHttpTransaction* trans = HttpTransaction(); + if (!trans) { + return; + } + + mPriorityDependency = GetPriorityDependencyFromTransaction(trans); + LOG3( + ("Http2StreamBase::CurrentBrowserIdChangedInternal %p " + "depends on stream 0x%X\n", + this, mPriorityDependency)); + } + + uint32_t modifyStreamID = GetWireStreamId(); + + if (modifyStreamID) { + session->SendPriorityFrame(modifyStreamID, mPriorityDependency, + mPriorityWeight); + } +} + +void Http2StreamBase::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 Http2StreamBase::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 Http2StreamBase::SetRecvdReset(bool aStatus) { + mRecvdReset = aStatus ? 1 : 0; + if (!aStatus) return; + mState = CLOSED; +} + +void Http2StreamBase::SetSentReset(bool aStatus) { + mSentReset = aStatus ? 1 : 0; + if (!aStatus) return; + mState = CLOSED; +} + +//----------------------------------------------------------------------------- +// nsAHttpSegmentReader +//----------------------------------------------------------------------------- + +nsresult Http2StreamBase::OnReadSegment(const char* buf, uint32_t count, + uint32_t* countRead) { + LOG3(("Http2StreamBase::OnReadSegment %p count=%d state=%x", this, count, + mUpstreamState)); + + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + if (!mSegmentReader) { + return NS_BASE_STREAM_WOULD_BLOCK; + } + + nsresult rv = NS_ERROR_UNEXPECTED; + uint32_t dataLength; + RefPtr session = Session(); + + 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 (!session->TryToActivate(this)) { + LOG3( + ("Http2StreamBase::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( + ("Http2StreamBase this=%p, id 0x%X request body suspended because " + "remote window is stream=%" PRId64 " session=%" PRId64 ".\n", + this, mStreamID, mServerReceiveWindow, + session->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 > session->ServerSessionWindow()) { + dataLength = static_cast(session->ServerSessionWindow()); + } + + if (dataLength > mServerReceiveWindow) { + dataLength = static_cast(mServerReceiveWindow); + } + + LOG3( + ("Http2StreamBase 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, + session->ServerSessionWindow(), Http2Session::kMaxFrameData, + dataLength)); + + session->DecrementServerSessionWindow(dataLength); + mServerReceiveWindow -= dataLength; + + LOG3(("Http2StreamBase %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(this->GetHttp2Stream() && + this->GetHttp2Stream()->IsReadingFromPushStream()); + rv = TransmitFrame(nullptr, nullptr, true); + break; + } + default: + MOZ_ASSERT(false, "Http2StreamBase::OnReadSegment non-write state"); + break; + } + + return rv; +} + +//----------------------------------------------------------------------------- +// nsAHttpSegmentWriter +//----------------------------------------------------------------------------- + +nsresult Http2StreamBase::OnWriteSegment(char* buf, uint32_t count, + uint32_t* countWritten) { + LOG3(("Http2StreamBase::OnWriteSegment %p count=%d state=%x 0x%X\n", this, + count, mUpstreamState, mStreamID)); + + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + if (!mSegmentWriter) { + return NS_BASE_STREAM_WOULD_BLOCK; + } + + // 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( + ("Http2StreamBase::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); +} + +// ----------------------------------------------------------------------------- +// mirror nsAHttpTransaction +// ----------------------------------------------------------------------------- + +bool Http2StreamBase::Do0RTT() { + MOZ_ASSERT(Transaction()); + mAttempting0RTT = false; + nsAHttpTransaction* trans = Transaction(); + if (trans) { + mAttempting0RTT = trans->Do0RTT(); + } + return mAttempting0RTT; +} + +nsresult Http2StreamBase::Finish0RTT(bool aRestart, bool aAlpnChanged) { + MOZ_ASSERT(Transaction()); + 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 = NS_OK; + nsAHttpTransaction* trans = Transaction(); + if (trans) { + rv = trans->Finish0RTT(aAlpnChanged, aAlpnChanged); + if (aRestart) { + nsHttpTransaction* hTrans = trans->QueryHttpTransaction(); + if (hTrans) { + hTrans->Refused0RTT(); + } + } + } + return rv; +} + +nsresult Http2StreamBase::GetOriginAttributes(mozilla::OriginAttributes* oa) { + if (!mSocketTransport) { + return NS_ERROR_UNEXPECTED; + } + + return mSocketTransport->GetOriginAttributes(oa); +} + +nsHttpTransaction* Http2StreamBase::HttpTransaction() { + return (Transaction()) ? Transaction()->QueryHttpTransaction() : nullptr; +} + +nsHttpConnectionInfo* Http2StreamBase::ConnectionInfo() { + if (Transaction()) { + return Transaction()->ConnectionInfo(); + } + return nullptr; +} + +} // namespace mozilla::net diff --git a/netwerk/protocol/http/Http2StreamBase.h b/netwerk/protocol/http/Http2StreamBase.h new file mode 100644 index 0000000000..80d13f3dcd --- /dev/null +++ b/netwerk/protocol/http/Http2StreamBase.h @@ -0,0 +1,377 @@ +/* -*- 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_Http2StreamBase_h +#define mozilla_net_Http2StreamBase_h + +// HTTP/2 - RFC7540 +// https://www.rfc-editor.org/rfc/rfc7540.txt + +#include "mozilla/Attributes.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/WeakPtr.h" +#include "nsAHttpTransaction.h" +#include "nsISupportsPriority.h" +#include "SimpleBuffer.h" +#include "nsISupportsImpl.h" +#include "nsIURI.h" + +class nsISocketTransport; +class nsIInputStream; +class nsIOutputStream; + +namespace mozilla { +class OriginAttributes; +} + +namespace mozilla::net { + +class nsStandardURL; +class Http2Session; +class Http2Stream; +class Http2PushedStream; +class Http2Decompressor; + +class Http2StreamBase : public nsAHttpSegmentReader, + public nsAHttpSegmentWriter, + public SupportsWeakPtr { + public: + NS_DECL_NSAHTTPSEGMENTREADER + + 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; + + Http2StreamBase(uint64_t, Http2Session*, int32_t, uint64_t); + + uint32_t StreamID() { return mStreamID; } + + 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); + + 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; } + + virtual nsAHttpTransaction* Transaction() { return nullptr; } + nsHttpTransaction* HttpTransaction(); + virtual nsIRequestContext* RequestContext() { return nullptr; } + + virtual void CloseStream(nsresult reason) = 0; + 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 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 TransactionBrowserId() { return mTransactionBrowserId; } + + // 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; } + + already_AddRefed Session(); + + // Mirrors nsAHttpTransaction + bool Do0RTT(); + nsresult Finish0RTT(bool aRestart, bool aAlpnChanged); + + nsresult GetOriginAttributes(mozilla::OriginAttributes* oa); + + virtual void CurrentBrowserIdChanged(uint64_t id); + void CurrentBrowserIdChangedInternal( + uint64_t id); // For use by pushed streams only + + virtual bool IsTunnel() { return false; } + + virtual uint32_t GetWireStreamId() { return mStreamID; } + virtual Http2Stream* GetHttp2Stream() { return nullptr; } + virtual Http2PushedStream* GetHttp2PushedStream() { return nullptr; } + + [[nodiscard]] virtual nsresult OnWriteSegment(char*, uint32_t, + uint32_t*) override; + + virtual nsHttpConnectionInfo* ConnectionInfo(); + + bool DataBuffered() { return mSimpleBuffer.Available(); } + + virtual nsresult Condition() { return NS_OK; } + + virtual void DisableSpdy() { + if (Transaction()) { + Transaction()->DisableSpdy(); + } + } + virtual void ReuseConnectionOnRestartOK(bool aReuse) { + if (Transaction()) { + Transaction()->ReuseConnectionOnRestartOK(aReuse); + } + } + virtual void MakeNonSticky() { + if (Transaction()) { + Transaction()->MakeNonSticky(); + } + } + + protected: + virtual ~Http2StreamBase(); + + virtual void HandleResponseHeaders(nsACString& aHeadersOut, + int32_t httpResponseCode) {} + virtual nsresult CallToWriteData(uint32_t count, uint32_t* countRead) = 0; + virtual nsresult CallToReadData(uint32_t count, uint32_t* countWritten) = 0; + virtual bool CloseSendStreamWhenDone() { return true; } + + // These internal states track request generation + enum upstreamStateType { + GENERATING_HEADERS, + GENERATING_BODY, + SENDING_BODY, + SENDING_FIN_STREAM, + UPSTREAM_COMPLETE + }; + + uint32_t mStreamID{0}; + + // The session that this stream is a subset of + nsWeakPtr 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. + RefPtr mSegmentReader; + nsAHttpSegmentWriter* mSegmentWriter{nullptr}; + + 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 { GENERATING_HEADERS }; + + // The HTTP/2 state for the stream from section 5.1 + enum stateType mState { IDLE }; + + // 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 + nsCOMPtr mSocketTransport; + + uint8_t mPriorityWeight = 0; // h2 weight + uint32_t mPriorityDependency = 0; // h2 stream id this one depends on + uint64_t mCurrentBrowserId; + uint64_t mTransactionBrowserId{0}; + + // The InlineFrame and associated data is used for composing control + // frames and data frame headers. + UniquePtr mTxInlineFrame; + uint32_t mTxInlineFrameSize{0}; + uint32_t mTxInlineFrameUsed{0}; + + uint32_t mPriority = 0; // geckoish weight + + // 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{0}; + + private: + friend class mozilla::DefaultDelete; + + [[nodiscard]] nsresult ParseHttpRequestHeaders(const char*, uint32_t, + uint32_t*); + [[nodiscard]] nsresult GenerateOpen(); + + virtual nsresult GenerateHeaders(nsCString& aCompressedData, + uint8_t& firstFrameFlags) = 0; + + void GenerateDataFrameHeader(uint32_t, bool); + + [[nodiscard]] nsresult BufferInput(uint32_t, uint32_t*); + + // 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; + + // 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{0}; + + // 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{0}; + + // True when sending is suspended becuase the server receive window is + // <= 0 + bool mBlockedOnRwin{false}; + + // For Progress Events + uint64_t mTotalSent{0}; + uint64_t mTotalRead{0}; + + // Used to store stream data when the transaction channel cannot keep up + // and flow control has not yet kicked in. + SimpleBuffer mSimpleBuffer; + + bool mAttempting0RTT{false}; +}; + +} // namespace mozilla::net + +#endif // mozilla_net_Http2StreamBase_h diff --git a/netwerk/protocol/http/Http2StreamTunnel.cpp b/netwerk/protocol/http/Http2StreamTunnel.cpp new file mode 100644 index 0000000000..4e0f05d58b --- /dev/null +++ b/netwerk/protocol/http/Http2StreamTunnel.cpp @@ -0,0 +1,764 @@ +/* -*- 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 "nsHttpHandler.h" +#include "Http2StreamTunnel.h" +#include "nsHttpConnectionInfo.h" +#include "nsQueryObject.h" +#include "nsProxyRelease.h" + +namespace mozilla::net { + +bool Http2StreamTunnel::DispatchRelease() { + if (OnSocketThread()) { + return false; + } + + gSocketTransportService->Dispatch( + NewNonOwningRunnableMethod("net::Http2StreamTunnel::Release", this, + &Http2StreamTunnel::Release), + NS_DISPATCH_NORMAL); + + return true; +} + +NS_IMPL_ADDREF(Http2StreamTunnel) +NS_IMETHODIMP_(MozExternalRefCountType) +Http2StreamTunnel::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, "Http2StreamTunnel"); + + if (0 == count) { + mRefCnt = 1; + delete (this); + return 0; + } + + return count; +} + +NS_INTERFACE_MAP_BEGIN(Http2StreamTunnel) + NS_INTERFACE_MAP_ENTRY(nsITransport) + NS_INTERFACE_MAP_ENTRY_CONCRETE(Http2StreamTunnel) + NS_INTERFACE_MAP_ENTRY(nsITransport) + NS_INTERFACE_MAP_ENTRY(nsISocketTransport) + NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference) +NS_INTERFACE_MAP_END_INHERITING(Http2StreamTunnel) + +Http2StreamTunnel::Http2StreamTunnel(Http2Session* session, int32_t priority, + uint64_t bcId, + nsHttpConnectionInfo* aConnectionInfo) + : Http2StreamBase(0, session, priority, bcId), + mConnectionInfo(aConnectionInfo) {} + +Http2StreamTunnel::~Http2StreamTunnel() { ClearTransactionsBlockedOnTunnel(); } + +void Http2StreamTunnel::HandleResponseHeaders(nsACString& aHeadersOut, + int32_t httpResponseCode) {} + +// TODO We do not need this. Fix in bug 1772212. +void Http2StreamTunnel::ClearTransactionsBlockedOnTunnel() { + nsresult rv = gHttpHandler->ConnMgr()->ProcessPendingQ(mConnectionInfo); + if (NS_FAILED(rv)) { + LOG3( + ("Http2StreamTunnel::ClearTransactionsBlockedOnTunnel %p\n" + " ProcessPendingQ failed: %08x\n", + this, static_cast(rv))); + } +} + +NS_IMETHODIMP +Http2StreamTunnel::SetKeepaliveEnabled(bool aKeepaliveEnabled) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +Http2StreamTunnel::SetKeepaliveVals(int32_t keepaliveIdleTime, + int32_t keepaliveRetryInterval) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +Http2StreamTunnel::GetSecurityCallbacks( + nsIInterfaceRequestor** aSecurityCallbacks) { + return mSocketTransport->GetSecurityCallbacks(aSecurityCallbacks); +} + +NS_IMETHODIMP +Http2StreamTunnel::SetSecurityCallbacks( + nsIInterfaceRequestor* aSecurityCallbacks) { + return NS_OK; +} + +NS_IMETHODIMP +Http2StreamTunnel::OpenInputStream(uint32_t aFlags, uint32_t aSegmentSize, + uint32_t aSegmentCount, + nsIInputStream** _retval) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +Http2StreamTunnel::OpenOutputStream(uint32_t aFlags, uint32_t aSegmentSize, + uint32_t aSegmentCount, + nsIOutputStream** _retval) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +void Http2StreamTunnel::CloseStream(nsresult aReason) { + LOG(("Http2StreamTunnel::CloseStream this=%p", this)); + RefPtr session = Session(); + if (NS_SUCCEEDED(mCondition)) { + mSession = nullptr; + // Let the session pickup that the stream has been closed. + mCondition = aReason; + if (NS_SUCCEEDED(aReason)) { + aReason = NS_BASE_STREAM_CLOSED; + } + mOutput->OnSocketReady(aReason); + mInput->OnSocketReady(aReason); + } +} + +NS_IMETHODIMP +Http2StreamTunnel::Close(nsresult aReason) { + LOG(("Http2StreamTunnel::Close this=%p", this)); + RefPtr session = Session(); + if (NS_SUCCEEDED(mCondition)) { + mSession = nullptr; + if (NS_SUCCEEDED(aReason)) { + aReason = NS_BASE_STREAM_CLOSED; + } + mOutput->CloseWithStatus(aReason); + mInput->CloseWithStatus(aReason); + // Let the session pickup that the stream has been closed. + mCondition = aReason; + } + return NS_OK; +} + +NS_IMETHODIMP +Http2StreamTunnel::SetEventSink(nsITransportEventSink* aSink, + nsIEventTarget* aEventTarget) { + return NS_OK; +} + +NS_IMETHODIMP +Http2StreamTunnel::Bind(NetAddr* aLocalAddr) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +Http2StreamTunnel::GetEchConfigUsed(bool* aEchConfigUsed) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +Http2StreamTunnel::SetEchConfig(const nsACString& aEchConfig) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +Http2StreamTunnel::ResolvedByTRR(bool* aResolvedByTRR) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP Http2StreamTunnel::GetEffectiveTRRMode( + nsIRequest::TRRMode* aEffectiveTRRMode) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP Http2StreamTunnel::GetTrrSkipReason( + nsITRRSkipReason::value* aTrrSkipReason) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +Http2StreamTunnel::IsAlive(bool* aAlive) { + RefPtr session = Session(); + if (mSocketTransport && session) { + return mSocketTransport->IsAlive(aAlive); + } + *aAlive = false; + return NS_OK; +} + +#define FWD_TS_T_PTR(fx, ts) \ + NS_IMETHODIMP \ + Http2StreamTunnel::fx(ts* arg) { return mSocketTransport->fx(arg); } + +#define FWD_TS_T_ADDREF(fx, ts) \ + NS_IMETHODIMP \ + Http2StreamTunnel::fx(ts** arg) { return mSocketTransport->fx(arg); } + +#define FWD_TS_T(fx, ts) \ + NS_IMETHODIMP \ + Http2StreamTunnel::fx(ts arg) { return mSocketTransport->fx(arg); } + +FWD_TS_T_PTR(GetKeepaliveEnabled, bool); +FWD_TS_T_PTR(GetSendBufferSize, uint32_t); +FWD_TS_T(SetSendBufferSize, uint32_t); +FWD_TS_T_PTR(GetPort, int32_t); +FWD_TS_T_PTR(GetPeerAddr, mozilla::net::NetAddr); +FWD_TS_T_PTR(GetSelfAddr, mozilla::net::NetAddr); +FWD_TS_T_ADDREF(GetScriptablePeerAddr, nsINetAddr); +FWD_TS_T_ADDREF(GetScriptableSelfAddr, nsINetAddr); +FWD_TS_T_ADDREF(GetTlsSocketControl, nsITLSSocketControl); +FWD_TS_T_PTR(GetConnectionFlags, uint32_t); +FWD_TS_T(SetConnectionFlags, uint32_t); +FWD_TS_T(SetIsPrivate, bool); +FWD_TS_T_PTR(GetTlsFlags, uint32_t); +FWD_TS_T(SetTlsFlags, uint32_t); +FWD_TS_T_PTR(GetRecvBufferSize, uint32_t); +FWD_TS_T(SetRecvBufferSize, uint32_t); +FWD_TS_T_PTR(GetResetIPFamilyPreference, bool); + +nsresult Http2StreamTunnel::GetOriginAttributes( + mozilla::OriginAttributes* aOriginAttributes) { + return mSocketTransport->GetOriginAttributes(aOriginAttributes); +} + +nsresult Http2StreamTunnel::SetOriginAttributes( + const mozilla::OriginAttributes& aOriginAttributes) { + return mSocketTransport->SetOriginAttributes(aOriginAttributes); +} + +NS_IMETHODIMP +Http2StreamTunnel::GetScriptableOriginAttributes( + JSContext* aCx, JS::MutableHandle aOriginAttributes) { + return mSocketTransport->GetScriptableOriginAttributes(aCx, + aOriginAttributes); +} + +NS_IMETHODIMP +Http2StreamTunnel::SetScriptableOriginAttributes( + JSContext* aCx, JS::Handle aOriginAttributes) { + return mSocketTransport->SetScriptableOriginAttributes(aCx, + aOriginAttributes); +} + +NS_IMETHODIMP +Http2StreamTunnel::GetHost(nsACString& aHost) { + return mSocketTransport->GetHost(aHost); +} + +NS_IMETHODIMP +Http2StreamTunnel::GetTimeout(uint32_t aType, uint32_t* _retval) { + return mSocketTransport->GetTimeout(aType, _retval); +} + +NS_IMETHODIMP +Http2StreamTunnel::SetTimeout(uint32_t aType, uint32_t aValue) { + return mSocketTransport->SetTimeout(aType, aValue); +} + +NS_IMETHODIMP +Http2StreamTunnel::SetReuseAddrPort(bool aReuseAddrPort) { + return mSocketTransport->SetReuseAddrPort(aReuseAddrPort); +} + +NS_IMETHODIMP +Http2StreamTunnel::SetLinger(bool aPolarity, int16_t aTimeout) { + return mSocketTransport->SetLinger(aPolarity, aTimeout); +} + +NS_IMETHODIMP +Http2StreamTunnel::GetQoSBits(uint8_t* aQoSBits) { + return mSocketTransport->GetQoSBits(aQoSBits); +} + +NS_IMETHODIMP +Http2StreamTunnel::SetQoSBits(uint8_t aQoSBits) { + return mSocketTransport->SetQoSBits(aQoSBits); +} + +NS_IMETHODIMP +Http2StreamTunnel::GetRetryDnsIfPossible(bool* aRetry) { + return mSocketTransport->GetRetryDnsIfPossible(aRetry); +} + +NS_IMETHODIMP +Http2StreamTunnel::GetStatus(nsresult* aStatus) { + return mSocketTransport->GetStatus(aStatus); +} + +already_AddRefed Http2StreamTunnel::CreateHttpConnection( + nsAHttpTransaction* httpTransaction, nsIInterfaceRequestor* aCallbacks, + PRIntervalTime aRtt, bool aIsWebSocket) { + mInput = new InputStreamTunnel(this); + mOutput = new OutputStreamTunnel(this); + RefPtr conn = new nsHttpConnection(); + + conn->SetTransactionCaps(httpTransaction->Caps()); + nsresult rv = + conn->Init(httpTransaction->ConnectionInfo(), + gHttpHandler->ConnMgr()->MaxRequestDelay(), this, mInput, + mOutput, true, NS_OK, aCallbacks, aRtt, aIsWebSocket); + MOZ_RELEASE_ASSERT(NS_SUCCEEDED(rv)); + mTransaction = httpTransaction; + return conn.forget(); +} + +nsresult Http2StreamTunnel::CallToReadData(uint32_t count, + uint32_t* countRead) { + LOG(("Http2StreamTunnel::CallToReadData this=%p", this)); + return mOutput->OnSocketReady(NS_OK); +} + +nsresult Http2StreamTunnel::CallToWriteData(uint32_t count, + uint32_t* countWritten) { + LOG(("Http2StreamTunnel::CallToWriteData this=%p", this)); + if (!mInput->HasCallback()) { + return NS_BASE_STREAM_WOULD_BLOCK; + } + return mInput->OnSocketReady(NS_OK); +} + +nsresult Http2StreamTunnel::GenerateHeaders(nsCString& aCompressedData, + uint8_t& firstFrameFlags) { + nsAutoCString authorityHeader; + authorityHeader = mConnectionInfo->GetOrigin(); + authorityHeader.Append(':'); + authorityHeader.AppendInt(mConnectionInfo->OriginPort()); + + RefPtr session = Session(); + + LOG3(("Http2StreamTunnel %p Stream ID 0x%X [session=%p] for %s\n", this, + mStreamID, session.get(), authorityHeader.get())); + + mRequestBodyLenRemaining = 0x0fffffffffffffffULL; + + nsresult rv = session->Compressor()->EncodeHeaderBlock( + mFlatHttpRequestHeaders, "CONNECT"_ns, EmptyCString(), authorityHeader, + EmptyCString(), EmptyCString(), true, aCompressedData); + NS_ENSURE_SUCCESS(rv, rv); + + // The size of the input headers is approximate + uint32_t ratio = + aCompressedData.Length() * 100 / + (11 + authorityHeader.Length() + mFlatHttpRequestHeaders.Length()); + + Telemetry::Accumulate(Telemetry::SPDY_SYN_RATIO, ratio); + + return NS_OK; +} + +OutputStreamTunnel::OutputStreamTunnel(Http2StreamTunnel* aStream) { + mWeakStream = do_GetWeakReference(aStream); +} + +OutputStreamTunnel::~OutputStreamTunnel() { + NS_ProxyRelease("OutputStreamTunnel::~OutputStreamTunnel", + gSocketTransportService, mWeakStream.forget()); +} + +nsresult OutputStreamTunnel::OnSocketReady(nsresult condition) { + LOG(("OutputStreamTunnel::OnSocketReady [this=%p cond=%" PRIx32 + " callback=%p]\n", + this, static_cast(condition), mCallback.get())); + + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + nsCOMPtr callback; + + // update condition, but be careful not to erase an already + // existing error condition. + if (NS_SUCCEEDED(mCondition)) { + mCondition = condition; + } + callback = std::move(mCallback); + + nsresult rv = NS_OK; + if (callback) { + rv = callback->OnOutputStreamReady(this); + MaybeSetRequestDone(callback); + } + + return rv; +} + +void OutputStreamTunnel::MaybeSetRequestDone( + nsIOutputStreamCallback* aCallback) { + RefPtr conn = do_QueryObject(aCallback); + if (!conn) { + return; + } + + RefPtr tunnel; + nsresult rv = GetStream(getter_AddRefs(tunnel)); + if (NS_FAILED(rv)) { + return; + } + + if (conn->RequestDone()) { + tunnel->SetRequestDone(); + } +} + +NS_IMPL_ISUPPORTS(OutputStreamTunnel, nsIOutputStream, nsIAsyncOutputStream) + +NS_IMETHODIMP +OutputStreamTunnel::Close() { return CloseWithStatus(NS_BASE_STREAM_CLOSED); } + +NS_IMETHODIMP +OutputStreamTunnel::Flush() { return NS_OK; } + +NS_IMETHODIMP +OutputStreamTunnel::StreamStatus() { return mCondition; } + +NS_IMETHODIMP +OutputStreamTunnel::Write(const char* buf, uint32_t count, + uint32_t* countWritten) { + LOG(("OutputStreamTunnel::Write [this=%p count=%u]\n", this, count)); + + *countWritten = 0; + if (NS_FAILED(mCondition)) { + return mCondition; + } + + RefPtr tunnel; + nsresult rv = GetStream(getter_AddRefs(tunnel)); + if (NS_FAILED(rv)) { + return rv; + } + + return tunnel->OnReadSegment(buf, count, countWritten); +} + +NS_IMETHODIMP +OutputStreamTunnel::WriteSegments(nsReadSegmentFun reader, void* closure, + uint32_t count, uint32_t* countRead) { + // stream is unbuffered + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +OutputStreamTunnel::WriteFrom(nsIInputStream* stream, uint32_t count, + uint32_t* countRead) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +OutputStreamTunnel::IsNonBlocking(bool* nonblocking) { + *nonblocking = true; + return NS_OK; +} + +NS_IMETHODIMP +OutputStreamTunnel::CloseWithStatus(nsresult reason) { + LOG(("OutputStreamTunnel::CloseWithStatus [this=%p reason=%" PRIx32 "]\n", + this, static_cast(reason))); + mCondition = reason; + + RefPtr tunnel = do_QueryReferent(mWeakStream); + mWeakStream = nullptr; + if (!tunnel) { + return NS_OK; + } + RefPtr session = tunnel->Session(); + if (!session) { + return NS_OK; + } + session->CleanupStream(tunnel, reason, Http2Session::CANCEL_ERROR); + return NS_OK; +} + +NS_IMETHODIMP +OutputStreamTunnel::AsyncWait(nsIOutputStreamCallback* callback, uint32_t flags, + uint32_t amount, nsIEventTarget* target) { + LOG(("OutputStreamTunnel::AsyncWait [this=%p]\n", this)); + + // The following parametr are not used: + MOZ_ASSERT(!flags); + MOZ_ASSERT(!amount); + Unused << target; + + RefPtr self(this); + if (NS_FAILED(mCondition)) { + Unused << NS_DispatchToCurrentThread(NS_NewRunnableFunction( + "OutputStreamTunnel::CallOnSocketReady", + [self{std::move(self)}]() { self->OnSocketReady(NS_OK); })); + } else if (callback) { + // Inform the proxy connection that the inner connetion wants to + // read data. + RefPtr tunnel; + nsresult rv = GetStream(getter_AddRefs(tunnel)); + if (NS_FAILED(rv)) { + return rv; + } + RefPtr session; + rv = GetSession(getter_AddRefs(session)); + if (NS_FAILED(rv)) { + return rv; + } + session->TransactionHasDataToWrite(tunnel); + } + + mCallback = callback; + return NS_OK; +} + +InputStreamTunnel::InputStreamTunnel(Http2StreamTunnel* aStream) { + mWeakStream = do_GetWeakReference(aStream); +} + +InputStreamTunnel::~InputStreamTunnel() { + NS_ProxyRelease("InputStreamTunnel::~InputStreamTunnel", + gSocketTransportService, mWeakStream.forget()); +} + +nsresult InputStreamTunnel::OnSocketReady(nsresult condition) { + LOG(("InputStreamTunnel::OnSocketReady [this=%p cond=%" PRIx32 "]\n", this, + static_cast(condition))); + + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + nsCOMPtr callback; + + // update condition, but be careful not to erase an already + // existing error condition. + if (NS_SUCCEEDED(mCondition)) { + mCondition = condition; + } + callback = std::move(mCallback); + + return callback ? callback->OnInputStreamReady(this) : NS_OK; +} + +NS_IMPL_ISUPPORTS(InputStreamTunnel, nsIInputStream, nsIAsyncInputStream) + +NS_IMETHODIMP +InputStreamTunnel::Close() { return CloseWithStatus(NS_BASE_STREAM_CLOSED); } + +NS_IMETHODIMP +InputStreamTunnel::Available(uint64_t* avail) { + LOG(("InputStreamTunnel::Available [this=%p]\n", this)); + + if (NS_FAILED(mCondition)) { + return mCondition; + } + + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +InputStreamTunnel::StreamStatus() { + LOG(("InputStreamTunnel::StreamStatus [this=%p]\n", this)); + + return mCondition; +} + +NS_IMETHODIMP +InputStreamTunnel::Read(char* buf, uint32_t count, uint32_t* countRead) { + LOG(("InputStreamTunnel::Read [this=%p count=%u]\n", this, count)); + + *countRead = 0; + + if (NS_FAILED(mCondition)) { + return mCondition; + } + + RefPtr tunnel; + nsresult rv = GetStream(getter_AddRefs(tunnel)); + if (NS_FAILED(rv)) { + return rv; + } + + return tunnel->OnWriteSegment(buf, count, countRead); +} + +NS_IMETHODIMP +InputStreamTunnel::ReadSegments(nsWriteSegmentFun writer, void* closure, + uint32_t count, uint32_t* countRead) { + // socket stream is unbuffered + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +InputStreamTunnel::IsNonBlocking(bool* nonblocking) { + *nonblocking = true; + return NS_OK; +} + +NS_IMETHODIMP +InputStreamTunnel::CloseWithStatus(nsresult reason) { + LOG(("InputStreamTunnel::CloseWithStatus [this=%p reason=%" PRIx32 "]\n", + this, static_cast(reason))); + mCondition = reason; + + RefPtr tunnel = do_QueryReferent(mWeakStream); + mWeakStream = nullptr; + if (!tunnel) { + return NS_OK; + } + RefPtr session = tunnel->Session(); + if (!session) { + return NS_OK; + } + session->CleanupStream(tunnel, reason, Http2Session::CANCEL_ERROR); + return NS_OK; +} + +NS_IMETHODIMP +InputStreamTunnel::AsyncWait(nsIInputStreamCallback* callback, uint32_t flags, + uint32_t amount, nsIEventTarget* target) { + LOG(("InputStreamTunnel::AsyncWait [this=%p mCondition=%x]\n", this, + static_cast(mCondition))); + + // The following parametr are not used: + MOZ_ASSERT(!flags); + MOZ_ASSERT(!amount); + Unused << target; + + RefPtr self(this); + if (NS_FAILED(mCondition)) { + Unused << NS_DispatchToCurrentThread(NS_NewRunnableFunction( + "InputStreamTunnel::CallOnSocketReady", + [self{std::move(self)}]() { self->OnSocketReady(NS_OK); })); + } else if (callback) { + // Inform the proxy connection that the inner connetion wants to + // read data. + RefPtr tunnel; + nsresult rv = GetStream(getter_AddRefs(tunnel)); + if (NS_FAILED(rv)) { + return rv; + } + RefPtr session; + rv = GetSession(getter_AddRefs(session)); + if (NS_FAILED(rv)) { + return rv; + } + if (tunnel->DataBuffered()) { + session->TransactionHasDataToRecv(tunnel); + } + } + + mCallback = callback; + return NS_OK; +} + +nsresult OutputStreamTunnel::GetStream(Http2StreamTunnel** aStream) { + RefPtr tunnel = do_QueryReferent(mWeakStream); + MOZ_ASSERT(tunnel); + if (!tunnel) { + return NS_ERROR_UNEXPECTED; + } + + tunnel.forget(aStream); + return NS_OK; +} + +nsresult OutputStreamTunnel::GetSession(Http2Session** aSession) { + RefPtr tunnel; + nsresult rv = GetStream(getter_AddRefs(tunnel)); + if (NS_FAILED(rv)) { + return rv; + } + RefPtr session = tunnel->Session(); + MOZ_ASSERT(session); + if (!session) { + return NS_ERROR_UNEXPECTED; + } + + session.forget(aSession); + return NS_OK; +} + +nsresult InputStreamTunnel::GetStream(Http2StreamTunnel** aStream) { + RefPtr tunnel = do_QueryReferent(mWeakStream); + MOZ_ASSERT(tunnel); + if (!tunnel) { + return NS_ERROR_UNEXPECTED; + } + + tunnel.forget(aStream); + return NS_OK; +} + +nsresult InputStreamTunnel::GetSession(Http2Session** aSession) { + RefPtr tunnel; + nsresult rv = GetStream(getter_AddRefs(tunnel)); + if (NS_FAILED(rv)) { + return rv; + } + RefPtr session = tunnel->Session(); + if (!session) { + return NS_ERROR_UNEXPECTED; + } + + session.forget(aSession); + return NS_OK; +} + +Http2StreamWebSocket::Http2StreamWebSocket( + Http2Session* session, int32_t priority, uint64_t bcId, + nsHttpConnectionInfo* aConnectionInfo) + : Http2StreamTunnel(session, priority, bcId, aConnectionInfo) { + LOG(("Http2StreamWebSocket ctor:%p", this)); +} + +Http2StreamWebSocket::~Http2StreamWebSocket() { + LOG(("Http2StreamWebSocket dtor:%p", this)); +} + +nsresult Http2StreamWebSocket::GenerateHeaders(nsCString& aCompressedData, + uint8_t& firstFrameFlags) { + nsHttpRequestHead* head = mTransaction->RequestHead(); + + nsAutoCString authorityHeader; + nsresult rv = head->GetHeader(nsHttp::Host, authorityHeader); + NS_ENSURE_SUCCESS(rv, rv); + + RefPtr session = Session(); + LOG3(("Http2StreamWebSocket %p Stream ID 0x%X [session=%p] for %s\n", this, + mStreamID, session.get(), authorityHeader.get())); + + nsDependentCString scheme(head->IsHTTPS() ? "https" : "http"); + nsAutoCString path; + head->Path(path); + + rv = session->Compressor()->EncodeHeaderBlock( + mFlatHttpRequestHeaders, "CONNECT"_ns, path, authorityHeader, scheme, + "websocket"_ns, false, aCompressedData); + NS_ENSURE_SUCCESS(rv, rv); + + mRequestBodyLenRemaining = 0x0fffffffffffffffULL; + + // The size of the input headers is approximate + uint32_t ratio = + aCompressedData.Length() * 100 / + (11 + authorityHeader.Length() + mFlatHttpRequestHeaders.Length()); + + Telemetry::Accumulate(Telemetry::SPDY_SYN_RATIO, ratio); + return NS_OK; +} + +void Http2StreamWebSocket::CloseStream(nsresult aReason) { + LOG(("Http2StreamWebSocket::CloseStream this=%p aReason=%x", this, + static_cast(aReason))); + if (mTransaction) { + mTransaction->Close(aReason); + mTransaction = nullptr; + } + Http2StreamTunnel::CloseStream(aReason); +} + +} // namespace mozilla::net diff --git a/netwerk/protocol/http/Http2StreamTunnel.h b/netwerk/protocol/http/Http2StreamTunnel.h new file mode 100644 index 0000000000..78f0300f4c --- /dev/null +++ b/netwerk/protocol/http/Http2StreamTunnel.h @@ -0,0 +1,151 @@ +/* -*- 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_Http2StreamTunnel_h +#define mozilla_net_Http2StreamTunnel_h + +#include "Http2StreamBase.h" +#include "nsHttpConnection.h" +#include "nsWeakReference.h" + +namespace mozilla::net { + +class OutputStreamTunnel; +class InputStreamTunnel; + +// c881f764-a183-45cb-9dec-d9872b2f47b2 +#define NS_HTTP2STREAMTUNNEL_IID \ + { \ + 0xc881f764, 0xa183, 0x45cb, { \ + 0x9d, 0xec, 0xd9, 0x87, 0x2b, 0x2f, 0x47, 0xb2 \ + } \ + } + +class Http2StreamTunnel : public Http2StreamBase, + public nsISocketTransport, + public nsSupportsWeakReference { + public: + NS_DECLARE_STATIC_IID_ACCESSOR(NS_HTTP2STREAMTUNNEL_IID) + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSITRANSPORT + NS_DECL_NSISOCKETTRANSPORT + + Http2StreamTunnel(Http2Session* session, int32_t priority, uint64_t bcId, + nsHttpConnectionInfo* aConnectionInfo); + bool IsTunnel() override { return true; } + + already_AddRefed CreateHttpConnection( + nsAHttpTransaction* httpTransaction, nsIInterfaceRequestor* aCallbacks, + PRIntervalTime aRtt, bool aIsWebSocket); + + nsHttpConnectionInfo* ConnectionInfo() override { return mConnectionInfo; } + + void SetRequestDone() { mSendClosed = true; } + nsresult Condition() override { return mCondition; } + void CloseStream(nsresult reason) override; + void DisableSpdy() override { + if (mTransaction) { + mTransaction->DisableHttp2ForProxy(); + } + } + void ReuseConnectionOnRestartOK(bool aReuse) override { + // Do nothing here. We'll never reuse the connection. + } + void MakeNonSticky() override {} + + protected: + ~Http2StreamTunnel(); + nsresult CallToReadData(uint32_t count, uint32_t* countRead) override; + nsresult CallToWriteData(uint32_t count, uint32_t* countWritten) override; + void HandleResponseHeaders(nsACString& aHeadersOut, + int32_t httpResponseCode) override; + nsresult GenerateHeaders(nsCString& aCompressedData, + uint8_t& firstFrameFlags) override; + bool CloseSendStreamWhenDone() override { return mSendClosed; } + + RefPtr mTransaction; + + void ClearTransactionsBlockedOnTunnel(); + bool DispatchRelease(); + + RefPtr mOutput; + RefPtr mInput; + RefPtr mConnectionInfo; + bool mSendClosed{false}; + nsresult mCondition{NS_OK}; +}; + +// f9d10060-f5d4-443e-ba59-f84ea975c5f0 +#define NS_OUTPUTSTREAMTUNNEL_IID \ + { \ + 0xf9d10060, 0xf5d4, 0x443e, { \ + 0xba, 0x59, 0xf8, 0x4e, 0xa9, 0x75, 0xc5, 0xf0 \ + } \ + } + +class OutputStreamTunnel : public nsIAsyncOutputStream { + public: + NS_DECLARE_STATIC_IID_ACCESSOR(NS_OUTPUTSTREAMTUNNEL_IID) + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIOUTPUTSTREAM + NS_DECL_NSIASYNCOUTPUTSTREAM + + explicit OutputStreamTunnel(Http2StreamTunnel* aStream); + + nsresult OnSocketReady(nsresult condition); + void MaybeSetRequestDone(nsIOutputStreamCallback* aCallback); + + private: + virtual ~OutputStreamTunnel(); + + nsresult GetStream(Http2StreamTunnel** aStream); + nsresult GetSession(Http2Session** aSession); + + nsWeakPtr mWeakStream; + nsCOMPtr mCallback; + nsresult mCondition{NS_OK}; +}; + +class InputStreamTunnel : public nsIAsyncInputStream { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIINPUTSTREAM + NS_DECL_NSIASYNCINPUTSTREAM + + explicit InputStreamTunnel(Http2StreamTunnel* aStream); + + nsresult OnSocketReady(nsresult condition); + bool HasCallback() { return !!mCallback; } + + private: + virtual ~InputStreamTunnel(); + + nsresult GetStream(Http2StreamTunnel** aStream); + nsresult GetSession(Http2Session** aSession); + + nsWeakPtr mWeakStream; + nsCOMPtr mCallback; + nsresult mCondition{NS_OK}; +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(Http2StreamTunnel, NS_HTTP2STREAMTUNNEL_IID) +NS_DEFINE_STATIC_IID_ACCESSOR(OutputStreamTunnel, NS_OUTPUTSTREAMTUNNEL_IID) + +class Http2StreamWebSocket : public Http2StreamTunnel { + public: + Http2StreamWebSocket(Http2Session* session, int32_t priority, uint64_t bcId, + nsHttpConnectionInfo* aConnectionInfo); + void CloseStream(nsresult reason) override; + + protected: + virtual ~Http2StreamWebSocket(); + nsresult GenerateHeaders(nsCString& aCompressedData, + uint8_t& firstFrameFlags) override; +}; + +} // namespace mozilla::net + +#endif // mozilla_net_Http2StreamTunnel_h diff --git a/netwerk/protocol/http/Http3Session.cpp b/netwerk/protocol/http/Http3Session.cpp new file mode 100644 index 0000000000..0369da94ac --- /dev/null +++ b/netwerk/protocol/http/Http3Session.cpp @@ -0,0 +1,2522 @@ +/* -*- 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 "ASpdySession.h" // because of SoftStreamError() +#include "Http3Session.h" +#include "Http3Stream.h" +#include "Http3StreamBase.h" +#include "Http3WebTransportSession.h" +#include "Http3WebTransportStream.h" +#include "HttpConnectionUDP.h" +#include "HttpLog.h" +#include "QuicSocketControl.h" +#include "SSLServerCertVerification.h" +#include "SSLTokensCache.h" +#include "ScopedNSSTypes.h" +#include "mozilla/RandomNum.h" +#include "mozilla/RefPtr.h" +#include "mozilla/Telemetry.h" +#include "mozilla/net/DNS.h" +#include "nsHttpHandler.h" +#include "nsIHttpActivityObserver.h" +#include "nsIOService.h" +#include "nsITLSSocketControl.h" +#include "nsNetAddr.h" +#include "nsQueryObject.h" +#include "nsSocketTransportService2.h" +#include "nsThreadUtils.h" +#include "sslerr.h" + +namespace mozilla::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; + +const uint32_t TRANSPORT_ERROR_STATELESS_RESET = 20; + +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() { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG(("Http3Session::Http3Session [this=%p]", this)); + + mCurrentBrowserId = gHttpHandler->ConnMgr()->CurrentBrowserId(); + mThroughCaptivePortal = gHttpHandler->GetThroughCaptivePortal(); +} + +static nsresult RawBytesToNetAddr(uint16_t aFamily, const uint8_t* aRemoteAddr, + uint16_t remotePort, NetAddr* netAddr) { + if (aFamily == AF_INET) { + netAddr->inet.family = AF_INET; + netAddr->inet.port = htons(remotePort); + memcpy(&netAddr->inet.ip, aRemoteAddr, 4); + } else if (aFamily == AF_INET6) { + netAddr->inet6.family = AF_INET6; + netAddr->inet6.port = htons(remotePort); + memcpy(&netAddr->inet6.ip.u8, aRemoteAddr, 16); + } else { + return NS_ERROR_UNEXPECTED; + } + + return NS_OK; +} + +nsresult Http3Session::Init(const nsHttpConnectionInfo* aConnInfo, + nsINetAddr* aSelfAddr, nsINetAddr* aPeerAddr, + HttpConnectionUDP* udpConn, uint32_t controlFlags, + nsIInterfaceRequestor* callbacks) { + LOG3(("Http3Session::Init %p", this)); + + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + MOZ_ASSERT(udpConn); + + mConnInfo = aConnInfo->Clone(); + mNetAddr = aPeerAddr; + + bool httpsProxy = + aConnInfo->ProxyInfo() ? aConnInfo->ProxyInfo()->IsHTTPS() : false; + + // Create security control and info object for quic. + mSocketControl = new QuicSocketControl( + httpsProxy ? aConnInfo->ProxyInfo()->Host() : aConnInfo->GetOrigin(), + httpsProxy ? aConnInfo->ProxyInfo()->Port() : aConnInfo->OriginPort(), + controlFlags, this); + + NetAddr selfAddr; + MOZ_ALWAYS_SUCCEEDS(aSelfAddr->GetNetAddr(&selfAddr)); + NetAddr peerAddr; + MOZ_ALWAYS_SUCCEEDS(aPeerAddr->GetNetAddr(&peerAddr)); + + LOG3( + ("Http3Session::Init origin=%s, alpn=%s, selfAddr=%s, peerAddr=%s," + " qpack table size=%u, max blocked streams=%u webtransport=%d " + "[this=%p]", + PromiseFlatCString(mConnInfo->GetOrigin()).get(), + PromiseFlatCString(mConnInfo->GetNPNToken()).get(), + selfAddr.ToString().get(), peerAddr.ToString().get(), + gHttpHandler->DefaultQpackTableSize(), + gHttpHandler->DefaultHttp3MaxBlockedStreams(), + mConnInfo->GetWebTransport(), this)); + + if (mConnInfo->GetWebTransport()) { + mWebTransportNegotiationStatus = WebTransportNegotiation::NEGOTIATING; + } + + uint32_t datagramSize = + StaticPrefs::network_webtransport_datagrams_enabled() + ? StaticPrefs::network_webtransport_datagram_size() + : 0; + nsresult rv = NeqoHttp3Conn::Init( + mConnInfo->GetOrigin(), mConnInfo->GetNPNToken(), selfAddr, peerAddr, + gHttpHandler->DefaultQpackTableSize(), + gHttpHandler->DefaultHttp3MaxBlockedStreams(), + StaticPrefs::network_http_http3_max_data(), + StaticPrefs::network_http_http3_max_stream_data(), + StaticPrefs::network_http_http3_version_negotiation_enabled(), + mConnInfo->GetWebTransport(), gHttpHandler->Http3QlogDir(), datagramSize, + StaticPrefs::network_http_http3_max_accumlated_time_ms(), + getter_AddRefs(mHttp3Connection)); + if (NS_FAILED(rv)) { + return rv; + } + + nsAutoCString peerId; + mSocketControl->GetPeerId(peerId); + nsTArray token; + SessionCacheInfo info; + udpConn->ChangeConnectionState(ConnectionState::TLS_HANDSHAKING); + + if (StaticPrefs::network_http_http3_enable_0rtt() && + NS_SUCCEEDED(SSLTokensCache::Get(peerId, token, info))) { + LOG(("Found a resumption token in the cache.")); + mHttp3Connection->SetResumptionToken(token); + mSocketControl->SetSessionCacheInfo(std::move(info)); + if (mHttp3Connection->IsZeroRtt()) { + LOG(("Can send ZeroRtt data")); + RefPtr self(this); + mState = ZERORTT; + udpConn->ChangeConnectionState(ConnectionState::ZERORTT); + mZeroRttStarted = TimeStamp::Now(); + // 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"); + } + } + + if (mState != ZERORTT) { + ZeroRttTelemetry(ZeroRttOutcome::NOT_USED); + } + + auto config = mConnInfo->GetEchConfig(); + if (config.IsEmpty()) { + if (StaticPrefs::security_tls_ech_grease_http3() && config.IsEmpty()) { + if ((RandomUint64().valueOr(0) % 100) >= + 100 - StaticPrefs::security_tls_ech_grease_probability()) { + // Setting an empty config enables GREASE mode. + mSocketControl->SetEchConfig(config); + mEchExtensionStatus = EchExtensionStatus::kGREASE; + } + } + } else if (gHttpHandler->EchConfigEnabled(true) && !config.IsEmpty()) { + mSocketControl->SetEchConfig(config); + mEchExtensionStatus = EchExtensionStatus::kReal; + HttpConnectionActivity activity( + mConnInfo->HashKey(), mConnInfo->GetOrigin(), mConnInfo->OriginPort(), + mConnInfo->EndToEndSSL(), !mConnInfo->GetEchConfig().IsEmpty(), + mConnInfo->IsHttp3()); + gHttpHandler->ObserveHttpActivityWithArgs( + activity, NS_ACTIVITY_TYPE_HTTP_CONNECTION, + NS_HTTP_ACTIVITY_SUBTYPE_ECH_SET, PR_Now(), 0, ""_ns); + } else { + mEchExtensionStatus = EchExtensionStatus::kNotPresent; + } + + // 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. + mUdpConn = udpConn; + return NS_OK; +} + +void Http3Session::DoSetEchConfig(const nsACString& aEchConfig) { + LOG(("Http3Session::DoSetEchConfig %p of length %zu", this, + aEchConfig.Length())); + nsTArray config; + config.AppendElements( + reinterpret_cast(aEchConfig.BeginReading()), + aEchConfig.Length()); + mHttp3Connection->SetEchConfig(config); +} + +nsresult Http3Session::SendPriorityUpdateFrame(uint64_t aStreamId, + uint8_t aPriorityUrgency, + bool aPriorityIncremental) { + return mHttp3Connection->PriorityUpdate(aStreamId, aPriorityUrgency, + aPriorityIncremental); +} + +// Shutdown the http3session and close all transactions. +void Http3Session::Shutdown() { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + if (mTimer) { + mTimer->Cancel(); + } + mTimer = nullptr; + + bool isEchRetry = mError == mozilla::psm::GetXPCOMFromNSSError( + SSL_ERROR_ECH_RETRY_WITH_ECH); + bool allowToRetryWithDifferentIPFamily = + mBeforeConnectedError && + gHttpHandler->ConnMgr()->AllowToRetryDifferentIPFamilyForHttp3(mConnInfo, + mError); + LOG(("Http3Session::Shutdown %p allowToRetryWithDifferentIPFamily=%d", this, + allowToRetryWithDifferentIPFamily)); + if ((mBeforeConnectedError || + (mError == NS_ERROR_NET_HTTP3_PROTOCOL_ERROR)) && + (mError != + mozilla::psm::GetXPCOMFromNSSError(SSL_ERROR_BAD_CERT_DOMAIN)) && + !isEchRetry && !mConnInfo->GetWebTransport() && + !allowToRetryWithDifferentIPFamily && !mDontExclude) { + gHttpHandler->ExcludeHttp3(mConnInfo); + } + + for (const auto& stream : mStreamTransactionHash.Values()) { + 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)); + if (isEchRetry) { + // We have to propagate this error to nsHttpTransaction, so the + // transaction will be restarted with a new echConfig. + stream->Close(mError); + } else { + if (allowToRetryWithDifferentIPFamily && mNetAddr) { + NetAddr addr; + mNetAddr->GetNetAddr(&addr); + gHttpHandler->ConnMgr()->SetRetryDifferentIPFamilyForHttp3( + mConnInfo, addr.raw.family); + // We want the transaction to be restarted with the same connection + // info. + stream->Transaction()->DoNotRemoveAltSvc(); + // We already set the preference in SetRetryDifferentIPFamilyForHttp3, + // so we want to keep it for the next retry. + stream->Transaction()->DoNotResetIPFamilyPreference(); + stream->Close(NS_ERROR_NET_RESET); + // Since Http3Session::Shutdown can be called multiple times, we set + // mDontExclude for not putting this domain into the excluded list. + mDontExclude = true; + } else { + 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->GetHttp3Stream() && + stream->GetHttp3Stream()->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 if (mError == NS_ERROR_NET_RESET) { + stream->Close(NS_ERROR_NET_RESET); + } else { + stream->Close(NS_ERROR_ABORT); + } + RemoveStreamFromQueues(stream); + if (stream->HasStreamId()) { + mStreamIdHash.Remove(stream->StreamId()); + } + } + mStreamTransactionHash.Clear(); + + for (const auto& stream : mWebTransportSessions) { + stream->Close(NS_ERROR_ABORT); + RemoveStreamFromQueues(stream); + mStreamIdHash.Remove(stream->StreamId()); + } + mWebTransportSessions.Clear(); + + for (const auto& stream : mWebTransportStreams) { + stream->Close(NS_ERROR_ABORT); + RemoveStreamFromQueues(stream); + mStreamIdHash.Remove(stream->StreamId()); + } + + RefPtr stream; + while ((stream = mQueuedStreams.PopFront())) { + LOG(("Close remaining stream in queue:%p", stream.get())); + stream->SetQueued(false); + stream->Close(NS_ERROR_ABORT); + } + mWebTransportStreams.Clear(); +} + +Http3Session::~Http3Session() { + LOG3(("Http3Session::~Http3Session %p", this)); + + EchOutcomeTelemetry(); + 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); + + 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); + } + + 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::RecvData -> +// Http3Session::RecvData +void Http3Session::ProcessInput(nsIUDPSocket* socket) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + MOZ_ASSERT(mUdpConn); + + LOG(("Http3Session::ProcessInput writer=%p [this=%p state=%d]", + mUdpConn.get(), this, mState)); + + while (true) { + nsTArray data; + NetAddr addr{}; + // RecvWithAddr actually does not return an error. + nsresult rv = socket->RecvWithAddr(&addr, data); + MOZ_ALWAYS_SUCCEEDS(rv); + if (NS_FAILED(rv) || data.IsEmpty()) { + break; + } + rv = mHttp3Connection->ProcessInput(addr, data); + MOZ_ALWAYS_SUCCEEDS(rv); + if (NS_FAILED(rv)) { + break; + } + + LOG(("Http3Session::ProcessInput received=%zu", data.Length())); + mTotalBytesRead += data.Length(); + } +} + +nsresult Http3Session::ProcessTransactionRead(uint64_t stream_id) { + 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); +} + +nsresult Http3Session::ProcessTransactionRead(Http3StreamBase* stream) { + nsresult rv = stream->WriteSegments(); + + 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() { + 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)); + break; + } + + MOZ_RELEASE_ASSERT(stream->GetHttp3Stream(), + "This must be a Http3Stream"); + + stream->SetResponseHeaders(data, event.header_ready.fin, + event.header_ready.interim); + + rv = ProcessTransactionRead(stream); + + if (NS_FAILED(rv)) { + LOG(("Http3Session::ProcessEvents [this=%p] rv=%" PRIx32, this, + static_cast(rv))); + return rv; + } + + mUdpConn->NotifyDataRead(); + break; + } + case Http3Event::Tag::DataReadable: { + MOZ_ASSERT(mState == CONNECTED); + LOG(("Http3Session::ProcessEvents - DataReadable")); + uint64_t id = event.data_readable.stream_id; + + nsresult rv = ProcessTransactionRead(id); + + if (NS_FAILED(rv)) { + LOG(("Http3Session::ProcessEvents [this=%p] rv=%" PRIx32, this, + static_cast(rv))); + return rv; + } + break; + } + case Http3Event::Tag::DataWritable: { + MOZ_ASSERT(CanSendData()); + 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 %p - Reset", this)); + ResetOrStopSendingRecvd(event.reset.stream_id, event.reset.error, + RESET); + break; + case Http3Event::Tag::StopSending: + LOG( + ("Http3Session::ProcessEvents %p - StopSeniding with error " + "0x%" PRIx64, + this, 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) { + RefPtr httpStream = stream->GetHttp3Stream(); + MOZ_RELEASE_ASSERT(httpStream, "This must be a Http3Stream"); + httpStream->StopSending(); + } + } else { + ResetOrStopSendingRecvd(event.reset.stream_id, event.reset.error, + STOP_SENDING); + } + 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(Nothing()); + } + break; + case Http3Event::Tag::ZeroRttRejected: + LOG(("Http3Session::ProcessEvents - ZeroRttRejected")); + if (mState == ZERORTT) { + mState = INITIALIZING; + mTransactionCount = 0; + Finish0Rtt(true); + ZeroRttTelemetry(ZeroRttOutcome::USED_REJECTED); + } + break; + case Http3Event::Tag::ResumptionToken: { + LOG(("Http3Session::ProcessEvents - ResumptionToken")); + if (StaticPrefs::network_http_http3_enable_0rtt() && !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); + ZeroRttTelemetry(ZeroRttOutcome::USED_SUCCEEDED); + } + + 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(); + // Maybe call ResumeSend: + // In case ZeroRtt has been used and it has been rejected, 2 events will + // be received: ZeroRttRejected and ConnectionConnected. ZeroRttRejected + // that will put all transaction into mReadyForWrite queue and it will + // call MaybeResumeSend, but that will not have an effect because the + // connection is ont in CONNECTED state. When ConnectionConnected event + // is received call MaybeResumeSend to trigger reads for the + // zero-rtt-rejected transactions. + MaybeResumeSend(); + } break; + case Http3Event::Tag::GoawayReceived: + LOG(("Http3Session::ProcessEvents - GoawayReceived")); + mUdpConn->SetCloseReason(ConnectionCloseReason::GO_AWAY); + 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); + + auto isStatelessResetOrNoError = [](CloseError& aError) -> bool { + if (aError.tag == CloseError::Tag::TransportInternalErrorOther && + aError.transport_internal_error_other._0 == + TRANSPORT_ERROR_STATELESS_RESET) { + return true; + } + if (aError.tag == CloseError::Tag::TransportError && + aError.transport_error._0 == 0) { + return true; + } + if (aError.tag == CloseError::Tag::PeerError && + aError.peer_error._0 == 0) { + return true; + } + if (aError.tag == CloseError::Tag::AppError && + aError.app_error._0 == HTTP3_APP_ERROR_NO_ERROR) { + return true; + } + if (aError.tag == CloseError::Tag::PeerAppError && + aError.peer_app_error._0 == HTTP3_APP_ERROR_NO_ERROR) { + return true; + } + return false; + }; + if (isStatelessResetOrNoError(event.connection_closing.error)) { + mError = NS_ERROR_NET_RESET; + } + if (event.connection_closing.error.tag == CloseError::Tag::EchRetry) { + mSocketControl->SetRetryEchConfig(Substring( + reinterpret_cast(data.Elements()), data.Length())); + mError = psm::GetXPCOMFromNSSError(SSL_ERROR_ECH_RETRY_WITH_ECH); + } + } + return mError; + break; + case Http3Event::Tag::ConnectionClosed: + LOG(("Http3Session::ProcessEvents - ConnectionClosed")); + if (NS_SUCCEEDED(mError)) { + mError = NS_ERROR_NET_TIMEOUT; + mUdpConn->SetCloseReason(ConnectionCloseReason::IDLE_TIMEOUT); + CloseConnectionTelemetry(event.connection_closed.error, false); + } + mIsClosedByNeqo = true; + if (event.connection_closed.error.tag == CloseError::Tag::EchRetry) { + mSocketControl->SetRetryEchConfig(Substring( + reinterpret_cast(data.Elements()), data.Length())); + mError = psm::GetXPCOMFromNSSError(SSL_ERROR_ECH_RETRY_WITH_ECH); + } + LOG(("Http3Session::ProcessEvents - ConnectionClosed error=%" PRIx32, + static_cast(mError))); + // We need to return here and let HttpConnectionUDP close the session. + return mError; + break; + case Http3Event::Tag::EchFallbackAuthenticationNeeded: { + nsCString echPublicName(reinterpret_cast(data.Elements()), + data.Length()); + LOG( + ("Http3Session::ProcessEvents - EchFallbackAuthenticationNeeded " + "echPublicName=%s", + echPublicName.get())); + if (!mAuthenticationStarted) { + mAuthenticationStarted = true; + CallCertVerification(Some(echPublicName)); + } + } break; + case Http3Event::Tag::WebTransport: { + switch (event.web_transport._0.tag) { + case WebTransportEventExternal::Tag::Negotiated: + LOG(("Http3Session::ProcessEvents - WebTransport %d", + event.web_transport._0.negotiated._0)); + MOZ_ASSERT(mWebTransportNegotiationStatus == + WebTransportNegotiation::NEGOTIATING); + mWebTransportNegotiationStatus = + event.web_transport._0.negotiated._0 + ? WebTransportNegotiation::SUCCEEDED + : WebTransportNegotiation::FAILED; + WebTransportNegotiationDone(); + break; + case WebTransportEventExternal::Tag::Session: { + MOZ_ASSERT(mState == CONNECTED); + + uint64_t id = event.web_transport._0.session._0; + LOG( + ("Http3Session::ProcessEvents - WebTransport Session " + " sessionId=0x%" PRIx64, + id)); + RefPtr stream = mStreamIdHash.Get(id); + if (!stream) { + LOG( + ("Http3Session::ProcessEvents - WebTransport Session - " + "stream not found " + "stream_id=0x%" PRIx64 " [this=%p].", + id, this)); + break; + } + + MOZ_RELEASE_ASSERT(stream->GetHttp3WebTransportSession(), + "It must be a WebTransport session"); + stream->SetResponseHeaders(data, false, false); + + rv = stream->WriteSegments(); + + 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.get(), stream->StreamId(), + static_cast(rv), stream->Done())); + // We need to keep the transaction, so we can use it to remove the + // stream from mStreamTransactionHash. + nsAHttpTransaction* trans = stream->Transaction(); + if (mStreamTransactionHash.Contains(trans)) { + CloseStream(stream, (rv == NS_BINDING_RETARGETED) + ? NS_BINDING_RETARGETED + : NS_OK); + mStreamTransactionHash.Remove(trans); + } else { + stream->GetHttp3WebTransportSession()->TransactionIsDone( + (rv == NS_BINDING_RETARGETED) ? NS_BINDING_RETARGETED + : NS_OK); + } + break; + } + + if (NS_FAILED(rv) && rv != NS_BASE_STREAM_WOULD_BLOCK) { + LOG(("Http3Session::ProcessEvents [this=%p] rv=%" PRIx32, this, + static_cast(rv))); + return rv; + } + } break; + case WebTransportEventExternal::Tag::SessionClosed: { + uint64_t id = event.web_transport._0.session_closed.stream_id; + LOG( + ("Http3Session::ProcessEvents - WebTransport SessionClosed " + " sessionId=0x%" PRIx64, + id)); + RefPtr stream = mStreamIdHash.Get(id); + if (!stream) { + LOG( + ("Http3Session::ProcessEvents - WebTransport SessionClosed - " + "stream not found " + "stream_id=0x%" PRIx64 " [this=%p].", + id, this)); + break; + } + + RefPtr wt = + stream->GetHttp3WebTransportSession(); + MOZ_RELEASE_ASSERT(wt, "It must be a WebTransport session"); + + bool cleanly = false; + + // TODO we do not handle the case when a WebTransport session stream + // is closed before headers are sent. + SessionCloseReasonExternal& reasonExternal = + event.web_transport._0.session_closed.reason; + uint32_t status = 0; + nsCString reason = ""_ns; + if (reasonExternal.tag == SessionCloseReasonExternal::Tag::Error) { + status = reasonExternal.error._0; + } else if (reasonExternal.tag == + SessionCloseReasonExternal::Tag::Status) { + status = reasonExternal.status._0; + cleanly = true; + } else { + status = reasonExternal.clean._0; + reason.Assign(reinterpret_cast(data.Elements()), + data.Length()); + cleanly = true; + } + LOG(("reason.tag=%u err=%u data=%s\n", + static_cast(reasonExternal.tag), status, + reason.get())); + wt->OnSessionClosed(cleanly, status, reason); + + } break; + case WebTransportEventExternal::Tag::NewStream: { + LOG( + ("Http3Session::ProcessEvents - WebTransport NewStream " + "streamId=0x%" PRIx64 " sessionId=0x%" PRIx64, + event.web_transport._0.new_stream.stream_id, + event.web_transport._0.new_stream.session_id)); + uint64_t sessionId = event.web_transport._0.new_stream.session_id; + RefPtr stream = mStreamIdHash.Get(sessionId); + if (!stream) { + LOG( + ("Http3Session::ProcessEvents - WebTransport NewStream - " + "session not found " + "sessionId=0x%" PRIx64 " [this=%p].", + sessionId, this)); + break; + } + + RefPtr wt = + stream->GetHttp3WebTransportSession(); + if (!wt) { + break; + } + + RefPtr wtStream = + wt->OnIncomingWebTransportStream( + event.web_transport._0.new_stream.stream_type, + event.web_transport._0.new_stream.stream_id); + if (!wtStream) { + break; + } + + // WebTransportStream is managed by Http3Session now. + mWebTransportStreams.AppendElement(wtStream); + mWebTransportStreamToSessionMap.InsertOrUpdate(wtStream->StreamId(), + wt->StreamId()); + mStreamIdHash.InsertOrUpdate(wtStream->StreamId(), + std::move(wtStream)); + } break; + case WebTransportEventExternal::Tag::Datagram: + LOG( + ("Http3Session::ProcessEvents - " + "WebTransportEventExternal::Tag::Datagram [this=%p]", + this)); + uint64_t sessionId = event.web_transport._0.datagram.session_id; + RefPtr stream = mStreamIdHash.Get(sessionId); + if (!stream) { + LOG( + ("Http3Session::ProcessEvents - WebTransport Datagram - " + "session not found " + "sessionId=0x%" PRIx64 " [this=%p].", + sessionId, this)); + break; + } + + RefPtr wt = + stream->GetHttp3WebTransportSession(); + if (!wt) { + break; + } + + wt->OnDatagramReceived(std::move(data)); + break; + } + } 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::SendData -> +// Http3Session::SendData +nsresult Http3Session::ProcessOutput(nsIUDPSocket* socket) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + MOZ_ASSERT(mUdpConn); + + LOG(("Http3Session::ProcessOutput reader=%p, [this=%p]", mUdpConn.get(), + this)); + + mSocket = socket; + nsresult rv = mHttp3Connection->ProcessOutputAndSend( + this, + [](void* aContext, uint16_t aFamily, const uint8_t* aAddr, uint16_t aPort, + const uint8_t* aData, uint32_t aLength) { + Http3Session* self = (Http3Session*)aContext; + + uint32_t written = 0; + NetAddr addr; + if (NS_FAILED(RawBytesToNetAddr(aFamily, aAddr, aPort, &addr))) { + return NS_OK; + } + + LOG3( + ("Http3Session::ProcessOutput sending packet with %u bytes to %s " + "port=%d [this=%p].", + aLength, addr.ToString().get(), aPort, self)); + + nsresult rv = + self->mSocket->SendWithAddress(&addr, aData, aLength, &written); + + LOG(("Http3Session::ProcessOutput sending packet rv=%d osError=%d", + static_cast(rv), NS_FAILED(rv) ? PR_GetOSError() : 0)); + if (NS_FAILED(rv) && (rv != NS_BASE_STREAM_WOULD_BLOCK)) { + self->mSocketError = rv; + // If there was an error that is not NS_BASE_STREAM_WOULD_BLOCK + // return from here. We do not need to set a timer, because we + // will close the connection. + return rv; + } + self->mTotalBytesWritten += aLength; + self->mLastWriteTime = PR_IntervalNow(); + return NS_OK; + }, + [](void* aContext, uint64_t timeout) { + Http3Session* self = (Http3Session*)aContext; + self->SetupTimer(timeout); + }); + mSocket = nullptr; + return rv; +} + +// 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(nsIUDPSocket* socket) { + 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 = SendData(socket); + if (NS_FAILED(rv)) { + return rv; + } + return ProcessEvents(); +} + +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; + } + + LOG3( + ("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; + } + + nsresult rv = NS_NewTimerWithCallback( + getter_AddRefs(mTimer), + [conn = RefPtr{mUdpConn}](nsITimer*) { conn->OnQuicTimeoutExpired(); }, + aTimeout, nsITimer::TYPE_ONE_SHOT, + "net::HttpConnectionUDP::OnQuicTimeout"); + + mTimerActive = true; + + if (NS_FAILED(rv)) { + NS_DispatchToCurrentThread( + NewRunnableMethod("net::HttpConnectionUDP::OnQuicTimeoutExpired", + mUdpConn, &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(); + // reset the read timers to wash away any idle time + mLastWriteTime = PR_IntervalNow(); + + ClassOfService cos; + if (trans) { + cos = trans->GetClassOfService(); + } + + Http3StreamBase* stream = nullptr; + + if (trans && trans->IsForWebTransport()) { + LOG3(("Http3Session::AddStream new WeTransport session %p atrans=%p.\n", + this, aHttpTransaction)); + stream = new Http3WebTransportSession(aHttpTransaction, this); + mHasWebTransportSession = true; + } else { + LOG3(("Http3Session::AddStream %p atrans=%p.\n", this, aHttpTransaction)); + stream = new Http3Stream(aHttpTransaction, this, cos, mCurrentBrowserId); + } + + mStreamTransactionHash.InsertOrUpdate(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); + } + if ((mWebTransportNegotiationStatus == + WebTransportNegotiation::NEGOTIATING) && + (trans && trans->IsForWebTransport())) { + LOG(("waiting for negotiation")); + mWaitingForWebTransportNegotiation.AppendElement(stream); + } + return true; + } + m0RTTStreams.AppendElement(stream); + } + + if ((mWebTransportNegotiationStatus == + WebTransportNegotiation::NEGOTIATING) && + (trans && trans->IsForWebTransport())) { + LOG(("waiting for negotiation")); + mWaitingForWebTransportNegotiation.AppendElement(stream); + return true; + } + + if (!mFirstHttpTransaction && !IsConnected()) { + mFirstHttpTransaction = aHttpTransaction->QueryHttpTransaction(); + LOG3(("Http3Session::AddStream first session=%p trans=%p ", this, + mFirstHttpTransaction.get())); + } + StreamReadyToWrite(stream); + + return true; +} + +bool Http3Session::CanReuse() { + // TODO: we assume "pooling" is disabled here, so we don't allow this session + // to be reused. "pooling" will be implemented in bug 1815735. + return CanSendData() && !(mGoawayReceived || mShouldClose) && + !mHasWebTransportSession; +} + +void Http3Session::QueueStream(Http3StreamBase* 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"); + + RefPtr stream; + while ((stream = mQueuedStreams.PopFront())) { + LOG3(("Http3Session::ProcessPending %p stream %p woken from queue.", this, + stream.get())); + MOZ_ASSERT(stream->Queued()); + stream->SetQueued(false); + mReadyForWrite.Push(stream); + } + MaybeResumeSend(); +} + +static void RemoveStreamFromQueue(Http3StreamBase* aStream, + nsRefPtrDeque& queue) { + size_t size = queue.GetSize(); + for (size_t count = 0; count < size; ++count) { + RefPtr stream = queue.PopFront(); + if (stream != aStream) { + queue.Push(stream); + } + } +} + +void Http3Session::RemoveStreamFromQueues(Http3StreamBase* 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, Http3StreamBase* 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 = NS_OK; + RefPtr httpStream = aStream->GetHttp3Stream(); + if (httpStream) { + rv = mHttp3Connection->Fetch( + aMethod, aScheme, aAuthorityHeader, aPath, aHeaders, aStreamId, + httpStream->PriorityUrgency(), httpStream->PriorityIncremental()); + } else { + MOZ_RELEASE_ASSERT(aStream->GetHttp3WebTransportSession(), + "It must be a WebTransport session"); + // Don't call CreateWebTransport if we are still waiting for the negotiation + // result. + if (mWebTransportNegotiationStatus == + WebTransportNegotiation::NEGOTIATING) { + if (!mWaitingForWebTransportNegotiation.Contains(aStream)) { + mWaitingForWebTransportNegotiation.AppendElement(aStream); + } + return NS_BASE_STREAM_WOULD_BLOCK; + } + rv = mHttp3Connection->CreateWebTransport(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()) { + MOZ_ASSERT(mConnectionIdleStart); + MOZ_ASSERT(mFirstStreamIdReuseIdleConnection.isNothing()); + + mConnectionIdleEnd = TimeStamp::Now(); + mFirstStreamIdReuseIdleConnection = Some(*aStreamId); + } + mStreamIdHash.InsertOrUpdate(*aStreamId, RefPtr{aStream}); + mTransactionCount++; + + return NS_OK; +} + +// This is called by Http3WebTransportStream::OnReadSegment. +// TODO: this function is almost the same as TryActivating(). +// We should try to reduce the duplicate code. +nsresult Http3Session::TryActivatingWebTransportStream( + uint64_t* aStreamId, Http3StreamBase* aStream) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + MOZ_ASSERT(*aStreamId == UINT64_MAX); + + LOG( + ("Http3Session::TryActivatingWebTransportStream [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::TryActivatingWebTransportStream %p stream=%p already " + "queued.\n", + this, aStream)); + return NS_BASE_STREAM_WOULD_BLOCK; + } + + nsresult rv = NS_OK; + RefPtr wtStream = + aStream->GetHttp3WebTransportStream(); + MOZ_RELEASE_ASSERT(wtStream, "It must be a WebTransport stream"); + rv = mHttp3Connection->CreateWebTransportStream( + wtStream->SessionId(), wtStream->StreamType(), aStreamId); + + if (NS_FAILED(rv)) { + LOG(( + "Http3Session::TryActivatingWebTransportStream returns error=0x%" PRIx32 + "[stream=%p, " + "this=%p]", + static_cast(rv), aStream, this)); + if (rv == NS_BASE_STREAM_WOULD_BLOCK) { + LOG3( + ("Http3Session::TryActivatingWebTransportStream %p stream=%p no room " + "for more " + "concurrent streams\n", + this, aStream)); + QueueStream(aStream); + return rv; + } + + return rv; + } + + LOG(("Http3Session::TryActivatingWebTransportStream streamId=0x%" PRIx64 + " for stream=%p [this=%p].", + *aStreamId, aStream, this)); + + MOZ_ASSERT(*aStreamId != UINT64_MAX); + + RefPtr session = mStreamIdHash.Get(wtStream->SessionId()); + MOZ_ASSERT(session); + Http3WebTransportSession* wtSession = session->GetHttp3WebTransportSession(); + MOZ_ASSERT(wtSession); + + wtSession->RemoveWebTransportStream(wtStream); + + // WebTransportStream is managed by Http3Session now. + mWebTransportStreams.AppendElement(wtStream); + mWebTransportStreamToSessionMap.InsertOrUpdate(*aStreamId, + session->StreamId()); + mStreamIdHash.InsertOrUpdate(*aStreamId, std::move(wtStream)); + 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::ResetOrStopSendingRecvd(uint64_t aStreamId, uint64_t aError, + ResetType aType) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + uint64_t sessionId = 0; + if (mWebTransportStreamToSessionMap.Get(aStreamId, &sessionId)) { + uint8_t wtError = Http3ErrorToWebTransportError(aError); + nsresult rv = GetNSResultFromWebTransportError(wtError); + + RefPtr stream = mStreamIdHash.Get(aStreamId); + if (stream) { + if (aType == RESET) { + stream->SetRecvdReset(); + } + RefPtr wtStream = + stream->GetHttp3WebTransportStream(); + if (wtStream) { + CloseWebTransportStream(wtStream, rv); + } + } + + RefPtr session = mStreamIdHash.Get(sessionId); + if (session) { + Http3WebTransportSession* wtSession = + session->GetHttp3WebTransportSession(); + MOZ_ASSERT(wtSession); + if (wtSession) { + if (aType == RESET) { + wtSession->OnStreamReset(aStreamId, rv); + } else { + wtSession->OnStreamStopSending(aStreamId, rv); + } + } + } + return; + } + + RefPtr stream = mStreamIdHash.Get(aStreamId); + if (!stream) { + return; + } + + RefPtr httpStream = stream->GetHttp3Stream(); + if (!httpStream) { + return; + } + + // 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. + httpStream->Transaction()->DisableHttp3(false); + httpStream->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. + httpStream->Transaction()->DoNotRemoveAltSvc(); + CloseStream(stream, NS_ERROR_NET_RESET); + } else { + if (httpStream->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) { + MOZ_ASSERT(false, "Http3Session::ReadSegments()"); + return NS_ERROR_UNEXPECTED; +} + +nsresult Http3Session::SendData(nsIUDPSocket* socket) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + LOG(("Http3Session::SendData [this=%p]", this)); + + // 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). + // 4) + + nsresult rv = NS_OK; + RefPtr stream; + + // Step 1) + while (CanSendData() && (stream = mReadyForWrite.PopFront())) { + LOG(("Http3Session::SendData call ReadSegments from stream=%p [this=%p]", + stream.get(), this)); + + rv = stream->ReadSegments(); + + // on stream error we return earlier to let the error be handled. + if (NS_FAILED(rv)) { + LOG3(("Http3Session::SendData %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::SendData %p soft error override\n", this)); + rv = NS_OK; + } else { + break; + } + } + } + + if (NS_SUCCEEDED(rv)) { + // Step 2: + // Call actual network write. + rv = ProcessOutput(socket); + } + + // Step 3: + MaybeResumeSend(); + + if (rv == NS_BASE_STREAM_WOULD_BLOCK) { + rv = NS_OK; + } + + // Let the connection know we sent some app data successfully. + if (stream && NS_SUCCEEDED(rv)) { + mUdpConn->NotifyDataWrite(); + } + + return rv; +} + +void Http3Session::StreamReadyToWrite(Http3StreamBase* aStream) { + MOZ_ASSERT(aStream); + mReadyForWrite.Push(aStream); + if (CanSendData() && mConnection) { + Unused << mConnection->ResumeSend(); + } +} + +void Http3Session::MaybeResumeSend() { + if ((mReadyForWrite.GetSize() > 0) && CanSendData() && mConnection) { + Unused << mConnection->ResumeSend(); + } +} + +nsresult Http3Session::ProcessSlowConsumers() { + if (mSlowConsumersReadyForRead.IsEmpty()) { + return NS_OK; + } + + RefPtr slowConsumer = + mSlowConsumersReadyForRead.ElementAt(0); + mSlowConsumersReadyForRead.RemoveElementAt(0); + + nsresult rv = ProcessTransactionRead(slowConsumer); + + return rv; +} + +nsresult Http3Session::WriteSegments(nsAHttpSegmentWriter* writer, + uint32_t count, uint32_t* countWritten) { + MOZ_ASSERT(false, "Http3Session::WriteSegments()"); + return NS_ERROR_UNEXPECTED; +} + +nsresult Http3Session::RecvData(nsIUDPSocket* socket) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + // Process slow consumers. + nsresult rv = ProcessSlowConsumers(); + if (NS_FAILED(rv)) { + LOG3(("Http3Session %p ProcessSlowConsumers returns 0x%" PRIx32 "\n", this, + static_cast(rv))); + return rv; + } + + ProcessInput(socket); + + rv = ProcessEvents(); + if (NS_FAILED(rv)) { + return rv; + } + + rv = SendData(socket); + if (NS_FAILED(rv)) { + return rv; + } + + return NS_OK; +} + +const uint32_t HTTP3_TELEMETRY_APP_NECKO = 42; + +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 the "closing" key and the + // value HTTP3_TELEMETRY_APP_NECKO. + Telemetry::Accumulate(Telemetry::HTTP3_CONNECTION_CLOSE_CODE_3, + "app_closing"_ns, HTTP3_TELEMETRY_APP_NECKO); + 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(); + } + mTimer = nullptr; + mConnection = nullptr; + mUdpConn = 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; + } + + if (mState == ZERORTT) { + ZeroRttTelemetry(aCallNeqoClose ? ZeroRttOutcome::USED_CONN_CLOSED_BY_NECKO + : ZeroRttOutcome::USED_CONN_ERROR); + } + + 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(Http3StreamBase* aStream, nsresult aResult) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + RefPtr wtStream = + aStream->GetHttp3WebTransportStream(); + if (wtStream) { + CloseWebTransportStream(wtStream, aResult); + return; + } + + RefPtr httpStream = aStream->GetHttp3Stream(); + if (httpStream && !httpStream->RecvdFin() && !httpStream->RecvdReset() && + httpStream->HasStreamId()) { + mHttp3Connection->CancelFetch(httpStream->StreamId(), + HTTP3_APP_ERROR_REQUEST_CANCELLED); + } + + aStream->Close(aResult); + CloseStreamInternal(aStream, aResult); +} + +void Http3Session::CloseStreamInternal(Http3StreamBase* aStream, + nsresult aResult) { + LOG3(("Http3Session::CloseStreamInternal %p %p 0x%" PRIx32, this, aStream, + static_cast(aResult))); + if (aStream->HasStreamId()) { + // We know the transaction reusing an idle connection has succeeded or + // failed. + if (mFirstStreamIdReuseIdleConnection.isSome() && + aStream->StreamId() == *mFirstStreamIdReuseIdleConnection) { + 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); + if (nsAHttpTransaction* transaction = aStream->Transaction()) { + mStreamTransactionHash.Remove(transaction); + } + mWebTransportSessions.RemoveElement(aStream); + mWebTransportStreams.RemoveElement(aStream); + // Close(NS_OK) implies that the NeqoHttp3Conn will be closed, so we can only + // do this when there is no Http3Steeam, WebTransportSession and + // WebTransportStream. + if ((mShouldClose || mGoawayReceived) && + (!mStreamTransactionHash.Count() && mWebTransportSessions.IsEmpty() && + mWebTransportStreams.IsEmpty())) { + MOZ_ASSERT(!IsClosing()); + Close(NS_OK); + } +} + +void Http3Session::CloseWebTransportStream(Http3WebTransportStream* aStream, + nsresult aResult) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG3(("Http3Session::CloseWebTransportStream %p %p 0x%" PRIx32, this, aStream, + static_cast(aResult))); + if (aStream && !aStream->RecvdFin() && !aStream->RecvdReset() && + (aStream->HasStreamId())) { + mHttp3Connection->ResetStream(aStream->StreamId(), + HTTP3_APP_ERROR_REQUEST_CANCELLED); + } + + aStream->Close(aResult); + CloseStreamInternal(aStream, aResult); +} + +void Http3Session::ResetWebTransportStream(Http3WebTransportStream* aStream, + uint64_t aErrorCode) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG3(("Http3Session::ResetWebTransportStream %p %p 0x%" PRIx64, this, aStream, + aErrorCode)); + mHttp3Connection->ResetStream(aStream->StreamId(), aErrorCode); +} + +void Http3Session::StreamStopSending(Http3WebTransportStream* aStream, + uint8_t aErrorCode) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG(("Http3Session::StreamStopSending %p %p 0x%" PRIx32, this, aStream, + static_cast(aErrorCode))); + mHttp3Connection->StreamStopSending(aStream->StreamId(), aErrorCode); +} + +nsresult Http3Session::TakeTransport(nsISocketTransport**, + nsIAsyncInputStream**, + nsIAsyncOutputStream**) { + MOZ_ASSERT(false, "TakeTransport of Http3Session"); + return NS_ERROR_UNEXPECTED; +} + +Http3WebTransportSession* Http3Session::GetWebTransportSession( + nsAHttpTransaction* aTransaction) { + RefPtr stream = mStreamTransactionHash.Get(aTransaction); + + if (!stream || !stream->GetHttp3WebTransportSession()) { + MOZ_ASSERT(false, "There must be a stream"); + return nullptr; + } + RemoveStreamFromQueues(stream); + mStreamTransactionHash.Remove(aTransaction); + mWebTransportSessions.AppendElement(stream); + return stream->GetHttp3WebTransportSession(); +} + +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::CloseWebTransportConn() { + LOG3(("Http3Session::CloseWebTransportConn %p\n", this)); + // We need to dispatch, since Http3Session could be released in + // HttpConnectionUDP::CloseTransaction. + gSocketTransportService->Dispatch( + NS_NewRunnableFunction("Http3Session::CloseWebTransportConn", + [self = RefPtr{this}]() { + if (self->mUdpConn) { + self->mUdpConn->CloseTransaction( + self, NS_ERROR_ABORT); + } + }), + NS_DISPATCH_NORMAL); +} + +void Http3Session::CurrentBrowserIdChanged(uint64_t id) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + mCurrentBrowserId = id; + + for (const auto& stream : mStreamTransactionHash.Values()) { + RefPtr httpStream = stream->GetHttp3Stream(); + if (httpStream) { + httpStream->CurrentBrowserIdChanged(id); + } + } +} + +// 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())); + + StreamHasDataToWrite(stream); +} + +void Http3Session::StreamHasDataToWrite(Http3StreamBase* aStream) { + if (!IsClosing()) { + StreamReadyToWrite(aStream); + } 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(Http3StreamBase* 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 || !CanSendData() || 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 sslSocketControl; + mConnection->GetTLSSocketControl(getter_AddRefs(sslSocketControl)); + if (!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.InsertOrUpdate(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.InsertOrUpdate(key2, joinedReturn); + } + } + return joinedReturn; +} + +void Http3Session::CallCertVerification(Maybe aEchPublicName) { + 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)); + } + + uint32_t providerFlags; + // the return value is always NS_OK, just ignore it. + Unused << mSocketControl->GetProviderFlags(&providerFlags); + + nsCString echConfig; + nsresult nsrv = mSocketControl->GetEchConfig(echConfig); + bool verifyToEchPublicName = NS_SUCCEEDED(nsrv) && !echConfig.IsEmpty() && + aEchPublicName && !aEchPublicName->IsEmpty(); + const nsACString& hostname = + verifyToEchPublicName ? *aEchPublicName : mSocketControl->GetHostName(); + + SECStatus rv = psm::AuthCertificateHookWithInfo( + mSocketControl, hostname, 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", + mUdpConn, &HttpConnectionUDP::OnQuicTimeoutExpired)); + mUdpConn->ChangeConnectionState(ConnectionState::TRANSFERING); + } +} + +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->SetNegotiatedNPN(secInfo.alpn); + + mSocketControl->SetInfo(secInfo.cipher, secInfo.version, secInfo.group, + secInfo.signature_scheme, secInfo.ech_accepted); + mHandshakeSucceeded = true; + } + + if (!mSocketControl->HasServerCert()) { + mSocketControl->RebuildCertificateInfoFromSSLTokenCache(); + } +} + +// Transport error have values from 0x0 to 0x11. +// (https://tools.ietf.org/html/draft-ietf-quic-transport-34#section-20.1) +// We will map this error to 0-16. +// 17 will capture error codes between and including 0x12 and 0x0ff. This +// error codes are not define by the spec but who know peer may sent them. +// CryptoAlerts have value 0x100 + alert code. The range of alert code is +// 0x00-0xff. (https://tools.ietf.org/html/draft-ietf-quic-tls_34#section-4.8) +// Since telemetry does not allow more than 100 bucket, we use three diffrent +// keys to map all alert codes. +const uint32_t HTTP3_TELEMETRY_TRANSPORT_INTERNAL_ERROR = 15; +const uint32_t HTTP3_TELEMETRY_TRANSPORT_END = 16; +const uint32_t HTTP3_TELEMETRY_TRANSPORT_UNKNOWN = 17; +const uint32_t HTTP3_TELEMETRY_TRANSPORT_CRYPTO_UNKNOWN = 18; +// All errors from CloseError::Tag::CryptoError will be map to 19 +const uint32_t HTTP3_TELEMETRY_CRYPTO_ERROR = 19; + +uint64_t GetCryptoAlertCode(nsCString& key, uint64_t error) { + if (error < 100) { + key.Append("_a"_ns); + return error; + } + if (error < 200) { + error -= 100; + key.Append("_b"_ns); + return error; + } + if (error < 256) { + error -= 200; + key.Append("_c"_ns); + return error; + } + return HTTP3_TELEMETRY_TRANSPORT_CRYPTO_UNKNOWN; +} + +uint64_t GetTransportErrorCodeForTelemetry(nsCString& key, uint64_t error) { + if (error <= HTTP3_TELEMETRY_TRANSPORT_END) { + return error; + } + if (error < 0x100) { + return HTTP3_TELEMETRY_TRANSPORT_UNKNOWN; + } + + return GetCryptoAlertCode(key, error - 0x100); +} + +// Http3 error codes are 0x100-0x110. +// (https://tools.ietf.org/html/draft-ietf-quic-http-33#section-8.1) +// The mapping is described below. +// 0x00-0x10 mapped to 0-16 +// 0x11-0xff mapped to 17 +// 0x100-0x110 mapped to 18-36 +// 0x111-0x1ff mapped to 37 +// 0x200-0x202 mapped to 38-40 +// Others mapped to 41 +const uint32_t HTTP3_TELEMETRY_APP_UNKNOWN_1 = 17; +const uint32_t HTTP3_TELEMETRY_APP_START = 18; +// Values between 0x111 and 0x1ff are no definded and will be map to 18. +const uint32_t HTTP3_TELEMETRY_APP_UNKNOWN_2 = 37; +// Error codes between 0x200 and 0x202 are related to qpack. +// (https://tools.ietf.org/html/draft-ietf-quic-qpack-20#section-6) +// They will be mapped to 19-21 +const uint32_t HTTP3_TELEMETRY_APP_QPACK_START = 38; +// Values greater or equal to 0x203 are no definded and will be map to 41. +const uint32_t HTTP3_TELEMETRY_APP_UNKNOWN_3 = 41; + +uint64_t GetAppErrorCodeForTelemetry(uint64_t error) { + if (error <= 0x10) { + return error; + } + if (error <= 0xff) { + return HTTP3_TELEMETRY_APP_UNKNOWN_1; + } + if (error <= 0x110) { + return error - 0x100 + HTTP3_TELEMETRY_APP_START; + } + if (error < 0x200) { + return HTTP3_TELEMETRY_APP_UNKNOWN_2; + } + if (error <= 0x202) { + return error - 0x200 + HTTP3_TELEMETRY_APP_QPACK_START; + } + return HTTP3_TELEMETRY_APP_UNKNOWN_3; +} + +void Http3Session::CloseConnectionTelemetry(CloseError& aError, bool aClosing) { + uint64_t value = 0; + nsCString key = EmptyCString(); + + switch (aError.tag) { + case CloseError::Tag::TransportInternalError: + key = "transport_internal"_ns; + value = HTTP3_TELEMETRY_TRANSPORT_INTERNAL_ERROR; + break; + case CloseError::Tag::TransportInternalErrorOther: + key = "transport_other"_ns; + value = aError.transport_internal_error_other._0; + break; + case CloseError::Tag::TransportError: + key = "transport"_ns; + value = GetTransportErrorCodeForTelemetry(key, aError.transport_error._0); + break; + case CloseError::Tag::CryptoError: + key = "transport"_ns; + value = HTTP3_TELEMETRY_CRYPTO_ERROR; + break; + case CloseError::Tag::CryptoAlert: + key = "transport_crypto_alert"_ns; + value = GetCryptoAlertCode(key, aError.crypto_alert._0); + break; + case CloseError::Tag::PeerAppError: + key = "peer_app"_ns; + value = GetAppErrorCodeForTelemetry(aError.peer_app_error._0); + break; + case CloseError::Tag::PeerError: + key = "peer_transport"_ns; + value = GetTransportErrorCodeForTelemetry(key, aError.peer_error._0); + break; + case CloseError::Tag::AppError: + key = "app"_ns; + value = GetAppErrorCodeForTelemetry(aError.app_error._0); + break; + case CloseError::Tag::EchRetry: + key = "transport_crypto_alert"_ns; + value = 100; + } + + MOZ_DIAGNOSTIC_ASSERT(value <= 100); + + key.Append(aClosing ? "_closing"_ns : "_closed"_ns); + + Telemetry::Accumulate(Telemetry::HTTP3_CONNECTION_CLOSE_CODE_3, key, 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 (CanSendData() && !mHttp3ConnectionReported) { + mHttp3ConnectionReported = true; + gHttpHandler->ConnMgr()->ReportHttp3Connection(mUdpConn); + MaybeResumeSend(); + } +} + +void Http3Session::EchOutcomeTelemetry() { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + nsAutoCString key; + switch (mEchExtensionStatus) { + case EchExtensionStatus::kNotPresent: + key = "NONE"; + break; + case EchExtensionStatus::kGREASE: + key = "GREASE"; + break; + case EchExtensionStatus::kReal: + key = "REAL"; + break; + } + + Telemetry::Accumulate(Telemetry::HTTP3_ECH_OUTCOME, key, + mHandshakeSucceeded ? 0 : 1); +} + +void Http3Session::ZeroRttTelemetry(ZeroRttOutcome aOutcome) { + Telemetry::Accumulate(Telemetry::HTTP3_0RTT_STATE, aOutcome); + + nsAutoCString key; + + switch (aOutcome) { + case USED_SUCCEEDED: + key = "succeeded"_ns; + break; + case USED_REJECTED: + key = "rejected"_ns; + break; + case USED_CONN_ERROR: + key = "conn_error"_ns; + break; + case USED_CONN_CLOSED_BY_NECKO: + key = "conn_closed_by_necko"_ns; + break; + default: + break; + } + + if (!key.IsEmpty()) { + MOZ_ASSERT(mZeroRttStarted); + Telemetry::AccumulateTimeDelta(Telemetry::HTTP3_0RTT_STATE_DURATION, key, + mZeroRttStarted, TimeStamp::Now()); + } +} + +nsresult Http3Session::GetTransactionTLSSocketControl( + nsITLSSocketControl** tlsSocketControl) { + NS_IF_ADDREF(*tlsSocketControl = mSocketControl); + return NS_OK; +} + +PRIntervalTime Http3Session::LastWriteTime() { return mLastWriteTime; } + +void Http3Session::WebTransportNegotiationDone() { + for (size_t i = 0; i < mWaitingForWebTransportNegotiation.Length(); ++i) { + if (mWaitingForWebTransportNegotiation[i]) { + mReadyForWrite.Push(mWaitingForWebTransportNegotiation[i]); + } + } + mWaitingForWebTransportNegotiation.Clear(); + MaybeResumeSend(); +} + +//========================================================================= +// WebTransport +//========================================================================= + +nsresult Http3Session::CloseWebTransport(uint64_t aSessionId, uint32_t aError, + const nsACString& aMessage) { + return mHttp3Connection->CloseWebTransport(aSessionId, aError, aMessage); +} + +nsresult Http3Session::CreateWebTransportStream( + uint64_t aSessionId, WebTransportStreamType aStreamType, + uint64_t* aStreamId) { + return mHttp3Connection->CreateWebTransportStream(aSessionId, aStreamType, + aStreamId); +} + +void Http3Session::SendDatagram(Http3WebTransportSession* aSession, + nsTArray& aData, + uint64_t aTrackingId) { + nsresult rv = mHttp3Connection->WebTransportSendDatagram(aSession->StreamId(), + aData, aTrackingId); + LOG(("Http3Session::SendDatagram %p res=%" PRIx32, this, + static_cast(rv))); + if (!aTrackingId) { + return; + } + + switch (rv) { + case NS_OK: + aSession->OnOutgoingDatagramOutCome( + aTrackingId, WebTransportSessionEventListener::DatagramOutcome::SENT); + break; + case NS_ERROR_NOT_AVAILABLE: + aSession->OnOutgoingDatagramOutCome( + aTrackingId, WebTransportSessionEventListener::DatagramOutcome:: + DROPPED_TOO_MUCH_DATA); + break; + default: + aSession->OnOutgoingDatagramOutCome( + aTrackingId, + WebTransportSessionEventListener::DatagramOutcome::UNKNOWN); + break; + } +} + +uint64_t Http3Session::MaxDatagramSize(uint64_t aSessionId) { + uint64_t size = 0; + Unused << mHttp3Connection->WebTransportMaxDatagramSize(aSessionId, &size); + return size; +} + +void Http3Session::SetSendOrder(Http3StreamBase* aStream, + Maybe aSendOrder) { + if (!IsClosing()) { + nsresult rv = mHttp3Connection->WebTransportSetSendOrder( + aStream->StreamId(), aSendOrder); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + Unused << rv; + } +} + +} // namespace mozilla::net diff --git a/netwerk/protocol/http/Http3Session.h b/netwerk/protocol/http/Http3Session.h new file mode 100644 index 0000000000..c72f24959e --- /dev/null +++ b/netwerk/protocol/http/Http3Session.h @@ -0,0 +1,388 @@ +/* -*- 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 "HttpTrafficAnalyzer.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/WeakPtr.h" +#include "mozilla/net/NeqoHttp3Conn.h" +#include "nsAHttpConnection.h" +#include "nsDeque.h" +#include "nsISupportsImpl.h" +#include "nsITimer.h" +#include "nsIUDPSocket.h" +#include "nsRefPtrHashtable.h" +#include "nsTHashMap.h" +#include "nsWeakReference.h" + +/* + * WebTransport + * + * Http3Session and the underlying neqo code support multiplexing of multiple + * WebTransport and multiplexing WebTransport sessions with regular HTTP + * traffic. Whether WebTransport sessions are polled, will be controlled by the + * nsHttpConnectionMgr. + * + * WebTransport support is negotiated using HTTP/3 setting. Before the settings + * are available all WebTransport transactions are queued in + * mWaitingForWebTransportNegotiation. The information on whether WebTransport + * is supported is received via an HTTP/3 event. The event is + * Http3Event::Tag::WebTransport with the value + * WebTransportEventExternal::Tag::Negotiated that can be true or false. If + * the WebTransport feature has been negotiated, queued transactions will be + * activated otherwise they will be canceled(i.e. WebTransportNegotiationDone). + * + * The lifetime of a WebTransport session + * + * A WebTransport lifetime consists of 2 parts: + * - WebTransport session setup + * - WebTransport session active time + * + * WebTransport session setup: + * A WebTransport session uses a regular HTTP request for negotiation. + * Therefore when a new WebTransport is started a nsHttpChannel and the + * corresponding nsHttpTransaction are created. The nsHttpTransaction is + * dispatched to a Http3Session and after the + * WebTransportEventExternal::Tag::Negotiated event it behaves almost the same + * as a regular transaction, e.g. it is added to mStreamTransactionHash and + * mStreamIdHash. For activating the session NeqoHttp3Conn::CreateWebTransport + * is called instead of NeqoHttp3Conn::Fetch(this is called for the regular + * HTTP requests). In this phase, the WebTransport session is canceled in the + * same way a regular request is canceled, by canceling the corresponding + * nsHttpChannel. If HTTP/3 connection is closed in this phase the + * corresponding nsHttpTransaction is canceled and this may cause the + * transaction to be restarted (this is the existing restart logic) or the + * error is propagated to the nsHttpChannel and its listener(via OnStartRequest + * and OnStopRequest as the regular HTTP request). + * The phase ends when a connection breaks or when the event + * Http3Event::Tag::WebTransport with the value + * WebTransportEventExternal::Tag::Session is received. The parameter + * aData(from NeqoHttp3Conn::GetEvent) contain the HTTP head of the response. + * The headers may be: + * - failed code, i.e. anything except 200. In this case, the nsHttpTransaction + * behaves the same as a normal HTTP request and the nsHttpChannel listener + * will be informed via OnStartRequest and OnStopRequest calls. + * - success code, i.e. 200. The code will be propagated to the + * nsHttpTransaction. The transaction will parse the header and call + * Http3Session::GetWebTransportSession. The function transfers WebTransport + * session into the next phase: + * - Removes nsHttpTransaction from mStreamTransactionHash. + * - Adds the stream to mWebTransportSessions + * - The nsHttpTransaction supplies Http3WebTransportSession to the + * WebTransportSessionProxy and vice versa. + * TODO remove this circular referencing. + * + * WebTransport session active time: + * During this phase the following actions are possible: + * - Cancelling a WebTransport session by the application: + * The application calls Http3WebTransportSession::CloseSession. This + * transfers Http3WebTransportSession into the CLOSE_PENDING state and calls + * Http3Session::ConnectSlowConsumer to add itself to the “ready for reading + * queue”. Consequently, the Http3Session will call + * Http3WebTransportSession::WriteSegments and + * Http3Session::CloseWebTransport will be called to send the closing signal + * to the peer. After this, the Http3WebTransportSession is in the state DONE + * and it will be removed from the Http3Session(the CloseStream function + * takes care of this). + * - The peer sending a session closing signal: + * Http3Session will receive a Http3Event::Tag::WebTransport event with value + * WebTransportEventExternal::Tag::SessionClosed. The + * Http3WebTransportSession::OnSessionClosed function for the corresponding + * stream wil be called. The function will inform the corresponding + * WebTransportSessionProxy by calling OnSessionClosed function. The + * Http3WebTransportSession is in the state DONE and will be removed from the + * Http3Session(the CloseStream function takes care of this). + */ + +namespace mozilla::net { + +class HttpConnectionUDP; +class Http3StreamBase; +class QuicSocketControl; + +// IID for the Http3Session interface +#define NS_HTTP3SESSION_IID \ + { \ + 0x8fc82aaf, 0xc4ef, 0x46ed, { \ + 0x89, 0x41, 0x93, 0x95, 0x8f, 0xac, 0x4f, 0x21 \ + } \ + } + +enum class EchExtensionStatus { + kNotPresent, // No ECH Extension was sent + kGREASE, // A GREASE ECH Extension was sent + kReal // A 'real' ECH Extension was sent +}; + +class Http3Session final : public nsAHttpTransaction, public nsAHttpConnection { + public: + NS_DECLARE_STATIC_IID_ACCESSOR(NS_HTTP3SESSION_IID) + + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSAHTTPTRANSACTION + NS_DECL_NSAHTTPCONNECTION(mConnection) + + Http3Session(); + nsresult Init(const nsHttpConnectionInfo* aConnInfo, nsINetAddr* selfAddr, + nsINetAddr* peerAddr, HttpConnectionUDP* udpConn, + uint32_t controlFlags, nsIInterfaceRequestor* callbacks); + + bool IsConnected() const { return mState == CONNECTED; } + bool CanSendData() 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(); + + // The following functions are used by Http3Stream and + // Http3WebTransportSession: + nsresult TryActivating(const nsACString& aMethod, const nsACString& aScheme, + const nsACString& aAuthorityHeader, + const nsACString& aPath, const nsACString& aHeaders, + uint64_t* aStreamId, Http3StreamBase* aStream); + // The folowing functions are used by Http3Stream: + 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); + + // The folowing functions are used by Http3WebTransportSession: + nsresult CloseWebTransport(uint64_t aSessionId, uint32_t aError, + const nsACString& aMessage); + nsresult CreateWebTransportStream(uint64_t aSessionId, + WebTransportStreamType aStreamType, + uint64_t* aStreamId); + void CloseStream(Http3StreamBase* aStream, nsresult aResult); + void CloseStreamInternal(Http3StreamBase* 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; + [[nodiscard]] nsresult GetTransactionTLSSocketControl( + nsITLSSocketControl**) override; + + // This function will be called by QuicSocketControl when the certificate + // verification is done. + void Authenticated(int32_t aError); + + nsresult ProcessOutputAndEvents(nsIUDPSocket* socket); + + void ReportHttp3Connection(); + + int64_t GetBytesWritten() { return mTotalBytesWritten; } + int64_t BytesRead() { return mTotalBytesRead; } + + nsresult SendData(nsIUDPSocket* socket); + nsresult RecvData(nsIUDPSocket* socket); + + void DoSetEchConfig(const nsACString& aEchConfig); + + nsresult SendPriorityUpdateFrame(uint64_t aStreamId, uint8_t aPriorityUrgency, + bool aPriorityIncremental); + + void ConnectSlowConsumer(Http3StreamBase* stream); + + nsresult TryActivatingWebTransportStream(uint64_t* aStreamId, + Http3StreamBase* aStream); + void CloseWebTransportStream(Http3WebTransportStream* aStream, + nsresult aResult); + void StreamHasDataToWrite(Http3StreamBase* aStream); + void ResetWebTransportStream(Http3WebTransportStream* aStream, + uint64_t aErrorCode); + void StreamStopSending(Http3WebTransportStream* aStream, uint8_t aErrorCode); + + void SendDatagram(Http3WebTransportSession* aSession, + nsTArray& aData, uint64_t aTrackingId); + + uint64_t MaxDatagramSize(uint64_t aSessionId); + + void SetSendOrder(Http3StreamBase* aStream, Maybe aSendOrder); + + void CloseWebTransportConn(); + + private: + ~Http3Session(); + + void CloseInternal(bool aCallNeqoClose); + void Shutdown(); + + bool RealJoinConnection(const nsACString& hostname, int32_t port, + bool justKidding); + + nsresult ProcessOutput(nsIUDPSocket* socket); + void ProcessInput(nsIUDPSocket* socket); + nsresult ProcessEvents(); + + nsresult ProcessTransactionRead(uint64_t stream_id); + nsresult ProcessTransactionRead(Http3StreamBase* stream); + nsresult ProcessSlowConsumers(); + + void SetupTimer(uint64_t aTimeout); + + enum ResetType { + RESET, + STOP_SENDING, + }; + void ResetOrStopSendingRecvd(uint64_t aStreamId, uint64_t aError, + ResetType aType); + + void QueueStream(Http3StreamBase* stream); + void RemoveStreamFromQueues(Http3StreamBase*); + void ProcessPending(); + + void CallCertVerification(Maybe aEchPublicName); + void SetSecInfo(); + + void EchOutcomeTelemetry(); + + void StreamReadyToWrite(Http3StreamBase* aStream); + void MaybeResumeSend(); + + void CloseConnectionTelemetry(CloseError& aError, bool aClosing); + void Finish0Rtt(bool aRestart); + + enum ZeroRttOutcome { + NOT_USED, + USED_SUCCEEDED, + USED_REJECTED, + USED_CONN_ERROR, + USED_CONN_CLOSED_BY_NECKO + }; + void ZeroRttTelemetry(ZeroRttOutcome aOutcome); + + RefPtr mHttp3Connection; + RefPtr mConnection; + // We need an extra map to store the mapping of WebTransportSession and + // WebTransportStreams to handle the case that a stream is already removed + // from mStreamIdHash and we still need the WebTransportSession. + nsTHashMap mWebTransportStreamToSessionMap; + nsRefPtrHashtable mStreamIdHash; + nsRefPtrHashtable, Http3StreamBase> + mStreamTransactionHash; + + nsRefPtrDeque mReadyForWrite; + + nsTArray> mSlowConsumersReadyForRead; + nsRefPtrDeque mQueuedStreams; + + enum State { + INITIALIZING, + ZERORTT, + CONNECTED, + CLOSING, + CLOSED + } mState{INITIALIZING}; + + bool mAuthenticationStarted{false}; + bool mCleanShutdown{false}; + bool mGoawayReceived{false}; + bool mShouldClose{false}; + bool mIsClosedByNeqo{false}; + bool mHttp3ConnectionReported = false; + // mError is neqo error (a protocol error) and that may mean that we will + // send some packets after that. + nsresult mError{NS_OK}; + // This is a socket error, there is no poioint in sending anything on that + // socket. + nsresult mSocketError{NS_OK}; + bool mBeforeConnectedError{false}; + uint64_t mCurrentBrowserId; + + // True if the mTimer is inited and waiting for firing. + bool mTimerActive{false}; + + RefPtr mUdpConn; + + // The underlying socket transport object is needed to propogate some events + RefPtr mSocketTransport; + + nsCOMPtr mTimer; + + nsTHashMap mJoinConnectionCache; + + RefPtr mSocketControl; + + 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; + TimeStamp mZeroRttStarted; + 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; + + bool mThroughCaptivePortal = false; + int64_t mTotalBytesRead = 0; // total data read + int64_t mTotalBytesWritten = 0; // total data read + PRIntervalTime mLastWriteTime = 0; + + // Records whether we sent an ECH Extension and whether it was a GREASE Xtn + EchExtensionStatus mEchExtensionStatus = EchExtensionStatus::kNotPresent; + + // Records whether the handshake finished successfully and we established a + // a connection. + bool mHandshakeSucceeded = false; + + nsCOMPtr mNetAddr; + + enum WebTransportNegotiation { DISABLED, NEGOTIATING, FAILED, SUCCEEDED }; + WebTransportNegotiation mWebTransportNegotiationStatus{ + WebTransportNegotiation::DISABLED}; + + nsTArray> mWaitingForWebTransportNegotiation; + // 1795854 implement the case when WebTransport is not supported. + // Also, implement the case when the HTTP/3 session fails before settings + // are exchanged. + void WebTransportNegotiationDone(); + + nsTArray> mWebTransportSessions; + nsTArray> mWebTransportStreams; + + bool mHasWebTransportSession = false; + // When true, we don't add this connection info into the Http/3 excluded list. + bool mDontExclude = false; + // The lifetime of the UDP socket is managed by the HttpConnectionUDP. This + // is only used in Http3Session::ProcessOutput. Using raw pointer here to + // improve performance. + nsIUDPSocket* mSocket; +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(Http3Session, NS_HTTP3SESSION_IID); + +} // namespace mozilla::net + +#endif // Http3Session_H__ diff --git a/netwerk/protocol/http/Http3Stream.cpp b/netwerk/protocol/http/Http3Stream.cpp new file mode 100644 index 0000000000..4f33ca07e2 --- /dev/null +++ b/netwerk/protocol/http/Http3Stream.cpp @@ -0,0 +1,533 @@ +/* -*- 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 "nsHttpTransaction.h" +#include "nsIClassOfService.h" +#include "nsISocketTransport.h" +#include "nsSocketTransportService2.h" +#include "mozilla/StaticPrefs_network.h" +#include "mozilla/Telemetry.h" +#include "nsIOService.h" +#include "nsHttpHandler.h" + +#include + +namespace mozilla { +namespace net { + +Http3StreamBase::Http3StreamBase(nsAHttpTransaction* trans, + Http3Session* session) + : mTransaction(trans), mSession(session) {} + +Http3StreamBase::~Http3StreamBase() = default; + +Http3Stream::Http3Stream(nsAHttpTransaction* httpTransaction, + Http3Session* session, const ClassOfService& cos, + uint64_t currentBrowserId) + : Http3StreamBase(httpTransaction, session), + mCurrentBrowserId(currentBrowserId) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG3(("Http3Stream::Http3Stream [this=%p]", this)); + + nsHttpTransaction* trans = mTransaction->QueryHttpTransaction(); + if (trans) { + mTransactionBrowserId = trans->BrowserId(); + } + + SetPriority(cos.Flags()); + SetIncremental(cos.Incremental()); +} + +void Http3Stream::Close(nsresult aResult) { + mRecvState = RECV_DONE; + mTransaction->Close(aResult); + // Clear the mSession to break the cycle. + mSession = nullptr; +} + +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 = %zu", + this, mFlatHttpRequestHeaders.Length())); + *countUsed = avail; + return false; + } + + uint32_t oldLen = mFlatHttpRequestHeaders.Length(); + mFlatHttpRequestHeaders.SetLength(endHeader + 2); + *countUsed = avail - (oldLen - endHeader) + 4; + + return true; +} + +void Http3Stream::SetPriority(uint32_t aCos) { + if (aCos & nsIClassOfService::UrgentStart) { + // coming from an user interaction => response should be the highest + // priority + mPriorityUrgency = 1; + } else if (aCos & nsIClassOfService::Leader) { + // main html document normal priority + mPriorityUrgency = 2; + } else if (aCos & nsIClassOfService::Unblocked) { + mPriorityUrgency = 3; + } else if (aCos & nsIClassOfService::Follower) { + mPriorityUrgency = 4; + } else if (aCos & nsIClassOfService::Speculative) { + mPriorityUrgency = 6; + } else if (aCos & nsIClassOfService::Background) { + // background tasks can be deprioritzed to the lowest priority + mPriorityUrgency = 6; + } else if (aCos & nsIClassOfService::Tail) { + mPriorityUrgency = 6; + } else { + // all others get a lower priority than the main html document + mPriorityUrgency = 4; + } +} + +void Http3Stream::SetIncremental(bool incremental) { + mPriorityIncremental = incremental; +} + +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); + +#ifdef DEBUG + nsAutoCString contentLength; + if (NS_SUCCEEDED(head->GetHeader(nsHttp::Content_Length, contentLength))) { + int64_t len; + if (nsHttp::ParseInt64(contentLength.get(), nullptr, &len)) { + mRequestBodyLenExpected = len; + } + } +#endif + + return mSession->TryActivating(method, scheme, authorityHeader, path, + mFlatHttpRequestHeaders, &mStreamId, this); +} + +void Http3Stream::CurrentBrowserIdChanged(uint64_t id) { + MOZ_ASSERT(gHttpHandler->ActiveTabPriority()); + + bool previouslyFocused = (mCurrentBrowserId == mTransactionBrowserId); + mCurrentBrowserId = id; + bool nowFocused = (mCurrentBrowserId == mTransactionBrowserId); + + if (!StaticPrefs:: + network_http_http3_send_background_tabs_deprioritization() || + previouslyFocused == nowFocused) { + return; + } + + mSession->SendPriorityUpdateFrame(mStreamId, PriorityUrgency(), + PriorityIncremental()); +} + +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(nullptr, NS_NET_STATUS_SENDING_TO, + mTotalSent); + + mSendState = SENDING_BODY; + break; + case SENDING_BODY: { + rv = mSession->SendRequestBody(mStreamId, buf, count, countRead); + if (rv == NS_BASE_STREAM_WOULD_BLOCK) { + mSendingBlockedByFlowControlCount++; + } + + if (NS_FAILED(rv)) { + LOG3( + ("Http3Stream::OnReadSegment %p sending body returns " + "error=0x%" PRIx32 ".", + this, static_cast(rv))); + break; + } + +#ifdef DEBUG + mRequestBodyLenSent += *countRead; +#endif + mTotalSent += *countRead; + mTransaction->OnTransportStatus(nullptr, NS_NET_STATUS_SENDING_TO, + mTotalSent); + } break; + case EARLY_RESPONSE: + // We do not need to send the rest of the request, so just ignore it. + *countRead = count; +#ifdef DEBUG + mRequestBodyLenSent += count; +#endif + break; + default: + MOZ_ASSERT(false, "We are done sending this request!"); + rv = NS_ERROR_UNEXPECTED; + break; + } + + mSocketOutCondition = rv; + + return mSocketOutCondition; +} + +void Http3Stream::SetResponseHeaders(nsTArray& aResponseHeaders, + bool aFin, bool interim) { + MOZ_ASSERT(mRecvState == BEFORE_HEADERS || + mRecvState == READING_INTERIM_HEADERS); + mFlatResponseHeaders.AppendElements(aResponseHeaders); + mRecvState = (interim) ? READING_INTERIM_HEADERS : 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: + case READING_INTERIM_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) { + if (mRecvState == READING_INTERIM_HEADERS) { + // neqo makes sure that fin cannot be received before the final + // headers are received. + MOZ_ASSERT(!mFin); + mRecvState = BEFORE_HEADERS; + } else { + mRecvState = mFin ? RECEIVED_FIN : READING_DATA; + } + } + + if (*countWritten == 0) { + rv = NS_BASE_STREAM_WOULD_BLOCK; + } else { + mTotalRead += *countWritten; + mTransaction->OnTransportStatus(nullptr, 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(nullptr, 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() { + 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) { + mTransaction->OnTransportStatus(nullptr, NS_NET_STATUS_WAITING_FOR, 0); + mSession->CloseSendingSide(mStreamId); + mSendState = SEND_DONE; + Telemetry::Accumulate( + Telemetry::HTTP3_SENDING_BLOCKED_BY_FLOW_CONTROL_PER_TRANS, + mSendingBlockedByFlowControlCount); + +#ifdef DEBUG + MOZ_ASSERT(mRequestBodyLenSent == mRequestBodyLenExpected); +#endif + 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() { + 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; + countWrittenSingle = 0; + rv = mTransaction->WriteSegmentsAgain( + this, nsIOService::gDefaultSegmentSize, &countWrittenSingle, &again); + 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); + mTotalSent = 0; + mTotalRead = 0; + mFin = false; + mSendingBlockedByFlowControlCount = 0; + mSocketInCondition = NS_ERROR_NOT_INITIALIZED; + mSocketOutCondition = NS_ERROR_NOT_INITIALIZED; + } + + return rv; +} + +uint8_t Http3Stream::PriorityUrgency() { + if (!StaticPrefs::network_http_http3_priority()) { + // send default priority which is equivalent to sending no priority + return 3; + } + + if (StaticPrefs::network_http_http3_send_background_tabs_deprioritization() && + mCurrentBrowserId != mTransactionBrowserId) { + // Low priority + return 6; + } + return mPriorityUrgency; +} + +bool Http3Stream::PriorityIncremental() { + if (!StaticPrefs::network_http_http3_priority()) { + // send default priority which is equivalent to sending no priority + return false; + } + return mPriorityIncremental; +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/Http3Stream.h b/netwerk/protocol/http/Http3Stream.h new file mode 100644 index 0000000000..1048b20ef5 --- /dev/null +++ b/netwerk/protocol/http/Http3Stream.h @@ -0,0 +1,165 @@ +/* -*- 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 "Http3StreamBase.h" +#include "mozilla/WeakPtr.h" +#include "nsIClassOfService.h" + +namespace mozilla { +namespace net { + +class Http3Session; + +class Http3Stream final : public nsAHttpSegmentReader, + public nsAHttpSegmentWriter, + public Http3StreamBase { + public: + NS_DECL_NSAHTTPSEGMENTREADER + NS_DECL_NSAHTTPSEGMENTWRITER + // for RefPtr + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(Http3Stream, override) + + Http3Stream(nsAHttpTransaction*, Http3Session*, const ClassOfService&, + uint64_t); + + Http3WebTransportSession* GetHttp3WebTransportSession() override { + return nullptr; + } + Http3WebTransportStream* GetHttp3WebTransportStream() override { + return nullptr; + } + Http3Stream* GetHttp3Stream() override { return this; } + + nsresult TryActivating(); + + void CurrentBrowserIdChanged(uint64_t id); + + [[nodiscard]] nsresult ReadSegments() override; + [[nodiscard]] nsresult WriteSegments() override; + + bool Done() const override { return mRecvState == RECV_DONE; } + + void Close(nsresult aResult) override; + bool RecvdData() const { return mDataReceived; } + + void StopSending(); + + void SetResponseHeaders(nsTArray& aResponseHeaders, bool fin, + bool interim) override; + + // Mirrors nsAHttpTransaction + bool Do0RTT() override; + nsresult Finish0RTT(bool aRestart) override; + + uint8_t PriorityUrgency(); + bool PriorityIncremental(); + + private: + ~Http3Stream() = default; + + bool GetHeadersString(const char* buf, uint32_t avail, uint32_t* countUsed); + nsresult StartRequest(); + + void SetPriority(uint32_t aCos); + void SetIncremental(bool incremental); + + /** + * 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{PREPARING_HEADERS}; + + /** + * RecvStreamState: + * - BEFORE_HEADERS: + * The stream has not received headers yet. + * - READING_HEADERS and READING_INTERIM_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 steam was in the READING_INTERIM_HEADERS state it will + * change back to the BEFORE_HEADERS state. If the stream has been + * in the READING_HEADERS state it will change to the READING_DATA + * state. If the stream was closed by the server after sending headers + * the stream will transit into RECEIVED_FIN state. neqo makes sure + * that response headers and data are received in the right order, + * e.g. 1xx cannot be received after a non-1xx response, fin cannot + * follow 1xx response, etc. + * - 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_INTERIM_HEADERS, + READING_DATA, + RECEIVED_FIN, + RECV_DONE + } mRecvState{BEFORE_HEADERS}; + + nsCString mFlatHttpRequestHeaders; + bool mDataReceived{false}; + nsTArray mFlatResponseHeaders; + uint64_t mTransactionBrowserId{0}; + uint64_t mCurrentBrowserId; + uint8_t mPriorityUrgency{3}; // urgency field of http priority + bool mPriorityIncremental{false}; + + // For Progress Events + uint64_t mTotalSent{0}; + uint64_t mTotalRead{0}; + + bool mAttempting0RTT = false; + + uint32_t mSendingBlockedByFlowControlCount = 0; + + nsresult mSocketInCondition = NS_ERROR_NOT_INITIALIZED; + nsresult mSocketOutCondition = NS_ERROR_NOT_INITIALIZED; + +#ifdef DEBUG + uint32_t mRequestBodyLenExpected{0}; + uint32_t mRequestBodyLenSent{0}; +#endif +}; + +} // namespace net +} // namespace mozilla + +#endif // mozilla_net_Http3Stream_h diff --git a/netwerk/protocol/http/Http3StreamBase.h b/netwerk/protocol/http/Http3StreamBase.h new file mode 100644 index 0000000000..9ca85de98e --- /dev/null +++ b/netwerk/protocol/http/Http3StreamBase.h @@ -0,0 +1,70 @@ +/* -*- 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_Http3StreamBase_h +#define mozilla_net_Http3StreamBase_h + +#include "nsAHttpTransaction.h" +#include "ARefBase.h" +#include "mozilla/WeakPtr.h" +#include "nsIClassOfService.h" + +namespace mozilla::net { + +class Http3Session; +class Http3Stream; +class Http3WebTransportSession; +class Http3WebTransportStream; + +class Http3StreamBase : public SupportsWeakPtr, public ARefBase { + public: + Http3StreamBase(nsAHttpTransaction* trans, Http3Session* session); + + virtual Http3WebTransportSession* GetHttp3WebTransportSession() = 0; + virtual Http3WebTransportStream* GetHttp3WebTransportStream() = 0; + virtual Http3Stream* GetHttp3Stream() = 0; + + bool HasStreamId() const { return mStreamId != UINT64_MAX; } + uint64_t StreamId() const { return mStreamId; } + + [[nodiscard]] virtual nsresult ReadSegments() = 0; + [[nodiscard]] virtual nsresult WriteSegments() = 0; + + virtual bool Done() const = 0; + + virtual void SetResponseHeaders(nsTArray& aResponseHeaders, bool fin, + bool interim) = 0; + + void SetQueued(bool aStatus) { mQueued = aStatus; } + bool Queued() const { return mQueued; } + + virtual void Close(nsresult aResult) = 0; + + nsAHttpTransaction* Transaction() { return mTransaction; } + + // Mirrors nsAHttpTransaction + virtual bool Do0RTT() { return false; } + virtual nsresult Finish0RTT(bool aRestart) { return NS_OK; } + + virtual bool RecvdFin() const { return mFin; } + virtual bool RecvdReset() const { return mResetRecv; } + virtual void SetRecvdReset() { mResetRecv = true; } + + protected: + ~Http3StreamBase(); + + uint64_t mStreamId{UINT64_MAX}; + int64_t mSendOrder{0}; + bool mSendOrderIsSet{false}; + RefPtr mTransaction; + RefPtr mSession; + bool mQueued{false}; + bool mFin{false}; + bool mResetRecv{false}; +}; + +} // namespace mozilla::net + +#endif // mozilla_net_Http3StreamBase_h diff --git a/netwerk/protocol/http/Http3WebTransportSession.cpp b/netwerk/protocol/http/Http3WebTransportSession.cpp new file mode 100644 index 0000000000..9ef4da70c0 --- /dev/null +++ b/netwerk/protocol/http/Http3WebTransportSession.cpp @@ -0,0 +1,518 @@ +/* -*- 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/. */ + +// HttpLog.h should generally be included first +#include "HttpLog.h" +#include "Http3WebTransportSession.h" +#include "Http3WebTransportStream.h" +#include "Http3Session.h" +#include "Http3Stream.h" +#include "nsHttpRequestHead.h" +#include "nsHttpTransaction.h" +#include "nsIClassOfService.h" +#include "nsISocketTransport.h" +#include "nsSocketTransportService2.h" +#include "nsIOService.h" +#include "nsHttpHandler.h" + +namespace mozilla::net { + +Http3WebTransportSession::Http3WebTransportSession(nsAHttpTransaction* trans, + Http3Session* aHttp3Session) + : Http3StreamBase(trans, aHttp3Session) {} + +Http3WebTransportSession::~Http3WebTransportSession() = default; + +nsresult Http3WebTransportSession::ReadSegments() { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG(("Http3WebTransportSession::ReadSegments %p mSendState=%d mRecvState=%d", + this, mSendState, mRecvState)); + if (mSendState == PROCESSING_DATAGRAM) { + return NS_OK; + } + + if ((mRecvState == RECV_DONE) || (mRecvState == ACTIVE) || + (mRecvState == CLOSE_PENDING)) { + // Don't transmit any request frames if the peer cannot respond or respone + // is already done. + LOG3(( + "Http3WebTransportSession %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(("Http3WebTransportSession::ReadSegments state=%d [this=%p]", + mSendState, this)); + switch (mSendState) { + case PREPARING_HEADERS: { + rv = mTransaction->ReadSegmentsAgain( + this, nsIOService::gDefaultSegmentSize, &transactionBytes, &again); + } break; + 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( + ("Http3WebTransportSession %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))); + } break; + default: + transactionBytes = 0; + rv = NS_OK; + break; + } + + LOG(("Http3WebTransportSession::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) { + mTransaction->OnTransportStatus(nullptr, NS_NET_STATUS_WAITING_FOR, 0); + + mSendState = PROCESSING_DATAGRAM; + rv = NS_OK; + again = false; + } + // write more to the socket until error or end-of-request... + } while (again && gHttpHandler->Active()); + return rv; +} + +bool Http3WebTransportSession::ConsumeHeaders(const char* buf, uint32_t avail, + uint32_t* countUsed) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG3(("Http3WebTransportSession::ConsumeHeaders %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( + ("Http3WebTransportSession::ConsumeHeaders %p " + "Need more header bytes. Len = %zu", + this, mFlatHttpRequestHeaders.Length())); + *countUsed = avail; + return false; + } + + uint32_t oldLen = mFlatHttpRequestHeaders.Length(); + mFlatHttpRequestHeaders.SetLength(endHeader + 2); + *countUsed = avail - (oldLen - endHeader) + 4; + + return true; +} + +nsresult Http3WebTransportSession::TryActivating() { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG(("Http3WebTransportSession::TryActivating [this=%p]", this)); + nsHttpRequestHead* head = mTransaction->RequestHead(); + + nsAutoCString host; + nsresult rv = head->GetHeader(nsHttp::Host, host); + if (NS_FAILED(rv)) { + MOZ_ASSERT(false); + return rv; + } + nsAutoCString path; + head->Path(path); + + return mSession->TryActivating(""_ns, ""_ns, host, path, + mFlatHttpRequestHeaders, &mStreamId, this); +} + +nsresult Http3WebTransportSession::OnReadSegment(const char* buf, + uint32_t count, + uint32_t* countRead) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + LOG(("Http3WebTransportSession::OnReadSegment count=%u state=%d [this=%p]", + count, mSendState, this)); + + nsresult rv = NS_OK; + + switch (mSendState) { + case PREPARING_HEADERS: { + if (!ConsumeHeaders(buf, count, countRead)) { + break; + } + mSendState = WAITING_TO_ACTIVATE; + } + [[fallthrough]]; + case WAITING_TO_ACTIVATE: + rv = TryActivating(); + if (rv == NS_BASE_STREAM_WOULD_BLOCK) { + LOG3( + ("Http3WebTransportSession::OnReadSegment %p cannot activate now. " + "queued.\n", + this)); + break; + } + if (NS_FAILED(rv)) { + LOG3( + ("Http3WebTransportSession::OnReadSegment %p cannot activate " + "error=0x%" PRIx32 ".", + this, static_cast(rv))); + break; + } + + // Successfully activated. + mTransaction->OnTransportStatus(nullptr, NS_NET_STATUS_SENDING_TO, 0); + + mSendState = PROCESSING_DATAGRAM; + break; + default: + MOZ_ASSERT(false, "We are done sending this request!"); + rv = NS_ERROR_UNEXPECTED; + break; + } + + mSocketOutCondition = rv; + + return mSocketOutCondition; +} + +nsresult Http3WebTransportSession::WriteSegments() { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG(("Http3WebTransportSession::WriteSegments [this=%p]", this)); + nsresult rv = NS_OK; + uint32_t countWrittenSingle = 0; + bool again = true; + + if (mRecvState == CLOSE_PENDING) { + mSession->CloseWebTransport(mStreamId, mStatus, mReason); + mRecvState = RECV_DONE; + // This will closed the steam because the stream is Done(). + return NS_OK; + } + + do { + mSocketInCondition = NS_OK; + countWrittenSingle = 0; + rv = mTransaction->WriteSegmentsAgain( + this, nsIOService::gDefaultSegmentSize, &countWrittenSingle, &again); + LOG(("Http3WebTransportSession::WriteSegments rv=0x%" PRIx32 + " countWrittenSingle=%" PRIu32 " socketin=%" PRIx32 " [this=%p]", + static_cast(rv), countWrittenSingle, + static_cast(mSocketInCondition), this)); + if (mTransaction->IsDone()) { + // An HTTP transaction used for setting up a WebTransport session will + // receive only response headers and afterward, it will be marked as + // done. At this point, the session negotiation has finished and the + // WebTransport session transfers into the ACTIVE state. + mRecvState = ACTIVE; + } + + 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; +} + +void Http3WebTransportSession::SetResponseHeaders( + nsTArray& aResponseHeaders, bool fin, bool interim) { + MOZ_ASSERT(mRecvState == BEFORE_HEADERS || + mRecvState == READING_INTERIM_HEADERS); + mFlatResponseHeaders.AppendElements(aResponseHeaders); + mRecvState = (interim) ? READING_INTERIM_HEADERS : READING_HEADERS; +} + +nsresult Http3WebTransportSession::OnWriteSegment(char* buf, uint32_t count, + uint32_t* countWritten) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + LOG(("Http3WebTransportSession::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: + case READING_INTERIM_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) { + if (mRecvState == READING_INTERIM_HEADERS) { + // neqo makes sure that fin cannot be received before the final + // headers are received. + mRecvState = BEFORE_HEADERS; + } else { + mRecvState = ACTIVE; + } + } + + if (*countWritten == 0) { + rv = NS_BASE_STREAM_WOULD_BLOCK; + } else { + mTransaction->OnTransportStatus(nullptr, NS_NET_STATUS_RECEIVING_FROM, + 0); + } + } break; + case ACTIVE: + case CLOSE_PENDING: + 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; +} + +void Http3WebTransportSession::Close(nsresult aResult) { + LOG(("Http3WebTransportSession::Close %p", this)); + if (mListener) { + mListener->OnSessionClosed(NS_SUCCEEDED(aResult), 0, ""_ns); + mListener = nullptr; + } + if (mTransaction) { + mTransaction->Close(aResult); + mTransaction = nullptr; + } + mRecvState = RECV_DONE; + mSendState = SEND_DONE; + + if (mSession) { + mSession->CloseWebTransportConn(); + mSession = nullptr; + } +} + +void Http3WebTransportSession::OnSessionClosed(bool aCleanly, uint32_t aStatus, + const nsACString& aReason) { + if (mTransaction) { + mTransaction->Close(NS_BASE_STREAM_CLOSED); + mTransaction = nullptr; + } + if (mListener) { + mListener->OnSessionClosed(aCleanly, aStatus, aReason); + mListener = nullptr; + } + mRecvState = RECV_DONE; + mSendState = SEND_DONE; + + mSession->CloseWebTransportConn(); +} + +void Http3WebTransportSession::CloseSession(uint32_t aStatus, + const nsACString& aReason) { + if ((mRecvState != CLOSE_PENDING) && (mRecvState != RECV_DONE)) { + mStatus = aStatus; + mReason = aReason; + mSession->ConnectSlowConsumer(this); + mRecvState = CLOSE_PENDING; + mSendState = SEND_DONE; + } + mListener = nullptr; +} + +void Http3WebTransportSession::TransactionIsDone(nsresult aResult) { + mTransaction->Close(aResult); + mTransaction = nullptr; +} + +void Http3WebTransportSession::CreateOutgoingBidirectionalStream( + std::function, nsresult>&&)>&& + aCallback) { + return CreateStreamInternal(true, std::move(aCallback)); +} + +void Http3WebTransportSession::CreateOutgoingUnidirectionalStream( + std::function, nsresult>&&)>&& + aCallback) { + return CreateStreamInternal(false, std::move(aCallback)); +} + +void Http3WebTransportSession::CreateStreamInternal( + bool aBidi, + std::function, nsresult>&&)>&& + aCallback) { + LOG(("Http3WebTransportSession::CreateStreamInternal this=%p aBidi=%d", this, + aBidi)); + if (mRecvState != ACTIVE) { + aCallback(Err(NS_ERROR_NOT_AVAILABLE)); + return; + } + + RefPtr stream = + aBidi ? new Http3WebTransportStream(mSession, mStreamId, + WebTransportStreamType::BiDi, + std::move(aCallback)) + : new Http3WebTransportStream(mSession, mStreamId, + WebTransportStreamType::UniDi, + std::move(aCallback)); + mSession->StreamHasDataToWrite(stream); + // Put the newly created stream in to |mStreams| to keep it alive. + mStreams.AppendElement(std::move(stream)); +} + +// This is called by Http3Session::TryActivatingWebTransportStream. When called, +// this means a WebTransport stream is successfully activated and the stream +// will be managed by Http3Session. +void Http3WebTransportSession::RemoveWebTransportStream( + Http3WebTransportStream* aStream) { + LOG( + ("Http3WebTransportSession::RemoveWebTransportStream " + "this=%p aStream=%p", + this, aStream)); + DebugOnly existed = mStreams.RemoveElement(aStream); + MOZ_ASSERT(existed); +} + +already_AddRefed +Http3WebTransportSession::OnIncomingWebTransportStream( + WebTransportStreamType aType, uint64_t aId) { + LOG( + ("Http3WebTransportSession::OnIncomingWebTransportStream " + "this=%p", + this)); + + if (mRecvState != ACTIVE) { + return nullptr; + } + + MOZ_ASSERT(!mTransaction); + RefPtr stream = + new Http3WebTransportStream(mSession, mStreamId, aType, aId); + if (NS_FAILED(stream->InitInputPipe())) { + return nullptr; + } + + if (aType == WebTransportStreamType::BiDi) { + if (NS_FAILED(stream->InitOutputPipe())) { + return nullptr; + } + } + + if (!mListener) { + return nullptr; + } + + mListener->OnIncomingStreamAvailableInternal(stream); + return stream.forget(); +} + +void Http3WebTransportSession::SendDatagram(nsTArray&& aData, + uint64_t aTrackingId) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG(("Http3WebTransportSession::SendDatagram this=%p", this)); + if (mSendState != PROCESSING_DATAGRAM) { + return; + } + + mSession->SendDatagram(this, aData, aTrackingId); + mSession->StreamHasDataToWrite(this); +} + +void Http3WebTransportSession::OnDatagramReceived(nsTArray&& aData) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG(("Http3WebTransportSession::OnDatagramReceived this=%p", this)); + if (mRecvState != ACTIVE || !mListener) { + return; + } + + mListener->OnDatagramReceivedInternal(std::move(aData)); +} + +void Http3WebTransportSession::GetMaxDatagramSize() { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + if (mRecvState != ACTIVE || !mListener) { + return; + } + + uint64_t size = mSession->MaxDatagramSize(mStreamId); + mListener->OnMaxDatagramSize(size); +} + +void Http3WebTransportSession::OnOutgoingDatagramOutCome( + uint64_t aId, WebTransportSessionEventListener::DatagramOutcome aOutCome) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG(("Http3WebTransportSession::OnOutgoingDatagramOutCome this=%p id=%" PRIx64 + ", outCome=%d mRecvState=%d", + this, aId, static_cast(aOutCome), mRecvState)); + if (mRecvState != ACTIVE || !mListener || !aId) { + return; + } + + mListener->OnOutgoingDatagramOutCome(aId, aOutCome); +} + +void Http3WebTransportSession::OnStreamStopSending(uint64_t aId, + nsresult aError) { + LOG(("OnStreamStopSending id:%" PRId64, aId)); + if (!mListener) { + return; + } + + mListener->OnStopSending(aId, aError); +} + +void Http3WebTransportSession::OnStreamReset(uint64_t aId, nsresult aError) { + LOG(("OnStreamReset id:%" PRId64, aId)); + if (!mListener) { + return; + } + + mListener->OnResetReceived(aId, aError); +} + +} // namespace mozilla::net diff --git a/netwerk/protocol/http/Http3WebTransportSession.h b/netwerk/protocol/http/Http3WebTransportSession.h new file mode 100644 index 0000000000..19afb1f172 --- /dev/null +++ b/netwerk/protocol/http/Http3WebTransportSession.h @@ -0,0 +1,123 @@ +/* -*- 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_Http3WebTransportSession_h +#define mozilla_net_Http3WebTransportSession_h + +#include "ARefBase.h" +#include "Http3StreamBase.h" +#include "nsIWebTransport.h" +#include "mozilla/WeakPtr.h" +#include "mozilla/net/NeqoHttp3Conn.h" + +namespace mozilla::net { + +class Http3Session; + +// TODO Http3WebTransportSession is very similar to Http3Stream. It should +// be built on top of it with a couple of small changes. The docs will be added +// when this is implemented. + +class Http3WebTransportSession final : public Http3StreamBase, + public nsAHttpSegmentWriter, + public nsAHttpSegmentReader { + public: + NS_DECL_NSAHTTPSEGMENTWRITER + NS_DECL_NSAHTTPSEGMENTREADER + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(Http3WebTransportSession, override) + + Http3WebTransportSession(nsAHttpTransaction*, Http3Session*); + + Http3WebTransportSession* GetHttp3WebTransportSession() override { + return this; + } + Http3WebTransportStream* GetHttp3WebTransportStream() override { + return nullptr; + } + Http3Stream* GetHttp3Stream() override { return nullptr; } + + [[nodiscard]] nsresult ReadSegments() override; + [[nodiscard]] nsresult WriteSegments() override; + + bool Done() const override { return mRecvState == RECV_DONE; } + + void Close(nsresult aResult) override; + + void SetResponseHeaders(nsTArray& aResponseHeaders, bool fin, + bool interim) override; + void SetWebTransportSessionEventListener( + WebTransportSessionEventListener* listener) { + mListener = listener; + } + + nsresult TryActivating(); + void TransactionIsDone(nsresult aResult); + void CloseSession(uint32_t aStatus, const nsACString& aReason); + void OnSessionClosed(bool aCleanly, uint32_t aStatus, + const nsACString& aReason); + + void CreateOutgoingBidirectionalStream( + std::function, nsresult>&&)>&& + aCallback); + void CreateOutgoingUnidirectionalStream( + std::function, nsresult>&&)>&& + aCallback); + void RemoveWebTransportStream(Http3WebTransportStream* aStream); + + already_AddRefed OnIncomingWebTransportStream( + WebTransportStreamType aType, uint64_t aId); + + void SendDatagram(nsTArray&& aData, uint64_t aTrackingId); + + void OnDatagramReceived(nsTArray&& aData); + + void GetMaxDatagramSize(); + + void OnOutgoingDatagramOutCome( + uint64_t aId, WebTransportSessionEventListener::DatagramOutcome aOutCome); + + void OnStreamStopSending(uint64_t aId, nsresult aError); + void OnStreamReset(uint64_t aId, nsresult aError); + + private: + virtual ~Http3WebTransportSession(); + + bool ConsumeHeaders(const char* buf, uint32_t avail, uint32_t* countUsed); + + void CreateStreamInternal( + bool aBidi, + std::function, nsresult>&&)>&& + aCallback); + + enum RecvStreamState { + BEFORE_HEADERS, + READING_HEADERS, + READING_INTERIM_HEADERS, + ACTIVE, + CLOSE_PENDING, + RECV_DONE + } mRecvState{BEFORE_HEADERS}; + + enum SendStreamState { + PREPARING_HEADERS, + WAITING_TO_ACTIVATE, + PROCESSING_DATAGRAM, + SEND_DONE, + } mSendState{PREPARING_HEADERS}; + + nsCString mFlatHttpRequestHeaders; + nsTArray mFlatResponseHeaders; + nsTArray> mStreams; + + nsresult mSocketInCondition = NS_ERROR_NOT_INITIALIZED; + nsresult mSocketOutCondition = NS_ERROR_NOT_INITIALIZED; + + RefPtr mListener; + uint32_t mStatus{0}; + nsCString mReason; +}; + +} // namespace mozilla::net +#endif // mozilla_net_Http3WebTransportSession_h diff --git a/netwerk/protocol/http/Http3WebTransportStream.cpp b/netwerk/protocol/http/Http3WebTransportStream.cpp new file mode 100644 index 0000000000..c76d8ef798 --- /dev/null +++ b/netwerk/protocol/http/Http3WebTransportStream.cpp @@ -0,0 +1,667 @@ +/* -*- 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/. */ + +#include "Http3WebTransportStream.h" + +#include "HttpLog.h" +#include "Http3Session.h" +#include "Http3WebTransportSession.h" +#include "mozilla/TimeStamp.h" +#include "nsHttpHandler.h" +#include "nsIOService.h" +#include "nsIPipe.h" +#include "nsSocketTransportService2.h" +#include "nsIWebTransportStream.h" + +namespace mozilla::net { + +namespace { + +// This is an nsAHttpTransaction that does nothing. +class DummyWebTransportStreamTransaction : public nsAHttpTransaction { + public: + NS_DECL_THREADSAFE_ISUPPORTS + + DummyWebTransportStreamTransaction() = default; + + void SetConnection(nsAHttpConnection*) override {} + nsAHttpConnection* Connection() override { return nullptr; } + void GetSecurityCallbacks(nsIInterfaceRequestor**) override {} + void OnTransportStatus(nsITransport* transport, nsresult status, + int64_t progress) override {} + bool IsDone() override { return false; } + nsresult Status() override { return NS_OK; } + uint32_t Caps() override { return 0; } + [[nodiscard]] nsresult ReadSegments(nsAHttpSegmentReader*, uint32_t, + uint32_t*) override { + return NS_OK; + } + [[nodiscard]] nsresult WriteSegments(nsAHttpSegmentWriter*, uint32_t, + uint32_t*) override { + return NS_OK; + } + void Close(nsresult reason) override {} + nsHttpConnectionInfo* ConnectionInfo() override { return nullptr; } + void SetProxyConnectFailed() override {} + nsHttpRequestHead* RequestHead() override { return nullptr; } + uint32_t Http1xTransactionCount() override { return 0; } + [[nodiscard]] nsresult TakeSubTransactions( + nsTArray>& outTransactions) override { + return NS_OK; + } + + private: + virtual ~DummyWebTransportStreamTransaction() = default; +}; + +NS_IMPL_ISUPPORTS(DummyWebTransportStreamTransaction, nsISupportsWeakReference) + +class WebTransportSendStreamStats : public nsIWebTransportSendStreamStats { + public: + NS_DECL_THREADSAFE_ISUPPORTS + + explicit WebTransportSendStreamStats(uint64_t aSent, uint64_t aAcked) + : mTimeStamp(TimeStamp::Now()), + mTotalSent(aSent), + mTotalAcknowledged(aAcked) {} + + NS_IMETHOD GetTimestamp(mozilla::TimeStamp* aTimestamp) override { + *aTimestamp = mTimeStamp; + return NS_OK; + } + NS_IMETHOD GetBytesSent(uint64_t* aBytesSent) override { + *aBytesSent = mTotalSent; + return NS_OK; + } + NS_IMETHOD GetBytesAcknowledged(uint64_t* aBytesAcknowledged) override { + *aBytesAcknowledged = mTotalAcknowledged; + return NS_OK; + } + + private: + virtual ~WebTransportSendStreamStats() = default; + + TimeStamp mTimeStamp; + uint64_t mTotalSent; + uint64_t mTotalAcknowledged; +}; + +NS_IMPL_ISUPPORTS(WebTransportSendStreamStats, nsIWebTransportSendStreamStats) + +class WebTransportReceiveStreamStats + : public nsIWebTransportReceiveStreamStats { + public: + NS_DECL_THREADSAFE_ISUPPORTS + + explicit WebTransportReceiveStreamStats(uint64_t aReceived) + : mTimeStamp(TimeStamp::Now()), mTotalReceived(aReceived) {} + + NS_IMETHOD GetTimestamp(mozilla::TimeStamp* aTimestamp) override { + *aTimestamp = mTimeStamp; + return NS_OK; + } + NS_IMETHOD GetBytesReceived(uint64_t* aByteReceived) override { + *aByteReceived = mTotalReceived; + return NS_OK; + } + + private: + virtual ~WebTransportReceiveStreamStats() = default; + + TimeStamp mTimeStamp; + uint64_t mTotalReceived; +}; + +NS_IMPL_ISUPPORTS(WebTransportReceiveStreamStats, + nsIWebTransportReceiveStreamStats) + +} // namespace + +NS_IMPL_ISUPPORTS(Http3WebTransportStream, nsIInputStreamCallback) + +Http3WebTransportStream::Http3WebTransportStream( + Http3Session* aSession, uint64_t aSessionId, WebTransportStreamType aType, + std::function, nsresult>&&)>&& + aCallback) + : Http3StreamBase(new DummyWebTransportStreamTransaction(), aSession), + mSessionId(aSessionId), + mStreamType(aType), + mStreamRole(OUTGOING), + mStreamReadyCallback(std::move(aCallback)) { + LOG(("Http3WebTransportStream outgoing ctor %p", this)); +} + +Http3WebTransportStream::Http3WebTransportStream(Http3Session* aSession, + uint64_t aSessionId, + WebTransportStreamType aType, + uint64_t aStreamId) + : Http3StreamBase(new DummyWebTransportStreamTransaction(), aSession), + mSessionId(aSessionId), + mStreamType(aType), + mStreamRole(INCOMING), + // WAITING_DATA indicates we are waiting + // Http3WebTransportStream::OnInputStreamReady to be called. + mSendState(WAITING_DATA), + mStreamReadyCallback(nullptr) { + LOG(("Http3WebTransportStream incoming ctor %p", this)); + mStreamId = aStreamId; +} + +Http3WebTransportStream::~Http3WebTransportStream() { + LOG(("Http3WebTransportStream dtor %p", this)); +} + +nsresult Http3WebTransportStream::TryActivating() { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + return mSession->TryActivatingWebTransportStream(&mStreamId, this); +} + +NS_IMETHODIMP Http3WebTransportStream::OnInputStreamReady( + nsIAsyncInputStream* aStream) { + LOG1( + ("Http3WebTransportStream::OnInputStreamReady [this=%p stream=%p " + "state=%d]", + this, aStream, mSendState)); + if (mSendState == SEND_DONE) { + // already closed + return NS_OK; + } + + mSendState = SENDING; + mSession->StreamHasDataToWrite(this); + return NS_OK; +} + +nsresult Http3WebTransportStream::InitOutputPipe() { + nsCOMPtr out; + nsCOMPtr in; + NS_NewPipe2(getter_AddRefs(in), getter_AddRefs(out), true, true, + nsIOService::gDefaultSegmentSize, + nsIOService::gDefaultSegmentCount); + + { + MutexAutoLock lock(mMutex); + mSendStreamPipeIn = std::move(in); + mSendStreamPipeOut = std::move(out); + } + + nsresult rv = + mSendStreamPipeIn->AsyncWait(this, 0, 0, gSocketTransportService); + if (NS_FAILED(rv)) { + return rv; + } + + mSendState = WAITING_DATA; + return NS_OK; +} + +nsresult Http3WebTransportStream::InitInputPipe() { + nsCOMPtr out; + nsCOMPtr in; + NS_NewPipe2(getter_AddRefs(in), getter_AddRefs(out), true, true, + nsIOService::gDefaultSegmentSize, + nsIOService::gDefaultSegmentCount); + + { + MutexAutoLock lock(mMutex); + mReceiveStreamPipeIn = std::move(in); + mReceiveStreamPipeOut = std::move(out); + } + + mRecvState = READING; + return NS_OK; +} + +void Http3WebTransportStream::GetWriterAndReader( + nsIAsyncOutputStream** aOutOutputStream, + nsIAsyncInputStream** aOutInputStream) { + nsCOMPtr output; + nsCOMPtr input; + { + MutexAutoLock lock(mMutex); + output = mSendStreamPipeOut; + input = mReceiveStreamPipeIn; + } + + output.forget(aOutOutputStream); + input.forget(aOutInputStream); +} + +already_AddRefed +Http3WebTransportStream::GetSendStreamStats() { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + nsCOMPtr stats = + new WebTransportSendStreamStats(mTotalSent, mTotalAcknowledged); + return stats.forget(); +} + +already_AddRefed +Http3WebTransportStream::GetReceiveStreamStats() { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + nsCOMPtr stats = + new WebTransportReceiveStreamStats(mTotalReceived); + return stats.forget(); +} + +nsresult Http3WebTransportStream::OnReadSegment(const char* buf, uint32_t count, + uint32_t* countRead) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + LOG(("Http3WebTransportStream::OnReadSegment count=%u state=%d [this=%p]", + count, mSendState, this)); + + nsresult rv = NS_OK; + + switch (mSendState) { + case WAITING_TO_ACTIVATE: + rv = TryActivating(); + if (rv == NS_BASE_STREAM_WOULD_BLOCK) { + LOG3( + ("Http3WebTransportStream::OnReadSegment %p cannot activate now. " + "queued.\n", + this)); + break; + } + if (NS_FAILED(rv)) { + LOG3( + ("Http3WebTransportStream::OnReadSegment %p cannot activate " + "error=0x%" PRIx32 ".", + this, static_cast(rv))); + mStreamReadyCallback(Err(rv)); + mStreamReadyCallback = nullptr; + break; + } + + rv = InitOutputPipe(); + if (NS_SUCCEEDED(rv) && mStreamType == WebTransportStreamType::BiDi) { + rv = InitInputPipe(); + } + if (NS_FAILED(rv)) { + LOG3( + ("Http3WebTransportStream::OnReadSegment %p failed to create pipe " + "error=0x%" PRIx32 ".", + this, static_cast(rv))); + mSendState = SEND_DONE; + mStreamReadyCallback(Err(rv)); + mStreamReadyCallback = nullptr; + break; + } + + // Successfully activated. + mStreamReadyCallback(RefPtr{this}); + mStreamReadyCallback = nullptr; + break; + case SENDING: { + rv = mSession->SendRequestBody(mStreamId, buf, count, countRead); + LOG3( + ("Http3WebTransportStream::OnReadSegment %p sending body returns " + "error=0x%" PRIx32 ".", + this, static_cast(rv))); + mTotalSent += *countRead; + } break; + case WAITING_DATA: + // Still waiting + LOG3(( + "Http3WebTransportStream::OnReadSegment %p Still waiting for data...", + this)); + break; + case SEND_DONE: + LOG3(("Http3WebTransportStream::OnReadSegment %p called after SEND_DONE ", + this)); + MOZ_ASSERT(false, "We are done sending this request!"); + MOZ_ASSERT(mStreamReadyCallback); + rv = NS_ERROR_UNEXPECTED; + mStreamReadyCallback(Err(rv)); + mStreamReadyCallback = nullptr; + break; + } + + mSocketOutCondition = rv; + + return mSocketOutCondition; +} + +// static +nsresult Http3WebTransportStream::ReadRequestSegment( + nsIInputStream* stream, void* closure, const char* buf, uint32_t offset, + uint32_t count, uint32_t* countRead) { + Http3WebTransportStream* wtStream = (Http3WebTransportStream*)closure; + nsresult rv = wtStream->OnReadSegment(buf, count, countRead); + LOG(("Http3WebTransportStream::ReadRequestSegment %p read=%u", wtStream, + *countRead)); + return rv; +} + +nsresult Http3WebTransportStream::ReadSegments() { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG(("Http3WebTransportStream::ReadSegments [this=%p]", this)); + nsresult rv = NS_OK; + uint32_t sendBytes = 0; + bool again = true; + do { + sendBytes = 0; + rv = mSocketOutCondition = NS_OK; + LOG(("Http3WebTransportStream::ReadSegments state=%d [this=%p]", mSendState, + this)); + switch (mSendState) { + case WAITING_TO_ACTIVATE: { + LOG3( + ("Http3WebTransportStream %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 != WAITING_DATA) { + break; + } + } + [[fallthrough]]; + case WAITING_DATA: + [[fallthrough]]; + case SENDING: { + if (mStreamRole == INCOMING && + mStreamType == WebTransportStreamType::UniDi) { + rv = NS_OK; + break; + } + mSendState = SENDING; + rv = mSendStreamPipeIn->ReadSegments(ReadRequestSegment, this, + nsIOService::gDefaultSegmentSize, + &sendBytes); + } break; + case SEND_DONE: { + return NS_OK; + } + default: + sendBytes = 0; + rv = NS_OK; + break; + } + + LOG(("Http3WebTransportStream::ReadSegments rv=0x%" PRIx32 + " read=%u sock-cond=%" PRIx32 " again=%d mSendFin=%d [this=%p]", + static_cast(rv), sendBytes, + static_cast(mSocketOutCondition), again, mSendFin, this)); + + // XXX some streams return NS_BASE_STREAM_CLOSED to indicate EOF. + if (rv == NS_BASE_STREAM_CLOSED || !mPendingTasks.IsEmpty()) { + rv = NS_OK; + sendBytes = 0; + } + + if (NS_FAILED(rv)) { + // if the writer didn't want to write any more data, then + // wait for the transaction to call ResumeSend. + if (rv == NS_BASE_STREAM_WOULD_BLOCK) { + mSendState = WAITING_DATA; + rv = mSendStreamPipeIn->AsyncWait(this, 0, 0, gSocketTransportService); + } + again = false; + + // Got a WebTransport specific error + if (rv >= NS_ERROR_WEBTRANSPORT_CODE_BASE && + rv <= NS_ERROR_WEBTRANSPORT_CODE_END) { + uint8_t errorCode = GetWebTransportErrorFromNSResult(rv); + mSendState = SEND_DONE; + Reset(WebTransportErrorToHttp3Error(errorCode)); + rv = NS_OK; + } + } else if (NS_FAILED(mSocketOutCondition)) { + if (mSocketOutCondition != NS_BASE_STREAM_WOULD_BLOCK) { + rv = mSocketOutCondition; + } + again = false; + } else if (!sendBytes) { + mSendState = SEND_DONE; + rv = NS_OK; + again = false; + if (!mPendingTasks.IsEmpty()) { + LOG(("Has pending tasks to do")); + nsTArray> tasks = std::move(mPendingTasks); + for (const auto& task : tasks) { + task(); + } + } + // Tell the underlying stream we're done + SendFin(); + } + + // write more to the socket until error or end-of-request... + } while (again && gHttpHandler->Active()); + return rv; +} + +nsresult Http3WebTransportStream::OnWriteSegment(char* buf, uint32_t count, + uint32_t* countWritten) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + LOG(("Http3WebTransportStream::OnWriteSegment [this=%p, state=%d", this, + static_cast(mRecvState))); + nsresult rv = NS_OK; + switch (mRecvState) { + case READING: { + rv = mSession->ReadResponseData(mStreamId, buf, count, countWritten, + &mFin); + if (*countWritten == 0) { + if (mFin) { + mRecvState = RECV_DONE; + rv = NS_BASE_STREAM_CLOSED; + } else { + rv = NS_BASE_STREAM_WOULD_BLOCK; + } + } else { + mTotalReceived += *countWritten; + 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; + break; + default: + rv = NS_ERROR_UNEXPECTED; + break; + } + + // 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; +} + +// static +nsresult Http3WebTransportStream::WritePipeSegment(nsIOutputStream* stream, + void* closure, char* buf, + uint32_t offset, + uint32_t count, + uint32_t* countWritten) { + Http3WebTransportStream* self = (Http3WebTransportStream*)closure; + + nsresult rv = self->OnWriteSegment(buf, count, countWritten); + if (NS_FAILED(rv)) { + return rv; + } + + LOG(("Http3WebTransportStream::WritePipeSegment %p written=%u", self, + *countWritten)); + + return rv; +} + +nsresult Http3WebTransportStream::WriteSegments() { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + if (!mReceiveStreamPipeOut) { + return NS_OK; + } + + LOG(("Http3WebTransportStream::WriteSegments [this=%p]", this)); + + nsresult rv = NS_OK; + uint32_t countWrittenSingle = 0; + bool again = true; + + do { + mSocketInCondition = NS_OK; + countWrittenSingle = 0; + rv = mReceiveStreamPipeOut->WriteSegments(WritePipeSegment, this, + nsIOService::gDefaultSegmentSize, + &countWrittenSingle); + LOG(("Http3WebTransportStream::WriteSegments rv=0x%" PRIx32 + " countWrittenSingle=%" PRIu32 " socketin=%" PRIx32 " [this=%p]", + static_cast(rv), countWrittenSingle, + static_cast(mSocketInCondition), this)); + if (NS_FAILED(rv)) { + 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; + if (rv == NS_BASE_STREAM_CLOSED) { + mReceiveStreamPipeOut->Close(); + rv = NS_OK; + } + } + again = false; + } + // read more from the socket until error... + } while (again && gHttpHandler->Active()); + + return rv; +} + +bool Http3WebTransportStream::Done() const { + return mSendState == SEND_DONE && mRecvState == RECV_DONE; +} + +void Http3WebTransportStream::Close(nsresult aResult) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG(("Http3WebTransportStream::Close [this=%p]", this)); + mTransaction = nullptr; + if (mSendStreamPipeIn) { + mSendStreamPipeIn->AsyncWait(nullptr, 0, 0, nullptr); + mSendStreamPipeIn->CloseWithStatus(aResult); + } + if (mReceiveStreamPipeOut) { + mReceiveStreamPipeOut->AsyncWait(nullptr, 0, 0, nullptr); + mReceiveStreamPipeOut->CloseWithStatus(aResult); + } + mSendState = SEND_DONE; + mRecvState = RECV_DONE; + mSession = nullptr; +} + +void Http3WebTransportStream::SendFin() { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG(("Http3WebTransportStream::SendFin [this=%p mSendState=%d]", this, + mSendState)); + + if (mSendFin || !mSession || mResetError) { + // Already closed. + return; + } + + mSendFin = true; + + switch (mSendState) { + case SENDING: { + mPendingTasks.AppendElement([self = RefPtr{this}]() { + self->mSession->CloseSendingSide(self->mStreamId); + }); + } break; + case WAITING_DATA: + mSendState = SEND_DONE; + [[fallthrough]]; + case SEND_DONE: + mSession->CloseSendingSide(mStreamId); + // StreamHasDataToWrite needs to be called to trigger ProcessOutput. + mSession->StreamHasDataToWrite(this); + break; + default: + MOZ_ASSERT_UNREACHABLE("invalid mSendState!"); + break; + } +} + +void Http3WebTransportStream::Reset(uint64_t aErrorCode) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG(("Http3WebTransportStream::Reset [this=%p, mSendState=%d]", this, + mSendState)); + + if (mResetError || !mSession || mSendFin) { + // The stream is already reset. + return; + } + + mResetError = Some(aErrorCode); + + switch (mSendState) { + case SENDING: { + LOG(("Http3WebTransportStream::Reset [this=%p] reset after sending data", + this)); + mPendingTasks.AppendElement([self = RefPtr{this}]() { + // "Reset" needs a special treatment here. If we are sending data and + // ResetWebTransportStream is called before Http3Session::ProcessOutput, + // neqo will drop the last piece of data. + NS_DispatchToCurrentThread( + NS_NewRunnableFunction("Http3WebTransportStream::Reset", [self]() { + self->mSession->ResetWebTransportStream(self, *self->mResetError); + self->mSession->StreamHasDataToWrite(self); + self->mSession->ConnectSlowConsumer(self); + })); + }); + } break; + case WAITING_DATA: + mSendState = SEND_DONE; + [[fallthrough]]; + case SEND_DONE: + mSession->ResetWebTransportStream(this, *mResetError); + // StreamHasDataToWrite needs to be called to trigger ProcessOutput. + mSession->StreamHasDataToWrite(this); + mSession->ConnectSlowConsumer(this); + break; + default: + MOZ_ASSERT_UNREACHABLE("invalid mSendState!"); + break; + } +} + +void Http3WebTransportStream::SendStopSending(uint8_t aErrorCode) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG(("Http3WebTransportStream::SendStopSending [this=%p, mSendState=%d]", + this, mSendState)); + + if (mSendState == WAITING_TO_ACTIVATE) { + return; + } + + if (mStopSendingError || !mSession) { + return; + } + + mStopSendingError = Some(aErrorCode); + + mSession->StreamStopSending(this, *mStopSendingError); + // StreamHasDataToWrite needs to be called to trigger ProcessOutput. + mSession->StreamHasDataToWrite(this); +} + +void Http3WebTransportStream::SetSendOrder(Maybe aSendOrder) { + mSession->SetSendOrder(this, aSendOrder); +} + +} // namespace mozilla::net diff --git a/netwerk/protocol/http/Http3WebTransportStream.h b/netwerk/protocol/http/Http3WebTransportStream.h new file mode 100644 index 0000000000..545e793246 --- /dev/null +++ b/netwerk/protocol/http/Http3WebTransportStream.h @@ -0,0 +1,136 @@ +/* -*- 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_Http3WebTransportStream_h +#define mozilla_net_Http3WebTransportStream_h + +#include +#include "Http3StreamBase.h" +#include "mozilla/net/NeqoHttp3Conn.h" +#include "mozilla/Maybe.h" +#include "mozilla/Result.h" +#include "mozilla/ResultVariant.h" +#include "nsIAsyncInputStream.h" +#include "nsIAsyncOutputStream.h" + +class nsIWebTransportSendStreamStats; +class nsIWebTransportReceiveStreamStats; + +namespace mozilla::net { + +class Http3WebTransportSession; + +class Http3WebTransportStream final : public Http3StreamBase, + public nsAHttpSegmentWriter, + public nsAHttpSegmentReader, + public nsIInputStreamCallback { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSAHTTPSEGMENTWRITER + NS_DECL_NSAHTTPSEGMENTREADER + NS_DECL_NSIINPUTSTREAMCALLBACK + + explicit Http3WebTransportStream( + Http3Session* aSession, uint64_t aSessionId, WebTransportStreamType aType, + std::function, nsresult>&&)>&& + aCallback); + explicit Http3WebTransportStream(Http3Session* aSession, uint64_t aSessionId, + WebTransportStreamType aType, + uint64_t aStreamId); + + Http3WebTransportSession* GetHttp3WebTransportSession() override { + return nullptr; + } + Http3WebTransportStream* GetHttp3WebTransportStream() override { + return this; + } + Http3Stream* GetHttp3Stream() override { return nullptr; } + + void SetSendOrder(Maybe aSendOrder); + + [[nodiscard]] nsresult ReadSegments() override; + [[nodiscard]] nsresult WriteSegments() override; + + bool Done() const override; + void Close(nsresult aResult) override; + + void SetResponseHeaders(nsTArray& aResponseHeaders, bool fin, + bool interim) override {} + + uint64_t SessionId() const { return mSessionId; } + WebTransportStreamType StreamType() const { return mStreamType; } + + void SendFin(); + void Reset(uint64_t aErrorCode); + void SendStopSending(uint8_t aErrorCode); + + already_AddRefed GetSendStreamStats(); + already_AddRefed GetReceiveStreamStats(); + void GetWriterAndReader(nsIAsyncOutputStream** aOutOutputStream, + nsIAsyncInputStream** aOutInputStream); + + // When mRecvState is RECV_DONE, this means we already received the FIN. + bool RecvDone() const { return mRecvState == RECV_DONE; } + + private: + friend class Http3WebTransportSession; + virtual ~Http3WebTransportStream(); + + nsresult TryActivating(); + static nsresult ReadRequestSegment(nsIInputStream*, void*, const char*, + uint32_t, uint32_t, uint32_t*); + static nsresult WritePipeSegment(nsIOutputStream*, void*, char*, uint32_t, + uint32_t, uint32_t*); + nsresult InitOutputPipe(); + nsresult InitInputPipe(); + + uint64_t mSessionId{UINT64_MAX}; + WebTransportStreamType mStreamType{WebTransportStreamType::BiDi}; + + enum StreamRole { + INCOMING, + OUTGOING, + } mStreamRole{INCOMING}; + + enum SendStreamState { + WAITING_TO_ACTIVATE, + WAITING_DATA, + SENDING, + SEND_DONE, + } mSendState{WAITING_TO_ACTIVATE}; + + enum RecvStreamState { BEFORE_READING, READING, RECEIVED_FIN, RECV_DONE }; + Atomic mRecvState{BEFORE_READING}; + + nsresult mSocketOutCondition = NS_ERROR_NOT_INITIALIZED; + nsresult mSocketInCondition = NS_ERROR_NOT_INITIALIZED; + + std::function, nsresult>&&)> + mStreamReadyCallback; + + Mutex mMutex{"Http3WebTransportStream::mMutex"}; + nsCOMPtr mSendStreamPipeIn; + nsCOMPtr mSendStreamPipeOut MOZ_GUARDED_BY(mMutex); + + nsCOMPtr mReceiveStreamPipeIn MOZ_GUARDED_BY(mMutex); + nsCOMPtr mReceiveStreamPipeOut; + + uint64_t mTotalSent = 0; + uint64_t mTotalReceived = 0; + // TODO: neqo doesn't expose this information for now. + uint64_t mTotalAcknowledged = 0; + bool mSendFin{false}; + // The error code used to reset the stream. Should be only set once. + Maybe mResetError; + // The error code used for STOP_SENDING. Should be only set once. + Maybe mStopSendingError; + + // This is used when SendFin or Reset is called when mSendState is SENDING. + nsTArray> mPendingTasks; +}; + +} // namespace mozilla::net + +#endif // mozilla_net_Http3WebTransportStream_h diff --git a/netwerk/protocol/http/HttpAuthUtils.cpp b/netwerk/protocol/http/HttpAuthUtils.cpp new file mode 100644 index 0000000000..9bd34ef769 --- /dev/null +++ b/netwerk/protocol/http/HttpAuthUtils.cpp @@ -0,0 +1,171 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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" +#include "nsIPrefBranch.h" +#include "nsIPrefService.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..bb113064a4 --- /dev/null +++ b/netwerk/protocol/http/HttpBackgroundChannelChild.cpp @@ -0,0 +1,499 @@ +/* -*- 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/Endpoint.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 may be nulled already. Use aChannelChild + aChannelChild->mCreateBackgroundChannelFailed = true; +#endif + mChannelChild = nullptr; + return NS_ERROR_FAILURE; + } + + mFirstODASource = ODA_PENDING; + mOnStopRequestCalled = false; + return NS_OK; +} + +void HttpBackgroundChannelChild::CreateDataBridge( + Endpoint&& aEndpoint) { + MOZ_ASSERT(OnSocketThread()); + + if (!mChannelChild) { + return; + } + + RefPtr dataBridgeChild = + new BackgroundDataBridgeChild(this); + aEndpoint.Bind(dataBridgeChild); +} + +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, + const HttpChannelAltDataStream& aAltData, + const TimeStamp& aOnStartRequestStart) { + 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, aAltData, + aOnStartRequestStart); + // 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 nsACString& aData, + const bool& aDataFromSocketProcess, + const TimeStamp& aOnDataAvailableStart) { + RefPtr self = this; + std::function callProcessOnTransportAndData = + [self, aChannelStatus, aTransportStatus, aOffset, aCount, + data = nsCString(aData), aDataFromSocketProcess, + aOnDataAvailableStart]() { + 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, + aOnDataAvailableStart); + }; + + // 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, const TimeStamp& aOnStopRequestStart) { + 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, aOnStopRequestStart]() mutable { + self->RecvOnStopRequest(aChannelStatus, aTiming, aLastActiveTabOptHit, + aResponseTrailers, std::move(consoleReports), + aFromSocketProcess, aOnStopRequestStart); + }); + + 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, aOnStopRequestStart); + } + 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, aOnStopRequestStart); + 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::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(); +} + +IPCResult HttpBackgroundChannelChild::RecvDetachStreamFilters() { + LOG(("HttpBackgroundChannelChild::RecvDetachStreamFilters [this=%p]\n", + this)); + MOZ_ASSERT(OnSocketThread()); + + if (NS_WARN_IF(!mChannelChild)) { + return IPC_OK(); + } + + mChannelChild->ProcessDetachStreamFilters(); + 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..da88ec9501 --- /dev/null +++ b/netwerk/protocol/http/HttpBackgroundChannelChild.h @@ -0,0 +1,156 @@ +/* -*- 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 "mozilla/ipc/Endpoint.h" +#include "nsIRunnable.h" +#include "nsTArray.h" + +using mozilla::ipc::IPCResult; + +namespace mozilla { +namespace net { + +class PBackgroundDataBridgeChild; +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, + const HttpChannelAltDataStream& aAltData, + const TimeStamp& aOnStartRequestStart); + + IPCResult RecvOnTransportAndData(const nsresult& aChannelStatus, + const nsresult& aTransportStatus, + const uint64_t& aOffset, + const uint32_t& aCount, + const nsACString& aData, + const bool& aDataFromSocketProcess, + const TimeStamp& aOnDataAvailableStart); + + IPCResult RecvOnStopRequest( + const nsresult& aChannelStatus, const ResourceTimingStructArgs& aTiming, + const TimeStamp& aLastActiveTabOptHit, + const nsHttpHeaderArray& aResponseTrailers, + nsTArray&& aConsoleReports, + const bool& aFromSocketProcess, const TimeStamp& aOnStopRequestStart); + + 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 RecvSetClassifierMatchedInfo(const ClassifierInfo& info); + + IPCResult RecvSetClassifierMatchedTrackingInfo(const ClassifierInfo& info); + + IPCResult RecvAttachStreamFilter( + Endpoint&& aEndpoint); + + IPCResult RecvDetachStreamFilters(); + + void ActorDestroy(ActorDestroyReason aWhy) override; + + void CreateDataBridge(Endpoint&& aEndpoint); + + 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 = false; + + // 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..e30fa13d37 --- /dev/null +++ b/netwerk/protocol/http/HttpBackgroundChannelParent.cpp @@ -0,0 +1,526 @@ +/* -*- 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 "mozilla/net/ChannelEventQueue.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, + const nsCOMPtr& aAltDataSource, + TimeStamp aOnStartRequestStart) { + 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, TimeStamp>( + "net::HttpBackgroundChannelParent::OnStartRequest", this, + &HttpBackgroundChannelParent::OnStartRequest, aResponseHead, + aUseResponseHead, aRequestHeaders, aArgs, aAltDataSource, + aOnStartRequestStart), + NS_DISPATCH_NORMAL); + + MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv)); + + return NS_SUCCEEDED(rv); + } + + HttpChannelAltDataStream altData; + if (aAltDataSource) { + nsAutoCString altDataType; + Unused << aAltDataSource->GetAltDataType(altDataType); + + if (!altDataType.IsEmpty()) { + nsCOMPtr inputStream; + nsresult rv = aAltDataSource->OpenAlternativeInputStream( + altDataType, getter_AddRefs(inputStream)); + if (NS_SUCCEEDED(rv)) { + Unused << mozilla::ipc::SerializeIPCStream(inputStream.forget(), + altData.altDataInputStream(), + /* aAllowLazy */ true); + } + } + } + + return SendOnStartRequest(aResponseHead, aUseResponseHead, aRequestHeaders, + aArgs, altData, aOnStartRequestStart); +} + +bool HttpBackgroundChannelParent::OnTransportAndData( + const nsresult& aChannelStatus, const nsresult& aTransportStatus, + const uint64_t& aOffset, const uint32_t& aCount, const nsCString& aData, + TimeStamp aOnDataAvailableStart) { + 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, aOnDataAvailableStart), + NS_DISPATCH_NORMAL); + + MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv)); + + return NS_SUCCEEDED(rv); + } + + nsHttp::SendFunc sendFunc = + [self = UnsafePtr(this), aChannelStatus, + aTransportStatus, + aOnDataAvailableStart](const nsDependentCSubstring& aData, + uint64_t aOffset, uint32_t aCount) { + return self->SendOnTransportAndData(aChannelStatus, aTransportStatus, + aOffset, aCount, aData, false, + aOnDataAvailableStart); + }; + + return nsHttp::SendDataInChunks(aData, aOffset, aCount, sendFunc); +} + +bool HttpBackgroundChannelParent::OnStopRequest( + const nsresult& aChannelStatus, const ResourceTimingStructArgs& aTiming, + const nsHttpHeaderArray& aResponseTrailers, + const nsTArray& aConsoleReports, + TimeStamp aOnStopRequestStart) { + 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, + TimeStamp>( + "net::HttpBackgroundChannelParent::OnStopRequest", this, + &HttpBackgroundChannelParent::OnStopRequest, aChannelStatus, + aTiming, aResponseTrailers, aConsoleReports, aOnStopRequestStart), + 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, + aOnStopRequestStart); +} + +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::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__); +} + +auto HttpBackgroundChannelParent::DetachStreamFilters() + -> RefPtr { + LOG(("HttpBackgroundChannelParent::DetachStreamFilters [this=%p]\n", this)); + if (NS_WARN_IF(!mIPCOpened) || !SendDetachStreamFilters()) { + return GenericPromise::CreateAndReject(NS_ERROR_FAILURE, __func__); + } + + return GenericPromise::CreateAndResolve(true, __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..7715d9bdfb --- /dev/null +++ b/netwerk/protocol/http/HttpBackgroundChannelParent.h @@ -0,0 +1,124 @@ +/* -*- 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, + const nsCOMPtr& aCacheEntry, + TimeStamp aOnStartRequestStart); + + // 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, + TimeStamp aOnDataAvailableStart); + + // To send OnStopRequest message over background channel. + bool OnStopRequest(const nsresult& aChannelStatus, + const ResourceTimingStructArgs& aTiming, + const nsHttpHeaderArray& aResponseTrailers, + const nsTArray& aConsoleReports, + TimeStamp aOnStopRequestStart); + + // 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 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); + + [[nodiscard]] RefPtr DetachStreamFilters(); + + protected: + void ActorDestroy(ActorDestroyReason aWhy) override; + + private: + virtual ~HttpBackgroundChannelParent(); + + Atomic mIPCOpened; + + // Used to ensure atomicity of mBackgroundThread + Mutex mBgThreadMutex MOZ_UNANNOTATED; + + 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..59242f6933 --- /dev/null +++ b/netwerk/protocol/http/HttpBaseChannel.cpp @@ -0,0 +1,6479 @@ +/* -*- 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 "mozIRemoteLazyInputStream.h" +#include "mozIThirdPartyUtil.h" +#include "mozilla/LoadInfo.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/Mutex.h" +#include "mozilla/NullPrincipal.h" +#include "mozilla/PermissionManager.h" +#include "mozilla/Components.h" +#include "mozilla/StaticPrefs_browser.h" +#include "mozilla/StaticPrefs_fission.h" +#include "mozilla/StaticPrefs_network.h" +#include "mozilla/StaticPrefs_security.h" +#include "mozilla/Telemetry.h" +#include "mozilla/Tokenizer.h" +#include "mozilla/browser/NimbusFeatures.h" +#include "mozilla/dom/BrowsingContext.h" +#include "mozilla/dom/CanonicalBrowsingContext.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/nsHTTPSOnlyUtils.h" +#include "mozilla/dom/nsMixedContentBlocker.h" +#include "mozilla/dom/Performance.h" +#include "mozilla/dom/PerformanceStorage.h" +#include "mozilla/dom/ProcessIsolation.h" +#include "mozilla/dom/RequestBinding.h" +#include "mozilla/dom/WindowGlobalParent.h" +#include "mozilla/net/OpaqueResponseUtils.h" +#include "mozilla/net/UrlClassifierCommon.h" +#include "mozilla/net/UrlClassifierFeatureFactory.h" +#include "nsBufferedStreams.h" +#include "nsCOMPtr.h" +#include "nsCRT.h" +#include "nsContentSecurityManager.h" +#include "nsContentSecurityUtils.h" +#include "nsContentUtils.h" +#include "nsDebug.h" +#include "nsEscape.h" +#include "nsGlobalWindowInner.h" +#include "nsGlobalWindowOuter.h" +#include "nsHttpChannel.h" +#include "nsHTTPCompressConv.h" +#include "nsHttpHandler.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 "nsIMultiplexInputStream.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 "nsString.h" +#include "nsThreadUtils.h" +#include "nsURLHelper.h" +#include "mozilla/RemoteLazyInputStreamChild.h" +#include "mozilla/net/SFVService.h" +#include "mozilla/dom/ContentChild.h" +#include "nsQueryObject.h" + +using mozilla::dom::RequestMode; + +#define LOGORB(msg, ...) \ + MOZ_LOG(GetORBLog(), LogLevel::Debug, \ + ("%s: %p " msg, __func__, this, ##__VA_ARGS__)) + +namespace mozilla { +namespace net { + +static bool IsHeaderBlacklistedForRedirectCopy(nsHttpAtom const& aHeader) { + // IMPORTANT: keep this list ASCII-code sorted + static nsHttpAtomLiteral const* blackList[] = { + &nsHttp::Accept, + &nsHttp::Accept_Encoding, + &nsHttp::Accept_Language, + &nsHttp::Alternate_Service_Used, + &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.get(), aVal->get()); + } + int operator()(nsHttpAtomLiteral const* aVal) const { + if (mTarget == *aVal) { + return 0; + } + return strcmp(mTarget.get(), aVal->get()); + } + }; + + 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) + +static OpaqueResponseFilterFetch ConfiguredFilterFetchResponseBehaviour() { + uint32_t pref = StaticPrefs:: + browser_opaqueResponseBlocking_filterFetchResponse_DoNotUseDirectly(); + if (NS_WARN_IF(pref > + static_cast(OpaqueResponseFilterFetch::All))) { + return OpaqueResponseFilterFetch::All; + } + + return static_cast(pref); +} + +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), + mBrowserId(0), + mAltDataLength(-1), + mChannelId(0), + mReqContentLength(0U), + mStatus(NS_OK), + mCanceled(false), + mFirstPartyClassificationFlags(0), + mThirdPartyClassificationFlags(0), + mLoadFlags(LOAD_NORMAL), + mCaps(0), + mClassOfService(0, false), + mTlsFlags(0), + mSuspendCount(0), + mInitialRwin(0), + mProxyResolveFlags(0), + mContentDispositionHint(UINT32_MAX), + mRequestMode(RequestMode::No_cors), + mRedirectMode(nsIHttpChannelInternal::REDIRECT_MODE_FOLLOW), + mLastRedirectFlags(0), + mPriority(PRIORITY_NORMAL), + mRedirectionLimit(gHttpHandler->RedirectionLimit()), + mRedirectCount(0), + mInternalRedirectCount(0), + mCachedOpaqueResponseBlockingPref( + StaticPrefs::browser_opaqueResponseBlocking()), + mChannelBlockedByOpaqueResponse(false), + mDummyChannelForImageCache(false), + mHasContentDecompressed(false) { + StoreApplyConversion(true); + StoreAllowSTS(true); + StoreTracingEnabled(true); + StoreReportTiming(true); + StoreAllowSpdy(true); + StoreAllowHttp3(true); + StoreAllowAltSvc(true); + StoreResponseTimeoutEnabled(true); + StoreAllRedirectsSameOrigin(true); + StoreAllRedirectsPassTimingAllowCheck(true); + StoreUpgradableToSecure(true); + StoreIsUserAgentHeaderModified(false); + + 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(mPrincipal.forget()); + arrayToRelease.AppendElement(mListener.forget()); + arrayToRelease.AppendElement(mCompressListener.forget()); + arrayToRelease.AppendElement(mORB.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; + } +} + +static bool isSecureOrTrustworthyURL(nsIURI* aURI) { + return aURI->SchemeIs("https") || + (StaticPrefs::network_http_encoding_trustworthy_is_https() && + nsMixedContentBlocker::IsPotentiallyTrustworthyLoopbackURL(aURI)); +} + +nsresult HttpBaseChannel::Init(nsIURI* aURI, uint32_t aCaps, + nsProxyInfo* aProxyInfo, + uint32_t aProxyResolveFlags, nsIURI* aProxyURI, + uint64_t aChannelId, + ExtContentPolicyType aContentPolicyType, + nsILoadInfo* aLoadInfo) { + 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; + mLoadInfo = aLoadInfo; + + // Construct connection info object + nsAutoCString host; + int32_t port = -1; + bool isHTTPS = isSecureOrTrustworthyURL(mURI); + + 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, + nsContentUtils::ShouldResistFingerprinting(this, + RFPTarget::HttpUserAgent)); + if (NS_FAILED(rv)) return rv; + + nsAutoCString type; + if (aProxyInfo && NS_SUCCEEDED(aProxyInfo->GetType(type)) && + !type.EqualsLiteral("unknown")) { + mProxyInfo = aProxyInfo; + } + + mCurrentThread = GetCurrentSerialEventTarget(); + 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 = do_AddRef(mLoadGroup).take(); + 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) { + if (!LoadIsOCSP()) { + return GetTRRModeImpl(aTRRMode); + } + + nsCOMPtr dns = do_GetService(NS_DNSSERVICE_CONTRACTID); + nsIDNSService::ResolverMode trrMode = nsIDNSService::MODE_NATIVEONLY; + // 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_DISABLED_MODE. + // We do this to prevent a TRR service channel's OCSP validation from + // blocking DNS resolution completely. + if (dns && NS_SUCCEEDED(dns->GetCurrentTrrMode(&trrMode)) && + trrMode == nsIDNSService::MODE_TRRONLY) { + *aTRRMode = nsIRequest::TRR_DISABLED_MODE; + return NS_OK; + } + + 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; + } + + nsAutoString customUserAgent; + bc->GetCustomUserAgent(customUserAgent); + 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 = do_AddRef(mOwner).take(); + 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) { + *aLoadInfo = do_AddRef(mLoadInfo).take(); + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetIsDocument(bool* aIsDocument) { + return NS_GetIsDocumentChannel(this, aIsDocument); +} + +NS_IMETHODIMP +HttpBaseChannel::GetNotificationCallbacks(nsIInterfaceRequestor** aCallbacks) { + *aCallbacks = do_AddRef(mCallbacks).take(); + 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() || mDummyChannelForImageCache) { + 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. + // DISPOSITION_FORCE_INLINE is used to explicitly set inline, used by + // the pdf reader when loading a attachment pdf without having to + // download it. + if (mContentDispositionHint == nsIChannel::DISPOSITION_ATTACHMENT || + mContentDispositionHint == nsIChannel::DISPOSITION_FORCE_INLINE) { + *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) { + if (!mDummyChannelForImageCache) { + MOZ_ASSERT_UNREACHABLE("HttpBaseChannel::SetContentLength"); + return NS_ERROR_NOT_IMPLEMENTED; + } + MOZ_ASSERT(mResponseHead); + mResponseHead->SetContentLength(value); + return NS_OK; +} + +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 = do_AddRef(mUploadStream).take(); + 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 = nullptr; + return NS_OK; +} + +namespace { + +class MIMEHeaderCopyVisitor final : public nsIHttpHeaderVisitor { + public: + explicit MIMEHeaderCopyVisitor(nsIMIMEInputStream* aDest) : mDest(aDest) {} + + NS_DECL_ISUPPORTS + NS_IMETHOD VisitHeader(const nsACString& aName, + const nsACString& aValue) override { + return mDest->AddHeader(PromiseFlatCString(aName).get(), + PromiseFlatCString(aValue).get()); + } + + private: + ~MIMEHeaderCopyVisitor() = default; + + nsCOMPtr mDest; +}; + +NS_IMPL_ISUPPORTS(MIMEHeaderCopyVisitor, nsIHttpHeaderVisitor) + +static void NormalizeCopyComplete(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 + + RefPtr ready = + already_AddRefed(static_cast(aClosure)); + if (NS_SUCCEEDED(aStatus)) { + ready->Resolve(true, __func__); + } else { + ready->Reject(aStatus, __func__); + } +} + +// Normalize the upload stream for a HTTP channel, so that is one of the +// expected and compatible types. Components like WebExtensions and DevTools +// expect that upload streams in the parent process are cloneable, seekable, and +// synchronous to read, which this function helps guarantee somewhat efficiently +// and without loss of information. +// +// If the replacement stream outparameter is not initialized to `nullptr`, the +// returned stream should be used instead of `aUploadStream` as the upload +// stream for the HTTP channel, and the previous stream should not be touched +// again. +// +// If aReadyPromise is non-nullptr after the function is called, it is a promise +// which should be awaited before continuing to `AsyncOpen` the HTTP channel, +// as the replacement stream will not be ready until it is resolved. +static nsresult NormalizeUploadStream(nsIInputStream* aUploadStream, + nsIInputStream** aReplacementStream, + GenericPromise** aReadyPromise) { + MOZ_ASSERT(XRE_IsParentProcess()); + + *aReplacementStream = nullptr; + *aReadyPromise = nullptr; + + // Unwrap RemoteLazyInputStream and normalize the contents as we're in the + // parent process. + if (nsCOMPtr lazyStream = + do_QueryInterface(aUploadStream)) { + nsCOMPtr internal; + if (NS_SUCCEEDED( + lazyStream->TakeInternalStream(getter_AddRefs(internal)))) { + nsCOMPtr replacement; + nsresult rv = NormalizeUploadStream(internal, getter_AddRefs(replacement), + aReadyPromise); + NS_ENSURE_SUCCESS(rv, rv); + + if (replacement) { + replacement.forget(aReplacementStream); + } else { + internal.forget(aReplacementStream); + } + return NS_OK; + } + } + + // Preserve MIME information on the stream when normalizing. + if (nsCOMPtr mime = do_QueryInterface(aUploadStream)) { + nsCOMPtr data; + nsresult rv = mime->GetData(getter_AddRefs(data)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr replacement; + rv = + NormalizeUploadStream(data, getter_AddRefs(replacement), aReadyPromise); + NS_ENSURE_SUCCESS(rv, rv); + + if (replacement) { + nsCOMPtr replacementMime( + do_CreateInstance("@mozilla.org/network/mime-input-stream;1", &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr visitor = + new MIMEHeaderCopyVisitor(replacementMime); + rv = mime->VisitHeaders(visitor); + NS_ENSURE_SUCCESS(rv, rv); + + rv = replacementMime->SetData(replacement); + NS_ENSURE_SUCCESS(rv, rv); + + replacementMime.forget(aReplacementStream); + } + return NS_OK; + } + + // Preserve "real" buffered input streams which wrap data (i.e. are backed by + // nsBufferedInputStream), but normalize the wrapped stream. + if (nsCOMPtr buffered = + do_QueryInterface(aUploadStream)) { + nsCOMPtr data; + if (NS_SUCCEEDED(buffered->GetData(getter_AddRefs(data)))) { + nsCOMPtr replacement; + nsresult rv = NormalizeUploadStream(data, getter_AddRefs(replacement), + aReadyPromise); + NS_ENSURE_SUCCESS(rv, rv); + if (replacement) { + // This buffer size should be kept in sync with HTMLFormSubmission. + rv = NS_NewBufferedInputStream(aReplacementStream, replacement.forget(), + 8192); + NS_ENSURE_SUCCESS(rv, rv); + } + return NS_OK; + } + } + + // Preserve multiplex input streams, normalizing each individual inner stream + // to avoid unnecessary copying. + if (nsCOMPtr multiplex = + do_QueryInterface(aUploadStream)) { + uint32_t count = multiplex->GetCount(); + nsTArray> streams(count); + nsTArray> promises(count); + bool replace = false; + for (uint32_t i = 0; i < count; ++i) { + nsCOMPtr inner; + nsresult rv = multiplex->GetStream(i, getter_AddRefs(inner)); + NS_ENSURE_SUCCESS(rv, rv); + + RefPtr promise; + nsCOMPtr replacement; + rv = NormalizeUploadStream(inner, getter_AddRefs(replacement), + getter_AddRefs(promise)); + NS_ENSURE_SUCCESS(rv, rv); + if (promise) { + promises.AppendElement(promise); + } + if (replacement) { + streams.AppendElement(replacement); + replace = true; + } else { + streams.AppendElement(inner); + } + } + + // If any of the inner streams needed to be replaced, replace the entire + // nsIMultiplexInputStream. + if (replace) { + nsresult rv; + nsCOMPtr replacement = + do_CreateInstance("@mozilla.org/io/multiplex-input-stream;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + for (auto& stream : streams) { + rv = replacement->AppendStream(stream); + NS_ENSURE_SUCCESS(rv, rv); + } + + MOZ_ALWAYS_SUCCEEDS(CallQueryInterface(replacement, aReplacementStream)); + } + + // Wait for all inner promises to settle before resolving the final promise. + if (!promises.IsEmpty()) { + RefPtr ready = + GenericPromise::AllSettled(GetCurrentSerialEventTarget(), promises) + ->Then(GetCurrentSerialEventTarget(), __func__, + [](GenericPromise::AllSettledPromiseType:: + ResolveOrRejectValue&& aResults) + -> RefPtr { + MOZ_ASSERT(aResults.IsResolve(), + "AllSettled never rejects"); + for (auto& result : aResults.ResolveValue()) { + if (result.IsReject()) { + return GenericPromise::CreateAndReject( + result.RejectValue(), __func__); + } + } + return GenericPromise::CreateAndResolve(true, __func__); + }); + ready.forget(aReadyPromise); + } + return NS_OK; + } + + // If the stream is cloneable, seekable and non-async, we can allow it. Async + // input streams can cause issues, as various consumers of input streams + // expect the payload to be synchronous and `Available()` to be the length of + // the stream, which is not true for asynchronous streams. + nsCOMPtr async = do_QueryInterface(aUploadStream); + nsCOMPtr seekable = do_QueryInterface(aUploadStream); + if (NS_InputStreamIsCloneable(aUploadStream) && seekable && !async) { + return NS_OK; + } + + // Asynchronously copy our non-normalized stream into a StorageStream so that + // it is seekable, cloneable, and synchronous once the copy completes. + + NS_WARNING("Upload Stream is being copied into StorageStream"); + + nsCOMPtr storageStream; + nsresult rv = + NS_NewStorageStream(4096, UINT32_MAX, getter_AddRefs(storageStream)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr sink; + rv = storageStream->GetOutputStream(0, getter_AddRefs(sink)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr replacementStream; + rv = storageStream->NewInputStream(0, getter_AddRefs(replacementStream)); + NS_ENSURE_SUCCESS(rv, rv); + + // Ensure the source stream is buffered before starting the copy so we can use + // ReadSegments, as nsStorageStream doesn't implement WriteSegments. + nsCOMPtr source = aUploadStream; + if (!NS_InputStreamIsBuffered(aUploadStream)) { + nsCOMPtr bufferedSource; + rv = NS_NewBufferedInputStream(getter_AddRefs(bufferedSource), + source.forget(), 4096); + NS_ENSURE_SUCCESS(rv, rv); + source = bufferedSource.forget(); + } + + // Perform an AsyncCopy into the input stream on the STS. + nsCOMPtr target = + do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID); + RefPtr ready = new GenericPromise::Private(__func__); + rv = NS_AsyncCopy(source, sink, target, NS_ASYNCCOPY_VIA_READSEGMENTS, 4096, + NormalizeCopyComplete, do_AddRef(ready).take()); + if (NS_WARN_IF(NS_FAILED(rv))) { + ready.get()->Release(); + return rv; + } + + replacementStream.forget(aReplacementStream); + ready.forget(aReadyPromise); + return NS_OK; +} + +} // anonymous namespace + +NS_IMETHODIMP +HttpBaseChannel::CloneUploadStream(int64_t* aContentLength, + nsIInputStream** aClonedStream) { + NS_ENSURE_ARG_POINTER(aContentLength); + NS_ENSURE_ARG_POINTER(aClonedStream); + *aClonedStream = nullptr; + + if (!XRE_IsParentProcess()) { + NS_WARNING("CloneUploadStream is only supported in the parent process"); + return NS_ERROR_NOT_AVAILABLE; + } + + 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); + + return InternalSetUploadStream(aStream, aContentLength, !aStreamHasHeaders); +} + +nsresult HttpBaseChannel::InternalSetUploadStream( + nsIInputStream* aUploadStream, int64_t aContentLength, + bool aSetContentLengthHeader) { + // If we're not on the main thread, such as for TRR, the content length must + // be provided, as we can't normalize our upload stream. + if (!NS_IsMainThread()) { + if (aContentLength < 0) { + MOZ_ASSERT_UNREACHABLE( + "Upload content length must be explicit off-main-thread"); + return NS_ERROR_INVALID_ARG; + } + + nsCOMPtr seekable = do_QueryInterface(aUploadStream); + if (!NS_InputStreamIsCloneable(aUploadStream) || !seekable) { + MOZ_ASSERT_UNREACHABLE( + "Upload stream must be cloneable & seekable off-main-thread"); + return NS_ERROR_INVALID_ARG; + } + + mUploadStream = aUploadStream; + ExplicitSetUploadStreamLength(aContentLength, aSetContentLengthHeader); + return NS_OK; + } + + // Normalize the upload stream we're provided to ensure that it is cloneable, + // seekable, and synchronous when in the parent process. + // + // This might be an async operation, in which case ready will be returned and + // resolved when the operation is complete. + nsCOMPtr replacement; + RefPtr ready; + if (XRE_IsParentProcess()) { + nsresult rv = NormalizeUploadStream( + aUploadStream, getter_AddRefs(replacement), getter_AddRefs(ready)); + NS_ENSURE_SUCCESS(rv, rv); + } + + mUploadStream = replacement ? replacement.get() : aUploadStream; + + // Once the upload stream is ready, fetch its length before proceeding with + // AsyncOpen. + auto onReady = [self = RefPtr{this}, aContentLength, aSetContentLengthHeader, + stream = mUploadStream]() { + auto setLengthAndResume = [self, aSetContentLengthHeader](int64_t aLength) { + self->StorePendingUploadStreamNormalization(false); + self->ExplicitSetUploadStreamLength(aLength >= 0 ? aLength : 0, + aSetContentLengthHeader); + self->MaybeResumeAsyncOpen(); + }; + + if (aContentLength >= 0) { + setLengthAndResume(aContentLength); + return; + } + + int64_t length; + if (InputStreamLengthHelper::GetSyncLength(stream, &length)) { + setLengthAndResume(length); + return; + } + + InputStreamLengthHelper::GetAsyncLength(stream, setLengthAndResume); + }; + StorePendingUploadStreamNormalization(true); + + // Resolve onReady synchronously unless a promise is returned. + if (ready) { + ready->Then(GetCurrentSerialEventTarget(), __func__, + [onReady = std::move(onReady)]( + GenericPromise::ResolveOrRejectValue&&) { onReady(); }); + } else { + onReady(); + } + return NS_OK; +} + +void HttpBaseChannel::ExplicitSetUploadStreamLength( + uint64_t aContentLength, bool aSetContentLengthHeader) { + // We already have the content length. We don't need to determinate it. + mReqContentLength = aContentLength; + + if (!aSetContentLengthHeader) { + 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::MaybeWaitForUploadStreamNormalization( + nsIStreamListener* aListener, nsISupports* aContext) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!LoadAsyncOpenWaitingForStreamNormalization(), + "AsyncOpen() called twice?"); + + if (!LoadPendingUploadStreamNormalization()) { + return false; + } + + mListener = aListener; + StoreAsyncOpenWaitingForStreamNormalization(true); + return true; +} + +void HttpBaseChannel::MaybeResumeAsyncOpen() { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!LoadPendingUploadStreamNormalization()); + + if (!LoadAsyncOpenWaitingForStreamNormalization()) { + return; + } + + nsCOMPtr listener; + listener.swap(mListener); + + StoreAsyncOpenWaitingForStreamNormalization(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, + isSecureOrTrustworthyURL(mURI))) { + RefPtr converter = new nsHTTPCompressConv(); + nsAutoCString from(val); + ToLowerCase(from); + rv = converter->AsyncConvertData(from.get(), "uncompressed", nextListener, + aCtxt); + 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 = do_AddRef(nextListener).take(); + 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::SetBrowserId(uint64_t aId) { + mBrowserId = aId; + return NS_OK; +} + +NS_IMETHODIMP HttpBaseChannel::GetBrowserId(uint64_t* aId) { + EnsureBrowserId(); + *aId = mBrowserId; + 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, + mLoadInfo->GetOriginAttributes().mPrivateBrowsingId > 0); + 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::GetTransferSize(uint64_t* aTransferSize) { + MutexAutoLock lock(mOnDataFinishedMutex); + *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) { + MutexAutoLock lock(mOnDataFinishedMutex); + *aEncodedBodySize = mEncodedBodySize; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetSupportsHTTP3(bool* aSupportsHTTP3) { + *aSupportsHTTP3 = mSupportsHTTP3; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetHasHTTPSRR(bool* aHasHTTPSRR) { + *aHasHTTPSRR = LoadHasHTTPSRR(); + 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 (aClone) { + // Record the telemetry once we set the referrer info to the channel + // successfully. + referrerInfo->RecordTelemetry(this); + } + + 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; + } + + // Mark that the User-Agent header has been modified. + if (nsHttp::ResolveAtom(aHeader) == nsHttp::User_Agent) { + StoreIsUserAgentHeaderModified(true); + } + + 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; + } + + // Mark that the User-Agent header has been modified. + if (nsHttp::ResolveAtom(aHeader) == nsHttp::User_Agent) { + StoreIsUserAgentHeaderModified(true); + } + + 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::GetAllowSTS(bool* value) { + NS_ENSURE_ARG_POINTER(value); + *value = LoadAllowSTS(); + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::SetAllowSTS(bool value) { + ENSURE_CALLED_BEFORE_CONNECT(); + StoreAllowSTS(value); + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetIsOCSP(bool* value) { + NS_ENSURE_ARG_POINTER(value); + *value = LoadIsOCSP(); + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::SetIsOCSP(bool value) { + ENSURE_CALLED_BEFORE_CONNECT(); + StoreIsOCSP(value); + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetIsUserAgentHeaderModified(bool* value) { + NS_ENSURE_ARG_POINTER(value); + *value = LoadIsUserAgentHeaderModified(); + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::SetIsUserAgentHeaderModified(bool value) { + StoreIsUserAgentHeaderModified(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( + nsITransportSecurityInfo* 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; + nsAutoCString version; + // https://fetch.spec.whatwg.org : + // Responses over an HTTP/2 connection will always have the empty byte + // sequence as status message as HTTP/2 does not support them. + if (NS_WARN_IF(NS_FAILED(GetProtocolVersion(version))) || + !version.EqualsLiteral("h2")) { + 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) { + // Try to use ALPN if available and if it is not for a proxy, i.e if an + // https proxy was not used or if https proxy was used but the connection to + // the origin server is also https. In the case, an https proxy was used and + // the connection to the origin server was http, mSecurityInfo will be from + // the proxy. + if (!mConnectionInfo || !mConnectionInfo->UsingHttpsProxy() || + mConnectionInfo->EndToEndSSL()) { + nsAutoCString protocol; + if (mSecurityInfo && + NS_SUCCEEDED(mSecurityInfo->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 = components::ThirdPartyUtil::Service(); + 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 + } + } + *aTopWindowURI = do_AddRef(mTopWindowURI).take(); + return rv; +} + +NS_IMETHODIMP +HttpBaseChannel::GetDocumentURI(nsIURI** aDocumentURI) { + NS_ENSURE_ARG_POINTER(aDocumentURI); + *aDocumentURI = do_AddRef(mDocumentURI).take(); + 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 there is no loadGroup attached to the current channel, we check the + // global private browsing state for the private channel instead. For + // non-private channel, we will always return false here. + // + // Note that we can only access the global private browsing state in the + // parent process. So, we will fallback to just return false in the content + // process. + if (!mLoadGroup) { + if (!XRE_IsParentProcess()) { + return false; + } + + return mLoadInfo->GetOriginAttributes().mPrivateBrowsingId != 0 && + !dom::CanonicalBrowsingContext::IsPrivateBrowsingActive(); + } + + return mLoadGroup->GetIsBrowsingContextDiscarded(); +} + +// 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; + bool isCoepCredentiallessEnabled; + rv = mLoadInfo->GetIsOriginTrialCoepCredentiallessEnabledForTopLevel( + &isCoepCredentiallessEnabled); + NS_ENSURE_SUCCESS(rv, rv); + rv = GetResponseEmbedderPolicy(isCoepCredentiallessEnabled, &resultPolicy); + if (NS_FAILED(rv)) { + return NS_OK; + } + + // https://html.spec.whatwg.org/multipage/origin.html#coep + if (mLoadInfo->GetExternalContentPolicyType() == + ExtContentPolicy::TYPE_SUBDOCUMENT && + !nsHttpChannel::IsRedirectStatus(mResponseHead->Status()) && + mLoadInfo->GetLoadingEmbedderPolicy() != + nsILoadInfo::EMBEDDER_POLICY_NULL && + resultPolicy != nsILoadInfo::EMBEDDER_POLICY_REQUIRE_CORP && + resultPolicy != nsILoadInfo::EMBEDDER_POLICY_CREDENTIALLESS) { + return NS_ERROR_DOM_COEP_FAILED; + } + + return NS_OK; +} + +// https://mikewest.github.io/corpp/#corp-check +nsresult HttpBaseChannel::ProcessCrossOriginResourcePolicyHeader() { + // Fetch 4.5.9 + dom::RequestMode requestMode; + MOZ_ALWAYS_SUCCEEDS(GetRequestMode(&requestMode)); + // XXX this seems wrong per spec? What about navigate + if (requestMode != RequestMode::No_cors) { + return NS_OK; + } + + // We only apply this for resources. + auto extContentPolicyType = mLoadInfo->GetExternalContentPolicyType(); + if (extContentPolicyType == ExtContentPolicy::TYPE_DOCUMENT || + extContentPolicyType == ExtContentPolicy::TYPE_WEBSOCKET || + extContentPolicyType == ExtContentPolicy::TYPE_SAVEAS_DOWNLOAD) { + return NS_OK; + } + + if (extContentPolicyType == 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; + } + + if (mLoadInfo->GetLoadingPrincipal()->IsSystemPrincipal()) { + return NS_OK; + } + + nsAutoCString content; + Unused << mResponseHead->GetHeader(nsHttp::Cross_Origin_Resource_Policy, + content); + + if (StaticPrefs::browser_tabs_remote_useCrossOriginEmbedderPolicy()) { + if (content.IsEmpty()) { + if (mLoadInfo->GetLoadingEmbedderPolicy() == + nsILoadInfo::EMBEDDER_POLICY_CREDENTIALLESS) { + bool requestIncludesCredentials = false; + nsresult rv = GetCorsIncludeCredentials(&requestIncludesCredentials); + if (NS_FAILED(rv)) { + return NS_OK; + } + // COEP: Set policy to `same-origin` if: response’s + // request-includes-credentials is true, or forNavigation is true. + if (requestIncludesCredentials || + extContentPolicyType == ExtContentPolicyType::TYPE_SUBDOCUMENT) { + content = "same-origin"_ns; + } + } else if (mLoadInfo->GetLoadingEmbedderPolicy() == + nsILoadInfo::EMBEDDER_POLICY_REQUIRE_CORP) { + // 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. + 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() { + MOZ_ASSERT(XRE_IsParentProcess()); + + 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; + } + + nsCOMPtr resultOrigin; + nsContentUtils::GetSecurityManager()->GetChannelResultPrincipal( + this, getter_AddRefs(resultOrigin)); + + // 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; + + // Add a permission to mark this site as high-value into the permission DB. + if (resultPolicy != nsILoadInfo::OPENER_POLICY_UNSAFE_NONE) { + mozilla::dom::AddHighValuePermission( + resultOrigin, mozilla::dom::kHighValueCOOPPermission); + } + + // 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 && + mLoadInfo->GetSandboxFlags()) { + LOG(( + "HttpBaseChannel::ComputeCrossOriginOpenerPolicyMismatch network error " + "for non empty sandboxing and non null COOP")); + return NS_ERROR_DOM_COOP_FAILED; + } + + // In xpcshell-tests we don't always have a current window global + RefPtr currentWindowGlobal = + ctx->Canonical()->GetCurrentWindowGlobal(); + if (!currentWindowGlobal) { + return NS_OK; + } + + // We use the top window principal as the documentOrigin + nsCOMPtr documentOrigin = + currentWindowGlobal->DocumentPrincipal(); + + 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 (!currentWindowGlobal->IsInitialDocument()) { + StoreHasCrossOriginOpenerPolicyMismatch(true); + return NS_OK; + } + + return NS_OK; +} + +nsresult HttpBaseChannel::ProcessCrossOriginSecurityHeaders() { + StoreProcessCrossOriginSecurityHeadersCalled(true); + nsresult rv = ProcessCrossOriginEmbedderPolicyHeader(); + if (NS_FAILED(rv)) { + return rv; + } + rv = ProcessCrossOriginResourcePolicyHeader(); + if (NS_FAILED(rv)) { + return rv; + } + return ComputeCrossOriginOpenerPolicyMismatch(); +} + +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_WORKER_STATIC_MODULE: + 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; + } + + if (aLoadInfo->GetLoadingPrincipal()->IsSameOrigin(aURI)) { + // 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"_ns), corsOrigin); + if (NS_SUCCEEDED(rv)) { + if (corsOrigin.Equals("*")) { + cors = true; + } else { + nsCOMPtr corsOriginURI; + rv = NS_NewURI(getter_AddRefs(corsOriginURI), corsOrigin); + if (NS_SUCCEEDED(rv)) { + if (aLoadInfo->GetLoadingPrincipal()->IsSameOrigin(corsOriginURI)) { + 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 || + internalType == nsIContentPolicy::TYPE_INTERNAL_WORKER_STATIC_MODULE) { + 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; +} + +bool HttpBaseChannel::ShouldFilterOpaqueResponse( + OpaqueResponseFilterFetch aFilterType) const { + MOZ_DIAGNOSTIC_ASSERT(ShouldBlockOpaqueResponse()); + + if (!mLoadInfo || ConfiguredFilterFetchResponseBehaviour() != aFilterType) { + return false; + } + + // We should filter a response in the parent if it is opaque and is the result + // of a fetch() function from the Fetch specification. + return mLoadInfo->InternalContentPolicyType() == nsIContentPolicy::TYPE_FETCH; +} + +bool HttpBaseChannel::ShouldBlockOpaqueResponse() const { + if (!mURI || !mResponseHead || !mLoadInfo) { + // if there is no uri, no response head or no loadInfo, then there is + // nothing to do + LOGORB("No block: no mURI, mResponseHead, or mLoadInfo"); + return false; + } + + nsCOMPtr principal = mLoadInfo->GetLoadingPrincipal(); + if (!principal || principal->IsSystemPrincipal()) { + // If it's a top-level load or a system principal, then there is nothing to + // do. + LOGORB("No block: top-level load or system principal"); + return false; + } + + // Check if the response is a opaque response, which means requestMode should + // be RequestMode::No_cors and responseType should be ResponseType::Opaque. + nsContentPolicyType contentPolicy = mLoadInfo->InternalContentPolicyType(); + + // Skip the RequestMode would be RequestMode::Navigate + if (contentPolicy == nsIContentPolicy::TYPE_DOCUMENT || + contentPolicy == nsIContentPolicy::TYPE_SUBDOCUMENT || + contentPolicy == nsIContentPolicy::TYPE_INTERNAL_FRAME || + contentPolicy == nsIContentPolicy::TYPE_INTERNAL_IFRAME || + // Skip the RequestMode would be RequestMode::Same_origin + contentPolicy == nsIContentPolicy::TYPE_INTERNAL_WORKER || + contentPolicy == nsIContentPolicy::TYPE_INTERNAL_SHARED_WORKER) { + return false; + } + + uint32_t securityMode = mLoadInfo->GetSecurityMode(); + // Skip when RequestMode would not be RequestMode::no_cors + if (securityMode != + nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_INHERITS_SEC_CONTEXT && + securityMode != nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL) { + LOGORB("No block: not no_cors requests"); + return false; + } + + // Only continue when ResponseType would be ResponseType::Opaque + if (mLoadInfo->GetTainting() != mozilla::LoadTainting::Opaque) { + LOGORB("No block: not opaque response"); + return false; + } + + auto extContentPolicyType = mLoadInfo->GetExternalContentPolicyType(); + if (extContentPolicyType == ExtContentPolicy::TYPE_OBJECT || + extContentPolicyType == ExtContentPolicy::TYPE_OBJECT_SUBREQUEST || + extContentPolicyType == ExtContentPolicy::TYPE_WEBSOCKET || + extContentPolicyType == ExtContentPolicy::TYPE_SAVEAS_DOWNLOAD) { + LOGORB("No block: object || websocket request || save as download"); + return false; + } + + // Ignore the request from object or embed elements + if (mLoadInfo->GetIsFromObjectOrEmbed()) { + LOGORB("No block: Request From or "); + return false; + } + + // Exclude no_cors System XHR + if (extContentPolicyType == ExtContentPolicy::TYPE_XMLHTTPREQUEST) { + if (securityMode == + nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_INHERITS_SEC_CONTEXT) { + LOGORB("No block: System XHR"); + return false; + } + } + + uint32_t httpsOnlyStatus = mLoadInfo->GetHttpsOnlyStatus(); + if (httpsOnlyStatus & nsILoadInfo::HTTPS_ONLY_BYPASS_ORB) { + LOGORB("No block: HTTPS_ONLY_BYPASS_ORB"); + return false; + } + + bool isInDevToolsContext; + mLoadInfo->GetIsInDevToolsContext(&isInDevToolsContext); + if (isInDevToolsContext) { + LOGORB("No block: Request created by devtools"); + return false; + } + + return true; +} + +OpaqueResponse HttpBaseChannel::BlockOrFilterOpaqueResponse( + OpaqueResponseBlocker* aORB, const nsAString& aReason, + const OpaqueResponseBlockedTelemetryReason aTelemetryReason, + const char* aFormat, ...) { + NimbusFeatures::RecordExposureEvent("opaqueResponseBlocking"_ns, true); + + const bool shouldFilter = + ShouldFilterOpaqueResponse(OpaqueResponseFilterFetch::BlockedByORB); + + if (MOZ_UNLIKELY(MOZ_LOG_TEST(GetORBLog(), LogLevel::Debug))) { + va_list ap; + va_start(ap, aFormat); + nsVprintfCString logString(aFormat, ap); + va_end(ap); + + LOGORB("%s: %s", shouldFilter ? "Filtered" : "Blocked", logString.get()); + } + + if (shouldFilter) { + Telemetry::AccumulateCategorical( + Telemetry::LABELS_ORB_BLOCK_INITIATOR::FILTERED_FETCH); + // The existence of `mORB` depends on `BlockOrFilterOpaqueResponse` being + // called before or after sniffing has completed. + // Another requirement is that `OpaqueResponseFilter` must come after + // `OpaqueResponseBlocker`, which is why in the case of having an + // `OpaqueResponseBlocker` we let it handle creating an + // `OpaqueResponseFilter`. + if (aORB) { + MOZ_DIAGNOSTIC_ASSERT(!mORB || aORB == mORB); + aORB->FilterResponse(); + } else { + mListener = new OpaqueResponseFilter(mListener); + } + return OpaqueResponse::Allow; + } + + LogORBError(aReason, aTelemetryReason); + return OpaqueResponse::Block; +} + +// The specification for ORB is currently being written: +// https://whatpr.org/fetch/1442.html#orb-algorithm +// The `opaque-response-safelist check` is implemented in: +// * `HttpBaseChannel::PerformOpaqueResponseSafelistCheckBeforeSniff` +// * `nsHttpChannel::DisableIsOpaqueResponseAllowedAfterSniffCheck` +// * `HttpBaseChannel::PerformOpaqueResponseSafelistCheckAfterSniff` +// * `OpaqueResponseBlocker::ValidateJavaScript` +OpaqueResponse +HttpBaseChannel::PerformOpaqueResponseSafelistCheckBeforeSniff() { + MOZ_ASSERT(XRE_IsParentProcess()); + + // https://whatpr.org/fetch/1442.html#http-fetch, step 6.4 + if (!ShouldBlockOpaqueResponse()) { + return OpaqueResponse::Allow; + } + + // Regardless of if ORB is enabled or not, we check if we should filter the + // response in the parent. This way data won't reach a content process that + // will create a filtered `Response` object. This is enabled when + // 'browser.opaqueResponseBlocking.filterFetchResponse' is + // `OpaqueResponseFilterFetch::All`. + // See https://fetch.spec.whatwg.org/#concept-filtered-response-opaque + if (ShouldFilterOpaqueResponse(OpaqueResponseFilterFetch::All)) { + mListener = new OpaqueResponseFilter(mListener); + + // If we're filtering a response in the parent, there will be no data to + // determine if it should be blocked or not so the only option we have is to + // allow it. + return OpaqueResponse::Allow; + } + + if (!mCachedOpaqueResponseBlockingPref) { + return OpaqueResponse::Allow; + } + + // If ORB is enabled, we check if we should filter the response in the parent. + // This way data won't reach a content process that will create a filtered + // `Response` object. We allow ORB to determine if the response should be + // blocked or filtered, but regardless no data should reach the content + // process. This is enabled when + // 'browser.opaqueResponseBlocking.filterFetchResponse' is + // `OpaqueResponseFilterFetch::AllowedByORB`. + // See https://fetch.spec.whatwg.org/#concept-filtered-response-opaque + if (ShouldFilterOpaqueResponse(OpaqueResponseFilterFetch::AllowedByORB)) { + mListener = new OpaqueResponseFilter(mListener); + } + + Telemetry::ScalarAdd( + Telemetry::ScalarID:: + OPAQUE_RESPONSE_BLOCKING_CROSS_ORIGIN_OPAQUE_RESPONSE_COUNT, + 1); + + PROFILER_MARKER_TEXT("ORB safelist check", NETWORK, {}, "Before sniff"_ns); + + // https://whatpr.org/fetch/1442.html#orb-algorithm + // Step 1 + nsAutoCString contentType; + mResponseHead->ContentType(contentType); + + // Step 2 + nsAutoCString contentTypeOptionsHeader; + bool nosniff = + mResponseHead->GetContentTypeOptionsHeader(contentTypeOptionsHeader) && + contentTypeOptionsHeader.EqualsIgnoreCase("nosniff"); + + // Step 3 + switch (GetOpaqueResponseBlockedReason(contentType, mResponseHead->Status(), + nosniff)) { + case OpaqueResponseBlockedReason::ALLOWED_SAFE_LISTED: + // Step 3.1 + return OpaqueResponse::Allow; + case OpaqueResponseBlockedReason::ALLOWED_SAFE_LISTED_SPEC_BREAKING: + LOGORB("Allowed %s in a spec breaking way", contentType.get()); + return OpaqueResponse::Allow; + case OpaqueResponseBlockedReason::BLOCKED_BLOCKLISTED_NEVER_SNIFFED: + return BlockOrFilterOpaqueResponse( + mORB, u"mimeType is an opaque-blocklisted-never-sniffed MIME type"_ns, + OpaqueResponseBlockedTelemetryReason::MIME_NEVER_SNIFFED, + "BLOCKED_BLOCKLISTED_NEVER_SNIFFED"); + case OpaqueResponseBlockedReason::BLOCKED_206_AND_BLOCKLISTED: + // Step 3.3 + return BlockOrFilterOpaqueResponse( + mORB, + u"response's status is 206 and mimeType is an opaque-blocklisted MIME type"_ns, + OpaqueResponseBlockedTelemetryReason::RESP_206_BLCLISTED, + "BLOCKED_206_AND_BLOCKEDLISTED"); + case OpaqueResponseBlockedReason:: + BLOCKED_NOSNIFF_AND_EITHER_BLOCKLISTED_OR_TEXTPLAIN: + // Step 3.4 + return BlockOrFilterOpaqueResponse( + mORB, + u"nosniff is true and mimeType is an opaque-blocklisted MIME type or its essence is 'text/plain'"_ns, + OpaqueResponseBlockedTelemetryReason::NOSNIFF_BLC_OR_TEXTP, + "BLOCKED_NOSNIFF_AND_EITHER_BLOCKLISTED_OR_TEXTPLAIN"); + default: + break; + } + + // Step 4 + // If it's a media subsequent request, we assume that it will only be made + // after a successful initial request. + bool isMediaRequest; + mLoadInfo->GetIsMediaRequest(&isMediaRequest); + if (isMediaRequest) { + bool isMediaInitialRequest; + mLoadInfo->GetIsMediaInitialRequest(&isMediaInitialRequest); + if (!isMediaInitialRequest) { + return OpaqueResponse::Allow; + } + } + + // Step 5 + if (mResponseHead->Status() == 206 && + !IsFirstPartialResponse(*mResponseHead)) { + return BlockOrFilterOpaqueResponse( + mORB, u"response status is 206 and not first partial response"_ns, + OpaqueResponseBlockedTelemetryReason::RESP_206_BLCLISTED, + "Is not a valid partial response given 0"); + } + + // Setup for steps 6, 7, 8 and 10. + // Steps 6 and 7 are handled by the sniffer framework. + // Steps 8 and 10 by are handled by + // `nsHttpChannel::DisableIsOpaqueResponseAllowedAfterSniffCheck` + if (mLoadFlags & nsIChannel::LOAD_CALL_CONTENT_SNIFFERS) { + mSnifferCategoryType = SnifferCategoryType::All; + } else { + mSnifferCategoryType = SnifferCategoryType::OpaqueResponseBlocking; + } + + mLoadFlags |= (nsIChannel::LOAD_CALL_CONTENT_SNIFFERS | + nsIChannel::LOAD_MEDIA_SNIFFER_OVERRIDES_CONTENT_TYPE); + + // Install an input stream listener that performs ORB checks that depend on + // inspecting the incoming data. It is crucial that `OnStartRequest` is called + // on this listener either after sniffing is completed or that we skip + // sniffing, otherwise `OpaqueResponseBlocker` will allow responses that it + // shouldn't. + mORB = new OpaqueResponseBlocker(mListener, this, contentType, nosniff); + mListener = mORB; + + nsAutoCString contentEncoding; + nsresult rv = + mResponseHead->GetHeader(nsHttp::Content_Encoding, contentEncoding); + + if (NS_SUCCEEDED(rv) && !contentEncoding.IsEmpty()) { + return OpaqueResponse::SniffCompressed; + } + mLoadFlags |= (nsIChannel::LOAD_CALL_CONTENT_SNIFFERS | + nsIChannel::LOAD_MEDIA_SNIFFER_OVERRIDES_CONTENT_TYPE); + return OpaqueResponse::Sniff; +} + +// The specification for ORB is currently being written: +// https://whatpr.org/fetch/1442.html#orb-algorithm +// The `opaque-response-safelist check` is implemented in: +// * `HttpBaseChannel::PerformOpaqueResponseSafelistCheckBeforeSniff` +// * `nsHttpChannel::DisableIsOpaqueResponseAllowedAfterSniffCheck` +// * `HttpBaseChannel::PerformOpaqueResponseSafelistCheckAfterSniff` +// * `OpaqueResponseBlocker::ValidateJavaScript` +OpaqueResponse HttpBaseChannel::PerformOpaqueResponseSafelistCheckAfterSniff( + const nsACString& aContentType, bool aNoSniff) { + PROFILER_MARKER_TEXT("ORB safelist check", NETWORK, {}, "After sniff"_ns); + + // https://whatpr.org/fetch/1442.html#orb-algorithm + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(mCachedOpaqueResponseBlockingPref); + + // Step 9 + bool isMediaRequest; + mLoadInfo->GetIsMediaRequest(&isMediaRequest); + if (isMediaRequest) { + return BlockOrFilterOpaqueResponse( + mORB, u"after sniff: media request"_ns, + OpaqueResponseBlockedTelemetryReason::AFTER_SNIFF_MEDIA, + "media request"); + } + + // Step 11 + if (aNoSniff) { + return BlockOrFilterOpaqueResponse( + mORB, u"after sniff: nosniff is true"_ns, + OpaqueResponseBlockedTelemetryReason::AFTER_SNIFF_NOSNIFF, "nosniff"); + } + + // Step 12 + if (mResponseHead && + (mResponseHead->Status() < 200 || mResponseHead->Status() > 299)) { + return BlockOrFilterOpaqueResponse( + mORB, u"after sniff: status code is not in allowed range"_ns, + OpaqueResponseBlockedTelemetryReason::AFTER_SNIFF_STA_CODE, + "status code (%d) is not allowed", mResponseHead->Status()); + } + + // Step 13 + if (!mResponseHead || aContentType.IsEmpty()) { + LOGORB("Allowed: mimeType is failure"); + return OpaqueResponse::Allow; + } + + // Step 14 + if (StringBeginsWith(aContentType, "image/"_ns) || + StringBeginsWith(aContentType, "video/"_ns) || + StringBeginsWith(aContentType, "audio/"_ns)) { + return BlockOrFilterOpaqueResponse( + mORB, + u"after sniff: content-type declares image/video/audio, but sniffing fails"_ns, + OpaqueResponseBlockedTelemetryReason::AFTER_SNIFF_CT_FAIL, + "ContentType is image/video/audio"); + } + + return OpaqueResponse::Sniff; +} + +bool HttpBaseChannel::NeedOpaqueResponseAllowedCheckAfterSniff() const { + return mORB ? mORB->IsSniffing() : false; +} + +void HttpBaseChannel::BlockOpaqueResponseAfterSniff( + const nsAString& aReason, + const OpaqueResponseBlockedTelemetryReason aTelemetryReason) { + MOZ_DIAGNOSTIC_ASSERT(mORB); + LogORBError(aReason, aTelemetryReason); + mORB->BlockResponse(this, NS_ERROR_FAILURE); +} + +void HttpBaseChannel::AllowOpaqueResponseAfterSniff() { + MOZ_DIAGNOSTIC_ASSERT(mORB); + mORB->AllowResponse(); +} + +void HttpBaseChannel::SetChannelBlockedByOpaqueResponse() { + mChannelBlockedByOpaqueResponse = true; + + RefPtr browsingContext = + dom::BrowsingContext::GetCurrentTopByBrowserId(mBrowserId); + if (!browsingContext) { + return; + } + + dom::WindowContext* windowContext = browsingContext->GetTopWindowContext(); + if (windowContext) { + windowContext->Canonical()->SetShouldReportHasBlockedOpaqueResponse( + mLoadInfo->InternalContentPolicyType()); + } +} + +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) { + auto RedirectedCachekeys = mRedirectedCachekeys.Lock(); + auto& ref = RedirectedCachekeys.ref(); + ref = 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 (const 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; +} + +bool HttpBaseChannel::BypassProxy() { + return StaticPrefs::network_proxy_allow_bypass() && LoadBypassProxy(); +} + +NS_IMETHODIMP +HttpBaseChannel::GetBypassProxy(bool* aBypassProxy) { + NS_ENSURE_ARG_POINTER(aBypassProxy); + + *aBypassProxy = BypassProxy(); + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::SetBypassProxy(bool aBypassProxy) { + if (StaticPrefs::network_proxy_allow_bypass()) { + StoreBypassProxy(aBypassProxy); + } else { + NS_WARNING("bypassProxy set but network.proxy.allow_bypass is disabled"); + return NS_ERROR_FAILURE; + } + 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::GetEffectiveTRRMode(nsIRequest::TRRMode* aEffectiveTRRMode) { + *aEffectiveTRRMode = mEffectiveTRRMode; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetTrrSkipReason(nsITRRSkipReason::value* aTrrSkipReason) { + *aTrrSkipReason = mTRRSkipReason; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetIsLoadedBySocketProcess(bool* aResult) { + NS_ENSURE_ARG_POINTER(aResult); + *aResult = LoadLoadedBySocketProcess(); + 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); + *aResult = do_AddRef(mAPIRedirectToURI).take(); + 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::GetRequestMode(RequestMode* aMode) { + *aMode = mRequestMode; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::SetRequestMode(RequestMode aMode) { + MOZ_ASSERT(aMode != RequestMode::EndGuard_); + mRequestMode = 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) || + LoadForceValidateCacheContent()) { + *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(); +} + +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; + + if (!StaticPrefs::dom_serviceWorkers_enabled()) { + return 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) { + // Reset PrincipalToInherit to a null principal. We'll credit the the + // redirecting resource's result principal as the new principal's precursor. + // This means that a data: URI will end up loading in a process based on the + // redirected-from URI. + nsCOMPtr redirectPrincipal; + nsContentUtils::GetSecurityManager()->GetChannelResultPrincipal( + this, getter_AddRefs(redirectPrincipal)); + nsCOMPtr nullPrincipalToInherit = + NullPrincipal::CreateWithInheritedAttributes(redirectPrincipal); + 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)); + + // Reset our sandboxed null principal ID when cloning loadInfo for an + // externally visible redirect. + if (!isInternalRedirect) { + // If we've redirected from http to something that isn't, clear + // the "external" flag, as loads that now go to other apps should be + // allowed to go ahead and not trip infinite-loop protection + // (see bug 1717314 for context). + if (!aNewURI->SchemeIs("http") && !aNewURI->SchemeIs("https")) { + newLoadInfo->SetLoadTriggeredFromExternal(false); + } + newLoadInfo->ResetSandboxedNullPrincipalID(); + } + + newLoadInfo->AppendRedirectHistoryEntry(this, 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; + mORB = 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(nsHttp::Cookie.val(), 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); + } + + // In case we are here because an upgrade happened through mixed content + // upgrading, CSP upgrade-insecure-requests, HTTPS-Only or HTTPS-First, we + // have to recalculate the referrer based on the original referrer to + // account for the different scheme. This does NOT apply to HSTS. + // See Bug 1857894 and order of https://fetch.spec.whatwg.org/#main-fetch. + // Otherwise, if we have a new referrer policy, we want to recalculate the + // referrer based on the old computed referrer (Bug 1678545). + bool wasNonHSTSUpgrade = + (aRedirectFlags & nsIChannelEventSink::REDIRECT_STS_UPGRADE) && + (!mLoadInfo->GetHstsStatus()); + if (wasNonHSTSUpgrade) { + nsCOMPtr referrer = mReferrerInfo->GetOriginalReferrer(); + config.referrerInfo = + new dom::ReferrerInfo(referrer, mReferrerInfo->ReferrerPolicy(), + mReferrerInfo->GetSendReferrer()); + } else if (referrerPolicy != dom::ReferrerPolicy::_empty) { + nsCOMPtr referrer = mReferrerInfo->GetComputedReferrer(); + config.referrerInfo = new dom::ReferrerInfo( + referrer, referrerPolicy, mReferrerInfo->GetSendReferrer()); + } else { + config.referrerInfo = mReferrerInfo; + } + } + } + + nsCOMPtr oldTimedChannel( + do_QueryInterface(static_cast(this))); + if (oldTimedChannel) { + config.timedChannelInfo = Some(dom::TimedChannelInfo()); + config.timedChannelInfo->timingEnabled() = LoadTimingEnabled(); + config.timedChannelInfo->redirectCount() = mRedirectCount; + config.timedChannelInfo->internalRedirectCount() = mInternalRedirectCount; + config.timedChannelInfo->asyncOpen() = mAsyncOpenTime; + config.timedChannelInfo->channelCreation() = mChannelCreationTimestamp; + config.timedChannelInfo->redirectStart() = mRedirectStartTimeStamp; + config.timedChannelInfo->redirectEnd() = mRedirectEndTimeStamp; + config.timedChannelInfo->initiatorType() = mInitiatorType; + config.timedChannelInfo->allRedirectsSameOrigin() = + LoadAllRedirectsSameOrigin(); + config.timedChannelInfo->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.timedChannelInfo->timingAllowCheckForPrincipal() = + Some(oldTimedChannel->TimingAllowCheck(principal)); + } + + config.timedChannelInfo->allRedirectsPassTimingAllowCheck() = + LoadAllRedirectsPassTimingAllowCheck(); + config.timedChannelInfo->launchServiceWorkerStart() = + mLaunchServiceWorkerStart; + config.timedChannelInfo->launchServiceWorkerEnd() = mLaunchServiceWorkerEnd; + config.timedChannelInfo->dispatchFetchEventStart() = + mDispatchFetchEventStart; + config.timedChannelInfo->dispatchFetchEventEnd() = mDispatchFetchEventEnd; + config.timedChannelInfo->handleFetchEventStart() = mHandleFetchEventStart; + config.timedChannelInfo->handleFetchEventEnd() = mHandleFetchEventEnd; + config.timedChannelInfo->responseStart() = + mTransactionTimings.responseStart; + config.timedChannelInfo->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->SetClassOfService(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.timedChannelInfo && newTimedChannel) { + newTimedChannel->SetTimingEnabled(config.timedChannelInfo->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.timedChannelInfo->redirectCount()); + int32_t newCount = config.timedChannelInfo->internalRedirectCount() + 1; + newTimedChannel->SetInternalRedirectCount(std::max( + newCount, static_cast( + config.timedChannelInfo->internalRedirectCount()))); + } else { + int32_t newCount = config.timedChannelInfo->redirectCount() + 1; + newTimedChannel->SetRedirectCount(std::max( + newCount, + static_cast(config.timedChannelInfo->redirectCount()))); + newTimedChannel->SetInternalRedirectCount( + config.timedChannelInfo->internalRedirectCount()); + } + + if (shouldHideTiming) { + if (!config.timedChannelInfo->channelCreation().IsNull()) { + newTimedChannel->SetChannelCreation( + config.timedChannelInfo->channelCreation()); + } + + if (!config.timedChannelInfo->asyncOpen().IsNull()) { + newTimedChannel->SetAsyncOpen(config.timedChannelInfo->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.timedChannelInfo->redirectStart().IsNull()) { + // Only do this for real redirects. Internal redirects should be hidden. + if (!shouldHideTiming) { + newTimedChannel->SetRedirectStart(config.timedChannelInfo->asyncOpen()); + } + } else { + newTimedChannel->SetRedirectStart( + config.timedChannelInfo->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.timedChannelInfo->redirectEnd(); + } else if (!config.timedChannelInfo->responseEnd().IsNull()) { + newRedirectEnd = config.timedChannelInfo->responseEnd(); + } else { + newRedirectEnd = TimeStamp::Now(); + } + newTimedChannel->SetRedirectEnd(newRedirectEnd); + + newTimedChannel->SetInitiatorType(config.timedChannelInfo->initiatorType()); + + nsCOMPtr loadInfo = newChannel->LoadInfo(); + MOZ_ASSERT(loadInfo); + + newTimedChannel->SetAllRedirectsSameOrigin( + config.timedChannelInfo->allRedirectsSameOrigin()); + + if (config.timedChannelInfo->timingAllowCheckForPrincipal()) { + newTimedChannel->SetAllRedirectsPassTimingAllowCheck( + config.timedChannelInfo->allRedirectsPassTimingAllowCheck() && + *config.timedChannelInfo->timingAllowCheckForPrincipal()); + } + + // Propagate service worker measurements across redirects. The + // PeformanceResourceTiming.workerStart API expects to see the + // worker start time after a redirect. + newTimedChannel->SetLaunchServiceWorkerStart( + config.timedChannelInfo->launchServiceWorkerStart()); + newTimedChannel->SetLaunchServiceWorkerEnd( + config.timedChannelInfo->launchServiceWorkerEnd()); + newTimedChannel->SetDispatchFetchEventStart( + config.timedChannelInfo->dispatchFetchEventStart()); + newTimedChannel->SetDispatchFetchEventEnd( + config.timedChannelInfo->dispatchFetchEventEnd()); + newTimedChannel->SetHandleFetchEventStart( + config.timedChannelInfo->handleFetchEventStart()); + newTimedChannel->SetHandleFetchEventEnd( + config.timedChannelInfo->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(); + timedChannelInfo = aInit.timedChannelInfo(); + uploadStream = aInit.uploadStream(); + uploadStreamLength = aInit.uploadStreamLength(); + uploadStreamHasHeaders = aInit.uploadStreamHasHeaders(); + contentType = aInit.contentType(); + contentLength = aInit.contentLength(); +} + +dom::ReplacementChannelConfigInit +HttpBaseChannel::ReplacementChannelConfig::Serialize() { + dom::ReplacementChannelConfigInit config; + config.redirectFlags() = redirectFlags; + config.classOfService() = classOfService; + config.privateBrowsing() = privateBrowsing; + config.method() = method; + config.referrerInfo() = referrerInfo; + config.timedChannelInfo() = timedChannelInfo; + config.uploadStream() = + uploadStream ? RemoteLazyInputStream::WrapStream(uploadStream) : nullptr; + config.uploadStreamLength() = uploadStreamLength; + 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; + } + + 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)); + bool sameOriginWithOriginalUri = SameOriginWithOriginalUri(newURI); + if (config.timedChannelInfo && newTimedChannel) { + newTimedChannel->SetAllRedirectsSameOrigin( + config.timedChannelInfo->allRedirectsSameOrigin() && + sameOriginWithOriginalUri); + } + + newChannel->SetLoadGroup(mLoadGroup); + newChannel->SetNotificationCallbacks(mCallbacks); + // TODO: create tests for cross-origin redirect in bug 1662896. + if (sameOriginWithOriginalUri) { + newChannel->SetContentDisposition(mContentDispositionHint); + if (mContentDispositionFilename) { + newChannel->SetContentDispositionFilename(*mContentDispositionFilename); + } + } + + 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 && mRequestMode == RequestMode::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)); + } + } + + // convery the IsUserAgentHeaderModified value. + if (httpInternal) { + rv = httpInternal->SetIsUserAgentHeaderModified( + LoadIsUserAgentHeaderModified()); + 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->SetBrowserId(mBrowserId); + 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(newChannel, redirectFlags)); + } + + // 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) + { + auto redirectedCachekeys = mRedirectedCachekeys.Lock(); + auto& ref = redirectedCachekeys.ref(); + if (ref) { + LOG( + ("HttpBaseChannel::SetupReplacementChannel " + "[this=%p] transferring chain of redirect cache-keys", + this)); + rv = httpInternal->SetCacheKeysRedirectChain(ref.release()); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + } + + // Preserve Request mode. + rv = httpInternal->SetRequestMode(mRequestMode); + 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 any properties + nsCOMPtr bag(do_QueryInterface(newChannel)); + if (bag) { + for (const auto& entry : mPropertyHash) { + bag->SetProperty(entry.GetKey(), entry.GetWeak()); + } + } + + // 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 (LoadForceValidateCacheContent()) { + Unused << cacheInfoChan->SetForceValidateCacheContent(true); + } + } + + 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)); + } + + // we need to strip Authentication headers for cross-origin requests + // Ref: https://fetch.spec.whatwg.org/#http-redirect-fetch + nsAutoCString authHeader; + if (StaticPrefs::network_http_redirect_stripAuthHeader() && + NS_SUCCEEDED( + httpChannel->GetRequestHeader("Authorization"_ns, authHeader))) { + if (NS_ShouldRemoveAuthHeaderOnRedirect(static_cast(this), + newChannel, redirectFlags)) { + rv = httpChannel->SetRequestHeader("Authorization"_ns, ""_ns, false); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + } + + return NS_OK; +} + +// check whether the new channel is of same origin as the current channel +bool HttpBaseChannel::IsNewChannelSameOrigin(nsIChannel* aNewChannel) { + bool isSameOrigin = false; + nsIScriptSecurityManager* ssm = nsContentUtils::GetSecurityManager(); + + if (!ssm) { + return false; + } + + nsCOMPtr newURI; + NS_GetFinalChannelURI(aNewChannel, getter_AddRefs(newURI)); + + nsresult rv = ssm->CheckSameOriginURI(newURI, mURI, false, false); + if (NS_SUCCEEDED(rv)) { + isSameOrigin = true; + } + + return isSameOrigin; +} + +bool HttpBaseChannel::ShouldTaintReplacementChannelOrigin( + nsIChannel* aNewChannel, uint32_t aRedirectFlags) { + if (LoadTaintedOriginFlag()) { + return true; + } + + if (NS_IsInternalSameURIRedirect(this, aNewChannel, aRedirectFlags) || + NS_IsHSTSUpgradeRedirect(this, aNewChannel, aRedirectFlags)) { + return false; + } + + // If new channel is not of same origin we need to taint unless + // mURI <-> mOriginalURI/LoadingPrincipal are same origin. + if (IsNewChannelSameOrigin(aNewChannel)) { + return false; + } + + nsresult rv; + + if (mLoadInfo->GetLoadingPrincipal()) { + bool sameOrigin = false; + rv = mLoadInfo->GetLoadingPrincipal()->IsSameOrigin(mURI, &sameOrigin); + if (NS_FAILED(rv)) { + return true; + } + return !sameOrigin; + } + if (!mOriginalURI) { + return true; + } + + nsIScriptSecurityManager* ssm = nsContentUtils::GetSecurityManager(); + if (!ssm) { + return true; + } + + rv = ssm->CheckSameOriginURI(mOriginalURI, mURI, false, false); + 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; +} + +// https://fetch.spec.whatwg.org/#tao-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); + + nsAutoCString serializedOrigin; + nsContentSecurityManager::GetSerializedOrigin(aOrigin, resourcePrincipal, + serializedOrigin, mLoadInfo); + + // All redirects are same origin + if (sameOrigin && (!serializedOrigin.IsEmpty() && + !serializedOrigin.EqualsLiteral("null"))) { + *_retval = true; + return NS_OK; + } + + nsAutoCString headerValue; + rv = GetResponseHeader("Timing-Allow-Origin"_ns, headerValue); + if (NS_FAILED(rv)) { + *_retval = false; + return NS_OK; + } + + 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 == serializedOrigin || 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::GetTransactionPending(TimeStamp* _retval) { + *_retval = mTransactionTimings.transactionPending; + 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) +IMPL_TIMING_ATTR(TransactionPending) + +#undef IMPL_TIMING_ATTR + +void HttpBaseChannel::MaybeReportTimingData() { + // If performance timing is disabled, there is no need for the Performance + // object anymore. + if (!LoadTimingEnabled()) { + return; + } + + // 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; + } + + // Devtools can create fetch requests on behalf the content document. + // If we don't exclude these requests, they'd also be reported + // to the content document. + bool isInDevToolsContext; + mLoadInfo->GetIsInDevToolsContext(&isInDevToolsContext); + if (isInDevToolsContext) { + return; + } + + mozilla::dom::PerformanceStorage* documentPerformance = + mLoadInfo->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; + } + + LoadInfoArgs loadInfoArgs; + mozilla::ipc::LoadInfoToLoadInfoArgs(mLoadInfo, &loadInfoArgs); + child->SendReportFrameTimingData(loadInfoArgs, 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)); + return static_cast(mRequestContext); +} + +void HttpBaseChannel::EnsureBrowserId() { + if (mBrowserId) { + return; + } + + RefPtr bc; + MOZ_ALWAYS_SUCCEEDS(mLoadInfo->GetBrowsingContext(getter_AddRefs(bc))); + + if (bc) { + mBrowserId = bc->GetBrowserId(); + } +} + +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) { + // for internal redirect due to auth retry we do not have any limit + // as we might restrict the number of times a user might retry + // authentication + if (aRedirectFlags & nsIChannelEventSink::REDIRECT_AUTH_RETRY) { + return NS_OK; + } + // 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; + } + + // in case https-only mode is enabled which upgrades top-level requests to + // https and the page answers with a redirect (meta, 302, win.location, ...) + // then this method can break the cycle which causes the https-only exception + // page to appear. Note that https-first mode breaks upgrade downgrade endless + // loops within ShouldUpgradeHTTPSFirstRequest because https-first does not + // display an exception page but needs a soft fallback/downgrade. + if (nsHTTPSOnlyUtils::IsUpgradeDowngradeEndlessLoop( + mURI, mLoadInfo, + {nsHTTPSOnlyUtils::UpgradeDowngradeEndlessLoopOptions:: + EnforceForHTTPSOnlyMode})) { + LOG(("upgrade downgrade redirect loop!\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); + const char* snifferType = [chan]() { + if (RefPtr httpChannel = do_QueryObject(chan)) { + switch (httpChannel->GetSnifferCategoryType()) { + case SnifferCategoryType::NetContent: + return NS_CONTENT_SNIFFER_CATEGORY; + case SnifferCategoryType::OpaqueResponseBlocking: + return NS_ORB_SNIFFER_CATEGORY; + case SnifferCategoryType::All: + return NS_CONTENT_AND_ORB_SNIFFER_CATEGORY; + default: + MOZ_ASSERT_UNREACHABLE("Unexpected SnifferCategoryType!"); + } + } + + return NS_CONTENT_SNIFFER_CATEGORY; + }(); + + nsAutoCString newType; + NS_SniffContent(snifferType, 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 (nsContentUtils::ComputeIsSecureContext(this)) { + ParseServerTimingHeader(mResponseHead, aServerTiming); + ParseServerTimingHeader(mResponseTrailers, aServerTiming); + } + + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::CancelByURLClassifier(nsresult aErrorCode) { + MOZ_ASSERT( + UrlClassifierFeatureFactory::IsClassifierBlockingErrorCode(aErrorCode)); + return Cancel(aErrorCode); +} + +NS_IMETHODIMP HttpBaseChannel::SetIPv4Disabled() { + mCaps |= NS_HTTP_DISABLE_IPV4; + return NS_OK; +} + +NS_IMETHODIMP HttpBaseChannel::SetIPv6Disabled() { + mCaps |= NS_HTTP_DISABLE_IPV6; + return NS_OK; +} + +NS_IMETHODIMP HttpBaseChannel::GetResponseEmbedderPolicy( + bool aIsOriginTrialCoepCredentiallessEnabled, + 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, aIsOriginTrialCoepCredentiallessEnabled); + 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; + bool isCoepCredentiallessEnabled; + rv = mLoadInfo->GetIsOriginTrialCoepCredentiallessEnabledForTopLevel( + &isCoepCredentiallessEnabled); + if (!isCoepCredentiallessEnabled) { + nsAutoCString originTrialToken; + Unused << mResponseHead->GetHeader(nsHttp::OriginTrial, originTrialToken); + if (!originTrialToken.IsEmpty()) { + nsCOMPtr resultPrincipal; + rv = nsContentUtils::GetSecurityManager()->GetChannelResultPrincipal( + this, getter_AddRefs(resultPrincipal)); + if (!NS_WARN_IF(NS_FAILED(rv))) { + OriginTrials trials; + trials.UpdateFromToken(NS_ConvertASCIItoUTF16(originTrialToken), + resultPrincipal); + if (trials.IsEnabled(OriginTrial::CoepCredentialless)) { + isCoepCredentiallessEnabled = true; + } + } + } + } + + NS_ENSURE_SUCCESS(rv, rv); + if (NS_SUCCEEDED( + GetResponseEmbedderPolicy(isCoepCredentiallessEnabled, &coep)) && + (coep == nsILoadInfo::EMBEDDER_POLICY_REQUIRE_CORP || + coep == nsILoadInfo::EMBEDDER_POLICY_CREDENTIALLESS)) { + 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() { + // Flush if we have a known window ID. + if (mLoadInfo->GetInnerWindowID() > 0) { + FlushReportsToConsole(mLoadInfo->GetInnerWindowID()); + return; + } + + // 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_FORCE_WAIT_HTTP_RR; + return NS_OK; +} + +bool HttpBaseChannel::Http3Allowed() const { + bool isDirectOrNoProxy = + mProxyInfo ? static_cast(mProxyInfo.get())->IsDirect() + : true; + return !mUpgradeProtocolCallback && isDirectOrNoProxy && + !(mCaps & NS_HTTP_BE_CONSERVATIVE) && !LoadBeConservative() && + LoadAllowHttp3(); +} + +void HttpBaseChannel::SetDummyChannelForImageCache() { + mDummyChannelForImageCache = true; + MOZ_ASSERT(!mResponseHead, + "SetDummyChannelForImageCache should only be called once"); + mResponseHead = MakeUnique(); +} + +void HttpBaseChannel::SetEarlyHints( + nsTArray&& aEarlyHints) { + mEarlyHints = std::move(aEarlyHints); +} + +nsTArray&& HttpBaseChannel::TakeEarlyHints() { + return std::move(mEarlyHints); +} + +NS_IMETHODIMP +HttpBaseChannel::SetEarlyHintPreloaderId(uint64_t aEarlyHintPreloaderId) { + mEarlyHintPreloaderId = aEarlyHintPreloaderId; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::GetEarlyHintPreloaderId(uint64_t* aEarlyHintPreloaderId) { + NS_ENSURE_ARG_POINTER(aEarlyHintPreloaderId); + *aEarlyHintPreloaderId = mEarlyHintPreloaderId; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::SetClassicScriptHintCharset( + const nsAString& aClassicScriptHintCharset) { + mClassicScriptHintCharset = aClassicScriptHintCharset; + return NS_OK; +} + +NS_IMETHODIMP HttpBaseChannel::GetClassicScriptHintCharset( + nsAString& aClassicScriptHintCharset) { + aClassicScriptHintCharset = mClassicScriptHintCharset; + return NS_OK; +} + +NS_IMETHODIMP HttpBaseChannel::SetDocumentCharacterSet( + const nsAString& aDocumentCharacterSet) { + mDocumentCharacterSet = aDocumentCharacterSet; + return NS_OK; +} + +NS_IMETHODIMP HttpBaseChannel::GetDocumentCharacterSet( + nsAString& aDocumentCharacterSet) { + aDocumentCharacterSet = mDocumentCharacterSet; + return NS_OK; +} + +void HttpBaseChannel::SetConnectionInfo(nsHttpConnectionInfo* aCI) { + mConnectionInfo = aCI ? aCI->Clone() : nullptr; +} + +NS_IMETHODIMP +HttpBaseChannel::GetIsProxyUsed(bool* aIsProxyUsed) { + if (mProxyInfo) { + if (!static_cast(mProxyInfo.get())->IsDirect()) { + StoreIsProxyUsed(true); + } + } + *aIsProxyUsed = LoadIsProxyUsed(); + return NS_OK; +} + +static void CollectORBBlockTelemetry( + const OpaqueResponseBlockedTelemetryReason aTelemetryReason, + ExtContentPolicy aPolicy) { + Telemetry::LABELS_ORB_BLOCK_REASON label{ + static_cast(aTelemetryReason)}; + Telemetry::AccumulateCategorical(label); + + switch (aPolicy) { + case ExtContentPolicy::TYPE_INVALID: + Telemetry::AccumulateCategorical( + Telemetry::LABELS_ORB_BLOCK_INITIATOR::INVALID); + break; + case ExtContentPolicy::TYPE_OTHER: + Telemetry::AccumulateCategorical( + Telemetry::LABELS_ORB_BLOCK_INITIATOR::OTHER); + break; + case ExtContentPolicy::TYPE_FETCH: + Telemetry::AccumulateCategorical( + Telemetry::LABELS_ORB_BLOCK_INITIATOR::BLOCKED_FETCH); + break; + case ExtContentPolicy::TYPE_SCRIPT: + Telemetry::AccumulateCategorical( + Telemetry::LABELS_ORB_BLOCK_INITIATOR::SCRIPT); + break; + case ExtContentPolicy::TYPE_IMAGE: + Telemetry::AccumulateCategorical( + Telemetry::LABELS_ORB_BLOCK_INITIATOR::IMAGE); + break; + case ExtContentPolicy::TYPE_STYLESHEET: + Telemetry::AccumulateCategorical( + Telemetry::LABELS_ORB_BLOCK_INITIATOR::STYLESHEET); + break; + case ExtContentPolicy::TYPE_XMLHTTPREQUEST: + Telemetry::AccumulateCategorical( + Telemetry::LABELS_ORB_BLOCK_INITIATOR::XMLHTTPREQUEST); + break; + case ExtContentPolicy::TYPE_DTD: + Telemetry::AccumulateCategorical( + Telemetry::LABELS_ORB_BLOCK_INITIATOR::DTD); + break; + case ExtContentPolicy::TYPE_FONT: + Telemetry::AccumulateCategorical( + Telemetry::LABELS_ORB_BLOCK_INITIATOR::FONT); + break; + case ExtContentPolicy::TYPE_MEDIA: + Telemetry::AccumulateCategorical( + Telemetry::LABELS_ORB_BLOCK_INITIATOR::MEDIA); + break; + case ExtContentPolicy::TYPE_CSP_REPORT: + Telemetry::AccumulateCategorical( + Telemetry::LABELS_ORB_BLOCK_INITIATOR::CSP_REPORT); + break; + case ExtContentPolicy::TYPE_XSLT: + Telemetry::AccumulateCategorical( + Telemetry::LABELS_ORB_BLOCK_INITIATOR::XSLT); + break; + case ExtContentPolicy::TYPE_IMAGESET: + Telemetry::AccumulateCategorical( + Telemetry::LABELS_ORB_BLOCK_INITIATOR::IMAGESET); + break; + case ExtContentPolicy::TYPE_WEB_MANIFEST: + Telemetry::AccumulateCategorical( + Telemetry::LABELS_ORB_BLOCK_INITIATOR::WEB_MANIFEST); + break; + case ExtContentPolicy::TYPE_SPECULATIVE: + Telemetry::AccumulateCategorical( + Telemetry::LABELS_ORB_BLOCK_INITIATOR::SPECULATIVE); + break; + case ExtContentPolicy::TYPE_UA_FONT: + Telemetry::AccumulateCategorical( + Telemetry::LABELS_ORB_BLOCK_INITIATOR::UA_FONT); + break; + case ExtContentPolicy::TYPE_PROXIED_WEBRTC_MEDIA: + Telemetry::AccumulateCategorical( + Telemetry::LABELS_ORB_BLOCK_INITIATOR::PROXIED_WEBRTC_MEDIA); + break; + case ExtContentPolicy::TYPE_PING: + Telemetry::AccumulateCategorical( + Telemetry::LABELS_ORB_BLOCK_INITIATOR::PING); + break; + case ExtContentPolicy::TYPE_BEACON: + Telemetry::AccumulateCategorical( + Telemetry::LABELS_ORB_BLOCK_INITIATOR::BEACON); + break; + case ExtContentPolicy::TYPE_WEB_TRANSPORT: + Telemetry::AccumulateCategorical( + Telemetry::LABELS_ORB_BLOCK_INITIATOR::WEB_TRANSPORT); + break; + case ExtContentPolicy::TYPE_WEB_IDENTITY: + // Don't bother extending the telemetry for this. + Telemetry::AccumulateCategorical( + Telemetry::LABELS_ORB_BLOCK_INITIATOR::OTHER); + break; + case ExtContentPolicy::TYPE_DOCUMENT: + case ExtContentPolicy::TYPE_SUBDOCUMENT: + case ExtContentPolicy::TYPE_OBJECT: + case ExtContentPolicy::TYPE_OBJECT_SUBREQUEST: + case ExtContentPolicy::TYPE_WEBSOCKET: + case ExtContentPolicy::TYPE_SAVEAS_DOWNLOAD: + MOZ_ASSERT_UNREACHABLE("Shouldn't block this type"); + // DOCUMENT, SUBDOCUMENT, OBJECT, OBJECT_SUBREQUEST, + // WEBSOCKET and SAVEAS_DOWNLOAD are excluded from ORB + Telemetry::AccumulateCategorical( + Telemetry::LABELS_ORB_BLOCK_INITIATOR::EXCLUDED); + break; + // Do not add default: so that compilers can catch the missing case. + } +} + +void HttpBaseChannel::LogORBError( + const nsAString& aReason, + const OpaqueResponseBlockedTelemetryReason aTelemetryReason) { + auto policy = mLoadInfo->GetExternalContentPolicyType(); + CollectORBBlockTelemetry(aTelemetryReason, policy); + + // Blocking `ExtContentPolicy::TYPE_BEACON` isn't web observable, so keep + // quiet in the console about blocking it. + if (policy == ExtContentPolicy::TYPE_BEACON) { + return; + } + + RefPtr doc; + mLoadInfo->GetLoadingDocument(getter_AddRefs(doc)); + + nsAutoCString uri; + nsresult rv = nsContentUtils::AnonymizeURI(mURI, uri); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + uint64_t contentWindowId; + GetTopLevelContentWindowId(&contentWindowId); + if (contentWindowId) { + nsContentUtils::ReportToConsoleByWindowID( + u"A resource is blocked by OpaqueResponseBlocking, please check browser console for details."_ns, + nsIScriptError::warningFlag, "ORB"_ns, contentWindowId, mURI); + } + + AutoTArray params; + params.AppendElement(NS_ConvertUTF8toUTF16(uri)); + params.AppendElement(aReason); + nsContentUtils::ReportToConsole(nsIScriptError::warningFlag, "ORB"_ns, doc, + nsContentUtils::eNECKO_PROPERTIES, + "ResourceBlockedORB", params); +} + +NS_IMETHODIMP HttpBaseChannel::SetEarlyHintLinkType( + uint32_t aEarlyHintLinkType) { + mEarlyHintLinkType = aEarlyHintLinkType; + return NS_OK; +} + +NS_IMETHODIMP HttpBaseChannel::GetEarlyHintLinkType( + uint32_t* aEarlyHintLinkType) { + *aEarlyHintLinkType = mEarlyHintLinkType; + return NS_OK; +} + +NS_IMETHODIMP +HttpBaseChannel::SetHasContentDecompressed(bool aValue) { + LOG(("HttpBaseChannel::SetHasContentDecompressed [this=%p value=%d]\n", this, + aValue)); + mHasContentDecompressed = aValue; + return NS_OK; +} +NS_IMETHODIMP +HttpBaseChannel::GetHasContentDecompressed(bool* value) { + *value = mHasContentDecompressed; + 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..f3678bb0c9 --- /dev/null +++ b/netwerk/protocol/http/HttpBaseChannel.h @@ -0,0 +1,1194 @@ +/* -*- 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 "OpaqueResponseUtils.h" +#include "mozilla/AtomicBitfields.h" +#include "mozilla/Atomics.h" +#include "mozilla/dom/DOMTypes.h" +#include "mozilla/DataMutex.h" +#include "mozilla/net/DNS.h" +#include "mozilla/net/NeckoChannelParams.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 "nsITransportSecurityInfo.h" +#include "nsIURI.h" +#include "nsIUploadChannel2.h" +#include "nsStringEnumerator.h" +#include "nsStringFwd.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 OpaqueResponseBlocker; +class PreferredAlternativeDataTypeParams; + +enum CacheDisposition : uint8_t { + kCacheUnresolved = 0, + kCacheHit = 1, + kCacheHitViaReval = 2, + kCacheMissedViaReval = 3, + kCacheMissed = 4, + kCacheUnknown = 5 +}; + +// These need to be kept in sync with +// "browser.opaqueResponseBlocking.filterFetchResponse" +enum class OpaqueResponseFilterFetch { Never, AllowedByORB, BlockedByORB, All }; + +/* + * 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, + nsILoadInfo* aLoadInfo); + + // 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; + NS_IMETHOD SetHasContentDecompressed(bool value) override; + NS_IMETHOD GetHasContentDecompressed(bool* value) 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 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 GetHasHTTPSRR(bool* aHasHTTPSRR) 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 GetBrowserId(uint64_t* aId) override; + NS_IMETHOD SetBrowserId(uint64_t aId) override; + NS_IMETHOD GetIsProxyUsed(bool* aIsProxyUsed) 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 GetBypassProxy(bool* aBypassProxy) override; + NS_IMETHOD SetBypassProxy(bool aBypassProxy) override; + bool BypassProxy(); + + NS_IMETHOD GetIsTRRServiceChannel(bool* aTRR) override; + NS_IMETHOD SetIsTRRServiceChannel(bool aTRR) override; + NS_IMETHOD GetIsResolvedByTRR(bool* aResolvedByTRR) override; + NS_IMETHOD GetEffectiveTRRMode( + nsIRequest::TRRMode* aEffectiveTRRMode) override; + NS_IMETHOD GetTrrSkipReason(nsITRRSkipReason::value* aTrrSkipReason) override; + NS_IMETHOD GetIsLoadedBySocketProcess(bool* aResult) override; + NS_IMETHOD GetIsOCSP(bool* value) override; + NS_IMETHOD SetIsOCSP(bool value) 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 GetRequestMode(dom::RequestMode* aRequestMode) override; + NS_IMETHOD SetRequestMode(dom::RequestMode aRequestMode) 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; + NS_IMETHOD SetIPv4Disabled(void) override; + NS_IMETHOD 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( + bool aIsOriginTrialCoepCredentiallessEnabled, + nsILoadInfo::CrossOriginEmbedderPolicy* aOutPolicy) override; + + inline void CleanRedirectCacheChainIfNecessary() { + auto redirectedCachekeys = mRedirectedCachekeys.Lock(); + redirectedCachekeys.ref() = nullptr; + } + NS_IMETHOD HTTPUpgrade(const nsACString& aProtocolName, + nsIHttpUpgradeListener* aListener) override; + void DoDiagnosticAssertWhenOnStopNotCalledOnDestroy() override; + + NS_IMETHOD SetWaitForHTTPSSVCRecord() override; + + NS_IMETHOD SetEarlyHintPreloaderId(uint64_t aEarlyHintPreloaderId) override; + NS_IMETHOD GetEarlyHintPreloaderId(uint64_t* aEarlyHintPreloaderId) override; + + NS_IMETHOD SetEarlyHintLinkType(uint32_t aEarlyHintLinkType) override; + NS_IMETHOD GetEarlyHintLinkType(uint32_t* aEarlyHintLinkType) override; + + NS_IMETHOD SetIsUserAgentHeaderModified(bool value) override; + NS_IMETHOD GetIsUserAgentHeaderModified(bool* value) override; + + NS_IMETHOD SetClassicScriptHintCharset( + const nsAString& aClassicScriptHintCharset) override; + NS_IMETHOD GetClassicScriptHintCharset( + nsAString& aClassicScriptHintCharset) override; + + NS_IMETHOD SetDocumentCharacterSet( + const nsAString& aDocumentCharacterSet) override; + NS_IMETHOD GetDocumentCharacterSet(nsAString& aDocumentCharacterSet) override; + + virtual void SetConnectionInfo( + mozilla::net::nsHttpConnectionInfo* aCI) 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.Flags(); + return NS_OK; + } + + NS_IMETHOD GetIncremental(bool* outIncremental) override { + *outIncremental = mClassOfService.Incremental(); + 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(); + } + + void SetDummyChannelForImageCache(); + + const NetAddr& GetSelfAddr() { return mSelfAddr; } + const NetAddr& GetPeerAddr() { return mPeerAddr; } + + [[nodiscard]] nsresult OverrideSecurityInfo( + nsITransportSecurityInfo* aSecurityInfo); + + void LogORBError(const nsAString& aReason, + const OpaqueResponseBlockedTelemetryReason aTelemetryReason); + + 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); + + void AddClassificationFlags(uint32_t aClassificationFlags, + bool aIsThirdParty); + + const uint64_t& ChannelId() const { return mChannelId; } + + nsresult InternalSetUploadStream(nsIInputStream* uploadStream, + int64_t aContentLength = -1, + bool aSetContentLengthHeader = false); + + 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; + ClassOfService classOfService = {0, false}; + Maybe privateBrowsing = Nothing(); + Maybe method; + nsCOMPtr referrerInfo; + Maybe timedChannelInfo; + nsCOMPtr uploadStream; + uint64_t uploadStreamLength = 0; + bool uploadStreamHasHeaders = false; + Maybe contentType; + Maybe contentLength; + + dom::ReplacementChannelConfigInit Serialize(); + }; + + 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(); } + + // https://fetch.spec.whatwg.org/#concept-request-tainted-origin + bool HasRedirectTaintedOrigin() { return LoadTaintedOriginFlag(); } + + bool ChannelBlockedByOpaqueResponse() const { + return mChannelBlockedByOpaqueResponse; + } + bool CachedOpaqueResponseBlockingPref() const { + return mCachedOpaqueResponseBlockingPref; + } + + TimeStamp GetOnStartRequestStartTime() const { + return mOnStartRequestStartTime; + } + TimeStamp GetDataAvailableStartTime() const { + return mOnDataAvailableStartTime; + } + TimeStamp GetOnStopRequestStartTime() const { + return mOnStopRequestStartTime; + } + + 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); + + void MaybeReportTimingData(); + nsIURI* GetReferringPage(); + nsPIDOMWindowInner* GetInnerDOMWindow(); + + void AddCookiesToRequest(); + [[nodiscard]] virtual nsresult SetupReplacementChannel( + nsIURI*, nsIChannel*, bool preserveMethod, uint32_t redirectFlags); + + bool IsNewChannelSameOrigin(nsIChannel* aNewChannel); + + // WHATWG Fetch Standard 4.4. HTTP-redirect fetch, step 10 + virtual bool ShouldTaintReplacementChannelOrigin(nsIChannel* aNewChannel, + uint32_t aRedirectFlags); + + // 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); + + [[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); + +#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 MaybeWaitForUploadStreamNormalization(nsIStreamListener* aListener, + nsISupports* aContext); + + void MaybeFlushConsoleReports(); + + bool IsBrowsingContextDiscarded() const; + + nsresult ProcessCrossOriginEmbedderPolicyHeader(); + + nsresult ProcessCrossOriginResourcePolicyHeader(); + + nsresult ComputeCrossOriginOpenerPolicyMismatch(); + + nsresult ProcessCrossOriginSecurityHeaders(); + + nsresult ValidateMIMEType(); + + bool ShouldFilterOpaqueResponse(OpaqueResponseFilterFetch aFilterType) const; + bool ShouldBlockOpaqueResponse() const; + OpaqueResponse BlockOrFilterOpaqueResponse( + OpaqueResponseBlocker* aORB, const nsAString& aReason, + const OpaqueResponseBlockedTelemetryReason aTelemetryReason, + const char* aFormat, ...); + + OpaqueResponse PerformOpaqueResponseSafelistCheckBeforeSniff(); + + OpaqueResponse PerformOpaqueResponseSafelistCheckAfterSniff( + const nsACString& aContentType, bool aNoSniff); + + bool NeedOpaqueResponseAllowedCheckAfterSniff() const; + void BlockOpaqueResponseAfterSniff( + const nsAString& aReason, + const OpaqueResponseBlockedTelemetryReason aTelemetryReason); + void AllowOpaqueResponseAfterSniff(); + void SetChannelBlockedByOpaqueResponse(); + bool Http3Allowed() const; + + friend class OpaqueResponseBlocker; + friend class PrivateBrowsingChannel; + friend class InterceptFailedOnStop; + friend class HttpChannelParent; + + 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 mAPIRedirectToURI; + nsCOMPtr mProxyURI; + nsCOMPtr mPrincipal; + nsCOMPtr mTopWindowURI; + nsCOMPtr mListener; + // An instance of nsHTTPCompressConv + nsCOMPtr mCompressListener; + nsCOMPtr mCurrentThread; + + RefPtr mORB; + + private: + // Proxy release all members above on main thread. + void ReleaseMainThreadOnlyReferences(); + + void ExplicitSetUploadStreamLength(uint64_t aContentLength, + bool aSetContentLengthHeader); + + 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; + UniquePtr mResponseHead; + UniquePtr mResponseTrailers; + RefPtr mConnectionInfo; + nsCOMPtr mProxyInfo; + nsCOMPtr mSecurityInfo; + nsCOMPtr mUpgradeProtocolCallback; + UniquePtr mContentDispositionFilename; + nsCOMPtr mReportCollector; + + RefPtr mHttpHandler; // keep gHttpHandler alive + // Accessed on MainThread and Cache2 IO thread + DataMutex>> mRedirectedCachekeys{ + "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; + TimeStamp mOnStartRequestStartTime; + TimeStamp mOnDataAvailableStartTime; + TimeStamp mOnStopRequestStartTime; + // 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 mBrowserId; + 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; + + // mutex to guard members accessed during OnDataFinished in + // HttpChannelChild.cpp + Mutex mOnDataFinishedMutex{"HttpChannelChild::OnDataFinishedMutex"}; + + UniquePtr mSource; + + uint32_t mLoadFlags; + uint32_t mCaps; + + ClassOfService mClassOfService; + // This should be set the the actual TRR mode used to resolve the request. + // Is initially set to TRR_DEFAULT_MODE, but should be updated to the actual + // mode used by the request + nsIRequest::TRRMode mEffectiveTRRMode = nsIRequest::TRR_DEFAULT_MODE; + TRRSkippedReason mTRRSkipReason = TRRSkippedReason::TRR_UNSET; + + public: + void SetEarlyHints( + nsTArray&& aEarlyHints); + nsTArray&& TakeEarlyHints(); + + protected: + // Storing Http 103 Early Hint preloads. The parent process is responsible to + // start the early hint preloads, but the http child needs to be able to look + // them up. They are sent via IPC and stored in this variable. This is set on + // main document channel + nsTArray mEarlyHints; + // EarlyHintRegistrar id to connect back to the preload. Set on preload + // channels started from the above list + uint64_t mEarlyHintPreloaderId = 0; + uint32_t mEarlyHintLinkType = 0; + + nsString mClassicScriptHintCharset; + nsString mDocumentCharacterSet; + + // 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, 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 behave as if the VALIDATE_ALWAYS flag has been set. + // Used to force validate the cached content. + (uint32_t, ForceValidateCacheContent, 1), + + // If true, we prefer the LOAD_FROM_CACHE flag over LOAD_BYPASS_CACHE or + // LOAD_BYPASS_LOCAL_CACHE. + (uint32_t, PreferCacheLoadOverBypass, 1), + + (uint32_t, IsProxyUsed, 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 upload stream normalization or + // length is still unknown. AsyncOpen() will be retriggered when + // normalization is complete and length has been determined. + (uint32_t, AsyncOpenWaitingForStreamNormalization, 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), + + // Tainted origin flag of a request, specified by + // WHATWG Fetch Standard 2.2.5. + (uint32_t, TaintedOriginFlag, 1), + + // If the channel is being used to check OCSP + (uint32_t, IsOCSP, 1), + + // Used by system requests such as remote settings and updates to + // retry requests without proxies. + (uint32_t, BypassProxy, 1), + + // Indicate whether the response of this channel is coming from + // socket process. + (uint32_t, LoadedBySocketProcess, 1), + + // Indicates whether the user-agent header has been modifed since the channel + // was created. + (uint32_t, IsUserAgentHeaderModified, 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; + + dom::RequestMode mRequestMode; + 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; + + enum class SnifferCategoryType { + NetContent = 0, + OpaqueResponseBlocking, + All + }; + SnifferCategoryType mSnifferCategoryType = SnifferCategoryType::NetContent; + + // Used to ensure the same pref value is being used across the + // lifetime of this http channel. + const bool mCachedOpaqueResponseBlockingPref; + bool mChannelBlockedByOpaqueResponse; + + bool mDummyChannelForImageCache; + + bool mHasContentDecompressed; + + // 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 upload stream + // normalization or the InputStreamLengthHelper::GetAsyncLength callback. + (bool, PendingUploadStreamNormalization, 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), + + // True if HTTPS RR is used during the connection establishment of this + // channel. + (uint32_t, HasHTTPSRR, 1), + + // Ensures that ProcessCrossOriginSecurityHeadersCalled has been called + // before calling CallOnStartRequest. + (uint32_t, ProcessCrossOriginSecurityHeadersCalled, 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 EnsureBrowserId(); +}; + +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..393d0aa37d --- /dev/null +++ b/netwerk/protocol/http/HttpChannelChild.cpp @@ -0,0 +1,3391 @@ +/* -*- 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/PBackgroundDataBridge.h" +#include "nsHttp.h" +#include "nsICacheEntry.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/PerfStats.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/dom/LinkStyle.h" +#include "mozilla/extensions/StreamFilterParent.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 "NetworkMarker.h" +#include "nsCOMPtr.h" +#include "nsContentPolicyUtils.h" +#include "nsDOMNavigationTiming.h" +#include "nsIThreadRetargetableStreamListener.h" +#include "nsStringStream.h" +#include "nsHttpChannel.h" +#include "nsHttpHandler.h" +#include "nsQueryObject.h" +#include "nsNetUtil.h" +#include "nsSerializationHelper.h" +#include "mozilla/Attributes.h" +#include "mozilla/Telemetry.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 "nsContentSecurityManager.h" +#include "nsICompressConvStats.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 "nsIOService.h" + +#include + +using namespace mozilla::dom; +using namespace mozilla::ipc; + +namespace mozilla::net { + +//----------------------------------------------------------------------------- +// HttpChannelChild +//----------------------------------------------------------------------------- + +HttpChannelChild::HttpChannelChild() + : HttpAsyncAborter(this), + NeckoTargetHolder(nullptr), + mCacheEntryAvailable(false), + mAltDataCacheEntryAvailable(false), + mSendResumeAt(false), + mKeptAlive(false), + mIPCActorDeleted(false), + mSuspendSent(false), + mIsFirstPartOfMultiPart(false), + mIsLastPartOfMultiPart(false), + mSuspendForWaitCompleteRedirectSetup(false), + mRecvOnStartRequestSentCalled(false), + mSuspendedByWaitingForPermissionCookie(false), + mAlreadyReleased(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)); + + // See HttpChannelChild::Release, HttpChannelChild should be always destroyed + // on the main thread. + MOZ_RELEASE_ASSERT(NS_IsMainThread()); + +#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 + + mEventQ->NotifyReleasingOwner(); + + 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 || mAlreadyReleased) { + NS_LOG_RELEASE(this, 0, "HttpChannelChild"); + delete this; + return 0; + } + + // This ensures that when the refcount goes to 0 again, we don't dispatch + // yet another runnable and get in a loop. + mAlreadyReleased = true; + + // 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); + NS_DispatchToMainThread(NS_NewRunnableFunction( + "~HttpChannelChild>DoNotifyListener", + [chan = std::move(channel)] { chan->DoNotifyListener(false); })); + // 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. + 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(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); + } +} + +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, + const HttpChannelAltDataStream& aAltData, + const TimeStamp& aOnStartRequestStartTime) { + LOG(("HttpChannelChild::ProcessOnStartRequest [this=%p]\n", this)); + MOZ_ASSERT(OnSocketThread()); + + TimeStamp start = TimeStamp::Now(); + + mAltDataInputStream = DeserializeIPCStream(aAltData.altDataInputStream()); + + mEventQ->RunOrEnqueue(new NeckoTargetChannelFunctionEvent( + this, [self = UnsafePtr(this), aResponseHead, + aUseResponseHead, aRequestHeaders, aArgs, start]() { + TimeDuration delay = TimeStamp::Now() - start; + glean::networking::http_content_onstart_delay.AccumulateRawDuration( + delay); + + 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(); + aTimings.transactionPending = aArgs.transactionPending(); +} + +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); + } + + mSecurityInfo = aArgs.securityInfo(); + + ipc::MergeParentLoadInfoForwarder(aArgs.loadInfoForwarder(), mLoadInfo); + + mIsFromCache = aArgs.isFromCache(); + mIsRacing = aArgs.isRacing(); + mCacheEntryAvailable = aArgs.cacheEntryAvailable(); + mCacheEntryId = aArgs.cacheEntryId(); + mCacheFetchCount = aArgs.cacheFetchCount(); + mProtocolVersion = aArgs.protocolVersion(); + mCacheExpirationTime = aArgs.cacheExpirationTime(); + mSelfAddr = aArgs.selfAddr(); + mPeerAddr = aArgs.peerAddr(); + + mRedirectCount = aArgs.redirectCount(); + mAvailableCachedAltDataType = aArgs.altDataType(); + StoreDeliveringAltData(aArgs.deliveringAltData()); + mAltDataLength = aArgs.altDataLength(); + StoreResolvedByTRR(aArgs.isResolvedByTRR()); + mEffectiveTRRMode = aArgs.effectiveTRRMode(); + mTRRSkipReason = aArgs.trrSkipReason(); + + SetApplyConversion(aArgs.applyConversion()); + + StoreAfterOnStartRequestBegun(true); + StoreHasHTTPSRR(aArgs.hasHTTPSRR()); + + AutoEventEnqueuer ensureSerialDispatch(mEventQ); + + mCacheKey = aArgs.cacheKey(); + + StoreIsProxyUsed(aArgs.isProxyUsed()); + + // 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); + + nsAutoCString cosString; + ClassOfService::ToString(mClassOfService, cosString); + if (!mAsyncOpenTime.IsNull() && + !aArgs.timing().transactionPending().IsNull()) { + Telemetry::AccumulateTimeDelta( + Telemetry::NETWORK_ASYNC_OPEN_CHILD_TO_TRANSACTION_PENDING_EXP_MS, + cosString, mAsyncOpenTime, aArgs.timing().transactionPending()); + PerfStats::RecordMeasurement( + PerfStats::Metric::HttpChannelAsyncOpenToTransactionPending, + aArgs.timing().transactionPending() - mAsyncOpenTime); + } + + const TimeStamp now = TimeStamp::Now(); + if (!aArgs.timing().responseStart().IsNull()) { + Telemetry::AccumulateTimeDelta( + Telemetry::NETWORK_RESPONSE_START_PARENT_TO_CONTENT_EXP_MS, cosString, + aArgs.timing().responseStart(), now); + PerfStats::RecordMeasurement( + PerfStats::Metric::HttpChannelResponseStartParentToContent, + now - aArgs.timing().responseStart()); + } + if (!mOnStartRequestStartTime.IsNull()) { + PerfStats::RecordMeasurement(PerfStats::Metric::OnStartRequestToContent, + now - mOnStartRequestStartTime); + } + + StoreAllRedirectsSameOrigin(aArgs.allRedirectsSameOrigin()); + + mMultiPartID = aArgs.multiPartID(); + mIsFirstPartOfMultiPart = aArgs.isFirstPartOfMultiPart(); + mIsLastPartOfMultiPart = aArgs.isLastPartOfMultiPart(); + + 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); + })); + return; + } + + // Remember whether HTTP3 is supported + if (mResponseHead) { + mSupportsHTTP3 = + nsHttpHandler::IsHttp3SupportedByServer(mResponseHead.get()); + } + + DoOnStartRequest(this); +} + +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) { + 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)) { + CancelWithReason(rv, "HttpChannelChild listener->OnStartRequest failed"_ns); + return; + } + + nsCOMPtr listener; + rv = DoApplyContentConversions(mListener, getter_AddRefs(listener), nullptr); + if (NS_FAILED(rv)) { + CancelWithReason(rv, + "HttpChannelChild DoApplyContentConversions failed"_ns); + } 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 nsACString& aData, + const TimeStamp& aOnDataAvailableStartTime) { + 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 = nsCString(aData), + aOnDataAvailableStartTime]() { + self->mOnDataAvailableStartTime = aOnDataAvailableStartTime; + 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 nsACString& aData) { + LOG(("HttpChannelChild::OnTransportAndData [this=%p]\n", this)); + + if (!mCanceled && NS_SUCCEEDED(mStatus)) { + mStatus = aChannelStatus; + } + + if (mCanceled || NS_FAILED(mStatus)) { + return; + } + + if (!mOnDataAvailableStartTime.IsNull()) { + PerfStats::RecordMeasurement(PerfStats::Metric::OnDataAvailableToContent, + TimeStamp::Now() - mOnDataAvailableStartTime); + } + + // 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)) { + CancelWithReason(rv, "HttpChannelChild NS_NewByteInputStream failed"_ns); + return; + } + + DoOnDataAvailable(this, 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); + } + } + + // mOnProgressEventSent indicates we have flushed all the + // progress events on the main thread. It is needed if + // we do not want to dispatch OnDataFinished before sending + // all of the progress updates. + if (progress == progressMax) { + mOnProgressEventSent = true; + } +} + +void HttpChannelChild::DoOnDataAvailable(nsIRequest* aRequest, + 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, "HttpChannelChild OnDataAvailable failed"_ns); + } + } +} + +void HttpChannelChild::SendOnDataFinished(const nsresult& aChannelStatus) { + LOG(("HttpChannelChild::SendOnDataFinished [this=%p]\n", this)); + + if (mCanceled) return; + + // we need to ensure we OnDataFinished only after all the progress + // updates are dispatched on the main thread + if (StaticPrefs::network_send_OnDataFinished_after_progress_updates() && + !mOnProgressEventSent) { + return; + } + + if (mListener) { + nsCOMPtr omtEventListener = + do_QueryInterface(mListener); + if (omtEventListener) { + LOG( + ("HttpChannelChild::SendOnDataFinished sending data end " + "notification[this=%p]\n", + this)); + // We want to calculate the delta time between this call and + // ProcessOnStopRequest. Complicating things is that OnStopRequest + // could come first, and that it will run on a different thread, so + // we need to synchronize and lock data. + omtEventListener->OnDataFinished(aChannelStatus); + } else { + LOG( + ("HttpChannelChild::SendOnDataFinished missing " + "nsIThreadRetargetableStreamListener " + "implementation [this=%p]\n", + this)); + } + } +} + +class RecordStopRequestDelta final { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(RecordStopRequestDelta); + + TimeStamp mOnStopRequestTime; + TimeStamp mOnDataFinishedTime; + + private: + ~RecordStopRequestDelta() { + if (mOnDataFinishedTime.IsNull() || mOnStopRequestTime.IsNull()) { + return; + } + + TimeDuration delta = (mOnStopRequestTime - mOnDataFinishedTime); + if (delta.ToMilliseconds() < 0) { + // Because Telemetry can't handle negatives + delta = -delta; + glean::networking::http_content_ondatafinished_to_onstop_delay_negative + .AccumulateRawDuration(delta); + } else { + glean::networking::http_content_ondatafinished_to_onstop_delay + .AccumulateRawDuration(delta); + } + } +}; + +void HttpChannelChild::ProcessOnStopRequest( + const nsresult& aChannelStatus, const ResourceTimingStructArgs& aTiming, + const nsHttpHeaderArray& aResponseTrailers, + nsTArray&& aConsoleReports, bool aFromSocketProcess, + const TimeStamp& aOnStopRequestStartTime) { + LOG( + ("HttpChannelChild::ProcessOnStopRequest [this=%p, " + "aFromSocketProcess=%d]\n", + this, aFromSocketProcess)); + MOZ_ASSERT(OnSocketThread()); + { // assign some of the members that would be accessed by the listeners + // upon getting OnDataFinished notications + MutexAutoLock lock(mOnDataFinishedMutex); + mTransferSize = aTiming.transferSize(); + mEncodedBodySize = aTiming.encodedBodySize(); + } + + RefPtr timing; + TimeStamp start = TimeStamp::Now(); + if (StaticPrefs::network_send_OnDataFinished()) { + timing = new RecordStopRequestDelta; + mEventQ->RunOrEnqueue(new ChannelFunctionEvent( + [self = UnsafePtr(this)]() { + return self->GetODATarget(); + }, + [self = UnsafePtr(this), status = aChannelStatus, + start, timing]() { + TimeStamp now = TimeStamp::Now(); + TimeDuration delay = now - start; + glean::networking::http_content_ondatafinished_delay + .AccumulateRawDuration(delay); + timing->mOnDataFinishedTime = now; + self->SendOnDataFinished(status); + })); + } + mEventQ->RunOrEnqueue(new NeckoTargetChannelFunctionEvent( + this, [self = UnsafePtr(this), aChannelStatus, aTiming, + aResponseTrailers, + consoleReports = CopyableTArray{aConsoleReports.Clone()}, + aFromSocketProcess, start, timing]() mutable { + TimeStamp now = TimeStamp::Now(); + TimeDuration delay = now - start; + glean::networking::http_content_onstop_delay.AccumulateRawDuration( + delay); + if (timing) { + timing->mOnStopRequestTime = now; + } + 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::RecordChannelCompletionDurationForEarlyHint() { + if (!mLoadGroup) { + return; + } + + uint32_t earlyHintType = 0; + nsCOMPtr req; + Unused << mLoadGroup->GetDefaultLoadRequest(getter_AddRefs(req)); + if (nsCOMPtr httpChannel = do_QueryInterface(req)) { + Unused << httpChannel->GetEarlyHintLinkType(&earlyHintType); + } + + if (!earlyHintType) { + return; + } + + nsAutoCString earlyHintKey; + if (mIsFromCache) { + earlyHintKey.Append("cache_"_ns); + } else { + earlyHintKey.Append("net_"_ns); + } + if (earlyHintType & LinkStyle::ePRECONNECT) { + earlyHintKey.Append("preconnect_"_ns); + } + if (earlyHintType & LinkStyle::ePRELOAD) { + earlyHintKey.Append("preload_"_ns); + earlyHintKey.Append(mEarlyHintPreloaderId ? "1"_ns : "0"_ns); + } + + Telemetry::AccumulateTimeDelta(Telemetry::EH_PERF_CHANNEL_COMPLETION_TIME, + earlyHintKey, mAsyncOpenTime, + TimeStamp::Now()); +} + +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 and mEncodedBodySize are set in ProcessOnStopRequest + // TODO: check if we need to move assignments of other members to + // ProcessOnStopRequest + + mCacheReadStart = aTiming.cacheReadStart(); + mCacheReadEnd = aTiming.cacheReadEnd(); + + const TimeStamp now = TimeStamp::Now(); + + if (profiler_thread_is_being_profiled_for_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, now, mTransferSize, kCacheUnknown, + mLoadInfo->GetInnerWindowID(), + mLoadInfo->GetOriginAttributes().mPrivateBrowsingId > 0, + &mTransactionTimings, std::move(mSource), + Some(nsDependentCString(contentType.get()))); + } + + RecordChannelCompletionDurationForEarlyHint(); + + TimeDuration channelCompletionDuration = now - mAsyncOpenTime; + if (mIsFromCache) { + PerfStats::RecordMeasurement(PerfStats::Metric::HttpChannelCompletion_Cache, + channelCompletionDuration); + } else { + PerfStats::RecordMeasurement( + PerfStats::Metric::HttpChannelCompletion_Network, + channelCompletionDuration); + } + PerfStats::RecordMeasurement(PerfStats::Metric::HttpChannelCompletion, + channelCompletionDuration); + + if (!aTiming.responseEnd().IsNull()) { + nsAutoCString cosString; + ClassOfService::ToString(mClassOfService, cosString); + Telemetry::AccumulateTimeDelta( + Telemetry::NETWORK_RESPONSE_END_PARENT_TO_CONTENT_MS, cosString, + aTiming.responseEnd(), now); + PerfStats::RecordMeasurement( + PerfStats::Metric::HttpChannelResponseEndParentToContent, + now - aTiming.responseEnd()); + } + + if (!mOnStopRequestStartTime.IsNull()) { + PerfStats::RecordMeasurement(PerfStats::Metric::OnStopRequestToContent, + now - mOnStopRequestStartTime); + } + + 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); + // 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; + } + + CollectMixedContentTelemetry(); + + 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, static_cast(mOMTResult)); +} + +void HttpChannelChild::CollectMixedContentTelemetry() { + MOZ_ASSERT(NS_IsMainThread()); + + nsContentPolicyType internalLoadType; + mLoadInfo->GetInternalContentPolicyType(&internalLoadType); + bool statusIsSuccess = NS_SUCCEEDED(mStatus); + RefPtr doc; + mLoadInfo->GetLoadingDocument(getter_AddRefs(doc)); + if (!doc) { + return; + } + if (internalLoadType == nsIContentPolicy::TYPE_INTERNAL_IMAGE || + internalLoadType == nsIContentPolicy::TYPE_INTERNAL_IMAGE_PRELOAD) { + if (mLoadInfo->GetBrowserUpgradeInsecureRequests()) { + doc->SetUseCounter( + statusIsSuccess + ? eUseCounter_custom_MixedContentUpgradedImageSuccess + : eUseCounter_custom_MixedContentUpgradedImageFailure); + } else if (mLoadInfo->GetBrowserWouldUpgradeInsecureRequests()) { + doc->SetUseCounter( + statusIsSuccess + ? eUseCounter_custom_MixedContentNotUpgradedImageSuccess + : eUseCounter_custom_MixedContentNotUpgradedImageFailure); + } + return; + } + if (internalLoadType == nsIContentPolicy::TYPE_INTERNAL_VIDEO) { + if (mLoadInfo->GetBrowserUpgradeInsecureRequests()) { + doc->SetUseCounter( + statusIsSuccess + ? eUseCounter_custom_MixedContentUpgradedVideoSuccess + : eUseCounter_custom_MixedContentUpgradedVideoFailure); + } else if (mLoadInfo->GetBrowserWouldUpgradeInsecureRequests()) { + doc->SetUseCounter( + statusIsSuccess + ? eUseCounter_custom_MixedContentNotUpgradedVideoSuccess + : eUseCounter_custom_MixedContentNotUpgradedVideoFailure); + } + return; + } + if (internalLoadType == nsIContentPolicy::TYPE_INTERNAL_AUDIO) { + if (mLoadInfo->GetBrowserUpgradeInsecureRequests()) { + doc->SetUseCounter( + statusIsSuccess + ? eUseCounter_custom_MixedContentUpgradedAudioSuccess + : eUseCounter_custom_MixedContentUpgradedAudioFailure); + } else if (mLoadInfo->GetBrowserWouldUpgradeInsecureRequests()) { + doc->SetUseCounter( + statusIsSuccess + ? eUseCounter_custom_MixedContentNotUpgradedAudioSuccess + : eUseCounter_custom_MixedContentNotUpgradedAudioFailure); + } + } +} + +void HttpChannelChild::DoOnStopRequest(nsIRequest* aRequest, + nsresult aChannelStatus) { + 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(); + + MaybeLogCOEPError(aChannelStatus); + + // 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(bool aUseEventQueue) { + 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; + // avoid reentrancy bugs by setting this now + StoreOnStartRequestCalled(true); + listener->OnStartRequest(this); + } + StoreOnStartRequestCalled(true); + + if (aUseEventQueue) { + mEventQ->RunOrEnqueue(new NeckoTargetChannelFunctionEvent( + this, [self = UnsafePtr(this)] { + self->ContinueDoNotifyListener(); + })); + } else { + 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 nsAString& messageTag, const nsAString& messageCategory) { + DebugOnly rv = AddSecurityMessage(messageTag, messageCategory); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + return IPC_OK(); +} + +mozilla::ipc::IPCResult HttpChannelChild::RecvRedirect1Begin( + const uint32_t& aRegistrarId, nsIURI* aNewUri, + const uint32_t& aNewLoadFlags, const uint32_t& aRedirectFlags, + const ParentLoadInfoForwarderArgs& aLoadInfoForwarder, + const nsHttpResponseHead& aResponseHead, + nsITransportSecurityInfo* aSecurityInfo, 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, + newUri = RefPtr{aNewUri}, aNewLoadFlags, aRedirectFlags, + aLoadInfoForwarder, aResponseHead, + aSecurityInfo = nsCOMPtr{aSecurityInfo}, aChannelId, aTiming]() { + self->Redirect1Begin(aRegistrarId, newUri, aNewLoadFlags, + aRedirectFlags, aLoadInfoForwarder, aResponseHead, + aSecurityInfo, 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, nsIURI* newOriginalURI, + const uint32_t& newLoadFlags, const uint32_t& redirectFlags, + const ParentLoadInfoForwarderArgs& loadInfoForwarder, + const nsHttpResponseHead& responseHead, + nsITransportSecurityInfo* securityInfo, const uint64_t& channelId, + const ResourceTimingStructArgs& timing) { + nsresult rv; + + LOG(("HttpChannelChild::Redirect1Begin [this=%p]\n", this)); + + MOZ_ASSERT(newOriginalURI, "newOriginalURI should not be null"); + + ipc::MergeParentLoadInfoForwarder(loadInfoForwarder, mLoadInfo); + ResourceTimingStructArgsToTimingsStruct(timing, mTransactionTimings); + + if (profiler_thread_is_being_profiled_for_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(), + mLoadInfo->GetOriginAttributes().mPrivateBrowsingId > 0, + &mTransactionTimings, std::move(mSource), + Some(nsDependentCString(contentType.get())), newOriginalURI, + redirectFlags, channelId); + } + + mSecurityInfo = securityInfo; + + nsCOMPtr newChannel; + rv = SetupRedirect(newOriginalURI, &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->CancelWithReason( + rv, "HttpChannelChild Redirect3 failed"_ns); + + // 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(); +} + +mozilla::ipc::IPCResult HttpChannelChild::RecvRedirectFailed( + const nsresult& status) { + LOG(("HttpChannelChild::RecvRedirectFailed this=%p status=%X\n", this, + static_cast(status))); + mEventQ->RunOrEnqueue(new NeckoTargetChannelFunctionEvent( + this, [self = UnsafePtr(this), status]() { + nsCOMPtr vetoHook; + self->GetCallback(vetoHook); + if (vetoHook) { + vetoHook->OnRedirectResult(status); + } + + if (RefPtr httpChannelChild = + do_QueryObject(self->mRedirectChannelChild)) { + // For sending an IPC message to parent channel so that the loading + // can be cancelled. + Unused << httpChannelChild->CancelWithReason( + status, "HttpChannelChild RecvRedirectFailed"_ns); + + // 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 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::ProcessSetClassifierMatchedInfo( + const nsACString& aList, const nsACString& aProvider, + const nsACString& aFullHash) { + LOG(("HttpChannelChild::ProcessSetClassifierMatchedInfo [this=%p]\n", this)); + MOZ_ASSERT(OnSocketThread()); + + mEventQ->RunOrEnqueue(new NeckoTargetChannelFunctionEvent( + this, + [self = UnsafePtr(this), aList = nsCString(aList), + aProvider = nsCString(aProvider), aFullHash = nsCString(aFullHash)]() { + self->SetMatchedInfo(aList, aProvider, aFullHash); + })); +} + +void HttpChannelChild::ProcessSetClassifierMatchedTrackingInfo( + const nsACString& aLists, const nsACString& 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(NS_OK); + } + + // 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)) { + mLoadInfo->AppendRedirectHistoryEntry(this, 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(); + + if (browserChild) { + MOZ_ASSERT(browserChild->WebNavigation()); + if (BrowsingContext* bc = browserChild->GetBrowsingContext()) { + mBrowserId = bc->BrowserId(); + } + } + + 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(); + if (profiler_thread_is_being_profiled_for_markers()) { + nsAutoCString requestMethod; + GetRequestMethod(requestMethod); + + profiler_add_network_marker( + mURI, requestMethod, mPriority, mChannelId, NetworkLoadType::LOAD_START, + mChannelCreationTimestamp, mLastStatusReported, 0, kCacheUnknown, + mLoadInfo->GetInnerWindowID(), + mLoadInfo->GetOriginAttributes().mPrivateBrowsingId > 0); + } + 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()); + nsCOMPtr redirectURI; + + DebugOnly rv = NS_OK; + + 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 (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) { + Unused << newHttpChannelInternal->GetApiRedirectToURI( + getter_AddRefs(redirectURI)); + } + + nsCOMPtr request = do_QueryInterface(mRedirectChannelChild); + if (request) { + request->GetLoadFlags(&loadFlags); + } + } + + 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); + } + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// HttpChannelChild::nsIRequest +//----------------------------------------------------------------------------- + +NS_IMETHODIMP HttpChannelChild::SetCanceledReason(const nsACString& aReason) { + return SetCanceledReasonImpl(aReason); +} + +NS_IMETHODIMP HttpChannelChild::GetCanceledReason(nsACString& aReason) { + return GetCanceledReasonImpl(aReason); +} + +NS_IMETHODIMP +HttpChannelChild::CancelWithReason(nsresult aStatus, + const nsACString& aReason) { + return CancelWithReasonImpl(aStatus, aReason); +} + +NS_IMETHODIMP +HttpChannelChild::Cancel(nsresult aStatus) { + LOG(("HttpChannelChild::Cancel [this=%p, status=%" PRIx32 "]\n", this, + static_cast(aStatus))); + // only logging on parent is necessary + Maybe logStack = CallingScriptLocationString(); + Maybe logOnParent; + if (logStack.isSome()) { + logOnParent = Some(""_ns); + logOnParent->AppendPrintf( + "[this=%p] cancelled call in child process from script: %s", this, + logStack->get()); + } + + 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(), + mCanceledReason, logOnParent); + } else if (MOZ_UNLIKELY(!LoadOnStartRequestCalled() || + !LoadOnStopRequestCalled())) { + Unused << AsyncAbort(mStatus); + } + } + return NS_OK; +} + +NS_IMETHODIMP +HttpChannelChild::Suspend() { + LOG(("HttpChannelChild::Suspend [this=%p, mSuspendCount=%" PRIu32 "\n", this, + mSuspendCount + 1)); + MOZ_ASSERT(NS_IsMainThread()); + + 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(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) { + if (RemoteChannelExists() && mSuspendSent) { + 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(nsITransportSecurityInfo** aSecurityInfo) { + NS_ENSURE_ARG_POINTER(aSecurityInfo); + *aSecurityInfo = do_AddRef(mSecurityInfo).take(); + 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 (MaybeWaitForUploadStreamNormalization(listener, nullptr)) { + return NS_OK; + } + + if (!LoadAsyncOpenTimeOverriden()) { + mAsyncOpenTime = TimeStamp::Now(); + } + + // 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(); + if (profiler_thread_is_being_profiled_for_markers()) { + nsAutoCString requestMethod; + GetRequestMethod(requestMethod); + + profiler_add_network_marker( + mURI, requestMethod, mPriority, mChannelId, NetworkLoadType::LOAD_START, + mChannelCreationTimestamp, mLastStatusReported, 0, kCacheUnknown, + mLoadInfo->GetInnerWindowID(), + mLoadInfo->GetOriginAttributes().mPrivateBrowsingId > 0); + } + StoreIsPending(true); + StoreWasOpened(true); + mListener = listener; + + 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() { + MutexAutoLock lock(mEventTargetMutex); + mNeckoTarget = GetMainThreadSerialEventTarget(); +} + +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 = GetMainThreadSerialEventTarget(); + } + return target.forget(); +} + +nsresult HttpChannelChild::ContinueAsyncOpen() { + nsresult rv; + // + // 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(); + } + } + if (BrowsingContext* bc = browserChild->GetBrowsingContext()) { + mBrowserId = bc->BrowserId(); + } + } + SetTopLevelContentWindowId(contentWindowId); + + if (browserChild && !browserChild->IPCOpen()) { + return NS_ERROR_FAILURE; + } + + ContentChild* cc = static_cast(gNeckoChild->Manager()); + if (cc->IsShuttingDown()) { + return NS_ERROR_FAILURE; + } + + // add ourselves to the load group. + if (mLoadGroup) { + mLoadGroup->AddRequest(this, nullptr); + } + + HttpChannelOpenArgs openArgs; + // No access to HttpChannelOpenArgs members, but they each have a + // function with the struct name that returns a ref. + openArgs.uri() = mURI; + openArgs.original() = mOriginalURI; + openArgs.doc() = mDocumentURI; + openArgs.apiRedirectTo() = mAPIRedirectToURI; + openArgs.loadFlags() = mLoadFlags; + openArgs.requestHeaders() = mClientSetRequestHeaders; + mRequestHead.Method(openArgs.requestMethod()); + openArgs.preferredAlternativeTypes() = mPreferredCachedAltDataTypes.Clone(); + openArgs.referrerInfo() = mReferrerInfo; + + if (mUploadStream) { + MOZ_ALWAYS_TRUE(SerializeIPCStream(do_AddRef(mUploadStream), + openArgs.uploadStream(), + /* aAllowLazy */ false)); + } + + Maybe optionalCorsPreflightArgs; + GetClientSetCorsPreflightParameters(optionalCorsPreflightArgs); + + // NB: This call forces us to cache mTopWindowURI if we haven't already. + nsCOMPtr uri; + GetTopWindowURI(mURI, getter_AddRefs(uri)); + + openArgs.topWindowURI() = mTopWindowURI; + + 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.allowSpdy() = LoadAllowSpdy(); + openArgs.allowHttp3() = LoadAllowHttp3(); + openArgs.allowAltSvc() = LoadAllowAltSvc(); + openArgs.beConservative() = LoadBeConservative(); + openArgs.bypassProxy() = BypassProxy(); + 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.requestMode() = mRequestMode; + openArgs.redirectMode() = mRedirectMode; + + openArgs.channelId() = mChannelId; + + openArgs.integrityMetadata() = mIntegrityMetadata; + + openArgs.contentWindowId() = contentWindowId; + openArgs.browserId() = mBrowserId; + + LOG(("HttpChannelChild::ContinueAsyncOpen this=%p gid=%" PRIu64 + " browser id=%" PRIx64, + this, mChannelId, mBrowserId)); + + 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.earlyHintPreloaderId() = mEarlyHintPreloaderId; + + openArgs.classicScriptHintCharset() = mClassicScriptHintCharset; + + openArgs.isUserAgentHeaderModified() = LoadIsUserAgentHeaderModified(); + + RefPtr doc; + mLoadInfo->GetLoadingDocument(getter_AddRefs(doc)); + + if (doc) { + nsAutoString documentCharacterSet; + doc->GetCharacterSet(documentCharacterSet); + openArgs.documentCharacterSet() = documentCharacterSet; + } + + // 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; + + // Mark that the User-Agent header has been modified. + if (nsHttp::ResolveAtom(aHeader) == nsHttp::User_Agent) { + StoreIsUserAgentHeaderModified(true); + } + + 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; + + // Mark that the User-Agent header has been modified. + if (nsHttp::ResolveAtom(aHeader) == nsHttp::User_Agent) { + StoreIsUserAgentHeaderModified(true); + } + + 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::GetIsAuthChannel(bool* aIsAuthChannel) { DROP_DEAD(); } + +//----------------------------------------------------------------------------- +// HttpChannelChild::nsICacheInfoChannel +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +HttpChannelChild::GetCacheTokenFetchCount(uint32_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::SetForceValidateCacheContent( + bool aForceValidateCacheContent) { + StoreForceValidateCacheContent(aForceValidateCacheContent); + return NS_OK; +} +NS_IMETHODIMP +HttpChannelChild::GetForceValidateCacheContent( + bool* aForceValidateCacheContent) { + NS_ENSURE_ARG(aForceValidateCacheContent); + *aForceValidateCacheContent = LoadForceValidateCacheContent(); + 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, + PreferredAlternativeDataDeliveryType 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(); + + if (!gNeckoChild->SendPAltDataOutputStreamConstructor( + stream, nsCString(aType), aPredictedSize, WrapNotNull(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::GetAlternativeDataInputStream(nsIInputStream** aInputStream) { + NS_ENSURE_ARG_POINTER(aInputStream); + + nsCOMPtr is = mAltDataInputStream; + is.forget(aInputStream); + + 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(); +} + +//----------------------------------------------------------------------------- +// 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.Flags() == inFlags) { + return NS_OK; + } + + mClassOfService.SetFlags(inFlags); + + LOG(("HttpChannelChild %p ClassOfService flags=%lu inc=%d", this, + mClassOfService.Flags(), mClassOfService.Incremental())); + + if (RemoteChannelExists()) { + SendSetClassOfService(mClassOfService); + } + return NS_OK; +} + +NS_IMETHODIMP +HttpChannelChild::AddClassFlags(uint32_t inFlags) { + mClassOfService.SetFlags(inFlags | mClassOfService.Flags()); + + LOG(("HttpChannelChild %p ClassOfService flags=%lu inc=%d", this, + mClassOfService.Flags(), mClassOfService.Incremental())); + + if (RemoteChannelExists()) { + SendSetClassOfService(mClassOfService); + } + return NS_OK; +} + +NS_IMETHODIMP +HttpChannelChild::ClearClassFlags(uint32_t inFlags) { + mClassOfService.SetFlags(~inFlags & mClassOfService.Flags()); + + LOG(("HttpChannelChild %p ClassOfService=%lu", this, + mClassOfService.Flags())); + + if (RemoteChannelExists()) { + SendSetClassOfService(mClassOfService); + } + return NS_OK; +} + +NS_IMETHODIMP +HttpChannelChild::SetClassOfService(ClassOfService inCos) { + mClassOfService = inCos; + LOG(("HttpChannelChild %p ClassOfService flags=%lu inc=%d", this, + mClassOfService.Flags(), mClassOfService.Incremental())); + if (RemoteChannelExists()) { + SendSetClassOfService(mClassOfService); + } + return NS_OK; +} +NS_IMETHODIMP +HttpChannelChild::SetIncremental(bool inIncremental) { + mClassOfService.SetIncremental(inIncremental); + LOG(("HttpChannelChild %p ClassOfService flags=%lu inc=%d", this, + mClassOfService.Flags(), mClassOfService.Incremental())); + 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::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) { + PrincipalInfo principalInfo; + MOZ_ASSERT(aURI, "aURI should not be null"); + 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(aURI, 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::GetIsFirstPart(bool* aIsFirstPart) { + if (!mMultiPartID) { + return NS_ERROR_NOT_AVAILABLE; + } + *aIsFirstPart = mIsFirstPartOfMultiPart; + 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(nsISerialEventTarget* aNewTarget) { + LOG(("HttpChannelChild::RetargetDeliveryTo [this=%p, aNewTarget=%p]", this, + aNewTarget)); + MOZ_ASSERT(NS_IsMainThread(), "Should be called on main thread only"); + 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); + MOZ_ASSERT(!mODATarget); + mODATarget = aNewTarget; + } + + mOMTResult = LABELS_HTTP_CHILD_OMT_STATS::success; + return NS_OK; +} + +NS_IMETHODIMP +HttpChannelChild::GetDeliveryTarget(nsISerialEventTarget** aEventTarget) { + MutexAutoLock lock(mEventTargetMutex); + + nsCOMPtr target = mODATarget; + if (!mODATarget) { + target = GetCurrentSerialEventTarget(); + } + 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)); +} + +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, + const nsACString& aReason) { + LOG(("HttpChannelChild::CancelOnMainThread [this=%p]", this)); + + if (NS_IsMainThread()) { + CancelWithReason(aRv, aReason); + 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. + nsCString reason(aReason); + mEventQ->PrependEvent(MakeUnique( + this, [self = UnsafePtr(this), aRv, reason]() { + self->CancelWithReason(aRv, reason); + })); + mEventQ->Resume(); +} + +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::RegisterStreamFilter( + RefPtr& aStreamFilter) { + MOZ_ASSERT(NS_IsMainThread()); + mStreamFilters.AppendElement(aStreamFilter); +} + +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::OnDetachStreamFilters() { + LOG(("HttpChannelChild::OnDetachStreamFilters [this=%p]\n", this)); + MOZ_ASSERT(NS_IsMainThread()); + for (auto& StreamFilter : mStreamFilters) { + StreamFilter->Disconnect("ServiceWorker fallback redirection"_ns); + } + mStreamFilters.Clear(); +} + +void HttpChannelChild::ProcessDetachStreamFilters() { + LOG(("HttpChannelChild::ProcessDetachStreamFilter [this=%p]\n", this)); + MOZ_ASSERT(OnSocketThread()); + + mEventQ->RunOrEnqueue(new NeckoTargetChannelFunctionEvent( + this, [self = UnsafePtr(this)]() { + self->OnDetachStreamFilters(); + })); +} + +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 nsAString& aMessage, const nsACString& aCategory, + const bool& aIsWarning) { + Unused << LogBlockedCORSRequest(aMessage, aCategory, aIsWarning); + return IPC_OK(); +} + +NS_IMETHODIMP +HttpChannelChild::LogBlockedCORSRequest(const nsAString& aMessage, + const nsACString& aCategory, + bool aIsWarning) { + uint64_t innerWindowID = mLoadInfo->GetInnerWindowID(); + bool privateBrowsing = !!mLoadInfo->GetOriginAttributes().mPrivateBrowsingId; + bool fromChromeContext = + mLoadInfo->TriggeringPrincipal()->IsSystemPrincipal(); + nsCORSListenerProxy::LogBlockedCORSRequest(innerWindowID, privateBrowsing, + fromChromeContext, aMessage, + aCategory, aIsWarning); + return NS_OK; +} + +mozilla::ipc::IPCResult HttpChannelChild::RecvLogMimeTypeMismatch( + const nsACString& aMessageName, const bool& aWarning, const nsAString& aURL, + const nsAString& 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::MaybeLogCOEPError(nsresult aStatus) { + if (aStatus == NS_ERROR_DOM_CORP_FAILED) { + RefPtr doc; + mLoadInfo->GetLoadingDocument(getter_AddRefs(doc)); + + nsAutoCString url; + mURI->GetSpec(url); + + AutoTArray params; + params.AppendElement(NS_ConvertUTF8toUTF16(url)); + // The MDN URL intentionally ends with a # so the webconsole linkification + // doesn't ignore the final ) of the URL + params.AppendElement( + u"https://developer.mozilla.org/docs/Web/HTTP/Cross-Origin_Resource_Policy_(CORP)#"_ns); + nsContentUtils::ReportToConsole(nsIScriptError::errorFlag, "COEP"_ns, doc, + nsContentUtils::eNECKO_PROPERTIES, + "CORPBlocked", 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; + { + MutexAutoLock lock(mBgChildMutex); + bgChild = mBgChild; + } + SocketProcessBridgeChild::GetSocketProcessBridge()->Then( + GetCurrentSerialEventTarget(), __func__, + [bgChild, channelId = ChannelId()]( + const RefPtr& aBridge) { + Endpoint parentEndpoint; + Endpoint childEndpoint; + PBackgroundDataBridge::CreateEndpoints(&parentEndpoint, &childEndpoint); + aBridge->SendInitBackgroundDataBridge(std::move(parentEndpoint), + channelId); + + gSocketTransportService->Dispatch( + NS_NewRunnableFunction( + "HttpBackgroundChannelChild::CreateDataBridge", + [bgChild, endpoint = std::move(childEndpoint)]() mutable { + bgChild->CreateDataBridge(std::move(endpoint)); + }), + NS_DISPATCH_NORMAL); + }, + []() { NS_WARNING("Failed to create SocketProcessBridgeChild"); }); +} + +NS_IMETHODIMP +HttpChannelChild::SetEarlyHintObserver(nsIEarlyHintObserver* aObserver) { + return NS_OK; +} + +NS_IMETHODIMP HttpChannelChild::SetWebTransportSessionEventListener( + WebTransportSessionEventListener* aListener) { + return NS_OK; +} + +} // namespace mozilla::net diff --git a/netwerk/protocol/http/HttpChannelChild.h b/netwerk/protocol/http/HttpChannelChild.h new file mode 100644 index 0000000000..38895a0555 --- /dev/null +++ b/netwerk/protocol/http/HttpChannelChild.h @@ -0,0 +1,481 @@ +/* -*- 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/StaticPrefsBase.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 "nsICacheEntry.h" +#include "nsICacheInfoChannel.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 nsIInterceptedBodyCallback; +class nsISerialEventTarget; +class nsITransportSecurityInfo; +class nsInputStreamPump; + +#define HTTP_CHANNEL_CHILD_IID \ + { \ + 0x321bd99e, 0x2242, 0x4dc6, { \ + 0xbb, 0xec, 0xd5, 0x06, 0x29, 0x7c, 0x39, 0x83 \ + } \ + } + +namespace mozilla::net { + +class HttpBackgroundChannelChild; + +class HttpChannelChild final : public PHttpChannelChild, + public HttpBaseChannel, + public HttpAsyncAborter, + public nsICacheInfoChannel, + public nsIProxiedChannel, + 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_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 SetCanceledReason(const nsACString& aReason) override; + NS_IMETHOD GetCanceledReason(nsACString& aReason) override; + NS_IMETHOD CancelWithReason(nsresult status, + const nsACString& reason) override; + NS_IMETHOD Cancel(nsresult status) override; + NS_IMETHOD Suspend() override; + NS_IMETHOD Resume() override; + // nsIChannel + NS_IMETHOD GetSecurityInfo(nsITransportSecurityInfo** 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 GetIsAuthChannel(bool* aIsAuthChannel) override; + NS_IMETHOD SetEarlyHintObserver(nsIEarlyHintObserver* aObserver) override; + NS_IMETHOD SetWebTransportSessionEventListener( + WebTransportSessionEventListener* aListener) 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; + NS_IMETHOD SetClassOfService(ClassOfService inCos) override; + NS_IMETHOD SetIncremental(bool inIncremental) override; + // nsIResumableChannel + NS_IMETHOD ResumeAt(uint64_t startPos, const nsACString& entityID) override; + + nsresult SetReferrerHeader(const nsACString& aReferrer, + bool aRespectBeforeConnect) override; + + [[nodiscard]] bool IsSuspended(); + + // Callback while background channel is ready. + void OnBackgroundChildReady(HttpBackgroundChannelChild* aBgChild); + // Callback while background channel is destroyed. + void OnBackgroundChildDestroyed(HttpBackgroundChannelChild* aBgChild); + + nsresult CrossProcessRedirectFinished(nsresult aStatus); + + void RegisterStreamFilter( + RefPtr& aStreamFilter); + + protected: + mozilla::ipc::IPCResult RecvOnStartRequestSent() override; + mozilla::ipc::IPCResult RecvFailedAsyncOpen(const nsresult& status) override; + mozilla::ipc::IPCResult RecvRedirect1Begin( + const uint32_t& registrarId, nsIURI* newOriginalURI, + const uint32_t& newLoadFlags, const uint32_t& redirectFlags, + const ParentLoadInfoForwarderArgs& loadInfoForwarder, + const nsHttpResponseHead& responseHead, + nsITransportSecurityInfo* securityInfo, const uint64_t& channelId, + const NetAddr& oldPeerAddr, + const ResourceTimingStructArgs& aTiming) override; + mozilla::ipc::IPCResult RecvRedirect3Complete() override; + mozilla::ipc::IPCResult RecvRedirectFailed(const nsresult& status) override; + mozilla::ipc::IPCResult RecvDeleteSelf() override; + + mozilla::ipc::IPCResult RecvReportSecurityMessage( + const nsAString& messageTag, const nsAString& messageCategory) override; + + mozilla::ipc::IPCResult RecvSetPriority(const int16_t& aPriority) override; + + mozilla::ipc::IPCResult RecvOriginalCacheInputStreamAvailable( + 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 nsAString& aMessage, const nsACString& aCategory, + const bool& aIsWarning) override; + NS_IMETHOD LogBlockedCORSRequest(const nsAString& aMessage, + const nsACString& aCategory, + bool aIsWarning = false) override; + + virtual mozilla::ipc::IPCResult RecvLogMimeTypeMismatch( + const nsACString& aMessageName, const bool& aWarning, + const nsAString& aURL, const nsAString& 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, + const HttpChannelAltDataStream& aAltData, + const TimeStamp& aOnStartRequestStartTime); + + // Callbacks while receiving OnTransportAndData/OnStopRequest/OnProgress/ + // OnStatus/FlushedForDiversion/DivertMessages on background IPC channel. + void ProcessOnTransportAndData(const nsresult& aChannelStatus, + const nsresult& aTransportStatus, + const uint64_t& aOffset, + const uint32_t& aCount, + const nsACString& aData, + const TimeStamp& aOnDataAvailableStartTime); + void ProcessOnStopRequest(const nsresult& aChannelStatus, + const ResourceTimingStructArgs& aTiming, + const nsHttpHeaderArray& aResponseTrailers, + nsTArray&& aConsoleReports, + bool aFromSocketProcess, + const TimeStamp& aOnStopRequestStartTime); + void ProcessOnConsoleReport( + nsTArray&& aConsoleReports); + + void ProcessNotifyClassificationFlags(uint32_t aClassificationFlags, + bool aIsThirdParty); + void ProcessSetClassifierMatchedInfo(const nsACString& aList, + const nsACString& aProvider, + const nsACString& aFullHash); + void ProcessSetClassifierMatchedTrackingInfo(const nsACString& aLists, + const nsACString& 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); + void ProcessDetachStreamFilters(); + + // 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); + void DoOnStatus(nsIRequest* aRequest, nsresult status); + void DoOnProgress(nsIRequest* aRequest, int64_t progress, + int64_t progressMax); + void DoOnDataAvailable(nsIRequest* aRequest, nsIInputStream* aStream, + uint64_t offset, uint32_t count); + void DoPreOnStopRequest(nsresult aStatus); + void DoOnStopRequest(nsIRequest* aRequest, nsresult aChannelStatus); + 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, const nsACString& aReason); + + nsresult MaybeLogCOEPError(nsresult aStatus); + + 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 mAltDataInputStream; + + // Used to ensure atomicity of mBgChild and mBgInitFailCallback + Mutex mBgChildMutex{"HttpChannelChild::BgChildMutex"}; + + // Associated HTTP background channel + RefPtr mBgChild MOZ_GUARDED_BY(mBgChildMutex); + + // Error handling procedure if failed to establish PBackground IPC + nsCOMPtr mBgInitFailCallback MOZ_GUARDED_BY(mBgChildMutex); + + // Remove the association with background channel after OnStopRequest + // or AsyncAbort. + void CleanupBackgroundChannel(); + + // Target thread for delivering ODA. + nsCOMPtr mODATarget MOZ_GUARDED_BY(mEventTargetMutex); + // Used to ensure atomicity of mNeckoTarget / mODATarget; + Mutex mEventTargetMutex{"HttpChannelChild::EventTargetMutex"}; + + TimeStamp mLastStatusReported; + + uint64_t mCacheEntryId{0}; + + // The result of RetargetDeliveryTo for this channel. + // |notRequested| represents OMT is not requested by the channel owner. + Atomic mOMTResult{ + LABELS_HTTP_CHILD_OMT_STATS::notRequested}; + + uint32_t mCacheKey{0}; + int32_t mCacheFetchCount{0}; + uint32_t mCacheExpirationTime{ + static_cast(nsICacheEntry::NO_EXPIRATION_TIME)}; + + // 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{false}; + + Atomic mIsFromCache{false}; + Atomic mIsRacing{false}; + // Set if we get the result and cache |mNeedToReportBytesRead| + Atomic mCacheNeedToReportBytesReadInitialized{ + false}; + // True if we need to tell the parent the size of unreported received data + Atomic mNeedToReportBytesRead{true}; + Atomic mOnProgressEventSent{false}; + // Attached StreamFilterParents + // Using raw pointer here since StreamFilterParent owns the channel. + // Should be only accessed on the main thread. + using StreamFilters = nsTArray; + StreamFilters mStreamFilters; + +#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{BCKCHILD_UNKNOWN}; + 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 first part + // is currently being processed. + uint8_t mIsFirstPartOfMultiPart : 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; + + // HttpChannelChild::Release has some special logic that makes sure + // OnStart/OnStop are always called when releasing the channel. + // But we have to make sure we only do this once - otherwise we could + // get stuck in a loop. + uint8_t mAlreadyReleased : 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 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 nsACString& 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, nsIURI* newOriginalURI, + const uint32_t& newLoadFlags, + const uint32_t& redirectFlags, + const ParentLoadInfoForwarderArgs& loadInfoForwarder, + const nsHttpResponseHead& responseHead, + nsITransportSecurityInfo* securityInfo, + const uint64_t& channelId, + const ResourceTimingStructArgs& timing); + void Redirect3Complete(); + void DeleteSelf(); + // aUseEventQueue should only be false when called from + // HttpChannelChild::Release to make sure OnStopRequest is called syncly. + void DoNotifyListener(bool aUseEventQueue = true); + void ContinueDoNotifyListener(); + void OnAfterLastPart(const nsresult& aStatus); + void MaybeConnectToSocketProcess(); + void OnDetachStreamFilters(); + void SendOnDataFinished(const nsresult& aChannelStatus); + + // 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(); + + // Collect telemetry for mixed content. + void CollectMixedContentTelemetry(); + + void RecordChannelCompletionDurationForEarlyHint(); + + 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 mozilla::net + +#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..f7601a7785 --- /dev/null +++ b/netwerk/protocol/http/HttpChannelParams.ipdlh @@ -0,0 +1,71 @@ +/* -*- 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 IPCStream; + +include "mozilla/dom/ReferrerInfoUtils.h"; +include "mozilla/ipc/TransportSecurityInfoUtils.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"; +[RefCounted] using class nsIReferrerInfo from "nsIReferrerInfo.h"; +[RefCounted] using class nsITransportSecurityInfo from "nsITransportSecurityInfo.h"; +using nsIRequest::TRRMode from "nsIRequest.h"; +using mozilla::net::TRRSkippedReason from "nsITRRSkipReason.h"; + +namespace mozilla { +namespace net { + +struct HttpChannelOnStartRequestArgs +{ + nullable nsITransportSecurityInfo securityInfo; + nullable nsIReferrerInfo overrideReferrerInfo; + uint64_t cacheEntryId; + int64_t altDataLength; + nsCString altDataType; + nsCString cookie; + NetAddr selfAddr; + NetAddr peerAddr; + ResourceTimingStructArgs timing; + ParentLoadInfoForwarderArgs loadInfoForwarder; + nsresult channelStatus; + TRRMode effectiveTRRMode; + TRRSkippedReason trrSkipReason; + uint32_t cacheFetchCount; + uint32_t cacheExpirationTime; + uint32_t cacheKey; + uint32_t? multiPartID; + bool isFromCache; + bool isRacing; + bool cacheEntryAvailable; + bool deliveringAltData; + bool applyConversion; + bool isResolvedByTRR; + bool allRedirectsSameOrigin; + bool isFirstPartOfMultiPart; + bool isLastPartOfMultiPart; + CrossOriginOpenerPolicy openerPolicy; + bool shouldWaitForOnStartRequestSent; + bool dataFromSocketProcess; + bool hasHTTPSRR; + bool isProxyUsed; + uint8_t redirectCount; + nsCString protocolVersion; +}; + +struct HttpChannelAltDataStream +{ + IPCStream? altDataInputStream; +}; + +} // namespace ipc +} // namespace mozilla diff --git a/netwerk/protocol/http/HttpChannelParent.cpp b/netwerk/protocol/http/HttpChannelParent.cpp new file mode 100644 index 0000000000..e734d15a7d --- /dev/null +++ b/netwerk/protocol/http/HttpChannelParent.cpp @@ -0,0 +1,2202 @@ +/* -*- 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 "ErrorList.h" +#include "HttpLog.h" + +#include "mozilla/ConsoleReportCollector.h" +#include "mozilla/dom/BrowsingContext.h" +#include "mozilla/ipc/IPCStreamUtils.h" +#include "mozilla/net/EarlyHintRegistrar.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/dom/WindowGlobalParent.h" +#include "mozilla/net/NeckoParent.h" +#include "mozilla/net/CookieServiceParent.h" +#include "mozilla/InputStreamLengthHelper.h" +#include "mozilla/IntegerPrintfMacros.h" +#include "mozilla/Preferences.h" +#include "mozilla/ProfilerLabels.h" +#include "mozilla/ProfilerMarkers.h" +#include "mozilla/StoragePrincipalHelper.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/Unused.h" +#include "HttpBackgroundChannelParent.h" +#include "ParentChannelListener.h" +#include "nsDebug.h" +#include "nsICacheInfoChannel.h" +#include "nsHttpHandler.h" +#include "nsNetCID.h" +#include "nsNetUtil.h" +#include "nsISupportsPriority.h" +#include "mozilla/net/BackgroundChannelRegistrar.h" +#include "nsSerializationHelper.h" +#include "nsISerializable.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/ChannelEventQueue.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 namespace mozilla; + +namespace geckoprofiler::markers { + +struct ChannelMarker { + static constexpr Span MarkerTypeName() { + return MakeStringSpan("ChannelMarker"); + } + static void StreamJSONMarkerData( + mozilla::baseprofiler::SpliceableJSONWriter& aWriter, + const mozilla::ProfilerString8View& aURL, uint64_t aChannelId) { + if (aURL.Length() != 0) { + aWriter.StringProperty("url", aURL); + } + aWriter.IntProperty("channelId", static_cast(aChannelId)); + } + static MarkerSchema MarkerTypeDisplay() { + using MS = MarkerSchema; + MS schema(MS::Location::MarkerChart, MS::Location::MarkerTable); + schema.SetTableLabel("{marker.name} - {marker.data.url}"); + schema.AddKeyFormatSearchable("url", MS::Format::Url, + MS::Searchable::Searchable); + schema.AddStaticLabelValue( + "Description", + "Timestamp capturing various phases of a network channel's lifespan."); + return schema; + } +}; + +} // namespace geckoprofiler::markers + +using mozilla::BasePrincipal; +using namespace mozilla::dom; +using namespace mozilla::ipc; + +namespace mozilla::net { + +HttpChannelParent::HttpChannelParent(dom::BrowserParent* iframeEmbedding, + nsILoadContext* aLoadContext, + PBOverrideStatus aOverrideStatus) + : mLoadContext(aLoadContext), + mIPCClosed(false), + mPBOverride(aOverrideStatus), + mStatus(NS_OK), + mIgnoreProgress(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; + } + mEventQ->NotifyReleasingOwner(); +} + +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.allowSpdy(), a.allowHttp3(), a.allowAltSvc(), a.beConservative(), + a.bypassProxy(), a.tlsFlags(), a.loadInfo(), a.cacheKey(), + a.requestContextID(), a.preflightArgs(), a.initialRwin(), + a.blockAuthPrompt(), a.allowStaleCacheContent(), + a.preferCacheLoadOverBypass(), a.contentTypeHint(), a.requestMode(), + a.redirectMode(), a.channelId(), a.integrityMetadata(), + a.contentWindowId(), a.preferredAlternativeTypes(), a.browserId(), + a.launchServiceWorkerStart(), a.launchServiceWorkerEnd(), + a.dispatchFetchEventStart(), a.dispatchFetchEventEnd(), + a.handleFetchEventStart(), a.handleFetchEventEnd(), + a.forceMainDocumentChannel(), a.navigationStartTimeStamp(), + a.earlyHintPreloaderId(), a.classicScriptHintCharset(), + a.documentCharacterSet(), a.isUserAgentHeaderModified()); + } + 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 PHttpChannelParent::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(nsIParentRedirectingChannel) + 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) { + // 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); + } +} + +void HttpChannelParent::InvokeEarlyHintPreloader( + nsresult rv, uint64_t aEarlyHintPreloaderId) { + LOG(("HttpChannelParent::InvokeEarlyHintPreloader [this=%p rv=%" PRIx32 "]\n", + this, static_cast(rv))); + MOZ_ASSERT(NS_IsMainThread()); + + ContentParentId cpId = + static_cast(Manager()->Manager())->ChildID(); + + RefPtr ehr = EarlyHintRegistrar::GetOrCreate(); + if (NS_SUCCEEDED(rv)) { + rv = ehr->LinkParentChannel(cpId, aEarlyHintPreloaderId, this) + ? NS_OK + : NS_ERROR_FAILURE; + } + + if (NS_FAILED(rv)) { + ehr->DeleteEntry(cpId, aEarlyHintPreloaderId); + AsyncOpenFailed(NS_ERROR_FAILURE); + } +} + +bool HttpChannelParent::DoAsyncOpen( + nsIURI* aURI, nsIURI* aOriginalURI, nsIURI* aDocURI, + nsIReferrerInfo* aReferrerInfo, nsIURI* aAPIRedirectToURI, + nsIURI* aTopWindowURI, const uint32_t& aLoadFlags, + const RequestHeaderTuples& requestHeaders, const nsCString& requestMethod, + const Maybe& uploadStream, const bool& uploadStreamHasHeaders, + const int16_t& priority, const ClassOfService& classOfService, + const uint8_t& redirectionLimit, const bool& allowSTS, + const uint32_t& thirdPartyFlags, const bool& doResumeAt, + const uint64_t& startPos, const nsCString& entityID, const bool& allowSpdy, + const bool& allowHttp3, const bool& allowAltSvc, const bool& beConservative, + const bool& bypassProxy, const uint32_t& tlsFlags, + const LoadInfoArgs& 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 dom::RequestMode& aRequestMode, + const uint32_t& aRedirectMode, const uint64_t& aChannelId, + const nsString& aIntegrityMetadata, const uint64_t& aContentWindowId, + const nsTArray& + aPreferredAlternativeTypes, + const uint64_t& aBrowserId, 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 uint64_t& aEarlyHintPreloaderId, + const nsAString& aClassicScriptHintCharset, + const nsAString& aDocumentCharacterSet, + const bool& aIsUserAgentHeaderModified) { + MOZ_ASSERT(aURI, "aURI should not be NULL"); + + if (aEarlyHintPreloaderId) { + // Wait for HttpBackgrounChannel to continue the async open procedure. + mEarlyHintPreloaderId = aEarlyHintPreloaderId; + RefPtr self = this; + WaitForBgParent(aChannelId) + ->Then( + GetMainThreadSerialEventTarget(), __func__, + [self, aEarlyHintPreloaderId]() { + self->mRequest.Complete(); + self->InvokeEarlyHintPreloader(NS_OK, aEarlyHintPreloaderId); + }, + [self, aEarlyHintPreloaderId](nsresult aStatus) { + self->mRequest.Complete(); + self->InvokeEarlyHintPreloader(aStatus, aEarlyHintPreloaderId); + }) + ->Track(mRequest); + return true; + } + + if (!aURI) { + // this check is neccessary to prevent null deref + // in opt builds + return false; + } + + LOG(("HttpChannelParent RecvAsyncOpen [this=%p uri=%s, gid=%" PRIu64 + " browserid=%" PRIx64 "]\n", + this, aURI->GetSpecOrDefault().get(), aChannelId, aBrowserId)); + + PROFILER_MARKER("Receive AsyncOpen in Parent", NETWORK, {}, ChannelMarker, + aURI->GetSpecOrDefault(), aChannelId); + + nsresult rv; + + nsCOMPtr ios(do_GetIOService(&rv)); + if (NS_FAILED(rv)) { + return SendFailedAsyncOpen(rv); + } + + nsAutoCString remoteType; + rv = GetRemoteType(remoteType); + if (NS_FAILED(rv)) { + return SendFailedAsyncOpen(rv); + } + + nsCOMPtr loadInfo; + rv = mozilla::ipc::LoadInfoArgsToLoadInfo(aLoadInfoArgs, remoteType, + getter_AddRefs(loadInfo)); + if (NS_FAILED(rv)) { + return SendFailedAsyncOpen(rv); + } + + nsCOMPtr channel; + rv = NS_NewChannelInternal(getter_AddRefs(channel), aURI, 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->SetRequestMode(aRequestMode); + httpChannel->SetRedirectMode(aRedirectMode); + + // Set the channelId allocated in child to the parent instance + httpChannel->SetChannelId(aChannelId); + httpChannel->SetTopLevelContentWindowId(aContentWindowId); + httpChannel->SetBrowserId(aBrowserId); + + 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); + } + + if (doResumeAt) httpChannel->ResumeAt(startPos, entityID); + + if (aOriginalURI) { + httpChannel->SetOriginalURI(aOriginalURI); + } + + if (aDocURI) { + httpChannel->SetDocumentURI(aDocURI); + } + + 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)); + } + + httpChannel->SetClassicScriptHintCharset(aClassicScriptHintCharset); + httpChannel->SetDocumentCharacterSet(aDocumentCharacterSet); + + if (aAPIRedirectToURI) { + httpChannel->RedirectTo(aAPIRedirectToURI); + } + + if (aTopWindowURI) { + httpChannel->SetTopWindowURI(aTopWindowURI); + } + + if (aLoadFlags != nsIRequest::LOAD_NORMAL) { + httpChannel->SetLoadFlags(aLoadFlags); + } + + if (aForceMainDocumentChannel) { + httpChannel->SetIsMainDocumentChannel(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); + } + } + + httpChannel->SetIsUserAgentHeaderModified(aIsUserAgentHeaderModified); + + RefPtr parentListener = new ParentChannelListener( + this, mBrowserParent ? mBrowserParent->GetBrowsingContext() : nullptr); + + httpChannel->SetRequestMethod(nsDependentCString(requestMethod.get())); + + if (aCorsPreflightArgs.isSome()) { + const CorsPreflightArgs& args = aCorsPreflightArgs.ref(); + httpChannel->SetCorsPreflightParameters(args.unsafeHeaders(), false); + } + + nsCOMPtr stream = DeserializeIPCStream(uploadStream); + if (stream) { + rv = httpChannel->InternalSetUploadStream(stream); + if (NS_FAILED(rv)) { + return SendFailedAsyncOpen(rv); + } + + httpChannel->SetUploadStreamHasHeaders(uploadStreamHasHeaders); + } + + nsCOMPtr cacheChannel = + do_QueryInterface(static_cast(httpChannel.get())); + if (cacheChannel) { + cacheChannel->SetCacheKey(aCacheKey); + for (const 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.Flags() || classOfService.Incremental()) { + httpChannel->SetClassOfService(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); + 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(mChannel->ChannelId()) + ->Then( + GetMainThreadSerialEventTarget(), __func__, + [self]() { + self->mRequest.Complete(); + self->TryInvokeAsyncOpen(NS_OK); + }, + [self](nsresult aStatus) { + self->mRequest.Complete(); + self->TryInvokeAsyncOpen(aStatus); + }) + ->Track(mRequest); + return true; +} + +RefPtr HttpChannelParent::WaitForBgParent( + uint64_t aChannelId) { + LOG(("HttpChannelParent::WaitForBgParent [this=%p]\n", this)); + MOZ_ASSERT(!mBgParent); + + if (!mChannel && !mEarlyHintPreloaderId) { + return GenericNonExclusivePromise::CreateAndReject(NS_ERROR_FAILURE, + __func__); + } + + nsCOMPtr registrar = + BackgroundChannelRegistrar::GetOrCreate(); + MOZ_ASSERT(registrar); + registrar->LinkHttpChannel(aChannelId, 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_WARNING("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); + } + } + + MOZ_ASSERT(!mBgParent); + MOZ_ASSERT(mPromise.IsEmpty()); + // Waiting for background channel + RefPtr self = this; + WaitForBgParent(mChannel->ChannelId()) + ->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 ClassOfService& cos) { + if (mChannel) { + mChannel->SetClassOfService(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, + const nsACString& reason, const mozilla::Maybe& logString) { + LOG(("HttpChannelParent::RecvCancel [this=%p, reason=%s]\n", this, + PromiseFlatCString(reason).get())); + + // logging child cancel reason on the parent side + if (logString.isSome()) { + LOG(("HttpChannelParent::RecvCancel: %s", logString->get())); + } + + // May receive cancel before channel has been constructed! + if (mChannel) { + mChannel->CancelWithReason(status, reason); + + 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; + + // If the channel is cancelled before the redirect is completed + // RecvRedirect2Verify will not be called, so we must clear the callback. + if (mRedirectCallback) { + mRedirectCallback->OnRedirectVerifyCallback(NS_ERROR_UNEXPECTED); + mRedirectCallback = nullptr; + } + + 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, + nsIURI* aAPIRedirectURI, + const Maybe& aCorsPreflightArgs) { + 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) { + if (aAPIRedirectURI) { + rv = newHttpChannel->RedirectTo(aAPIRedirectURI); + 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)); + } + } + + 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)); + 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; + if (mChannel) { + WaitForBgParent(mChannel->ChannelId()) + ->Then( + GetMainThreadSerialEventTarget(), __func__, + [callback]() { callback->ReadyToVerify(NS_OK); }, + [callback](const nsresult& aResult) { + NS_ERROR("failed to establish the background channel"); + callback->ReadyToVerify(aResult); + }); + } else { + // mChannel can be null for several reasons (AsyncOpenFailed, etc) + NS_ERROR("No channel for ContinueVerification"); + GetMainThreadSerialEventTarget()->Dispatch(NS_NewRunnableFunction( + __func__, [callback] { callback->ReadyToVerify(NS_ERROR_FAILURE); })); + } + return NS_OK; +} + +void HttpChannelParent::ContinueRedirect2Verify(const nsresult& aResult) { + LOG( + ("HttpChannelParent::ContinueRedirect2Verify " + "[this=%p result=%" PRIx32 "]\n", + this, static_cast(aResult))); + + 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; + } else { + LOG( + ("RecvRedirect2Verify[%p]: NO CALLBACKS! | " + "mRedirectChannelId: %" PRIx64 ", mRedirectChannel: %p", + this, mRedirectChannelId, mRedirectChannel.get())); + } +} + +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::RecvRemoveCorsPreflightCacheEntry( + nsIURI* uri, const mozilla::ipc::PrincipalInfo& requestingPrincipal, + const OriginAttributes& originAttributes) { + if (!uri) { + 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(uri, principal, + originAttributes); + return IPC_OK(); +} + +mozilla::ipc::IPCResult HttpChannelParent::RecvSetCookies( + const nsACString& aBaseDomain, const OriginAttributes& aOriginAttributes, + nsIURI* aHost, const bool& aFromHttp, nsTArray&& aCookies) { + net::PCookieServiceParent* csParent = + LoneManagedOrNullAsserts(Manager()->ManagedPCookieServiceParent()); + NS_ENSURE_TRUE(csParent, IPC_OK()); + + auto* cs = static_cast(csParent); + + BrowsingContext* browsingContext = nullptr; + if (mBrowserParent) { + browsingContext = mBrowserParent->GetBrowsingContext(); + } + + return cs->SetCookies(nsCString(aBaseDomain), aOriginAttributes, aHost, + aFromHttp, aCookies, browsingContext); +} + +//----------------------------------------------------------------------------- +// 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. + + aChannel->GetCacheReadStart(&timeStamp); + args.cacheReadStart() = timeStamp; + + aChannel->GetCacheReadEnd(&timeStamp); + args.cacheReadEnd() = timeStamp; + + aChannel->GetTransactionPending(&timeStamp); + args.transactionPending() = 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 isFirstPartOfMultiPart = false; + 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->GetIsFirstPart(&isFirstPartOfMultiPart); + 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.isFirstPartOfMultiPart() = isFirstPartOfMultiPart; + 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()); + httpChannelImpl->GetProtocolVersion(args.protocolVersion()); + + 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; + } + + // 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() = static_cast(mCacheEntry); + + httpChannelImpl->GetCacheKey(&args.cacheKey()); + httpChannelImpl->GetAlternativeDataType(args.altDataType()); + } + + args.altDataLength() = chan->GetAltDataLength(); + args.deliveringAltData() = chan->IsDeliveringAltData(); + + args.securityInfo() = SecurityInfo(); + + chan->GetRedirectCount(&args.redirectCount()); + chan->GetHasHTTPSRR(&args.hasHTTPSRR()); + + chan->GetIsProxyUsed(&args.isProxyUsed()); + + 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; + } + + if (chan->ChannelBlockedByOpaqueResponse() && + chan->CachedOpaqueResponseBlockingPref()) { + responseHead->ClearHeaders(); + } + + 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; + + nsCOMPtr altDataSource; + nsCOMPtr cacheChannel = + do_QueryInterface(static_cast(mChannel.get())); + if (cacheChannel) { + for (const auto& pref : cacheChannel->PreferredAlternativeDataTypes()) { + if (pref.type() == args.altDataType() && + pref.deliverAltData() == + nsICacheInfoChannel::PreferredAlternativeDataDeliveryType:: + SERIALIZE) { + altDataSource = mCacheEntry; + break; + } + } + } + + nsIRequest::TRRMode effectiveMode = nsIRequest::TRR_DEFAULT_MODE; + mChannel->GetEffectiveTRRMode(&effectiveMode); + args.effectiveTRRMode() = effectiveMode; + + TRRSkippedReason reason = TRRSkippedReason::TRR_UNSET; + mChannel->GetTrrSkipReason(&reason); + args.trrSkipReason() = reason; + + if (mIPCClosed || + !mBgParent->OnStartRequest( + *responseHead, useResponseHead, + cleanedUpRequest ? cleanedUpRequestHeaders : requestHead->Headers(), + args, altDataSource, chan->GetOnStartRequestStartTime())) { + 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(); + } + + if (!args.timing().domainLookupEnd().IsNull() && + !args.timing().connectStart().IsNull()) { + nsAutoCString protocolVersion; + mChannel->GetProtocolVersion(protocolVersion); + uint32_t classOfServiceFlags = 0; + mChannel->GetClassFlags(&classOfServiceFlags); + nsAutoCString cosString; + ClassOfService::ToString(classOfServiceFlags, cosString); + nsAutoCString key( + nsPrintfCString("%s_%s", protocolVersion.get(), cosString.get())); + Telemetry::AccumulateTimeDelta( + Telemetry::NETWORK_DNS_END_TO_CONNECT_START_EXP_MS, key, + args.timing().domainLookupEnd(), args.timing().connectStart()); + } + + 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); + TimeStamp onStopRequestStart; + if (httpChannel) { + httpChannel->StealConsoleReports(consoleReports); + onStopRequestStart = httpChannel->GetOnStopRequestStartTime(); + } + + // 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, onStopRequestStart)) { + 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); + TimeStamp onDataAvailableStart = TimeStamp::Now(); + if (httpChannelImpl) { + if (httpChannelImpl->IsReadingFromCache()) { + transportStatus = NS_NET_STATUS_READING; + } + onDataAvailableStart = httpChannelImpl->GetDataAvailableStartTime(); + } + + 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, onDataAvailableStart)) { + 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(); + } + Maybe ipcStream; + if (mCacheEntry) { + nsCOMPtr inputStream; + nsresult rv = mCacheEntry->OpenInputStream(0, getter_AddRefs(inputStream)); + if (NS_SUCCEEDED(rv)) { + Unused << mozilla::ipc::SerializeIPCStream( + inputStream.forget(), ipcStream, /* aAllowLazy */ false); + } + } + + Unused << SendOriginalCacheInputStreamAvailable(ipcStream); + 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::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; +} + +bool HttpChannelParent::IsRedirectDueToAuthRetry(uint32_t redirectFlags) { + return (redirectFlags & nsIChannelEventSink::REDIRECT_AUTH_RETRY); +} + +//----------------------------------------------------------------------------- +// 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 or + // internal redirect due to auth retries, 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); + + // 1. We only want to hide the special internal redirects from + // nsHttpChannel to InterceptedHttpChannel. + // 2. We want to allow through internal redirects + // initiated from the InterceptedHttpChannel even if they are to another + // InterceptedHttpChannel, except the interception reset, since + // corresponding HttpChannelChild/Parent objects can be reused for reset + // case. + // 3. If this is an internal redirect due to auth retry then we will + // hide it from the child process + + if ((!oldIntercepted && newIntercepted) || + (oldIntercepted && !newIntercepted && oldIntercepted->IsReset()) || + (IsRedirectDueToAuthRetry(redirectFlags))) { + // 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()); + } + + // If this is ServiceWorker fallback redirect, info HttpChannelChild to + // detach StreamFilters. Otherwise StreamFilters will be attached twice + // on the same HttpChannelChild when opening the new nsHttpChannel. + if (oldIntercepted) { + Unused << DetachStreamFilters(); + } + + // Re-link the HttpChannelParent to the new channel. + nsCOMPtr linkedChannel; + rv = NS_LinkRedirectChannels(mRedirectChannelId, this, + getter_AddRefs(linkedChannel)); + NS_ENSURE_SUCCESS(rv, rv); + MOZ_ASSERT(linkedChannel == newChannel); + + // We immediately store the channel 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)); + + uint32_t newLoadFlags = nsIRequest::LOAD_NORMAL; + MOZ_ALWAYS_SUCCEEDS(newChannel->GetLoadFlags(&newLoadFlags)); + + nsCOMPtr securityInfo(SecurityInfo()); + + // 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; + } + + if (!mIPCClosed) { + if (!SendRedirect1Begin(mRedirectChannelId, newOriginalURI, newLoadFlags, + redirectFlags, loadInfoForwarderArg, *responseHead, + securityInfo, channelId, mChannel->GetPeerAddr(), + GetTimingAttributes(mChannel))) { + return NS_BINDING_ABORTED; + } + } + + // Result is handled in RecvRedirect2Verify above + + mRedirectChannel = newChannel; + mRedirectCallback = callback; + return NS_OK; +} + +NS_IMETHODIMP +HttpChannelParent::CompleteRedirect(nsresult status) { + LOG(("HttpChannelParent::CompleteRedirect [this=%p status=0x%X]\n", this, + static_cast(status))); + + // 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 (!mIPCClosed) { + // TODO: check return value: assume child dead if failed + if (NS_SUCCEEDED(status)) { + Unused << SendRedirect3Complete(); + } else { + Unused << SendRedirectFailed(status); + } + } + + 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; +} + +already_AddRefed HttpChannelParent::SecurityInfo() { + if (!mChannel) { + return nullptr; + } + nsCOMPtr securityInfo; + mChannel->GetSecurityInfo(getter_AddRefs(securityInfo)); + return securityInfo.forget(); +} + +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; +} + +//----------------------------------------------------------------------------- +// 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, + bool aIsWarning) { + if (mIPCClosed || + NS_WARN_IF(!SendLogBlockedCORSRequest( + nsString(aMessage), nsCString(aCategory), aIsWarning))) { + 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(nsresult status) { + LOG(("HttpChannelParent::OnRedirectResult [this=%p, status=0x%X]", this, + static_cast(status))); + + nsresult rv = NS_OK; + + 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) { + if (NS_FAILED(rv)) { + status = rv; + } else { + status = NS_ERROR_NULL_POINTER; + } + } + + CompleteRedirect(status); + + if (NS_SUCCEEDED(status)) { + 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)); +} + +auto HttpChannelParent::DetachStreamFilters() -> RefPtr { + LOG(("HttpChannelParent::DeattachStreamFilter [this=%p]", this)); + MOZ_ASSERT(!mAfterOnStartRequestBegun); + + if (NS_WARN_IF(mIPCClosed)) { + return GenericPromise::CreateAndReject(NS_ERROR_FAILURE, __func__); + } + + MOZ_ASSERT(mBgParent); + return InvokeAsync(mBgParent->GetBackgroundTarget(), mBgParent.get(), + __func__, + &HttpBackgroundChannelParent::DetachStreamFilters); +} + +void HttpChannelParent::SetHttpChannelFromEarlyHintPreloader( + HttpBaseChannel* aChannel) { + MOZ_ASSERT(aChannel); + if (mChannel) { + MOZ_ASSERT(false, "SetHttpChannel called with mChannel aready set"); + return; + } + + mChannel = aChannel; +} + +void HttpChannelParent::SetCookie(nsCString&& aCookie) { + LOG(("HttpChannelParent::SetCookie [this=%p]", this)); + MOZ_ASSERT(!mAfterOnStartRequestBegun); + MOZ_ASSERT(mCookie.IsEmpty()); + + // The loadGroup of the channel in the parent process could be null in the + // XPCShell content process test, see test_cookiejars_wrap.js. In this case, + // we cannot explicitly set the loadGroup for the parent channel because it's + // created from the content process. To workaround this, we add a testing pref + // to skip this check. + if (!Preferences::GetBool( + "network.cookie.skip_browsing_context_check_in_parent_for_testing") && + mChannel->IsBrowsingContextDiscarded()) { + return; + } + mCookie = std::move(aCookie); +} + +} // namespace mozilla::net diff --git a/netwerk/protocol/http/HttpChannelParent.h b/netwerk/protocol/http/HttpChannelParent.h new file mode 100644 index 0000000000..fdd55f9a1d --- /dev/null +++ b/netwerk/protocol/http/HttpChannelParent.h @@ -0,0 +1,333 @@ +/* -*- 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 "HttpBaseChannel.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 "mozilla/dom/ipc/IdType.h" +#include "nsIMultiPartChannel.h" +#include "nsIURI.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; + +class HttpChannelParent final : public nsIInterfaceRequestor, + public PHttpChannelParent, + public nsIParentRedirectingChannel, + public nsIProgressEventSink, + 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_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 aOverrideStatus); + + [[nodiscard]] bool Init(const HttpChannelCreationArgs& aArgs); + + // 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); + + void InvokeEarlyHintPreloader(nsresult rv, uint64_t aEarlyHintPreloaderId); + + // 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); + [[nodiscard]] RefPtr DetachStreamFilters(); + + // Should only be called from EarlyHintPreloader. mChannel should be null at + // the point of calling. Sets mChannel to aChannel. Used by the + // EarlyHintPreloader to redirect the channel to this parent as soon as the + // final channel becomes available after all http redirects. + void SetHttpChannelFromEarlyHintPreloader(HttpBaseChannel* aChannel); + + protected: + // used to connect redirected-to channel in parent with just created + // ChildChannel. Used during redirects. + [[nodiscard]] bool ConnectChannel(const uint32_t& registrarId); + + [[nodiscard]] bool DoAsyncOpen( + nsIURI* uri, nsIURI* originalUri, nsIURI* docUri, + nsIReferrerInfo* aReferrerInfo, nsIURI* aAPIRedirectToURI, + nsIURI* topWindowUri, const uint32_t& loadFlags, + const RequestHeaderTuples& requestHeaders, const nsCString& requestMethod, + const Maybe& uploadStream, const bool& uploadStreamHasHeaders, + const int16_t& priority, const ClassOfService& classOfService, + const uint8_t& redirectionLimit, const bool& allowSTS, + const uint32_t& thirdPartyFlags, const bool& doResumeAt, + const uint64_t& startPos, const nsCString& entityID, + const bool& allowSpdy, const bool& allowHttp3, const bool& allowAltSvc, + const bool& beConservative, const bool& bypassProxy, + const uint32_t& tlsFlags, const LoadInfoArgs& 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 dom::RequestMode& aRequestMode, const uint32_t& aRedirectMode, + const uint64_t& aChannelId, const nsString& aIntegrityMetadata, + const uint64_t& aContentWindowId, + const nsTArray& + aPreferredAlternativeTypes, + const uint64_t& aBrowserId, 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 uint64_t& aEarlyHintPreloaderId, + const nsAString& aClassicScriptHintCharset, + const nsAString& aDocumentCharacterSet, + const bool& aIsUserAgentHeaderModified); + + virtual mozilla::ipc::IPCResult RecvSetPriority( + const int16_t& priority) override; + virtual mozilla::ipc::IPCResult RecvSetClassOfService( + const ClassOfService& 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, + const nsACString& reason, + const mozilla::Maybe& logString) 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, + nsIURI* apiRedirectUri, + const Maybe& aCorsPreflightArgs) override; + virtual mozilla::ipc::IPCResult RecvDocumentChannelCleanup( + const bool& clearCacheEntry) override; + virtual mozilla::ipc::IPCResult RecvRemoveCorsPreflightCacheEntry( + nsIURI* uri, const mozilla::ipc::PrincipalInfo& requestingPrincipal, + const OriginAttributes& originAttributes) override; + virtual mozilla::ipc::IPCResult RecvSetCookies( + const nsACString& aBaseDomain, const OriginAttributes& aOriginAttributes, + nsIURI* aHost, const bool& aFromHttp, + nsTArray&& aCookies) override; + virtual mozilla::ipc::IPCResult RecvBytesRead(const int32_t& aCount) override; + virtual mozilla::ipc::IPCResult RecvOpenOriginalCacheInputStream() 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, + bool aIsWarning = false) 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: + already_AddRefed SecurityInfo(); + + // 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( + uint64_t aChannelId); + + // 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(); + + bool IsRedirectDueToAuthRetry(uint32_t redirectFlags); + + int32_t mSendWindowSize; + + friend class HttpBackgroundChannelParent; + + uint64_t mEarlyHintPreloaderId{}; + + RefPtr mChannel; + nsCOMPtr mCacheEntry; + + nsCOMPtr mRedirectChannel; + nsCOMPtr mRedirectCallback; + + 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 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..a94bc839ba --- /dev/null +++ b/netwerk/protocol/http/HttpConnectionBase.cpp @@ -0,0 +1,101 @@ +/* -*- 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() { + 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); +} + +void HttpConnectionBase::ChangeConnectionState(ConnectionState aState) { + LOG(("HttpConnectionBase::ChangeConnectionState this=%p (%d->%d)", this, + static_cast(mConnectionState), static_cast(aState))); + + // The state can't move backward. + if (aState <= mConnectionState) { + return; + } + + mConnectionState = aState; +} + +void HttpConnectionBase::RecordConnectionCloseTelemetry(nsresult aReason) { + /** + * + * The returned telemetry key has the format: + * "Version_EndToEndSSL_IsTrrServiceChannel_ExperienceState_ConnectionState" + * + * - Version: The HTTP version of the connection. + * - EndToEndSSL: Indicates whether SSL encryption is end-to-end. + * - IsTrrServiceChannel: Specifies if the connection is used to send TRR + * requests. + * - ExperienceState: ConnectionExperienceState + * - ConnectionState: The connection state before closing. + */ + auto key = nsPrintfCString("%d_%d_%d_%d_%d", static_cast(Version()), + mConnInfo->EndToEndSSL(), + mConnInfo->GetIsTrrServiceChannel(), + static_cast(mExperienceState), + static_cast(mConnectionState)); + SetCloseReason(ToCloseReason(aReason)); + LOG(("RecordConnectionCloseTelemetry key=%s reason=%d\n", key.get(), + static_cast(mCloseReason))); + Telemetry::Accumulate(Telemetry::HTTP_CONNECTION_CLOSE_REASON, key, + static_cast(mCloseReason)); +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/HttpConnectionBase.h b/netwerk/protocol/http/HttpConnectionBase.h new file mode 100644 index 0000000000..a4ccac735f --- /dev/null +++ b/netwerk/protocol/http/HttpConnectionBase.h @@ -0,0 +1,246 @@ +/* -*- 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 "mozilla/Mutex.h" +#include "ARefBase.h" +#include "TimingStruct.h" +#include "HttpTrafficAnalyzer.h" + +#include "mozilla/net/DNS.h" +#include "nsIAsyncInputStream.h" +#include "nsIAsyncOutputStream.h" +#include "nsIInterfaceRequestor.h" +#include "nsITimer.h" + +class nsISocketTransport; +class nsITLSSocketControl; + +namespace mozilla { +namespace net { + +class nsHttpHandler; +class ASpdySession; +class Http3WebTransportSession; + +enum class ConnectionState : uint32_t { + HALF_OPEN = 0, + INITED, + TLS_HANDSHAKING, + ZERORTT, + TRANSFERING, + CLOSED +}; + +enum class ConnectionExperienceState : uint32_t { + Not_Experienced = 0, + First_Request_Sent = (1 << 0), + First_Response_Received = (1 << 1), + Experienced = (1 << 2), +}; + +MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(ConnectionExperienceState); + +// 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(); + + // 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; + + 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; + + Http3WebTransportSession* GetWebTransportSession( + nsAHttpTransaction* aTransaction) { + return nullptr; + } + + 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 WebSocketSupport GetWebSocketSupport() { + return WebSocketSupport::NO_SUPPORT; + } + + void GetConnectionInfo(nsHttpConnectionInfo** ci) { + *ci = do_AddRef(mConnInfo).take(); + } + virtual void GetTLSSocketControl(nsITLSSocketControl** 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; + virtual int64_t BytesWritten() = 0; // 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; + + virtual nsISocketTransport* Transport() { return nullptr; } + + virtual nsresult GetSelfAddr(NetAddr* addr) = 0; + virtual nsresult GetPeerAddr(NetAddr* addr) = 0; + virtual bool ResolvedByTRR() = 0; + virtual nsIRequest::TRRMode EffectiveTRRMode() = 0; + virtual TRRSkippedReason TRRSkipReason() = 0; + virtual bool GetEchConfigUsed() = 0; + virtual PRIntervalTime LastWriteTime() = 0; + + void ChangeConnectionState(ConnectionState aState); + void SetCloseReason(ConnectionCloseReason aReason) { + if (mCloseReason == ConnectionCloseReason::UNSET) { + mCloseReason = aReason; + } + } + + void RecordConnectionCloseTelemetry(nsresult aReason); + + protected: + // The capabailities associated with the most recent transaction + uint32_t mTransactionCaps{0}; + + RefPtr mConnInfo; + + bool mExperienced{false}; + // Used to track whether this connection is serving the first request. + bool mHasFirstHttpTransaction{false}; + + bool mBootstrappedTimingsSet{false}; + TimingStruct mBootstrappedTimings; + + Mutex mCallbacksLock MOZ_UNANNOTATED{"nsHttpConnection::mCallbacksLock"}; + nsMainThreadPtrHandle mCallbacks; + + nsTArray mTrafficCategory; + PRIntervalTime mRtt{0}; + nsresult mErrorBeforeConnect = NS_OK; + + ConnectionState mConnectionState = ConnectionState::HALF_OPEN; + + // Represent if the connection has served more than one request. + ConnectionExperienceState mExperienceState = + ConnectionExperienceState::Not_Experienced; + + ConnectionCloseReason mCloseReason = ConnectionCloseReason::UNSET; +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(HttpConnectionBase, HTTPCONNECTIONBASE_IID) + +#define NS_DECL_HTTPCONNECTIONBASE \ + [[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 GetTLSSocketControl(nsITLSSocketControl** 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; \ + PRIntervalTime LastWriteTime() 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..96756ca035 --- /dev/null +++ b/netwerk/protocol/http/HttpConnectionMgrChild.cpp @@ -0,0 +1,191 @@ +/* -*- 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 "mozilla/net/WebSocketConnectionChild.h" +#include "nsHttpConnectionInfo.h" +#include "nsHttpConnectionMgr.h" +#include "nsHttpHandler.h" +#include "nsISpeculativeConnect.h" + +namespace mozilla::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::RecvUpdateCurrentBrowserId( + const uint64_t& aId) { + mConnMgr->UpdateCurrentBrowserId(aId); + 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 ClassOfService& 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( + const 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(); +} + +mozilla::ipc::IPCResult HttpConnectionMgrChild::RecvStartWebSocketConnection( + PHttpTransactionChild* aTransWithStickyConn, uint32_t aListenerId) { + RefPtr child = new WebSocketConnectionChild(); + child->Init(aListenerId); + nsCOMPtr listener = + static_cast(child.get()); + Unused << mConnMgr->CompleteUpgrade( + ToRealHttpTransaction(aTransWithStickyConn), listener); + return IPC_OK(); +} + +} // namespace mozilla::net diff --git a/netwerk/protocol/http/HttpConnectionMgrChild.h b/netwerk/protocol/http/HttpConnectionMgrChild.h new file mode 100644 index 0000000000..2279f9ec46 --- /dev/null +++ b/netwerk/protocol/http/HttpConnectionMgrChild.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 HttpConnectionMgrChild_h__ +#define HttpConnectionMgrChild_h__ + +#include "mozilla/net/PHttpConnectionMgrChild.h" +#include "mozilla/RefPtr.h" + +namespace mozilla::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 RecvUpdateCurrentBrowserId(const uint64_t& aId); + 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 ClassOfService& aClassOfService); + mozilla::ipc::IPCResult RecvCancelTransaction(PHttpTransactionChild* aTrans, + const nsresult& aReason); + mozilla::ipc::IPCResult RecvSpeculativeConnect( + const HttpConnectionInfoCloneArgs& aConnInfo, + Maybe aOverriderArgs, uint32_t aCaps, + Maybe aTrans, const bool& aFetchHTTPSRR); + mozilla::ipc::IPCResult RecvStartWebSocketConnection( + PHttpTransactionChild* aTransWithStickyConn, uint32_t aListenerId); + + private: + virtual ~HttpConnectionMgrChild(); + + RefPtr mConnMgr; +}; + +} // namespace mozilla::net + +#endif // HttpConnectionMgrChild_h__ diff --git a/netwerk/protocol/http/HttpConnectionMgrParent.cpp b/netwerk/protocol/http/HttpConnectionMgrParent.cpp new file mode 100644 index 0000000000..594fea20ec --- /dev/null +++ b/netwerk/protocol/http/HttpConnectionMgrParent.cpp @@ -0,0 +1,319 @@ +/* -*- 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 "mozilla/net/WebSocketConnectionParent.h" +#include "nsHttpConnectionInfo.h" +#include "nsIHttpChannelInternal.h" +#include "nsIInterfaceRequestor.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsISpeculativeConnect.h" +#include "nsIOService.h" +#include "nsQueryObject.h" + +namespace mozilla::net { + +nsTHashMap> + HttpConnectionMgrParent::sHttpUpgradeListenerMap; +uint32_t HttpConnectionMgrParent::sListenerId = 0; +StaticMutex HttpConnectionMgrParent::sLock; + +// static +uint32_t HttpConnectionMgrParent::AddHttpUpgradeListenerToMap( + nsIHttpUpgradeListener* aListener) { + StaticMutexAutoLock lock(sLock); + uint32_t id = sListenerId++; + sHttpUpgradeListenerMap.InsertOrUpdate(id, nsCOMPtr{aListener}); + return id; +} + +// static +void HttpConnectionMgrParent::RemoveHttpUpgradeListenerFromMap(uint32_t aId) { + StaticMutexAutoLock lock(sLock); + sHttpUpgradeListenerMap.Remove(aId); +} + +// static +Maybe> +HttpConnectionMgrParent::GetAndRemoveHttpUpgradeListener(uint32_t aId) { + StaticMutexAutoLock lock(sLock); + return sHttpUpgradeListenerMap.Extract(aId); +} + +NS_IMPL_ISUPPORTS0(HttpConnectionMgrParent) + +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::UpdateCurrentBrowserId(uint64_t aId) { + RefPtr self = this; + auto task = [self, aId]() { + Unused << self->SendUpdateCurrentBrowserId(aId); + }; + 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(WrapNotNull(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( + WrapNotNull(aTrans->AsHttpTransactionParent()), aPriority, + WrapNotNull(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( + WrapNotNull(aTrans->AsHttpTransactionParent()), aPriority); + return NS_OK; +} + +void HttpConnectionMgrParent::UpdateClassOfServiceOnTransaction( + HttpTransactionShell* aTrans, const ClassOfService& aClassOfService) { + MOZ_ASSERT(gIOService->SocketProcessReady()); + + if (!CanSend()) { + return; + } + + Unused << SendUpdateClassOfServiceOnTransaction( + WrapNotNull(aTrans->AsHttpTransactionParent()), aClassOfService); +} + +nsresult HttpConnectionMgrParent::CancelTransaction( + HttpTransactionShell* aTrans, nsresult aReason) { + MOZ_ASSERT(gIOService->SocketProcessReady()); + + if (!CanSend()) { + return NS_ERROR_NOT_AVAILABLE; + } + + Unused << SendCancelTransaction( + WrapNotNull(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(WrapNotNull(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) { + // Do nothing. +} + +void HttpConnectionMgrParent::ExcludeHttp3(const nsHttpConnectionInfo* ci) { + // Do nothing. +} + +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) { + MOZ_ASSERT(aTrans->AsHttpTransactionParent()); + + if (!CanSend()) { + // OnUpgradeFailed is expected to be called on socket thread. + nsCOMPtr target = + do_GetService(NS_SOCKETTRANSPORTSERVICE_CONTRACTID); + if (target) { + nsCOMPtr listener = aUpgradeListener; + target->Dispatch(NS_NewRunnableFunction( + "HttpConnectionMgrParent::CompleteUpgrade", [listener]() { + Unused << listener->OnUpgradeFailed(NS_ERROR_NOT_AVAILABLE); + })); + } + return NS_OK; + } + + // We need to link the id and the upgrade listener here, so + // WebSocketConnectionParent can connect to the listener correctly later. + uint32_t id = AddHttpUpgradeListenerToMap(aUpgradeListener); + Unused << SendStartWebSocketConnection( + WrapNotNull(aTrans->AsHttpTransactionParent()), id); + return NS_OK; +} + +nsHttpConnectionMgr* HttpConnectionMgrParent::AsHttpConnectionMgr() { + return nullptr; +} + +HttpConnectionMgrParent* HttpConnectionMgrParent::AsHttpConnectionMgrParent() { + return this; +} + +} // namespace mozilla::net diff --git a/netwerk/protocol/http/HttpConnectionMgrParent.h b/netwerk/protocol/http/HttpConnectionMgrParent.h new file mode 100644 index 0000000000..f884dfd0a5 --- /dev/null +++ b/netwerk/protocol/http/HttpConnectionMgrParent.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 HttpConnectionMgrParent_h__ +#define HttpConnectionMgrParent_h__ + +#include "HttpConnectionMgrShell.h" +#include "mozilla/net/PHttpConnectionMgrParent.h" +#include "mozilla/StaticMutex.h" + +namespace mozilla::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() = default; + + static uint32_t AddHttpUpgradeListenerToMap( + nsIHttpUpgradeListener* aListener); + static void RemoveHttpUpgradeListenerFromMap(uint32_t aId); + static Maybe> + GetAndRemoveHttpUpgradeListener(uint32_t aId); + + private: + virtual ~HttpConnectionMgrParent() = default; + + bool mShutDown{false}; + static uint32_t sListenerId; + static StaticMutex sLock MOZ_UNANNOTATED; + static nsTHashMap> + sHttpUpgradeListenerMap; +}; + +} // namespace mozilla::net + +#endif // HttpConnectionMgrParent_h__ diff --git a/netwerk/protocol/http/HttpConnectionMgrShell.h b/netwerk/protocol/http/HttpConnectionMgrShell.h new file mode 100644 index 0000000000..9c58ad5e63 --- /dev/null +++ b/netwerk/protocol/http/HttpConnectionMgrShell.h @@ -0,0 +1,229 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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::net { + +class ARefBase; +class EventTokenBucket; +class HttpTransactionShell; +class nsHttpConnectionInfo; +class HttpConnectionBase; +class nsHttpConnectionMgr; +class HttpConnectionMgrParent; +class SpeculativeTransaction; +class ClassOfService; + +//---------------------------------------------------------------------------- +// 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 UpdateCurrentBrowserId(uint64_t aId) = 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*, const ClassOfService& 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 UpdateCurrentBrowserId(uint64_t aId) 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*, const ClassOfService& 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 mozilla::net + +#endif // HttpConnectionMgrShell_h__ diff --git a/netwerk/protocol/http/HttpConnectionUDP.cpp b/netwerk/protocol/http/HttpConnectionUDP.cpp new file mode 100644 index 0000000000..b0e39a6d56 --- /dev/null +++ b/netwerk/protocol/http/HttpConnectionUDP.cpp @@ -0,0 +1,706 @@ +/* -*- 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/StaticPrefs_network.h" +#include "mozilla/Telemetry.h" +#include "HttpConnectionUDP.h" +#include "nsHttpHandler.h" +#include "Http3Session.h" +#include "nsComponentManagerUtils.h" +#include "nsISocketProvider.h" +#include "nsNetAddr.h" +#include "nsINetAddr.h" + +namespace mozilla { +namespace net { + +//----------------------------------------------------------------------------- +// HttpConnectionUDP +//----------------------------------------------------------------------------- + +HttpConnectionUDP::HttpConnectionUDP() : mHttpHandler(gHttpHandler) { + LOG(("Creating HttpConnectionUDP @%p\n", this)); +} + +HttpConnectionUDP::~HttpConnectionUDP() { + LOG(("Destroying HttpConnectionUDP @%p\n", this)); + + if (mForceSendTimer) { + mForceSendTimer->Cancel(); + mForceSendTimer = nullptr; + } +} + +nsresult HttpConnectionUDP::Init(nsHttpConnectionInfo* info, + nsIDNSRecord* dnsRecord, nsresult status, + nsIInterfaceRequestor* callbacks, + uint32_t caps) { + LOG1(("HttpConnectionUDP::Init this=%p", this)); + NS_ENSURE_ARG_POINTER(info); + NS_ENSURE_TRUE(!mConnInfo, NS_ERROR_ALREADY_INITIALIZED); + + mConnInfo = info; + MOZ_ASSERT(mConnInfo); + MOZ_ASSERT(mConnInfo->IsHttp3()); + + mErrorBeforeConnect = status; + mAlpnToken = mConnInfo->GetNPNToken(); + if (NS_FAILED(mErrorBeforeConnect)) { + // See explanation for non-strictness of this operation in + // SetSecurityCallbacks. + mCallbacks = new nsMainThreadPtrHolder( + "HttpConnectionUDP::mCallbacks", callbacks, false); + SetCloseReason(ToCloseReason(mErrorBeforeConnect)); + return mErrorBeforeConnect; + } + + nsCOMPtr dnsAddrRecord = do_QueryInterface(dnsRecord); + if (!dnsAddrRecord) { + return NS_ERROR_FAILURE; + } + dnsAddrRecord->IsTRR(&mResolvedByTRR); + dnsAddrRecord->GetEffectiveTRRMode(&mEffectiveTRRMode); + dnsAddrRecord->GetTrrSkipReason(&mTRRSkipReason); + NetAddr peerAddr; + nsresult rv = dnsAddrRecord->GetNextAddr(mConnInfo->GetRoutedHost().IsEmpty() + ? mConnInfo->OriginPort() + : mConnInfo->RoutedPort(), + &peerAddr); + if (NS_FAILED(rv)) { + return rv; + } + + mSocket = do_CreateInstance("@mozilla.org/network/udp-socket;1", &rv); + if (NS_FAILED(rv)) { + return rv; + } + + // We need an address here so that we can convey the IP version of the + // socket. + NetAddr local; + memset(&local, 0, sizeof(local)); + local.raw.family = peerAddr.raw.family; + rv = mSocket->InitWithAddress(&local, nullptr, false, 1); + if (NS_FAILED(rv)) { + mSocket = nullptr; + return rv; + } + + rv = mSocket->SetRecvBufferSize( + StaticPrefs::network_http_http3_recvBufferSize()); + if (NS_FAILED(rv)) { + LOG(("HttpConnectionUDP::Init SetRecvBufferSize failed %d [this=%p]", + static_cast(rv), this)); + mSocket->Close(); + mSocket = nullptr; + return rv; + } + + if (peerAddr.raw.family == AF_INET) { + rv = mSocket->SetDontFragment(true); + if (NS_FAILED(rv)) { + LOG(("HttpConnectionUDP::Init SetDontFragment failed %d [this=%p]", + static_cast(rv), this)); + } + } + + // get the resulting socket address. + rv = mSocket->GetLocalAddr(getter_AddRefs(mSelfAddr)); + if (NS_FAILED(rv)) { + mSocket->Close(); + mSocket = nullptr; + return rv; + } + + uint32_t controlFlags = 0; + if (caps & NS_HTTP_LOAD_ANONYMOUS) { + controlFlags |= nsISocketProvider::ANONYMOUS_CONNECT; + } + if (mConnInfo->GetPrivate()) { + controlFlags |= nsISocketProvider::NO_PERMANENT_STORAGE; + } + if (((caps & NS_HTTP_BE_CONSERVATIVE) || mConnInfo->GetBeConservative()) && + gHttpHandler->ConnMgr()->BeConservativeIfProxied( + mConnInfo->ProxyInfo())) { + controlFlags |= nsISocketProvider::BE_CONSERVATIVE; + } + + if (mResolvedByTRR) { + controlFlags |= nsISocketProvider::USED_PRIVATE_DNS; + } + + mPeerAddr = new nsNetAddr(&peerAddr); + mHttp3Session = new Http3Session(); + rv = mHttp3Session->Init(mConnInfo, mSelfAddr, mPeerAddr, this, controlFlags, + callbacks); + if (NS_FAILED(rv)) { + LOG( + ("HttpConnectionUDP::Init mHttp3Session->Init failed " + "[this=%p rv=%x]\n", + this, static_cast(rv))); + mSocket->Close(); + mSocket = nullptr; + mHttp3Session = nullptr; + return rv; + } + + ChangeConnectionState(ConnectionState::INITED); + // See explanation for non-strictness of this operation in + // SetSecurityCallbacks. + mCallbacks = new nsMainThreadPtrHolder( + "HttpConnectionUDP::mCallbacks", callbacks, false); + + // Call SyncListen at the end of this function. This call will actually + // attach the sockte to SocketTransportService. + rv = mSocket->SyncListen(this); + if (NS_FAILED(rv)) { + mSocket->Close(); + mSocket = nullptr; + return rv; + } + + 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()) { + mHasFirstHttpTransaction = true; + // For QUIC we have HttpConnecitonUDP before the actual connection + // has been establish so wait for TLS handshake to be finished before + // we mark the connection 'experienced'. + if (!mExperienced && mHttp3Session && 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); + + // 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 (NS_FAILED(mErrorBeforeConnect)) { + CloseTransaction(nullptr, mErrorBeforeConnect); + trans->Close(mErrorBeforeConnect); + gHttpHandler->ExcludeHttp3(mConnInfo); + return mErrorBeforeConnect; + } + + if (!mHttp3Session->AddStream(trans, pri, mCallbacks)) { + MOZ_ASSERT(false); // this cannot happen! + trans->Close(NS_ERROR_ABORT); + return NS_ERROR_FAILURE; + } + + if (mHasFirstHttpTransaction && mExperienced) { + mHasFirstHttpTransaction = false; + mExperienceState |= ConnectionExperienceState::Experienced; + } + + 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 (mConnectionState != ConnectionState::CLOSED) { + RecordConnectionCloseTelemetry(reason); + ChangeConnectionState(ConnectionState::CLOSED); + } + + 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 (mSocket) { + mSocket->Close(); + mSocket = 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 (NS_FAILED(mErrorBeforeConnect)) { + 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?"); + + DebugOnly rv = + responseHead->SetHeader(nsHttp::X_Firefox_Http3, mAlpnToken); + 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() - mHttp3Session->LastWriteTime()) < 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::GetTLSSocketControl(nsITLSSocketControl** secinfo) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG(("HttpConnectionUDP::GetTLSSocketControl http3Session=%p\n", + mHttp3Session.get())); + + if (mHttp3Session && + NS_SUCCEEDED(mHttp3Session->GetTransactionTLSSocketControl(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) { + return mConn->RecvData(); + } + + MOZ_ASSERT(mConn->mForceSendPending); + mConn->mForceSendPending = false; + + return mConn->SendData(); + } + + private: + RefPtr mConn; + bool mDoRecv; +}; + +nsresult HttpConnectionUDP::ResumeSend() { + LOG(("HttpConnectionUDP::ResumeSend [this=%p]\n", this)); + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + RefPtr self(this); + NS_DispatchToCurrentThread( + NS_NewRunnableFunction("HttpConnectionUDP::CallSendData", + [self{std::move(self)}]() { self->SendData(); })); + return NS_OK; +} + +nsresult HttpConnectionUDP::ResumeRecv() { return NS_OK; } + +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; } + +PRIntervalTime HttpConnectionUDP::LastWriteTime() { + return mHttp3Session->LastWriteTime(); +} + +//----------------------------------------------------------------------------- +// 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; +} + +void HttpConnectionUDP::OnQuicTimeoutExpired() { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG(("HttpConnectionUDP::OnQuicTimeoutExpired [this=%p]\n", this)); + // if the transaction was dropped... + if (!mHttp3Session) { + LOG((" no transaction; ignoring event\n")); + return; + } + + nsresult rv = mHttp3Session->ProcessOutputAndEvents(mSocket); + if (NS_FAILED(rv)) { + CloseTransaction(mHttp3Session, 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(nsIUDPSocketSyncListener) + NS_INTERFACE_MAP_ENTRY(nsIInterfaceRequestor) + NS_INTERFACE_MAP_ENTRY(HttpConnectionBase) + NS_INTERFACE_MAP_ENTRY_CONCRETE(HttpConnectionUDP) +NS_INTERFACE_MAP_END + +void HttpConnectionUDP::NotifyDataRead() { + mExperienceState |= ConnectionExperienceState::First_Response_Received; +} + +void HttpConnectionUDP::NotifyDataWrite() { + mExperienceState |= ConnectionExperienceState::First_Request_Sent; +} + +// called on the socket transport thread +nsresult HttpConnectionUDP::RecvData() { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + // if the transaction was dropped... + if (!mHttp3Session) { + LOG((" no Http3Session; ignoring event\n")); + return NS_OK; + } + + nsresult rv = mHttp3Session->RecvData(mSocket); + LOG(("HttpConnectionUDP::OnInputReady %p rv=%" PRIx32, this, + static_cast(rv))); + + if (NS_FAILED(rv)) CloseTransaction(mHttp3Session, rv); + + return NS_OK; +} + +// called on the socket transport thread +nsresult HttpConnectionUDP::SendData() { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + // if the transaction was dropped... + if (!mHttp3Session) { + LOG((" no Http3Session; ignoring event\n")); + return NS_OK; + } + + nsresult rv = mHttp3Session->SendData(mSocket); + LOG(("HttpConnectionUDP::OnInputReady %p rv=%" PRIx32, this, + static_cast(rv))); + + if (NS_FAILED(rv)) CloseTransaction(mHttp3Session, rv); + + 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; } + +int64_t HttpConnectionUDP::BytesWritten() { + if (!mHttp3Session) { + return 0; + } + return mHttp3Session->GetBytesWritten(); +} + +NS_IMETHODIMP HttpConnectionUDP::OnPacketReceived(nsIUDPSocket* aSocket) { + RecvData(); + return NS_OK; +} + +NS_IMETHODIMP HttpConnectionUDP::OnStopListening(nsIUDPSocket* aSocket, + nsresult aStatus) { + CloseTransaction(mHttp3Session, aStatus); + return NS_OK; +} + +nsresult HttpConnectionUDP::GetSelfAddr(NetAddr* addr) { + if (mSelfAddr) { + return mSelfAddr->GetNetAddr(addr); + } + return NS_ERROR_FAILURE; +} + +nsresult HttpConnectionUDP::GetPeerAddr(NetAddr* addr) { + if (mPeerAddr) { + return mPeerAddr->GetNetAddr(addr); + } + return NS_ERROR_FAILURE; +} + +bool HttpConnectionUDP::ResolvedByTRR() { return mResolvedByTRR; } + +nsIRequest::TRRMode HttpConnectionUDP::EffectiveTRRMode() { + return mEffectiveTRRMode; +} + +TRRSkippedReason HttpConnectionUDP::TRRSkipReason() { return mTRRSkipReason; } + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/HttpConnectionUDP.h b/netwerk/protocol/http/HttpConnectionUDP.h new file mode 100644 index 0000000000..e5cae24586 --- /dev/null +++ b/netwerk/protocol/http/HttpConnectionUDP.h @@ -0,0 +1,138 @@ +/* -*- 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 "mozilla/Mutex.h" +#include "ARefBase.h" +#include "TimingStruct.h" +#include "HttpTrafficAnalyzer.h" + +#include "nsIAsyncInputStream.h" +#include "nsIAsyncOutputStream.h" +#include "nsISupportsPriority.h" +#include "nsIInterfaceRequestor.h" +#include "nsITimer.h" +#include "Http3Session.h" + +class nsIDNSRecord; +class nsISocketTransport; +class nsITLSSocketControl; + +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 nsIUDPSocketSyncListener, + public nsIInterfaceRequestor { + private: + virtual ~HttpConnectionUDP(); + + public: + NS_DECLARE_STATIC_IID_ACCESSOR(HTTPCONNECTIONUDP_IID) + NS_DECL_HTTPCONNECTIONBASE + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIUDPSOCKETSYNCLISTENER + NS_DECL_NSIINTERFACEREQUESTOR + + HttpConnectionUDP(); + + [[nodiscard]] nsresult Init(nsHttpConnectionInfo* info, + nsIDNSRecord* dnsRecord, nsresult status, + nsIInterfaceRequestor* callbacks, uint32_t caps); + + friend class HttpConnectionUDPForceIO; + + [[nodiscard]] static nsresult ReadFromStream(nsIInputStream*, void*, + const char*, uint32_t, uint32_t, + uint32_t*); + + bool UsingHttp3() override { return true; } + + void OnQuicTimeoutExpired(); + + int64_t BytesWritten() override; + + nsresult GetSelfAddr(NetAddr* addr) override; + nsresult GetPeerAddr(NetAddr* addr) override; + bool ResolvedByTRR() override; + nsIRequest::TRRMode EffectiveTRRMode() override; + TRRSkippedReason TRRSkipReason() override; + bool GetEchConfigUsed() override { return false; } + + void NotifyDataRead(); + void NotifyDataWrite(); + + private: + [[nodiscard]] nsresult OnTransactionDone(nsresult reason); + nsresult RecvData(); + nsresult SendData(); + + private: + RefPtr mHttpHandler; // keep gHttpHandler alive + + RefPtr mInputOverflow; + + bool mConnectedTransport = false; + bool mDontReuse = false; + bool mIsReused = false; + bool mLastTransactionExpectedNoContent = false; + + int32_t mPriority = nsISupportsPriority::PRIORITY_NORMAL; + + private: + // For ForceSend() + static void ForceSendIO(nsITimer* aTimer, void* aClosure); + [[nodiscard]] nsresult MaybeForceSendIO(); + bool mForceSendPending = false; + nsCOMPtr mForceSendTimer; + + PRIntervalTime mLastRequestBytesSentTime = 0; + nsCOMPtr mSocket; + + nsCOMPtr mSelfAddr; + nsCOMPtr mPeerAddr; + bool mResolvedByTRR = false; + nsIRequest::TRRMode mEffectiveTRRMode = nsIRequest::TRR_DEFAULT_MODE; + TRRSkippedReason mTRRSkipReason = nsITRRSkipReason::TRR_UNSET; + + private: + // Http3 + RefPtr mHttp3Session; + nsCString mAlpnToken; +}; + +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..58de97590b --- /dev/null +++ b/netwerk/protocol/http/HttpLog.h @@ -0,0 +1,73 @@ +/* -*- 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 { +Maybe CallingScriptLocationString(); +void LogCallingScriptLocation(void* instance); +void LogCallingScriptLocation(void* instance, + const Maybe& aLogLocation); +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..5b8f3bbb21 --- /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 constexpr nsLiteralCString 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..c4294cb0b4 --- /dev/null +++ b/netwerk/protocol/http/HttpTransactionChild.cpp @@ -0,0 +1,665 @@ +/* -*- 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/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" +#include "OpaqueResponseUtils.h" + +namespace mozilla::net { + +NS_IMPL_ISUPPORTS(HttpTransactionChild, nsIRequestObserver, nsIStreamListener, + nsITransportEventSink, nsIThrottledInputChannel, + nsIThreadRetargetableStreamListener, nsIEarlyHintObserver); + +//----------------------------------------------------------------------------- +// HttpTransactionChild +//----------------------------------------------------------------------------- + +HttpTransactionChild::HttpTransactionChild() { + 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 browserId, uint8_t httpTrafficCategory, uint64_t requestContextID, + ClassOfService 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().transWithPushedStream().AsChild().get()); + transWithPushedStream = transChild->GetHttpTransaction(); + pushedStreamId = aPushedStreamArg.ref().pushedStreamId(); + } + + nsresult rv = mTransaction->Init( + caps, cinfo, requestHead, requestBody, requestContentLength, + requestBodyHasHeaders, GetCurrentSerialEventTarget(), + nullptr, // TODO: security callback, fix in bug 1512479. + this, browserId, 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 ClassOfService& 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, + TimeStamp::Now()); + }; + + LOG((" ODA to parent process")); + if (!nsHttp::SendDataInChunks(data, aOffset, aCount, sendFunc)) { + return NS_ERROR_FAILURE; + } + return NS_OK; + } + + 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, TimeStamp::Now()); + }; + + 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, so 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, + TimeStamp::Now()); + }; + + 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; + args.transactionPending() = aTiming.transactionPending; + 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(); + + nsCOMPtr securityInfo(mTransaction->SecurityInfo()); + if (securityInfo) { + nsAutoCString protocol; + if (NS_SUCCEEDED(securityInfo->GetNegotiatedNPN(protocol)) && + !protocol.IsEmpty()) { + mProtocolVersion.Assign(protocol); + } + } + + 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 (GetOpaqueResponseBlockedReason(*head) == + OpaqueResponseBlockedReason::BLOCKED_SHOULD_SNIFF) { + 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(); + + nsIRequest::TRRMode mode = nsIRequest::TRR_DEFAULT_MODE; + TRRSkippedReason reason = nsITRRSkipReason::TRR_UNSET; + { + NetAddr selfAddr; + NetAddr peerAddr; + bool isTrr = false; + bool echConfigUsed = false; + if (mTransaction) { + mTransaction->GetNetworkAddresses(selfAddr, peerAddr, isTrr, mode, reason, + echConfigUsed); + } + } + + Unused << SendOnStartRequest( + status, optionalHead, securityInfo, mTransaction->ProxyConnectFailed(), + ToTimingStructArgs(mTransaction->Timings()), proxyConnectResponseCode, + dataForSniffer, optionalAltSvcUsed, !!mDataBridgeParent, + mTransaction->TakeRestartedState(), mTransaction->HTTPSSVCReceivedStage(), + mTransaction->GetSupportsHTTP3(), mode, reason, mTransaction->Caps(), + TimeStamp::Now()); + 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.transferSize() = mTransaction->GetTransferSize(); + args.transactionPending() = mTransaction->GetPendingTime(); + 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(), + TimeStamp::Now()); + 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(), + TimeStamp::Now()); + 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, infoArgs, TimeStamp::Now()); + + 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; + nsIRequest::TRRMode mode = nsIRequest::TRR_DEFAULT_MODE; + TRRSkippedReason reason = nsITRRSkipReason::TRR_UNSET; + if (mTransaction) { + mTransaction->GetNetworkAddresses(selfAddr, peerAddr, isTrr, mode, reason, + echConfigUsed); + } else { + nsCOMPtr socketTransport = + do_QueryInterface(aTransport); + if (socketTransport) { + socketTransport->GetSelfAddr(&selfAddr); + socketTransport->GetPeerAddr(&peerAddr); + socketTransport->ResolvedByTRR(&isTrr); + socketTransport->GetEffectiveTRRMode(&mode); + socketTransport->GetTrrSkipReason(&reason); + socketTransport->GetEchConfigUsed(&echConfigUsed); + } + } + arg.emplace(selfAddr, peerAddr, isTrr, mode, reason, 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; +} + +NS_IMETHODIMP +HttpTransactionChild::OnDataFinished(nsresult aStatus) { return NS_OK; } + +NS_IMETHODIMP +HttpTransactionChild::EarlyHint(const nsACString& aValue, + const nsACString& aReferrerPolicy, + const nsACString& aCSPHeader) { + LOG(("HttpTransactionChild::EarlyHint")); + if (CanSend()) { + Unused << SendEarlyHint(aValue, aReferrerPolicy, aCSPHeader); + } + return NS_OK; +} + +} // namespace mozilla::net diff --git a/netwerk/protocol/http/HttpTransactionChild.h b/netwerk/protocol/http/HttpTransactionChild.h new file mode 100644 index 0000000000..7dcb4d4c63 --- /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 "nsIEarlyHintObserver.h" +#include "nsIRequest.h" +#include "nsIStreamListener.h" +#include "nsIThreadRetargetableStreamListener.h" +#include "nsIThrottledInputChannel.h" +#include "nsITransport.h" + +class nsInputStreamPump; + +namespace mozilla::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 nsITransportEventSink, + public nsIThrottledInputChannel, + public nsIThreadRetargetableStreamListener, + public nsIEarlyHintObserver { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMLISTENER + NS_DECL_NSITRANSPORTEVENTSINK + NS_DECL_NSITHROTTLEDINPUTCHANNEL + NS_DECL_NSITHREADRETARGETABLESTREAMLISTENER + NS_DECL_NSIEARLYHINTOBSERVER + + 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 ClassOfService& 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& infoArgs, + nsHttpRequestHead* requestHead, + nsIInputStream* requestBody, // use the trick in bug 1277681 + uint64_t requestContentLength, bool requestBodyHasHeaders, + uint64_t topLevelOuterContentWindowId, uint8_t httpTrafficCategory, + uint64_t requestContextID, ClassOfService 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{false}; + Atomic mStatus{NS_OK}; + uint64_t mChannelId{0}; + nsHttpRequestHead mRequestHead; + bool mIsDocumentLoad{false}; + uint64_t mLogicalOffset{0}; + TimeStamp mRedirectStart; + TimeStamp mRedirectEnd; + nsCString mProtocolVersion; + + nsCOMPtr mUploadStream; + RefPtr mTransaction; + nsCOMPtr mTransactionPump; + Maybe mTransactionObserverResult; + RefPtr mThrottleQueue; + RefPtr mDataBridgeParent; +}; + +} // namespace mozilla::net + +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..5408aee641 --- /dev/null +++ b/netwerk/protocol/http/HttpTransactionParent.cpp @@ -0,0 +1,924 @@ +/* -*- 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/ChannelEventQueue.h" +#include "mozilla/net/InputChannelThrottleQueueParent.h" +#include "mozilla/net/SocketProcessParent.h" +#include "nsHttpHandler.h" +#include "nsIThreadRetargetableStreamListener.h" +#include "nsITransportSecurityInfo.h" +#include "nsNetUtil.h" +#include "nsQueryObject.h" +#include "nsSerializationHelper.h" +#include "nsStreamUtils.h" +#include "nsStringStream.h" + +namespace mozilla::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"); + nsrefcnt count = --mRefCnt; + NS_LOG_RELEASE(this, count, "HttpTransactionParent"); + if (count == 0) { + 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) + : mIsDocumentLoad(aIsDocumentLoad) { + LOG(("Creating HttpTransactionParent @%p\n", this)); + mEventQ = new ChannelEventQueue(static_cast(this)); +} + +HttpTransactionParent::~HttpTransactionParent() { + LOG(("Destroying HttpTransactionParent @%p\n", this)); + mEventQ->NotifyReleasingOwner(); +} + +//----------------------------------------------------------------------------- +// 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 browserId, HttpTrafficCategory trafficCategory, + nsIRequestContext* requestContext, ClassOfService 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 = GetCurrentSerialEventTarget(); + mChannelId = channelId; + mTransactionObserver = std::move(transactionObserver); + mOnPushCallback = std::move(aOnPushCallback); + mCaps = caps; + mConnInfo = cinfo->Clone(); + mIsHttp3Used = cinfo->IsHttp3(); + + HttpConnectionInfoCloneArgs infoArgs; + nsHttpConnectionInfo::SerializeHttpConnectionInfo(cinfo, infoArgs); + + Maybe ipcStream; + if (!mozilla::ipc::SerializeIPCStream(do_AddRef(requestBody), ipcStream, + /* aAllowLazy */ false)) { + return NS_ERROR_FAILURE; + } + + uint64_t requestContextID = requestContext ? requestContext->GetID() : 0; + + Maybe pushedStreamArg; + if (aTransWithPushedStream && aPushedStreamId) { + MOZ_ASSERT(aTransWithPushedStream->AsHttpTransactionParent()); + pushedStreamArg.emplace( + WrapNotNull(aTransWithPushedStream->AsHttpTransactionParent()), + 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(WrapNotNull(tqParent.get())); + } + } + + // TODO: Figure out if we have to implement nsIThreadRetargetableRequest in + // bug 1544378. + if (!SendInit(caps, infoArgs, *requestHead, ipcStream, requestContentLength, + requestBodyHasHeaders, browserId, + 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(nsISerialEventTarget** 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 = GetMainThreadSerialEventTarget(); + } + return target.forget(); +} + +NS_IMETHODIMP HttpTransactionParent::RetargetDeliveryTo( + nsISerialEventTarget* 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, + nsIRequest::TRRMode& aEffectiveTRRMode, TRRSkippedReason& aSkipReason, + bool& aEchConfigUsed) { + self = mSelfAddr; + peer = mPeerAddr; + aResolvedByTRR = mResolvedByTRR; + aEffectiveTRRMode = mEffectiveTRRMode; + aSkipReason = mTRRSkipReason; + 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; +} + +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 = GetMainThreadSerialEventTarget(); + return target.forget(); +} + +mozilla::ipc::IPCResult HttpTransactionParent::RecvOnStartRequest( + const nsresult& aStatus, const Maybe& aResponseHead, + nsITransportSecurityInfo* aSecurityInfo, 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, + const nsIRequest::TRRMode& aMode, const TRRSkippedReason& aTrrSkipReason, + const uint32_t& aCaps, const TimeStamp& aOnStartRequestStartTime) { + mEventQ->RunOrEnqueue(new NeckoTargetChannelFunctionEvent( + this, [self = UnsafePtr(this), aStatus, + aResponseHead, securityInfo = nsCOMPtr{aSecurityInfo}, + aProxyConnectFailed, aTimings, aProxyConnectResponseCode, + aDataForSniffer = CopyableTArray{std::move(aDataForSniffer)}, + aAltSvcUsed, aDataToChildProcess, aRestarted, + aHTTPSSVCReceivedStage, aSupportsHttp3, aMode, aTrrSkipReason, + aCaps, aOnStartRequestStartTime]() mutable { + self->DoOnStartRequest( + aStatus, aResponseHead, securityInfo, aProxyConnectFailed, aTimings, + aProxyConnectResponseCode, std::move(aDataForSniffer), aAltSvcUsed, + aDataToChildProcess, aRestarted, aHTTPSSVCReceivedStage, + aSupportsHttp3, aMode, aTrrSkipReason, aCaps, + aOnStartRequestStartTime); + })); + 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(); + aTimings.transactionPending = aArgs.transactionPending(); +} + +void HttpTransactionParent::DoOnStartRequest( + const nsresult& aStatus, const Maybe& aResponseHead, + nsITransportSecurityInfo* aSecurityInfo, 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, + const nsIRequest::TRRMode& aMode, const TRRSkippedReason& aSkipReason, + const uint32_t& aCaps, const TimeStamp& aOnStartRequestStartTime) { + 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; + mEffectiveTRRMode = aMode; + mTRRSkipReason = aSkipReason; + mCaps = aCaps; + mSecurityInfo = aSecurityInfo; + mOnStartRequestStartTime = aOnStartRequestStartTime; + + 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( + nsHttp::Alternate_Service_Used.val(), 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(); + mEffectiveTRRMode = aNetworkAddressArg->mode(); + mTRRSkipReason = aNetworkAddressArg->trrSkipReason(); + 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, + const TimeStamp& aOnDataAvailableStartTime) { + 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, + aOnDataAvailableStartTime]() { + self->DoOnDataAvailable(aData, aOffset, aCount, + aOnDataAvailableStartTime); + })); + return IPC_OK(); +} + +void HttpTransactionParent::DoOnDataAvailable( + const nsCString& aData, const uint64_t& aOffset, const uint32_t& aCount, + const TimeStamp& aOnDataAvailableStartTime) { + 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; + } + + mOnDataAvailableStartTime = aOnDataAvailableStartTime; + 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 HttpConnectionInfoCloneArgs& aArgs, + const TimeStamp& aOnStopRequestStartTime) { + 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)}, + cinfo{std::move(cinfo)}, aOnStopRequestStartTime]() mutable { + self->DoOnStopRequest(aStatus, aResponseIsComplete, aTransferSize, + aTimings, aResponseTrailers, + std::move(aTransactionObserverResult), cinfo, + aOnStopRequestStartTime); + })); + return IPC_OK(); +} + +void HttpTransactionParent::DoOnStopRequest( + const nsresult& aStatus, const bool& aResponseIsComplete, + const int64_t& aTransferSize, const TimingStructArgs& aTimings, + const Maybe& aResponseTrailers, + Maybe&& aTransactionObserverResult, + nsHttpConnectionInfo* aConnInfo, const TimeStamp& aOnStopRequestStartTime) { + 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; + mOnStopRequestStartTime = aOnStopRequestStartTime; + + TimingStructArgsToTimingsStruct(aTimings, mTimings); + + if (aResponseTrailers.isSome()) { + mResponseTrailers = MakeUnique(aResponseTrailers.ref()); + } + 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 + +mozilla::ipc::IPCResult HttpTransactionParent::RecvEarlyHint( + const nsCString& aValue, const nsACString& aReferrerPolicy, + const nsACString& aCSPHeader) { + LOG( + ("HttpTransactionParent::RecvEarlyHint header=%s aReferrerPolicy=%s " + "aCSPHeader=%s", + PromiseFlatCString(aValue).get(), + PromiseFlatCString(aReferrerPolicy).get(), + PromiseFlatCString(aCSPHeader).get())); + nsCOMPtr obs = do_QueryInterface(mChannel); + if (obs) { + Unused << obs->EarlyHint(aValue, aReferrerPolicy, aCSPHeader); + } + + return IPC_OK(); +} + +//----------------------------------------------------------------------------- +// 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::SetCanceledReason( + const nsACString& aReason) { + return SetCanceledReasonImpl(aReason); +} + +NS_IMETHODIMP HttpTransactionParent::GetCanceledReason(nsACString& aReason) { + return GetCanceledReasonImpl(aReason); +} + +NS_IMETHODIMP HttpTransactionParent::CancelWithReason( + nsresult aStatus, const nsACString& aReason) { + return CancelWithReasonImpl(aStatus, aReason); +} + +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; } + +void HttpTransactionParent::SetIsForWebTransport(bool SetIsForWebTransport) { + // TODO: bug 1791727 +} + +mozilla::TimeStamp HttpTransactionParent::GetPendingTime() { + return mTimings.transactionPending; +} + +} // namespace mozilla::net diff --git a/netwerk/protocol/http/HttpTransactionParent.h b/netwerk/protocol/http/HttpTransactionParent.h new file mode 100644 index 0000000000..e6e74ebf97 --- /dev/null +++ b/netwerk/protocol/http/HttpTransactionParent.h @@ -0,0 +1,197 @@ +/* -*- 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 "nsCOMPtr.h" +#include "nsHttp.h" +#include "nsIRequest.h" +#include "nsIThreadRetargetableRequest.h" +#include "nsITransport.h" +#include "nsITransportSecurityInfo.h" + +namespace mozilla::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, + nsITransportSecurityInfo* aSecurityInfo, 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, + const nsIRequest::TRRMode& aMode, const TRRSkippedReason& aSkipReason, + const uint32_t& aCaps, const TimeStamp& aOnStartRequestStartTime); + 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, + const TimeStamp& aOnDataAvailableStartTime); + 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 HttpConnectionInfoCloneArgs& aArgs, + const TimeStamp& aOnStopRequestStartTime); + mozilla::ipc::IPCResult RecvOnInitFailed(const nsresult& aStatus); + + mozilla::ipc::IPCResult RecvOnH2PushStream(const uint32_t& aPushedStreamId, + const nsCString& aResourceUrl, + const nsCString& aRequestString); + mozilla::ipc::IPCResult RecvEarlyHint(const nsCString& aValue, + const nsACString& aReferrerPolicy, + const nsACString& aCSPHeader); + + virtual mozilla::TimeStamp GetPendingTime() override; + + already_AddRefed GetNeckoTarget(); + + void SetSniffedTypeToChannel( + nsInputStreamPump::PeekSegmentFun aCallTypeSniffers, + nsIChannel* aChannel); + + void SetRedirectTimestamp(TimeStamp aRedirectStart, TimeStamp aRedirectEnd) { + mRedirectStart = aRedirectStart; + mRedirectEnd = aRedirectEnd; + } + + virtual TimeStamp GetOnStartRequestStartTime() const override { + return mOnStartRequestStartTime; + } + virtual TimeStamp GetDataAvailableStartTime() const override { + return mOnDataAvailableStartTime; + } + virtual TimeStamp GetOnStopRequestStartTime() const override { + return mOnStopRequestStartTime; + } + + private: + virtual ~HttpTransactionParent(); + + void GetStructFromInfo(nsHttpConnectionInfo* aInfo, + HttpConnectionInfoCloneArgs& aArgs); + void DoOnStartRequest( + const nsresult& aStatus, const Maybe& aResponseHead, + nsITransportSecurityInfo* aSecurityInfo, 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, + const nsIRequest::TRRMode& aMode, const TRRSkippedReason& aSkipReason, + const uint32_t& aCaps, const TimeStamp& aOnStartRequestStartTime); + void DoOnDataAvailable(const nsCString& aData, const uint64_t& aOffset, + const uint32_t& aCount, + const TimeStamp& aOnDataAvailableStartTime); + void DoOnStopRequest( + const nsresult& aStatus, const bool& aResponseIsComplete, + const int64_t& aTransferSize, const TimingStructArgs& aTimings, + const Maybe& responseTrailers, + Maybe&& aTransactionObserverResult, + nsHttpConnectionInfo* aConnInfo, + const TimeStamp& aOnStopRequestStartTime); + 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 MOZ_UNANNOTATED{ + "HttpTransactionParent::EventTargetMutex"}; + nsCOMPtr mSecurityInfo; + UniquePtr mResponseHead; + UniquePtr mResponseTrailers; + RefPtr mEventQ; + + bool mResponseIsComplete{false}; + int64_t mTransferSize{0}; + int64_t mRequestSize{0}; + bool mIsHttp3Used = false; + bool mProxyConnectFailed{false}; + Atomic mCanceled{false}; + Atomic mStatus{NS_OK}; + int32_t mSuspendCount{0}; + bool mResponseHeadTaken{false}; + bool mResponseTrailersTaken{false}; + bool mOnStartRequestCalled{false}; + bool mOnStopRequestCalled{false}; + bool mResolvedByTRR{false}; + nsIRequest::TRRMode mEffectiveTRRMode{nsIRequest::TRR_DEFAULT_MODE}; + TRRSkippedReason mTRRSkipReason{nsITRRSkipReason::TRR_UNSET}; + bool mEchConfigUsed = false; + int32_t mProxyConnectResponseCode{0}; + uint64_t mChannelId{0}; + bool mDataSentToChildProcess{false}; + bool mIsDocumentLoad; + bool mRestarted{false}; + Atomic mCaps{0}; + TimeStamp mRedirectStart; + TimeStamp mRedirectEnd; + + NetAddr mSelfAddr; + NetAddr mPeerAddr; + TimingStruct mTimings; + TimeStamp mDomainLookupStart; + TimeStamp mDomainLookupEnd; + TimeStamp mOnStartRequestStartTime; + TimeStamp mOnDataAvailableStartTime; + TimeStamp mOnStopRequestStartTime; + 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 mozilla::net + +#endif // nsHttpTransactionParent_h__ diff --git a/netwerk/protocol/http/HttpTransactionShell.h b/netwerk/protocol/http/HttpTransactionShell.h new file mode 100644 index 0000000000..495ab19a1b --- /dev/null +++ b/netwerk/protocol/http/HttpTransactionShell.h @@ -0,0 +1,242 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 "TimingStruct.h" +#include "mozilla/Maybe.h" +#include "mozilla/UniquePtr.h" +#include "nsIClassOfService.h" +#include "nsIEarlyHintObserver.h" +#include "nsISupports.h" +#include "nsITransportSecurityInfo.h" +#include "nsInputStreamPump.h" + +class nsIEventTraget; +class nsIInputStream; +class nsIInterfaceRequestor; +class nsIRequest; +class nsIRequestContext; +class nsITransportEventSink; + +namespace mozilla::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 browserId + // indicate the id of the browser 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 browserId, + HttpTrafficCategory trafficCategory, nsIRequestContext* requestContext, + ClassOfService 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, + nsIRequest::TRRMode& aEffectiveTRRMode, + TRRSkippedReason& aSkipReason, + 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 TimingStruct Timings() = 0; + + virtual mozilla::TimeStamp GetPendingTime() = 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; + + virtual void SetIsForWebTransport(bool aIsForWebTransport) = 0; + + virtual TimeStamp GetOnStartRequestStartTime() const { return TimeStamp(); } + virtual TimeStamp GetDataAvailableStartTime() const { return TimeStamp(); } + virtual TimeStamp GetOnStopRequestStartTime() const { return TimeStamp(); } +}; + +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 browserId, \ + HttpTrafficCategory trafficCategory, nsIRequestContext* requestContext, \ + ClassOfService 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, \ + nsIRequest::TRRMode& aEffectiveTRRMode, TRRSkippedReason& aSkipReason, \ + 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 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; \ + virtual void SetIsForWebTransport(bool aIsForWebTransport) override; + +} // namespace mozilla::net + +#endif // HttpTransactionShell_h__ diff --git a/netwerk/protocol/http/HttpWinUtils.cpp b/netwerk/protocol/http/HttpWinUtils.cpp new file mode 100644 index 0000000000..b566cdc4f6 --- /dev/null +++ b/netwerk/protocol/http/HttpWinUtils.cpp @@ -0,0 +1,111 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 "HttpWinUtils.h" +#include "nsIURI.h" +#include "nsHttpChannel.h" +#include "mozilla/ClearOnShutdown.h" +#include + +namespace mozilla { +namespace net { + +static StaticRefPtr sPopCookieManager; +static bool sPopCookieManagerAvailable = true; + +void AddWindowsSSO(nsHttpChannel* channel) { + if (!sPopCookieManagerAvailable) { + return; + } + HRESULT hr; + if (!sPopCookieManager) { + GUID CLSID_ProofOfPossessionCookieInfoManager; + GUID IID_IProofOfPossessionCookieInfoManager; + + CLSIDFromString(L"{A9927F85-A304-4390-8B23-A75F1C668600}", + &CLSID_ProofOfPossessionCookieInfoManager); + IIDFromString(L"{CDAECE56-4EDF-43DF-B113-88E4556FA1BB}", + &IID_IProofOfPossessionCookieInfoManager); + + hr = CoCreateInstance(CLSID_ProofOfPossessionCookieInfoManager, NULL, + CLSCTX_INPROC_SERVER, + IID_IProofOfPossessionCookieInfoManager, + reinterpret_cast(&sPopCookieManager)); + if (FAILED(hr)) { + sPopCookieManagerAvailable = false; + return; + } + + RunOnShutdown([&] { + if (sPopCookieManager) { + sPopCookieManager = nullptr; + } + }); + } + + DWORD cookieCount = 0; + ProofOfPossessionCookieInfo* cookieInfo = nullptr; + + nsCOMPtr uri; + channel->GetURI(getter_AddRefs(uri)); + + nsAutoCString urispec; + uri->GetSpec(urispec); + + hr = sPopCookieManager->GetCookieInfoForUri( + NS_ConvertUTF8toUTF16(urispec).get(), &cookieCount, &cookieInfo); + if (FAILED(hr)) { + return; + } + + nsAutoCString host; + uri->GetHost(host); + bool addCookies = false; + if (StringEndsWith(host, ".live.com"_ns)) { + addCookies = true; + } + + nsAutoString allCookies; + + for (DWORD i = 0; i < cookieCount; i++) { + nsAutoString cookieData; + cookieData.Assign(cookieInfo[i].data); + // Strip old Set-Cookie info for WinInet + int32_t semicolon = cookieData.FindChar(';'); + if (semicolon >= 0) { + cookieData.SetLength(semicolon); + } + if (StringBeginsWith(nsDependentString(cookieInfo[i].name), u"x-ms-"_ns)) { + channel->SetRequestHeader(NS_ConvertUTF16toUTF8(cookieInfo[i].name), + NS_ConvertUTF16toUTF8(cookieData), + true /* merge */); + } else if (addCookies) { + if (!allCookies.IsEmpty()) { + allCookies.AppendLiteral("; "); + } + allCookies.Append(cookieInfo[i].name); + allCookies.AppendLiteral("="); + allCookies.Append(cookieData); + } + } + + // Merging cookie headers doesn't work correctly as it separates the new + // cookies using commas instead of semicolons, so we have to replace + // the entire header. + if (!allCookies.IsEmpty()) { + nsAutoCString cookieHeader; + channel->GetRequestHeader(nsHttp::Cookie.val(), cookieHeader); + if (!cookieHeader.IsEmpty()) { + cookieHeader.AppendLiteral("; "); + } + cookieHeader.Append(NS_ConvertUTF16toUTF8(allCookies)); + channel->SetRequestHeader(nsHttp::Cookie.val(), cookieHeader, false); + } + if (cookieInfo) { + FreeProofOfPossessionCookieInfoArray(cookieInfo, cookieCount); + } +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/HttpWinUtils.h b/netwerk/protocol/http/HttpWinUtils.h new file mode 100644 index 0000000000..7d78f7188b --- /dev/null +++ b/netwerk/protocol/http/HttpWinUtils.h @@ -0,0 +1,18 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 HttpWinUtils_h__ +#define HttpWinUtils_h__ + +namespace mozilla { +namespace net { + +class nsHttpChannel; + +void AddWindowsSSO(nsHttpChannel* channel); + +} // namespace net +} // namespace mozilla + +#endif // HttpWinUtils_h__ diff --git a/netwerk/protocol/http/InterceptedHttpChannel.cpp b/netwerk/protocol/http/InterceptedHttpChannel.cpp new file mode 100644 index 0000000000..c58fbc9639 --- /dev/null +++ b/netwerk/protocol/http/InterceptedHttpChannel.cpp @@ -0,0 +1,1714 @@ +/* -*- 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 "NetworkMarker.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 "nsIHttpHeaderVisitor.h" +#include "nsIRedirectResultListener.h" +#include "nsStringStream.h" +#include "nsStreamUtils.h" +#include "nsQueryObject.h" +#include "mozilla/Logging.h" + +namespace mozilla::net { + +mozilla::LazyLogModule gInterceptedLog("Intercepted"); + +#define INTERCEPTED_LOG(args) MOZ_LOG(gInterceptedLog, LogLevel::Debug, args) + +NS_IMPL_ISUPPORTS_INHERITED(InterceptedHttpChannel, HttpBaseChannel, + nsIInterceptedChannel, nsICacheInfoChannel, + nsIAsyncVerifyRedirectCallback, nsIRequestObserver, + nsIStreamListener, nsIThreadRetargetableRequest, + nsIThreadRetargetableStreamListener, + nsIClassOfService) + +InterceptedHttpChannel::InterceptedHttpChannel( + PRTime aCreationTime, const TimeStamp& aCreationTimestamp, + const TimeStamp& aAsyncOpenTimestamp) + : HttpAsyncAborter(this), + mProgress(0), + mProgressReported(0), + mSynthesizedStreamLength(-1), + mResumeStartPos(0), + 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. + INTERCEPTED_LOG(("Creating InterceptedHttpChannel [%p]", this)); + mChannelCreationTime = aCreationTime; + mChannelCreationTimestamp = aCreationTimestamp; + mInterceptedChannelCreationTimestamp = TimeStamp::Now(); + 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) { + INTERCEPTED_LOG( + ("InterceptedHttpChannel::SetupReplacementChannel [%p] flag: %u", this, + 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() { + // We save this timestamp from outside of the if block in case we enable the + // profiler after AsyncOpen(). + INTERCEPTED_LOG(("InterceptedHttpChannel::AsyncOpenInternal [%p]", this)); + mLastStatusReported = TimeStamp::Now(); + if (profiler_thread_is_being_profiled_for_markers()) { + nsAutoCString requestMethod; + GetRequestMethod(requestMethod); + + profiler_add_network_marker( + mURI, requestMethod, mPriority, mChannelId, NetworkLoadType::LOAD_START, + mChannelCreationTimestamp, mLastStatusReported, 0, kCacheUnknown, + mLoadInfo->GetInnerWindowID(), + mLoadInfo->GetOriginAttributes().mPrivateBrowsingId > 0); + } + + // 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; + + // Start the interception, record the start time. + mTimeStamps.Init(this); + mTimeStamps.RecordTime(); + + // 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); + } + }); + + // The fetch event will not be dispatched, record current time for + // FetchHandlerStart and FetchHandlerFinish. + SetFetchHandlerStart(TimeStamp::Now()); + SetFetchHandlerFinish(TimeStamp::Now()); + + 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(false); + 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); + } else { + // Redirect success, record the finish time and the final status. + mTimeStamps.RecordTime(InterceptionTimeStamps::Redirected); + } + + 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, 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() { + INTERCEPTED_LOG( + ("InterceptedHttpChannel::OpenRedirectChannel [%p], mRedirectChannel: %p", + this, mRedirectChannel.get())); + 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(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::SetCanceledReason( + const nsACString& aReason) { + return SetCanceledReasonImpl(aReason); +} + +NS_IMETHODIMP InterceptedHttpChannel::GetCanceledReason(nsACString& aReason) { + return GetCanceledReasonImpl(aReason); +} + +NS_IMETHODIMP +InterceptedHttpChannel::CancelWithReason(nsresult aStatus, + const nsACString& aReason) { + return CancelWithReasonImpl(aStatus, aReason); +} + +NS_IMETHODIMP +InterceptedHttpChannel::Cancel(nsresult aStatus) { + INTERCEPTED_LOG(("InterceptedHttpChannel::Cancel [%p]", this)); + // 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; + } + + // The interception is canceled, record the finish time stamp and the final + // status + mTimeStamps.RecordTime(InterceptionTimeStamps::Canceled); + + mCanceled = true; + + if (mLastStatusReported && profiler_thread_is_being_profiled_for_markers()) { + // These do allocations/frees/etc; avoid if not active + // mLastStatusReported can be null if Cancel is called before we added the + // start marker. + nsAutoCString requestMethod; + GetRequestMethod(requestMethod); + + int32_t priority = PRIORITY_NORMAL; + GetPriority(&priority); + + uint64_t size = 0; + GetEncodedBodySize(&size); + + profiler_add_network_marker( + mURI, requestMethod, priority, mChannelId, NetworkLoadType::LOAD_CANCEL, + mLastStatusReported, TimeStamp::Now(), size, kCacheUnknown, + mLoadInfo->GetInnerWindowID(), + mLoadInfo->GetOriginAttributes().mPrivateBrowsingId > 0, + &mTransactionTimings, std::move(mSource)); + } + + 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( + nsITransportSecurityInfo** aSecurityInfo) { + nsCOMPtr ref(mSecurityInfo); + ref.forget(aSecurityInfo); + return NS_OK; +} + +NS_IMETHODIMP +InterceptedHttpChannel::AsyncOpen(nsIStreamListener* aListener) { + INTERCEPTED_LOG(("InterceptedHttpChannel::AsyncOpen [%p], listener: %p", this, + 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, + bool aIsWarning) { + // 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::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.SetFlags(aClassFlags); + return NS_OK; +} + +NS_IMETHODIMP +InterceptedHttpChannel::ClearClassFlags(uint32_t aClassFlags) { + mClassOfService.SetFlags(~aClassFlags & mClassOfService.Flags()); + return NS_OK; +} + +NS_IMETHODIMP +InterceptedHttpChannel::AddClassFlags(uint32_t aClassFlags) { + mClassOfService.SetFlags(aClassFlags | mClassOfService.Flags()); + return NS_OK; +} + +NS_IMETHODIMP +InterceptedHttpChannel::SetClassOfService(ClassOfService cos) { + mClassOfService = cos; + return NS_OK; +} + +NS_IMETHODIMP +InterceptedHttpChannel::SetIncremental(bool incremental) { + mClassOfService.SetIncremental(incremental); + 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); +} + +namespace { + +class ResetInterceptionHeaderVisitor final : public nsIHttpHeaderVisitor { + nsCOMPtr mTarget; + + ~ResetInterceptionHeaderVisitor() = default; + + NS_IMETHOD + VisitHeader(const nsACString& aHeader, const nsACString& aValue) override { + // We skip Cookie header here, since it will be added during + // nsHttpChannel::AsyncOpen. + if (aHeader.Equals(nsHttp::Cookie.val())) { + return NS_OK; + } + if (aValue.IsEmpty()) { + return mTarget->SetEmptyRequestHeader(aHeader); + } + return mTarget->SetRequestHeader(aHeader, aValue, false /* merge */); + } + + public: + explicit ResetInterceptionHeaderVisitor(nsIHttpChannel* aTarget) + : mTarget(aTarget) { + MOZ_DIAGNOSTIC_ASSERT(mTarget); + } + + NS_DECL_ISUPPORTS +}; + +NS_IMPL_ISUPPORTS(ResetInterceptionHeaderVisitor, nsIHttpHeaderVisitor) + +} // anonymous namespace + +NS_IMETHODIMP +InterceptedHttpChannel::ResetInterception(bool aBypass) { + INTERCEPTED_LOG(("InterceptedHttpChannel::ResetInterception [%p] bypass: %s", + this, aBypass ? "true" : "false")); + if (mCanceled) { + return mStatus; + } + + mInterceptionReset = true; + + uint32_t flags = nsIChannelEventSink::REDIRECT_INTERNAL; + + nsCOMPtr newChannel; + nsCOMPtr redirectLoadInfo = + CloneLoadInfoForRedirect(mURI, flags); + + if (aBypass) { + redirectLoadInfo->ClearController(); + // TODO: Audit whether we should also be calling + // ServiceWorkerManager::StopControllingClient for maximum correctness. + } + + nsresult rv = + NS_NewChannelInternal(getter_AddRefs(newChannel), mURI, redirectLoadInfo, + nullptr, // PerformanceStorage + nullptr, // aLoadGroup + nullptr, // aCallbacks + mLoadFlags); + NS_ENSURE_SUCCESS(rv, rv); + + if (profiler_thread_is_being_profiled_for_markers()) { + nsAutoCString requestMethod; + GetRequestMethod(requestMethod); + + int32_t priority = PRIORITY_NORMAL; + GetPriority(&priority); + + uint64_t size = 0; + GetEncodedBodySize(&size); + + nsAutoCString contentType; + if (mResponseHead) { + mResponseHead->ContentType(contentType); + } + + RefPtr newBaseChannel = do_QueryObject(newChannel); + MOZ_ASSERT(newBaseChannel, + "The redirect channel should be a base channel."); + profiler_add_network_marker( + mURI, requestMethod, priority, mChannelId, + NetworkLoadType::LOAD_REDIRECT, mLastStatusReported, TimeStamp::Now(), + size, kCacheUnknown, mLoadInfo->GetInnerWindowID(), + mLoadInfo->GetOriginAttributes().mPrivateBrowsingId > 0, + &mTransactionTimings, std::move(mSource), + Some(nsDependentCString(contentType.get())), mURI, flags, + newBaseChannel->ChannelId()); + } + + rv = SetupReplacementChannel(mURI, newChannel, true, flags); + NS_ENSURE_SUCCESS(rv, rv); + + // Restore the non-default headers for fallback channel. + nsCOMPtr httpChannel(do_QueryInterface(newChannel)); + nsCOMPtr visitor = + new ResetInterceptionHeaderVisitor(httpChannel); + rv = VisitNonDefaultRequestHeaders(visitor); + 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); + } else { + // ResetInterception success, record the finish time stamps and the final + // status. + mTimeStamps.RecordTime(InterceptionTimeStamps::Reset); + } + + 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); + + NS_ENSURE_SUCCESS(mSynthesizedResponseHead->ParseStatusLine(statusLine), + NS_ERROR_FAILURE); + 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; + } + + 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::SetFetchHandlerStart(TimeStamp aTimeStamp) { + mTimeStamps.RecordTime(std::move(aTimeStamp)); + return NS_OK; +} + +NS_IMETHODIMP +InterceptedHttpChannel::SetFetchHandlerFinish(TimeStamp aTimeStamp) { + mTimeStamps.RecordTime(std::move(aTimeStamp)); + return NS_OK; +} + +NS_IMETHODIMP +InterceptedHttpChannel::SetRemoteWorkerLaunchStart(TimeStamp aTimeStamp) { + mServiceWorkerLaunchStart = aTimeStamp > mTimeStamps.mInterceptionStart + ? aTimeStamp + : mTimeStamps.mInterceptionStart; + return NS_OK; +} + +NS_IMETHODIMP +InterceptedHttpChannel::SetRemoteWorkerLaunchEnd(TimeStamp aTimeStamp) { + mServiceWorkerLaunchEnd = aTimeStamp > mTimeStamps.mInterceptionStart + ? aTimeStamp + : mTimeStamps.mInterceptionStart; + return NS_OK; +} + +NS_IMETHODIMP +InterceptedHttpChannel::SetLaunchServiceWorkerStart(TimeStamp aTimeStamp) { + mServiceWorkerLaunchStart = aTimeStamp; + return NS_OK; +} + +NS_IMETHODIMP +InterceptedHttpChannel::GetLaunchServiceWorkerStart(TimeStamp* aRetVal) { + MOZ_ASSERT(aRetVal); + *aRetVal = mServiceWorkerLaunchStart; + return NS_OK; +} + +NS_IMETHODIMP +InterceptedHttpChannel::SetLaunchServiceWorkerEnd(TimeStamp aTimeStamp) { + mServiceWorkerLaunchEnd = aTimeStamp; + return NS_OK; +} + +NS_IMETHODIMP +InterceptedHttpChannel::GetLaunchServiceWorkerEnd(TimeStamp* aRetVal) { + MOZ_ASSERT(aRetVal); + *aRetVal = mServiceWorkerLaunchEnd; + return NS_OK; +} + +NS_IMETHODIMP +InterceptedHttpChannel::GetDispatchFetchEventStart(TimeStamp* aRetVal) { + MOZ_ASSERT(aRetVal); + *aRetVal = mTimeStamps.mInterceptionStart; + return NS_OK; +} + +NS_IMETHODIMP +InterceptedHttpChannel::GetDispatchFetchEventEnd(TimeStamp* aRetVal) { + MOZ_ASSERT(aRetVal); + *aRetVal = mTimeStamps.mFetchHandlerStart; + return NS_OK; +} + +NS_IMETHODIMP +InterceptedHttpChannel::GetHandleFetchEventStart(TimeStamp* aRetVal) { + MOZ_ASSERT(aRetVal); + *aRetVal = mTimeStamps.mFetchHandlerStart; + return NS_OK; +} + +NS_IMETHODIMP +InterceptedHttpChannel::GetHandleFetchEventEnd(TimeStamp* aRetVal) { + MOZ_ASSERT(aRetVal); + *aRetVal = mTimeStamps.mFetchHandlerFinish; + return NS_OK; +} + +NS_IMETHODIMP +InterceptedHttpChannel::GetIsReset(bool* aResult) { + *aResult = mInterceptionReset; + 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(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) { + INTERCEPTED_LOG(("InterceptedHttpChannel::OnStartRequest [%p]", this)); + MOZ_ASSERT(NS_IsMainThread()); + + if (!mProgressSink) { + GetCallback(mProgressSink); + } + + MOZ_ASSERT_IF(!mLoadInfo->GetServiceWorkerTaintingSynthesized(), + mLoadInfo->GetLoadingPrincipal()); + // No need to do ORB checks if these conditions hold. + MOZ_DIAGNOSTIC_ASSERT(mLoadInfo->GetServiceWorkerTaintingSynthesized() || + mLoadInfo->GetLoadingPrincipal()->IsSystemPrincipal()); + + 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) { + INTERCEPTED_LOG(("InterceptedHttpChannel::OnStopRequest [%p]", this)); + MOZ_ASSERT(NS_IsMainThread()); + + if (NS_SUCCEEDED(mStatus)) { + mStatus = aStatus; + } + + MaybeCallBodyCallback(); + + mTimeStamps.RecordTime(InterceptionTimeStamps::Synthesized); + + // 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(); + + if (profiler_thread_is_being_profiled_for_markers()) { + // These do allocations/frees/etc; avoid if not active + nsAutoCString requestMethod; + GetRequestMethod(requestMethod); + + int32_t priority = PRIORITY_NORMAL; + GetPriority(&priority); + + uint64_t size = 0; + GetEncodedBodySize(&size); + + nsAutoCString contentType; + if (mResponseHead) { + mResponseHead->ContentType(contentType); + } + profiler_add_network_marker( + mURI, requestMethod, priority, mChannelId, NetworkLoadType::LOAD_STOP, + mLastStatusReported, TimeStamp::Now(), size, kCacheUnknown, + mLoadInfo->GetInnerWindowID(), + mLoadInfo->GetOriginAttributes().mPrivateBrowsingId > 0, + &mTransactionTimings, std::move(mSource), + Some(nsDependentCString(contentType.get()))); + } + + 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::OnDataFinished(nsresult aStatus) { + if (mCanceled || !mListener) { + return aStatus; + } + nsCOMPtr retargetableListener = + do_QueryInterface(mListener); + if (retargetableListener) { + return retargetableListener->OnDataFinished(aStatus); + } + + return NS_OK; +} + +NS_IMETHODIMP +InterceptedHttpChannel::RetargetDeliveryTo(nsISerialEventTarget* 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(nsISerialEventTarget** 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(uint32_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::SetForceValidateCacheContent( + bool aForceValidateCacheContent) { + // We store aForceValidateCacheContent locally because + // mSynthesizedCacheInfo isn't present until a response + // is actually synthesized, which is too late for the value + // to be forwarded during the redirect to the intercepted + // channel. + StoreForceValidateCacheContent(aForceValidateCacheContent); + + if (mSynthesizedCacheInfo) { + return mSynthesizedCacheInfo->SetForceValidateCacheContent( + aForceValidateCacheContent); + } + return NS_OK; +} +NS_IMETHODIMP +InterceptedHttpChannel::GetForceValidateCacheContent( + bool* aForceValidateCacheContent) { + *aForceValidateCacheContent = LoadForceValidateCacheContent(); +#ifdef DEBUG + if (mSynthesizedCacheInfo) { + bool synthesizedForceValidateCacheContent; + mSynthesizedCacheInfo->GetForceValidateCacheContent( + &synthesizedForceValidateCacheContent); + MOZ_ASSERT(*aForceValidateCacheContent == + synthesizedForceValidateCacheContent); + } +#endif + return NS_OK; +} + +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, + PreferredAlternativeDataDeliveryType 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::GetAlternativeDataInputStream( + nsIInputStream** aInputStream) { + if (mSynthesizedCacheInfo) { + return mSynthesizedCacheInfo->GetAlternativeDataInputStream(aInputStream); + } + 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; +} + +// InterceptionTimeStamps implementation +InterceptedHttpChannel::InterceptionTimeStamps::InterceptionTimeStamps() + : mStage(InterceptedHttpChannel::InterceptionTimeStamps::InterceptionStart), + mStatus(InterceptedHttpChannel::InterceptionTimeStamps::Created) {} + +void InterceptedHttpChannel::InterceptionTimeStamps::Init( + nsIChannel* aChannel) { + MOZ_ASSERT(aChannel); + MOZ_ASSERT(mStatus == Created); + + mStatus = Initialized; + + mIsNonSubresourceRequest = nsContentUtils::IsNonSubresourceRequest(aChannel); + mKey = mIsNonSubresourceRequest ? "navigation"_ns : "subresource"_ns; + nsCOMPtr interceptedChannel = + do_QueryInterface(aChannel); + // It must be a InterceptedHttpChannel + MOZ_ASSERT(interceptedChannel); + if (!mIsNonSubresourceRequest) { + interceptedChannel->GetSubresourceTimeStampKey(aChannel, mSubresourceKey); + } +} + +void InterceptedHttpChannel::InterceptionTimeStamps::RecordTime( + InterceptedHttpChannel::InterceptionTimeStamps::Status&& aStatus, + TimeStamp&& aTimeStamp) { + // Only allow passing Synthesized, Reset, Redirected, and Canceled in this + // method. + MOZ_ASSERT(aStatus == Synthesized || aStatus == Reset || + aStatus == Canceled || aStatus == Redirected); + if (mStatus == Canceled) { + return; + } + + // If current status is not Initialized, only Canceled can be recorded. + // That means it is canceled after other operation is done, ex. synthesized. + MOZ_ASSERT(mStatus == Initialized || aStatus == Canceled); + + switch (mStatus) { + case Initialized: + mStatus = aStatus; + break; + case Synthesized: + mStatus = CanceledAfterSynthesized; + break; + case Reset: + mStatus = CanceledAfterReset; + break; + case Redirected: + mStatus = CanceledAfterRedirected; + break; + // Channel is cancelled before calling AsyncOpenInternal(), no need to + // record the cancel time stamp. + case Created: + return; + default: + MOZ_ASSERT(false); + break; + } + + RecordTimeInternal(std::move(aTimeStamp)); +} + +void InterceptedHttpChannel::InterceptionTimeStamps::RecordTime( + TimeStamp&& aTimeStamp) { + MOZ_ASSERT(mStatus == Initialized || mStatus == Canceled); + if (mStatus == Canceled) { + return; + } + RecordTimeInternal(std::move(aTimeStamp)); +} + +void InterceptedHttpChannel::InterceptionTimeStamps::RecordTimeInternal( + TimeStamp&& aTimeStamp) { + MOZ_ASSERT(mStatus != Created); + + if (mStatus == Canceled && mStage != InterceptionFinish) { + mFetchHandlerStart = aTimeStamp; + mFetchHandlerFinish = aTimeStamp; + mStage = InterceptionFinish; + } + + switch (mStage) { + case InterceptionStart: { + MOZ_ASSERT(mInterceptionStart.IsNull()); + mInterceptionStart = aTimeStamp; + mStage = FetchHandlerStart; + break; + } + case (FetchHandlerStart): { + MOZ_ASSERT(mFetchHandlerStart.IsNull()); + mFetchHandlerStart = aTimeStamp; + mStage = FetchHandlerFinish; + break; + } + case (FetchHandlerFinish): { + MOZ_ASSERT(mFetchHandlerFinish.IsNull()); + mFetchHandlerFinish = aTimeStamp; + mStage = InterceptionFinish; + break; + } + case InterceptionFinish: { + mInterceptionFinish = aTimeStamp; + SaveTimeStamps(); + return; + } + default: { + return; + } + } +} + +void InterceptedHttpChannel::InterceptionTimeStamps::GenKeysWithStatus( + nsCString& aKey, nsCString& aSubresourceKey) { + nsAutoCString statusString; + switch (mStatus) { + case Synthesized: + statusString = "synthesized"_ns; + break; + case Reset: + statusString = "reset"_ns; + break; + case Redirected: + statusString = "redirected"_ns; + break; + case Canceled: + statusString = "canceled"_ns; + break; + case CanceledAfterSynthesized: + statusString = "canceled-after-synthesized"_ns; + break; + case CanceledAfterReset: + statusString = "canceled-after-reset"_ns; + break; + case CanceledAfterRedirected: + statusString = "canceled-after-redirected"_ns; + break; + default: + return; + } + aKey = mKey; + aSubresourceKey = mSubresourceKey; + aKey.AppendLiteral("_"); + aSubresourceKey.AppendLiteral("_"); + aKey.Append(statusString); + aSubresourceKey.Append(statusString); +} + +void InterceptedHttpChannel::InterceptionTimeStamps::SaveTimeStamps() { + MOZ_ASSERT(mStatus != Initialized && mStatus != Created); + + if (mStatus == Synthesized || mStatus == Reset) { + Telemetry::HistogramID id = + Telemetry::SERVICE_WORKER_FETCH_EVENT_FINISH_SYNTHESIZED_RESPONSE_MS_2; + if (mStatus == Reset) { + id = Telemetry::SERVICE_WORKER_FETCH_EVENT_CHANNEL_RESET_MS_2; + } + + Telemetry::Accumulate( + id, mKey, + static_cast( + (mInterceptionFinish - mFetchHandlerFinish).ToMilliseconds())); + if (!mIsNonSubresourceRequest && !mSubresourceKey.IsEmpty()) { + Telemetry::Accumulate( + id, mSubresourceKey, + static_cast( + (mInterceptionFinish - mFetchHandlerFinish).ToMilliseconds())); + } + } + + if (!mFetchHandlerStart.IsNull()) { + Telemetry::Accumulate( + Telemetry::SERVICE_WORKER_FETCH_EVENT_DISPATCH_MS_2, mKey, + static_cast( + (mFetchHandlerStart - mInterceptionStart).ToMilliseconds())); + + if (!mIsNonSubresourceRequest && !mSubresourceKey.IsEmpty()) { + Telemetry::Accumulate( + Telemetry::SERVICE_WORKER_FETCH_EVENT_DISPATCH_MS_2, mSubresourceKey, + static_cast( + (mFetchHandlerStart - mInterceptionStart).ToMilliseconds())); + } + } + + nsAutoCString key, subresourceKey; + GenKeysWithStatus(key, subresourceKey); + + Telemetry::Accumulate( + Telemetry::SERVICE_WORKER_FETCH_INTERCEPTION_DURATION_MS_2, key, + static_cast( + (mInterceptionFinish - mInterceptionStart).ToMilliseconds())); + if (!mIsNonSubresourceRequest && !mSubresourceKey.IsEmpty()) { + Telemetry::Accumulate( + Telemetry::SERVICE_WORKER_FETCH_INTERCEPTION_DURATION_MS_2, + subresourceKey, + static_cast( + (mInterceptionFinish - mInterceptionStart).ToMilliseconds())); + } +} + +} // namespace mozilla::net diff --git a/netwerk/protocol/http/InterceptedHttpChannel.h b/netwerk/protocol/http/InterceptedHttpChannel.h new file mode 100644 index 0000000000..b534999513 --- /dev/null +++ b/netwerk/protocol/http/InterceptedHttpChannel.h @@ -0,0 +1,316 @@ +/* -*- 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::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 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 mInterceptedChannelCreationTimestamp; + + // For the profiler markers + TimeStamp mLastStatusReported; + + Atomic mProgress; + int64_t mProgressReported; + int64_t mSynthesizedStreamLength; + uint64_t mResumeStartPos; + nsCString mResumeEntityId; + nsString mStatusHost; + Atomic mCallingStatusAndProgress; + bool mInterceptionReset{false}; + + /** + * InterceptionTimeStamps is used to record the time stamps of the + * interception. + * The general usage: + * Step 1. Initialize the InterceptionTimeStamps; + * InterceptionTimeStamps::Init(channel); + * Step 2. Record time for each stage + * InterceptionTimeStamps::RecordTime(); or + * InterceptionTimeStamps::RecordTime(timeStamp); + * Step 3. Record time for the last stage with the final status + * InterceptionTimeStamps::RecordTime(InterceptionTimeStamps::Synthesized); + */ + class InterceptionTimeStamps final { + public: + // The possible status of the interception. + enum Status { + Created, + Initialized, + Synthesized, + Reset, + Redirected, + Canceled, + CanceledAfterSynthesized, + CanceledAfterReset, + CanceledAfterRedirected + }; + + InterceptionTimeStamps(); + ~InterceptionTimeStamps() = default; + + /** + * Initialize with the given channel. + * This method should be called before any RecordTime(). + */ + void Init(nsIChannel* aChannel); + + /** + * Record the given time stamp for current stage. If there is no given time + * stamp, TimeStamp::Now() will be recorded. + * The current stage is auto moved to the next one. + */ + void RecordTime(TimeStamp&& aTimeStamp = TimeStamp::Now()); + + /** + * Record the given time stamp for the last stage(InterceptionFinish) and + * set the final status to the given status. + * If these is no given time stamp, TimeStamp::Now() will be recorded. + * Notice that this method is for the last stage, it calls SaveTimeStamps() + * to write data into telemetries. + */ + void RecordTime(Status&& aStatus, + TimeStamp&& aTimeStamp = TimeStamp::Now()); + + // The time stamp which the intercepted channel is created and async opend. + TimeStamp mInterceptionStart; + + // The time stamp which the interception finishes. + TimeStamp mInterceptionFinish; + + // The time stamp which the fetch event starts to be handled by fetch event + // handler. + TimeStamp mFetchHandlerStart; + + // The time stamp which the fetch event handling finishes. It would the time + // which remote worker sends result back. + TimeStamp mFetchHandlerFinish; + + private: + // The stage of interception. + enum Stage { + InterceptionStart, + FetchHandlerStart, + FetchHandlerFinish, + InterceptionFinish + } mStage; + + // The final status of the interception. + Status mStatus; + + bool mIsNonSubresourceRequest; + // The keys used for telemetries. + nsCString mKey; + nsCString mSubresourceKey; + + void RecordTimeInternal(TimeStamp&& aTimeStamp); + + // Generate the record keys with final status. + void GenKeysWithStatus(nsCString& aKey, nsCString& aSubresourceKey); + + // Save the time stamps into telemetries. + void SaveTimeStamps(); + }; + + InterceptionTimeStamps mTimeStamps; + + 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(); + + TimeStamp mServiceWorkerLaunchStart; + TimeStamp mServiceWorkerLaunchEnd; + + 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 SetCanceledReason(const nsACString& aReason) override; + NS_IMETHOD GetCanceledReason(nsACString& aReason) override; + NS_IMETHOD CancelWithReason(nsresult status, + const nsACString& reason) override; + + NS_IMETHOD + Cancel(nsresult aStatus) override; + + NS_IMETHOD + Suspend(void) override; + + NS_IMETHOD + Resume(void) override; + + NS_IMETHOD + GetSecurityInfo(nsITransportSecurityInfo** aSecurityInfo) override; + + NS_IMETHOD + AsyncOpen(nsIStreamListener* aListener) override; + + NS_IMETHOD + LogBlockedCORSRequest(const nsAString& aMessage, const nsACString& aCategory, + bool aIsWarning) override; + + NS_IMETHOD + LogMimeTypeMismatch(const nsACString& aMessageName, bool aWarning, + const nsAString& aURL, + const nsAString& aContentType) 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 + SetClassOfService(ClassOfService cos) override; + + NS_IMETHOD + SetIncremental(bool incremental) override; + + NS_IMETHOD + ResumeAt(uint64_t startPos, const nsACString& entityID) override; + + NS_IMETHOD + SetEarlyHintObserver(nsIEarlyHintObserver* aObserver) override { + return NS_OK; + } + + NS_IMETHOD SetWebTransportSessionEventListener( + WebTransportSessionEventListener* aListener) override { + return NS_OK; + } + + NS_IMETHOD SetLaunchServiceWorkerStart(TimeStamp aTimeStamp) override; + NS_IMETHOD GetLaunchServiceWorkerStart(TimeStamp* aRetVal) override; + + NS_IMETHOD SetLaunchServiceWorkerEnd(TimeStamp aTimeStamp) override; + NS_IMETHOD GetLaunchServiceWorkerEnd(TimeStamp* aRetVal) override; + + NS_IMETHOD GetDispatchFetchEventStart(TimeStamp* aRetVal) override; + NS_IMETHOD GetDispatchFetchEventEnd(TimeStamp* aRetVal) override; + + NS_IMETHOD GetHandleFetchEventStart(TimeStamp* aRetVal) override; + NS_IMETHOD GetHandleFetchEventEnd(TimeStamp* aRetVal) override; + + void DoNotifyListenerCleanup() override; + + void DoAsyncAbort(nsresult aStatus) override; +}; + +} // namespace mozilla::net + +#endif // mozilla_net_InterceptedHttpChannel_h diff --git a/netwerk/protocol/http/MockHttpAuth.cpp b/netwerk/protocol/http/MockHttpAuth.cpp new file mode 100644 index 0000000000..588b30fadc --- /dev/null +++ b/netwerk/protocol/http/MockHttpAuth.cpp @@ -0,0 +1,46 @@ +/* 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 "MockHttpAuth.h" + +namespace mozilla::net { + +NS_IMPL_ISUPPORTS(MockHttpAuth, nsIHttpAuthenticator) + +NS_IMETHODIMP MockHttpAuth::ChallengeReceived( + nsIHttpAuthenticableChannel* aChannel, const nsACString& aChallenge, + bool aProxyAuth, nsISupports** aSessionState, + nsISupports** aContinuationState, bool* aInvalidatesIdentity) { + *aInvalidatesIdentity = false; + return NS_OK; +} + +NS_IMETHODIMP MockHttpAuth::GenerateCredentialsAsync( + nsIHttpAuthenticableChannel* aChannel, + nsIHttpAuthenticatorCallback* aCallback, const nsACString& aChallenge, + bool aProxyAuth, const nsAString& aDomain, const nsAString& aUser, + const nsAString& aPassword, nsISupports* aSessionState, + nsISupports* aContinuationState, nsICancelable** aCancel) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP MockHttpAuth::GenerateCredentials( + nsIHttpAuthenticableChannel* aChannel, const nsACString& aChallenge, + bool aProxyAuth, const nsAString& aDomain, const nsAString& aUser, + const nsAString& aPassword, nsISupports** aSessionState, + nsISupports** aContinuationState, uint32_t* aFlags, nsACString& _retval) { + _retval.Assign("moz_test_credentials"); + return NS_OK; +} + +NS_IMETHODIMP MockHttpAuth::GetAuthFlags(uint32_t* aAuthFlags) { + *aAuthFlags |= nsIHttpAuthenticator::CONNECTION_BASED; + return NS_OK; +} + +} // namespace mozilla::net diff --git a/netwerk/protocol/http/MockHttpAuth.h b/netwerk/protocol/http/MockHttpAuth.h new file mode 100644 index 0000000000..a1f737c955 --- /dev/null +++ b/netwerk/protocol/http/MockHttpAuth.h @@ -0,0 +1,31 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 MockHttpAuth_h__ +#define MockHttpAuth_h__ + +#include "nsIHttpAuthenticator.h" + +namespace mozilla::net { + +// This authenticator is only used for testing purposes. +class MockHttpAuth : public nsIHttpAuthenticator { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIHTTPAUTHENTICATOR + + MockHttpAuth() = default; + + static already_AddRefed Create() { + nsCOMPtr authenticator = new MockHttpAuth(); + return authenticator.forget(); + } + + private: + virtual ~MockHttpAuth() = default; +}; + +} // namespace mozilla::net + +#endif // MockHttpAuth_h__ diff --git a/netwerk/protocol/http/NetworkMarker.cpp b/netwerk/protocol/http/NetworkMarker.cpp new file mode 100644 index 0000000000..ece1845fef --- /dev/null +++ b/netwerk/protocol/http/NetworkMarker.cpp @@ -0,0 +1,188 @@ +/* -*- 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/. */ + +#include "NetworkMarker.h" + +#include "HttpBaseChannel.h" +#include "nsIChannelEventSink.h" + +namespace mozilla { +namespace net { + +static constexpr net::TimingStruct scEmptyNetTimingStruct; + +void profiler_add_network_marker( + nsIURI* aURI, const nsACString& aRequestMethod, int32_t aPriority, + uint64_t aChannelId, NetworkLoadType aType, mozilla::TimeStamp aStart, + mozilla::TimeStamp aEnd, int64_t aCount, + mozilla::net::CacheDisposition aCacheDisposition, uint64_t aInnerWindowID, + bool aIsPrivateBrowsing, const mozilla::net::TimingStruct* aTimings, + UniquePtr aSource, + const Maybe& aContentType, nsIURI* aRedirectURI, + uint32_t aRedirectFlags, uint64_t aRedirectChannelId) { + if (!profiler_thread_is_being_profiled_for_markers()) { + return; + } + + nsAutoCStringN<2048> name; + name.AppendASCII("Load "); + // top 32 bits are process id of the load + name.AppendInt(aChannelId & 0xFFFFFFFFu); + + // These can do allocations/frees/etc; avoid if not active + nsAutoCStringN<2048> spec; + if (aURI) { + aURI->GetAsciiSpec(spec); + name.AppendASCII(": "); + name.Append(spec); + } + + nsAutoCString redirect_spec; + if (aRedirectURI) { + aRedirectURI->GetAsciiSpec(redirect_spec); + } + + struct NetworkMarker { + static constexpr Span MarkerTypeName() { + return MakeStringSpan("Network"); + } + static void StreamJSONMarkerData( + baseprofiler::SpliceableJSONWriter& aWriter, mozilla::TimeStamp aStart, + mozilla::TimeStamp aEnd, int64_t aID, const ProfilerString8View& aURI, + const ProfilerString8View& aRequestMethod, NetworkLoadType aType, + int32_t aPri, int64_t aCount, net::CacheDisposition aCacheDisposition, + bool aIsPrivateBrowsing, const net::TimingStruct& aTimings, + const ProfilerString8View& aRedirectURI, + const ProfilerString8View& aContentType, uint32_t aRedirectFlags, + int64_t aRedirectChannelId) { + // This payload still streams a startTime and endTime property because it + // made the migration to MarkerTiming on the front-end easier. + aWriter.TimeProperty("startTime", aStart); + aWriter.TimeProperty("endTime", aEnd); + + aWriter.IntProperty("id", aID); + aWriter.StringProperty("status", GetNetworkState(aType)); + if (Span cacheString = GetCacheState(aCacheDisposition); + !cacheString.IsEmpty()) { + aWriter.StringProperty("cache", cacheString); + } + aWriter.IntProperty("pri", aPri); + if (aCount > 0) { + aWriter.IntProperty("count", aCount); + } + if (aURI.Length() != 0) { + aWriter.StringProperty("URI", aURI); + } + if (aRedirectURI.Length() != 0) { + aWriter.StringProperty("RedirectURI", aRedirectURI); + aWriter.StringProperty("redirectType", getRedirectType(aRedirectFlags)); + aWriter.BoolProperty( + "isHttpToHttpsRedirect", + aRedirectFlags & nsIChannelEventSink::REDIRECT_STS_UPGRADE); + + if (aRedirectChannelId != 0) { + aWriter.IntProperty("redirectId", aRedirectChannelId); + } + } + + aWriter.StringProperty("requestMethod", aRequestMethod); + + if (aContentType.Length() != 0) { + aWriter.StringProperty("contentType", aContentType); + } else { + aWriter.NullProperty("contentType"); + } + + if (aIsPrivateBrowsing) { + aWriter.BoolProperty("isPrivateBrowsing", aIsPrivateBrowsing); + } + + if (aType != NetworkLoadType::LOAD_START) { + aWriter.TimeProperty("domainLookupStart", aTimings.domainLookupStart); + aWriter.TimeProperty("domainLookupEnd", aTimings.domainLookupEnd); + aWriter.TimeProperty("connectStart", aTimings.connectStart); + aWriter.TimeProperty("tcpConnectEnd", aTimings.tcpConnectEnd); + aWriter.TimeProperty("secureConnectionStart", + aTimings.secureConnectionStart); + aWriter.TimeProperty("connectEnd", aTimings.connectEnd); + aWriter.TimeProperty("requestStart", aTimings.requestStart); + aWriter.TimeProperty("responseStart", aTimings.responseStart); + aWriter.TimeProperty("responseEnd", aTimings.responseEnd); + } + } + static MarkerSchema MarkerTypeDisplay() { + return MarkerSchema::SpecialFrontendLocation{}; + } + + private: + static Span GetNetworkState(NetworkLoadType aType) { + switch (aType) { + case NetworkLoadType::LOAD_START: + return MakeStringSpan("STATUS_START"); + case NetworkLoadType::LOAD_STOP: + return MakeStringSpan("STATUS_STOP"); + case NetworkLoadType::LOAD_REDIRECT: + return MakeStringSpan("STATUS_REDIRECT"); + case NetworkLoadType::LOAD_CANCEL: + return MakeStringSpan("STATUS_CANCEL"); + default: + MOZ_ASSERT(false, "Unexpected NetworkLoadType enum value."); + return MakeStringSpan(""); + } + } + + static Span GetCacheState( + net::CacheDisposition aCacheDisposition) { + switch (aCacheDisposition) { + case net::kCacheUnresolved: + return MakeStringSpan("Unresolved"); + case net::kCacheHit: + return MakeStringSpan("Hit"); + case net::kCacheHitViaReval: + return MakeStringSpan("HitViaReval"); + case net::kCacheMissedViaReval: + return MakeStringSpan("MissedViaReval"); + case net::kCacheMissed: + return MakeStringSpan("Missed"); + case net::kCacheUnknown: + return MakeStringSpan(""); + default: + MOZ_ASSERT(false, "Unexpected CacheDisposition enum value."); + return MakeStringSpan(""); + } + } + + static Span getRedirectType(uint32_t aRedirectFlags) { + MOZ_ASSERT(aRedirectFlags != 0, "aRedirectFlags should be non-zero"); + if (aRedirectFlags & nsIChannelEventSink::REDIRECT_TEMPORARY) { + return MakeStringSpan("Temporary"); + } + if (aRedirectFlags & nsIChannelEventSink::REDIRECT_PERMANENT) { + return MakeStringSpan("Permanent"); + } + if (aRedirectFlags & nsIChannelEventSink::REDIRECT_INTERNAL) { + return MakeStringSpan("Internal"); + } + MOZ_ASSERT(false, "Couldn't find a redirect type from aRedirectFlags"); + return MakeStringSpan(""); + } + }; + + profiler_add_marker( + name, geckoprofiler::category::NETWORK, + {MarkerTiming::Interval(aStart, aEnd), + MarkerStack::TakeBacktrace(std::move(aSource)), + MarkerInnerWindowId(aInnerWindowID)}, + NetworkMarker{}, aStart, aEnd, static_cast(aChannelId), spec, + aRequestMethod, aType, aPriority, aCount, aCacheDisposition, + aIsPrivateBrowsing, aTimings ? *aTimings : scEmptyNetTimingStruct, + redirect_spec, + aContentType ? ProfilerString8View(*aContentType) : ProfilerString8View(), + aRedirectFlags, aRedirectChannelId); +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/NetworkMarker.h b/netwerk/protocol/http/NetworkMarker.h new file mode 100644 index 0000000000..cab1de5f07 --- /dev/null +++ b/netwerk/protocol/http/NetworkMarker.h @@ -0,0 +1,40 @@ +/* -*- 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 NetworkMarker_h__ +#define NetworkMarker_h__ + +#include "mozilla/ProfilerMarkers.h" + +namespace mozilla { +namespace net { + +struct TimingStruct; +enum CacheDisposition : uint8_t; + +enum class NetworkLoadType { + LOAD_START, + LOAD_STOP, + LOAD_REDIRECT, + LOAD_CANCEL +}; + +void profiler_add_network_marker( + nsIURI* aURI, const nsACString& aRequestMethod, int32_t aPriority, + uint64_t aChannelId, NetworkLoadType aType, mozilla::TimeStamp aStart, + mozilla::TimeStamp aEnd, int64_t aCount, + mozilla::net::CacheDisposition aCacheDisposition, uint64_t aInnerWindowID, + bool aIsPrivateBrowsing, + const mozilla::net::TimingStruct* aTimings = nullptr, + mozilla::UniquePtr aSource = nullptr, + const mozilla::Maybe& aContentType = mozilla::Nothing(), + nsIURI* aRedirectURI = nullptr, uint32_t aRedirectFlags = 0, + uint64_t aRedirectChannelId = 0); + +} // namespace net +} // namespace mozilla + +#endif // NetworkMarker_h__ diff --git a/netwerk/protocol/http/NullHttpChannel.cpp b/netwerk/protocol/http/NullHttpChannel.cpp new file mode 100644 index 0000000000..eae11441af --- /dev/null +++ b/netwerk/protocol/http/NullHttpChannel.cpp @@ -0,0 +1,865 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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() { + 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::GetBrowserId(uint64_t*) { return NS_ERROR_NOT_IMPLEMENTED; } + +NS_IMETHODIMP +NullHttpChannel::SetBrowserId(uint64_t) { 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::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; +} + +void NullHttpChannel::SetSource( + mozilla::UniquePtr aSource) {} + +//----------------------------------------------------------------------------- +// NullHttpChannel::nsIChannel +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +NullHttpChannel::GetOriginalURI(nsIURI** aOriginalURI) { + *aOriginalURI = do_AddRef(mOriginalURI).take(); + return NS_OK; +} + +NS_IMETHODIMP +NullHttpChannel::SetOriginalURI(nsIURI* aOriginalURI) { + mOriginalURI = aOriginalURI; + return NS_OK; +} + +NS_IMETHODIMP +NullHttpChannel::GetURI(nsIURI** aURI) { + *aURI = do_AddRef(mURI).take(); + 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(nsITransportSecurityInfo** 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::SetCanceledReason(const nsACString& aReason) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP NullHttpChannel::GetCanceledReason(nsACString& aReason) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP NullHttpChannel::CancelWithReason(nsresult aStatus, + const nsACString& aReason) { + 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) { + *_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::GetTransactionPending(mozilla::TimeStamp* aRetVal) { + *aRetVal = 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& aCategor, + bool aIsWarning) { + 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; +} + +NS_IMETHODIMP NullHttpChannel::SetClassicScriptHintCharset( + const nsAString& aClassicScriptHintCharset) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP NullHttpChannel::GetClassicScriptHintCharset( + nsAString& aClassicScriptHintCharset) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP NullHttpChannel::SetDocumentCharacterSet( + const nsAString& aDocumentCharacterSet) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP NullHttpChannel::GetDocumentCharacterSet( + nsAString& aDocumentCharacterSet) { + 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) +IMPL_TIMING_ATTR(TransactionPending) + +#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..478d13e0a2 --- /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{false}; + bool mAllRedirectsPassTimingAllowCheck{false}; +}; + +} // 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..f27eb16138 --- /dev/null +++ b/netwerk/protocol/http/NullHttpTransaction.cpp @@ -0,0 +1,212 @@ +/* -*- 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" + +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), + 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; + } + } 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..cedbc6fd63 --- /dev/null +++ b/netwerk/protocol/http/NullHttpTransaction.h @@ -0,0 +1,94 @@ +/* -*- 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 BrowserId() override { return 0; } + + TimingStruct Timings() { return mTimings; } + + mozilla::TimeStamp GetTcpConnectEnd() { return mTimings.tcpConnectEnd; } + mozilla::TimeStamp GetSecureConnectionStart() { + return mTimings.secureConnectionStart; + } + + protected: + virtual ~NullHttpTransaction(); + + private: + nsresult mStatus; + + protected: + uint32_t mCaps; + nsHttpRequestHead* mRequestHead; + + private: + bool mIsDone; + bool mClaimed; + TimingStruct mTimings; + + 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/ObliviousHttpChannel.cpp b/netwerk/protocol/http/ObliviousHttpChannel.cpp new file mode 100644 index 0000000000..fafbf84b0b --- /dev/null +++ b/netwerk/protocol/http/ObliviousHttpChannel.cpp @@ -0,0 +1,860 @@ +/* -*- 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 "ObliviousHttpChannel.h" + +#include "BinaryHttpRequest.h" +#include "nsIHttpHeaderVisitor.h" +#include "nsStringStream.h" + +namespace mozilla::net { + +NS_IMPL_ISUPPORTS(ObliviousHttpChannel, nsIChannel, nsIHttpChannel, + nsIObliviousHttpChannel, nsIHttpChannelInternal, + nsIIdentChannel, nsIRequest, nsIRequestObserver, + nsIStreamListener, nsIUploadChannel2, nsITimedChannel) + +ObliviousHttpChannel::ObliviousHttpChannel( + nsIURI* targetURI, const nsTArray& encodedConfig, + nsIHttpChannel* innerChannel) + : mTargetURI(targetURI), + mEncodedConfig(encodedConfig.Clone()), + mInnerChannel(innerChannel), + mInnerChannelInternal(do_QueryInterface(innerChannel)), + mInnerChannelTimed(do_QueryInterface(innerChannel)) { + LOG(("ObliviousHttpChannel ctor [this=%p]", this)); + MOZ_ASSERT(mInnerChannel); + MOZ_ASSERT(mInnerChannelInternal); + MOZ_ASSERT(mInnerChannelTimed); +} + +ObliviousHttpChannel::~ObliviousHttpChannel() { + LOG(("ObliviousHttpChannel dtor [this=%p]", this)); +} + +//----------------------------------------------------------------------------- +// ObliviousHttpChannel::nsIHttpChannel +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +ObliviousHttpChannel::GetTopLevelContentWindowId(uint64_t* aWindowId) { + return mInnerChannel->GetTopLevelContentWindowId(aWindowId); +} + +NS_IMETHODIMP +ObliviousHttpChannel::SetTopLevelContentWindowId(uint64_t aWindowId) { + return mInnerChannel->SetTopLevelContentWindowId(aWindowId); +} + +NS_IMETHODIMP +ObliviousHttpChannel::GetBrowserId(uint64_t* aWindowId) { + return mInnerChannel->GetBrowserId(aWindowId); +} + +NS_IMETHODIMP +ObliviousHttpChannel::SetBrowserId(uint64_t aId) { + return mInnerChannel->SetBrowserId(aId); +} + +NS_IMETHODIMP +ObliviousHttpChannel::GetTransferSize(uint64_t* aTransferSize) { + return mInnerChannel->GetTransferSize(aTransferSize); +} + +NS_IMETHODIMP +ObliviousHttpChannel::GetRequestSize(uint64_t* aRequestSize) { + return mInnerChannel->GetRequestSize(aRequestSize); +} + +NS_IMETHODIMP +ObliviousHttpChannel::GetDecodedBodySize(uint64_t* aDecodedBodySize) { + return mInnerChannel->GetDecodedBodySize(aDecodedBodySize); +} + +NS_IMETHODIMP +ObliviousHttpChannel::GetRequestMethod(nsACString& aRequestMethod) { + aRequestMethod.Assign(mMethod); + return NS_OK; +} + +NS_IMETHODIMP +ObliviousHttpChannel::SetRequestMethod(const nsACString& aRequestMethod) { + mMethod.Assign(aRequestMethod); + return NS_OK; +} + +NS_IMETHODIMP +ObliviousHttpChannel::GetReferrerInfo(nsIReferrerInfo** aReferrerInfo) { + return mInnerChannel->GetReferrerInfo(aReferrerInfo); +} + +NS_IMETHODIMP +ObliviousHttpChannel::SetReferrerInfo(nsIReferrerInfo* aReferrerInfo) { + return mInnerChannel->SetReferrerInfo(aReferrerInfo); +} + +NS_IMETHODIMP +ObliviousHttpChannel::SetReferrerInfoWithoutClone( + nsIReferrerInfo* aReferrerInfo) { + return mInnerChannel->SetReferrerInfoWithoutClone(aReferrerInfo); +} + +NS_IMETHODIMP +ObliviousHttpChannel::GetRequestHeader(const nsACString& aHeader, + nsACString& _retval) { + _retval.Truncate(); + auto value = mHeaders.Lookup(aHeader); + if (!value) { + return NS_ERROR_NOT_AVAILABLE; + } + _retval.Assign(*value); + return NS_OK; +} + +NS_IMETHODIMP +ObliviousHttpChannel::SetRequestHeader(const nsACString& aHeader, + const nsACString& aValue, bool aMerge) { + mHeaders.WithEntryHandle(aHeader, [&aValue, aMerge](auto&& entry) { + if (!entry) { + entry.Insert(aValue); + return; + } + if (!aMerge) { + entry.Update(aValue); + return; + } + nsAutoCString newValue(*entry); + newValue.AppendLiteral(", "); + newValue.Append(aValue); + entry.Update(newValue); + }); + return NS_OK; +} + +NS_IMETHODIMP +ObliviousHttpChannel::SetNewReferrerInfo( + const nsACString& aUrl, nsIReferrerInfo::ReferrerPolicyIDL aPolicy, + bool aSendReferrer) { + return mInnerChannel->SetNewReferrerInfo(aUrl, aPolicy, aSendReferrer); +} + +NS_IMETHODIMP +ObliviousHttpChannel::SetEmptyRequestHeader(const nsACString& aHeader) { + return SetRequestHeader(aHeader, EmptyCString(), false); +} + +NS_IMETHODIMP +ObliviousHttpChannel::VisitRequestHeaders(nsIHttpHeaderVisitor* aVisitor) { + for (auto iter = mHeaders.ConstIter(); !iter.Done(); iter.Next()) { + nsresult rv = aVisitor->VisitHeader(iter.Key(), iter.Data()); + if (NS_FAILED(rv)) { + return rv; + } + } + return NS_OK; +} + +NS_IMETHODIMP +ObliviousHttpChannel::VisitNonDefaultRequestHeaders( + nsIHttpHeaderVisitor* aVisitor) { + return mInnerChannel->VisitNonDefaultRequestHeaders(aVisitor); +} + +NS_IMETHODIMP +ObliviousHttpChannel::GetAllowSTS(bool* aAllowSTS) { + return mInnerChannel->GetAllowSTS(aAllowSTS); +} + +NS_IMETHODIMP +ObliviousHttpChannel::SetAllowSTS(bool aAllowSTS) { + return mInnerChannel->SetAllowSTS(aAllowSTS); +} + +NS_IMETHODIMP +ObliviousHttpChannel::GetRedirectionLimit(uint32_t* aRedirectionLimit) { + return mInnerChannel->GetRedirectionLimit(aRedirectionLimit); +} + +NS_IMETHODIMP +ObliviousHttpChannel::SetRedirectionLimit(uint32_t aRedirectionLimit) { + return mInnerChannel->SetRedirectionLimit(aRedirectionLimit); +} + +NS_IMETHODIMP +ObliviousHttpChannel::GetResponseStatus(uint32_t* aResponseStatus) { + if (!mBinaryHttpResponse) { + return NS_ERROR_NOT_AVAILABLE; + } + uint16_t responseStatus; + nsresult rv = mBinaryHttpResponse->GetStatus(&responseStatus); + if (NS_FAILED(rv)) { + return rv; + } + *aResponseStatus = responseStatus; + return NS_OK; +} + +NS_IMETHODIMP +ObliviousHttpChannel::GetResponseStatusText(nsACString& aResponseStatusText) { + LOG(("ObliviousHttpChannel::GetResponseStatusText NOT IMPLEMENTED [this=%p]", + this)); + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ObliviousHttpChannel::GetRequestSucceeded(bool* aRequestSucceeded) { + uint32_t responseStatus; + nsresult rv = GetResponseStatus(&responseStatus); + if (NS_FAILED(rv)) { + return rv; + } + *aRequestSucceeded = (responseStatus / 100) == 2; + return NS_OK; +} + +NS_IMETHODIMP +ObliviousHttpChannel::GetIsMainDocumentChannel(bool* aValue) { + return mInnerChannel->GetIsMainDocumentChannel(aValue); +} + +NS_IMETHODIMP +ObliviousHttpChannel::SetIsMainDocumentChannel(bool aValue) { + return mInnerChannel->SetIsMainDocumentChannel(aValue); +} + +NS_IMETHODIMP +ObliviousHttpChannel::GetResponseHeader(const nsACString& header, + nsACString& _retval) { + if (!mBinaryHttpResponse) { + return NS_ERROR_NOT_AVAILABLE; + } + nsTArray responseHeaderNames; + nsTArray responseHeaderValues; + nsresult rv = mBinaryHttpResponse->GetHeaderNames(responseHeaderNames); + if (NS_FAILED(rv)) { + return rv; + } + rv = mBinaryHttpResponse->GetHeaderValues(responseHeaderValues); + if (NS_FAILED(rv)) { + return rv; + } + for (size_t i = 0; + i < responseHeaderNames.Length() && i < responseHeaderValues.Length(); + i++) { + if (responseHeaderNames[i].EqualsIgnoreCase(header)) { + _retval.Assign(responseHeaderValues[i]); + return NS_OK; + } + } + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP +ObliviousHttpChannel::SetResponseHeader(const nsACString& header, + const nsACString& value, bool merge) { + LOG(("ObliviousHttpChannel::SetResponseHeader NOT IMPLEMENTED [this=%p]", + this)); + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ObliviousHttpChannel::VisitResponseHeaders(nsIHttpHeaderVisitor* aVisitor) { + if (!mBinaryHttpResponse) { + return NS_ERROR_NOT_AVAILABLE; + } + nsTArray responseHeaderNames; + nsTArray responseHeaderValues; + nsresult rv = mBinaryHttpResponse->GetHeaderNames(responseHeaderNames); + if (NS_FAILED(rv)) { + return rv; + } + rv = mBinaryHttpResponse->GetHeaderValues(responseHeaderValues); + if (NS_FAILED(rv)) { + return rv; + } + for (size_t i = 0; + i < responseHeaderNames.Length() && i < responseHeaderValues.Length(); + i++) { + rv = aVisitor->VisitHeader(responseHeaderNames[i], responseHeaderValues[i]); + if (NS_FAILED(rv)) { + return rv; + } + } + return rv; +} + +NS_IMETHODIMP +ObliviousHttpChannel::GetOriginalResponseHeader( + const nsACString& header, nsIHttpHeaderVisitor* aVisitor) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ObliviousHttpChannel::VisitOriginalResponseHeaders( + nsIHttpHeaderVisitor* aVisitor) { + LOG( + ("ObliviousHttpChannel::VisitOriginalResponseHeaders NOT IMPLEMENTED " + "[this=%p]", + this)); + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ObliviousHttpChannel::ShouldStripRequestBodyHeader(const nsACString& aMethod, + bool* aResult) { + LOG( + ("ObliviousHttpChannel::ShouldStripRequestBodyHeader NOT IMPLEMENTED " + "[this=%p]", + this)); + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ObliviousHttpChannel::IsNoStoreResponse(bool* _retval) { + LOG(("ObliviousHttpChannel::IsNoStoreResponse NOT IMPLEMENTED [this=%p]", + this)); + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ObliviousHttpChannel::IsNoCacheResponse(bool* _retval) { + LOG(("ObliviousHttpChannel::IsNoCacheResponse NOT IMPLEMENTED [this=%p]", + this)); + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ObliviousHttpChannel::IsPrivateResponse(bool* _retval) { + LOG(("ObliviousHttpChannel::IsPrivateResponse NOT IMPLEMENTED [this=%p]", + this)); + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ObliviousHttpChannel::RedirectTo(nsIURI* aNewURI) { + LOG(("ObliviousHttpChannel::RedirectTo NOT IMPLEMENTED [this=%p]", this)); + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ObliviousHttpChannel::UpgradeToSecure() { + LOG(("ObliviousHttpChannel::UpgradeToSecure NOT IMPLEMENTED [this=%p]", + this)); + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ObliviousHttpChannel::GetRequestContextID(uint64_t* _retval) { + return mInnerChannel->GetRequestContextID(_retval); +} + +NS_IMETHODIMP +ObliviousHttpChannel::SetRequestContextID(uint64_t rcID) { + return mInnerChannel->SetRequestContextID(rcID); +} + +NS_IMETHODIMP +ObliviousHttpChannel::GetProtocolVersion(nsACString& aProtocolVersion) { + return mInnerChannel->GetProtocolVersion(aProtocolVersion); +} + +NS_IMETHODIMP +ObliviousHttpChannel::GetEncodedBodySize(uint64_t* aEncodedBodySize) { + LOG(("ObliviousHttpChannel::GetEncodedBodySize NOT IMPLEMENTED [this=%p]", + this)); + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ObliviousHttpChannel::LogBlockedCORSRequest(const nsAString& aMessage, + const nsACString& aCategory, + bool aIsWarning) { + return mInnerChannel->LogBlockedCORSRequest(aMessage, aCategory, aIsWarning); +} + +NS_IMETHODIMP +ObliviousHttpChannel::LogMimeTypeMismatch(const nsACString& aMessageName, + bool aWarning, const nsAString& aURL, + const nsAString& aContentType) { + return mInnerChannel->LogMimeTypeMismatch(aMessageName, aWarning, aURL, + aContentType); +} + +void ObliviousHttpChannel::SetSource( + mozilla::UniquePtr aSource) { + LOG(("ObliviousHttpChannel::SetSource NOT IMPLEMENTED [this=%p]", this)); + // NS_ERROR_NOT_IMPLEMENTED +} + +void ObliviousHttpChannel::SetConnectionInfo(nsHttpConnectionInfo* aCi) { + if (mInnerChannelInternal) { + mInnerChannelInternal->SetConnectionInfo(aCi); + } +} + +void ObliviousHttpChannel::DoDiagnosticAssertWhenOnStopNotCalledOnDestroy() { + if (mInnerChannelInternal) { + mInnerChannelInternal->DoDiagnosticAssertWhenOnStopNotCalledOnDestroy(); + } +} + +void ObliviousHttpChannel::DisableAltDataCache() { + if (mInnerChannelInternal) { + mInnerChannelInternal->DisableAltDataCache(); + } +} + +void ObliviousHttpChannel::SetAltDataForChild(bool aIsForChild) { + if (mInnerChannelInternal) { + mInnerChannelInternal->SetAltDataForChild(aIsForChild); + } +} + +void ObliviousHttpChannel::SetCorsPreflightParameters( + nsTArray> const& aUnsafeHeaders, + bool aShouldStripRequestBodyHeader) { + if (mInnerChannelInternal) { + mInnerChannelInternal->SetCorsPreflightParameters( + aUnsafeHeaders, aShouldStripRequestBodyHeader); + } +} + +//----------------------------------------------------------------------------- +// ObliviousHttpChannel::nsIChannel +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +ObliviousHttpChannel::GetOriginalURI(nsIURI** aOriginalURI) { + return mInnerChannel->GetOriginalURI(aOriginalURI); +} + +NS_IMETHODIMP +ObliviousHttpChannel::SetOriginalURI(nsIURI* aOriginalURI) { + return mInnerChannel->SetOriginalURI(aOriginalURI); +} + +NS_IMETHODIMP +ObliviousHttpChannel::GetURI(nsIURI** aURI) { + *aURI = do_AddRef(mTargetURI).take(); + return NS_OK; +} + +NS_IMETHODIMP +ObliviousHttpChannel::GetOwner(nsISupports** aOwner) { + return mInnerChannel->GetOwner(aOwner); +} + +NS_IMETHODIMP +ObliviousHttpChannel::SetOwner(nsISupports* aOwner) { + return mInnerChannel->SetOwner(aOwner); +} + +NS_IMETHODIMP +ObliviousHttpChannel::GetNotificationCallbacks( + nsIInterfaceRequestor** aNotificationCallbacks) { + return mInnerChannel->GetNotificationCallbacks(aNotificationCallbacks); +} + +NS_IMETHODIMP +ObliviousHttpChannel::SetNotificationCallbacks( + nsIInterfaceRequestor* aNotificationCallbacks) { + return mInnerChannel->SetNotificationCallbacks(aNotificationCallbacks); +} + +NS_IMETHODIMP +ObliviousHttpChannel::GetSecurityInfo( + nsITransportSecurityInfo** aSecurityInfo) { + return mInnerChannel->GetSecurityInfo(aSecurityInfo); +} + +NS_IMETHODIMP +ObliviousHttpChannel::GetContentType(nsACString& aContentType) { + return GetResponseHeader("content-type"_ns, aContentType); +} + +NS_IMETHODIMP +ObliviousHttpChannel::SetContentType(const nsACString& aContentType) { + mContentType = aContentType; + return NS_OK; +} + +NS_IMETHODIMP +ObliviousHttpChannel::GetContentCharset(nsACString& aContentCharset) { + return mInnerChannel->GetContentCharset(aContentCharset); +} + +NS_IMETHODIMP +ObliviousHttpChannel::SetContentCharset(const nsACString& aContentCharset) { + return mInnerChannel->SetContentCharset(aContentCharset); +} + +NS_IMETHODIMP +ObliviousHttpChannel::GetContentLength(int64_t* aContentLength) { + return mInnerChannel->GetContentLength(aContentLength); +} + +NS_IMETHODIMP +ObliviousHttpChannel::SetContentLength(int64_t aContentLength) { + return mInnerChannel->SetContentLength(aContentLength); +} + +NS_IMETHODIMP +ObliviousHttpChannel::Open(nsIInputStream** aStream) { + LOG(("ObliviousHttpChannel::Open NOT IMPLEMENTED [this=%p]", this)); + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +ObliviousHttpChannel::AsyncOpen(nsIStreamListener* aListener) { + LOG(("ObliviousHttpChannel::AsyncOpen [this=%p, listener=%p]", this, + aListener)); + mStreamListener = aListener; + nsresult rv = mInnerChannel->SetRequestMethod("POST"_ns); + if (NS_FAILED(rv)) { + return rv; + } + rv = mInnerChannel->SetRequestHeader("Content-Type"_ns, + "message/ohttp-req"_ns, false); + if (NS_FAILED(rv)) { + return rv; + } + nsAutoCString scheme; + if (!mTargetURI) { + return NS_ERROR_NULL_POINTER; + } + rv = mTargetURI->GetScheme(scheme); + if (NS_FAILED(rv)) { + return rv; + } + nsAutoCString authority; + rv = mTargetURI->GetHostPort(authority); + if (NS_FAILED(rv)) { + return rv; + } + nsAutoCString path; + rv = mTargetURI->GetPathQueryRef(path); + if (NS_FAILED(rv)) { + return rv; + } + nsTArray headerNames; + nsTArray headerValues; + for (auto iter = mHeaders.ConstIter(); !iter.Done(); iter.Next()) { + headerNames.AppendElement(iter.Key()); + headerValues.AppendElement(iter.Data()); + } + if (!mContentType.IsEmpty() && !headerNames.Contains("Content-Type")) { + headerNames.AppendElement("Content-Type"_ns); + headerValues.AppendElement(mContentType); + } + nsCOMPtr bhttp( + do_GetService("@mozilla.org/network/binary-http;1")); + nsCOMPtr bhttpRequest(new BinaryHttpRequest( + mMethod, scheme, authority, path, std::move(headerNames), + std::move(headerValues), std::move(mContent))); + nsTArray encodedRequest; + rv = bhttp->EncodeRequest(bhttpRequest, encodedRequest); + if (NS_FAILED(rv)) { + return rv; + } + nsCOMPtr obliviousHttp( + do_GetService("@mozilla.org/network/oblivious-http;1")); + if (!obliviousHttp) { + return NS_ERROR_FAILURE; + } + rv = obliviousHttp->EncapsulateRequest(mEncodedConfig, encodedRequest, + getter_AddRefs(mEncapsulatedRequest)); + if (NS_FAILED(rv)) { + return rv; + } + nsTArray encRequest; + rv = mEncapsulatedRequest->GetEncRequest(encRequest); + if (NS_FAILED(rv)) { + return rv; + } + nsCOMPtr uploadChannel(do_QueryInterface(mInnerChannel)); + if (!uploadChannel) { + return NS_ERROR_UNEXPECTED; + } + nsCOMPtr uploadStream; + uint32_t streamLength = encRequest.Length(); + rv = NS_NewByteInputStream(getter_AddRefs(uploadStream), + std::move(encRequest)); + if (NS_FAILED(rv)) { + return rv; + } + rv = uploadChannel->ExplicitSetUploadStream( + uploadStream, "message/ohttp-req"_ns, streamLength, "POST"_ns, false); + if (NS_FAILED(rv)) { + return rv; + } + return mInnerChannel->AsyncOpen(this); +} + +NS_IMETHODIMP +ObliviousHttpChannel::GetCanceled(bool* aCanceled) { + return mInnerChannel->GetCanceled(aCanceled); +} + +NS_IMETHODIMP +ObliviousHttpChannel::GetContentDisposition(uint32_t* aContentDisposition) { + return mInnerChannel->GetContentDisposition(aContentDisposition); +} + +NS_IMETHODIMP +ObliviousHttpChannel::SetContentDisposition(uint32_t aContentDisposition) { + return mInnerChannel->SetContentDisposition(aContentDisposition); +} + +NS_IMETHODIMP +ObliviousHttpChannel::GetContentDispositionFilename( + nsAString& aContentDispositionFilename) { + return mInnerChannel->GetContentDispositionFilename( + aContentDispositionFilename); +} + +NS_IMETHODIMP +ObliviousHttpChannel::SetContentDispositionFilename( + const nsAString& aContentDispositionFilename) { + return mInnerChannel->SetContentDispositionFilename( + aContentDispositionFilename); +} + +NS_IMETHODIMP +ObliviousHttpChannel::GetContentDispositionHeader( + nsACString& aContentDispositionHeader) { + return mInnerChannel->GetContentDispositionHeader(aContentDispositionHeader); +} + +NS_IMETHODIMP +ObliviousHttpChannel::GetLoadInfo(nsILoadInfo** aLoadInfo) { + return mInnerChannel->GetLoadInfo(aLoadInfo); +} + +NS_IMETHODIMP +ObliviousHttpChannel::SetLoadInfo(nsILoadInfo* aLoadInfo) { + return mInnerChannel->SetLoadInfo(aLoadInfo); +} + +NS_IMETHODIMP +ObliviousHttpChannel::GetIsDocument(bool* aIsDocument) { + return mInnerChannel->GetIsDocument(aIsDocument); +} + +//----------------------------------------------------------------------------- +// ObliviousHttpChannel::nsIStreamListener +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +ObliviousHttpChannel::OnDataAvailable(nsIRequest* aRequest, + nsIInputStream* aStream, uint64_t aOffset, + uint32_t aCount) { + LOG( + ("ObliviousHttpChannel::OnDataAvailable [this=%p, request=%p, stream=%p, " + "offset=%" PRIu64 ", count=%u]", + this, aRequest, aStream, aOffset, aCount)); + MOZ_ASSERT(aOffset == mRawResponse.Length()); + size_t oldLength = mRawResponse.Length(); + size_t newLength = oldLength + aCount; + if (newLength < oldLength) { // i.e., overflow + return NS_ERROR_FAILURE; + } + mRawResponse.SetCapacity(newLength); + mRawResponse.SetLengthAndRetainStorage(newLength); + void* dest = mRawResponse.Elements() + oldLength; + uint64_t written = 0; + nsresult rv = NS_ReadInputStreamToBuffer(aStream, &dest, aCount, &written); + if (NS_FAILED(rv)) { + return rv; + } + if (written != aCount) { + return NS_ERROR_FAILURE; + } + return NS_OK; +} + +//----------------------------------------------------------------------------- +// ObliviousHttpChannel::nsIRequestObserver +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +ObliviousHttpChannel::OnStartRequest(nsIRequest* aRequest) { + LOG(("ObliviousHttpChannel::OnStartRequest [this=%p, request=%p]", this, + aRequest)); + return NS_OK; +} + +nsresult ObliviousHttpChannel::ProcessOnStopRequest() { + if (mRawResponse.IsEmpty()) { + return NS_OK; + } + nsCOMPtr obliviousHttp( + do_GetService("@mozilla.org/network/oblivious-http;1")); + if (!obliviousHttp) { + return NS_ERROR_FAILURE; + } + nsCOMPtr response; + nsresult rv = mEncapsulatedRequest->GetResponse(getter_AddRefs(response)); + if (NS_FAILED(rv)) { + return rv; + } + nsTArray decapsulated; + rv = response->Decapsulate(mRawResponse, decapsulated); + if (NS_FAILED(rv)) { + return rv; + } + nsCOMPtr bhttp( + do_GetService("@mozilla.org/network/binary-http;1")); + if (!bhttp) { + return NS_ERROR_FAILURE; + } + return bhttp->DecodeResponse(decapsulated, + getter_AddRefs(mBinaryHttpResponse)); +} + +void ObliviousHttpChannel::EmitOnDataAvailable() { + if (!mBinaryHttpResponse) { + return; + } + nsTArray content; + nsresult rv = mBinaryHttpResponse->GetContent(content); + if (NS_FAILED(rv)) { + return; + } + if (content.IsEmpty()) { + return; + } + if (content.Length() > std::numeric_limits::max()) { + return; + } + uint32_t contentLength = (uint32_t)content.Length(); + nsCOMPtr contentStream; + rv = NS_NewByteInputStream(getter_AddRefs(contentStream), std::move(content)); + if (NS_FAILED(rv)) { + return; + } + rv = mStreamListener->OnDataAvailable(this, contentStream, 0, contentLength); + Unused << rv; +} + +NS_IMETHODIMP +ObliviousHttpChannel::OnStopRequest(nsIRequest* aRequest, + nsresult aStatusCode) { + LOG(("ObliviousHttpChannel::OnStopRequest [this=%p, request=%p, status=%u]", + this, aRequest, (uint32_t)aStatusCode)); + + auto releaseStreamListener = MakeScopeExit( + [self = RefPtr{this}]() mutable { self->mStreamListener = nullptr; }); + + if (NS_SUCCEEDED(aStatusCode)) { + bool requestSucceeded; + nsresult rv = mInnerChannel->GetRequestSucceeded(&requestSucceeded); + if (NS_SUCCEEDED(rv) && requestSucceeded) { + aStatusCode = ProcessOnStopRequest(); + } + } + Unused << mStreamListener->OnStartRequest(this); + if (NS_SUCCEEDED(aStatusCode)) { + EmitOnDataAvailable(); + } + Unused << mStreamListener->OnStopRequest(this, aStatusCode); + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// ObliviousHttpChannel::nsIObliviousHttpChannel +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +ObliviousHttpChannel::GetRelayChannel(nsIHttpChannel** aChannel) { + if (!aChannel) { + return NS_ERROR_INVALID_ARG; + } + *aChannel = do_AddRef(mInnerChannel).take(); + return NS_OK; +} + +//----------------------------------------------------------------------------- +// ObliviousHttpChannel::nsIUploadChannel2 +//----------------------------------------------------------------------------- + +NS_IMETHODIMP ObliviousHttpChannel::ExplicitSetUploadStream( + nsIInputStream* aStream, const nsACString& aContentType, + int64_t aContentLength, const nsACString& aMethod, bool aStreamHasHeaders) { + // This function should only be called before AsyncOpen. + if (mStreamListener) { + return NS_ERROR_IN_PROGRESS; + } + if (aMethod != "POST"_ns || aStreamHasHeaders) { + return NS_ERROR_INVALID_ARG; + } + mMethod.Assign(aMethod); + uint64_t available; + if (aContentLength < 0) { + nsresult rv = aStream->Available(&available); + if (NS_FAILED(rv)) { + return rv; + } + } else { + available = aContentLength; + } + if (available > std::numeric_limits::max()) { + return NS_ERROR_FAILURE; + } + mContent.SetCapacity(available); + mContent.SetLengthAndRetainStorage(available); + void* dest = mContent.Elements(); + uint64_t written = 0; + nsresult rv = + NS_ReadInputStreamToBuffer(aStream, &dest, (int64_t)available, &written); + if (NS_FAILED(rv)) { + return rv; + } + if (written != available) { + return NS_ERROR_FAILURE; + } + mContentType = aContentType; + return NS_OK; +} + +NS_IMETHODIMP ObliviousHttpChannel::GetUploadStreamHasHeaders( + bool* aUploadStreamHasHeaders) { + *aUploadStreamHasHeaders = false; + return NS_OK; +} + +NS_IMETHODIMP ObliviousHttpChannel::CloneUploadStream( + int64_t* aContentLength, nsIInputStream** _retval) { + LOG(("ObliviousHttpChannel::CloneUploadStream NOT IMPLEMENTED [this=%p]", + this)); + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP ObliviousHttpChannel::SetClassicScriptHintCharset( + const nsAString& aClassicScriptHintCharset) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP ObliviousHttpChannel::GetClassicScriptHintCharset( + nsAString& aClassicScriptHintCharset) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP ObliviousHttpChannel::SetDocumentCharacterSet( + const nsAString& aDocumentCharacterSet) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP ObliviousHttpChannel::GetDocumentCharacterSet( + nsAString& aDocumenharacterSet) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +} // namespace mozilla::net diff --git a/netwerk/protocol/http/ObliviousHttpChannel.h b/netwerk/protocol/http/ObliviousHttpChannel.h new file mode 100644 index 0000000000..9dc9c9f689 --- /dev/null +++ b/netwerk/protocol/http/ObliviousHttpChannel.h @@ -0,0 +1,72 @@ +/* -*- 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_ObliviousHttpChannel_h +#define mozilla_net_ObliviousHttpChannel_h + +#include "nsIBinaryHttp.h" +#include "nsIHttpChannel.h" +#include "nsIHttpChannelInternal.h" +#include "nsIObliviousHttp.h" +#include "nsIObliviousHttpChannel.h" +#include "nsIStreamListener.h" +#include "nsITimedChannel.h" +#include "nsIUploadChannel2.h" +#include "nsTHashMap.h" + +namespace mozilla::net { + +class ObliviousHttpChannel final : public nsIObliviousHttpChannel, + public nsIHttpChannelInternal, + public nsIStreamListener, + public nsIUploadChannel2, + public nsITimedChannel { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSICHANNEL + NS_DECL_NSIHTTPCHANNEL + NS_DECL_NSIOBLIVIOUSHTTPCHANNEL + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMLISTENER + NS_DECL_NSIUPLOADCHANNEL2 + + ObliviousHttpChannel(nsIURI* targetURI, + const nsTArray& encodedConfig, + nsIHttpChannel* innerChannel); + + NS_FORWARD_SAFE_NSIREQUEST(mInnerChannel) + NS_FORWARD_SAFE_NSIIDENTCHANNEL(mInnerChannel) + NS_FORWARD_SAFE_NSIHTTPCHANNELINTERNAL(mInnerChannelInternal) + NS_FORWARD_SAFE_NSITIMEDCHANNEL(mInnerChannelTimed) + + protected: + ~ObliviousHttpChannel(); + + nsresult ProcessOnStopRequest(); + void EmitOnDataAvailable(); + + nsCOMPtr mTargetURI; + nsTArray mEncodedConfig; + + nsCString mMethod{"GET"_ns}; + nsCString mContentType; + nsTHashMap mHeaders; + nsTArray mContent; + + nsCOMPtr mInnerChannel; + nsCOMPtr mInnerChannelInternal; + nsCOMPtr mInnerChannelTimed; + nsCOMPtr mEncapsulatedRequest; + nsTArray mRawResponse; + nsCOMPtr mBinaryHttpResponse; + + nsCOMPtr mStreamListener; +}; + +} // namespace mozilla::net + +#endif // mozilla_net_ObliviousHttpChannel_h diff --git a/netwerk/protocol/http/ObliviousHttpService.cpp b/netwerk/protocol/http/ObliviousHttpService.cpp new file mode 100644 index 0000000000..f1a1cc7b88 --- /dev/null +++ b/netwerk/protocol/http/ObliviousHttpService.cpp @@ -0,0 +1,235 @@ +/* -*- 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 "ObliviousHttpService.h" + +#include "DNSUtils.h" +#include "ObliviousHttpChannel.h" +#include "mozilla/Base64.h" +#include "mozilla/StaticPrefs_network.h" +#include "nsIObserverService.h" +#include "nsIPrefService.h" +#include "nsNetUtil.h" +#include "nsPrintfCString.h" + +namespace mozilla::net { + +NS_IMPL_ISUPPORTS(ObliviousHttpService, nsIObliviousHttpService, nsIObserver, + nsIStreamLoaderObserver) + +ObliviousHttpService::ObliviousHttpService() + : mTRRConfig(ObliviousHttpConfig(), "ObliviousHttpService::mTRRConfig") { + MOZ_ASSERT(NS_IsMainThread()); + + nsCOMPtr prefBranch(do_GetService(NS_PREFSERVICE_CONTRACTID)); + if (prefBranch) { + prefBranch->AddObserver("network.trr.ohttp", this, false); + } + + if (nsCOMPtr obs = + mozilla::services::GetObserverService()) { + obs->AddObserver(this, "xpcom-shutdown", false); + obs->AddObserver(this, "network:captive-portal-connectivity-changed", + false); + obs->AddObserver(this, "network:trr-confirmation", false); + } + + ReadPrefs("*"_ns); +} + +static constexpr nsLiteralCString kTRRohttpConfigURIPref = + "network.trr.ohttp.config_uri"_ns; +static constexpr nsLiteralCString kTRRohttpRelayURIPref = + "network.trr.ohttp.relay_uri"_ns; + +void ObliviousHttpService::FetchConfig(bool aConfigURIChanged) { + auto scopeExit = MakeScopeExit([&] { + nsCOMPtr obs(mozilla::services::GetObserverService()); + if (!obs) { + return; + } + obs->NotifyObservers(nullptr, "ohttp-service-config-loaded", u"no-changes"); + }); + + { + auto trrConfig = mTRRConfig.Lock(); + if (aConfigURIChanged) { + // If the config URI has changed, we need to clear the config. + trrConfig->mEncodedConfig.Clear(); + } else if (trrConfig->mEncodedConfig.Length()) { + // The URI hasn't changed and we already have a config. No need to reload + return; + } + } + + nsAutoCString configURIString; + nsresult rv = + Preferences::GetCString(kTRRohttpConfigURIPref.get(), configURIString); + if (NS_FAILED(rv)) { + return; + } + nsCOMPtr configURI; + rv = NS_NewURI(getter_AddRefs(configURI), configURIString); + if (NS_FAILED(rv)) { + return; + } + + nsCOMPtr channel; + rv = DNSUtils::CreateChannelHelper(configURI, getter_AddRefs(channel)); + if (NS_FAILED(rv)) { + return; + } + rv = channel->SetLoadFlags( + nsIRequest::LOAD_ANONYMOUS | nsIRequest::INHIBIT_CACHING | + nsIRequest::LOAD_BYPASS_CACHE | nsIChannel::LOAD_BYPASS_URL_CLASSIFIER); + if (NS_FAILED(rv)) { + return; + } + nsCOMPtr httpChannel(do_QueryInterface(channel)); + if (!httpChannel) { + return; + } + // This connection should not use TRR + rv = httpChannel->SetTRRMode(nsIRequest::TRR_DISABLED_MODE); + if (NS_FAILED(rv)) { + return; + } + nsCOMPtr loader; + rv = NS_NewStreamLoader(getter_AddRefs(loader), this); + if (NS_FAILED(rv)) { + return; + } + rv = httpChannel->AsyncOpen(loader); + + if (NS_SUCCEEDED(rv)) { + scopeExit.release(); + return; + } + + nsPrintfCString msg( + "ObliviousHttpService::FetchConfig AsyncOpen failed rv=%X", + static_cast(rv)); + NS_WARNING(msg.get()); +} + +void ObliviousHttpService::ReadPrefs(const nsACString& whichPref) { + if (whichPref.Equals(kTRRohttpRelayURIPref) || whichPref.EqualsLiteral("*")) { + nsAutoCString relayURIString; + nsresult rv = + Preferences::GetCString(kTRRohttpRelayURIPref.get(), relayURIString); + if (NS_FAILED(rv)) { + return; + } + nsCOMPtr relayURI; + rv = NS_NewURI(getter_AddRefs(relayURI), relayURIString); + if (NS_FAILED(rv)) { + return; + } + auto trrConfig = mTRRConfig.Lock(); + trrConfig->mRelayURI = relayURI; + } + + if (whichPref.Equals(kTRRohttpConfigURIPref) || + whichPref.EqualsLiteral("*")) { + FetchConfig(true); + } +} + +// nsIObliviousHttpService + +NS_IMETHODIMP +ObliviousHttpService::NewChannel(nsIURI* relayURI, nsIURI* targetURI, + const nsTArray& encodedConfig, + nsIChannel** result) { + nsCOMPtr innerChannel; + nsresult rv = + DNSUtils::CreateChannelHelper(relayURI, getter_AddRefs(innerChannel)); + if (NS_FAILED(rv)) { + return rv; + } + nsCOMPtr innerHttpChannel(do_QueryInterface(innerChannel)); + if (!innerHttpChannel) { + return NS_ERROR_FAILURE; + } + nsCOMPtr obliviousHttpChannel( + new ObliviousHttpChannel(targetURI, encodedConfig, innerHttpChannel)); + obliviousHttpChannel.forget(result); + return NS_OK; +} + +NS_IMETHODIMP +ObliviousHttpService::GetTRRSettings(nsIURI** relayURI, + nsTArray& encodedConfig) { + auto trrConfig = mTRRConfig.Lock(); + *relayURI = do_AddRef(trrConfig->mRelayURI).take(); + encodedConfig.Assign(trrConfig->mEncodedConfig.Clone()); + return NS_OK; +} + +NS_IMETHODIMP +ObliviousHttpService::ClearTRRConfig() { + auto trrConfig = mTRRConfig.Lock(); + trrConfig->mEncodedConfig.Clear(); + return NS_OK; +} + +// nsIObserver + +NS_IMETHODIMP +ObliviousHttpService::Observe(nsISupports* subject, const char* topic, + const char16_t* data) { + if (!strcmp(topic, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID)) { + ReadPrefs(NS_ConvertUTF16toUTF8(data)); + } else if (!strcmp(topic, "xpcom-shutdown")) { + if (nsCOMPtr prefBranch = + do_GetService(NS_PREFSERVICE_CONTRACTID)) { + prefBranch->RemoveObserver("network.trr.ohttp", this); + } + + if (nsCOMPtr obs = + mozilla::services::GetObserverService()) { + obs->RemoveObserver(this, "xpcom-shutdown"); + obs->RemoveObserver(this, "network:captive-portal-connectivity-changed"); + obs->RemoveObserver(this, "network:trr-confirmation"); + } + } else if (!strcmp(topic, "network:captive-portal-connectivity-changed") || + (!strcmp(topic, "network:trr-confirmation") && data && + u"CONFIRM_FAILED"_ns.Equals(data))) { + FetchConfig(false); + } + return NS_OK; +} + +// nsIStreamLoaderObserver + +NS_IMETHODIMP +ObliviousHttpService::OnStreamComplete(nsIStreamLoader* aLoader, + nsISupports* aContext, nsresult aStatus, + uint32_t aLength, + const uint8_t* aContent) { + if (NS_SUCCEEDED(aStatus)) { + auto trrConfig = mTRRConfig.Lock(); + trrConfig->mEncodedConfig.Clear(); + trrConfig->mEncodedConfig.AppendElements(aContent, aLength); + } + + nsCOMPtr observerService( + mozilla::services::GetObserverService()); + if (!observerService) { + return NS_ERROR_FAILURE; + } + nsresult rv = observerService->NotifyObservers( + nullptr, "ohttp-service-config-loaded", + NS_SUCCEEDED(aStatus) ? u"success" : u"failed"); + if (NS_FAILED(rv)) { + return rv; + } + + return NS_OK; +} + +} // namespace mozilla::net diff --git a/netwerk/protocol/http/ObliviousHttpService.h b/netwerk/protocol/http/ObliviousHttpService.h new file mode 100644 index 0000000000..2d5b390072 --- /dev/null +++ b/netwerk/protocol/http/ObliviousHttpService.h @@ -0,0 +1,46 @@ +/* -*- 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_ObliviousHttpService_h +#define mozilla_net_ObliviousHttpService_h + +#include "mozilla/DataMutex.h" +#include "nsCOMPtr.h" +#include "nsIObliviousHttp.h" +#include "nsIObserver.h" +#include "nsIStreamLoader.h" + +namespace mozilla::net { + +class ObliviousHttpConfig { + public: + nsCOMPtr mRelayURI; + nsTArray mEncodedConfig; +}; + +class ObliviousHttpService final : public nsIObliviousHttpService, + public nsIObserver, + public nsIStreamLoaderObserver { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIOBLIVIOUSHTTPSERVICE + NS_DECL_NSIOBSERVER + NS_DECL_NSISTREAMLOADEROBSERVER + + ObliviousHttpService(); + + private: + ~ObliviousHttpService() = default; + void ReadPrefs(const nsACString& whichPref); + void FetchConfig(bool aConfigURIChanged); + + DataMutex mTRRConfig; +}; + +} // namespace mozilla::net + +#endif // mozilla_net_ObliviousHttpService_h diff --git a/netwerk/protocol/http/OpaqueResponseUtils.cpp b/netwerk/protocol/http/OpaqueResponseUtils.cpp new file mode 100644 index 0000000000..6597c0b771 --- /dev/null +++ b/netwerk/protocol/http/OpaqueResponseUtils.cpp @@ -0,0 +1,679 @@ +/* -*- 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/OpaqueResponseUtils.h" + +#include "mozilla/dom/Document.h" +#include "mozilla/StaticPrefs_browser.h" +#include "mozilla/dom/JSValidatorParent.h" +#include "ErrorList.h" +#include "nsContentUtils.h" +#include "nsHttpResponseHead.h" +#include "nsISupports.h" +#include "nsMimeTypes.h" +#include "nsStreamUtils.h" +#include "nsThreadUtils.h" +#include "nsStringStream.h" +#include "HttpBaseChannel.h" + +static mozilla::LazyLogModule gORBLog("ORB"); + +#define LOGORB(msg, ...) \ + MOZ_LOG(gORBLog, LogLevel::Debug, \ + ("%s: %p " msg, __func__, this, ##__VA_ARGS__)) + +namespace mozilla::net { + +static bool IsOpaqueSafeListedMIMEType(const nsACString& aContentType) { + if (aContentType.EqualsLiteral(TEXT_CSS) || + aContentType.EqualsLiteral(IMAGE_SVG_XML)) { + return true; + } + + NS_ConvertUTF8toUTF16 typeString(aContentType); + return nsContentUtils::IsJavascriptMIMEType(typeString); +} + +// These need to be kept in sync with +// "browser.opaqueResponseBlocking.mediaExceptionsStrategy" +enum class OpaqueResponseMediaException { NoExceptions, AllowSome, AllowAll }; + +static OpaqueResponseMediaException ConfiguredMediaExceptionsStrategy() { + uint32_t pref = StaticPrefs:: + browser_opaqueResponseBlocking_mediaExceptionsStrategy_DoNotUseDirectly(); + if (NS_WARN_IF(pref > static_cast( + OpaqueResponseMediaException::AllowAll))) { + return OpaqueResponseMediaException::AllowAll; + } + + return static_cast(pref); +} + +static bool IsOpaqueSafeListedSpecBreakingMIMEType( + const nsACString& aContentType, bool aNoSniff) { + // Avoid trouble with DASH/HLS. See bug 1698040. + if (aContentType.EqualsLiteral(APPLICATION_DASH_XML) || + aContentType.EqualsLiteral(APPLICATION_MPEGURL) || + aContentType.EqualsLiteral(AUDIO_MPEG_URL) || + aContentType.EqualsLiteral(TEXT_VTT)) { + return true; + } + + // Do what Chromium does. This is from bug 1828375, and we should ideally + // revert this. + if (aContentType.EqualsLiteral(TEXT_PLAIN) && aNoSniff) { + return true; + } + + switch (ConfiguredMediaExceptionsStrategy()) { + case OpaqueResponseMediaException::NoExceptions: + break; + case OpaqueResponseMediaException::AllowSome: + if (aContentType.EqualsLiteral(AUDIO_MP3) || + aContentType.EqualsLiteral(AUDIO_AAC) || + aContentType.EqualsLiteral(AUDIO_AACP) || + aContentType.EqualsLiteral(MULTIPART_MIXED_REPLACE)) { + return true; + } + break; + case OpaqueResponseMediaException::AllowAll: + if (StringBeginsWith(aContentType, "audio/"_ns) || + StringBeginsWith(aContentType, "video/"_ns) || + aContentType.EqualsLiteral(MULTIPART_MIXED_REPLACE)) { + return true; + } + break; + } + + return false; +} + +static bool IsOpaqueBlockListedMIMEType(const nsACString& aContentType) { + return aContentType.EqualsLiteral(TEXT_HTML) || + StringEndsWith(aContentType, "+json"_ns) || + aContentType.EqualsLiteral(APPLICATION_JSON) || + aContentType.EqualsLiteral(TEXT_JSON) || + StringEndsWith(aContentType, "+xml"_ns) || + aContentType.EqualsLiteral(APPLICATION_XML) || + aContentType.EqualsLiteral(TEXT_XML); +} + +static bool IsOpaqueBlockListedNeverSniffedMIMEType( + const nsACString& aContentType) { + return aContentType.EqualsLiteral(APPLICATION_GZIP2) || + aContentType.EqualsLiteral(APPLICATION_MSEXCEL) || + aContentType.EqualsLiteral(APPLICATION_MSPPT) || + aContentType.EqualsLiteral(APPLICATION_MSWORD) || + aContentType.EqualsLiteral(APPLICATION_MSWORD_TEMPLATE) || + aContentType.EqualsLiteral(APPLICATION_PDF) || + aContentType.EqualsLiteral(APPLICATION_MPEGURL) || + aContentType.EqualsLiteral(APPLICATION_VND_CES_QUICKPOINT) || + aContentType.EqualsLiteral(APPLICATION_VND_CES_QUICKSHEET) || + aContentType.EqualsLiteral(APPLICATION_VND_CES_QUICKWORD) || + aContentType.EqualsLiteral(APPLICATION_VND_MS_EXCEL) || + aContentType.EqualsLiteral(APPLICATION_VND_MS_EXCEL2) || + aContentType.EqualsLiteral(APPLICATION_VND_MS_PPT) || + aContentType.EqualsLiteral(APPLICATION_VND_MS_PPT2) || + aContentType.EqualsLiteral(APPLICATION_VND_MS_WORD) || + aContentType.EqualsLiteral(APPLICATION_VND_MS_WORD2) || + aContentType.EqualsLiteral(APPLICATION_VND_MS_WORD3) || + aContentType.EqualsLiteral(APPLICATION_VND_MSWORD) || + aContentType.EqualsLiteral( + APPLICATION_VND_PRESENTATIONML_PRESENTATION) || + aContentType.EqualsLiteral(APPLICATION_VND_PRESENTATIONML_TEMPLATE) || + aContentType.EqualsLiteral(APPLICATION_VND_SPREADSHEETML_SHEET) || + aContentType.EqualsLiteral(APPLICATION_VND_SPREADSHEETML_TEMPLATE) || + aContentType.EqualsLiteral( + APPLICATION_VND_WORDPROCESSINGML_DOCUMENT) || + aContentType.EqualsLiteral( + APPLICATION_VND_WORDPROCESSINGML_TEMPLATE) || + aContentType.EqualsLiteral(APPLICATION_VND_PRESENTATION_OPENXML) || + aContentType.EqualsLiteral(APPLICATION_VND_PRESENTATION_OPENXMLM) || + aContentType.EqualsLiteral(APPLICATION_VND_SPREADSHEET_OPENXML) || + aContentType.EqualsLiteral(APPLICATION_VND_WORDPROSSING_OPENXML) || + aContentType.EqualsLiteral(APPLICATION_GZIP) || + aContentType.EqualsLiteral(APPLICATION_XPROTOBUF) || + aContentType.EqualsLiteral(APPLICATION_XPROTOBUFFER) || + aContentType.EqualsLiteral(APPLICATION_ZIP) || + aContentType.EqualsLiteral(AUDIO_MPEG_URL) || + aContentType.EqualsLiteral(MULTIPART_BYTERANGES) || + aContentType.EqualsLiteral(MULTIPART_SIGNED) || + aContentType.EqualsLiteral(TEXT_EVENT_STREAM) || + aContentType.EqualsLiteral(TEXT_CSV) || + aContentType.EqualsLiteral(TEXT_VTT) || + aContentType.EqualsLiteral(APPLICATION_DASH_XML); +} + +OpaqueResponseBlockedReason GetOpaqueResponseBlockedReason( + const nsACString& aContentType, uint16_t aStatus, bool aNoSniff) { + if (aContentType.IsEmpty()) { + return OpaqueResponseBlockedReason::BLOCKED_SHOULD_SNIFF; + } + + if (IsOpaqueSafeListedMIMEType(aContentType)) { + return OpaqueResponseBlockedReason::ALLOWED_SAFE_LISTED; + } + + // For some MIME types we deviate from spec and allow when we ideally + // shouldn't. These are returnened before any blocking takes place. + if (IsOpaqueSafeListedSpecBreakingMIMEType(aContentType, aNoSniff)) { + return OpaqueResponseBlockedReason::ALLOWED_SAFE_LISTED_SPEC_BREAKING; + } + + if (IsOpaqueBlockListedNeverSniffedMIMEType(aContentType)) { + return OpaqueResponseBlockedReason::BLOCKED_BLOCKLISTED_NEVER_SNIFFED; + } + + if (aStatus == 206 && IsOpaqueBlockListedMIMEType(aContentType)) { + return OpaqueResponseBlockedReason::BLOCKED_206_AND_BLOCKLISTED; + } + + nsAutoCString contentTypeOptionsHeader; + if (aNoSniff && (IsOpaqueBlockListedMIMEType(aContentType) || + aContentType.EqualsLiteral(TEXT_PLAIN))) { + return OpaqueResponseBlockedReason:: + BLOCKED_NOSNIFF_AND_EITHER_BLOCKLISTED_OR_TEXTPLAIN; + } + + return OpaqueResponseBlockedReason::BLOCKED_SHOULD_SNIFF; +} + +OpaqueResponseBlockedReason GetOpaqueResponseBlockedReason( + nsHttpResponseHead& aResponseHead) { + nsAutoCString contentType; + aResponseHead.ContentType(contentType); + + nsAutoCString contentTypeOptionsHeader; + bool nosniff = + aResponseHead.GetContentTypeOptionsHeader(contentTypeOptionsHeader) && + contentTypeOptionsHeader.EqualsIgnoreCase("nosniff"); + + return GetOpaqueResponseBlockedReason(contentType, aResponseHead.Status(), + nosniff); +} + +Result, nsresult> +ParseContentRangeHeaderString(const nsAutoCString& aRangeStr) { + // Parse the range header: e.g. Content-Range: bytes 7000-7999/8000. + const int32_t spacePos = aRangeStr.Find(" "_ns); + const int32_t dashPos = aRangeStr.Find("-"_ns, spacePos); + const int32_t slashPos = aRangeStr.Find("/"_ns, dashPos); + + nsAutoCString rangeStartText; + aRangeStr.Mid(rangeStartText, spacePos + 1, dashPos - (spacePos + 1)); + + nsresult rv; + const int64_t rangeStart = rangeStartText.ToInteger64(&rv); + if (NS_FAILED(rv)) { + return Err(rv); + } + if (0 > rangeStart) { + return Err(NS_ERROR_ILLEGAL_VALUE); + } + + nsAutoCString rangeEndText; + aRangeStr.Mid(rangeEndText, dashPos + 1, slashPos - (dashPos + 1)); + const int64_t rangeEnd = rangeEndText.ToInteger64(&rv); + if (NS_FAILED(rv)) { + return Err(rv); + } + if (rangeStart > rangeEnd) { + return Err(NS_ERROR_ILLEGAL_VALUE); + } + + nsAutoCString rangeTotalText; + aRangeStr.Right(rangeTotalText, aRangeStr.Length() - (slashPos + 1)); + if (rangeTotalText[0] == '*') { + return std::make_tuple(rangeStart, rangeEnd, (int64_t)-1); + } + + const int64_t rangeTotal = rangeTotalText.ToInteger64(&rv); + if (NS_FAILED(rv)) { + return Err(rv); + } + if (rangeEnd >= rangeTotal) { + return Err(NS_ERROR_ILLEGAL_VALUE); + } + + return std::make_tuple(rangeStart, rangeEnd, rangeTotal); +} + +bool IsFirstPartialResponse(nsHttpResponseHead& aResponseHead) { + MOZ_ASSERT(aResponseHead.Status() == 206); + + nsAutoCString contentRange; + Unused << aResponseHead.GetHeader(nsHttp::Content_Range, contentRange); + + auto rangeOrErr = ParseContentRangeHeaderString(contentRange); + if (rangeOrErr.isErr()) { + return false; + } + + const int64_t responseFirstBytePos = std::get<0>(rangeOrErr.unwrap()); + return responseFirstBytePos == 0; +} + +LogModule* GetORBLog() { return gORBLog; } + +OpaqueResponseFilter::OpaqueResponseFilter(nsIStreamListener* aNext) + : mNext(aNext) { + LOGORB(); +} + +NS_IMETHODIMP +OpaqueResponseFilter::OnStartRequest(nsIRequest* aRequest) { + LOGORB(); + nsCOMPtr httpBaseChannel = do_QueryInterface(aRequest); + MOZ_ASSERT(httpBaseChannel); + + nsHttpResponseHead* responseHead = httpBaseChannel->GetResponseHead(); + + if (responseHead) { + // Filtered opaque responses doesn't need headers, so we just drop them. + responseHead->ClearHeaders(); + } + + mNext->OnStartRequest(aRequest); + return NS_OK; +} + +NS_IMETHODIMP +OpaqueResponseFilter::OnDataAvailable(nsIRequest* aRequest, + nsIInputStream* aInputStream, + uint64_t aOffset, uint32_t aCount) { + LOGORB(); + uint32_t result; + // No data for filtered opaque responses should reach the content process, so + // we just discard them. + return aInputStream->ReadSegments(NS_DiscardSegment, nullptr, aCount, + &result); +} + +NS_IMETHODIMP +OpaqueResponseFilter::OnStopRequest(nsIRequest* aRequest, + nsresult aStatusCode) { + LOGORB(); + mNext->OnStopRequest(aRequest, aStatusCode); + return NS_OK; +} + +NS_IMPL_ISUPPORTS(OpaqueResponseFilter, nsIStreamListener, nsIRequestObserver) + +OpaqueResponseBlocker::OpaqueResponseBlocker(nsIStreamListener* aNext, + HttpBaseChannel* aChannel, + const nsCString& aContentType, + bool aNoSniff) + : mNext(aNext), mContentType(aContentType), mNoSniff(aNoSniff) { + // Storing aChannel as a member is tricky as aChannel owns us and it's + // hard to ensure aChannel is alive when we about to use it without + // creating a cycle. This is all doable but need some extra efforts. + // + // So we are just passing aChannel from the caller when we need to use it. + MOZ_ASSERT(aChannel); + + if (MOZ_UNLIKELY(MOZ_LOG_TEST(gORBLog, LogLevel::Debug))) { + nsCOMPtr uri; + aChannel->GetURI(getter_AddRefs(uri)); + if (uri) { + LOGORB(" channel=%p, uri=%s", aChannel, uri->GetSpecOrDefault().get()); + } + } + MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess()); + MOZ_DIAGNOSTIC_ASSERT(aChannel->CachedOpaqueResponseBlockingPref()); +} + +NS_IMETHODIMP +OpaqueResponseBlocker::OnStartRequest(nsIRequest* aRequest) { + LOGORB(); + + if (mState == State::Sniffing) { + Unused << EnsureOpaqueResponseIsAllowedAfterSniff(aRequest); + } + + // mState will remain State::Sniffing if we need to wait + // for JS validator to make a decision. + // + // When the state is Sniffing, we can't call mNext->OnStartRequest + // because fetch requests need the cancellation to be done + // before its FetchDriver::OnStartRequest is called, otherwise it'll + // resolve the promise regardless the decision of JS validator. + if (mState != State::Sniffing) { + nsresult rv = mNext->OnStartRequest(aRequest); + return NS_SUCCEEDED(mStatus) ? rv : mStatus; + } + + return NS_OK; +} + +NS_IMETHODIMP +OpaqueResponseBlocker::OnStopRequest(nsIRequest* aRequest, + nsresult aStatusCode) { + LOGORB(); + + nsresult statusForStop = aStatusCode; + + if (mState == State::Blocked && NS_FAILED(mStatus)) { + statusForStop = mStatus; + } + + if (mState == State::Sniffing) { + // It is the call to JSValidatorParent::OnStopRequest that will trigger the + // JS parser. + mStartOfJavaScriptValidation = TimeStamp::Now(); + + MOZ_ASSERT(mJSValidator); + mPendingOnStopRequestStatus = Some(aStatusCode); + mJSValidator->OnStopRequest(aStatusCode, *aRequest); + return NS_OK; + } + + return mNext->OnStopRequest(aRequest, statusForStop); +} + +NS_IMETHODIMP +OpaqueResponseBlocker::OnDataAvailable(nsIRequest* aRequest, + nsIInputStream* aInputStream, + uint64_t aOffset, uint32_t aCount) { + LOGORB(); + + if (mState == State::Allowed) { + return mNext->OnDataAvailable(aRequest, aInputStream, aOffset, aCount); + } + + if (mState == State::Blocked) { + return NS_ERROR_FAILURE; + } + + MOZ_ASSERT(mState == State::Sniffing); + + nsCString data; + if (!data.SetLength(aCount, fallible)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + uint32_t read; + nsresult rv = aInputStream->Read(data.BeginWriting(), aCount, &read); + if (NS_FAILED(rv)) { + return rv; + } + + MOZ_ASSERT(mJSValidator); + + mJSValidator->OnDataAvailable(data); + + return NS_OK; +} + +nsresult OpaqueResponseBlocker::EnsureOpaqueResponseIsAllowedAfterSniff( + nsIRequest* aRequest) { + nsCOMPtr httpBaseChannel = do_QueryInterface(aRequest); + MOZ_ASSERT(httpBaseChannel); + + // The `AfterSniff` check shouldn't be run when + // 1. We have made a decision already + // 2. The JS validator is running, so we should wait + // for its result. + if (mState != State::Sniffing || mJSValidator) { + return NS_OK; + } + + nsCOMPtr loadInfo; + + nsresult rv = + httpBaseChannel->GetLoadInfo(getter_AddRefs(loadInfo)); + if (NS_FAILED(rv)) { + LOGORB("Failed to get LoadInfo"); + BlockResponse(httpBaseChannel, rv); + return rv; + } + + nsCOMPtr uri; + rv = httpBaseChannel->GetURI(getter_AddRefs(uri)); + if (NS_FAILED(rv)) { + LOGORB("Failed to get uri"); + BlockResponse(httpBaseChannel, rv); + return rv; + } + + switch (httpBaseChannel->PerformOpaqueResponseSafelistCheckAfterSniff( + mContentType, mNoSniff)) { + case OpaqueResponse::Block: + BlockResponse(httpBaseChannel, NS_ERROR_FAILURE); + return NS_ERROR_FAILURE; + case OpaqueResponse::Allow: + AllowResponse(); + return NS_OK; + case OpaqueResponse::Sniff: + case OpaqueResponse::SniffCompressed: + break; + } + + MOZ_ASSERT(mState == State::Sniffing); + return ValidateJavaScript(httpBaseChannel, uri, loadInfo); +} + +OpaqueResponse +OpaqueResponseBlocker::EnsureOpaqueResponseIsAllowedAfterJavaScriptValidation( + HttpBaseChannel* aChannel, bool aAllow) { + if (aAllow) { + return OpaqueResponse::Allow; + } + + return aChannel->BlockOrFilterOpaqueResponse( + this, u"Javascript validation failed"_ns, + OpaqueResponseBlockedTelemetryReason::JS_VALIDATION_FAILED, + "Javascript validation failed"); +} + +static void RecordTelemetry(const TimeStamp& aStartOfValidation, + const TimeStamp& aStartOfJavaScriptValidation, + OpaqueResponseBlocker::ValidatorResult aResult) { + using ValidatorResult = OpaqueResponseBlocker::ValidatorResult; + MOZ_DIAGNOSTIC_ASSERT(aStartOfValidation); + + auto key = [aResult]() { + switch (aResult) { + case ValidatorResult::JavaScript: + return "javascript"_ns; + case ValidatorResult::JSON: + return "json"_ns; + case ValidatorResult::Other: + return "other"_ns; + case ValidatorResult::Failure: + return "failure"_ns; + } + MOZ_ASSERT_UNREACHABLE("Switch statement should be saturated"); + return "failure"_ns; + }(); + + TimeStamp now = TimeStamp::Now(); + PROFILER_MARKER_TEXT( + "ORB safelist check", NETWORK, + MarkerTiming::Interval(aStartOfValidation, aStartOfJavaScriptValidation), + nsPrintfCString("Receive data for validation (%s)", key.get())); + + PROFILER_MARKER_TEXT( + "ORB safelist check", NETWORK, + MarkerTiming::Interval(aStartOfJavaScriptValidation, now), + nsPrintfCString("JS Validation (%s)", key.get())); + + Telemetry::AccumulateTimeDelta(Telemetry::ORB_RECEIVE_DATA_FOR_VALIDATION_MS, + key, aStartOfValidation, + aStartOfJavaScriptValidation); + + Telemetry::AccumulateTimeDelta(Telemetry::ORB_JAVASCRIPT_VALIDATION_MS, key, + aStartOfJavaScriptValidation, now); +} + +// The specification for ORB is currently being written: +// https://whatpr.org/fetch/1442.html#orb-algorithm +// The `opaque-response-safelist check` is implemented in: +// * `HttpBaseChannel::OpaqueResponseSafelistCheckBeforeSniff` +// * `nsHttpChannel::DisableIsOpaqueResponseAllowedAfterSniffCheck` +// * `HttpBaseChannel::OpaqueResponseSafelistCheckAfterSniff` +// * `OpaqueResponseBlocker::ValidateJavaScript` +nsresult OpaqueResponseBlocker::ValidateJavaScript(HttpBaseChannel* aChannel, + nsIURI* aURI, + nsILoadInfo* aLoadInfo) { + MOZ_DIAGNOSTIC_ASSERT(aChannel); + MOZ_ASSERT(aURI && aLoadInfo); + + if (!StaticPrefs::browser_opaqueResponseBlocking_javascriptValidator()) { + LOGORB("Allowed: JS Validator is disabled"); + AllowResponse(); + return NS_OK; + } + + int64_t contentLength; + nsresult rv = aChannel->GetContentLength(&contentLength); + if (NS_FAILED(rv)) { + LOGORB("Blocked: No Content Length"); + BlockResponse(aChannel, rv); + return rv; + } + + Telemetry::ScalarAdd( + Telemetry::ScalarID::OPAQUE_RESPONSE_BLOCKING_JAVASCRIPT_VALIDATION_COUNT, + 1); + + LOGORB("Send %s to the validator", aURI->GetSpecOrDefault().get()); + // https://whatpr.org/fetch/1442.html#orb-algorithm, step 15 + mJSValidator = dom::JSValidatorParent::Create(); + mJSValidator->IsOpaqueResponseAllowed( + [self = RefPtr{this}, channel = nsCOMPtr{aChannel}, uri = nsCOMPtr{aURI}, + loadInfo = nsCOMPtr{aLoadInfo}, startOfValidation = TimeStamp::Now()]( + Maybe aSharedData, ValidatorResult aResult) { + MOZ_LOG(gORBLog, LogLevel::Debug, + ("JSValidator resolved for %s with %s", + uri->GetSpecOrDefault().get(), + aSharedData.isSome() ? "true" : "false")); + bool allowed = aResult == ValidatorResult::JavaScript; + switch (self->EnsureOpaqueResponseIsAllowedAfterJavaScriptValidation( + channel, allowed)) { + case OpaqueResponse::Allow: + // It's possible that the JS validation failed for this request, + // however we decided that we need to filter the response instead + // of blocking. So we set allowed to true manually when that's the + // case. + allowed = true; + self->AllowResponse(); + break; + case OpaqueResponse::Block: + // We'll filter the data out later + self->AllowResponse(); + break; + default: + MOZ_ASSERT_UNREACHABLE( + "We should only ever have Allow or Block here."); + allowed = false; + self->BlockResponse(channel, NS_ERROR_FAILURE); + break; + } + + self->ResolveAndProcessData(channel, allowed, aSharedData); + if (aSharedData.isSome()) { + self->mJSValidator->DeallocShmem(aSharedData.ref()); + } + + RecordTelemetry(startOfValidation, self->mStartOfJavaScriptValidation, + aResult); + + Unused << dom::PJSValidatorParent::Send__delete__(self->mJSValidator); + self->mJSValidator = nullptr; + }); + + return NS_OK; +} + +bool OpaqueResponseBlocker::IsSniffing() const { + return mState == State::Sniffing; +} + +void OpaqueResponseBlocker::AllowResponse() { + LOGORB("Sniffer is done, allow response, this=%p", this); + MOZ_ASSERT(mState == State::Sniffing); + mState = State::Allowed; +} + +void OpaqueResponseBlocker::BlockResponse(HttpBaseChannel* aChannel, + nsresult aStatus) { + LOGORB("Sniffer is done, block response, this=%p", this); + MOZ_ASSERT(mState == State::Sniffing); + mState = State::Blocked; + mStatus = aStatus; + aChannel->SetChannelBlockedByOpaqueResponse(); + aChannel->CancelWithReason(mStatus, + "OpaqueResponseBlocker::BlockResponse"_ns); +} + +void OpaqueResponseBlocker::FilterResponse() { + MOZ_ASSERT(mState == State::Sniffing); + + if (mShouldFilter) { + return; + } + + mShouldFilter = true; + + mNext = new OpaqueResponseFilter(mNext); +} + +void OpaqueResponseBlocker::ResolveAndProcessData( + HttpBaseChannel* aChannel, bool aAllowed, Maybe& aSharedData) { + if (!aAllowed) { + // OpaqueResponseFilter allows us to filter the headers + mNext = new OpaqueResponseFilter(mNext); + } + + nsresult rv = OnStartRequest(aChannel); + + if (!aAllowed || NS_FAILED(rv)) { + MOZ_ASSERT_IF(!aAllowed, mState == State::Allowed); + // No need to call OnDataAvailable because + // 1. The input stream is consumed by + // OpaqueResponseBlocker::OnDataAvailable already + // 2. We don't want to pass any data over + MaybeRunOnStopRequest(aChannel); + return; + } + + MOZ_ASSERT(mState == State::Allowed); + + if (aSharedData.isNothing()) { + MaybeRunOnStopRequest(aChannel); + return; + } + + const ipc::Shmem& mem = aSharedData.ref(); + nsCOMPtr input; + rv = NS_NewByteInputStream(getter_AddRefs(input), + Span(mem.get(), mem.Size()), + NS_ASSIGNMENT_DEPEND); + + if (NS_WARN_IF(NS_FAILED(rv))) { + BlockResponse(aChannel, rv); + MaybeRunOnStopRequest(aChannel); + return; + } + + // When this line reaches, the state is either State::Allowed or + // State::Blocked. The OnDataAvailable call will either call + // the next listener or reject the request. + OnDataAvailable(aChannel, input, 0, mem.Size()); + + MaybeRunOnStopRequest(aChannel); +} + +void OpaqueResponseBlocker::MaybeRunOnStopRequest(HttpBaseChannel* aChannel) { + MOZ_ASSERT(mState != State::Sniffing); + if (mPendingOnStopRequestStatus.isSome()) { + OnStopRequest(aChannel, mPendingOnStopRequestStatus.value()); + } +} + +NS_IMPL_ISUPPORTS(OpaqueResponseBlocker, nsIStreamListener, nsIRequestObserver) + +} // namespace mozilla::net diff --git a/netwerk/protocol/http/OpaqueResponseUtils.h b/netwerk/protocol/http/OpaqueResponseUtils.h new file mode 100644 index 0000000000..7b37095b01 --- /dev/null +++ b/netwerk/protocol/http/OpaqueResponseUtils.h @@ -0,0 +1,216 @@ +/* -*- 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_OpaqueResponseUtils_h +#define mozilla_net_OpaqueResponseUtils_h + +#include "ipc/EnumSerializer.h" +#include "mozilla/TimeStamp.h" +#include "nsIContentPolicy.h" +#include "nsIEncodedChannel.h" +#include "nsIStreamListener.h" +#include "nsUnknownDecoder.h" +#include "nsMimeTypes.h" +#include "nsIHttpChannel.h" + +#include "mozilla/Variant.h" +#include "mozilla/Logging.h" + +#include "nsCOMPtr.h" +#include "nsString.h" +#include "nsTArray.h" + +class nsIContentSniffer; + +namespace mozilla::dom { +class JSValidatorParent; +} + +namespace mozilla::ipc { +class Shmem; +} + +namespace mozilla::net { + +class HttpBaseChannel; +class nsHttpResponseHead; + +enum class OpaqueResponseBlockedReason : uint32_t { + ALLOWED_SAFE_LISTED, + ALLOWED_SAFE_LISTED_SPEC_BREAKING, + BLOCKED_BLOCKLISTED_NEVER_SNIFFED, + BLOCKED_206_AND_BLOCKLISTED, + BLOCKED_NOSNIFF_AND_EITHER_BLOCKLISTED_OR_TEXTPLAIN, + BLOCKED_SHOULD_SNIFF +}; + +enum class OpaqueResponseBlockedTelemetryReason : uint32_t { + MIME_NEVER_SNIFFED, + RESP_206_BLCLISTED, + NOSNIFF_BLC_OR_TEXTP, + RESP_206_NO_FIRST, + AFTER_SNIFF_MEDIA, + AFTER_SNIFF_NOSNIFF, + AFTER_SNIFF_STA_CODE, + AFTER_SNIFF_CT_FAIL, + MEDIA_NOT_INITIAL, + MEDIA_INCORRECT_RESP, + JS_VALIDATION_FAILED +}; + +enum class OpaqueResponse { Block, Allow, SniffCompressed, Sniff }; + +OpaqueResponseBlockedReason GetOpaqueResponseBlockedReason( + const nsACString& aContentType, uint16_t aStatus, bool aNoSniff); + +OpaqueResponseBlockedReason GetOpaqueResponseBlockedReason( + nsHttpResponseHead& aResponseHead); + +// Returns a tuple of (rangeStart, rangeEnd, rangeTotal) from the input range +// header string if succeed. +Result, nsresult> +ParseContentRangeHeaderString(const nsAutoCString& aRangeStr); + +bool IsFirstPartialResponse(nsHttpResponseHead& aResponseHead); + +LogModule* GetORBLog(); + +// Helper class to filter data for opaque responses destined for `Window.fetch`. +// See https://fetch.spec.whatwg.org/#concept-filtered-response-opaque. +class OpaqueResponseFilter final : public nsIStreamListener { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMLISTENER; + + explicit OpaqueResponseFilter(nsIStreamListener* aNext); + + private: + virtual ~OpaqueResponseFilter() = default; + + nsCOMPtr mNext; +}; + +class OpaqueResponseBlocker final : public nsIStreamListener { + enum class State { Sniffing, Allowed, Blocked }; + + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMLISTENER; + + OpaqueResponseBlocker(nsIStreamListener* aNext, HttpBaseChannel* aChannel, + const nsCString& aContentType, bool aNoSniff); + + bool IsSniffing() const; + void AllowResponse(); + void BlockResponse(HttpBaseChannel* aChannel, nsresult aStatus); + void FilterResponse(); + + nsresult EnsureOpaqueResponseIsAllowedAfterSniff(nsIRequest* aRequest); + + OpaqueResponse EnsureOpaqueResponseIsAllowedAfterJavaScriptValidation( + HttpBaseChannel* aChannel, bool aAllow); + + // The four possible results for validation. `JavaScript` and `JSON` are + // self-explanatory. `JavaScript` is the only successful result, in the sense + // that it will allow the opaque response, whereas `JSON` will block. `Other` + // is the case where validation fails, because the response is neither + // `JavaScript` nor `JSON`, but the framework itself works as intended. + // `Failure` implies that something has gone wrong, such as allocation, etc. + enum class ValidatorResult : uint32_t { JavaScript, JSON, Other, Failure }; + + private: + virtual ~OpaqueResponseBlocker() = default; + + nsresult ValidateJavaScript(HttpBaseChannel* aChannel, nsIURI* aURI, + nsILoadInfo* aLoadInfo); + + void ResolveAndProcessData(HttpBaseChannel* aChannel, bool aAllowed, + Maybe& aSharedData); + + void MaybeRunOnStopRequest(HttpBaseChannel* aChannel); + + nsCOMPtr mNext; + + const nsCString mContentType; + const bool mNoSniff; + bool mShouldFilter = false; + + State mState = State::Sniffing; + nsresult mStatus = NS_OK; + + TimeStamp mStartOfJavaScriptValidation; + + RefPtr mJSValidator; + + Maybe mPendingOnStopRequestStatus{Nothing()}; +}; + +class nsCompressedAudioVideoImageDetector : public nsUnknownDecoder { + const std::function mCallback; + + public: + nsCompressedAudioVideoImageDetector( + nsIStreamListener* aListener, + std::function&& aCallback) + : nsUnknownDecoder(aListener), mCallback(aCallback) {} + + protected: + virtual void DetermineContentType(nsIRequest* aRequest) override { + nsCOMPtr httpChannel = do_QueryInterface(aRequest); + if (!httpChannel) { + return; + } + + const char* testData = mBuffer; + uint32_t testDataLen = mBufferLen; + // Check if data are compressed. + nsAutoCString decodedData; + + // ConvertEncodedData is always called only on a single thread for each + // instance of an object. + nsresult rv = ConvertEncodedData(aRequest, mBuffer, mBufferLen); + if (NS_SUCCEEDED(rv)) { + MutexAutoLock lock(mMutex); + decodedData = mDecodedData; + } + if (!decodedData.IsEmpty()) { + testData = decodedData.get(); + testDataLen = std::min(decodedData.Length(), 512u); + } + + mCallback(httpChannel, (const uint8_t*)testData, testDataLen); + + nsAutoCString contentType; + rv = httpChannel->GetContentType(contentType); + + MutexAutoLock lock(mMutex); + if (!contentType.IsEmpty()) { + mContentType = contentType; + } else { + mContentType = UNKNOWN_CONTENT_TYPE; + } + + nsCOMPtr encodedChannel = do_QueryInterface(httpChannel); + if (encodedChannel) { + encodedChannel->SetHasContentDecompressed(true); + } + } +}; +} // namespace mozilla::net + +namespace IPC { +template <> +struct ParamTraits + : public ContiguousEnumSerializerInclusive< + mozilla::net::OpaqueResponseBlocker::ValidatorResult, + mozilla::net::OpaqueResponseBlocker::ValidatorResult::JavaScript, + mozilla::net::OpaqueResponseBlocker::ValidatorResult::Failure> {}; +} // namespace IPC + +#endif // mozilla_net_OpaqueResponseUtils_h diff --git a/netwerk/protocol/http/PAltDataOutputStream.ipdl b/netwerk/protocol/http/PAltDataOutputStream.ipdl new file mode 100644 index 0000000000..9fbf589e95 --- /dev/null +++ b/netwerk/protocol/http/PAltDataOutputStream.ipdl @@ -0,0 +1,42 @@ +/* -*- 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 { + +[ManualDealloc] +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..c4b4f494b5 --- /dev/null +++ b/netwerk/protocol/http/PAltService.ipdl @@ -0,0 +1,38 @@ +/* -*- 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 { + +protocol PAltService +{ + manager PSocketProcess; + +parent: + async ClearHostMapping(nsCString host, int32_t port, + OriginAttributes originAttributes); + + async ProcessHeader(nsCString buf, + nsCString originScheme, + nsCString originHost, + int32_t originPort, + nsCString username, + bool privateBrowsing, + 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..001b53a4c7 --- /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 { + +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..d5a3be4fab --- /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 HttpChannelParams; +include NeckoChannelParams; + +namespace mozilla { +namespace net { + +//------------------------------------------------------------------- +[ParentProc=Socket, ChildProc=Content] +async protocol PBackgroundDataBridge +{ +child: + async OnTransportAndData(uint64_t offset, + uint32_t count, + nsCString data, + TimeStamp onDataAvailableStart); + + async OnStopRequest(nsresult aStatus, + ResourceTimingStructArgs timing, + TimeStamp lastActiveTabOptimization, + nsHttpHeaderArray responseTrailers, + TimeStamp onStopRequestStart); + +both: + async __delete__(); +}; + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/PHttpBackgroundChannel.ipdl b/netwerk/protocol/http/PHttpBackgroundChannel.ipdl new file mode 100644 index 0000000000..1f930992a9 --- /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"; + +namespace mozilla { +namespace net { + +//------------------------------------------------------------------- +async protocol PHttpBackgroundChannel +{ + manager PBackground; + +child: + async OnStartRequest(nsHttpResponseHead responseHead, + bool useResponseHead, + nsHttpHeaderArray requestHeaders, + HttpChannelOnStartRequestArgs args, + HttpChannelAltDataStream altData, + TimeStamp onStartRequestStart); + + // 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, + nsCString data, + bool dataFromSocketProcess, + TimeStamp onDataAvailableStart); + + + async OnStopRequest(nsresult channelStatus, + ResourceTimingStructArgs timing, + TimeStamp lastActiveTabOptimization, + nsHttpHeaderArray responseTrailers, + ConsoleReportCollected[] consoleReport, + bool fromSocketProcess, + TimeStamp onStopRequestStart); + + 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 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 DetachStreamFilters(); + + 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..76243a0635 --- /dev/null +++ b/netwerk/protocol/http/PHttpChannel.ipdl @@ -0,0 +1,147 @@ +/* -*- 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 PBackgroundSharedTypes; +include NeckoChannelParams; +include IPCServiceWorkerDescriptor; +include IPCStream; +include HttpChannelParams; + +include "mozilla/dom/ReferrerInfoUtils.h"; +include "mozilla/net/NeckoMessageUtils.h"; +include "mozilla/net/ClassOfService.h"; + +using mozilla::OriginAttributes from "mozilla/ipc/BackgroundUtils.h"; +using mozilla::net::ClassOfService from "mozilla/net/ClassOfService.h"; +[RefCounted] using class nsIURI from "mozilla/ipc/URIUtils.h"; + +namespace mozilla { +namespace net { + +//------------------------------------------------------------------- +[ChildImpl=virtual, ParentImpl=virtual] +protocol PHttpChannel +{ + manager PNecko; + +parent: + // Note: channels are opened during construction, so no open method here: + // see PNecko.ipdl + + async SetClassOfService(ClassOfService cos); + + async Suspend(); + async Resume(); + + async Cancel(nsresult status, uint32_t requestBlockingReason, + nsCString aReason, nsCString? logString); + + // Reports approval/veto of redirect by child process redirect observers + async Redirect2Verify(nsresult result, RequestHeaderTuples changedHeaders, + uint32_t sourceRequestBlockingReason, + ChildLoadInfoForwarderArgs? targetLoadInfoForwarder, + uint32_t loadFlags, + nullable nsIReferrerInfo referrerInfo, + nullable nsIURI apiRedirectTo, + CorsPreflightArgs? corsPreflightArgs); + + // 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); + + // 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(nullable nsIURI uri, + PrincipalInfo requestingPrincipal, + OriginAttributes originAttributes); + + // Send cookies to the parent for a given channel. Compared to PCookieService + // SetCookies this method allows the parent to determine which BrowsingContext + // the request was sent for. + async SetCookies(nsCString baseDomain, + OriginAttributes attrs, + nullable nsIURI host, + bool fromHttp, + CookieStruct[] cookies); + + // 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(); + + // 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, + nullable nsIURI newOriginalUri, + uint32_t newLoadFlags, + uint32_t redirectFlags, + ParentLoadInfoForwarderArgs loadInfoForwarder, + nsHttpResponseHead responseHead, + nullable nsITransportSecurityInfo securityInfo, + uint64_t channelId, + NetAddr oldPeerAddr, + ResourceTimingStructArgs timing); + + // Called if redirect successful so that child can complete setup. + async Redirect3Complete(); + + // Called if the redirect failed/was vetoed + async RedirectFailed(nsresult status); + + // 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(); + + // 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, + bool isWarning); + + async LogMimeTypeMismatch(nsCString messageName, + bool warning, + nsString url, + nsString contentType); + + async OriginalCacheInputStreamAvailable(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..8e2840db68 --- /dev/null +++ b/netwerk/protocol/http/PHttpChannelParams.h @@ -0,0 +1,290 @@ +/* -*- 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" + +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(MessageWriter* aWriter, const paramType& aParam) { + WriteParam(aWriter, aParam.mHeader); + WriteParam(aWriter, aParam.mValue); + WriteParam(aWriter, aParam.mMerge); + WriteParam(aWriter, aParam.mEmpty); + } + + static bool Read(MessageReader* aReader, paramType* aResult) { + if (!ReadParam(aReader, &aResult->mHeader) || + !ReadParam(aReader, &aResult->mValue) || + !ReadParam(aReader, &aResult->mMerge) || + !ReadParam(aReader, &aResult->mEmpty)) + return false; + + return true; + } +}; + +template <> +struct ParamTraits { + typedef mozilla::net::nsHttpAtom paramType; + + static void Write(MessageWriter* aWriter, const paramType& aParam) { + // aParam.get() cannot be null. + MOZ_ASSERT(aParam.get(), "null nsHTTPAtom value"); + nsAutoCString value(aParam.get()); + WriteParam(aWriter, value); + } + + static bool Read(MessageReader* aReader, paramType* aResult) { + nsAutoCString value; + if (!ReadParam(aReader, &value)) return false; + + *aResult = mozilla::net::nsHttp::ResolveAtom(value); + MOZ_ASSERT(aResult->get(), "atom table not initialized"); + return true; + } +}; + +template <> +struct ParamTraits { + typedef mozilla::net::nsHttpHeaderArray::nsEntry paramType; + + static void Write(MessageWriter* aWriter, const paramType& aParam) { + if (aParam.headerNameOriginal.IsEmpty()) { + WriteParam(aWriter, aParam.header); + } else { + WriteParam(aWriter, aParam.headerNameOriginal); + } + WriteParam(aWriter, aParam.value); + switch (aParam.variety) { + case mozilla::net::nsHttpHeaderArray::eVarietyUnknown: + WriteParam(aWriter, (uint8_t)0); + break; + case mozilla::net::nsHttpHeaderArray::eVarietyRequestOverride: + WriteParam(aWriter, (uint8_t)1); + break; + case mozilla::net::nsHttpHeaderArray::eVarietyRequestDefault: + WriteParam(aWriter, (uint8_t)2); + break; + case mozilla::net::nsHttpHeaderArray::eVarietyRequestEnforceDefault: + WriteParam(aWriter, (uint8_t)3); + break; + case mozilla::net::nsHttpHeaderArray:: + eVarietyResponseNetOriginalAndResponse: + WriteParam(aWriter, (uint8_t)4); + break; + case mozilla::net::nsHttpHeaderArray::eVarietyResponseNetOriginal: + WriteParam(aWriter, (uint8_t)5); + break; + case mozilla::net::nsHttpHeaderArray::eVarietyResponse: + WriteParam(aWriter, (uint8_t)6); + } + } + + static bool Read(MessageReader* aReader, paramType* aResult) { + uint8_t variety; + nsAutoCString header; + if (!ReadParam(aReader, &header) || !ReadParam(aReader, &aResult->value) || + !ReadParam(aReader, &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::eVarietyRequestEnforceDefault; + break; + case 4: + aResult->variety = mozilla::net::nsHttpHeaderArray:: + eVarietyResponseNetOriginalAndResponse; + break; + case 5: + aResult->variety = + mozilla::net::nsHttpHeaderArray::eVarietyResponseNetOriginal; + break; + case 6: + aResult->variety = mozilla::net::nsHttpHeaderArray::eVarietyResponse; + break; + default: + return false; + } + + return true; + } +}; + +template <> +struct ParamTraits { + typedef mozilla::net::nsHttpHeaderArray paramType; + + static void Write(MessageWriter* aWriter, const paramType& aParam) { + paramType& p = const_cast(aParam); + + WriteParam(aWriter, p.mHeaders); + } + + static bool Read(MessageReader* aReader, paramType* aResult) { + if (!ReadParam(aReader, &aResult->mHeaders)) return false; + + return true; + } +}; + +template <> +struct ParamTraits { + typedef mozilla::net::nsHttpRequestHead paramType; + + static void Write(MessageWriter* aWriter, + const paramType& aParam) MOZ_NO_THREAD_SAFETY_ANALYSIS { + aParam.Enter(); + WriteParam(aWriter, aParam.mHeaders); + WriteParam(aWriter, aParam.mMethod); + WriteParam(aWriter, static_cast(aParam.mVersion)); + WriteParam(aWriter, aParam.mRequestURI); + WriteParam(aWriter, aParam.mPath); + WriteParam(aWriter, aParam.mOrigin); + WriteParam(aWriter, static_cast(aParam.mParsedMethod)); + WriteParam(aWriter, aParam.mHTTPS); + aParam.Exit(); + } + + static bool Read(MessageReader* aReader, paramType* aResult) { + uint32_t version; + uint8_t method; + aResult->Enter(); + if (!ReadParam(aReader, &aResult->mHeaders) || + !ReadParam(aReader, &aResult->mMethod) || + !ReadParam(aReader, &version) || + !ReadParam(aReader, &aResult->mRequestURI) || + !ReadParam(aReader, &aResult->mPath) || + !ReadParam(aReader, &aResult->mOrigin) || + !ReadParam(aReader, &method) || !ReadParam(aReader, &aResult->mHTTPS)) { + aResult->Exit(); + return false; + } + + aResult->mVersion = static_cast(version); + aResult->mParsedMethod = + static_cast(method); + aResult->Exit(); + 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(MessageWriter* aWriter, + const paramType& aParam) MOZ_NO_THREAD_SAFETY_ANALYSIS { + aParam.Enter(); + WriteParam(aWriter, aParam.mHeaders); + WriteParam(aWriter, static_cast(aParam.mVersion)); + WriteParam(aWriter, aParam.mStatus); + WriteParam(aWriter, aParam.mStatusText); + WriteParam(aWriter, aParam.mContentLength); + WriteParam(aWriter, aParam.mContentType); + WriteParam(aWriter, aParam.mContentCharset); + WriteParam(aWriter, aParam.mHasCacheControl); + WriteParam(aWriter, aParam.mCacheControlPublic); + WriteParam(aWriter, aParam.mCacheControlPrivate); + WriteParam(aWriter, aParam.mCacheControlNoStore); + WriteParam(aWriter, aParam.mCacheControlNoCache); + WriteParam(aWriter, aParam.mCacheControlImmutable); + WriteParam(aWriter, aParam.mCacheControlStaleWhileRevalidateSet); + WriteParam(aWriter, aParam.mCacheControlStaleWhileRevalidate); + WriteParam(aWriter, aParam.mCacheControlMaxAgeSet); + WriteParam(aWriter, aParam.mCacheControlMaxAge); + WriteParam(aWriter, aParam.mPragmaNoCache); + aParam.Exit(); + } + + static bool Read(MessageReader* aReader, paramType* aResult) { + uint32_t version; + aResult->Enter(); + if (!ReadParam(aReader, &aResult->mHeaders) || + !ReadParam(aReader, &version) || + !ReadParam(aReader, &aResult->mStatus) || + !ReadParam(aReader, &aResult->mStatusText) || + !ReadParam(aReader, &aResult->mContentLength) || + !ReadParam(aReader, &aResult->mContentType) || + !ReadParam(aReader, &aResult->mContentCharset) || + !ReadParam(aReader, &aResult->mHasCacheControl) || + !ReadParam(aReader, &aResult->mCacheControlPublic) || + !ReadParam(aReader, &aResult->mCacheControlPrivate) || + !ReadParam(aReader, &aResult->mCacheControlNoStore) || + !ReadParam(aReader, &aResult->mCacheControlNoCache) || + !ReadParam(aReader, &aResult->mCacheControlImmutable) || + !ReadParam(aReader, &aResult->mCacheControlStaleWhileRevalidateSet) || + !ReadParam(aReader, &aResult->mCacheControlStaleWhileRevalidate) || + !ReadParam(aReader, &aResult->mCacheControlMaxAgeSet) || + !ReadParam(aReader, &aResult->mCacheControlMaxAge) || + !ReadParam(aReader, &aResult->mPragmaNoCache)) { + aResult->Exit(); + return false; + } + + aResult->mVersion = static_cast(version); + aResult->Exit(); + 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..dfd46bc3cb --- /dev/null +++ b/netwerk/protocol/http/PHttpConnectionMgr.ipdl @@ -0,0 +1,46 @@ +/* -*- 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; + +include "mozilla/net/ClassOfService.h"; + +using mozilla::net::ClassOfService from "mozilla/net/ClassOfService.h"; + +namespace mozilla { +namespace net { + +async protocol PHttpConnectionMgr +{ + manager PSocketProcess; + +child: + async __delete__(); + + async DoShiftReloadConnectionCleanupWithConnInfo(HttpConnectionInfoCloneArgs aArgs); + async UpdateCurrentBrowserId(uint64_t aId); + 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, + ClassOfService aClassOfService); + async CancelTransaction(PHttpTransaction aTrans, nsresult aReason); + async SpeculativeConnect(HttpConnectionInfoCloneArgs aConnInfo, + SpeculativeConnectionOverriderArgs? aOverriderArgs, + uint32_t aCaps, PAltSvcTransaction? aTrans, + bool aFetchHTTPSRR); + async StartWebSocketConnection(PHttpTransaction aTransWithStickyConn, + uint32_t aListenerId); +}; + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/PHttpTransaction.ipdl b/netwerk/protocol/http/PHttpTransaction.ipdl new file mode 100644 index 0000000000..670dc296ce --- /dev/null +++ b/netwerk/protocol/http/PHttpTransaction.ipdl @@ -0,0 +1,119 @@ +/* -*- 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 PInputChannelThrottleQueue; + +include IPCStream; +include NeckoChannelParams; + +include "mozilla/ipc/TransportSecurityInfoUtils.h"; +include "mozilla/net/NeckoMessageUtils.h"; +include "mozilla/net/ClassOfService.h"; + +using class mozilla::net::nsHttpRequestHead from "nsHttpRequestHead.h"; +using class mozilla::net::nsHttpHeaderArray from "nsHttpHeaderArray.h"; +using mozilla::net::NetAddr from "mozilla/net/DNS.h"; +using mozilla::net::ClassOfService from "mozilla/net/ClassOfService.h"; +[RefCounted] using class nsITransportSecurityInfo from "nsITransportSecurityInfo.h"; +using mozilla::net::TRRSkippedReason from "nsITRRSkipReason.h"; +using nsIRequest::TRRMode from "nsIRequest.h"; + +namespace mozilla { +namespace net { + +struct H2PushedStreamArg { + PHttpTransaction transWithPushedStream; + uint32_t pushedStreamId; +}; + +struct NetworkAddressArg { + NetAddr selfAddr; + NetAddr peerAddr; + bool resolvedByTRR; + TRRMode mode; + TRRSkippedReason trrSkipReason; + bool echConfigUsed; +}; + +protocol PHttpTransaction +{ + manager PSocketProcess; + +parent: + async OnStartRequest(nsresult status, + nsHttpResponseHead? responseHead, + nullable nsITransportSecurityInfo securityInfo, + bool proxyConnectFailed, + TimingStructArgs timings, + int32_t proxyConnectResponseCode, + uint8_t[] dataForSniffer, + nsCString? altSvcUsed, + bool dataToChildProcess, + bool restarted, + uint32_t HTTPSSVCReceivedStage, + bool supportsHttp3, + TRRMode trrMode, + TRRSkippedReason trrSkipReason, + uint32_t caps, + TimeStamp onStartRequestStart); + async OnTransportStatus(nsresult status, + int64_t progress, + int64_t progressMax, + NetworkAddressArg? networkAddressArg); + async OnDataAvailable(nsCString data, + uint64_t offset, + uint32_t count, + TimeStamp onDataAvailableStart); + async OnStopRequest(nsresult status, + bool responseIsComplete, + int64_t transferSize, + TimingStructArgs timings, + nsHttpHeaderArray? responseTrailers, + TransactionObserverResult? transactionObserverResult, + TimeStamp lastActiveTabOptimization, + HttpConnectionInfoCloneArgs connInfoArgs, + TimeStamp onStopRequestStart); + async OnInitFailed(nsresult status); + async OnH2PushStream(uint32_t pushedStreamId, + nsCString resourceUrl, + nsCString requestString); + async EarlyHint(nsCString linkHeader, nsCString referrerPolicy, nsCString cspHeader); + +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, + ClassOfService 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..bea6d6be7c --- /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 "nsTHashMap.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: + nsTHashMap 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..d35a0a9a12 --- /dev/null +++ b/netwerk/protocol/http/ParentChannelListener.cpp @@ -0,0 +1,292 @@ +/* -*- 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 "nsIHttpHeaderVisitor.h" +#include "nsIPrompt.h" +#include "nsISecureBrowserUI.h" +#include "nsIThreadRetargetableStreamListener.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::dom::ServiceWorkerInterceptController; + +namespace mozilla { +namespace net { + +ParentChannelListener::ParentChannelListener( + nsIStreamListener* aListener, + dom::CanonicalBrowsingContext* aBrowsingContext) + : mNextListener(aListener), mBrowsingContext(aBrowsingContext) { + LOG(("ParentChannelListener::ParentChannelListener [this=%p, next=%p]", this, + aListener)); + + 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); +} + +NS_IMETHODIMP +ParentChannelListener::OnDataFinished(nsresult aStatus) { + if (!mNextListener) { + return NS_ERROR_FAILURE; + } + nsCOMPtr listener = + do_QueryInterface(mNextListener); + if (listener) { + return listener->OnDataFinished(aStatus); + } + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// 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..cd0fbfeadb --- /dev/null +++ b/netwerk/protocol/http/ParentChannelListener.h @@ -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/. */ + +#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 \ + } \ + } + +class ParentChannelListener final : public nsIInterfaceRequestor, + 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); + + // 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..53c457a4b1 --- /dev/null +++ b/netwerk/protocol/http/PendingTransactionInfo.cpp @@ -0,0 +1,136 @@ +/* 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 "NullHttpTransaction.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 (mDnsAndSock) { + RefPtr dnsAndSock = do_QueryReferent(mDnsAndSock); + LOG( + ("PendingTransactionInfo::PendingTransactionInfo " + "[trans=%p halfOpen=%p]", + mTransaction.get(), dnsAndSock.get())); + if (dnsAndSock) { + dnsAndSock->Unclaim(); + } + mDnsAndSock = 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(), mDnsAndSock.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 alreadyDnsAndSockOrWaitingForTLS = false; + if (mDnsAndSock) { + MOZ_ASSERT(!mActiveConn); + RefPtr dnsAndSock = do_QueryReferent(mDnsAndSock); + LOG( + ("PendingTransactionInfo::IsAlreadyClaimedInitializingConn " + "[trans=%p, dnsAndSock=%p]\n", + mTransaction.get(), dnsAndSock.get())); + if (dnsAndSock) { + alreadyDnsAndSockOrWaitingForTLS = true; + } else { + // If we have not found the halfOpen socket, remove the pointer. + mDnsAndSock = nullptr; + } + } else if (mActiveConn) { + MOZ_ASSERT(!mDnsAndSock); + 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()))) { + alreadyDnsAndSockOrWaitingForTLS = true; + } else { + // If we have not found the connection, remove the pointer. + mActiveConn = nullptr; + } + } + + return alreadyDnsAndSockOrWaitingForTLS; +} + +nsWeakPtr PendingTransactionInfo::ForgetDnsAndConnectSocketAndActiveConn() { + nsWeakPtr dnsAndSock = mDnsAndSock; + + mDnsAndSock = nullptr; + mActiveConn = nullptr; + return dnsAndSock; +} + +void PendingTransactionInfo::RememberDnsAndConnectSocket( + DnsAndConnectSocket* sock) { + mDnsAndSock = + do_GetWeakReference(static_cast(sock)); +} + +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)); + nsCOMPtr tlsSocketControl; + conn->GetTLSSocketControl(getter_AddRefs(tlsSocketControl)); + if (tlsSocketControl) { + Unused << tlsSocketControl->Claim(); + } + return true; + } + return false; +} + +void PendingTransactionInfo::AddDnsAndConnectSocket(DnsAndConnectSocket* sock) { + mDnsAndSock = + 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..863b43ae03 --- /dev/null +++ b/netwerk/protocol/http/PendingTransactionInfo.h @@ -0,0 +1,63 @@ +/* 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 "DnsAndConnectSocket.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 DnsAndConnectSocket or + // a connection in TLS handshake phase. + bool IsAlreadyClaimedInitializingConn(); + + // This function return a weak poointer to DnsAndConnectSocket. + // The pointer is used by the caller(ConnectionEntry) to remove the + // DnsAndConnectSocket from the internal list. PendingTransactionInfo + // cannot perform this opereation. + [[nodiscard]] nsWeakPtr ForgetDnsAndConnectSocketAndActiveConn(); + + // Remember associated DnsAndConnectSocket. + void RememberDnsAndConnectSocket(DnsAndConnectSocket* 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 AddDnsAndConnectSocket(DnsAndConnectSocket* sock); + + nsHttpTransaction* Transaction() const { return mTransaction; } + + private: + RefPtr mTransaction; + nsWeakPtr mDnsAndSock; + 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..9598769cc8 --- /dev/null +++ b/netwerk/protocol/http/PendingTransactionQueue.cpp @@ -0,0 +1,287 @@ +/* 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->BrowserId() : 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, bid=%" PRIu64 "\n", + info->Transaction(), info->Transaction()->BrowserId())); + + uint64_t windowId = TabIdForQueuing(info->Transaction()); + nsTArray>* const infoArray = + mPendingTransactionTable.GetOrInsertNew(windowId); + + // XXX At least if a new array was empty before, this isn't efficient, as it + // does an insert-sort. It would be better to just append all elements and + // then sort. + 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 (const auto& entry : mPendingTransactionTable) { + if (windowId && entry.GetKey() == windowId) { + continue; + } + + uint32_t count = 0; + for (; count < entry.GetWeak()->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, entry.GetWeak()->ElementAt(count)); + ++totalCount; + } + entry.GetWeak()->RemoveElementsAt(0, count); + + if (maxCount && totalCount == maxCount) { + if (entry.GetWeak()->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 (const auto& data : mPendingTransactionTable.Values()) { + length += data->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 (const auto& entry : mPendingTransactionTable) { + LOG(("] window id = %" PRIx64 " queue [", entry.GetKey())); + for (const auto& info : *entry.GetWeak()) { + LOG((" %p", info->Transaction())); + } + } + LOG(("]")); +} + +void PendingTransactionQueue::Compact() { + mUrgentStartQ.Compact(); + for (const auto& data : mPendingTransactionTable.Values()) { + data->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 (const auto& data : mPendingTransactionTable.Values()) { + for (const auto& pendingTransInfo : *data) { + LOG(("PendingTransactionQueue::CancelAllTransactions %p\n", + pendingTransInfo->Transaction())); + pendingTransInfo->Transaction()->Close(reason); + } + data->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..183b9f5fd5 --- /dev/null +++ b/netwerk/protocol/http/QuicSocketControl.cpp @@ -0,0 +1,128 @@ +/* -*- 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 "nsSocketTransportService2.h" +#include "nsThreadUtils.h" +#include "sslt.h" +#include "ssl.h" + +namespace mozilla { +namespace net { + +QuicSocketControl::QuicSocketControl(const nsCString& aHostName, int32_t aPort, + uint32_t aProviderFlags, + Http3Session* aHttp3Session) + : CommonSocketControl(aHostName, aPort, aProviderFlags) { + COMMON_SOCKET_CONTROL_ASSERT_ON_OWNING_THREAD(); + mHttp3Session = do_GetWeakReference( + static_cast(aHttp3Session)); +} + +void QuicSocketControl::SetCertVerificationResult(PRErrorCode errorCode) { + COMMON_SOCKET_CONTROL_ASSERT_ON_OWNING_THREAD(); + SetUsedPrivateDNS(GetProviderFlags() & nsISocketProvider::USED_PRIVATE_DNS); + + if (errorCode) { + mFailedVerification = true; + SetCanceled(errorCode); + } + + CallAuthenticated(); +} + +NS_IMETHODIMP +QuicSocketControl::GetSSLVersionOffered(int16_t* aSSLVersionOffered) { + COMMON_SOCKET_CONTROL_ASSERT_ON_OWNING_THREAD(); + *aSSLVersionOffered = nsITLSSocketControl::TLS_VERSION_1_3; + return NS_OK; +} + +void QuicSocketControl::CallAuthenticated() { + COMMON_SOCKET_CONTROL_ASSERT_ON_OWNING_THREAD(); + RefPtr http3Session = do_QueryReferent(mHttp3Session); + if (http3Session) { + http3Session->Authenticated(GetErrorCode()); + } +} + +void QuicSocketControl::HandshakeCompleted() { + COMMON_SOCKET_CONTROL_ASSERT_ON_OWNING_THREAD(); + uint32_t state = nsIWebProgressListener::STATE_IS_SECURE; + + // If we're here, the TLS handshake has succeeded. If the overridable error + // category is nonzero, the user has added an override for a certificate + // error. + if (mOverridableErrorCategory.isSome() && + *mOverridableErrorCategory != + nsITransportSecurityInfo::OverridableErrorCategory::ERROR_UNSET) { + state |= nsIWebProgressListener::STATE_CERT_USER_OVERRIDDEN; + } + + SetSecurityState(state); + mHandshakeCompleted = true; +} + +void QuicSocketControl::SetNegotiatedNPN(const nsACString& aValue) { + COMMON_SOCKET_CONTROL_ASSERT_ON_OWNING_THREAD(); + mNegotiatedNPN = aValue; + mNPNCompleted = true; +} + +void QuicSocketControl::SetInfo(uint16_t aCipherSuite, + uint16_t aProtocolVersion, + uint16_t aKeaGroupName, + uint16_t aSignatureScheme, bool aEchAccepted) { + COMMON_SOCKET_CONTROL_ASSERT_ON_OWNING_THREAD(); + SSLCipherSuiteInfo cipherInfo; + if (SSL_GetCipherSuiteInfo(aCipherSuite, &cipherInfo, sizeof cipherInfo) == + SECSuccess) { + mCipherSuite.emplace(aCipherSuite); + mProtocolVersion.emplace(aProtocolVersion & 0xFF); + mKeaGroupName.emplace(getKeaGroupName(aKeaGroupName)); + mSignatureSchemeName.emplace(getSignatureName(aSignatureScheme)); + mIsAcceptedEch.emplace(aEchAccepted); + } +} + +NS_IMETHODIMP +QuicSocketControl::GetEchConfig(nsACString& aEchConfig) { + COMMON_SOCKET_CONTROL_ASSERT_ON_OWNING_THREAD(); + aEchConfig = mEchConfig; + return NS_OK; +} + +NS_IMETHODIMP +QuicSocketControl::SetEchConfig(const nsACString& aEchConfig) { + COMMON_SOCKET_CONTROL_ASSERT_ON_OWNING_THREAD(); + mEchConfig = aEchConfig; + RefPtr http3Session = do_QueryReferent(mHttp3Session); + if (http3Session) { + http3Session->DoSetEchConfig(mEchConfig); + } + return NS_OK; +} + +NS_IMETHODIMP +QuicSocketControl::GetRetryEchConfig(nsACString& aEchConfig) { + COMMON_SOCKET_CONTROL_ASSERT_ON_OWNING_THREAD(); + aEchConfig = mRetryEchConfig; + return NS_OK; +} + +void QuicSocketControl::SetRetryEchConfig(const nsACString& aEchConfig) { + COMMON_SOCKET_CONTROL_ASSERT_ON_OWNING_THREAD(); + mRetryEchConfig = aEchConfig; +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/QuicSocketControl.h b/netwerk/protocol/http/QuicSocketControl.h new file mode 100644 index 0000000000..1042a2d659 --- /dev/null +++ b/netwerk/protocol/http/QuicSocketControl.h @@ -0,0 +1,67 @@ +/* -*- 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_INLINE_DECL_REFCOUNTING_INHERITED(QuicSocketControl, CommonSocketControl); + + NS_IMETHOD GetSSLVersionOffered(int16_t* aSSLVersionOffered) override; + + QuicSocketControl(const nsCString& aHostName, int32_t aPort, + uint32_t aProviderFlags, Http3Session* aHttp3Session); + + void SetNegotiatedNPN(const nsACString& aValue); + void SetInfo(uint16_t aCipherSuite, uint16_t aProtocolVersion, + uint16_t aKeaGroup, uint16_t aSignatureScheme, + bool aEchAccepted); + + void CallAuthenticated(); + + void HandshakeCompleted(); + void SetCertVerificationResult(PRErrorCode errorCode) override; + + NS_IMETHOD GetEchConfig(nsACString& aEchConfig) override; + NS_IMETHOD SetEchConfig(const nsACString& aEchConfig) override; + NS_IMETHOD GetRetryEchConfig(nsACString& aEchConfig) override; + void SetRetryEchConfig(const nsACString& aEchConfig); + + private: + ~QuicSocketControl() = default; + + // For Authentication done callback and echConfig. + nsWeakPtr mHttp3Session; + + nsCString mEchConfig; + nsCString mRetryEchConfig; +}; + +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..eab54c24d5 --- /dev/null +++ b/netwerk/protocol/http/SpeculativeTransaction.cpp @@ -0,0 +1,89 @@ +/* -*- 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), + mCloseCallback(std::move(aCallback)) {} + +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(GetCurrentSerialEventTarget(), + 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) { + gHttpHandler->ConnMgr()->DoSpeculativeConnection(this, false); + 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; + } +} + +void SpeculativeTransaction::InvokeCallback() { + if (mCloseCallback) { + mCloseCallback(true); + 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..851fe2b39f --- /dev/null +++ b/netwerk/protocol/http/SpeculativeTransaction.h @@ -0,0 +1,70 @@ +/* -*- 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; + void InvokeCallback(); + + protected: + virtual ~SpeculativeTransaction() = default; + + 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/TLSTransportLayer.cpp b/netwerk/protocol/http/TLSTransportLayer.cpp new file mode 100644 index 0000000000..eff46898ea --- /dev/null +++ b/netwerk/protocol/http/TLSTransportLayer.cpp @@ -0,0 +1,866 @@ +/* -*- 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/. */ + +// HttpLog.h should generally be included first +#include "HttpLog.h" + +#include "Http2StreamTunnel.h" +#include "TLSTransportLayer.h" +#include "nsISocketProvider.h" +#include "nsITLSSocketControl.h" +#include "nsQueryObject.h" +#include "nsSocketProviderService.h" +#include "nsSocketTransport2.h" + +namespace mozilla::net { + +//----------------------------------------------------------------------------- +// TLSTransportLayerInputStream impl +//----------------------------------------------------------------------------- + +NS_IMPL_QUERY_INTERFACE(TLSTransportLayer::InputStreamWrapper, nsIInputStream, + nsIAsyncInputStream) + +NS_IMETHODIMP_(MozExternalRefCountType) +TLSTransportLayer::InputStreamWrapper::AddRef() { return mTransport->AddRef(); } + +NS_IMETHODIMP_(MozExternalRefCountType) +TLSTransportLayer::InputStreamWrapper::Release() { + return mTransport->Release(); +} + +TLSTransportLayer::InputStreamWrapper::InputStreamWrapper( + nsIAsyncInputStream* aInputStream, TLSTransportLayer* aTransport) + : mSocketIn(aInputStream), mTransport(aTransport) {} + +NS_IMETHODIMP +TLSTransportLayer::InputStreamWrapper::Close() { + LOG(("TLSTransportLayer::InputStreamWrapper::Close [this=%p]\n", this)); + return mSocketIn->Close(); +} + +NS_IMETHODIMP TLSTransportLayer::InputStreamWrapper::Available( + uint64_t* avail) { + LOG(("TLSTransportLayer::InputStreamWrapper::Available [this=%p]\n", this)); + return mSocketIn->Available(avail); +} + +NS_IMETHODIMP TLSTransportLayer::InputStreamWrapper::StreamStatus() { + LOG(("TLSTransportLayer::InputStreamWrapper::StreamStatus [this=%p]\n", + this)); + return mSocketIn->StreamStatus(); +} + +nsresult TLSTransportLayer::InputStreamWrapper::ReadDirectly( + char* buf, uint32_t count, uint32_t* countRead) { + LOG(("TLSTransportLayer::InputStreamWrapper::ReadDirectly [this=%p]\n", + this)); + return mSocketIn->Read(buf, count, countRead); +} + +NS_IMETHODIMP +TLSTransportLayer::InputStreamWrapper::Read(char* buf, uint32_t count, + uint32_t* countRead) { + LOG(("TLSTransportLayer::InputStreamWrapper::Read [this=%p]\n", this)); + + *countRead = 0; + + if (NS_FAILED(mStatus)) { + return (mStatus == NS_BASE_STREAM_CLOSED) ? NS_OK : mStatus; + } + + int32_t bytesRead = PR_Read(mTransport->mFD, buf, count); + if (bytesRead > 0) { + *countRead = bytesRead; + } else if (bytesRead < 0) { + PRErrorCode code = PR_GetError(); + if (code == PR_WOULD_BLOCK_ERROR) { + LOG(( + "TLSTransportLayer::InputStreamWrapper::Read %p PR_Read would block ", + this)); + return NS_BASE_STREAM_WOULD_BLOCK; + } + // If reading from the socket succeeded (NS_SUCCEEDED(mStatus)), + // but the nss layer encountered an error remember the error. + if (NS_SUCCEEDED(mStatus)) { + mStatus = ErrorAccordingToNSPR(code); + LOG(("TLSTransportLayer::InputStreamWrapper::Read %p nss error %" PRIx32 + ".\n", + this, static_cast(mStatus))); + } + } + + if (NS_SUCCEEDED(mStatus) && !bytesRead) { + LOG( + ("TLSTransportLayer::InputStreamWrapper::Read %p " + "Second layer of TLS stripping results in STREAM_CLOSED\n", + this)); + mStatus = NS_BASE_STREAM_CLOSED; + } + + LOG(("TLSTransportLayer::InputStreamWrapper::Read %p rv=%" PRIx32 + " didread=%d " + "2 layers of ssl stripped to plaintext\n", + this, static_cast(mStatus), bytesRead)); + return mStatus; +} + +NS_IMETHODIMP +TLSTransportLayer::InputStreamWrapper::ReadSegments(nsWriteSegmentFun writer, + void* closure, + uint32_t count, + uint32_t* countRead) { + LOG(("TLSTransportLayer::InputStreamWrapper::ReadSegments [this=%p]\n", + this)); + return mSocketIn->ReadSegments(writer, closure, count, countRead); +} + +NS_IMETHODIMP +TLSTransportLayer::InputStreamWrapper::IsNonBlocking(bool* nonblocking) { + return mSocketIn->IsNonBlocking(nonblocking); +} + +NS_IMETHODIMP +TLSTransportLayer::InputStreamWrapper::CloseWithStatus(nsresult reason) { + LOG( + ("TLSTransportLayer::InputStreamWrapper::CloseWithStatus [this=%p " + "reason=%" PRIx32 "]\n", + this, static_cast(reason))); + return mSocketIn->CloseWithStatus(reason); +} + +NS_IMETHODIMP +TLSTransportLayer::InputStreamWrapper::AsyncWait( + nsIInputStreamCallback* callback, uint32_t flags, uint32_t amount, + nsIEventTarget* target) { + LOG( + ("TLSTransportLayer::InputStreamWrapper::AsyncWait [this=%p, " + "callback=%p]\n", + this, callback)); + mTransport->mInputCallback = callback; + // Don't bother to call PR_POLL when |callback| is NULL. We call |AsyncWait| + // directly to null out the underlying callback. + if (!callback) { + return mSocketIn->AsyncWait(nullptr, 0, 0, nullptr); + } + + PRPollDesc pd; + pd.fd = mTransport->mFD; + pd.in_flags = PR_POLL_READ | PR_POLL_EXCEPT; + // Only run PR_Poll on the socket thread. Also, make sure this lives at least + // as long as that operation. + auto DoPoll = [self = RefPtr{this}, pd(pd)]() mutable { + int32_t rv = PR_Poll(&pd, 1, PR_INTERVAL_NO_TIMEOUT); + LOG(("TLSTransportLayer::InputStreamWrapper::AsyncWait rv=%d", rv)); + }; + if (OnSocketThread()) { + DoPoll(); + } else { + gSocketTransportService->Dispatch(NS_NewRunnableFunction( + "TLSTransportLayer::InputStreamWrapper::AsyncWait", DoPoll)); + } + return NS_OK; +} + +//----------------------------------------------------------------------------- +// TLSTransportLayerOutputStream impl +//----------------------------------------------------------------------------- + +NS_IMPL_QUERY_INTERFACE(TLSTransportLayer::OutputStreamWrapper, nsIOutputStream, + nsIAsyncOutputStream) + +NS_IMETHODIMP_(MozExternalRefCountType) +TLSTransportLayer::OutputStreamWrapper::AddRef() { + return mTransport->AddRef(); +} + +NS_IMETHODIMP_(MozExternalRefCountType) +TLSTransportLayer::OutputStreamWrapper::Release() { + return mTransport->Release(); +} + +TLSTransportLayer::OutputStreamWrapper::OutputStreamWrapper( + nsIAsyncOutputStream* aOutputStream, TLSTransportLayer* aTransport) + : mSocketOut(aOutputStream), mTransport(aTransport) {} + +NS_IMETHODIMP +TLSTransportLayer::OutputStreamWrapper::Close() { + LOG(("TLSTransportLayer::OutputStreamWrapper::Close [this=%p]\n", this)); + return mSocketOut->Close(); +} + +NS_IMETHODIMP +TLSTransportLayer::OutputStreamWrapper::Flush() { + LOG(("TLSTransportLayerOutputStream::Flush [this=%p]\n", this)); + return mSocketOut->Flush(); +} + +NS_IMETHODIMP +TLSTransportLayer::OutputStreamWrapper::StreamStatus() { + LOG(("TLSTransportLayerOutputStream::StreamStatus [this=%p]\n", this)); + return mSocketOut->StreamStatus(); +} + +nsresult TLSTransportLayer::OutputStreamWrapper::WriteDirectly( + const char* buf, uint32_t count, uint32_t* countWritten) { + LOG( + ("TLSTransportLayer::OutputStreamWrapper::WriteDirectly [this=%p " + "count=%u]\n", + this, count)); + return mSocketOut->Write(buf, count, countWritten); +} + +NS_IMETHODIMP +TLSTransportLayer::OutputStreamWrapper::Write(const char* buf, uint32_t count, + uint32_t* countWritten) { + LOG(("TLSTransportLayer::OutputStreamWrapper::Write [this=%p count=%u]\n", + this, count)); + + *countWritten = 0; + + if (NS_FAILED(mStatus)) { + return (mStatus == NS_BASE_STREAM_CLOSED) ? NS_OK : mStatus; + } + + int32_t written = PR_Write(mTransport->mFD, buf, count); + LOG( + ("TLSTransportLayer::OutputStreamWrapper::Write %p PRWrite(%d) = %d " + "%d\n", + this, count, written, PR_GetError() == PR_WOULD_BLOCK_ERROR)); + + if (written > 0) { + *countWritten = written; + } else if (written < 0) { + PRErrorCode code = PR_GetError(); + if (code == PR_WOULD_BLOCK_ERROR) { + LOG( + ("TLSTransportLayer::OutputStreamWrapper::Write %p PRWrite would " + "block ", + this)); + return NS_BASE_STREAM_WOULD_BLOCK; + } + + // Writing to the socket succeeded, but failed in nss layer. + if (NS_SUCCEEDED(mStatus)) { + mStatus = ErrorAccordingToNSPR(code); + } + } + + return mStatus; +} + +NS_IMETHODIMP +TLSTransportLayer::OutputStreamWrapper::WriteSegments(nsReadSegmentFun reader, + void* closure, + uint32_t count, + uint32_t* countRead) { + return mSocketOut->WriteSegments(reader, closure, count, countRead); +} + +// static +nsresult TLSTransportLayer::OutputStreamWrapper::WriteFromSegments( + nsIInputStream* input, void* closure, const char* fromSegment, + uint32_t offset, uint32_t count, uint32_t* countRead) { + OutputStreamWrapper* self = (OutputStreamWrapper*)closure; + return self->Write(fromSegment, count, countRead); +} + +NS_IMETHODIMP +TLSTransportLayer::OutputStreamWrapper::WriteFrom(nsIInputStream* stream, + uint32_t count, + uint32_t* countRead) { + return stream->ReadSegments(WriteFromSegments, this, count, countRead); +} + +NS_IMETHODIMP +TLSTransportLayer::OutputStreamWrapper::IsNonBlocking(bool* nonblocking) { + return mSocketOut->IsNonBlocking(nonblocking); +} + +NS_IMETHODIMP +TLSTransportLayer::OutputStreamWrapper::CloseWithStatus(nsresult reason) { + LOG(("OutputStreamWrapper::CloseWithStatus [this=%p reason=%" PRIx32 "]\n", + this, static_cast(reason))); + return mSocketOut->CloseWithStatus(reason); +} + +NS_IMETHODIMP +TLSTransportLayer::OutputStreamWrapper::AsyncWait( + nsIOutputStreamCallback* callback, uint32_t flags, uint32_t amount, + nsIEventTarget* target) { + LOG( + ("TLSTransportLayer::OutputStreamWrapper::AsyncWait [this=%p, " + "mOutputCallback=%p " + "callback=%p]\n", + this, mTransport->mOutputCallback.get(), callback)); + mTransport->mOutputCallback = callback; + // Don't bother to call PR_POLL when |callback| is NULL. We call |AsyncWait| + // directly to null out the underlying callback. + if (!callback) { + return mSocketOut->AsyncWait(nullptr, 0, 0, nullptr); + } + + PRPollDesc pd; + pd.fd = mTransport->mFD; + pd.in_flags = PR_POLL_WRITE | PR_POLL_EXCEPT; + int32_t rv = PR_Poll(&pd, 1, PR_INTERVAL_NO_TIMEOUT); + LOG(("TLSTransportLayer::OutputStreamWrapper::AsyncWait rv=%d", rv)); + return NS_OK; +} + +//----------------------------------------------------------------------------- +// TLSTransportLayer impl +//----------------------------------------------------------------------------- + +static PRDescIdentity sTLSTransportLayerIdentity; +static PRIOMethods sTLSTransportLayerMethods; +static PRIOMethods* sTLSTransportLayerMethodsPtr = nullptr; + +bool TLSTransportLayer::DispatchRelease() { + if (OnSocketThread()) { + return false; + } + + gSocketTransportService->Dispatch( + NewNonOwningRunnableMethod("net::TLSTransportLayer::Release", this, + &TLSTransportLayer::Release), + NS_DISPATCH_NORMAL); + + return true; +} + +NS_IMPL_ADDREF(TLSTransportLayer) +NS_IMETHODIMP_(MozExternalRefCountType) +TLSTransportLayer::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, "TLSTransportLayer"); + + if (0 == count) { + mRefCnt = 1; + delete (this); + return 0; + } + + return count; +} + +NS_INTERFACE_MAP_BEGIN(TLSTransportLayer) + NS_INTERFACE_MAP_ENTRY(nsISocketTransport) + NS_INTERFACE_MAP_ENTRY(nsITransport) + NS_INTERFACE_MAP_ENTRY(nsIInputStreamCallback) + NS_INTERFACE_MAP_ENTRY(nsIOutputStreamCallback) + NS_INTERFACE_MAP_ENTRY_CONCRETE(TLSTransportLayer) +NS_INTERFACE_MAP_END + +TLSTransportLayer::TLSTransportLayer(nsISocketTransport* aTransport, + nsIAsyncInputStream* aInputStream, + nsIAsyncOutputStream* aOutputStream, + nsIInputStreamCallback* aOwner) + : mSocketTransport(aTransport), + mSocketInWrapper(aInputStream, this), + mSocketOutWrapper(aOutputStream, this), + mOwner(aOwner) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG(("TLSTransportLayer ctor this=[%p]", this)); +} + +TLSTransportLayer::~TLSTransportLayer() { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG(("TLSTransportLayer dtor this=[%p]", this)); + if (mFD) { + PR_Close(mFD); + mFD = nullptr; + } + mTLSSocketControl = nullptr; +} + +bool TLSTransportLayer::Init(const char* aTLSHost, int32_t aTLSPort) { + LOG(("TLSTransportLayer::Init this=[%p]", this)); + nsCOMPtr provider; + nsCOMPtr spserv = + nsSocketProviderService::GetOrCreate(); + if (!spserv) { + return false; + } + + spserv->GetSocketProvider("ssl", getter_AddRefs(provider)); + if (!provider) { + return false; + } + + // 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 (!sTLSTransportLayerMethodsPtr) { + // one time initialization + sTLSTransportLayerIdentity = PR_GetUniqueIdentity("TLSTransportLayer"); + sTLSTransportLayerMethods = *PR_GetDefaultIOMethods(); + sTLSTransportLayerMethods.getpeername = GetPeerName; + sTLSTransportLayerMethods.getsocketoption = GetSocketOption; + sTLSTransportLayerMethods.setsocketoption = SetSocketOption; + sTLSTransportLayerMethods.read = Read; + sTLSTransportLayerMethods.write = Write; + sTLSTransportLayerMethods.send = Send; + sTLSTransportLayerMethods.recv = Recv; + sTLSTransportLayerMethods.close = Close; + sTLSTransportLayerMethods.poll = Poll; + sTLSTransportLayerMethodsPtr = &sTLSTransportLayerMethods; + } + + mFD = PR_CreateIOLayerStub(sTLSTransportLayerIdentity, + &sTLSTransportLayerMethods); + if (!mFD) { + return false; + } + + mFD->secret = reinterpret_cast(this); + + return NS_SUCCEEDED(provider->AddToSocket( + PR_AF_INET, aTLSHost, aTLSPort, nullptr, OriginAttributes(), 0, 0, mFD, + getter_AddRefs(mTLSSocketControl))); +} + +NS_IMETHODIMP +TLSTransportLayer::OnInputStreamReady(nsIAsyncInputStream* in) { + nsCOMPtr callback = std::move(mInputCallback); + if (callback) { + return callback->OnInputStreamReady(&mSocketInWrapper); + } + return NS_OK; +} + +NS_IMETHODIMP +TLSTransportLayer::OnOutputStreamReady(nsIAsyncOutputStream* out) { + nsCOMPtr callback = std::move(mOutputCallback); + nsresult rv = NS_OK; + if (callback) { + rv = callback->OnOutputStreamReady(&mSocketOutWrapper); + + RefPtr tunnel = do_QueryObject(out); + if (tunnel) { + tunnel->MaybeSetRequestDone(callback); + } + } + return rv; +} + +NS_IMETHODIMP +TLSTransportLayer::SetKeepaliveEnabled(bool aKeepaliveEnabled) { + if (!mSocketTransport) { + return NS_ERROR_FAILURE; + } + return mSocketTransport->SetKeepaliveEnabled(aKeepaliveEnabled); +} + +NS_IMETHODIMP +TLSTransportLayer::SetKeepaliveVals(int32_t keepaliveIdleTime, + int32_t keepaliveRetryInterval) { + if (!mSocketTransport) { + return NS_ERROR_FAILURE; + } + return mSocketTransport->SetKeepaliveVals(keepaliveIdleTime, + keepaliveRetryInterval); +} + +NS_IMETHODIMP +TLSTransportLayer::GetSecurityCallbacks( + nsIInterfaceRequestor** aSecurityCallbacks) { + if (!mSocketTransport) { + return NS_ERROR_FAILURE; + } + return mSocketTransport->GetSecurityCallbacks(aSecurityCallbacks); +} + +NS_IMETHODIMP +TLSTransportLayer::SetSecurityCallbacks( + nsIInterfaceRequestor* aSecurityCallbacks) { + if (!mSocketTransport) { + return NS_ERROR_FAILURE; + } + + return mSocketTransport->SetSecurityCallbacks(aSecurityCallbacks); +} + +NS_IMETHODIMP +TLSTransportLayer::OpenInputStream(uint32_t aFlags, uint32_t aSegmentSize, + uint32_t aSegmentCount, + nsIInputStream** _retval) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +TLSTransportLayer::OpenOutputStream(uint32_t aFlags, uint32_t aSegmentSize, + uint32_t aSegmentCount, + nsIOutputStream** _retval) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +TLSTransportLayer::Close(nsresult aReason) { + LOG(("TLSTransportLayer::Close [this=%p reason=%" PRIx32 "]\n", this, + static_cast(aReason))); + + mInputCallback = nullptr; + mOutputCallback = nullptr; + if (mSocketTransport) { + mSocketTransport->Close(aReason); + mSocketTransport = nullptr; + } + mSocketInWrapper.AsyncWait(nullptr, 0, 0, nullptr); + mSocketOutWrapper.AsyncWait(nullptr, 0, 0, nullptr); + + if (mOwner) { + RefPtr self = this; + Unused << NS_DispatchToCurrentThread(NS_NewRunnableFunction( + "TLSTransportLayer::Close", [self{std::move(self)}]() { + nsCOMPtr inputCallback = + std::move(self->mOwner); + if (inputCallback) { + // This is hack. We need to make + // nsHttpConnection::OnInputStreamReady be called, so + // nsHttpConnection::CloseTransaction can be called to release the + // transaction. + Unused << inputCallback->OnInputStreamReady( + &self->mSocketInWrapper); + } + })); + } + + return NS_OK; +} + +NS_IMETHODIMP +TLSTransportLayer::SetEventSink(nsITransportEventSink* aSink, + nsIEventTarget* aEventTarget) { + if (!mSocketTransport) { + return NS_ERROR_FAILURE; + } + return mSocketTransport->SetEventSink(aSink, aEventTarget); +} + +NS_IMETHODIMP +TLSTransportLayer::Bind(NetAddr* aLocalAddr) { + if (!mSocketTransport) { + return NS_ERROR_FAILURE; + } + return mSocketTransport->Bind(aLocalAddr); +} + +NS_IMETHODIMP +TLSTransportLayer::GetEchConfigUsed(bool* aEchConfigUsed) { + if (!mSocketTransport) { + return NS_ERROR_FAILURE; + } + return mSocketTransport->GetEchConfigUsed(aEchConfigUsed); +} + +NS_IMETHODIMP +TLSTransportLayer::SetEchConfig(const nsACString& aEchConfig) { + if (!mSocketTransport) { + return NS_ERROR_FAILURE; + } + return mSocketTransport->SetEchConfig(aEchConfig); +} + +NS_IMETHODIMP +TLSTransportLayer::ResolvedByTRR(bool* aResolvedByTRR) { + if (!mSocketTransport) { + return NS_ERROR_FAILURE; + } + return mSocketTransport->ResolvedByTRR(aResolvedByTRR); +} + +NS_IMETHODIMP TLSTransportLayer::GetEffectiveTRRMode( + nsIRequest::TRRMode* aEffectiveTRRMode) { + if (!mSocketTransport) { + return NS_ERROR_FAILURE; + } + return mSocketTransport->GetEffectiveTRRMode(aEffectiveTRRMode); +} + +NS_IMETHODIMP TLSTransportLayer::GetTrrSkipReason( + nsITRRSkipReason::value* aTrrSkipReason) { + if (!mSocketTransport) { + return NS_ERROR_FAILURE; + } + return mSocketTransport->GetTrrSkipReason(aTrrSkipReason); +} + +#define FWD_TS_PTR(fx, ts) \ + NS_IMETHODIMP \ + TLSTransportLayer::fx(ts* arg) { \ + if (!mSocketTransport) return NS_ERROR_FAILURE; \ + return mSocketTransport->fx(arg); \ + } + +#define FWD_TS_ADDREF(fx, ts) \ + NS_IMETHODIMP \ + TLSTransportLayer::fx(ts** arg) { \ + if (!mSocketTransport) return NS_ERROR_FAILURE; \ + return mSocketTransport->fx(arg); \ + } + +#define FWD_TS(fx, ts) \ + NS_IMETHODIMP \ + TLSTransportLayer::fx(ts arg) { \ + if (!mSocketTransport) return NS_ERROR_FAILURE; \ + return mSocketTransport->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_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 TLSTransportLayer::GetTlsSocketControl( + nsITLSSocketControl** tlsSocketControl) { + if (!mTLSSocketControl) { + return NS_ERROR_ABORT; + } + + *tlsSocketControl = do_AddRef(mTLSSocketControl).take(); + return NS_OK; +} + +nsresult TLSTransportLayer::GetOriginAttributes( + mozilla::OriginAttributes* aOriginAttributes) { + if (!mSocketTransport) { + return NS_ERROR_FAILURE; + } + return mSocketTransport->GetOriginAttributes(aOriginAttributes); +} + +nsresult TLSTransportLayer::SetOriginAttributes( + const mozilla::OriginAttributes& aOriginAttributes) { + if (!mSocketTransport) { + return NS_ERROR_FAILURE; + } + return mSocketTransport->SetOriginAttributes(aOriginAttributes); +} + +NS_IMETHODIMP +TLSTransportLayer::GetScriptableOriginAttributes( + JSContext* aCx, JS::MutableHandle aOriginAttributes) { + if (!mSocketTransport) { + return NS_ERROR_FAILURE; + } + return mSocketTransport->GetScriptableOriginAttributes(aCx, + aOriginAttributes); +} + +NS_IMETHODIMP +TLSTransportLayer::SetScriptableOriginAttributes( + JSContext* aCx, JS::Handle aOriginAttributes) { + if (!mSocketTransport) { + return NS_ERROR_FAILURE; + } + return mSocketTransport->SetScriptableOriginAttributes(aCx, + aOriginAttributes); +} + +NS_IMETHODIMP +TLSTransportLayer::GetHost(nsACString& aHost) { + if (!mSocketTransport) { + return NS_ERROR_FAILURE; + } + return mSocketTransport->GetHost(aHost); +} + +NS_IMETHODIMP +TLSTransportLayer::GetTimeout(uint32_t aType, uint32_t* _retval) { + if (!mSocketTransport) { + return NS_ERROR_FAILURE; + } + return mSocketTransport->GetTimeout(aType, _retval); +} + +NS_IMETHODIMP +TLSTransportLayer::SetTimeout(uint32_t aType, uint32_t aValue) { + if (!mSocketTransport) { + return NS_ERROR_FAILURE; + } + return mSocketTransport->SetTimeout(aType, aValue); +} + +NS_IMETHODIMP +TLSTransportLayer::SetReuseAddrPort(bool aReuseAddrPort) { + if (!mSocketTransport) { + return NS_ERROR_FAILURE; + } + return mSocketTransport->SetReuseAddrPort(aReuseAddrPort); +} + +NS_IMETHODIMP +TLSTransportLayer::SetLinger(bool aPolarity, int16_t aTimeout) { + if (!mSocketTransport) { + return NS_ERROR_FAILURE; + } + return mSocketTransport->SetLinger(aPolarity, aTimeout); +} + +NS_IMETHODIMP +TLSTransportLayer::GetQoSBits(uint8_t* aQoSBits) { + if (!mSocketTransport) { + return NS_ERROR_FAILURE; + } + return mSocketTransport->GetQoSBits(aQoSBits); +} + +NS_IMETHODIMP +TLSTransportLayer::SetQoSBits(uint8_t aQoSBits) { + if (!mSocketTransport) { + return NS_ERROR_FAILURE; + } + return mSocketTransport->SetQoSBits(aQoSBits); +} + +NS_IMETHODIMP +TLSTransportLayer::GetRetryDnsIfPossible(bool* aRetry) { + if (!mSocketTransport) { + return NS_ERROR_FAILURE; + } + return mSocketTransport->GetRetryDnsIfPossible(aRetry); +} + +NS_IMETHODIMP +TLSTransportLayer::GetStatus(nsresult* aStatus) { + if (!mSocketTransport) { + return NS_ERROR_FAILURE; + } + return mSocketTransport->GetStatus(aStatus); +} + +int32_t TLSTransportLayer::OutputInternal(const char* aBuf, int32_t aAmount) { + LOG(("TLSTransportLayer::OutputInternal %p %d", this, aAmount)); + + uint32_t outCountWrite = 0; + nsresult rv = mSocketOutWrapper.WriteDirectly(aBuf, aAmount, &outCountWrite); + if (NS_FAILED(rv)) { + if (rv == NS_BASE_STREAM_WOULD_BLOCK) { + PR_SetError(PR_WOULD_BLOCK_ERROR, 0); + } else { + PR_SetError(PR_UNKNOWN_ERROR, 0); + } + return -1; + } + + return outCountWrite; +} + +int32_t TLSTransportLayer::InputInternal(char* aBuf, int32_t aAmount) { + LOG(("TLSTransportLayer::InputInternal aAmount=%d\n", aAmount)); + + uint32_t outCountRead = 0; + nsresult rv = mSocketInWrapper.ReadDirectly(aBuf, aAmount, &outCountRead); + if (NS_FAILED(rv)) { + if (rv == NS_BASE_STREAM_WOULD_BLOCK) { + PR_SetError(PR_WOULD_BLOCK_ERROR, 0); + } else { + PR_SetError(PR_UNKNOWN_ERROR, 0); + } + return -1; + } + return outCountRead; +} + +PRStatus TLSTransportLayer::GetPeerName(PRFileDesc* aFD, PRNetAddr* addr) { + TLSTransportLayer* self = reinterpret_cast(aFD->secret); + NetAddr peeraddr; + if (NS_FAILED(self->Transport()->GetPeerAddr(&peeraddr))) { + return PR_FAILURE; + } + NetAddrToPRNetAddr(&peeraddr, addr); + return PR_SUCCESS; +} + +PRStatus TLSTransportLayer::GetSocketOption(PRFileDesc* aFD, + PRSocketOptionData* aOpt) { + if (aOpt->option == PR_SockOpt_Nonblocking) { + aOpt->value.non_blocking = PR_TRUE; + return PR_SUCCESS; + } + return PR_FAILURE; +} + +PRStatus TLSTransportLayer::SetSocketOption(PRFileDesc* aFD, + const PRSocketOptionData* aOpt) { + return PR_FAILURE; +} + +PRStatus TLSTransportLayer::Close(PRFileDesc* aFD) { return PR_SUCCESS; } + +int32_t TLSTransportLayer::Write(PRFileDesc* aFD, const void* aBuf, + int32_t aAmount) { + TLSTransportLayer* self = reinterpret_cast(aFD->secret); + return self->OutputInternal(static_cast(aBuf), aAmount); +} + +int32_t TLSTransportLayer::Send(PRFileDesc* aFD, const void* aBuf, + int32_t aAmount, int, PRIntervalTime) { + return Write(aFD, aBuf, aAmount); +} + +int32_t TLSTransportLayer::Read(PRFileDesc* aFD, void* aBuf, int32_t aAmount) { + TLSTransportLayer* self = reinterpret_cast(aFD->secret); + return self->InputInternal(static_cast(aBuf), aAmount); +} + +int32_t TLSTransportLayer::Recv(PRFileDesc* aFD, void* aBuf, int32_t aAmount, + int, PRIntervalTime) { + return Read(aFD, aBuf, aAmount); +} + +int16_t TLSTransportLayer::Poll(PRFileDesc* fd, int16_t in_flags, + int16_t* out_flags) { + LOG(("TLSTransportLayer::Poll fd=%p inf_flags=%d\n", fd, (int)in_flags)); + *out_flags = in_flags; + + TLSTransportLayer* self = reinterpret_cast(fd->secret); + if (!self) { + return 0; + } + + if (in_flags & PR_POLL_READ) { + self->mSocketInWrapper.mSocketIn->AsyncWait(self, 0, 0, nullptr); + } else if (in_flags & PR_POLL_WRITE) { + self->mSocketOutWrapper.mSocketOut->AsyncWait(self, 0, 0, nullptr); + } + + return in_flags; +} + +bool TLSTransportLayer::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; +} + +} // namespace mozilla::net diff --git a/netwerk/protocol/http/TLSTransportLayer.h b/netwerk/protocol/http/TLSTransportLayer.h new file mode 100644 index 0000000000..85489ffa40 --- /dev/null +++ b/netwerk/protocol/http/TLSTransportLayer.h @@ -0,0 +1,170 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 TLSTransportLayer_h__ +#define TLSTransportLayer_h__ + +#include "nsSocketTransportService2.h" +#include "nsIInterfaceRequestor.h" +#include "nsISocketTransport.h" +#include "nsIAsyncInputStream.h" +#include "nsIAsyncOutputStream.h" +#include "prio.h" + +namespace mozilla::net { + +// TLSTransportLayer will provide a secondary TLS layer. It will be added as a +// layer between nsHttpConnection and nsSocketTransport. +// The mSocketTransport, mSocketIn, and mSocketOut of nsHttpConnection will be +// replaced by TLSTransportLayer. +// +// The input path of reading data from a socket is shown below. +// nsHttpConnection::OnSocketReadable +// nsHttpConnection::OnWriteSegment +// nsHttpConnection::mSocketIn->Read +// TLSTransportLayer::InputStreamWrapper::Read +// TLSTransportLayer::InputInternal +// TLSTransportLayer::InputStreamWrapper::ReadDirectly +// nsSocketInputStream::Read +// +// The output path of writing data to a socket is shown below. +// nsHttpConnection::OnSocketWritable +// nsHttpConnection::OnReadSegment +// TLSTransportLayer::OutputStreamWrapper::Write +// TLSTransportLayer::OutputInternal +// TLSTransportLayer::OutputStreamWrapper::WriteDirectly +// nsSocketOutputStream::Write + +// 9d6a3bc6-1f90-41d0-9b02-33ccd169052b +#define NS_TLSTRANSPORTLAYER_IID \ + { \ + 0x9d6a3bc6, 0x1f90, 0x41d0, { \ + 0x9b, 0x02, 0x33, 0xcc, 0xd1, 0x69, 0x05, 0x2b \ + } \ + } + +class TLSTransportLayer final : public nsISocketTransport, + public nsIInputStreamCallback, + public nsIOutputStreamCallback { + public: + NS_DECLARE_STATIC_IID_ACCESSOR(NS_TLSTRANSPORTLAYER_IID) + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSITRANSPORT + NS_DECL_NSISOCKETTRANSPORT + NS_DECL_NSIINPUTSTREAMCALLBACK + NS_DECL_NSIOUTPUTSTREAMCALLBACK + + explicit TLSTransportLayer(nsISocketTransport* aTransport, + nsIAsyncInputStream* aInputStream, + nsIAsyncOutputStream* aOutputStream, + nsIInputStreamCallback* aOwner); + bool Init(const char* aTLSHost, int32_t aTLSPort); + already_AddRefed GetInputStreamWrapper() { + nsCOMPtr stream = &mSocketInWrapper; + return stream.forget(); + } + already_AddRefed GetOutputStreamWrapper() { + nsCOMPtr stream = &mSocketOutWrapper; + return stream.forget(); + } + + bool HasDataToRecv(); + + void ReleaseOwner() { mOwner = nullptr; } + + private: + class InputStreamWrapper : public nsIAsyncInputStream { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIINPUTSTREAM + NS_DECL_NSIASYNCINPUTSTREAM + + explicit InputStreamWrapper(nsIAsyncInputStream* aInputStream, + TLSTransportLayer* aTransport); + + nsresult ReadDirectly(char* buf, uint32_t count, uint32_t* countRead); + nsresult Status() { return mStatus; } + void SetStatus(nsresult aStatus) { mStatus = aStatus; } + + private: + friend class TLSTransportLayer; + virtual ~InputStreamWrapper() = default; + nsresult ReturnDataFromBuffer(char* buf, uint32_t count, + uint32_t* countRead); + + nsCOMPtr mSocketIn; + + nsresult mStatus{NS_OK}; + // The lifetime of InputStreamWrapper and OutputStreamWrapper are bound to + // TLSTransportLayer, so using |mTransport| as a raw pointer should be safe. + TLSTransportLayer* MOZ_OWNING_REF mTransport; + }; + + class OutputStreamWrapper : public nsIAsyncOutputStream { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIOUTPUTSTREAM + NS_DECL_NSIASYNCOUTPUTSTREAM + + explicit OutputStreamWrapper(nsIAsyncOutputStream* aOutputStream, + TLSTransportLayer* aTransport); + + nsresult WriteDirectly(const char* buf, uint32_t count, + uint32_t* countWritten); + nsresult Status() { return mStatus; } + void SetStatus(nsresult aStatus) { mStatus = aStatus; } + + private: + friend class TLSTransportLayer; + virtual ~OutputStreamWrapper() = default; + static nsresult WriteFromSegments(nsIInputStream*, void*, const char*, + uint32_t offset, uint32_t count, + uint32_t* countRead); + + nsCOMPtr mSocketOut; + + nsresult mStatus{NS_OK}; + TLSTransportLayer* MOZ_OWNING_REF mTransport; + }; + + virtual ~TLSTransportLayer(); + bool DispatchRelease(); + + nsISocketTransport* Transport() { return mSocketTransport; } + + int32_t OutputInternal(const char* aBuf, int32_t aAmount); + int32_t InputInternal(char* aBuf, int32_t aAmount); + + static PRStatus GetPeerName(PRFileDesc* fd, PRNetAddr* addr); + static PRStatus GetSocketOption(PRFileDesc* fd, PRSocketOptionData* aOpt); + static PRStatus SetSocketOption(PRFileDesc* fd, + const PRSocketOptionData* data); + static int32_t Write(PRFileDesc* fd, const void* buf, int32_t amount); + static int32_t Read(PRFileDesc* fd, void* buf, int32_t amount); + static int32_t Send(PRFileDesc* fd, const void* buf, int32_t amount, + int flags, PRIntervalTime timeout); + static int32_t Recv(PRFileDesc* fd, void* buf, int32_t amount, int flags, + PRIntervalTime timeout); + static PRStatus Close(PRFileDesc* fd); + static int16_t Poll(PRFileDesc* fd, int16_t in_flags, int16_t* out_flags); + + nsCOMPtr mSocketTransport; + InputStreamWrapper mSocketInWrapper; + OutputStreamWrapper mSocketOutWrapper; + nsCOMPtr mTLSSocketControl; + nsCOMPtr mInputCallback; + nsCOMPtr mOutputCallback; + PRFileDesc* mFD{nullptr}; + nsCOMPtr mOwner; +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(TLSTransportLayer, NS_TLSTRANSPORTLAYER_IID) + +} // namespace mozilla::net + +inline nsISupports* ToSupports(mozilla::net::TLSTransportLayer* aTransport) { + return static_cast(aTransport); +} + +#endif diff --git a/netwerk/protocol/http/TRRServiceChannel.cpp b/netwerk/protocol/http/TRRServiceChannel.cpp new file mode 100644 index 0000000000..3a316d1e45 --- /dev/null +++ b/netwerk/protocol/http/TRRServiceChannel.cpp @@ -0,0 +1,1581 @@ +/* -*- 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" +#include "TRRService.h" + +namespace mozilla::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(nsIUploadChannel2) + NS_INTERFACE_MAP_ENTRY_CONCRETE(TRRServiceChannel) +NS_INTERFACE_MAP_END_INHERITING(HttpBaseChannel) + +TRRServiceChannel::TRRServiceChannel() + : HttpAsyncAborter(this), + mProxyRequest(nullptr, "TRRServiceChannel::mProxyRequest"), + mCurrentEventTarget(GetCurrentSerialEventTarget()) { + LOG(("TRRServiceChannel ctor [this=%p]\n", this)); +} + +TRRServiceChannel::~TRRServiceChannel() { + LOG(("TRRServiceChannel dtor [this=%p]\n", this)); +} + +NS_IMETHODIMP TRRServiceChannel::SetCanceledReason(const nsACString& aReason) { + return SetCanceledReasonImpl(aReason); +} + +NS_IMETHODIMP TRRServiceChannel::GetCanceledReason(nsACString& aReason) { + return GetCanceledReasonImpl(aReason); +} + +NS_IMETHODIMP +TRRServiceChannel::CancelWithReason(nsresult aStatus, + const nsACString& aReason) { + return CancelWithReasonImpl(aStatus, aReason); +} + +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(nsITransportSecurityInfo** securityInfo) { + NS_ENSURE_ARG_POINTER(securityInfo); + *securityInfo = do_AddRef(mSecurityInfo).take(); + 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::MaybeWaitForUploadStreamNormalization can only be used on + // main thread, so we can only return an error here. +#ifdef NIGHTLY_BUILD + MOZ_ASSERT(!LoadPendingUploadStreamNormalization()); +#endif + if (LoadPendingUploadStreamNormalization()) { + return NS_ERROR_FAILURE; + } + + if (!gHttpHandler->Active()) { + LOG((" after HTTP shutdown...")); + ReleaseListeners(); + return NS_ERROR_NOT_AVAILABLE; + } + + 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 mConnectionInfo is already supplied, we don't need to do proxy + // resolution again. + if (!mProxyInfo && !mConnectionInfo && + !(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; +} + +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, proxyInfo, OriginAttributes(), isHttps); + // TODO: Bug 1622778 for using AltService in socket process. + StoreAllowAltSvc(XRE_IsParentProcess() && LoadAllowAltSvc()); + bool http2Allowed = !gHttpHandler->IsHttp2Excluded(connInfo); + bool http3Allowed = Http3Allowed(); + if (!http3Allowed) { + mCaps |= NS_HTTP_DISALLOW_HTTP3; + } + + 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, 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)); + } 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.Flags() & nsIClassOfService::Leader) { + mCaps |= NS_HTTP_LOAD_AS_BLOCKING; + } + if (mClassOfService.Flags() & nsIClassOfService::Unblocked) { + mCaps |= NS_HTTP_LOAD_UNBLOCKED; + } + if (mClassOfService.Flags() & nsIClassOfService::UrgentStart && + gHttpHandler->IsUrgentStartEnabled()) { + mCaps |= NS_HTTP_URGENT_START; + SetPriority(nsISupportsPriority::PRIORITY_HIGHEST); + } + } + + 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_DISALLOW_HTTPS_RR; + } + + mCaps |= NS_HTTP_TRR_FLAGS_FROM_MODE(nsIRequest::GetTRRMode()); + + // Finalize ConnectionInfo flags before SpeculativeConnect + mConnectionInfo->SetAnonymous((mLoadFlags & LOAD_ANONYMOUS) != 0); + mConnectionInfo->SetPrivate(mPrivateBrowsing); + 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); + + if (mLoadFlags & LOAD_FRESH_CONNECTION) { + Telemetry::ScalarAdd( + Telemetry::ScalarID::NETWORKING_TRR_CONNECTION_CYCLE_COUNT, + NS_ConvertUTF8toUTF16(TRRService::ProviderKey()), 1); + nsresult rv = + gHttpHandler->ConnMgr()->DoSingleConnectionCleanup(mConnectionInfo); + LOG( + ("TRRServiceChannel::BeginConnect " + "DoSingleConnectionCleanup succeeded=%d %08x [this=%p]", + NS_SUCCEEDED(rv), static_cast(rv), this)); + } + + 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=%lu, inc=%d, prio=%d]\n", + this, mClassOfService.Flags(), mClassOfService.Incremental(), mPriority)); + + NS_ENSURE_TRUE(!mTransaction, NS_ERROR_ALREADY_INITIALIZED); + + nsresult rv; + + if (!LoadAllowSpdy()) { + mCaps |= NS_HTTP_DISALLOW_SPDY; + } + // Check a proxy info from mConnectionInfo. TRR channel may use a proxy that + // is set in mConnectionInfo but acutally the channel do not have mProxyInfo + // set. This can happend when network.trr.async_connInfo is true. + bool useNonDirectProxy = mConnectionInfo->ProxyInfo() + ? !mConnectionInfo->ProxyInfo()->IsDirect() + : false; + if (!Http3Allowed() || useNonDirectProxy) { + 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 (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, + mBrowserId, 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()); + nsIDNSService::DNSFlags dnsFlags = nsIDNSService::RESOLVE_DEFAULT_FLAGS; + if (mCaps & NS_HTTP_REFRESH_DNS) { + dnsFlags |= nsIDNSService::RESOLVE_BYPASS_CACHE; + } + nsresult rv = mDNSPrefetch->PrefetchHigh(dnsFlags); + NS_ENSURE_SUCCESS_VOID(rv); +} + +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); + } + + auto processHeaderTask = [altSvc, scheme, originHost, originPort, + userName(mUsername), + privateBrowsing(mPrivateBrowsing), callbacks, + proxyInfo, caps(mCaps)]() { + if (XRE_IsSocketProcess()) { + AltServiceChild::ProcessHeader(altSvc, scheme, originHost, originPort, + userName, privateBrowsing, callbacks, + proxyInfo, caps & NS_HTTP_DISALLOW_SPDY, + OriginAttributes()); + return; + } + + AltSvcMapping::ProcessHeader( + altSvc, scheme, originHost, originPort, userName, privateBrowsing, + 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 (mTransaction->ProxyConnectFailed()) { + LOG(("TRRServiceChannel proxy connect failed httpStatus: %d", + httpStatus)); + MOZ_ASSERT(mConnectionInfo->UsingConnect(), + "proxy connect failed but not using CONNECT?"); + nsresult rv = HttpProxyResponseToErrorCode(httpStatus); + mTransaction->DontReuseConnection(); + Cancel(rv); + return CallOnStartRequest(); + } + + 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()); + } + + if (mContentTypeHint.IsEmpty()) { + return NS_OK; + } + + // Make sure we set content-type on the old channel properly. + MOZ_ASSERT(mContentTypeHint.Equals("application/dns-message")); + + // Apply TRR specific settings. Note that we already know mContentTypeHint is + // "application/dns-message" here. + return TRR::SetupTRRServiceChannelInternal( + httpChannel, + mRequestHead.ParsedMethod() == nsHttpRequestHead::kMethod_Get, + mContentTypeHint); +} + +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, + bool aIsWarning) { + 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::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=%lu inc=%d", + this, mClassOfService.Flags(), mClassOfService.Incremental())); + + if (mTransaction) { + gHttpHandler->UpdateClassOfServiceOnTransaction(mTransaction, + mClassOfService); + } +} + +NS_IMETHODIMP +TRRServiceChannel::SetClassFlags(uint32_t inFlags) { + uint32_t previous = mClassOfService.Flags(); + mClassOfService.SetFlags(inFlags); + if (previous != mClassOfService.Flags()) { + OnClassOfServiceUpdated(); + } + return NS_OK; +} + +NS_IMETHODIMP +TRRServiceChannel::SetIncremental(bool inFlag) { + bool previous = mClassOfService.Incremental(); + mClassOfService.SetIncremental(inFlag); + if (previous != mClassOfService.Incremental()) { + OnClassOfServiceUpdated(); + } + return NS_OK; +} + +NS_IMETHODIMP +TRRServiceChannel::SetClassOfService(ClassOfService cos) { + ClassOfService previous = mClassOfService; + mClassOfService = cos; + if (previous != mClassOfService) { + OnClassOfServiceUpdated(); + } + return NS_OK; +} + +NS_IMETHODIMP +TRRServiceChannel::AddClassFlags(uint32_t inFlags) { + uint32_t previous = mClassOfService.Flags(); + mClassOfService.SetFlags(inFlags | mClassOfService.Flags()); + if (previous != mClassOfService.Flags()) { + OnClassOfServiceUpdated(); + } + return NS_OK; +} + +NS_IMETHODIMP +TRRServiceChannel::ClearClassFlags(uint32_t inFlags) { + uint32_t previous = mClassOfService.Flags(); + mClassOfService.SetFlags(~inFlags & mClassOfService.Flags()); + if (previous != mClassOfService.Flags()) { + 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 = do_AddRef(mProxyInfo).take(); + } else { + *result = do_AddRef(mConnectionInfo->ProxyInfo()).take(); + } + 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)) { + 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 mozilla::net diff --git a/netwerk/protocol/http/TRRServiceChannel.h b/netwerk/protocol/http/TRRServiceChannel.h new file mode 100644 index 0000000000..0d726deb57 --- /dev/null +++ b/netwerk/protocol/http/TRRServiceChannel.h @@ -0,0 +1,177 @@ +/* -*- 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::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 SetCanceledReason(const nsACString& aReason) override; + NS_IMETHOD GetCanceledReason(nsACString& aReason) override; + NS_IMETHOD CancelWithReason(nsresult status, + const nsACString& reason) override; + 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(nsITransportSecurityInfo** aSecurityInfo) override; + NS_IMETHOD AsyncOpen(nsIStreamListener* aListener) override; + + NS_IMETHOD LogBlockedCORSRequest(const nsAString& aMessage, + const nsACString& aCategory, + bool aIsWarning) override; + NS_IMETHOD LogMimeTypeMismatch(const nsACString& aMessageName, bool aWarning, + const nsAString& aURL, + const nsAString& aContentType) 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; + NS_IMETHOD SetIncremental(bool inFlag) override; + NS_IMETHOD SetClassOfService(ClassOfService cos) override; + // nsIResumableChannel + NS_IMETHOD ResumeAt(uint64_t startPos, const nsACString& entityID) override; + NS_IMETHOD SetEarlyHintObserver(nsIEarlyHintObserver* aObserver) override { + return NS_OK; + } + NS_IMETHOD SetWebTransportSessionEventListener( + WebTransportSessionEventListener* aListener) override { + return NS_OK; + } + + [[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); + 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; + // Skip this check for TRRServiceChannel. + virtual bool ShouldTaintReplacementChannelOrigin( + nsIChannel* aNewChannel, uint32_t aRedirectFlags) override { + return false; + } + virtual bool SameOriginWithOriginalUri(nsIURI* aURI) override; + bool DispatchRelease(); + + nsCString mUsername; + + // Needed for accurate DNS timing + RefPtr mDNSPrefetch; + + nsCOMPtr mTransactionPump; + RefPtr mTransaction; + uint32_t mPushedStreamId{0}; + RefPtr mTransWithPushedStream; + DataMutex> mProxyRequest; + nsCOMPtr mCurrentEventTarget; + + friend class HttpAsyncAborter; + friend class nsHttpHandler; +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(TRRServiceChannel, NS_TRRSERVICECHANNEL_IID) + +} // namespace mozilla::net + +#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..74645e2bff --- /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; + TimeStamp transactionPending; +}; + +struct ResourceTimingStruct : TimingStruct { + TimeStamp fetchStart; + TimeStamp redirectStart; + TimeStamp redirectEnd; + uint64_t transferSize; + uint64_t encodedBodySize; + + // 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/TlsHandshaker.cpp b/netwerk/protocol/http/TlsHandshaker.cpp new file mode 100644 index 0000000000..5288fb939d --- /dev/null +++ b/netwerk/protocol/http/TlsHandshaker.cpp @@ -0,0 +1,336 @@ +/* -*- 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 "TlsHandshaker.h" +#include "mozilla/StaticPrefs_network.h" +#include "nsHttpConnection.h" +#include "nsHttpConnectionInfo.h" +#include "nsHttpHandler.h" +#include "nsITLSSocketControl.h" + +#define TLS_EARLY_DATA_NOT_AVAILABLE 0 +#define TLS_EARLY_DATA_AVAILABLE_BUT_NOT_USED 1 +#define TLS_EARLY_DATA_AVAILABLE_AND_USED 2 + +namespace mozilla::net { + +NS_IMPL_ISUPPORTS(TlsHandshaker, nsITlsHandshakeCallbackListener) + +TlsHandshaker::TlsHandshaker(nsHttpConnectionInfo* aInfo, + nsHttpConnection* aOwner) + : mConnInfo(aInfo), mOwner(aOwner) { + LOG(("TlsHandshaker ctor %p", this)); +} + +TlsHandshaker::~TlsHandshaker() { LOG(("TlsHandshaker dtor %p", this)); } + +NS_IMETHODIMP +TlsHandshaker::CertVerificationDone() { + LOG(("TlsHandshaker::CertVerificationDone mOwner=%p", mOwner.get())); + if (mOwner) { + Unused << mOwner->ResumeSend(); + } + return NS_OK; +} + +NS_IMETHODIMP +TlsHandshaker::ClientAuthCertificateSelected() { + LOG(("TlsHandshaker::ClientAuthCertificateSelected mOwner=%p", mOwner.get())); + if (mOwner) { + Unused << mOwner->ResumeSend(); + } + return NS_OK; +} + +NS_IMETHODIMP +TlsHandshaker::HandshakeDone() { + LOG(("TlsHandshaker::HandshakeDone mOwner=%p", mOwner.get())); + if (mOwner) { + mTlsHandshakeComplitionPending = true; + + // HandshakeDone needs to be dispatched so that it is not called inside + // nss locks. + RefPtr self(this); + NS_DispatchToCurrentThread(NS_NewRunnableFunction( + "TlsHandshaker::HandshakeDoneInternal", [self{std::move(self)}]() { + if (self->mTlsHandshakeComplitionPending && self->mOwner) { + self->mOwner->HandshakeDoneInternal(); + self->mTlsHandshakeComplitionPending = false; + } + })); + } + return NS_OK; +} + +void TlsHandshaker::SetupSSL(bool aInSpdyTunnel, bool aForcePlainText) { + if (!mOwner) { + return; + } + + LOG1(("TlsHandshaker::SetupSSL %p caps=0x%X %s\n", mOwner.get(), + mOwner->TransactionCaps(), 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() || aForcePlainText) { + return; + } + + // if we are connected to the proxy with TLS, start the TLS + // flow immediately without waiting for a CONNECT sequence. + DebugOnly rv{}; + if (aInSpdyTunnel) { + rv = InitSSLParams(false, true); + } else { + bool usingHttpsProxy = mConnInfo->UsingHttpsProxy(); + rv = InitSSLParams(usingHttpsProxy, usingHttpsProxy); + } + MOZ_ASSERT(NS_SUCCEEDED(rv)); +} + +nsresult TlsHandshaker::InitSSLParams(bool connectingToProxy, + bool proxyStartSSL) { + LOG(("TlsHandshaker::InitSSLParams [mOwner=%p] connectingToProxy=%d\n", + mOwner.get(), connectingToProxy)); + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + if (!mOwner) { + return NS_ERROR_ABORT; + } + + nsCOMPtr ssl; + mOwner->GetTLSSocketControl(getter_AddRefs(ssl)); + if (!ssl) { + return NS_ERROR_FAILURE; + } + + // If proxy is use or 0RTT is excluded for a origin, don't use early-data. + if (mConnInfo->UsingProxy() || gHttpHandler->Is0RttTcpExcluded(mConnInfo)) { + ssl->DisableEarlyData(); + } + + if (proxyStartSSL) { + nsresult rv = ssl->ProxyStartSSL(); + if (NS_FAILED(rv)) { + return rv; + } + } + + if (NS_SUCCEEDED( + SetupNPNList(ssl, mOwner->TransactionCaps(), connectingToProxy)) && + NS_SUCCEEDED(ssl->SetHandshakeCallbackListener(this))) { + LOG(("InitSSLParams Setting up SPDY Negotiation OK mOwner=%p", + mOwner.get())); + mNPNComplete = false; + } + + return NS_OK; +} + +// 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 TlsHandshaker::SetupNPNList(nsITLSSocketControl* ssl, uint32_t caps, + bool connectingToProxy) { + nsTArray protocolArray; + + // 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 (StaticPrefs::network_http_http2_enabled() && + (connectingToProxy || !(caps & NS_HTTP_DISALLOW_SPDY)) && + !(connectingToProxy && (caps & NS_HTTP_DISALLOW_HTTP2_PROXY))) { + LOG(("nsHttpConnection::SetupSSL Allow SPDY NPN selection")); + const SpdyInformation* info = gHttpHandler->SpdyInfo(); + if (info->ALPNCallbacks(ssl)) { + protocolArray.AppendElement(info->VersionString); + } + } else { + LOG(("nsHttpConnection::SetupSSL Disallow SPDY NPN selection")); + } + + nsresult rv = ssl->SetNPNList(protocolArray); + LOG(("TlsHandshaker::SetupNPNList %p %" PRIx32 "\n", mOwner.get(), + static_cast(rv))); + return rv; +} + +// Checks if TLS handshake is needed and it is responsible to move it forward. +bool TlsHandshaker::EnsureNPNComplete() { + if (!mOwner) { + mNPNComplete = true; + return true; + } + + nsCOMPtr transport = mOwner->Transport(); + MOZ_ASSERT(transport); + if (!transport) { + // this cannot happen + mNPNComplete = true; + return true; + } + + if (mNPNComplete) { + return true; + } + + if (mTlsHandshakeComplitionPending) { + return false; + } + + nsCOMPtr ssl; + mOwner->GetTLSSocketControl(getter_AddRefs(ssl)); + if (!ssl) { + FinishNPNSetup(false, false); + return true; + } + + if (!m0RTTChecked) { + // We reuse m0RTTChecked. We want to send this status only once. + RefPtr transaction = mOwner->Transaction(); + nsCOMPtr transport = mOwner->Transport(); + if (transaction && transport) { + transaction->OnTransportStatus(transport, + NS_NET_STATUS_TLS_HANDSHAKE_STARTING, 0); + } + } + + LOG(("TlsHandshaker::EnsureNPNComplete [mOwner=%p] drive TLS handshake", + mOwner.get())); + nsresult rv = ssl->DriveHandshake(); + if (NS_FAILED(rv) && rv != NS_BASE_STREAM_WOULD_BLOCK) { + FinishNPNSetup(false, true); + return true; + } + + Check0RttEnabled(ssl); + return false; +} + +void TlsHandshaker::EarlyDataDone() { + if (mEarlyDataState == EarlyData::USED) { + mEarlyDataState = EarlyData::DONE_USED; + } else if (mEarlyDataState == EarlyData::CANNOT_BE_USED) { + mEarlyDataState = EarlyData::DONE_CANNOT_BE_USED; + } else if (mEarlyDataState == EarlyData::NOT_AVAILABLE) { + mEarlyDataState = EarlyData::DONE_NOT_AVAILABLE; + } +} + +void TlsHandshaker::FinishNPNSetup(bool handshakeSucceeded, + bool hasSecurityInfo) { + LOG(("TlsHandshaker::FinishNPNSetup mOwner=%p", mOwner.get())); + mNPNComplete = true; + + mOwner->PostProcessNPNSetup(handshakeSucceeded, hasSecurityInfo, + EarlyDataUsed()); + EarlyDataDone(); +} + +void TlsHandshaker::Check0RttEnabled(nsITLSSocketControl* ssl) { + if (!mOwner) { + return; + } + + if (m0RTTChecked) { + return; + } + + m0RTTChecked = true; + + if (mConnInfo->UsingProxy()) { + return; + } + + // 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. + if (NS_FAILED(ssl->GetAlpnEarlySelection(mEarlyNegotiatedALPN))) { + LOG1( + ("TlsHandshaker::Check0RttEnabled %p - " + "early selected alpn not available", + mOwner.get())); + } else { + mOwner->ChangeConnectionState(ConnectionState::ZERORTT); + LOG1( + ("TlsHandshaker::Check0RttEnabled %p -" + "early selected alpn: %s", + mOwner.get(), mEarlyNegotiatedALPN.get())); + const SpdyInformation* info = gHttpHandler->SpdyInfo(); + if (!mEarlyNegotiatedALPN.Equals(info->VersionString)) { + // This is the HTTP/1 case. + // Check if early-data is allowed for this transaction. + RefPtr transaction = mOwner->Transaction(); + if (transaction && transaction->Do0RTT()) { + LOG( + ("TlsHandshaker::Check0RttEnabled [mOwner=%p] - We " + "can do 0RTT (http/1)!", + mOwner.get())); + mEarlyDataState = EarlyData::USED; + } else { + mEarlyDataState = EarlyData::CANNOT_BE_USED; + // Poll for read now. Polling for write will cause us to busy wait. + // When the handshake is done the polling flags will be set correctly. + Unused << mOwner->ResumeRecv(); + } + } else { + // We have h2, we can at least 0-RTT the preamble and opening + // SETTINGS, etc, and maybe some of the first request + LOG( + ("TlsHandshaker::Check0RttEnabled [mOwner=%p] - Starting " + "0RTT for h2!", + mOwner.get())); + mEarlyDataState = EarlyData::USED; + mOwner->Start0RTTSpdy(info->Version); + } + } +} + +void TlsHandshaker::EarlyDataTelemetry(int16_t tlsVersion, + bool earlyDataAccepted, + int64_t aContentBytesWritten0RTT) { + // Send the 0RTT telemetry only for tls1.3 + if (tlsVersion > nsITLSSocketControl::TLS_VERSION_1_2) { + Telemetry::Accumulate(Telemetry::TLS_EARLY_DATA_NEGOTIATED, + (mEarlyDataState == EarlyData::NOT_AVAILABLE) + ? TLS_EARLY_DATA_NOT_AVAILABLE + : ((mEarlyDataState == EarlyData::USED) + ? TLS_EARLY_DATA_AVAILABLE_AND_USED + : TLS_EARLY_DATA_AVAILABLE_BUT_NOT_USED)); + if (EarlyDataUsed()) { + Telemetry::Accumulate(Telemetry::TLS_EARLY_DATA_ACCEPTED, + earlyDataAccepted); + } + if (earlyDataAccepted) { + Telemetry::Accumulate(Telemetry::TLS_EARLY_DATA_BYTES_WRITTEN, + aContentBytesWritten0RTT); + } + } +} + +} // namespace mozilla::net diff --git a/netwerk/protocol/http/TlsHandshaker.h b/netwerk/protocol/http/TlsHandshaker.h new file mode 100644 index 0000000000..fc01b07518 --- /dev/null +++ b/netwerk/protocol/http/TlsHandshaker.h @@ -0,0 +1,95 @@ +/* -*- 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 TlsHandshaker_h__ +#define TlsHandshaker_h__ + +#include "nsITlsHandshakeListener.h" + +class nsISocketTransport; +class nsITLSSocketControl; + +namespace mozilla::net { + +class nsHttpConnection; +class nsHttpConnectionInfo; + +class TlsHandshaker : public nsITlsHandshakeCallbackListener { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSITLSHANDSHAKECALLBACKLISTENER + + TlsHandshaker(nsHttpConnectionInfo* aInfo, nsHttpConnection* aOwner); + + void SetupSSL(bool aInSpdyTunnel, bool aForcePlainText); + [[nodiscard]] nsresult InitSSLParams(bool connectingToProxy, + bool ProxyStartSSL); + [[nodiscard]] nsresult SetupNPNList(nsITLSSocketControl* ssl, uint32_t caps, + bool connectingToProxy); + // Makes certain the SSL handshake is complete and NPN negotiation + // has had a chance to happen + [[nodiscard]] bool EnsureNPNComplete(); + void FinishNPNSetup(bool handshakeSucceeded, bool hasSecurityInfo); + bool EarlyDataAvailable() const { + return mEarlyDataState == EarlyData::USED || + mEarlyDataState == EarlyData::CANNOT_BE_USED; + } + bool EarlyDataWasAvailable() const { + return mEarlyDataState != EarlyData::NOT_AVAILABLE && + mEarlyDataState != EarlyData::DONE_NOT_AVAILABLE; + } + bool EarlyDataUsed() const { return mEarlyDataState == EarlyData::USED; } + bool EarlyDataCanNotBeUsed() const { + return mEarlyDataState == EarlyData::CANNOT_BE_USED; + } + void EarlyDataDone(); + void EarlyDataTelemetry(int16_t tlsVersion, bool earlyDataAccepted, + int64_t aContentBytesWritten0RTT); + + bool NPNComplete() const { return mNPNComplete; } + bool SetupSSLCalled() const { return mSetupSSLCalled; } + bool TlsHandshakeComplitionPending() const { + return mTlsHandshakeComplitionPending; + } + const nsCString& EarlyNegotiatedALPN() const { return mEarlyNegotiatedALPN; } + void SetNPNComplete() { mNPNComplete = true; } + void NotifyClose() { + mTlsHandshakeComplitionPending = false; + mOwner = nullptr; + } + + private: + virtual ~TlsHandshaker(); + + void Check0RttEnabled(nsITLSSocketControl* ssl); + + // SPDY related + bool mSetupSSLCalled{false}; + bool mNPNComplete{false}; + + bool mTlsHandshakeComplitionPending{false}; + // Helper variable for 0RTT handshake; + // Possible 0RTT has been checked. + bool m0RTTChecked{false}; + // 0RTT data state. + enum EarlyData { + NOT_AVAILABLE, + USED, + CANNOT_BE_USED, + DONE_NOT_AVAILABLE, + DONE_USED, + DONE_CANNOT_BE_USED, + }; + EarlyData mEarlyDataState{EarlyData::NOT_AVAILABLE}; + nsCString mEarlyNegotiatedALPN; + RefPtr mConnInfo; + // nsHttpConnection and TlsHandshaker create a reference cycle. To break this + // cycle, NotifyClose() needs to be called in nsHttpConnection::Close(). + RefPtr mOwner; +}; + +} // namespace mozilla::net + +#endif // TlsHandshaker_h__ diff --git a/netwerk/protocol/http/WellKnownOpportunisticUtils.sys.mjs b/netwerk/protocol/http/WellKnownOpportunisticUtils.sys.mjs new file mode 100644 index 0000000000..c37eb104d0 --- /dev/null +++ b/netwerk/protocol/http/WellKnownOpportunisticUtils.sys.mjs @@ -0,0 +1,26 @@ +/* -*- 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/. */ + +export 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; + }, +}; diff --git a/netwerk/protocol/http/binary_http/Cargo.toml b/netwerk/protocol/http/binary_http/Cargo.toml new file mode 100644 index 0000000000..97db55bc33 --- /dev/null +++ b/netwerk/protocol/http/binary_http/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "binary_http" +version = "0.1.0" +edition = "2021" + +[dependencies] +nserror = { path = "../../../../xpcom/rust/nserror" } +nsstring = { path = "../../../../xpcom/rust/nsstring" } +bhttp = "0.3" +thin-vec = { version = "0.2.1", features = ["gecko-ffi"] } +xpcom = { path = "../../../../xpcom/rust/xpcom" } diff --git a/netwerk/protocol/http/binary_http/src/binary_http.h b/netwerk/protocol/http/binary_http/src/binary_http.h new file mode 100644 index 0000000000..2051a00e83 --- /dev/null +++ b/netwerk/protocol/http/binary_http/src/binary_http.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/. */ + +#ifndef _binary_http_h_ +#define _binary_http_h_ + +#include "nsISupportsUtils.h" // for nsresult, etc. + +// {b43b3f73-8160-4ab2-9f5d-4129a9708081} +#define NS_BINARY_HTTP_CID \ + { \ + 0xb43b3f73, 0x8160, 0x4ab2, { \ + 0x9f, 0x5d, 0x41, 0x29, 0xa9, 0x70, 0x80, 0x81 \ + } \ + } + +extern "C" { +nsresult binary_http_constructor(REFNSIID iid, void** result); +}; + +#endif // _binary_http_h_ diff --git a/netwerk/protocol/http/binary_http/src/lib.rs b/netwerk/protocol/http/binary_http/src/lib.rs new file mode 100644 index 0000000000..fab357dcca --- /dev/null +++ b/netwerk/protocol/http/binary_http/src/lib.rs @@ -0,0 +1,264 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +extern crate bhttp; +extern crate nserror; +extern crate nsstring; +extern crate thin_vec; +#[macro_use] +extern crate xpcom; + +use bhttp::{Message, Mode}; +use nserror::{nsresult, NS_ERROR_FAILURE, NS_ERROR_INVALID_ARG, NS_ERROR_UNEXPECTED, NS_OK}; +use nsstring::{nsACString, nsCString}; +use std::io::Cursor; +use thin_vec::ThinVec; +use xpcom::interfaces::{nsIBinaryHttpRequest, nsIBinaryHttpResponse}; +use xpcom::RefPtr; + +enum HeaderComponent { + Name, + Value, +} + +// Extracts either the names or the values of a slice of header (name, value) pairs. +fn extract_header_components( + headers: &[(Vec, Vec)], + component: HeaderComponent, +) -> ThinVec { + let mut header_components = ThinVec::with_capacity(headers.len()); + for (name, value) in headers { + match component { + HeaderComponent::Name => header_components.push(nsCString::from(name.clone())), + HeaderComponent::Value => header_components.push(nsCString::from(value.clone())), + } + } + header_components +} + +#[xpcom(implement(nsIBinaryHttpRequest), atomic)] +struct BinaryHttpRequest { + method: Vec, + scheme: Vec, + authority: Vec, + path: Vec, + headers: Vec<(Vec, Vec)>, + content: Vec, +} + +impl BinaryHttpRequest { + xpcom_method!(get_method => GetMethod() -> nsACString); + fn get_method(&self) -> Result { + Ok(nsCString::from(self.method.clone())) + } + + xpcom_method!(get_scheme => GetScheme() -> nsACString); + fn get_scheme(&self) -> Result { + Ok(nsCString::from(self.scheme.clone())) + } + + xpcom_method!(get_authority => GetAuthority() -> nsACString); + fn get_authority(&self) -> Result { + Ok(nsCString::from(self.authority.clone())) + } + + xpcom_method!(get_path => GetPath() -> nsACString); + fn get_path(&self) -> Result { + Ok(nsCString::from(self.path.clone())) + } + + xpcom_method!(get_content => GetContent() -> ThinVec); + fn get_content(&self) -> Result, nsresult> { + Ok(self.content.clone().into_iter().collect()) + } + + xpcom_method!(get_header_names => GetHeaderNames() -> ThinVec); + fn get_header_names(&self) -> Result, nsresult> { + Ok(extract_header_components( + &self.headers, + HeaderComponent::Name, + )) + } + + xpcom_method!(get_header_values => GetHeaderValues() -> ThinVec); + fn get_header_values(&self) -> Result, nsresult> { + Ok(extract_header_components( + &self.headers, + HeaderComponent::Value, + )) + } +} + +#[xpcom(implement(nsIBinaryHttpResponse), atomic)] +struct BinaryHttpResponse { + status: u16, + headers: Vec<(Vec, Vec)>, + content: Vec, +} + +impl BinaryHttpResponse { + xpcom_method!(get_status => GetStatus() -> u16); + fn get_status(&self) -> Result { + Ok(self.status) + } + + xpcom_method!(get_content => GetContent() -> ThinVec); + fn get_content(&self) -> Result, nsresult> { + Ok(self.content.clone().into_iter().collect()) + } + + xpcom_method!(get_header_names => GetHeaderNames() -> ThinVec); + fn get_header_names(&self) -> Result, nsresult> { + Ok(extract_header_components( + &self.headers, + HeaderComponent::Name, + )) + } + + xpcom_method!(get_header_values => GetHeaderValues() -> ThinVec); + fn get_header_values(&self) -> Result, nsresult> { + Ok(extract_header_components( + &self.headers, + HeaderComponent::Value, + )) + } +} + +#[xpcom(implement(nsIBinaryHttp), atomic)] +struct BinaryHttp {} + +impl BinaryHttp { + xpcom_method!(encode_request => EncodeRequest(request: *const nsIBinaryHttpRequest) -> ThinVec); + fn encode_request(&self, request: &nsIBinaryHttpRequest) -> Result, nsresult> { + let mut method = nsCString::new(); + unsafe { request.GetMethod(&mut *method) }.to_result()?; + let mut scheme = nsCString::new(); + unsafe { request.GetScheme(&mut *scheme) }.to_result()?; + let mut authority = nsCString::new(); + unsafe { request.GetAuthority(&mut *authority) }.to_result()?; + let mut path = nsCString::new(); + unsafe { request.GetPath(&mut *path) }.to_result()?; + let mut message = Message::request( + method.to_vec(), + scheme.to_vec(), + authority.to_vec(), + path.to_vec(), + ); + let mut header_names = ThinVec::new(); + unsafe { request.GetHeaderNames(&mut header_names) }.to_result()?; + let mut header_values = ThinVec::with_capacity(header_names.len()); + unsafe { request.GetHeaderValues(&mut header_values) }.to_result()?; + if header_names.len() != header_values.len() { + return Err(NS_ERROR_INVALID_ARG); + } + for (name, value) in header_names.iter().zip(header_values.iter()) { + message.put_header(name.to_vec(), value.to_vec()); + } + let mut content = ThinVec::new(); + unsafe { request.GetContent(&mut content) }.to_result()?; + message.write_content(content); + let mut encoded = ThinVec::new(); + message + .write_bhttp(Mode::KnownLength, &mut encoded) + .map_err(|_| NS_ERROR_FAILURE)?; + Ok(encoded) + } + + xpcom_method!(decode_response => DecodeResponse(response: *const ThinVec) -> *const nsIBinaryHttpResponse); + fn decode_response( + &self, + response: &ThinVec, + ) -> Result, nsresult> { + let mut cursor = Cursor::new(response); + let decoded = Message::read_bhttp(&mut cursor).map_err(|_| NS_ERROR_UNEXPECTED)?; + let status = decoded.control().status().ok_or(NS_ERROR_UNEXPECTED)?; + let headers = decoded + .header() + .iter() + .map(|field| (field.name().to_vec(), field.value().to_vec())) + .collect(); + let content = decoded.content().to_vec(); + let binary_http_response = BinaryHttpResponse::allocate(InitBinaryHttpResponse { + status, + headers, + content, + }); + binary_http_response + .query_interface::() + .ok_or(NS_ERROR_FAILURE) + } + + xpcom_method!(decode_request => DecodeRequest(request: *const ThinVec) -> *const nsIBinaryHttpRequest); + fn decode_request( + &self, + request: &ThinVec, + ) -> Result, nsresult> { + let mut cursor = Cursor::new(request); + let decoded = Message::read_bhttp(&mut cursor).map_err(|_| NS_ERROR_UNEXPECTED)?; + let method = decoded + .control() + .method() + .ok_or(NS_ERROR_UNEXPECTED)? + .to_vec(); + let scheme = decoded + .control() + .scheme() + .ok_or(NS_ERROR_UNEXPECTED)? + .to_vec(); + // authority and path can be empty, in which case we return empty arrays + let authority = decoded.control().authority().unwrap_or(&[]).to_vec(); + let path = decoded.control().path().unwrap_or(&[]).to_vec(); + let headers = decoded + .header() + .iter() + .map(|field| (field.name().to_vec(), field.value().to_vec())) + .collect(); + let content = decoded.content().to_vec(); + let binary_http_request = BinaryHttpRequest::allocate(InitBinaryHttpRequest { + method, + scheme, + authority, + path, + headers, + content, + }); + binary_http_request + .query_interface::() + .ok_or(NS_ERROR_FAILURE) + } + + xpcom_method!(encode_response => EncodeResponse(response: *const nsIBinaryHttpResponse) -> ThinVec); + fn encode_response(&self, response: &nsIBinaryHttpResponse) -> Result, nsresult> { + let mut status = 0; + unsafe { response.GetStatus(&mut status) }.to_result()?; + let mut message = Message::response(status); + let mut header_names = ThinVec::new(); + unsafe { response.GetHeaderNames(&mut header_names) }.to_result()?; + let mut header_values = ThinVec::with_capacity(header_names.len()); + unsafe { response.GetHeaderValues(&mut header_values) }.to_result()?; + if header_names.len() != header_values.len() { + return Err(NS_ERROR_INVALID_ARG); + } + for (name, value) in header_names.iter().zip(header_values.iter()) { + message.put_header(name.to_vec(), value.to_vec()); + } + let mut content = ThinVec::new(); + unsafe { response.GetContent(&mut content) }.to_result()?; + message.write_content(content); + let mut encoded = ThinVec::new(); + message + .write_bhttp(Mode::KnownLength, &mut encoded) + .map_err(|_| NS_ERROR_FAILURE)?; + Ok(encoded) + } +} + +#[no_mangle] +pub extern "C" fn binary_http_constructor( + iid: *const xpcom::nsIID, + result: *mut *mut xpcom::reexports::libc::c_void, +) -> nsresult { + let binary_http = BinaryHttp::allocate(InitBinaryHttp {}); + unsafe { binary_http.QueryInterface(iid, result) } +} diff --git a/netwerk/protocol/http/components.conf b/netwerk/protocol/http/components.conf new file mode 100644 index 0000000000..8c489d0105 --- /dev/null +++ b/netwerk/protocol/http/components.conf @@ -0,0 +1,33 @@ +# -*- 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'], + 'esModule': 'resource://gre/modules/WellKnownOpportunisticUtils.sys.mjs', + 'constructor': 'WellKnownOpportunisticUtils', + }, + { + 'cid': '{b43b3f73-8160-4ab2-9f5d-4129a9708081}', + 'contract_ids': ['@mozilla.org/network/binary-http;1'], + 'headers': ['/netwerk/protocol/http/binary_http/src/binary_http.h'], + 'legacy_constructor': 'binary_http_constructor', + }, + { + 'cid': '{d581149e-3319-4563-b95e-46c64af5c4e8}', + 'contract_ids': ['@mozilla.org/network/oblivious-http;1'], + 'headers': ['/netwerk/protocol/http/oblivious_http/src/oblivious_http.h'], + 'legacy_constructor': 'oblivious_http_constructor', + }, + { + 'cid': '{b1f08d56-fca6-4290-9500-d5168dc9d8c3}', + 'contract_ids': ['@mozilla.org/network/oblivious-http-service;1'], + 'interfaces': ['nsIObliviousHttpService'], + 'type': 'mozilla::net::ObliviousHttpService', + 'headers': ['/netwerk/protocol/http/ObliviousHttpService.h'], + }, +] 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/metrics.yaml b/netwerk/protocol/http/metrics.yaml new file mode 100644 index 0000000000..5c92eb2afd --- /dev/null +++ b/netwerk/protocol/http/metrics.yaml @@ -0,0 +1,275 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +# Adding a new metric? We have docs for that! +# https://firefox-source-docs.mozilla.org/toolkit/components/glean/user/new_definitions_file.html + +--- +$schema: moz://mozilla.org/schemas/glean/metrics/2-0-0 +$tags: + - 'Core :: Networking: HTTP' + + +netwerk: + early_hints: + type: labeled_counter + labels: + - stylesheet + - fonts + - scripts + - fetch + - image + - other + description: > + Counts the different type of resources that are sent for early hints. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1772124 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1772124 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1797695 + notification_emails: + - necko@mozilla.com + - vgosu@mozilla.com + expires: never + eh_link_type: + type: labeled_counter + labels: + - dns-prefetch + - icon + - modulepreload + - preconnect + - prefetch + - preload + - prerender + - stylesheet + - other + description: > + Counts different type of link headers that are sent in early hint + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1797936 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1797936 + notification_emails: + - necko@mozilla.com + - manuel@mozilla.com + expires: never + +network: + data_size_per_type: + type: labeled_counter + labels: + - text_html + - text_css + - text_json + - text_plain + - text_javascript + - text_other + - audio + - video + - multipart + - icon + - image + - ocsp + - xpinstall + - wasm + - pdf + - octet_stream + - proxy + - compressed + - x509 + - application_other + description: > + Number of KB we transferred keyed by "contentType" + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1808695 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1808695 + notification_emails: + - necko@mozilla.com + - rtestard@mozilla.com + expires: 130 + telemetry_mirror: NETWORKING_DATA_TRANSFERRED_PER_CONTENT_TYPE + no_lint: + - COMMON_PREFIX + + data_size_pb_per_type: + type: labeled_counter + labels: + - text_html + - text_css + - text_json + - text_plain + - text_javascript + - text_other + - audio + - video + - multipart + - icon + - image + - ocsp + - xpinstall + - wasm + - pdf + - octet_stream + - proxy + - compressed + - x509 + - application_other + description: > + Number of KB we transferred keyed by "contentType" + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1808695 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1808695 + notification_emails: + - necko@mozilla.com + - rtestard@mozilla.com + expires: 130 + telemetry_mirror: NETWORKING_DATA_TRANSFERRED_PB_PER_CONTENT_TYPE + no_lint: + - COMMON_PREFIX + + cors_authorization_header: + type: labeled_counter + labels: + - allowed + - disallowed + - covered_by_wildcard + description: > + Count how many times we see `Authorization` header in + `Access-Control-Request-Headers` header and the possible outcomes. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1687364 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1687364 + notification_emails: + - necko@mozilla.com + - kershaw@mozilla.com + expires: 130 + + cache_hit_time: + type: timing_distribution + time_unit: millisecond + telemetry_mirror: NETWORK_CACHE_V2_HIT_TIME_MS + description: > + Time to open existing cache entry file. + (Migrated from the geckoview metric of the same name). + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1489524 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1580077 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1877839 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1580077#c10 + notification_emails: + - necko@mozilla.com + expires: never + + font_download_end: + type: timing_distribution + time_unit: millisecond + telemetry_mirror: WEBFONT_DOWNLOAD_TIME_AFTER_START + description: > + Time after navigationStart that all webfont downloads are completed. + (Migrated from the geckoview metric of the same name). + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1877839 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1580077 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1877839 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1580077#c10 + notification_emails: + - perf-telemetry-alerts@mozilla.com + - necko@mozilla.com + - bdekoz@mozilla.com + expires: never + + first_from_cache: + type: timing_distribution + time_unit: millisecond + description: > + In the HTTP page channel, time from connection open to cache read start. + Corresponds to Legacy histogram HTTP_PAGE_OPEN_TO_FIRST_FROM_CACHE_V2 in + Desktop. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1580077 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1877839 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1580077#c10 + notification_emails: + - necko@mozilla.com + expires: never + + tcp_connection: + type: timing_distribution + time_unit: millisecond + description: > + In the HTTP page channel, time from the TCP SYN packet is received to + the connection is established and ready for HTTP. + Corresponds to Legacy histogram HTTP_PAGE_TCP_CONNECTION_2 in Desktop + (Migrated from the geckoview metric of the same name). + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=772589 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1580077 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1877839 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1580077#c10 + notification_emails: + - necko@mozilla.com + - vgosu@mozilla.com + expires: never + + dns_start: + type: timing_distribution + time_unit: millisecond + description: > + In the HTTP page channel, time from connection open to the DNS request + being issued. + Corresponds to Legacy histogram HTTP_PAGE_DNS_ISSUE_TIME in Desktop. + (Migrated from the geckoview metric of the same name). + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1580077 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1877839 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1580077#c10 + notification_emails: + - necko@mozilla.com + - vgosu@mozilla.com + expires: never + + dns_end: + type: timing_distribution + time_unit: millisecond + description: > + In the HTTP page channel, time from the DNS request being issued to + the response. + Corresponds to Legacy histogram HTTP_PAGE_DNS_LOOKUP_TIME in Desktop. + (Migrated from the geckoview metric of the same name). + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1580077 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1877839 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1580077#c10 + notification_emails: + - necko@mozilla.com + - vgosu@mozilla.com + expires: never + + tls_handshake: + type: timing_distribution + time_unit: millisecond + description: > + In the HTTP page channel, time from after the TCP SYN packet is + received to the secure connection is established and ready for HTTP. + Corresponds to Legacy histogram HTTP_PAGE_TLS_HANDSHAKE in Desktop. + (Migrated from the geckoview metric of the same name). + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=772589 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1580077 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1877839 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1580077#c10 + notification_emails: + - necko@mozilla.com + - vgosu@mozilla.com + expires: never diff --git a/netwerk/protocol/http/moz.build b/netwerk/protocol/http/moz.build new file mode 100644 index 0000000000..db6b2a1a68 --- /dev/null +++ b/netwerk/protocol/http/moz.build @@ -0,0 +1,219 @@ +# -*- 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", + "nsIBinaryHttp.idl", + "nsIEarlyHintObserver.idl", + "nsIHttpActivityObserver.idl", + "nsIHttpAuthenticableChannel.idl", + "nsIHttpAuthenticator.idl", + "nsIHttpAuthManager.idl", + "nsIHttpChannel.idl", + "nsIHttpChannelAuthProvider.idl", + "nsIHttpChannelChild.idl", + "nsIHttpChannelInternal.idl", + "nsIHttpHeaderVisitor.idl", + "nsIHttpProtocolHandler.idl", + "nsIObliviousHttp.idl", + "nsIObliviousHttpChannel.idl", + "nsIRaceCacheWithNetwork.idl", + "nsITlsHandshakeListener.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", + "ClassOfService.h", + "EarlyHintPreloader.h", + "EarlyHintRegistrar.h", + "EarlyHintsService.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", + "OpaqueResponseUtils.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", + "BinaryHttpRequest.cpp", + "CacheControlParser.cpp", + "CachePushChecker.cpp", + "ConnectionDiagnostics.cpp", + "ConnectionEntry.cpp", + "ConnectionHandle.cpp", + "DnsAndConnectSocket.cpp", + "EarlyHintPreconnect.cpp", + "EarlyHintPreloader.cpp", + "EarlyHintRegistrar.cpp", + "EarlyHintsService.cpp", + "Http2Compression.cpp", + "Http2Push.cpp", + "Http2Session.cpp", + "Http2Stream.cpp", + "Http2StreamBase.cpp", + "Http2StreamTunnel.cpp", + "Http3Session.cpp", + "Http3Stream.cpp", + "Http3WebTransportSession.cpp", + "Http3WebTransportStream.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", + "InterceptedHttpChannel.cpp", + "MockHttpAuth.cpp", + "NetworkMarker.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", + "ObliviousHttpChannel.cpp", + "ObliviousHttpService.cpp", + "OpaqueResponseUtils.cpp", + "ParentChannelListener.cpp", + "PendingTransactionInfo.cpp", + "PendingTransactionQueue.cpp", + "QuicSocketControl.cpp", + "SpeculativeTransaction.cpp", + "TlsHandshaker.cpp", + "TLSTransportLayer.cpp", + "TRRServiceChannel.cpp", +] + +if CONFIG["MOZ_WIDGET_TOOLKIT"] == "windows": + UNIFIED_SOURCES += [ + "HttpWinUtils.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", + "PHttpBackgroundChannel.ipdl", + "PHttpChannel.ipdl", + "PHttpConnectionMgr.ipdl", + "PHttpTransaction.ipdl", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" + +LOCAL_INCLUDES += [ + "/dom/base", + "/netwerk/base", + "/netwerk/cookie", + "/netwerk/dns", + "/netwerk/ipc", + "/netwerk/socket/neqo_glue", + "/netwerk/url-classifier", +] + +if CONFIG["MOZ_AUTH_EXTENSION"]: + LOCAL_INCLUDES += [ + "/extensions/auth", + ] + +EXTRA_JS_MODULES += [ + "HPKEConfigManager.sys.mjs", + "WellKnownOpportunisticUtils.sys.mjs", +] + +XPCOM_MANIFESTS += [ + "components.conf", +] + +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..7b28096bb4 --- /dev/null +++ b/netwerk/protocol/http/nsAHttpConnection.h @@ -0,0 +1,292 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 "mozilla/net/DNS.h" +#include "nsHttp.h" +#include "nsISupports.h" +#include "nsAHttpTransaction.h" +#include "Http3WebTransportSession.h" +#include "HttpTrafficAnalyzer.h" +#include "nsIRequest.h" + +class nsIAsyncInputStream; +class nsIAsyncOutputStream; +class nsISocketTransport; +class nsITLSSocketControl; + +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; + + [[nodiscard]] virtual Http3WebTransportSession* GetWebTransportSession( + nsAHttpTransaction* aTransaction) = 0; + + // called by a transaction to get the TLS socket control from the socket. + virtual void GetTLSSocketControl(nsITLSSocketControl**) = 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 CurrentBrowserIdChanged(uint64_t id) = 0; + + // categories set by nsHttpTransaction to identify how this connection is + // being used. + virtual void SetTrafficCategory(HttpTrafficCategory) = 0; + + virtual nsresult GetSelfAddr(NetAddr* addr) = 0; + virtual nsresult GetPeerAddr(NetAddr* addr) = 0; + virtual bool ResolvedByTRR() = 0; + virtual nsIRequest::TRRMode EffectiveTRRMode() = 0; + virtual nsITRRSkipReason::value TRRSkipReason() = 0; + virtual bool GetEchConfigUsed() = 0; + virtual PRIntervalTime LastWriteTime() = 0; + virtual void SetCloseReason(ConnectionCloseReason aReason) = 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; \ + [[nodiscard]] Http3WebTransportSession* GetWebTransportSession( \ + nsAHttpTransaction* aTransaction) 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 CurrentBrowserIdChanged(uint64_t id) 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 GetTLSSocketControl(nsITLSSocketControl** result) override { \ + if (!(fwdObject)) { \ + *result = nullptr; \ + return; \ + } \ + return (fwdObject)->GetTLSSocketControl(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); \ + } \ + nsresult GetSelfAddr(NetAddr* addr) override { \ + if (!(fwdObject)) return NS_ERROR_FAILURE; \ + return (fwdObject)->GetSelfAddr(addr); \ + } \ + nsresult GetPeerAddr(NetAddr* addr) override { \ + if (!(fwdObject)) return NS_ERROR_FAILURE; \ + return (fwdObject)->GetPeerAddr(addr); \ + } \ + bool ResolvedByTRR() override { \ + return (!(fwdObject)) ? false : (fwdObject)->ResolvedByTRR(); \ + } \ + nsIRequest::TRRMode EffectiveTRRMode() override { \ + return (!(fwdObject)) ? nsIRequest::TRR_DEFAULT_MODE \ + : (fwdObject)->EffectiveTRRMode(); \ + } \ + nsITRRSkipReason::value TRRSkipReason() override { \ + return (!(fwdObject)) ? nsITRRSkipReason::TRR_UNSET \ + : (fwdObject)->TRRSkipReason(); \ + } \ + bool GetEchConfigUsed() override { \ + return (!(fwdObject)) ? false : (fwdObject)->GetEchConfigUsed(); \ + } \ + void SetCloseReason(ConnectionCloseReason aReason) override { \ + if (fwdObject) (fwdObject)->SetCloseReason(aReason); \ + } \ + PRIntervalTime LastWriteTime() override; + +// 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..06912c3695 --- /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 "nsTArray.h" +#include "nsWeakReference.h" +#include "nsIRequest.h" +#include "nsITRRSkipReason.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 nsIRequestContext; +class nsISVCBRecord; +class nsITLSSocketControl; +class nsITransport; + +namespace mozilla { +namespace net { + +class nsAHttpConnection; +class nsAHttpSegmentReader; +class nsAHttpSegmentWriter; +class nsHttpTransaction; +class nsHttpRequestHead; +class nsHttpConnectionInfo; +class NullHttpTransaction; + +enum class WebSocketSupport { UNSURE, NO_SUPPORT, SUPPORTED }; + +//---------------------------------------------------------------------------- +// 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; } + + // 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 socket control is part of the connection, but sometimes + // in the case of TLS tunneled within TLS the transaction might present + // a more specific socket control 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 GetTransactionTLSSocketControl( + nsITLSSocketControl**) { + return NS_ERROR_NOT_IMPLEMENTED; + } + + virtual void DisableSpdy() {} + // When called, we disallow to connect through a Http/2 proxy. + virtual void DisableHttp2ForProxy() {} + virtual void DisableHttp3(bool aAllowRetryHTTPSRR) {} + virtual void MakeNonSticky() {} + virtual void MakeRestartable() {} + virtual void ReuseConnectionOnRestartOK(bool) {} + virtual void SetIsHttp2Websocket(bool) {} + virtual bool IsHttp2Websocket() { return false; } + virtual void SetTRRInfo(nsIRequest::TRRMode aMode, + TRRSkippedReason aSkipReason){}; + + // 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() {} + + // We call this function if we do want to reset IP family preference again on + // the next restart. + virtual void DoNotResetIPFamilyPreference() {} + + // 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; + } + + virtual uint64_t BrowserId() { + MOZ_ASSERT(false); + return 0; + } + + 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..90ac6a4ddc --- /dev/null +++ b/netwerk/protocol/http/nsCORSListenerProxy.cpp @@ -0,0 +1,1753 @@ +/* -*- 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 "nsIThreadRetargetableStreamListener.h" +#include "nsString.h" +#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 "nsComponentManagerUtils.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/ExpandedPrincipal.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 "mozilla/dom/ReferrerInfo.h" +#include "mozilla/dom/RequestBinding.h" +#include "mozilla/glean/GleanMetrics.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 inline nsAutoString GetStatusCodeAsString(nsIHttpChannel* aHttp) { + nsAutoString result; + uint32_t code; + if (NS_SUCCEEDED(aHttp->GetResponseStatus(&code))) { + result.AppendInt(code); + } + return result; +} + +static void LogBlockedRequest(nsIRequest* aRequest, const char* aProperty, + const char16_t* aParam, uint32_t aBlockingReason, + nsIHttpChannel* aCreatingChannel, + bool aIsWarning = false) { + nsresult rv = NS_OK; + + nsCOMPtr channel = do_QueryInterface(aRequest); + + if (!aIsWarning) { + 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, aIsWarning); + 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, + aIsWarning); +} + +////////////////////////////////////////////////////////////////////////// +// Preflight cache + +class nsPreflightCache { + public: + struct TokenTime { + nsCString token; + TimeStamp expirationTime; + }; + + struct CacheEntry : public LinkedListElement { + explicit CacheEntry(nsCString& aKey, bool aPrivateBrowsing) + : mKey(aKey), mPrivateBrowsing(aPrivateBrowsing) { + MOZ_COUNT_CTOR(nsPreflightCache::CacheEntry); + } + + ~CacheEntry() { MOZ_COUNT_DTOR(nsPreflightCache::CacheEntry); } + + void PurgeExpired(TimeStamp now); + bool CheckRequest(const nsCString& aMethod, + const nsTArray& aHeaders); + + nsCString mKey; + bool mPrivateBrowsing{false}; + 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 PurgePrivateBrowsingEntries(); + + 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::PurgePrivateBrowsingEntries() { + for (auto iter = mTable.Iter(); !iter.Done(); iter.Next()) { + auto* entry = iter.UserData(); + if (entry->mPrivateBrowsing) { + // last private browsing window closed, remove preflight cache entries + entry->removeFrom(sPreflightCache->mList); + iter.Remove(); + } + } +} + +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. + auto newEntry = + MakeUnique(key, aOriginAttributes.mPrivateBrowsingId != 0); + + 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!"); + } + } + + auto* newEntryWeakRef = mTable.InsertOrUpdate(key, std::move(newEntry)).get(); + mList.insertFront(newEntryWeakRef); + + return newEntryWeakRef; +} + +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; +} + +/* static */ +void nsCORSListenerProxy::ClearCache() { + if (!sPreflightCache) { + return; + } + sPreflightCache->Clear(); +} + +// static +void nsCORSListenerProxy::ClearPrivateBrowsingCache() { + if (!sPreflightCache) { + return; + } + sPreflightCache->PurgePrivateBrowsingEntries(); +} + +// Usually, when using an expanded principal, there's no particularly good +// origin to do the request with. However if the expanded principal only wraps +// one principal, we can use that one instead. +// +// This is needed so that DevTools can still do CORS-enabled requests (since +// DevTools uses a triggering principal expanding the node principal to bypass +// CSP checks, see Element::CreateDevToolsPrincipal(), bug 1604562, and bug +// 1391994). +static nsIPrincipal* GetOriginHeaderPrincipal(nsIPrincipal* aPrincipal) { + while (aPrincipal && aPrincipal->GetIsExpandedPrincipal()) { + auto* ep = BasePrincipal::Cast(aPrincipal)->As(); + if (ep->AllowList().Length() != 1) { + break; + } + aPrincipal = ep->AllowList()[0]; + } + return aPrincipal; +} + +nsCORSListenerProxy::nsCORSListenerProxy(nsIStreamListener* aOuter, + nsIPrincipal* aRequestingPrincipal, + bool aWithCredentials) + : mOuterListener(aOuter), + mRequestingPrincipal(aRequestingPrincipal), + mOriginHeaderPrincipal(GetOriginHeaderPrincipal(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() = default; + + 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{0}; + + ~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, "CORSDidNotSucceed2", 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, "CORSDidNotSucceed2", nullptr, + nsILoadInfo::BLOCKING_REASON_CORSDIDNOTSUCCEED, + topChannel); + } + return status; + } + + // Test that things worked on a HTTP level + nsCOMPtr http = do_QueryInterface(aRequest); + if (!http) { + nsCOMPtr channel = do_QueryInterface(aRequest); + nsCOMPtr uri; + NS_GetFinalChannelURI(channel, getter_AddRefs(uri)); + if (uri && uri->SchemeIs("moz-extension")) { + // moz-extension:-URLs do not support CORS, but can universally be read + // if an extension lists the resource in web_accessible_resources. + // Access will be checked in UpdateChannel. + return NS_OK; + } + 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; + } + + // 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)) { + auto statusCode = GetStatusCodeAsString(http); + LogBlockedRequest(aRequest, "CORSMissingAllowOrigin2", statusCode.get(), + 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(!mOriginHeaderPrincipal->GetIsExpandedPrincipal()); + nsAutoCString origin; + mOriginHeaderPrincipal->GetWebExposedOriginSerialization(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); +} + +NS_IMETHODIMP +nsCORSListenerProxy::OnDataFinished(nsresult aStatus) { + nsCOMPtr listener; + { + MutexAutoLock lock(mMutex); + listener = mOuterListener; + } + if (!listener) { + return NS_ERROR_FAILURE; + } + nsCOMPtr retargetableListener = + do_QueryInterface(listener); + if (retargetableListener) { + return retargetableListener->OnDataFinished(aStatus); + } + + return NS_OK; +} + +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 { + mIsRedirect = true; + // 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_FAILED(rv)) { + aOldChannel->Cancel(rv); + return rv; + } + + if (!oldChannelPrincipal->Equals(newChannelPrincipal)) { + // Spec says to set our source origin to a unique origin. + mOriginHeaderPrincipal = + NullPrincipal::CreateWithInheritedAttributes(oldChannelPrincipal); + } + } + + 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(); + + // Introduced for DevTools in order to allow overriding some requests + // with the content of data: URIs. + if (loadInfo->GetAllowInsecureRedirectToDataURI() && uri->SchemeIs("data")) { + return NS_OK; + } + + // 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->SetRequestMode(dom::RequestMode::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 (uri->SchemeIs("moz-extension")) { + // moz-extension:-URLs do not support CORS, but can universally be read + // if an extension lists the resource in web_accessible_resources. + // This is enforced via the CheckLoadURIWithPrincipal call above: + // moz-extension resources have the URI_DANGEROUS_TO_LOAD flag, unless + // listed in web_accessible_resources. + return NS_OK; + } + + 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; + + if (mIsRedirect || StaticPrefs::network_cors_preflight_block_userpass_uri()) { + // https://fetch.spec.whatwg.org/#http-redirect-fetch + // Step 9. If request’s mode is "cors", locationURL includes credentials, + // and request’s origin is not same origin with locationURL’s origin, + // then return a network error. + + nsAutoCString 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->GetWebExposedOriginSerialization(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(net::nsHttp::Origin.val(), 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; + if (StaticPrefs::network_cors_preflight_allow_client_cert()) { + flags |= nsIRequest::LOAD_ANONYMOUS_ALLOW_CLIENT_CERT; + } + 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) { + // Note: A preflight is not needed for moz-extension:-requests either, but + // there is already a check for that in the caller of CheckPreflightNeeded, + // in UpdateChannel. + 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) { + auto statusCode = GetStatusCodeAsString(http); + LogBlockedRequest(aChannel, "CORSDidNotSucceed2", statusCode.get(), + 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) { + auto statusCode = GetStatusCodeAsString(http); + LogBlockedRequest(aRequest, "CORSPreflightDidNotSucceed3", statusCode.get(), + 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 wildcard = false; + bool hasAuthorizationHeader = 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) { + wildcard = true; + } else { + headers.AppendElement(header); + } + + if (header.LowerCaseEqualsASCII("authorization")) { + hasAuthorizationHeader = true; + } + } + + bool authorizationInPreflightHeaders = false; + bool authorizationCoveredByWildcard = false; + for (uint32_t i = 0; i < mPreflightHeaders.Length(); ++i) { + // Cache the result of the authorization header. + bool isAuthorization = + mPreflightHeaders[i].LowerCaseEqualsASCII("authorization"); + if (wildcard) { + if (!isAuthorization) { + continue; + } else { + authorizationInPreflightHeaders = true; + if (StaticPrefs:: + network_cors_preflight_authorization_covered_by_wildcard() && + !hasAuthorizationHeader) { + // When `Access-Control-Allow-Headers` is `*` and there is no + // `Authorization` header listed, we send a deprecation warning to the + // console. + LogBlockedRequest(aRequest, "CORSAllowHeaderFromPreflightDeprecation", + nullptr, 0, parentHttpChannel, true); + glean::network::cors_authorization_header + .Get("covered_by_wildcard"_ns) + .Add(1); + authorizationCoveredByWildcard = true; + continue; + } + } + } + + const auto& comparator = nsCaseInsensitiveCStringArrayComparator(); + if (!headers.Contains(mPreflightHeaders[i], comparator)) { + LogBlockedRequest( + aRequest, "CORSMissingAllowHeaderFromPreflight2", + NS_ConvertUTF8toUTF16(mPreflightHeaders[i]).get(), + nsILoadInfo::BLOCKING_REASON_CORSMISSINGALLOWHEADERFROMPREFLIGHT, + parentHttpChannel); + if (isAuthorization) { + glean::network::cors_authorization_header.Get("disallowed"_ns).Add(1); + } + return NS_ERROR_DOM_BAD_URI; + } + } + + if (authorizationInPreflightHeaders && !authorizationCoveredByWildcard) { + glean::network::cors_authorization_header.Get("allowed"_ns).Add(1); + } + + 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; + + nsLoadFlags loadFlags; + rv = aRequestChannel->GetLoadFlags(&loadFlags); + NS_ENSURE_SUCCESS(rv, rv); + + // Disable preflight cache if devtools says so or on other force reloads + bool disableCache = (loadFlags & nsIRequest::LOAD_BYPASS_CACHE); + + 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); + + // 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; + + if (StaticPrefs::network_cors_preflight_allow_client_cert()) { + loadFlags |= nsIRequest::LOAD_ANONYMOUS_ALLOW_CLIENT_CERT; + } + + 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, bool aIsWarning) { + 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; + } + + uint32_t errorFlag = + aIsWarning ? nsIScriptError::warningFlag : nsIScriptError::errorFlag; + + // 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 + errorFlag, aCategory, + aInnerWindowID); + } else { + rv = scriptError->Init(aMessage, + u""_ns, // sourceName + u""_ns, // sourceLine + 0, // lineNumber + 0, // columnNumber + errorFlag, aCategory, 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..e96b0a8aca --- /dev/null +++ b/netwerk/protocol/http/nsCORSListenerProxy.h @@ -0,0 +1,130 @@ +/* -*- 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 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(); + static void ClearCache(); + static void ClearPrivateBrowsingCache(); + + [[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, + bool aIsWarning = false); + + 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& aUnsafeHeaders, 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; + bool mIsRedirect = false; + // 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 MOZ_UNANNOTATED; +}; + +#endif diff --git a/netwerk/protocol/http/nsHttp.cpp b/netwerk/protocol/http/nsHttp.cpp new file mode 100644 index 0000000000..768ad91729 --- /dev/null +++ b/netwerk/protocol/http/nsHttp.cpp @@ -0,0 +1,1160 @@ +/* -*- 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/DataMutex.h" +#include "mozilla/HashFunctions.h" +#include "mozilla/OriginAttributes.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 "nsIStandardURL.h" +#include "nsJSUtils.h" +#include "nsStandardURL.h" +#include "sslerr.h" +#include +#include +#include "nsLiteralString.h" +#include + +namespace mozilla { +namespace net { + +const uint32_t kHttp3VersionCount = 5; +const nsCString kHttp3Versions[] = {"h3-29"_ns, "h3-30"_ns, "h3-31"_ns, + "h3-32"_ns, "h3"_ns}; + +// https://datatracker.ietf.org/doc/html/draft-ietf-webtrans-http3/#section-4.3 +constexpr uint64_t kWebTransportErrorCodeStart = 0x52e4a40fa8db; +constexpr uint64_t kWebTransportErrorCodeEnd = 0x52e4a40fa9e2; + +// find out how many atoms we have +#define HTTP_ATOM(_name, _value) Unused_##_name, +enum { +#include "nsHttpAtomList.h" + NUM_HTTP_ATOMS +}; +#undef HTTP_ATOM + +static StaticDataMutex> + sAtomTable("nsHttp::sAtomTable"); + +// This is set to true in DestroyAtomTable so we don't try to repopulate the +// table if ResolveAtom gets called during shutdown for some reason. +static Atomic sTableDestroyed{false}; + +// We put the atoms in a hash table for speedy lookup.. see ResolveAtom. +namespace nsHttp { + +nsresult CreateAtomTable( + nsTHashtable& base) { + if (sTableDestroyed) { + return NS_ERROR_ILLEGAL_DURING_SHUTDOWN; + } + // fill the table with our known atoms + const nsHttpAtomLiteral* atoms[] = { +#define HTTP_ATOM(_name, _value) &(_name), +#include "nsHttpAtomList.h" +#undef HTTP_ATOM + }; + + if (!base.IsEmpty()) { + return NS_OK; + } + for (const auto* atom : atoms) { + Unused << base.PutEntry(atom->val(), fallible); + } + + LOG(("Added static atoms to atomTable")); + return NS_OK; +} + +nsresult CreateAtomTable() { + LOG(("CreateAtomTable")); + auto atomTable = sAtomTable.Lock(); + return CreateAtomTable(atomTable.ref()); +} + +void DestroyAtomTable() { + LOG(("DestroyAtomTable")); + sTableDestroyed = true; + auto atomTable = sAtomTable.Lock(); + atomTable.ref().Clear(); +} + +// this function may be called from multiple threads +nsHttpAtom ResolveAtom(const nsACString& str) { + if (str.IsEmpty()) { + return {}; + } + + auto atomTable = sAtomTable.Lock(); + + if (atomTable.ref().IsEmpty()) { + if (sTableDestroyed) { + NS_WARNING("ResolveAtom called during shutdown"); + return {}; + } + + NS_WARNING("ResolveAtom called before CreateAtomTable"); + if (NS_FAILED(CreateAtomTable(atomTable.ref()))) { + return {}; + } + } + + // Check if we already have an entry in the table + auto* entry = atomTable.ref().GetEntry(str); + if (entry) { + return nsHttpAtom(entry->GetKey()); + } + + LOG(("Putting %s header into atom table", nsPromiseFlatCString(str).get())); + // Put the string in the table. If it works create the atom. + entry = atomTable.ref().PutEntry(str, fallible); + if (entry) { + return nsHttpAtom(entry->GetKey()); + } + return {}; +} + +// +// 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 (nsCRT::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 forceValidateCacheContent, 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) || + forceValidateCacheContent) && + !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 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) { + return !(chr < 33 || chr == 127 || chr == '(' || chr == ')' || chr == '<' || + chr == '>' || chr == '@' || chr == ',' || chr == ';' || chr == ':' || + chr == '"' || chr == '/' || chr == '[' || chr == ']' || chr == '?' || + chr == '=' || chr == '{' || chr == '}' || chr == '\\'); +} + +ParsedHeaderPair::ParsedHeaderPair(const char* name, int32_t nameLen, + const char* val, int32_t valLen, + bool isQuotedValue) + : mName(nsDependentCSubstring(nullptr, size_t(0))), + mValue(nsDependentCSubstring(nullptr, size_t(0))), + 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); +} + +Maybe CallingScriptLocationString() { + if (!LOG4_ENABLED() && !xpc::IsInAutomation()) { + return Nothing(); + } + + JSContext* cx = nsContentUtils::GetCurrentJSContext(); + if (!cx) { + return Nothing(); + } + + nsAutoCString fileNameString; + uint32_t line = 0, col = 0; + if (!nsJSUtils::GetCallingLocation(cx, fileNameString, &line, &col)) { + return Nothing(); + } + + nsCString logString = ""_ns; + logString.AppendPrintf("%s:%u:%u", fileNameString.get(), line, col); + return Some(logString); +} + +void LogCallingScriptLocation(void* instance) { + Maybe logLocation = CallingScriptLocationString(); + LogCallingScriptLocation(instance, logLocation); +} + +void LogCallingScriptLocation(void* instance, + const Maybe& aLogLocation) { + if (aLogLocation.isNothing()) { + return; + } + + nsCString logString; + logString.AppendPrintf("%p called from script: ", instance); + logString.AppendPrintf("%s", aLogLocation->get()); + LOG(("%s", logString.get())); +} + +void LogHeaders(const char* lineStart) { + nsAutoCString buf; + const char* endOfLine; + while ((endOfLine = strstr(lineStart, "\r\n"))) { + buf.Assign(lineStart, endOfLine - lineStart); + if (StaticPrefs::network_http_sanitize_headers_in_logs() && + (nsCRT::strcasestr(buf.get(), "authorization: ") || + nsCRT::strcasestr(buf.get(), "proxy-authorization: "))) { + char* p = strchr(buf.BeginWriting(), ' '); + 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; +} + +SupportedAlpnRank H3VersionToRank(const nsACString& aVersion) { + for (uint32_t i = 0; i < kHttp3VersionCount; i++) { + if (aVersion.Equals(kHttp3Versions[i])) { + return static_cast( + static_cast(SupportedAlpnRank::HTTP_3_DRAFT_29) + i); + } + } + + return SupportedAlpnRank::NOT_SUPPORTED; +} + +SupportedAlpnRank IsAlpnSupported(const nsACString& aAlpn) { + if (nsHttpHandler::IsHttp3Enabled() && + gHttpHandler->IsHttp3VersionSupported(aAlpn)) { + return H3VersionToRank(aAlpn); + } + + if (StaticPrefs::network_http_http2_enabled()) { + SpdyInformation* spdyInfo = gHttpHandler->SpdyInfo(); + if (aAlpn.Equals(spdyInfo->VersionString)) { + return SupportedAlpnRank::HTTP_2; + } + } + + if (aAlpn.LowerCaseEqualsASCII("http/1.1")) { + return SupportedAlpnRank::HTTP_1_1; + } + + return SupportedAlpnRank::NOT_SUPPORTED; +} + +// On some security error when 0RTT is used we want to restart transactions +// without 0RTT. Some firewalls do not behave well with 0RTT and cause this +// errors. +bool SecurityErrorThatMayNeedRestart(nsresult aReason) { + return (aReason == + psm::GetXPCOMFromNSSError(SSL_ERROR_PROTOCOL_VERSION_ALERT)) || + (aReason == psm::GetXPCOMFromNSSError(SSL_ERROR_BAD_MAC_ALERT)); +} + +nsresult 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 MakeOriginURL(const nsACString& scheme, const nsACString& origin, + nsCOMPtr& url) { + return NS_MutateURI(new nsStandardURL::Mutator()) + .Apply(&nsIStandardURLMutator::Init, nsIStandardURL::URLTYPE_AUTHORITY, + scheme.EqualsLiteral("http") ? NS_HTTP_DEFAULT_PORT + : NS_HTTPS_DEFAULT_PORT, + origin, nullptr, nullptr, nullptr) + .Finalize(url); +} + +void 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 = 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 GetNSResultFromWebTransportError(uint8_t aErrorCode) { + return static_cast((uint32_t)NS_ERROR_WEBTRANSPORT_CODE_BASE + + (uint32_t)aErrorCode); +} + +uint8_t GetWebTransportErrorFromNSResult(nsresult aResult) { + if (aResult < NS_ERROR_WEBTRANSPORT_CODE_BASE || + aResult > NS_ERROR_WEBTRANSPORT_CODE_END) { + return 0; + } + + return static_cast((uint32_t)aResult - + (uint32_t)NS_ERROR_WEBTRANSPORT_CODE_BASE); +} + +uint64_t WebTransportErrorToHttp3Error(uint8_t aErrorCode) { + return kWebTransportErrorCodeStart + aErrorCode + aErrorCode / 0x1e; +} + +uint8_t Http3ErrorToWebTransportError(uint64_t aErrorCode) { + // Ensure the code is within the valid range. + if (aErrorCode < kWebTransportErrorCodeStart || + aErrorCode > kWebTransportErrorCodeEnd) { + return 0; + } + + uint64_t shifted = aErrorCode - kWebTransportErrorCodeStart; + uint64_t result = shifted - shifted / 0x1f; + + if (result <= std::numeric_limits::max()) { + return (uint8_t)result; + } + + return 0; +} + +ConnectionCloseReason ToCloseReason(nsresult aErrorCode) { + if (NS_SUCCEEDED(aErrorCode)) { + return ConnectionCloseReason::OK; + } + + if (aErrorCode == NS_ERROR_UNKNOWN_HOST) { + return ConnectionCloseReason::DNS_ERROR; + } + if (aErrorCode == NS_ERROR_NET_RESET) { + return ConnectionCloseReason::NET_RESET; + } + if (aErrorCode == NS_ERROR_CONNECTION_REFUSED) { + return ConnectionCloseReason::NET_REFUSED; + } + if (aErrorCode == NS_ERROR_SOCKET_ADDRESS_NOT_SUPPORTED) { + return ConnectionCloseReason::SOCKET_ADDRESS_NOT_SUPPORTED; + } + if (aErrorCode == NS_ERROR_NET_TIMEOUT) { + return ConnectionCloseReason::NET_TIMEOUT; + } + if (aErrorCode == NS_ERROR_OUT_OF_MEMORY) { + return ConnectionCloseReason::OUT_OF_MEMORY; + } + if (aErrorCode == NS_ERROR_SOCKET_ADDRESS_IN_USE) { + return ConnectionCloseReason::SOCKET_ADDRESS_IN_USE; + } + if (aErrorCode == NS_BINDING_ABORTED) { + return ConnectionCloseReason::BINDING_ABORTED; + } + if (aErrorCode == NS_BINDING_REDIRECTED) { + return ConnectionCloseReason::BINDING_REDIRECTED; + } + if (aErrorCode == NS_ERROR_ABORT) { + return ConnectionCloseReason::ERROR_ABORT; + } + + int32_t code = -1 * NS_ERROR_GET_CODE(aErrorCode); + if (mozilla::psm::IsNSSErrorCode(code)) { + return ConnectionCloseReason::SECURITY_ERROR; + } + + return ConnectionCloseReason::OTHER_NET_ERROR; +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/nsHttp.h b/netwerk/protocol/http/nsHttp.h new file mode 100644 index 0000000000..d0bd4cfe67 --- /dev/null +++ b/netwerk/protocol/http/nsHttp.h @@ -0,0 +1,527 @@ +/* -*- 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/OriginAttributes.h" +#include "mozilla/TimeStamp.h" + +#include "mozilla/UniquePtr.h" +#include "NSSErrorsService.h" + +class nsICacheEntry; + +namespace mozilla { + +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 }; + +enum class SupportedAlpnRank : uint8_t { + NOT_SUPPORTED = 0, + HTTP_1_1 = 1, + HTTP_2 = 2, + // Note that the order here MUST be the same as the order in kHttp3Versions. + HTTP_3_DRAFT_29 = 3, + HTTP_3_DRAFT_30 = 4, + HTTP_3_DRAFT_31 = 5, + HTTP_3_DRAFT_32 = 6, + HTTP_3_VER_1 = 7, +}; + +// IMPORTANT: when adding new values, always add them to the end, otherwise +// it will mess up telemetry. +enum class ConnectionCloseReason : uint32_t { + UNSET = 0, + OK, + IDLE_TIMEOUT, + TLS_TIMEOUT, + GO_AWAY, + DNS_ERROR, + NET_RESET, + NET_TIMEOUT, + NET_REFUSED, + NET_INTERRUPT, + NET_INADEQ_SEQURITY, + SOCKET_ADDRESS_NOT_SUPPORTED, + OUT_OF_MEMORY, + SOCKET_ADDRESS_IN_USE, + BINDING_ABORTED, + BINDING_REDIRECTED, + ERROR_ABORT, + CLOSE_EXISTING_CONN_FOR_COALESCING, + CLOSE_NEW_CONN_FOR_COALESCING, + CANT_REUSED, + OTHER_NET_ERROR, + SECURITY_ERROR, +}; + +ConnectionCloseReason ToCloseReason(nsresult aErrorCode); + +inline bool IsHttp3(SupportedAlpnRank aRank) { + return aRank >= SupportedAlpnRank::HTTP_3_DRAFT_29; +} + +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)) + +// 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 << 21) + +// Force a transaction to stay in pending queue until the HTTPS RR is +// available. +#define NS_HTTP_FORCE_WAIT_HTTP_RR (1 << 22) + +// This is used for a temporary workaround for a web-compat issue. The flag is +// only set on CORS preflight request to allowed sending client certificates +// on a connection for an anonymous request. +#define NS_HTTP_LOAD_ANONYMOUS_CONNECT_ALLOW_CLIENT_CERT (1 << 23) + +#define NS_HTTP_DISALLOW_HTTPS_RR (1 << 24) + +#define NS_HTTP_DISALLOW_ECH (1 << 25) + +// Used to indicate that an HTTP Connection should obey Resist Fingerprinting +// and set the User-Agent accordingly. +#define NS_HTTP_USE_RFP (1 << 26) + +// If set, then the initial TLS handshake failed. +#define NS_HTTP_IS_RETRY (1 << 27) + +// When set, disallow to connect to a HTTP/2 proxy. +#define NS_HTTP_DISALLOW_HTTP2_PROXY (1 << 28) + +#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; +struct nsHttpAtomLiteral; + +namespace nsHttp { +[[nodiscard]] nsresult CreateAtomTable(); +void DestroyAtomTable(); + +// will dynamically add atoms to the table if they don't already exist +nsHttpAtom ResolveAtom(const nsACString& s); + +// 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* seps); + +// 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 forceValidateCacheContent, 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 GetLastActiveTabLoadOptimizationHit(); +void SetLastActiveTabLoadOptimizationHit(TimeStamp const& when); +bool IsBeforeLastActiveTabLoadOptimization(TimeStamp const& when); + +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 + +struct nsHttpAtomLiteral; +struct nsHttpAtom { + nsHttpAtom() = default; + nsHttpAtom(const nsHttpAtom& other) = default; + + explicit operator bool() const { return !_val.IsEmpty(); } + + const char* get() const { + if (_val.IsEmpty()) { + return nullptr; + } + return _val.BeginReading(); + } + + const nsCString& val() const { return _val; } + + void operator=(const nsHttpAtom& a) { _val = a._val; } + + // This constructor is mainly used to build the static atom list + // Avoid using it for anything else. + explicit nsHttpAtom(const nsACString& val) : _val(val) {} + + private: + nsCString _val; + friend nsHttpAtom nsHttp::ResolveAtom(const nsACString& s); +}; + +struct nsHttpAtomLiteral { + const char* get() const { return _data.get(); } + nsLiteralCString const& val() const { return _data; } + + template + constexpr explicit nsHttpAtomLiteral(const char (&val)[N]) : _data(val) {} + + operator nsHttpAtom() const { return nsHttpAtom(_data); } + + private: + nsLiteralCString _data; +}; + +inline bool operator==(nsHttpAtomLiteral const& self, + nsHttpAtomLiteral const& other) { + return self.get() == other.get(); +} +inline bool operator!=(nsHttpAtomLiteral const& self, + nsHttpAtomLiteral const& other) { + return self.get() != other.get(); +} + +inline bool operator==(nsHttpAtom const& self, nsHttpAtomLiteral const& other) { + return self.val() == other.val(); +} +inline bool operator!=(nsHttpAtom const& self, nsHttpAtomLiteral const& other) { + return self.val() != other.val(); +} + +inline bool operator==(nsHttpAtomLiteral const& self, nsHttpAtom const& other) { + return self.val() == other.val(); +} +inline bool operator!=(nsHttpAtomLiteral const& self, nsHttpAtom const& other) { + return self.val() != other.val(); +} + +inline bool operator==(nsHttpAtom const& self, nsHttpAtom const& other) { + return self.val() == other.val(); +} +inline bool operator!=(nsHttpAtom const& self, nsHttpAtom const& other) { + return self.val() != other.val(); +} + +namespace nsHttp { + +// 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) \ + inline constexpr nsHttpAtomLiteral _name(_value); +#include "nsHttpAtomList.h" +#undef HTTP_ATOM +} // 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& fullHeader, + 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); + +// Convert an alpn string to SupportedAlpnType. +SupportedAlpnRank IsAlpnSupported(const nsACString& aAlpn); + +static inline bool AllowedErrorForHTTPSRRFallback(nsresult aError) { + return psm::IsNSSErrorCode(-1 * NS_ERROR_GET_CODE(aError)) || + aError == NS_ERROR_NET_RESET || + aError == NS_ERROR_CONNECTION_REFUSED || + aError == NS_ERROR_UNKNOWN_HOST || aError == NS_ERROR_NET_TIMEOUT; +} + +bool SecurityErrorThatMayNeedRestart(nsresult aReason); + +[[nodiscard]] nsresult MakeOriginURL(const nsACString& origin, + nsCOMPtr& url); + +[[nodiscard]] nsresult MakeOriginURL(const nsACString& scheme, + const nsACString& origin, + nsCOMPtr& url); + +void CreatePushHashKey(const nsCString& scheme, const nsCString& hostHeader, + const mozilla::OriginAttributes& originAttributes, + uint64_t serial, const nsACString& pathInfo, + nsCString& outOrigin, nsCString& outKey); + +nsresult GetNSResultFromWebTransportError(uint8_t aErrorCode); + +uint8_t GetWebTransportErrorFromNSResult(nsresult aResult); + +uint64_t WebTransportErrorToHttp3Error(uint8_t aErrorCode); + +uint8_t Http3ErrorToWebTransportError(uint64_t aErrorCode); + +} // 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..b432bb5ea8 --- /dev/null +++ b/netwerk/protocol/http/nsHttpActivityDistributor.cpp @@ -0,0 +1,298 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 { + +using ObserverHolder = nsMainThreadPtrHolder; +using ObserverHandle = nsMainThreadPtrHandle; + +NS_IMPL_ISUPPORTS(nsHttpActivityDistributor, nsIHttpActivityDistributor, + nsIHttpActivityObserver) + +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::ObserveConnectionActivity( + const nsACString& aHost, int32_t aPort, bool aSSL, bool aHasECH, + bool aIsHttp3, uint32_t aActivityType, uint32_t aActivitySubtype, + PRTime aTimestamp, const nsACString& aExtraStringData) { + MOZ_ASSERT(XRE_IsParentProcess() && NS_IsMainThread()); + + for (size_t i = 0; i < mObservers.Length(); i++) { + Unused << mObservers[i]->ObserveConnectionActivity( + aHost, aPort, aSSL, aHasECH, aIsHttp3, aActivityType, aActivitySubtype, + aTimestamp, 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( + static_cast(channel), aActivityType, aActivitySubtype, + aTimestamp, aExtraSizeData, extraStringData); + } else if (args.type() == HttpActivityArgs::THttpConnectionActivity) { + const HttpConnectionActivity& activity = + args.get_HttpConnectionActivity(); + Unused << self->ObserveConnectionActivity( + activity.host(), activity.port(), activity.ssl(), activity.hasECH(), + activity.isHttp3(), aActivityType, aActivitySubtype, aTimestamp, + activity.connInfoKey()); + } + }; + + if (!NS_IsMainThread()) { + return NS_DispatchToMainThread(NS_NewRunnableFunction( + "net::nsHttpActivityDistributor::ObserveActivityWithArgs", task)); + } + + task(); + return NS_OK; +} + +bool nsHttpActivityDistributor::Activated() { return mActivated; } + +bool nsHttpActivityDistributor::ObserveProxyResponseEnabled() { + return mObserveProxyResponse; +} + +bool nsHttpActivityDistributor::ObserveConnectionEnabled() { + return mObserveConnection; +} + +NS_IMETHODIMP +nsHttpActivityDistributor::GetIsActive(bool* isActive) { + NS_ENSURE_ARG_POINTER(isActive); + if (XRE_IsSocketProcess()) { + *isActive = mActivated; + return NS_OK; + } + + MutexAutoLock lock(mLock); + *isActive = mActivated = !!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()); + if (!NS_IsMainThread()) { + // We only support calling this from the main thread. + return NS_ERROR_FAILURE; + } + + 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 (wasEmpty) { + mActivated = true; + if (nsIOService::UseSocketProcess()) { + 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()); + if (!NS_IsMainThread()) { + // We only support calling this from the main thread. + return NS_ERROR_FAILURE; + } + + ObserverHandle observer( + new ObserverHolder("nsIHttpActivityObserver", aObserver)); + + { + MutexAutoLock lock(mLock); + if (!mObservers.RemoveElement(observer)) { + return NS_ERROR_FAILURE; + } + mActivated = mObservers.IsEmpty(); + } + + if (nsIOService::UseSocketProcess() && !mActivated) { + auto task = []() { + SocketProcessParent* parent = SocketProcessParent::GetSingleton(); + if (parent && parent->CanSend()) { + Unused << parent->SendOnHttpActivityDistributorActivated(false); + } + }; + gIOService->CallOrWaitForSocketProcess(task); + } + return NS_OK; +} + +NS_IMETHODIMP +nsHttpActivityDistributor::GetObserveProxyResponse( + bool* aObserveProxyResponse) { + NS_ENSURE_ARG_POINTER(aObserveProxyResponse); + bool result = mObserveProxyResponse; + *aObserveProxyResponse = result; + return NS_OK; +} + +NS_IMETHODIMP +nsHttpActivityDistributor::SetObserveProxyResponse(bool aObserveProxyResponse) { + if (!NS_IsMainThread()) { + // We only support calling this from the main thread. + return NS_ERROR_FAILURE; + } + + mObserveProxyResponse = aObserveProxyResponse; + if (nsIOService::UseSocketProcess()) { + auto task = [aObserveProxyResponse]() { + SocketProcessParent* parent = SocketProcessParent::GetSingleton(); + if (parent && parent->CanSend()) { + Unused << parent->SendOnHttpActivityDistributorObserveProxyResponse( + aObserveProxyResponse); + } + }; + gIOService->CallOrWaitForSocketProcess(task); + } + return NS_OK; +} + +NS_IMETHODIMP +nsHttpActivityDistributor::GetObserveConnection(bool* aObserveConnection) { + NS_ENSURE_ARG_POINTER(aObserveConnection); + + *aObserveConnection = mObserveConnection; + return NS_OK; +} + +NS_IMETHODIMP +nsHttpActivityDistributor::SetObserveConnection(bool aObserveConnection) { + if (!NS_IsMainThread()) { + // We only support calling this from the main thread. + return NS_ERROR_FAILURE; + } + + mObserveConnection = aObserveConnection; + if (nsIOService::UseSocketProcess()) { + auto task = [aObserveConnection]() { + SocketProcessParent* parent = SocketProcessParent::GetSingleton(); + if (parent && parent->CanSend()) { + Unused << parent->SendOnHttpActivityDistributorObserveConnection( + aObserveConnection); + } + }; + 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..b2d9ee20a4 --- /dev/null +++ b/netwerk/protocol/http/nsHttpActivityDistributor.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 nsHttpActivityDistributor_h__ +#define nsHttpActivityDistributor_h__ + +#include "nsIHttpActivityObserver.h" +#include "nsTArray.h" +#include "nsProxyRelease.h" +#include "mozilla/Atomics.h" +#include "mozilla/Mutex.h" + +namespace mozilla { +namespace net { + +class nsHttpActivityDistributor : public nsIHttpActivityDistributor { + public: + using ObserverArray = + nsTArray>; + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIHTTPACTIVITYOBSERVER + NS_DECL_NSIHTTPACTIVITYDISTRIBUTOR + + nsHttpActivityDistributor() = default; + + protected: + virtual ~nsHttpActivityDistributor() = default; + + ObserverArray mObservers; + Mutex mLock MOZ_UNANNOTATED{"nsHttpActivityDistributor.mLock"}; + Atomic mActivated{false}; + Atomic mObserveProxyResponse{false}; + Atomic mObserveConnection{false}; +}; + +} // 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..02e51e5ab1 --- /dev/null +++ b/netwerk/protocol/http/nsHttpAtomList.h @@ -0,0 +1,111 @@ +/* -*- 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_Security_Policy, "Content-Security-Policy") +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(GlobalPrivacyControl, "Sec-GPC") +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(OriginTrial, "Origin-Trial") +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(Referrer_Policy, "Referrer-Policy") +HTTP_ATOM(Retry_After, "Retry-After") +HTTP_ATOM(Sec_WebSocket_Extensions, "Sec-WebSocket-Extensions") +HTTP_ATOM(Sec_WebSocket_Protocol, "Sec-WebSocket-Protocol") +HTTP_ATOM(Sec_WebSocket_Version, "Sec-WebSocket-Version") +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_Http3, "X-Firefox-Http3") +HTTP_ATOM(X_Frame_Options, "X-Frame-Options") + +// 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..6b2eb81d9e --- /dev/null +++ b/netwerk/protocol/http/nsHttpAuthCache.cpp @@ -0,0 +1,349 @@ +/* -*- 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 nsACString& scheme, const nsACString& 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); +} + +//----------------------------------------------------------------------------- +// 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 nsACString& scheme, + const nsACString& host, + int32_t port, + const nsACString& path, + nsACString const& originSuffix, + nsHttpAuthEntry** entry) { + LOG(("nsHttpAuthCache::GetAuthEntryForPath %p [path=%s]\n", this, + path.BeginReading())); + + 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 nsACString& scheme, + const nsACString& host, + int32_t port, + const nsACString& realm, + nsACString const& originSuffix, + nsHttpAuthEntry** entry) + +{ + LOG(("nsHttpAuthCache::GetAuthEntryForDomain %p [realm=%s]\n", this, + realm.BeginReading())); + + 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 nsACString& scheme, const nsACString& host, int32_t port, + const nsACString& path, const nsACString& realm, const nsACString& creds, + const nsACString& challenge, nsACString const& originSuffix, + const nsHttpAuthIdentity* ident, nsISupports* metadata) { + nsresult rv; + + LOG(("nsHttpAuthCache::SetAuthEntry %p [realm=%s path=%s metadata=%p]\n", + this, realm.BeginReading(), path.BeginReading(), metadata)); + + nsAutoCString key; + nsHttpAuthNode* node = LookupAuthNode(scheme, host, port, originSuffix, key); + + if (!node) { + // create a new entry node and set the given entry + auto node = UniquePtr(new nsHttpAuthNode); + LOG((" new nsHttpAuthNode %p for key='%s'", node.get(), key.get())); + rv = node->SetAuthEntry(path, realm, creds, challenge, ident, metadata); + if (NS_FAILED(rv)) { + return rv; + } + + mDB.InsertOrUpdate(key, std::move(node)); + return NS_OK; + } + + return node->SetAuthEntry(path, realm, creds, challenge, ident, metadata); +} + +void nsHttpAuthCache::ClearAuthEntry(const nsACString& scheme, + const nsACString& host, int32_t port, + const nsACString& 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 nsACString& scheme, + const nsACString& 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) { + AppendToArray(aValue, mDB.Keys()); +} + +//----------------------------------------------------------------------------- +// nsHttpAuthIdentity +//----------------------------------------------------------------------------- + +void nsHttpAuthIdentity::Clear() { + mUser.Truncate(); + mPass.Truncate(); + mDomain.Truncate(); +} + +bool nsHttpAuthIdentity::Equals(const nsHttpAuthIdentity& ident) const { + // we could probably optimize this with a single loop, but why bother? + return mUser == ident.mUser && mPass == ident.mPass && + mDomain == ident.mDomain; +} + +//----------------------------------------------------------------------------- +// nsHttpAuthEntry +//----------------------------------------------------------------------------- + +nsresult nsHttpAuthEntry::AddPath(const nsACString& aPath) { + for (const auto& p : mPaths) { + if (StringBeginsWith(aPath, p)) { + return NS_OK; // subpath already exists in the list + } + } + + mPaths.AppendElement(aPath); + return NS_OK; +} + +nsresult nsHttpAuthEntry::Set(const nsACString& path, const nsACString& realm, + const nsACString& creds, const nsACString& chall, + const nsHttpAuthIdentity* ident, + nsISupports* metadata) { + if (ident) { + mIdent = *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. + mIdent.Clear(); + } + + nsresult rv = AddPath(path); + if (NS_FAILED(rv)) { + return rv; + } + + mRealm = realm; + mCreds = creds; + mChallenge = chall; + 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 nsACString& aPath) { + // 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]; + + for (const auto& entryPath : entry->mPaths) { + // proxy auth entries have no path, so require exact match on + // empty path string. + if (entryPath.IsEmpty()) { + if (aPath.IsEmpty()) { + return entry.get(); + } + } else if (StringBeginsWith(aPath, entryPath)) { + return entry.get(); + } + } + } + return nullptr; +} + +nsHttpAuthNode::EntryList::const_iterator nsHttpAuthNode::LookupEntryItrByRealm( + const nsACString& realm) const { + return std::find_if(mList.cbegin(), mList.cend(), [&realm](const auto& val) { + return realm.Equals(val->Realm()); + }); +} + +nsHttpAuthEntry* nsHttpAuthNode::LookupEntryByRealm(const nsACString& realm) { + auto itr = LookupEntryItrByRealm(realm); + if (itr != mList.cend()) { + return itr->get(); + } + + return nullptr; +} + +nsresult nsHttpAuthNode::SetAuthEntry(const nsACString& path, + const nsACString& realm, + const nsACString& creds, + const nsACString& 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 nsACString& 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..66bcfb36e2 --- /dev/null +++ b/netwerk/protocol/http/nsHttpAuthCache.h @@ -0,0 +1,219 @@ +/* -*- 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 { + +//----------------------------------------------------------------------------- +// nsHttpAuthIdentity +//----------------------------------------------------------------------------- + +class nsHttpAuthIdentity { + public: + nsHttpAuthIdentity() = default; + nsHttpAuthIdentity(const nsAString& domain, const nsAString& user, + const nsAString& password) + : mUser(user), mPass(password), mDomain(domain) {} + ~nsHttpAuthIdentity() { Clear(); } + + const nsString& Domain() const { return mDomain; } + const nsString& User() const { return mUser; } + const nsString& Password() const { return mPass; } + + void Clear(); + + bool Equals(const nsHttpAuthIdentity& ident) const; + bool IsEmpty() const { + return mUser.IsEmpty() && mPass.IsEmpty() && mDomain.IsEmpty(); + } + + private: + nsString mUser; + nsString mPass; + nsString mDomain; +}; + +//----------------------------------------------------------------------------- +// nsHttpAuthEntry +//----------------------------------------------------------------------------- + +class nsHttpAuthEntry { + public: + const nsCString& Realm() const { return mRealm; } + const nsCString& Creds() const { return mCreds; } + const nsCString& Challenge() const { return mChallenge; } + const nsString& Domain() const { return mIdent.Domain(); } + const nsString& User() const { return mIdent.User(); } + const nsString& Pass() const { return mIdent.Password(); } + + const nsHttpAuthIdentity& Identity() const { return mIdent; } + + [[nodiscard]] nsresult AddPath(const nsACString& aPath); + + nsCOMPtr mMetaData; + + private: + nsHttpAuthEntry(const nsACString& path, const nsACString& realm, + const nsACString& creds, const nsACString& challenge, + const nsHttpAuthIdentity* ident, nsISupports* metadata) { + DebugOnly rv = + Set(path, realm, creds, challenge, ident, metadata); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + ~nsHttpAuthEntry() = default; + + [[nodiscard]] nsresult Set(const nsACString& path, const nsACString& realm, + const nsACString& creds, + const nsACString& challenge, + const nsHttpAuthIdentity* ident, + nsISupports* metadata); + + nsHttpAuthIdentity mIdent; + + nsTArray mPaths; + + nsCString mRealm; + nsCString mCreds; + nsCString 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 nsACString& path); + + // realm must not be null + nsHttpAuthEntry* LookupEntryByRealm(const nsACString& realm); + EntryList::const_iterator LookupEntryItrByRealm( + const nsACString& realm) const; + + // if a matching entry is found, then credentials will be changed. + [[nodiscard]] nsresult SetAuthEntry(const nsACString& path, + const nsACString& realm, + const nsACString& creds, + const nsACString& challenge, + const nsHttpAuthIdentity* ident, + nsISupports* metadata); + + void ClearAuthEntry(const nsACString& 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 nsACString& scheme, + const nsACString& host, + int32_t port, + const nsACString& 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 nsACString& scheme, + const nsACString& host, + int32_t port, + const nsACString& 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 nsACString& scheme, const nsACString& host, int32_t port, + const nsACString& path, const nsACString& realm, const nsACString& creds, + const nsACString& challenge, nsACString const& originSuffix, + const nsHttpAuthIdentity* ident, nsISupports* metadata); + + void ClearAuthEntry(const nsACString& scheme, const nsACString& host, + int32_t port, const nsACString& 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 nsACString& scheme, + const nsACString& 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..14c4e46fce --- /dev/null +++ b/netwerk/protocol/http/nsHttpAuthManager.cpp @@ -0,0 +1,105 @@ +/* -*- 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) + +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(aScheme, aHost, aPort, aPath, + originSuffix, &entry); + } else { + rv = auth_cache->GetAuthEntryForDomain(aScheme, aHost, aPort, aRealm, + 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(aUserDomain, aUserName, aUserPassword); + + nsAutoCString originSuffix; + if (aPrincipal) { + aPrincipal->OriginAttributesRef().CreateSuffix(originSuffix); + } + + nsHttpAuthCache* auth_cache = aIsPrivate ? mPrivateAuthCache : mAuthCache; + return auth_cache->SetAuthEntry(aScheme, aHost, aPort, aPath, aRealm, + ""_ns, // credentials + ""_ns, // 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..b49c4a44e6 --- /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() = default; + [[nodiscard]] nsresult Init(); + + protected: + virtual ~nsHttpAuthManager() = default; + + nsHttpAuthCache* mAuthCache{nullptr}; + nsHttpAuthCache* mPrivateAuthCache{nullptr}; +}; + +} // 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..c3de5cb5e8 --- /dev/null +++ b/netwerk/protocol/http/nsHttpBasicAuth.cpp @@ -0,0 +1,101 @@ +/* -*- 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 "nsCRT.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 nsACString& 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 nsACString& aChallenge, + bool isProxyAuth, const nsAString& domain, const nsAString& username, + const nsAString& password, nsISupports* sessionState, + nsISupports* continuationState, nsICancelable** aCancellable) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsHttpBasicAuth::GenerateCredentials( + nsIHttpAuthenticableChannel* authChannel, const nsACString& aChallenge, + bool isProxyAuth, const nsAString& domain, const nsAString& user, + const nsAString& password, nsISupports** sessionState, + nsISupports** continuationState, uint32_t* aFlags, nsACString& creds) { + LOG(("nsHttpBasicAuth::GenerateCredentials [challenge=%s]\n", + aChallenge.BeginReading())); + + *aFlags = 0; + + // we only know how to deal with Basic auth for http. + bool isBasicAuth = StringBeginsWith(aChallenge, "basic"_ns, + nsCaseInsensitiveCStringComparator); + NS_ENSURE_TRUE(isBasicAuth, NS_ERROR_UNEXPECTED); + + // we work with UTF-8 around here + nsAutoCString userpass; + CopyUTF16toUTF8(user, userpass); + userpass.Append(':'); // always send a ':' (see bug 129565) + AppendUTF16toUTF8(password, userpass); + + nsAutoCString authString{"Basic "_ns}; + nsresult rv = Base64EncodeAppend(userpass, authString); + NS_ENSURE_SUCCESS(rv, rv); + + creds = 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..b3806f761b --- /dev/null +++ b/netwerk/protocol/http/nsHttpChannel.cpp @@ -0,0 +1,10430 @@ +/* -*- 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/ScopeExit.h" +#include "mozilla/Sprintf.h" +#include "mozilla/dom/nsCSPContext.h" +#include "mozilla/dom/nsCSPService.h" +#include "mozilla/StoragePrincipalHelper.h" + +#include "nsContentSecurityUtils.h" +#include "nsHttp.h" +#include "nsHttpChannel.h" +#include "nsHttpChannelAuthProvider.h" +#include "nsHttpHandler.h" +#include "nsString.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 "nsIURLQueryStringStripper.h" +#include "nsIWebTransport.h" +#include "nsCRT.h" +#include "nsMimeTypes.h" +#include "nsNetCID.h" +#include "nsNetUtil.h" +#include "nsIURL.h" +#include "nsIStreamTransportService.h" +#include "prnetdb.h" +#include "nsEscape.h" +#include "nsComponentManagerUtils.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 "nsIConsoleService.h" +#include "mozilla/AntiTrackingRedirectHeuristic.h" +#include "mozilla/AntiTrackingUtils.h" +#include "mozilla/Attributes.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/DebugOnly.h" +#include "mozilla/PerfStats.h" +#include "mozilla/Preferences.h" +#include "mozilla/ProfilerLabels.h" +#include "mozilla/Components.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/dom/ReferrerInfo.h" +#include "mozilla/Telemetry.h" +#include "AlternateServices.h" +#include "NetworkMarker.h" +#include "nsIHttpPushListener.h" +#include "nsIX509Cert.h" +#include "ScopedNSSTypes.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 "ThirdPartyUtil.h" +#include "InterceptedHttpChannel.h" +#include "../../cache2/CacheFileUtils.h" +#include "nsIMultiplexInputStream.h" +#include "nsINetworkLinkService.h" +#include "mozilla/ContentBlockingAllowList.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/ServiceWorkerUtils.h" +#include "mozilla/dom/nsHTTPSOnlyStreamListener.h" +#include "mozilla/dom/nsHTTPSOnlyUtils.h" +#include "mozilla/net/AsyncUrlChannelClassifier.h" +#include "mozilla/net/CookieJarSettings.h" +#include "mozilla/net/NeckoChannelParams.h" +#include "mozilla/net/OpaqueResponseUtils.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" +#include "nsUnknownDecoder.h" +#ifdef XP_WIN +# include "HttpWinUtils.h" +#endif +#ifdef FUZZING +# include "mozilla/StaticPrefs_fuzzing.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; +} + +} // 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 MOZ_STACK_CLASS AutoRedirectVetoNotifier { + public: + explicit AutoRedirectVetoNotifier(nsHttpChannel* channel, nsresult& aRv) + : mChannel(channel), mRv(aRv) { + if (mChannel->LoadHasAutoRedirectVetoNotifier()) { + MOZ_CRASH("Nested AutoRedirectVetoNotifier on the stack"); + mChannel = nullptr; + return; + } + + mChannel->StoreHasAutoRedirectVetoNotifier(true); + } + ~AutoRedirectVetoNotifier() { ReportRedirectResult(mRv); } + void RedirectSucceeded() { ReportRedirectResult(NS_OK); } + + private: + nsHttpChannel* mChannel; + bool mCalledReport = false; + nsresult& mRv; + void ReportRedirectResult(nsresult aRv); +}; + +void AutoRedirectVetoNotifier::ReportRedirectResult(nsresult aRv) { + if (!mChannel) return; + + if (mCalledReport) { + return; + } + mCalledReport = true; + + mChannel->mRedirectChannel = nullptr; + + if (NS_SUCCEEDED(aRv)) { + mChannel->RemoveAsNonTailRequest(); + } + + nsCOMPtr vetoHook; + NS_QueryNotificationCallbacks(mChannel, NS_GET_IID(nsIRedirectResultListener), + getter_AddRefs(vetoHook)); + + nsHttpChannel* channel = mChannel; + mChannel = nullptr; + + if (vetoHook) vetoHook->OnRedirectResult(aRv); + + // Drop after the notification + channel->StoreHasAutoRedirectVetoNotifier(false); +} + +//----------------------------------------------------------------------------- +// nsHttpChannel +//----------------------------------------------------------------------------- + +nsHttpChannel::nsHttpChannel() : HttpAsyncAborter(this) { + LOG(("Creating nsHttpChannel [this=%p, nsIChannel=%p]\n", this, + static_cast(this))); + mChannelCreationTime = PR_Now(); + mChannelCreationTimestamp = TimeStamp::Now(); +} + +nsHttpChannel::~nsHttpChannel() { + LOG(("Destroying nsHttpChannel [this=%p, nsIChannel=%p]\n", this, + static_cast(this))); + + if (LOG_ENABLED()) { + nsCString webExtension; + this->GetPropertyAsACString(u"cancelledByExtension"_ns, webExtension); + if (!webExtension.IsEmpty()) { + LOG(("channel [%p] cancelled by extension [id=%s]", this, + webExtension.get())); + } + } + + 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(mAuthProvider.forget()); + arrayToRelease.AppendElement(mRedirectChannel.forget()); + arrayToRelease.AppendElement(mPreflightChannel.forget()); + arrayToRelease.AppendElement(mDNSPrefetch.forget()); + + MOZ_DIAGNOSTIC_ASSERT( + !mEarlyHintObserver, + "Early hint observer should have been released in ReleaseListeners()"); + arrayToRelease.AppendElement(mEarlyHintObserver.forget()); + MOZ_DIAGNOSTIC_ASSERT( + !mChannelClassifier, + "Channel classifier should have been released in ReleaseListeners()"); + arrayToRelease.AppendElement( + mChannelClassifier.forget().downcast()); + MOZ_DIAGNOSTIC_ASSERT( + !mWarningReporter, + "Warning reporter should have been released in ReleaseListeners()"); + arrayToRelease.AppendElement(mWarningReporter.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, + nsILoadInfo* aLoadInfo) { + nsresult rv = + HttpBaseChannel::Init(uri, caps, proxyInfo, proxyResolveFlags, proxyURI, + channelId, aContentPolicyType, aLoadInfo); + 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, + bool aIsWarning) { + if (mWarningReporter) { + return mWarningReporter->LogBlockedCORSRequest(aMessage, aCategory, + aIsWarning); + } + 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)); + + // notify "http-on-modify-request-before-cookies" observers + gHttpHandler->OnModifyRequestBeforeCookies(this); + + AddCookiesToRequest(); + +#ifdef XP_WIN + + auto prefEnabledForCurrentContainer = [&]() { + uint32_t containerId = mLoadInfo->GetOriginAttributes().mUserContextId; + // Make sure that the default container ID is 0 + static_assert(nsIScriptSecurityManager::DEFAULT_USER_CONTEXT_ID == 0); + nsPrintfCString prefName("network.http.windows-sso.container-enabled.%u", + containerId); + + bool enabled = false; + Preferences::GetBool(prefName.get(), &enabled); + + LOG(("Pref for %s is %d\n", prefName.get(), enabled)); + + return enabled; + }; + + // If Windows 10 SSO is enabled, we potentially add auth information to + // secure top level loads (DOCUMENTs) and iframes (SUBDOCUMENTs) that + // aren't anonymous or private browsing. + if (StaticPrefs::network_http_windows_sso_enabled() && + mURI->SchemeIs("https") && !(mLoadFlags & LOAD_ANONYMOUS) && + !mPrivateBrowsing) { + ExtContentPolicyType type = mLoadInfo->GetExternalContentPolicyType(); + if ((type == ExtContentPolicy::TYPE_DOCUMENT || + type == ExtContentPolicy::TYPE_SUBDOCUMENT) && + prefEnabledForCurrentContainer()) { + AddWindowsSSO(this); + } + } +#endif + + // notify "http-on-modify-request" observers + CallOnModifyRequestObservers(); + + return CallOrWaitForResume( + [](auto* self) { return self->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); +} + +nsresult nsHttpChannel::OnBeforeConnect() { + nsresult rv = NS_OK; + + // 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); + } + + if (LoadAuthRedirectedChannel()) { + // This channel is a result of a redirect due to auth retry + // We have already checked for HSTS upgarde in the redirecting channel. + // We can safely skip those checks + return ContinueOnBeforeConnect(false, rv); + } + + SecFetch::AddSecFetchHeader(this); + + // Check to see if we should redirect this channel to the unstripped URI. To + // revert the query stripping if the loading channel is in the content + // blocking allow list. + if (ContentBlockingAllowList::Check(this)) { + nsCOMPtr unstrippedURI; + mLoadInfo->GetUnstrippedURI(getter_AddRefs(unstrippedURI)); + + if (unstrippedURI) { + return AsyncCall(&nsHttpChannel::HandleAsyncRedirectToUnstrippedURI); + } + } + + nsCOMPtr resultPrincipal; + if (!mURI->SchemeIs("https")) { + nsContentUtils::GetSecurityManager()->GetChannelResultPrincipal( + this, getter_AddRefs(resultPrincipal)); + } + + // Check if we already know about the HSTS status of the host + nsISiteSecurityService* sss = gHttpHandler->GetSSService(); + NS_ENSURE_TRUE(sss, NS_ERROR_OUT_OF_MEMORY); + bool isSecureURI; + OriginAttributes originAttributes; + if (!StoragePrincipalHelper::GetOriginAttributesForHSTS(this, + originAttributes)) { + return NS_ERROR_FAILURE; + } + rv = sss->IsSecureURI(mURI, originAttributes, &isSecureURI); + NS_ENSURE_SUCCESS(rv, rv); + // Save that on the loadInfo so it can later be consumed by SecurityInfo.jsm + mLoadInfo->SetHstsStatus(isSecureURI); + + RefPtr bc; + mLoadInfo->GetBrowsingContext(getter_AddRefs(bc)); + if (bc && bc->Top()->GetForceOffline()) { + return NS_ERROR_OFFLINE; + } + + // 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, 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); + } + + auto shouldSkipUpgradeWithHTTPSRR = [&]() -> bool { + // Skip using HTTPS RR to upgrade when this is not a top-level load and the + // loading principal is http. + if ((mLoadInfo->GetExternalContentPolicyType() != + ExtContentPolicy::TYPE_DOCUMENT) && + (mLoadInfo->GetLoadingPrincipal() && + mLoadInfo->GetLoadingPrincipal()->SchemeIs("http"))) { + return true; + } + + nsAutoCString uriHost; + mURI->GetAsciiHost(uriHost); + + if (gHttpHandler->IsHostExcludedForHTTPSRR(uriHost)) { + return true; + } + + if (nsHTTPSOnlyUtils::IsUpgradeDowngradeEndlessLoop( + mURI, mLoadInfo, + {nsHTTPSOnlyUtils::UpgradeDowngradeEndlessLoopOptions:: + EnforceForHTTPSRR})) { + // Add the host to a excluded list because: + // 1. We don't need to do the same check again. + // 2. Other subresources in the same host will be also excluded. + gHttpHandler->ExcludeHTTPSRRHost(uriHost); + LOG(("[%p] skip HTTPS upgrade for host [%s]", this, uriHost.get())); + return true; + } + + return false; + }; + + if (shouldSkipUpgradeWithHTTPSRR()) { + StoreUseHTTPSSVC(false); + // If the website does not want to use HTTPS RR, we should set + // NS_HTTP_DISALLOW_HTTPS_RR. This is for avoiding HTTPS RR being used by + // the transaction. + mCaps |= NS_HTTP_DISALLOW_HTTPS_RR; + 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, hasHTTPSRR); + } + + auto dnsStrategy = GetProxyDNSStrategy(); + if (!(dnsStrategy & DNS_PREFETCH_ORIGIN)) { + return ContinueOnBeforeConnect(aShouldUpgrade, aStatus); + } + + LOG(("nsHttpChannel::MaybeUseHTTPSRRForUpgrade [%p] wait for HTTPS RR", + this)); + + 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))); + return ContinueOnBeforeConnect(aShouldUpgrade, aStatus); + } + + StoreWaitHTTPSSVCRecord(true); + return NS_OK; +} + +nsresult nsHttpChannel::ContinueOnBeforeConnect(bool aShouldUpgrade, + nsresult aStatus, + bool aUpgradeWithHTTPSRR) { + 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 && !mURI->SchemeIs("https")) { + mozilla::glean::networking::https_upgrade_with_https_rr + .Get(aUpgradeWithHTTPSRR ? "https_rr"_ns : "others"_ns) + .Add(1); + 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") && + StaticPrefs::network_http_http2_websockets()) { + // 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; + // Because NS_HTTP_STICKY_CONNECTION breaks HTTPS RR fallabck mecnahism, we + // can not use HTTPS RR for upgrade requests. + mCaps |= NS_HTTP_DISALLOW_HTTPS_RR; + } + + if (LoadIsTRRServiceChannel()) { + mCaps |= NS_HTTP_LARGE_KEEPALIVE; + mCaps |= NS_HTTP_DISALLOW_HTTPS_RR; + } + + if (mTransactionSticky) { + MOZ_ASSERT(LoadAuthRedirectedChannel()); + // this means this is a redirected channel channel due to auth retry and a + // connection based auth scheme was used + // we have a reference to the old-transaction with sticky connection which + // we need to use + mCaps |= NS_HTTP_STICKY_CONNECTION; + } + + mCaps |= NS_HTTP_TRR_FLAGS_FROM_MODE(nsIRequest::GetTRRMode()); + + // Finalize ConnectionInfo flags before SpeculativeConnect + mConnectionInfo->SetAnonymous((mLoadFlags & LOAD_ANONYMOUS) != 0); + mConnectionInfo->SetPrivate(mPrivateBrowsing); + 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); + mConnectionInfo->SetAnonymousAllowClientCert( + (mLoadFlags & LOAD_ANONYMOUS_ALLOW_CLIENT_CERT) != 0); + + // notify "http-on-before-connect" observers + gHttpHandler->OnBeforeConnect(this); + + return CallOrWaitForResume([](auto* self) { return self->Connect(); }); +} + +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(); + } + + // Step 8.18 of HTTP-network-or-cache fetch + // https://fetch.spec.whatwg.org/#http-network-or-cache-fetch + nsAutoCString rangeVal; + if (NS_SUCCEEDED(GetRequestHeader("Range"_ns, rangeVal))) { + SetRequestHeader("Accept-Encoding"_ns, "identity"_ns, true); + } + + bool isTrackingResource = IsThirdPartyTrackingResource(); + LOG(("nsHttpChannel %p tracking resource=%d, cos=%lu, inc=%d", this, + isTrackingResource, mClassOfService.Flags(), + mClassOfService.Incremental())); + + 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) { + 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) { + 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... + nsresult rv = DoConnect(mTransactionSticky); + mTransactionSticky = nullptr; + return rv; +} + +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 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). + RefPtr bc; + mLoadInfo->GetBrowsingContext(getter_AddRefs(bc)); + + if (gIOService->IsOffline() || mUpgradeProtocolCallback || + !(mCaps & NS_HTTP_ALLOW_KEEPALIVE) || + (bc && bc->Top()->GetForceOffline())) { + return; + } + + // LOAD_ONLY_FROM_CACHE and LOAD_NO_NETWORK_IO must not hit network. + // LOAD_FROM_CACHE is unlikely to hit network, so skip preconnects for it. + if (mLoadFlags & + (LOAD_ONLY_FROM_CACHE | LOAD_FROM_CACHE | LOAD_NO_NETWORK_IO)) { + 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 | NS_HTTP_REFRESH_DNS), + gHttpHandler->EchConfigEnabled()); +} + +void nsHttpChannel::DoNotifyListenerCleanup() { + // We don't need this info anymore + CleanRedirectCacheChainIfNecessary(); +} + +void nsHttpChannel::ReleaseListeners() { + HttpBaseChannel::ReleaseListeners(); + mChannelClassifier = nullptr; + mWarningReporter = nullptr; + mEarlyHintObserver = nullptr; + mWebTransportSessionEventListener = 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); +} + +nsresult nsHttpChannel::SetupTransaction() { + LOG(("nsHttpChannel::SetupTransaction [this=%p, cos=%lu, inc=%d prio=%d]\n", + this, mClassOfService.Flags(), mClassOfService.Incremental(), + 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; + } + + if (mLoadFlags & LOAD_ANONYMOUS_ALLOW_CLIENT_CERT) { + mCaps |= NS_HTTP_LOAD_ANONYMOUS_CONNECT_ALLOW_CLIENT_CERT; + } + + if (nsContentUtils::ShouldResistFingerprinting(this, + RFPTarget::HttpUserAgent)) { + mCaps |= NS_HTTP_USE_RFP; + } + + // 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()) { + if (NS_WARN_IF(!gIOService->SocketProcessReady())) { + return NS_ERROR_NOT_AVAILABLE; + } + SocketProcessParent* socketProcess = SocketProcessParent::GetSingleton(); + if (!socketProcess->CanSend()) { + return NS_ERROR_NOT_AVAILABLE; + } + + 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); + + if (socketProcess) { + MOZ_ALWAYS_TRUE( + 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 (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; + } + + if (mWebTransportSessionEventListener) { + mCaps |= NS_HTTP_STICKY_CONNECTION; + } + + 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; + }; + } + + EnsureBrowserId(); + EnsureRequestContext(); + + HttpTrafficCategory category = CreateTrafficCategory(); + std::function observer; + if (mTransactionObserver) { + observer = [transactionObserver{std::move(mTransactionObserver)}]( + TransactionObserverResult&& aResult) { + transactionObserver->Complete(aResult.versionOk(), aResult.authOk(), + aResult.closeReason()); + }; + } + mTransaction->SetIsForWebTransport(!!mWebTransportSessionEventListener); + rv = mTransaction->Init( + mCaps, mConnectionInfo, &mRequestHead, mUploadStream, mReqContentLength, + LoadUploadStreamHasHeaders(), GetCurrentSerialEventTarget(), callbacks, + this, mBrowserId, 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.Flags() & 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 = AntiTrackingUtils::IsThirdPartyChannel(this); + + 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"); + + MOZ_RELEASE_ASSERT(mCanceled || LoadProcessCrossOriginSecurityHeadersCalled(), + "Security headers need to have been processed before " + "calling CallOnStartRequest"); + + mEarlyHintObserver = nullptr; + + 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; + } + + // EnsureOpaqueResponseIsAllowed and EnsureOpauqeResponseIsAllowedAfterSniff + // are the checks for Opaque Response Blocking to ensure that we block as many + // cross-origin responses with CORS headers as possible that are not either + // Javascript or media to avoid leaking their contents through side channels. + OpaqueResponse opaqueResponse = + PerformOpaqueResponseSafelistCheckBeforeSniff(); + if (opaqueResponse == OpaqueResponse::Block) { + SetChannelBlockedByOpaqueResponse(); + CancelWithReason(NS_ERROR_FAILURE, + "OpaqueResponseBlocker::BlockResponse"_ns); + return NS_ERROR_FAILURE; + } + + // 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! + mListener = new nsUnknownDecoder(mListener); + unknownDecoderStarted = true; + } + } + + // If unknownDecoder is not going to be launched, call + // EnsureOpaqueResponseIsAllowedAfterSniff immediately. + if (!unknownDecoderStarted) { + if (opaqueResponse == OpaqueResponse::SniffCompressed) { + mListener = new nsCompressedAudioVideoImageDetector( + mListener, &HttpBaseChannel::CallTypeSniffers); + } else if (opaqueResponse == OpaqueResponse::Sniff) { + MOZ_DIAGNOSTIC_ASSERT(mORB); + nsresult rv = mORB->EnsureOpaqueResponseIsAllowedAfterSniff(this); + + if (NS_FAILED(rv)) { + return rv; + } + } + } + + // 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(&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()->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); + } + } + + 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_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 an HTTP Strict Transport Security (HSTS) header. + */ +nsresult nsHttpChannel::ProcessHSTSHeader(nsITransportSecurityInfo* aSecInfo) { + nsHttpAtom atom(nsHttp::ResolveAtom("Strict-Transport-Security"_ns)); + + nsAutoCString securityHeader; + nsresult rv = mResponseHead->GetHeader(atom, securityHeader); + if (rv == NS_ERROR_NOT_AVAILABLE) { + LOG(("nsHttpChannel: No %s header, continuing load.\n", atom.get())); + return NS_OK; + } + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (!aSecInfo) { + LOG(("nsHttpChannel::ProcessHSTSHeader: no securityInfo?")); + return NS_ERROR_INVALID_ARG; + } + nsITransportSecurityInfo::OverridableErrorCategory overridableErrorCategory; + rv = aSecInfo->GetOverridableErrorCategory(&overridableErrorCategory); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + if (overridableErrorCategory != + nsITransportSecurityInfo::OverridableErrorCategory::ERROR_UNSET) { + LOG( + ("nsHttpChannel::ProcessHSTSHeader: untrustworthy connection - not " + "processing header")); + return NS_ERROR_FAILURE; + } + + nsISiteSecurityService* sss = gHttpHandler->GetSSService(); + NS_ENSURE_TRUE(sss, NS_ERROR_OUT_OF_MEMORY); + + OriginAttributes originAttributes; + if (NS_WARN_IF(!StoragePrincipalHelper::GetOriginAttributesForHSTS( + this, originAttributes))) { + return NS_ERROR_FAILURE; + } + + uint32_t failureResult; + rv = sss->ProcessHeader(mURI, securityHeader, originAttributes, nullptr, + nullptr, &failureResult); + if (NS_FAILED(rv)) { + nsAutoString consoleErrorCategory(u"Invalid HSTS Headers"_ns); + nsAutoString consoleErrorTag; + GetSTSConsoleErrorTag(failureResult, consoleErrorTag); + Unused << AddSecurityMessage(consoleErrorTag, consoleErrorCategory); + LOG(("nsHttpChannel: Failed to parse %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 + if (HostIsIPLiteral(asciiHost)) { + 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); + + // Only process HSTS headers for first-party loads. This prevents a + // proliferation of useless HSTS state for partitioned third parties. + if (!mLoadInfo->GetIsThirdPartyContextToTopWindow()) { + rv = ProcessHSTSHeader(mSecurityInfo); + 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; + } + + if (!mSecurityInfo) { + return; + } + + uint32_t state; + if (NS_SUCCEEDED(mSecurityInfo->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); + } + } + + uint16_t tlsVersion; + nsresult rv = mSecurityInfo->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 (mWebTransportSessionEventListener) { + 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, mPrivateBrowsing, + 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 (strstr(alt_service.get(), "h3-")) { + saw_quic = 1; + } else if (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 = NS_OK; + + 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; + } + + // 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)) { + mStatus = 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")); + } + + // 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 credentials as invalid. + mAuthProvider->ClearProxyIdent(); + } + if (!LoadAuthRedirectedChannel() && + 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 if (httpStatus == 401 && + StaticPrefs:: + network_auth_supress_auth_prompt_for_XFO_failures() && + !nsContentSecurityUtils::CheckCSPFrameAncestorAndXFO(this)) { + // CSP Frame Ancestor and X-Frame-Options check has failed + // Do not prompt http auth - Bug 1629307 + 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)) { + mStatus = rv; + LOG(("CheckForSuperfluousAuth failed [rv=%x]\n", + static_cast(rv))); + } + } + rv = ProcessNormal(); + } else { + mIsAuthChannel = true; + mAuthRetryPending = true; + if (StaticPrefs::network_auth_use_redirect_for_retries()) { + if (NS_SUCCEEDED(RedirectToNewChannelForAuthRetry())) { + return NS_OK; + } + mAuthRetryPending = false; + rv = ProcessNormal(); + } + } + break; + + case 408: + case 425: + case 429: + // Do not cache 408, 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; +} + +static void ReportHttpResponseVersion(HttpVersion version) { + if (Telemetry::CanRecordPrereleaseData()) { + Telemetry::Accumulate(Telemetry::HTTP_RESPONSE_VERSION, + static_cast(version)); + } + + nsAutoCString versionLabel; + switch (version) { + case HttpVersion::v0_9: + case HttpVersion::v1_0: + case HttpVersion::v1_1: + versionLabel = "http_1"_ns; + break; + case HttpVersion::v2_0: + versionLabel = "http_2"_ns; + break; + case HttpVersion::v3_0: + versionLabel = "http_3"_ns; + break; + default: + versionLabel = "unknown"_ns; + break; + } + mozilla::glean::networking::http_response_version.Get(versionLabel).Add(1); +} + +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; + + 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); + } + } + + ReportHttpResponseVersion(mResponseHead->Version()); +} + +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); + 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() { + LOG(("nsHttpChannel::ProcessNormal [this=%p]\n", this)); + + 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; + } + + rv = ProcessCrossOriginSecurityHeaders(); + if (NS_FAILED(rv)) { + mStatus = rv; + HandleAsyncAbort(); + return rv; + } + + // 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); + + 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)); +#ifdef MOZ_PROXY_DIRECT_FAILOVER + if (NS_FAILED(rv)) { + if (!StaticPrefs::network_proxy_failover_direct()) { + return rv; + } + // If this request used a failed proxy and there is no failover available, + // fallback to DIRECT connections for conservative requests. + if (LoadBeConservative()) { + rv = pps->NewProxyInfo("direct"_ns, ""_ns, 0, ""_ns, ""_ns, 0, UINT32_MAX, + nullptr, getter_AddRefs(pi)); + } +#endif + if (NS_FAILED(rv)) { + return rv; + } +#ifdef MOZ_PROXY_DIRECT_FAILOVER + } +#endif + + // 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)); + } + } +} + +void nsHttpChannel::HandleAsyncRedirectToUnstrippedURI() { + MOZ_ASSERT(!mCallOnResume, "How did that happen?"); + + if (mSuspendCount) { + LOG( + ("Waiting until resume to do async redirect to unstripped URI " + "[this=%p]\n", + this)); + mCallOnResume = [](nsHttpChannel* self) { + self->HandleAsyncRedirectToUnstrippedURI(); + return NS_OK; + }; + return; + } + + nsCOMPtr unstrippedURI; + mLoadInfo->GetUnstrippedURI(getter_AddRefs(unstrippedURI)); + + // Clear the unstripped URI from the loadInfo before starting redirect in case + // endless redirect. + mLoadInfo->SetUnstrippedURI(nullptr); + + nsresult rv = StartRedirectChannelToURI( + unstrippedURI, 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::RedirectToNewChannelForAuthRetry() { + LOG(("nsHttpChannel::RedirectToNewChannelForAuthRetry %p", this)); + nsresult rv = NS_OK; + + nsCOMPtr redirectLoadInfo = CloneLoadInfoForRedirect( + mURI, nsIChannelEventSink::REDIRECT_INTERNAL | + nsIChannelEventSink::REDIRECT_AUTH_RETRY); + + nsCOMPtr ioService; + + rv = gHttpHandler->GetIOService(getter_AddRefs(ioService)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr newChannel; + rv = gHttpHandler->NewProxiedChannel(mURI, mProxyInfo, mProxyResolveFlags, + mProxyURI, mLoadInfo, + getter_AddRefs(newChannel)); + + NS_ENSURE_SUCCESS(rv, rv); + + rv = SetupReplacementChannel(mURI, newChannel, true, + nsIChannelEventSink::REDIRECT_INTERNAL | + nsIChannelEventSink::REDIRECT_AUTH_RETRY); + NS_ENSURE_SUCCESS(rv, rv); + + // rewind the upload stream + if (mUploadStream) { + nsCOMPtr seekable = do_QueryInterface(mUploadStream); + nsresult rv = NS_ERROR_NO_INTERFACE; + if (seekable) { + rv = seekable->Seek(nsISeekableStream::NS_SEEK_SET, 0); + } + + // This should not normally happen, but it's possible that big memory + // blobs originating in the other process can't be rewinded. + // In that case we just fail the request, otherwise the content length + // will not match and this load will never complete. + NS_ENSURE_SUCCESS(rv, rv); + } + + RefPtr httpChannelImpl = do_QueryObject(newChannel); + + MOZ_ASSERT(mAuthProvider); + httpChannelImpl->mAuthProvider = std::move(mAuthProvider); + + httpChannelImpl->mProxyInfo = mProxyInfo; + + if ((mCaps & NS_HTTP_STICKY_CONNECTION) || + mTransaction->HasStickyConnection()) { + mConnectionInfo = mTransaction->GetConnInfo(); + + httpChannelImpl->mTransactionSticky = mTransaction; + + if (mTransaction->Http2Disabled()) { + httpChannelImpl->mCaps |= NS_HTTP_DISALLOW_SPDY; + } + if (mTransaction->Http3Disabled()) { + httpChannelImpl->mCaps |= NS_HTTP_DISALLOW_HTTP3; + } + } + httpChannelImpl->mCaps |= NS_HTTP_STICKY_CONNECTION; + if (LoadAuthConnectionRestartable()) { + httpChannelImpl->mCaps |= NS_HTTP_CONNECTION_RESTARTABLE; + } else { + httpChannelImpl->mCaps &= ~NS_HTTP_CONNECTION_RESTARTABLE; + } + + MOZ_ASSERT(mConnectionInfo); + httpChannelImpl->mConnectionInfo = mConnectionInfo->Clone(); + + // we need to store the state to skip unnecessary checks in the new channel + httpChannelImpl->StoreAuthRedirectedChannel(true); + + // We must copy proxy and auth header to the new channel. + // Although the new channel can populate auth headers from auth cache, we + // would still like to use the auth headers generated in this channel. The + // main reason for doing this is that certain connection-based/stateful auth + // schemes like NTLM will fail when we try generate the credentials more than + // the number of times the server has presented us the challenge due to the + // usage of nonce in generating the credentials Copying the auth header will + // bypass generation of the credentials + nsAutoCString authVal; + if (NS_SUCCEEDED(GetRequestHeader("Proxy-Authorization"_ns, authVal))) { + httpChannelImpl->SetRequestHeader("Proxy-Authorization"_ns, authVal, false); + } + if (NS_SUCCEEDED(GetRequestHeader("Authorization"_ns, authVal))) { + httpChannelImpl->SetRequestHeader("Authorization"_ns, authVal, false); + } + + httpChannelImpl->SetBlockAuthPrompt(LoadBlockAuthPrompt()); + mRedirectChannel = newChannel; + + rv = gHttpHandler->AsyncOnChannelRedirect( + this, newChannel, + nsIChannelEventSink::REDIRECT_INTERNAL | + nsIChannelEventSink::REDIRECT_AUTH_RETRY); + + if (NS_SUCCEEDED(rv)) rv = WaitForRedirectCallback(); + + // redirected channel will be opened after we receive the OnStopRequest + + if (NS_FAILED(rv)) { + AutoRedirectVetoNotifier notifier(this, rv); + mRedirectChannel = nullptr; + } + + return rv; +} +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, rv); + + /* 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, rv); + + if (NS_FAILED(rv)) return rv; + + if (!mRedirectChannel) { + LOG(( + "nsHttpChannel::OpenRedirectChannel unexpected null redirect channel")); + return NS_ERROR_FAILURE; + } + + // 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::OpenRedirectChannel); + rv = gHttpHandler->AsyncOnChannelRedirect(this, newChannel, flags); + + if (NS_SUCCEEDED(rv)) rv = WaitForRedirectCallback(); + + if (NS_FAILED(rv)) { + AutoRedirectVetoNotifier notifier(this, rv); + PopRedirectAsyncFunc(&nsHttpChannel::OpenRedirectChannel); + } + + 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); + + // 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 (nsCRT::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; + } 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); + }); +} + +// 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); + + 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; + } + + return OpenCacheEntryInternal(isHttps); +} + +nsresult nsHttpChannel::OpenCacheEntryInternal(bool isHttps) { + 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 WaitForCacheEntry + AutoCacheWaitFlags waitFlags(this); + + nsAutoCString cacheKey; + + nsCOMPtr cacheStorageService( + components::CacheStorage::Service()); + if (!cacheStorageService) { + return NS_ERROR_NOT_AVAILABLE; + } + + nsCOMPtr cacheStorage; + mCacheEntryURI = mURI; + + RefPtr info = GetLoadContextInfo(this); + if (!info) { + return NS_ERROR_FAILURE; + } + + uint32_t cacheEntryOpenFlags; + bool offline = gIOService->IsOffline(); + + RefPtr bc; + mLoadInfo->GetBrowsingContext(getter_AddRefs(bc)); + + bool maybeRCWN = false; + + nsAutoCString cacheControlRequestHeader; + Unused << mRequestHead.GetHeader(nsHttp::Cache_Control, + cacheControlRequestHeader); + CacheControlParser cacheControlRequest(cacheControlRequestHeader); + if (cacheControlRequest.NoStore()) { + return NS_OK; + } + + if (offline || (mLoadFlags & INHIBIT_CACHING) || + (bc && bc->Top()->GetForceOffline())) { + if (BYPASS_LOCAL_CACHE(mLoadFlags, LoadPreferCacheLoadOverBypass()) && + !offline) { + return NS_OK; + } + cacheEntryOpenFlags = nsICacheStorage::OPEN_READONLY; + StoreCacheEntryIsReadOnly(true); + } else if (BYPASS_LOCAL_CACHE(mLoadFlags, LoadPreferCacheLoadOverBypass())) { + 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 (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 { + // Try to race only if we use disk cache storage + maybeRCWN = mRequestHead.IsSafeMethod(); + rv = cacheStorageService->DiskCacheStorage(info, + getter_AddRefs(cacheStorage)); + } + NS_ENSURE_SUCCESS(rv, rv); + + if ((mClassOfService.Flags() & 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"); + } + bool isThirdParty = false; + if (StaticPrefs::network_fetch_cache_partition_cross_origin() && + (NS_FAILED(mLoadInfo->TriggeringPrincipal()->IsThirdPartyChannel( + this, &isThirdParty)) || + isThirdParty) && + (mLoadInfo->InternalContentPolicyType() == nsIContentPolicy::TYPE_FETCH || + mLoadInfo->InternalContentPolicyType() == + nsIContentPolicy::TYPE_XMLHTTPREQUEST || + mLoadInfo->InternalContentPolicyType() == + nsIContentPolicy::TYPE_INTERNAL_XMLHTTPREQUEST)) { + mCacheIdExtension.Append("FETCH"); + } + + mCacheOpenWithPriority = cacheEntryOpenFlags & nsICacheStorage::OPEN_PRIORITY; + mCacheQueueSizeWhenOpen = + CacheStorageService::CacheQueueSize(mCacheOpenWithPriority); + + if ((mNetworkTriggerDelay || StaticPrefs::network_http_rcwn_enabled()) && + maybeRCWN) { + 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 + auto callback = MakeRefPtr(this); + NS_NewTimerWithCallback(getter_AddRefs(mCacheOpenTimer), callback, + mCacheOpenDelay, nsITimer::TYPE_ONE_SHOT); + } + NS_ENSURE_SUCCESS(rv, rv); + + waitFlags.Keep(WAIT_FOR_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, 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; + } + 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 + if ((LoadCacheEntryIsReadOnly() && + !(mLoadFlags & nsIRequest::INHIBIT_CACHING))) { + 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); + 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); + 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); + auto prefetchStatus = Telemetry::LABELS_PREDICTOR_PREFETCH_USE_STATUS::Used; + + 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; + prefetchStatus = Telemetry::LABELS_PREDICTOR_PREFETCH_USE_STATUS::WouldVary; + } else { + if (mCachedResponseHead->ExpiresInPast() || + mCachedResponseHead->MustValidateIfExpired()) { + prefetchStatus = Telemetry::LABELS_PREDICTOR_PREFETCH_USE_STATUS::Expired; + } + doValidation = nsHttp::ValidationRequired( + isForcedValid, mCachedResponseHead.get(), mLoadFlags, + LoadAllowStaleCacheContent(), LoadForceValidateCacheContent(), + 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)); + if (doValidation) { + prefetchStatus = Telemetry::LABELS_PREDICTOR_PREFETCH_USE_STATUS::Auth; + } + } + + // 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)); + + auto redirectedCachekeys = mRedirectedCachekeys.Lock(); + auto& ref = redirectedCachekeys.ref(); + if (!ref) { + ref = MakeUnique>(); + } else if (ref->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) { + ref->AppendElement(cacheKey); + } else { + prefetchStatus = + Telemetry::LABELS_PREDICTOR_PREFETCH_USE_STATUS::Redirect; + } + } + + mCachedContentIsValid = !doValidation; + + if (isForcedValid) { + // Telemetry value is only useful if this was a prefetched item + if (!doValidation) { + // Could have gotten to a funky state with some of the if chain above + // and in nsHttp::ValidationRequired. Make sure we get it right here. + prefetchStatus = Telemetry::LABELS_PREDICTOR_PREFETCH_USE_STATUS::Used; + + entry->MarkForcedValidUse(); + } + Telemetry::AccumulateCategorical(prefetchStatus); + } + + 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 + // + // do not override conditional headers when consumer has defined its own + if (!mCachedResponseHead->NoStore() && + (mRequestHead.IsGet() || mRequestHead.IsHead()) && + !LoadCustomConditionalRequest() && !weaklyFramed && + (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); + 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, + nsresult status) { + MOZ_ASSERT(NS_IsMainThread()); + + nsresult rv; + + LOG( + ("nsHttpChannel::OnCacheEntryAvailable [this=%p entry=%p " + "new=%d status=%" PRIx32 "]\n", + this, entry, aNew, static_cast(status))); + + // if the channel's already fired onStopRequest, then we should ignore + // this event. + if (!LoadIsPending()) { + mCacheInputStream.CloseAndRelease(); + return NS_OK; + } + + rv = OnCacheEntryAvailableInternal(entry, aNew, 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, + 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; + } + + rv = OnNormalCacheEntryAvailable(entry, aNew, status); + + if (NS_FAILED(rv) && (mLoadFlags & LOAD_ONLY_FROM_CACHE)) { + 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) { + StoreWaitForCacheEntry(LoadWaitForCacheEntry() & ~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); + } + } + + return NS_OK; +} + +// Generates the proper cache-key for this instance of nsHttpChannel +nsresult nsHttpChannel::GenerateCacheKey(uint32_t postID, + nsACString& cacheKey) { + AssembleCacheKey(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); + + return NS_OK; +} + +nsresult nsHttpChannel::OpenCacheInputStream(nsICacheEntry* cacheEntry, + bool startBuffering) { + 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; + } + + MOZ_ASSERT(mCachedSecurityInfo); + if (!mCachedSecurityInfo) { + 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. + LOG( + ("Will skip read from cache based on LOAD_ONLY_IF_MODIFIED " + "load flag\n")); + return NS_OK; + } + + // 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() == + nsICacheInfoChannel::PreferredAlternativeDataDeliveryType::ASYNC; + 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( + components::StreamTransport::Service()); + 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()) { + 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); + } + + 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::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( + components::CacheStorage::Service()); + if (!cacheStorageService) { + return; + } + + nsCOMPtr cacheStorage; + RefPtr info = GetLoadContextInfo(this); + Unused << cacheStorageService->DiskCacheStorage(info, + 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 DoAddCacheEntryHeaders(nsHttpChannel* self, nsICacheEntry* entry, + nsHttpRequestHead* requestHead, + nsHttpResponseHead* responseHead, + nsITransportSecurityInfo* 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; +} + +//----------------------------------------------------------------------------- +// 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)); + + if (!mEndMarkerAdded && profiler_thread_is_being_profiled_for_markers()) { + mEndMarkerAdded = true; + + nsAutoCString requestMethod; + GetRequestMethod(requestMethod); + + int32_t priority = PRIORITY_NORMAL; + GetPriority(&priority); + + TimingStruct timings; + if (mTransaction) { + timings = mTransaction->Timings(); + } + + uint64_t size = 0; + GetEncodedBodySize(&size); + + nsAutoCString contentType; + if (mResponseHead) { + mResponseHead->ContentType(contentType); + } + + RefPtr newIdentChannel = do_QueryObject(newChannel); + uint64_t channelId = 0; + if (newIdentChannel) { + channelId = newIdentChannel->ChannelId(); + } + profiler_add_network_marker( + mURI, requestMethod, priority, mChannelId, + NetworkLoadType::LOAD_REDIRECT, mLastStatusReported, TimeStamp::Now(), + size, mCacheDisposition, mLoadInfo->GetInnerWindowID(), + mLoadInfo->GetOriginAttributes().mPrivateBrowsingId > 0, &timings, + std::move(mSource), Some(nsDependentCString(contentType.get())), newURI, + redirectFlags, channelId); + } + + nsresult rv = HttpBaseChannel::SetupReplacementChannel( + newURI, newChannel, preserveMethod, redirectFlags); + if (NS_FAILED(rv)) return rv; + + rv = CheckRedirectLimit(redirectFlags); + NS_ENSURE_SUCCESS(rv, rv); + + // pass on the early hint observer to be able to process `103 Early Hints` + // responses after cross origin redirects + if (mEarlyHintObserver) { + if (RefPtr httpChannelImpl = do_QueryObject(newChannel)) { + httpChannelImpl->SetEarlyHintObserver(mEarlyHintObserver); + } + mEarlyHintObserver = nullptr; + } + + // We don't support redirection for WebTransport for now. + mWebTransportSessionEventListener = nullptr; + + 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)); + + nsresult rv = ProcessCrossOriginSecurityHeaders(); + if (NS_FAILED(rv)) { + mStatus = rv; + HandleAsyncAbort(); + return rv; + } + + 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))); + + 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 (!StaticPrefs::network_allow_redirect_to_data() && + !mLoadInfo->GetAllowInsecureRedirectToDataURI() && + mRedirectURI->SchemeIs("data")) { + LOG(("Invalid data URI for redirect!")); + nsContentSecurityManager::ReportBlockedDataURI(mRedirectURI, mLoadInfo, + true); + return NS_ERROR_DOM_BAD_URI; + } + + // Perform the URL query string stripping for redirects. We will only strip + // the query string if it is redirecting to a third-party URI in the top + // level. + if (StaticPrefs::privacy_query_stripping_redirect()) { + ThirdPartyUtil* thirdPartyUtil = ThirdPartyUtil::GetInstance(); + bool isThirdPartyRedirectURI = true; + thirdPartyUtil->IsThirdPartyURI(mURI, mRedirectURI, + &isThirdPartyRedirectURI); + if (isThirdPartyRedirectURI && mLoadInfo->GetExternalContentPolicyType() == + ExtContentPolicy::TYPE_DOCUMENT) { + Telemetry::AccumulateCategorical( + Telemetry::LABELS_QUERY_STRIPPING_COUNT::Redirect); + + nsCOMPtr prin; + ContentBlockingAllowList::RecomputePrincipal( + mRedirectURI, mLoadInfo->GetOriginAttributes(), getter_AddRefs(prin)); + + bool isRedirectURIInAllowList = false; + if (prin) { + ContentBlockingAllowList::Check(prin, mPrivateBrowsing, + isRedirectURIInAllowList); + } + + if (!isRedirectURIInAllowList) { + nsCOMPtr strippedURI; + + nsCOMPtr queryStripper = + components::URLQueryStringStripper::Service(&rv); + NS_ENSURE_SUCCESS(rv, rv); + + uint32_t numStripped; + + rv = queryStripper->Strip(mRedirectURI, mPrivateBrowsing, + getter_AddRefs(strippedURI), &numStripped); + NS_ENSURE_SUCCESS(rv, rv); + + if (numStripped) { + mUnstrippedRedirectURI = mRedirectURI; + mRedirectURI = strippedURI; + + // Record telemetry, but only if we stripped any query params. + Telemetry::AccumulateCategorical( + Telemetry::LABELS_QUERY_STRIPPING_COUNT::StripForRedirect); + Telemetry::Accumulate(Telemetry::QUERY_STRIPPING_PARAM_COUNT, + numStripped); + } + } + } + } + + if (NS_WARN_IF(!mRedirectURI)) { + LOG(("Invalid redirect URI after performaing query string stripping")); + return NS_ERROR_FAILURE; + } + + return ContinueProcessRedirectionAfterFallback(NS_OK); +} + +nsresult nsHttpChannel::ContinueProcessRedirectionAfterFallback(nsresult rv) { + // 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; + } + + uint32_t redirectFlags; + if (nsHttp::IsPermanentRedirect(mRedirectType)) { + redirectFlags = nsIChannelEventSink::REDIRECT_PERMANENT; + } else { + redirectFlags = nsIChannelEventSink::REDIRECT_TEMPORARY; + } + + nsCOMPtr ioService; + rv = gHttpHandler->GetIOService(getter_AddRefs(ioService)); + if (NS_FAILED(rv)) return rv; + + nsCOMPtr newChannel; + nsCOMPtr redirectLoadInfo = + CloneLoadInfoForRedirect(mRedirectURI, redirectFlags); + + // Propagate the unstripped redirect URI. + redirectLoadInfo->SetUnstrippedURI(mUnstrippedRedirectURI); + + 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, rv); + PopRedirectAsyncFunc(&nsHttpChannel::ContinueProcessRedirection); + } + + return rv; +} + +nsresult nsHttpChannel::ContinueProcessRedirection(nsresult rv) { + AutoRedirectVetoNotifier notifier(this, rv); + + 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(); + } + + if (StaticPrefs::network_auth_use_redirect_for_retries()) { + return CallOrWaitForResume( + [](auto* self) { return self->RedirectToNewChannelForAuthRetry(); }); + } + + return NS_OK; +} + +NS_IMETHODIMP nsHttpChannel::OnAuthCancelled(bool userCancel) { + LOG(("nsHttpChannel::OnAuthCancelled [this=%p]", this)); + MOZ_ASSERT(mAuthRetryPending, "OnAuthCancelled should not be called twice"); + + 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); + + // Make sure to process security headers before calling CallOnStartRequest. + nsresult rv = ProcessCrossOriginSecurityHeaders(); + if (NS_FAILED(rv)) { + mStatus = rv; + HandleAsyncAbort(); + return rv; + } + + // ensure call of OnStartRequest of the current listener here, + // it would not be called otherwise at all + 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(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(nsIRequestTailUnblockCallback) + NS_INTERFACE_MAP_ENTRY_CONCRETE(nsHttpChannel) + NS_INTERFACE_MAP_ENTRY(nsIEarlyHintObserver) +NS_INTERFACE_MAP_END_INHERITING(HttpBaseChannel) + +//----------------------------------------------------------------------------- +// nsHttpChannel::nsIRequest +//----------------------------------------------------------------------------- + +NS_IMETHODIMP nsHttpChannel::SetCanceledReason(const nsACString& aReason) { + return SetCanceledReasonImpl(aReason); +} + +NS_IMETHODIMP nsHttpChannel::GetCanceledReason(nsACString& aReason) { + return GetCanceledReasonImpl(aReason); +} + +NS_IMETHODIMP +nsHttpChannel::CancelWithReason(nsresult aStatus, const nsACString& aReason) { + return CancelWithReasonImpl(aStatus, aReason); +} + +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 ", reason=%s]\n", this, + static_cast(status), mCanceledReason.get())); + MOZ_ASSERT_IF(!(mConnectionInfo && mConnectionInfo->UsingConnect()) && + NS_SUCCEEDED(mStatus), + !AllowedErrorForHTTPSRRFallback(status)); + + mEarlyHintObserver = nullptr; + mWebTransportSessionEventListener = nullptr; + + 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) { + LOG(("nsHttpChannel::CancelInternal [this=%p]\n", this)); + bool channelClassifierCancellationPending = + !!LoadChannelClassifierCancellationPending(); + if (UrlClassifierFeatureFactory::IsClassifierBlockingErrorCode(status)) { + StoreChannelClassifierCancellationPending(0); + } + + // We don't want the content process to see any header values + // when the request is blocked by ORB + if (mChannelBlockedByOpaqueResponse && mCachedOpaqueResponseBlockingPref) { + mResponseHead->ClearHeaders(); + } + + mEarlyHintObserver = nullptr; + mWebTransportSessionEventListener = nullptr; + mCanceled = true; + mStatus = NS_FAILED(status) ? status : NS_ERROR_ABORT; + + if (mLastStatusReported && !mEndMarkerAdded && + profiler_thread_is_being_profiled_for_markers()) { + // These do allocations/frees/etc; avoid if not active + // mLastStatusReported can be null if Cancel is called before we added the + // start marker. + mEndMarkerAdded = true; + + nsAutoCString requestMethod; + GetRequestMethod(requestMethod); + + int32_t priority = PRIORITY_NORMAL; + GetPriority(&priority); + + uint64_t size = 0; + GetEncodedBodySize(&size); + + profiler_add_network_marker( + mURI, requestMethod, priority, mChannelId, NetworkLoadType::LOAD_CANCEL, + mLastStatusReported, TimeStamp::Now(), size, mCacheDisposition, + mLoadInfo->GetInnerWindowID(), + mLoadInfo->GetOriginAttributes().mPrivateBrowsingId > 0, + &mTransactionTimings, std::move(mSource)); + } + + // If we don't have mTransactionPump and mCachePump, we need to call + // AsyncAbort to make sure this channel's listener got notified. + bool needAsyncAbort = !mTransactionPump && !mCachePump; + + 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); + needAsyncAbort = false; + Unused << AsyncAbort(status); + } else if (channelClassifierCancellationPending) { + // If mCallOnResume is not null here, it's set in + // nsHttpChannel::CancelByURLClassifier. We can override mCallOnResume since + // mCanceled is true and nsHttpChannel::ContinueCancellingByURLClassifier + // does nothing. + if (mCallOnResume) { + mCallOnResume = nullptr; + } + // If we're coming from an asynchronous path when canceling a channel due + // to safe-browsing protection, we need to AsyncAbort the channel now. + needAsyncAbort = false; + Unused << AsyncAbort(status); + } + + // If we already have mCallOnResume, AsyncAbort will be called in + // ResumeInternal. + if (needAsyncAbort && !mCallOnResume && !mSuspendCount) { + LOG(("nsHttpChannel::CancelInternal do AsyncAbort [this=%p]\n", this)); + CloseCacheEntry(false); + 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); + + mEarlyHintObserver = nullptr; + mWebTransportSessionEventListener = nullptr; +} + +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(nsITransportSecurityInfo** securityInfo) { + NS_ENSURE_ARG_POINTER(securityInfo); + *securityInfo = do_AddRef(mSecurityInfo).take(); + 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)); + mOpenerCallingScriptLocation = CallingScriptLocationString(); + LogCallingScriptLocation(this, mOpenerCallingScriptLocation); + 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 NS_FAILED(mStatus) ? mStatus : NS_ERROR_FAILURE; + } + + if (MaybeWaitForUploadStreamNormalization(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 no one called SetLoadGroup or SetNotificationCallbacks, the private + // state has not been updated on PrivateBrowsingChannel (which we derive + // from) Same if the loadinfo has changed since the creation of the channel. + // Hence, we have to call UpdatePrivateBrowsing() here + UpdatePrivateBrowsing(); + + AntiTrackingUtils::UpdateAntiTrackingInfoForChannel(this); + + // Recalculate the default userAgent header after the AntiTrackingInfo gets + // updated because we can only know whether the site is exempted from + // fingerprinting protection after we have the AntiTracking Info. + // + // Note that we don't recalculate the header if it has been modified since the + // channel was created because we want to preserve the modified header. + if (!LoadIsUserAgentHeaderModified()) { + rv = mRequestHead.SetHeader( + nsHttp::User_Agent, + gHttpHandler->UserAgent(nsContentUtils::ShouldResistFingerprinting( + this, RFPTarget::HttpUserAgent)), + false, nsHttpHeaderArray::eVarietyRequestEnforceDefault); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + + 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; + } + + AsyncOpenFinal(TimeStamp::Now()); + + return NS_OK; +} + +void nsHttpChannel::AsyncOpenFinal(TimeStamp aTimeStamp) { + // We save this timestamp from outside of the if block in case we enable the + // profiler after AsyncOpen(). + mLastStatusReported = TimeStamp::Now(); + if (profiler_thread_is_being_profiled_for_markers()) { + nsAutoCString requestMethod; + GetRequestMethod(requestMethod); + + profiler_add_network_marker( + mURI, requestMethod, mPriority, mChannelId, NetworkLoadType::LOAD_START, + mChannelCreationTimestamp, mLastStatusReported, 0, mCacheDisposition, + mLoadInfo->GetInnerWindowID(), + mLoadInfo->GetOriginAttributes().mPrivateBrowsingId > 0); + } + + // 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)); + + bool willCallback = false; + // 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. + // We skip the check in case this is an internal redirected channel + if (!LoadAuthRedirectedChannel() && NS_ShouldClassifyChannel(this)) { + RefPtr self = this; + willCallback = NS_SUCCEEDED( + AsyncUrlChannelClassifier::CheckChannel(this, [self]() -> void { + nsCOMPtr uri; + self->GetURI(getter_AddRefs(uri)); + MOZ_ASSERT(uri); + + // Finish the AntiTracking Heuristic before + // MaybeResolveProxyAndBeginConnect(). + FinishAntiTrackingRedirectHeuristic(self, uri); + + self->MaybeResolveProxyAndBeginConnect(); + })); + } + + 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. + MaybeResolveProxyAndBeginConnect(); + } +} + +void 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)) && + !BypassProxy() && NS_SUCCEEDED(ResolveProxy())) { + return; + } + + if (!gHttpHandler->Active()) { + LOG( + ("nsHttpChannel::MaybeResolveProxyAndBeginConnect [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); + } +} + +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 +// MaybeResolveProxyAndBeginConnect and OnProxyAvailable ever call +// BeginConnect. +nsresult nsHttpChannel::BeginConnect() { + LOG(("nsHttpChannel::BeginConnect [this=%p]\n", this)); + nsresult rv; + + // It is the caller's responsibility to not call us late in shutdown. + MOZ_ASSERT(gHttpHandler->Active()); + + // 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(); + SetGlobalPrivacyControl(); + + 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, mPrivateBrowsing, + mCallbacks, originAttributes); + + RefPtr connInfo; +#ifdef FUZZING + if (StaticPrefs::fuzzing_necko_http3()) { + connInfo = + new nsHttpConnectionInfo(host, port, "h3"_ns, mUsername, proxyInfo, + originAttributes, host, port, true); + } else { +#endif + if (mWebTransportSessionEventListener) { + connInfo = + new nsHttpConnectionInfo(host, port, "h3"_ns, mUsername, proxyInfo, + originAttributes, isHttps, true, true); + } else { + connInfo = new nsHttpConnectionInfo(host, port, ""_ns, mUsername, + proxyInfo, originAttributes, isHttps); + } +#ifdef FUZZING + } +#endif + + bool http2Allowed = !gHttpHandler->IsHttp2Excluded(connInfo); + + bool http3Allowed = Http3Allowed(); + if (!http3Allowed) { + mCaps |= NS_HTTP_DISALLOW_HTTP3; + } + + RefPtr mapping; + if (!mConnectionInfo && LoadAllowAltSvc() && // per channel + !mWebTransportSessionEventListener && (http2Allowed || http3Allowed) && + !(mLoadFlags & LOAD_FRESH_CONNECTION) && + AltSvcMapping::AcceptableProxy(proxyInfo) && + (scheme.EqualsLiteral("http") || scheme.EqualsLiteral("https")) && + (mapping = gHttpHandler->GetAltServiceMapping( + scheme, host, port, mPrivateBrowsing, 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()); + } + // Like what we did for 'Authorization' header, we need to do the same for + // 'Alt-Used' for avoiding this header being shown in the ServiceWorker + // FetchEvent. + Unused << mRequestHead.ClearHeader(nsHttp::Alternate_Service_Used); + rv = mRequestHead.SetHeader(nsHttp::Alternate_Service_Used, altUsedLine, + false, + nsHttpHeaderArray::eVarietyRequestDefault); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + + nsCOMPtr consoleService = + do_GetService(NS_CONSOLESERVICE_CONTRACTID); + if (consoleService && !host.Equals(mapping->AlternateHost())) { + 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); + } 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); + } + + bool httpsRRAllowed = + !LoadBeConservative() && !(mCaps & NS_HTTP_BE_CONSERVATIVE) && + !(mLoadInfo->TriggeringPrincipal()->IsSystemPrincipal() && + mLoadInfo->GetExternalContentPolicyType() != + ExtContentPolicy::TYPE_DOCUMENT) && + !mConnectionInfo->UsingConnect(); + if (!httpsRRAllowed) { + mCaps |= NS_HTTP_DISALLOW_HTTPS_RR; + } + // No need to lookup HTTPSSVC record if mHTTPSSVCRecord already contains a + // value. + StoreUseHTTPSSVC(StaticPrefs::network_dns_upgrade_with_https_rr() && + httpsRRAllowed && mHTTPSSVCRecord.isNothing()); + + // Need to re-ask the handler, since mConnectionInfo may not be the connInfo + // we used earlier + if (!mConnectionInfo->IsHttp3() && + gHttpHandler->IsHttp2Excluded(mConnectionInfo)) { + StoreAllowSpdy(0); + mCaps |= NS_HTTP_DISALLOW_SPDY; + mConnectionInfo->SetNoSpdy(true); + } + + // We can be passed with the auth provider if this channel was + // a result of redirect due to auth retry + if (!mAuthProvider) { + 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.Flags() & nsIClassOfService::Leader) { + mCaps |= NS_HTTP_LOAD_AS_BLOCKING; + } + if (mClassOfService.Flags() & nsIClassOfService::Unblocked) { + mCaps |= NS_HTTP_LOAD_UNBLOCKED; + } + if (mClassOfService.Flags() & 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; + } + // skip classifier checks if this channel was the result of internal auth + // redirect + bool shouldBeClassified = + !LoadAuthRedirectedChannel() && 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 = CallOrWaitForResume( + [](nsHttpChannel* self) { return self->PrepareToConnect(); }); + 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() { + // 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)) || + LoadAuthRedirectedChannel()) { + 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()); + nsIDNSService::DNSFlags dnsFlags = nsIDNSService::RESOLVE_DEFAULT_FLAGS; + if (mCaps & NS_HTTP_REFRESH_DNS) { + dnsFlags |= nsIDNSService::RESOLVE_BYPASS_CACHE; + } + nsresult rv = mDNSPrefetch->PrefetchHigh(dnsFlags); + + 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__); + } + + if (gHttpHandler->UseHTTPSRRAsAltSvcEnabled() && !mHTTPSSVCRecord && + !(mCaps & NS_HTTP_DISALLOW_HTTPS_RR)) { + MOZ_ASSERT(!mHTTPSSVCRecord); + + OriginAttributes originAttributes; + StoragePrincipalHelper::GetOriginAttributesForHTTPSRR(this, + originAttributes); + + RefPtr resolver = + new nsDNSPrefetch(mURI, originAttributes, nsIRequest::GetTRRMode()); + Unused << resolver->FetchHTTPSSVC(mCaps & NS_HTTP_REFRESH_DNS, true, + [](nsIDNSHTTPSSVCRecord*) { + // Do nothing. This is a DNS prefetch. + }); + } + } + + 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::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() -> 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__); + return request->mPromise; + } + + mozilla::ipc::Endpoint parent; + mozilla::ipc::Endpoint child; + nsresult rv = extensions::PStreamFilter::CreateEndpoints(&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; +} + +//----------------------------------------------------------------------------- +// HttpChannel::nsIClassOfService +//----------------------------------------------------------------------------- + +void nsHttpChannel::OnClassOfServiceUpdated() { + LOG(("nsHttpChannel::OnClassOfServiceUpdated this=%p, cos=%lu, inc=%d", this, + mClassOfService.Flags(), mClassOfService.Incremental())); + + if (mTransaction) { + gHttpHandler->UpdateClassOfServiceOnTransaction(mTransaction, + mClassOfService); + } + if (EligibleForTailing()) { + RemoveAsNonTailRequest(); + } else { + AddAsNonTailRequest(); + } +} + +NS_IMETHODIMP +nsHttpChannel::SetClassFlags(uint32_t inFlags) { + uint32_t previous = mClassOfService.Flags(); + mClassOfService.SetFlags(inFlags); + if (previous != mClassOfService.Flags()) { + OnClassOfServiceUpdated(); + } + return NS_OK; +} + +NS_IMETHODIMP +nsHttpChannel::AddClassFlags(uint32_t inFlags) { + uint32_t previous = mClassOfService.Flags(); + mClassOfService.SetFlags(inFlags | mClassOfService.Flags()); + if (previous != mClassOfService.Flags()) { + OnClassOfServiceUpdated(); + } + return NS_OK; +} + +NS_IMETHODIMP +nsHttpChannel::ClearClassFlags(uint32_t inFlags) { + uint32_t previous = mClassOfService.Flags(); + mClassOfService.SetFlags(~inFlags & mClassOfService.Flags()); + if (previous != mClassOfService.Flags()) { + OnClassOfServiceUpdated(); + } + return NS_OK; +} + +NS_IMETHODIMP +nsHttpChannel::SetClassOfService(ClassOfService cos) { + ClassOfService previous = mClassOfService; + mClassOfService = cos; + if (previous != mClassOfService) { + OnClassOfServiceUpdated(); + } + return NS_OK; +} + +NS_IMETHODIMP +nsHttpChannel::SetIncremental(bool incremental) { + bool previous = mClassOfService.Incremental(); + mClassOfService.SetIncremental(incremental); + if (previous != mClassOfService.Incremental()) { + 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 = do_AddRef(mProxyInfo).take(); + } else { + *result = do_AddRef(mConnectionInfo->ProxyInfo()).take(); + } + 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; +} + +NS_IMETHODIMP +nsHttpChannel::GetTransactionPending(TimeStamp* _retval) { + if (mTransaction) { + *_retval = mTransaction->GetPendingTime(); + } else { + *_retval = mTransactionTimings.transactionPending; + } + 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 +//----------------------------------------------------------------------------- + +void nsHttpChannel::RecordOnStartTelemetry(nsresult aStatus, + bool aIsNavigation) { + Telemetry::Accumulate(Telemetry::HTTP_CHANNEL_ONSTART_SUCCESS, + NS_SUCCEEDED(aStatus)); + + if (mTransaction) { + Telemetry::Accumulate( + Telemetry::HTTP3_CHANNEL_ONSTART_SUCCESS, + (mTransaction->IsHttp3Used()) ? "http3"_ns : "no_http3"_ns, + NS_SUCCEEDED(aStatus)); + } + + enum class HttpOnStartState : uint32_t { + Success = 0, + DNSError = 1, + Others = 2, + }; + + if (TRRService::Get() && TRRService::Get()->IsConfirmed()) { + // Note this telemetry probe is not working when DNS resolution is done in + // the socket process. + HttpOnStartState state = HttpOnStartState::Others; + if (NS_SUCCEEDED(aStatus)) { + state = HttpOnStartState::Success; + } else if (aStatus == NS_ERROR_UNKNOWN_HOST || + aStatus == NS_ERROR_UNKNOWN_PROXY_HOST) { + state = HttpOnStartState::DNSError; + } + + if (aIsNavigation) { + Telemetry::Accumulate(Telemetry::HTTP_CHANNEL_PAGE_ONSTART_SUCCESS_TRR3, + TRRService::ProviderKey(), + static_cast(state)); + } else { + Telemetry::Accumulate(Telemetry::HTTP_CHANNEL_SUB_ONSTART_SUCCESS_TRR3, + TRRService::ProviderKey(), + static_cast(state)); + } + } + + if (nsIOService::UseSocketProcess() && mTransaction) { + const TimeStamp now = TimeStamp::Now(); + TimeStamp responseEnd = mTransaction->GetResponseEnd(); + if (!responseEnd.IsNull()) { + PerfStats::RecordMeasurement(PerfStats::Metric::ResponseEndSocketToParent, + now - responseEnd); + } + + mOnStartRequestStartTime = mTransaction->GetOnStartRequestStartTime(); + if (!mOnStartRequestStartTime.IsNull()) { + PerfStats::RecordMeasurement( + PerfStats::Metric::OnStartRequestSocketToParent, + now - mOnStartRequestStartTime); + } + } else { + mOnStartRequestStartTime = TimeStamp::Now(); + } +} + +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; + } + + if (mStatus == NS_ERROR_NON_LOCAL_CONNECTION_REFUSED) { + MOZ_CRASH_UNSAFE(nsPrintfCString("Attempting to connect to non-local " + "address! opener is [%s], uri is " + "[%s]", + mOpenerCallingScriptLocation + ? mOpenerCallingScriptLocation->get() + : "unknown", + mURI->GetSpecOrDefault().get()) + .get()); + } + + LOG(("nsHttpChannel::OnStartRequest [this=%p request=%p status=%" PRIx32 + "]\n", + this, request, static_cast(static_cast(mStatus)))); + + RecordOnStartTelemetry(mStatus, IsNavigation()); + + 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() && stage != HTTPSSVC_NOT_USED) { + Telemetry::Accumulate(Telemetry::DNS_HTTPSSVC_RECORD_RECEIVING_STAGE, + stage); + } + + if (HTTPS_RR_IS_USED(stage)) { + nsAutoCString suffix(LoadEchConfigUsed() ? "_ech_used" : ""); + // Determine the result string based on the status. + nsAutoCString result(NS_SUCCEEDED(mStatus) ? "success" : "failure"); + result.Append(suffix); + + mozilla::glean::networking::http_channel_onstart_success_https_rr + .Get(result) + .Add(1); + StoreHasHTTPSRR(true); + } + + StoreLoadedBySocketProcess(mTransaction->AsHttpTransactionParent() != + nullptr); + + bool isTrr; + bool echConfigUsed; + mTransaction->GetNetworkAddresses(mSelfAddr, mPeerAddr, isTrr, + mEffectiveTRRMode, mTRRSkipReason, + echConfigUsed); + StoreResolvedByTRR(isTrr); + StoreEchConfigUsed(echConfigUsed); + } + + // 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 = ProcessCrossOriginSecurityHeaders(); + if (NS_FAILED(rv)) { + mStatus = rv; + HandleAsyncAbort(); + return rv; + } + + // 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 || mStatus == NS_ERROR_NET_RESET)) { + 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; + } + + return ContinueOnStartRequest4(NS_OK); +} + +nsresult nsHttpChannel::ContinueOnStartRequest4(nsresult result) { + LOG(("nsHttpChannel::ContinueOnStartRequest4 [this=%p]", this)); + + if (NS_SUCCEEDED(mStatus) && mResponseHead && mAuthProvider) { + uint32_t httpStatus = mResponseHead->Status(); + if (httpStatus != 401 && httpStatus != 407) { + nsresult rv = mAuthProvider->CheckForSuperfluousAuth(); + if (NS_FAILED(rv)) { + mStatus = rv; + LOG((" CheckForSuperfluousAuth failed (%08x)", + static_cast(rv))); + } + } + } + + return CallOnStartRequest(); +} + +static void ReportHTTPSRRTelemetry( + const Maybe>& aMaybeRecord) { + bool hasHTTPSRR = aMaybeRecord && (aMaybeRecord.ref() != nullptr); + if (!hasHTTPSRR) { + mozilla::glean::networking::https_rr_presented.Get("none"_ns).Add(1); + return; + } + + const nsCOMPtr& record = aMaybeRecord.ref(); + nsCOMPtr svcbRecord; + if (NS_SUCCEEDED(record->GetServiceModeRecord(false, false, + getter_AddRefs(svcbRecord)))) { + MOZ_ASSERT(svcbRecord); + + Maybe> alpn = + svcbRecord->GetAlpn(); + bool isHttp3 = alpn ? IsHttp3(std::get<1>(*alpn)) : false; + mozilla::glean::networking::https_rr_presented + .Get(isHttp3 ? "presented_with_http3"_ns : "presented"_ns) + .Add(1); + } +} + +static nsLiteralCString ContentTypeToTelemetryLabel(nsHttpChannel* aChannel) { + nsAutoCString contentType; + aChannel->GetContentType(contentType); + + if (StringBeginsWith(contentType, "text/"_ns)) { + if (contentType.EqualsLiteral(TEXT_HTML)) { + return "text_html"_ns; + } + if (contentType.EqualsLiteral(TEXT_CSS)) { + return "text_css"_ns; + } + if (contentType.EqualsLiteral(TEXT_JSON)) { + return "text_json"_ns; + } + if (contentType.EqualsLiteral(TEXT_PLAIN)) { + return "text_plain"_ns; + } + if (contentType.EqualsLiteral(TEXT_JAVASCRIPT)) { + return "text_javascript"_ns; + } + return "text_other"_ns; + } + + if (StringBeginsWith(contentType, "audio/"_ns)) { + return "audio"_ns; + } + + if (StringBeginsWith(contentType, "video/"_ns)) { + return "video"_ns; + } + + if (StringBeginsWith(contentType, "multipart/"_ns)) { + return "multipart"_ns; + } + + if (StringBeginsWith(contentType, "image/"_ns)) { + if (contentType.EqualsLiteral(IMAGE_ICO) || + contentType.EqualsLiteral(IMAGE_ICO_MS) || + contentType.EqualsLiteral(IMAGE_ICON_MS)) { + return "icon"_ns; + } + return "image"_ns; + } + + if (StringBeginsWith(contentType, "application/"_ns)) { + if (contentType.EqualsLiteral(APPLICATION_JSON)) { + return "text_json"_ns; + } + if (contentType.EqualsLiteral(APPLICATION_OGG)) { + return "video"_ns; + } + if (contentType.EqualsLiteral("application/ocsp-response")) { + return "ocsp"_ns; + } + if (contentType.EqualsLiteral(APPLICATION_XPINSTALL)) { + return "xpinstall"_ns; + } + if (contentType.EqualsLiteral(APPLICATION_WASM)) { + return "wasm"_ns; + } + if (contentType.EqualsLiteral(APPLICATION_PDF) || + contentType.EqualsLiteral(APPLICATION_POSTSCRIPT)) { + return "pdf"_ns; + } + if (contentType.EqualsLiteral(APPLICATION_OCTET_STREAM)) { + return "octet_stream"_ns; + } + if (contentType.EqualsLiteral(APPLICATION_ECMASCRIPT) || + contentType.EqualsLiteral(APPLICATION_JAVASCRIPT) || + contentType.EqualsLiteral(APPLICATION_XJAVASCRIPT)) { + return "text_javascript"_ns; + } + if (contentType.EqualsLiteral(APPLICATION_NS_PROXY_AUTOCONFIG) || + contentType.EqualsLiteral(APPLICATION_NS_JAVASCRIPT_AUTOCONFIG)) { + return "proxy"_ns; + } + if (contentType.EqualsLiteral(APPLICATION_BROTLI) || + contentType.Find("zip") != kNotFound || + contentType.Find("compress") != kNotFound) { + return "compressed"_ns; + } + if (contentType.Find("x509") != kNotFound) { + return "x509"_ns; + } + return "application_other"_ns; + } + + if (contentType.EqualsLiteral(BINARY_OCTET_STREAM)) { + return "octet_stream"_ns; + } + + return "other"_ns; +} + +nsresult nsHttpChannel::LogConsoleError(const char* aTag) { + nsCOMPtr console( + do_GetService(NS_CONSOLESERVICE_CONTRACTID)); + NS_ENSURE_TRUE(console, NS_ERROR_OUT_OF_MEMORY); + + nsCOMPtr loadInfo = LoadInfo(); + NS_ENSURE_TRUE(console, NS_ERROR_OUT_OF_MEMORY); + uint64_t innerWindowID = loadInfo->GetInnerWindowID(); + + nsAutoString errorText; + nsresult rv = nsContentUtils::GetLocalizedString( + nsContentUtils::eNECKO_PROPERTIES, aTag, errorText); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr error(do_CreateInstance(NS_SCRIPTERROR_CONTRACTID)); + NS_ENSURE_TRUE(error, NS_ERROR_OUT_OF_MEMORY); + + rv = error->InitWithSourceURI(errorText, mURI, u""_ns, 0, 0, + nsIScriptError::errorFlag, + "Invalid HTTP Status Lines"_ns, innerWindowID); + NS_ENSURE_SUCCESS(rv, rv); + console->LogMessage(error); + return NS_OK; +} + +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 (mStatus == NS_ERROR_PARSING_HTTP_STATUS_LINE) { + Unused << LogConsoleError("InvalidHTTPResponseStatusLine"); + } + + if (WRONG_RACING_RESPONSE_SOURCE(request)) { + return NS_OK; + } + + // It's possible that LoadUseHTTPSSVC() is false, but we already have + // mHTTPSSVCRecord. + if (LoadUseHTTPSSVC() || mHTTPSSVCRecord) { + ReportHTTPSRRTelemetry(mHTTPSSVCRecord); + } + + // 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) && + // we should only auth retry in this channel if are not + // redirecting a new channel for authentication retries + !StaticPrefs::network_auth_use_redirect_for_retries()); + + 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 ((NS_FAILED(status)) && transactionWithStickyConn) { + // Close (don't reuse) the sticky connection if 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(); + + // Make sure the size does not overflow. + int32_t totalSize = static_cast( + std::clamp(mRequestSize + mTransferSize, 0LU, + std::numeric_limits::max())); + + // Record telemetry for transferred size keyed by contentType + nsLiteralCString label = ContentTypeToTelemetryLabel(this); + if (mPrivateBrowsing) { + mozilla::glean::network::data_size_pb_per_type.Get(label).Add(totalSize); + } else { + mozilla::glean::network::data_size_per_type.Get(label).Add(totalSize); + } + + // 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(); + + if (nsIOService::UseSocketProcess() && mTransaction) { + mOnStopRequestStartTime = mTransaction->GetOnStopRequestStartTime(); + if (!mOnStopRequestStartTime.IsNull()) { + PerfStats::RecordMeasurement( + PerfStats::Metric::OnStopRequestSocketToParent, + TimeStamp::Now() - mOnStopRequestStartTime); + } + } else { + mOnStopRequestStartTime = TimeStamp::Now(); + } + + // 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."); + if (!LoadOnStartRequestCalled()) { + nsCOMPtr listener(mListener); + StoreOnStartRequestCalled(true); + listener->OnStartRequest(this); + } + } else { + StoreOnStartRequestCalled(true); + NS_WARNING("OnStartRequest skipped because of null listener"); + } + mAuthRetryPending = false; + } + + // 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) { + if (nsIOService::UseSocketProcess() && upgradeConnect) { + // TODO: Support connection upgrade for socket process in bug 1632809. + Unused << mUpgradeProtocolCallback->OnUpgradeFailed( + NS_ERROR_NOT_IMPLEMENTED); + return ContinueOnStopRequest(aStatus, aIsFromNet, aContentComplete); + } + + nsresult rv = gHttpHandler->CompleteUpgrade(aTransWithStickyConn, + mUpgradeProtocolCallback); + mUpgradeProtocolCallback = nullptr; + 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); + + // Collect specific telemetry for measuring image, video, audio + // success/failure rates in regular browsing mode and when auto upgrading of + // subresources is enabled. Note that we only evaluate actual image types, not + // favicons. + nsContentPolicyType internalLoadType; + mLoadInfo->GetInternalContentPolicyType(&internalLoadType); + bool statusIsSuccess = NS_SUCCEEDED(aStatus); + if (internalLoadType == nsIContentPolicy::TYPE_INTERNAL_IMAGE || + internalLoadType == nsIContentPolicy::TYPE_INTERNAL_IMAGE_PRELOAD) { + if (mLoadInfo->GetBrowserDidUpgradeInsecureRequests()) { + Telemetry::AccumulateCategorical( + statusIsSuccess + ? Telemetry::LABELS_MIXED_CONTENT_IMAGES::ImgUpSuccess + : Telemetry::LABELS_MIXED_CONTENT_IMAGES::ImgUpFailure); + } else { + Telemetry::AccumulateCategorical( + statusIsSuccess + ? Telemetry::LABELS_MIXED_CONTENT_IMAGES::ImgNoUpSuccess + : Telemetry::LABELS_MIXED_CONTENT_IMAGES::ImgNoUpFailure); + } + } + if (internalLoadType == nsIContentPolicy::TYPE_INTERNAL_VIDEO) { + if (mLoadInfo->GetBrowserDidUpgradeInsecureRequests()) { + Telemetry::AccumulateCategorical( + statusIsSuccess + ? Telemetry::LABELS_MIXED_CONTENT_VIDEO::VideoUpSuccess + : Telemetry::LABELS_MIXED_CONTENT_VIDEO::VideoUpFailure); + } else { + Telemetry::AccumulateCategorical( + statusIsSuccess + ? Telemetry::LABELS_MIXED_CONTENT_VIDEO::VideoNoUpSuccess + : Telemetry::LABELS_MIXED_CONTENT_VIDEO::VideoNoUpFailure); + } + } + if (internalLoadType == nsIContentPolicy::TYPE_INTERNAL_AUDIO) { + if (mLoadInfo->GetBrowserDidUpgradeInsecureRequests()) { + Telemetry::AccumulateCategorical( + statusIsSuccess + ? Telemetry::LABELS_MIXED_CONTENT_AUDIO::AudioUpSuccess + : Telemetry::LABELS_MIXED_CONTENT_AUDIO::AudioUpFailure); + } else { + Telemetry::AccumulateCategorical( + statusIsSuccess + ? Telemetry::LABELS_MIXED_CONTENT_AUDIO::AudioNoUpSuccess + : Telemetry::LABELS_MIXED_CONTENT_AUDIO::AudioNoUpFailure); + } + } + + // 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); + + // We are about to perform a different network request. + // We must set mRaceCacheWithNetwork to false because otherwise + // we would ignore the network response thinking we didn't need it. + mRaceCacheWithNetwork = false; + + // 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(); + + if (!mEndMarkerAdded && profiler_thread_is_being_profiled_for_markers()) { + // These do allocations/frees/etc; avoid if not active + mEndMarkerAdded = true; + + nsAutoCString requestMethod; + GetRequestMethod(requestMethod); + + int32_t priority = PRIORITY_NORMAL; + GetPriority(&priority); + + uint64_t size = 0; + GetEncodedBodySize(&size); + + nsAutoCString contentType; + if (mResponseHead) { + mResponseHead->ContentType(contentType); + } + profiler_add_network_marker( + mURI, requestMethod, priority, mChannelId, NetworkLoadType::LOAD_STOP, + mLastStatusReported, TimeStamp::Now(), size, mCacheDisposition, + mLoadInfo->GetInnerWindowID(), + mLoadInfo->GetOriginAttributes().mPrivateBrowsingId > 0, + &mTransactionTimings, std::move(mSource), + Some(nsDependentCString(contentType.get()))); + } + + if (mAuthRetryPending && + StaticPrefs::network_auth_use_redirect_for_retries()) { + nsresult rv = OpenRedirectChannel(aStatus); + LOG(("Opening redirect channel for auth retry %x", + static_cast(rv))); + if (NS_FAILED(rv)) { + if (mListener) { + MOZ_ASSERT(!LoadOnStartRequestCalled(), + "We should not call OnStartRequest twice."); + if (!LoadOnStartRequestCalled()) { + nsCOMPtr listener(mListener); + StoreOnStartRequestCalled(true); + listener->OnStartRequest(this); + } + } else { + StoreOnStartRequestCalled(true); + NS_WARNING("OnStartRequest skipped because of null listener"); + } + } + mAuthRetryPending = false; + } + 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; + + mRedirectChannel = nullptr; + + // notify "http-on-stop-connect" observers + gHttpHandler->OnStopRequest(this); + + RemoveAsNonTailRequest(); + + if (mChannelBlockedByOpaqueResponse && mCachedOpaqueResponseBlockingPref) { + mResponseHead->ClearHeaders(); + } + // 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 (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; + } + + if (nsIOService::UseSocketProcess() && mTransaction) { + mOnDataAvailableStartTime = mTransaction->GetDataAvailableStartTime(); + if (!mOnDataAvailableStartTime.IsNull()) { + PerfStats::RecordMeasurement( + PerfStats::Metric::OnDataAvailableSocketToParent, + TimeStamp::Now() - mOnDataAvailableStartTime); + } + } else { + mOnDataAvailableStartTime = TimeStamp::Now(); + } + 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(nsISerialEventTarget* 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 = GetMainThreadSerialEventTarget(); + NS_ENSURE_TRUE(main, NS_ERROR_UNEXPECTED); + rv = retargetableCachePump->RetargetDeliveryTo(main); + } + } + return rv; +} + +NS_IMETHODIMP +nsHttpChannel::GetDeliveryTarget(nsISerialEventTarget** 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; +} + +NS_IMETHODIMP +nsHttpChannel::OnDataFinished(nsresult aStatus) { + nsCOMPtr listener = + do_QueryInterface(mListener); + + if (listener) { + return listener->OnDataFinished(aStatus); + } + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// 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, + mEffectiveTRRMode, mTRRSkipReason, + echConfigUsed); + } else { + nsCOMPtr socketTransport = do_QueryInterface(trans); + if (socketTransport) { + socketTransport->GetSelfAddr(&mSelfAddr); + socketTransport->GetPeerAddr(&mPeerAddr); + socketTransport->ResolvedByTRR(&isTrr); + socketTransport->GetEffectiveTRRMode(&mEffectiveTRRMode); + 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(uint32_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::SetForceValidateCacheContent(bool aForceValidateCacheContent) { + LOG(("nsHttpChannel::SetForceValidateCacheContent [this=%p, allow=%d]", this, + aForceValidateCacheContent)); + StoreForceValidateCacheContent(aForceValidateCacheContent); + return NS_OK; +} +NS_IMETHODIMP +nsHttpChannel::GetForceValidateCacheContent(bool* aForceValidateCacheContent) { + NS_ENSURE_ARG(aForceValidateCacheContent); + *aForceValidateCacheContent = LoadForceValidateCacheContent(); + 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, + PreferredAlternativeDataDeliveryType 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::GetAlternativeDataInputStream(nsIInputStream** aInputStream) { + NS_ENSURE_ARG_POINTER(aInputStream); + + *aInputStream = nullptr; + + nsCOMPtr cacheEntry = + mCacheEntry ? mCacheEntry : mAltDataCacheEntry; + if (!mAvailableCachedAltDataType.IsEmpty() && cacheEntry) { + nsresult rv = cacheEntry->OpenAlternativeInputStream( + mAvailableCachedAltDataType, aInputStream); + NS_ENSURE_SUCCESS(rv, rv); + } + 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::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); + nsresult rv = NS_ERROR_NO_INTERFACE; + if (seekable) { + rv = seekable->Seek(nsISeekableStream::NS_SEEK_SET, 0); + } + + // This should not normally happen, but it's possible that big memory + // blobs originating in the other process can't be rewinded. + // In that case we just fail the request, otherwise the content length + // will not match and this load will never complete. + NS_ENSURE_SUCCESS(rv, rv); + } + + // 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::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. + // However, if we are redirecting due to auth retries, we open the + // redirected channel after OnStopRequest. In that case we should not + // release the reference + if (!StaticPrefs::network_auth_use_redirect_for_retries() || + !mAuthRetryPending) { + 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)); + + if (mHTTPSSVCRecord) { + MOZ_ASSERT(false, "OnHTTPSRRAvailable called twice!"); + return; + } + + 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, !!httprr); + 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. + // 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( + components::CacheStorage::Service()); + rv = cacheStorageService ? NS_OK : NS_ERROR_FAILURE; + + nsCOMPtr cacheStorage; + if (NS_SUCCEEDED(rv)) { + RefPtr info = GetLoadContextInfo(this); + rv = cacheStorageService->DiskCacheStorage(info, + 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, + GetCurrentSerialEventTarget(), + 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 LoadWaitForCacheEntry() != 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); +} + +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); +} + +// This is loosely based on: +// https://fetch.spec.whatwg.org/#serializing-a-request-origin +static bool HasNullRequestOrigin(nsHttpChannel* aChannel, nsIURI* aURI, + bool isAddonRequest) { + // Step 1. If request has a redirect-tainted origin, then return "null". + if (aChannel->HasRedirectTaintedOrigin()) { + if (StaticPrefs::network_http_origin_redirectTainted()) { + return true; + } + } + + // Non-standard: Only allow HTTP and HTTPS origins. + if (!ReferrerInfo::IsReferrerSchemeAllowed(aURI)) { + // And moz-extension: for add-on initiated requests. + if (!aURI->SchemeIs("moz-extension") || !isAddonRequest) { + return true; + } + } + + // Non-standard: Hide onion URLs. + if (StaticPrefs::network_http_referer_hideOnionSource()) { + nsAutoCString host; + if (NS_SUCCEEDED(aURI->GetAsciiHost(host)) && + StringEndsWith(host, ".onion"_ns)) { + return ReferrerInfo::IsCrossOriginRequest(aChannel); + } + } + + // Step 2. Return request’s origin, serialized. + return false; +} + +// Step 8.12. of HTTP-network-or-cache fetch +// +// https://fetch.spec.whatwg.org/#append-a-request-origin-header +void nsHttpChannel::SetOriginHeader() { + auto* triggeringPrincipal = + BasePrincipal::Cast(mLoadInfo->TriggeringPrincipal()); + + if (triggeringPrincipal->IsSystemPrincipal()) { + // We can't infer an Origin header from the system principal, + // this means system requests use whatever Origin header was specified. + return; + } + bool isAddonRequest = triggeringPrincipal->AddonPolicy() || + triggeringPrincipal->ContentScriptAddonPolicy(); + + // Non-standard: Handle already existing Origin header. + nsAutoCString existingHeader; + Unused << mRequestHead.GetHeader(nsHttp::Origin, existingHeader); + if (!existingHeader.IsEmpty()) { + LOG(("nsHttpChannel::SetOriginHeader Origin header already present")); + auto const shouldNullifyOriginHeader = + [&existingHeader, isAddonRequest](nsHttpChannel* aChannel) { + nsCOMPtr uri; + nsresult rv = NS_NewURI(getter_AddRefs(uri), existingHeader); + if (NS_FAILED(rv)) { + return false; + } + + if (HasNullRequestOrigin(aChannel, uri, isAddonRequest)) { + return true; + } + + nsCOMPtr info = aChannel->LoadInfo(); + if (info->GetTainting() == mozilla::LoadTainting::CORS) { + return false; + } + + return ReferrerInfo::ShouldSetNullOriginHeader(aChannel, uri); + }; + + if (!existingHeader.EqualsLiteral("null") && + shouldNullifyOriginHeader(this)) { + LOG(("nsHttpChannel::SetOriginHeader null Origin by Referrer-Policy")); + MOZ_ALWAYS_SUCCEEDS( + mRequestHead.SetHeader(nsHttp::Origin, "null"_ns, false /* merge */)); + } + return; + } + + if (StaticPrefs::network_http_sendOriginHeader() == 0) { + // Custom user setting: 0 means never send Origin header. + return; + } + + // Step 1. Let serializedOrigin be the result of byte-serializing a request + // origin with request. + nsAutoCString serializedOrigin; + nsCOMPtr uri; + { + if (NS_FAILED(triggeringPrincipal->GetURI(getter_AddRefs(uri)))) { + return; + } + + if (!uri) { + if (isAddonRequest) { + // For add-on compatibility prefer sending no header at all + // instead of `Origin: null`. + return; + } + + // Otherwise use "null" when the triggeringPrincipal's URI is nullptr. + serializedOrigin.AssignLiteral("null"); + } else if (HasNullRequestOrigin(this, uri, isAddonRequest)) { + serializedOrigin.AssignLiteral("null"); + } else { + nsContentUtils::GetWebExposedOriginSerialization(uri, serializedOrigin); + } + } + + // Step 2. If request’s response tainting is "cors" or request’s mode is + // "websocket", then append (`Origin`, serializedOrigin) to request’s header + // list. + // + // Note: We don't handle "websocket" here (yet?). + if (mLoadInfo->GetTainting() == mozilla::LoadTainting::CORS) { + MOZ_ALWAYS_SUCCEEDS(mRequestHead.SetHeader(nsHttp::Origin, serializedOrigin, + false /* merge */)); + return; + } + + // Step 3. Otherwise, if request’s method is neither `GET` nor `HEAD`, then: + if (mRequestHead.IsGet() || mRequestHead.IsHead()) { + return; + } + + if (!serializedOrigin.EqualsLiteral("null")) { + // Step 3.1. (Implemented by ReferrerInfo::ShouldSetNullOriginHeader) + if (ReferrerInfo::ShouldSetNullOriginHeader(this, uri)) { + serializedOrigin.AssignLiteral("null"); + } else if (StaticPrefs::network_http_sendOriginHeader() == 1) { + // Non-standard: Restrict Origin to same-origin loads if requested by user + nsAutoCString currentOrigin; + nsContentUtils::GetWebExposedOriginSerialization(mURI, currentOrigin); + if (!serializedOrigin.EqualsIgnoreCase(currentOrigin.get())) { + // Origin header suppressed by user setting. + serializedOrigin.AssignLiteral("null"); + } + } + } + + // Step 3.2. Append (`Origin`, serializedOrigin) to request’s header list. + MOZ_ALWAYS_SUCCEEDS(mRequestHead.SetHeader(nsHttp::Origin, serializedOrigin, + false /* merge */)); +} + +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::SetGlobalPrivacyControl() { + MOZ_ASSERT(NS_IsMainThread(), "Must be called on the main thread"); + + if (StaticPrefs::privacy_globalprivacycontrol_functionality_enabled() && + (StaticPrefs::privacy_globalprivacycontrol_enabled() || + (StaticPrefs::privacy_globalprivacycontrol_pbmode_enabled() && + NS_UsePrivateBrowsing(this)))) { + // Send the header with a value of 1 to indicate opting-out + DebugOnly rv = + mRequestHead.SetHeader(nsHttp::GlobalPrivacyControl, "1"_ns, false); + } +} + +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) { + LOG(("nsHttpChannel::Test_delayCacheEntryOpeningBy this=%p timeout=%d", this, + aTimeout)); + MOZ_ASSERT(NS_IsMainThread(), "Must be called on the main thread"); + mRaceCacheWithNetwork = true; + mCacheOpenDelay = aTimeout; + if (mCacheOpenTimer) { + mCacheOpenTimer->SetDelay(aTimeout); + } + return NS_OK; +} + +NS_IMETHODIMP +nsHttpChannel::Test_triggerDelayedOpenCacheEntry() { + LOG(("nsHttpChannel::Test_triggerDelayedOpenCacheEntry this=%p", this)); + 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 (mNetworkTriggerDelay) { + aDelay = mNetworkTriggerDelay; + } + + 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); + } + + MOZ_ASSERT(!mNetworkTriggerTimer); + mNetworkTriggerTimer = NS_NewTimer(); + auto callback = MakeRefPtr(this); + LOG(("Creating new networkTriggertimer for delay")); + mNetworkTriggerTimer->InitWithCallback(callback, 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. + mRaceCacheWithNetwork = + AwaitingCacheCallbacks() && + (mCacheOpenFunc || StaticPrefs::network_http_rcwn_enabled()); + + LOG((" triggering network rcwn=%d\n", bool(mRaceCacheWithNetwork))); + return ContinueConnect(); +} + +void nsHttpChannel::MaybeRaceCacheWithNetwork() { + nsresult rv; + + nsCOMPtr netLinkSvc = + do_GetService(NS_NETWORK_LINK_SERVICE_CONTRACTID, &rv); + if (NS_FAILED(rv)) { + return; + } + + uint32_t linkType; + rv = netLinkSvc->GetLinkType(&linkType); + if (NS_FAILED(rv)) { + return; + } + + if (!(linkType == nsINetworkLinkService::LINK_TYPE_ETHERNET || +#ifndef MOZ_WIDGET_ANDROID + // On Android we don't assume an unknown link type is unmetered + linkType == nsINetworkLinkService::LINK_TYPE_UNKNOWN || +#endif + linkType == nsINetworkLinkService::LINK_TYPE_USB || + linkType == nsINetworkLinkService::LINK_TYPE_WIFI)) { + return; + } + + // Don't trigger the network if the load flags say so. + if (mLoadFlags & (LOAD_ONLY_FROM_CACHE | LOAD_NO_NETWORK_IO)) { + return; + } + + // We must not race if the channel has a failure status code. + if (NS_FAILED(mStatus)) { + return; + } + + // If a CORS Preflight is required we must not race. + if (LoadRequireCORSPreflight() && !LoadIsCorsPreflightDone()) { + return; + } + + 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() || mNetworkTriggerDelay, + "The pref must be turned on."); + LOG(("nsHttpChannel::MaybeRaceCacheWithNetwork [this=%p, delay=%u]\n", this, + mRaceDelay)); + + TriggerNetworkWithDelay(mRaceDelay); +} + +NS_IMETHODIMP +nsHttpChannel::Test_triggerNetwork(int32_t aTimeout) { + LOG(("nsHttpChannel::Test_triggerNetwork this=%p timeout=%d", this, + aTimeout)); + MOZ_ASSERT(NS_IsMainThread(), "Must be called on the main thread"); + + // We set the trigger delay to the specified timeout. + mRaceCacheWithNetwork = true; + mNetworkTriggerDelay = aTimeout; + + // If we already have a timer, set the delay/ + if (mNetworkTriggerTimer) { + // If the timeout is 0 and there is a timer, we can trigger + // the network immediately. + MOZ_ASSERT(LoadWasOpened(), "Must have been opened before"); + if (!aTimeout) { + return TriggerNetwork(); + } + mNetworkTriggerTimer->SetDelay(aTimeout); + } + return NS_OK; +} + +nsHttpChannel::TimerCallback::TimerCallback(nsHttpChannel* aChannel) + : mChannel(aChannel) {} + +NS_IMPL_ISUPPORTS(nsHttpChannel::TimerCallback, nsITimerCallback, nsINamed) + +NS_IMETHODIMP +nsHttpChannel::TimerCallback::GetName(nsACString& aName) { + aName.AssignLiteral("nsHttpChannel"); + return NS_OK; +} + +NS_IMETHODIMP +nsHttpChannel::TimerCallback::Notify(nsITimer* aTimer) { + if (aTimer == mChannel->mCacheOpenTimer) { + return mChannel->Test_triggerDelayedOpenCacheEntry(); + } + if (aTimer == mChannel->mNetworkTriggerTimer) { + return mChannel->TriggerNetwork(); + } + MOZ_CRASH("Unknown timer"); + + return NS_OK; +} + +bool nsHttpChannel::EligibleForTailing() { + if (!(mClassOfService.Flags() & nsIClassOfService::Tail)) { + return false; + } + + if (mClassOfService.Flags() & + (nsIClassOfService::UrgentStart | nsIClassOfService::Leader | + nsIClassOfService::TailForbidden)) { + return false; + } + + if (mClassOfService.Flags() & nsIClassOfService::Unblocked && + !(mClassOfService.Flags() & 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(); +} + +// The specification for ORB is currently being written: +// https://whatpr.org/fetch/1442.html#orb-algorithm +// The `opaque-response-safelist check` is implemented in: +// * `HttpBaseChannel::PerformOpaqueResponseSafelistCheckBeforeSniff` +// * `nsHttpChannel::DisableIsOpaqueResponseAllowedAfterSniffCheck` +// * `HttpBaseChannel::PerformOpaqueResponseSafelistCheckAfterSniff` +// * `OpaqueResponseBlocker::ValidateJavaScript` +// +// Should only be called by nsMediaSniffer::GetMIMETypeFromContent and +// imageLoader::GetMIMETypeFromContent when the content type can be +// recognized by these sniffers. +void nsHttpChannel::DisableIsOpaqueResponseAllowedAfterSniffCheck( + SnifferType aType) { + // https://whatpr.org/fetch/1442.html#orb-algorithm + // This method covers steps, 8 and 10. + MOZ_ASSERT(XRE_IsParentProcess()); + + if (NeedOpaqueResponseAllowedCheckAfterSniff()) { + MOZ_ASSERT(mCachedOpaqueResponseBlockingPref); + + // If the sniffer type is media and the request comes from a media element, + // we would like to check: + // - Whether the information provided by the media element shows it's an + // initial request. + // - Whether the response's status is either 200 or 206. + // + // If any of the results is false, then we set + // mBlockOpaqueResponseAfterSniff to true and block the response later. + if (aType == SnifferType::Media) { + // Step 8 + MOZ_ASSERT(mLoadInfo); + + bool isMediaRequest; + mLoadInfo->GetIsMediaRequest(&isMediaRequest); + if (isMediaRequest) { + bool isInitialRequest; + mLoadInfo->GetIsMediaInitialRequest(&isInitialRequest); + MOZ_ASSERT(isInitialRequest); + + if (!isInitialRequest) { + // Step 8.1 + BlockOpaqueResponseAfterSniff( + u"media request after sniffing, but not initial request"_ns, + OpaqueResponseBlockedTelemetryReason::MEDIA_NOT_INITIAL); + return; + } + + if (mResponseHead->Status() != 200 && mResponseHead->Status() != 206) { + // Step 8.2 + BlockOpaqueResponseAfterSniff( + u"media request's response status is neither 200 nor 206"_ns, + OpaqueResponseBlockedTelemetryReason::MEDIA_INCORRECT_RESP); + return; + } + } + } + + // Step 8.3 if `aType == SnifferType::Media` + // Step 9 can be skipped, only `HTMLMediaElement` ever sets isMediaRequest. + // Step 10 if `aType == SnifferType::Image` + AllowOpaqueResponseAfterSniff(); + } +} + +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); + } + 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(); + + nsCOMPtr redirectLoadInfo = + CloneLoadInfoForRedirect(mURI, nsIChannelEventSink::REDIRECT_INTERNAL); + + nsresult rv = intercepted->Init( + mURI, mCaps, static_cast(mProxyInfo.get()), + mProxyResolveFlags, mProxyURI, mChannelId, type, 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. + 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, rv); + + PopRedirectAsyncFunc(&nsHttpChannel::ContinueAsyncRedirectChannelToURI); + } + + return rv; +} + +void nsHttpChannel::ReEvaluateReferrerAfterTrackingStatusIsKnown() { + nsCOMPtr cjs; + if (mLoadInfo) { + Unused << mLoadInfo->GetCookieJarSettings(getter_AddRefs(cjs)); + } + if (!cjs) { + cjs = net::CookieJarSettings::Create(mLoadInfo->GetLoadingPrincipal()); + } + 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())); +} + +NS_IMETHODIMP +nsHttpChannel::SetEarlyHintObserver(nsIEarlyHintObserver* aObserver) { + mEarlyHintObserver = aObserver; + return NS_OK; +} + +NS_IMETHODIMP +nsHttpChannel::EarlyHint(const nsACString& aLinkHeader, + const nsACString& aReferrerPolicy, + const nsACString& aCspHeader) { + LOG(("nsHttpChannel::EarlyHint.\n")); + + if (mEarlyHintObserver && nsContentUtils::ComputeIsSecureContext(this)) { + LOG(("nsHttpChannel::EarlyHint propagated.\n")); + mEarlyHintObserver->EarlyHint(aLinkHeader, aReferrerPolicy, aCspHeader); + } + return NS_OK; +} + +NS_IMETHODIMP nsHttpChannel::SetWebTransportSessionEventListener( + WebTransportSessionEventListener* aListener) { + mWebTransportSessionEventListener = aListener; + return NS_OK; +} + +already_AddRefed +nsHttpChannel::GetWebTransportSessionEventListener() { + RefPtr wt = + mWebTransportSessionEventListener; + return wt.forget(); +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/http/nsHttpChannel.h b/netwerk/protocol/http/nsHttpChannel.h new file mode 100644 index 0000000000..dede167f87 --- /dev/null +++ b/netwerk/protocol/http/nsHttpChannel.h @@ -0,0 +1,869 @@ +/* -*- 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 "AlternateServices.h" +#include "AutoClose.h" +#include "HttpBaseChannel.h" +#include "TimingStruct.h" +#include "mozilla/AtomicBitfields.h" +#include "mozilla/Atomics.h" +#include "mozilla/Mutex.h" +#include "mozilla/extensions/PStreamFilterParent.h" +#include "mozilla/net/DocumentLoadListener.h" +#include "nsIAsyncVerifyRedirectCallback.h" +#include "nsICacheEntry.h" +#include "nsICacheEntryOpenCallback.h" +#include "nsICachingChannel.h" +#include "nsICorsPreflightCallback.h" +#include "nsIDNSListener.h" +#include "nsIEarlyHintObserver.h" +#include "nsIHttpAuthenticableChannel.h" +#include "nsIProtocolProxyCallback.h" +#include "nsIRaceCacheWithNetwork.h" +#include "nsIStreamListener.h" +#include "nsIThreadRetargetableRequest.h" +#include "nsIThreadRetargetableStreamListener.h" +#include "nsITransportSecurityInfo.h" +#include "nsTArray.h" +#include "nsWeakReference.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 nsICachingChannel, + public nsICacheEntryOpenCallback, + public nsITransportEventSink, + public nsIProtocolProxyCallback, + public nsIHttpAuthenticableChannel, + public nsIAsyncVerifyRedirectCallback, + public nsIThreadRetargetableRequest, + public nsIThreadRetargetableStreamListener, + public nsIDNSListener, + public nsSupportsWeakReference, + public nsICorsPreflightCallback, + public nsIRaceCacheWithNetwork, + public nsIRequestTailUnblockCallback, + public nsIEarlyHintObserver { + 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_NSIASYNCVERIFYREDIRECTCALLBACK + NS_DECL_NSITHREADRETARGETABLEREQUEST + NS_DECL_NSIDNSLISTENER + NS_DECLARE_STATIC_IID_ACCESSOR(NS_HTTPCHANNEL_IID) + NS_DECL_NSIRACECACHEWITHNETWORK + NS_DECL_NSIREQUESTTAILUNBLOCKCALLBACK + NS_DECL_NSIEARLYHINTOBSERVER + + // 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, + nsILoadInfo* aLoadInfo) 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 SetCanceledReason(const nsACString& aReason) override; + NS_IMETHOD GetCanceledReason(nsACString& aReason) override; + NS_IMETHOD CancelWithReason(nsresult status, + const nsACString& reason) override; + NS_IMETHOD Cancel(nsresult status) override; + NS_IMETHOD Suspend() override; + NS_IMETHOD Resume() override; + // nsIChannel + NS_IMETHOD GetSecurityInfo(nsITransportSecurityInfo** aSecurityInfo) override; + NS_IMETHOD AsyncOpen(nsIStreamListener* aListener) override; + // nsIHttpChannel + NS_IMETHOD GetEncodedBodySize(uint64_t* aEncodedBodySize) override; + // nsIHttpChannelInternal + 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; + NS_IMETHOD SetClassOfService(ClassOfService cos) override; + NS_IMETHOD SetIncremental(bool incremental) 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; + + NS_IMETHOD GetTransactionPending( + mozilla::TimeStamp* aTransactionPending) 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, + bool aIsWarning) override; + NS_IMETHOD LogMimeTypeMismatch(const nsACString& aMessageName, bool aWarning, + const nsAString& aURL, + const nsAString& aContentType) override; + + NS_IMETHOD SetEarlyHintObserver(nsIEarlyHintObserver* aObserver) override; + NS_IMETHOD SetWebTransportSessionEventListener( + WebTransportSessionEventListener* aListener) override; + + void SetWarningReporter(HttpChannelSecurityWarningReporter* aReporter); + HttpChannelSecurityWarningReporter* GetWarningReporter(); + + bool DataSentToChildProcess() { return LoadDataSentToChildProcess(); } + + enum class SnifferType { Media, Image }; + void DisableIsOpaqueResponseAllowedAfterSniffCheck(SnifferType aType); + + public: /* internal necko use only */ + uint32_t GetRequestTime() const { return mRequestTime; } + + void AsyncOpenFinal(TimeStamp aTimeStamp); + + [[nodiscard]] nsresult OpenCacheEntry(bool isHttps); + [[nodiscard]] nsresult OpenCacheEntryInternal(bool isHttps); + [[nodiscard]] nsresult ContinueConnect(); + + [[nodiscard]] nsresult StartRedirectChannelToURI(nsIURI*, uint32_t); + + SnifferCategoryType GetSnifferCategoryType() const { + return mSnifferCategoryType; + } + + // 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->StoreWaitForCacheEntry(nsHttpChannel::WAIT_FOR_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->StoreWaitForCacheEntry(mChannel->LoadWaitForCacheEntry() & + mKeep); + } + + private: + nsHttpChannel* mChannel; + uint32_t mKeep : 1; + }; + + 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(); + + already_AddRefed + GetWebTransportSessionEventListener(); + + private: // used for alternate service validation + RefPtr mTransactionObserver; + + public: + void SetTransactionObserver(TransactionObserver* arg) { + mTransactionObserver = arg; + } + TransactionObserver* GetTransactionObserver() { return mTransactionObserver; } + + CacheDisposition mCacheDisposition{kCacheUnresolved}; + + protected: + virtual ~nsHttpChannel(); + + private: + using nsContinueRedirectionFunc = nsresult (nsHttpChannel::*)(nsresult); + + // 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.) + void 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 PrepareToConnect(); + [[nodiscard]] nsresult OnBeforeConnect(); + [[nodiscard]] nsresult ContinueOnBeforeConnect( + bool aShouldUpgrade, nsresult aStatus, bool aUpgradeWithHTTPSRR = false); + nsresult MaybeUseHTTPSRRForUpgrade(bool aShouldUpgrade, nsresult aStatus); + void OnHTTPSRRAvailable(nsIDNSHTTPSSVCRecord* aRecord); + [[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 redirectType); + [[nodiscard]] nsresult ContinueProcessRedirection(nsresult); + [[nodiscard]] nsresult ContinueProcessRedirectionAfterFallback(nsresult); + [[nodiscard]] nsresult ProcessFailedProxyConnect(uint32_t httpStatus); + 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(); + [[nodiscard]] nsresult PromptTempRedirect(); + [[nodiscard]] virtual nsresult SetupReplacementChannel( + nsIURI*, nsIChannel*, bool preserveMethod, + uint32_t redirectFlags) override; + void HandleAsyncRedirectToUnstrippedURI(); + + // proxy specific methods + [[nodiscard]] nsresult ProxyFailover(); + [[nodiscard]] nsresult AsyncDoReplaceWithProxy(nsIProxyInfo*); + [[nodiscard]] nsresult ResolveProxy(); + + // cache specific methods + [[nodiscard]] nsresult OnNormalCacheEntryAvailable(nsICacheEntry* aEntry, + bool aNew, + nsresult aEntryStatus); + [[nodiscard]] nsresult OnCacheEntryAvailableInternal(nsICacheEntry* entry, + bool aNew, + nsresult status); + [[nodiscard]] nsresult GenerateCacheKey(uint32_t postID, nsACString& key); + [[nodiscard]] nsresult UpdateExpirationTime(); + [[nodiscard]] nsresult CheckPartial(nsICacheEntry* aEntry, int64_t* aSize, + int64_t* aContentLength); + [[nodiscard]] nsresult ReadFromCache(bool alreadyMarkedValid); + void CloseCacheEntry(bool doomOnFailure); + [[nodiscard]] nsresult InitCacheEntry(); + void UpdateInhibitPersistentCachingFlag(); + [[nodiscard]] nsresult AddCacheEntryHeaders(nsICacheEntry* entry); + [[nodiscard]] nsresult FinalizeCacheEntry(); + [[nodiscard]] nsresult InstallCacheListener(int64_t offset = 0); + void MaybeInvalidateCacheEntryForSubsequentGet(); + void AsyncOnExamineCachedResponse(); + + // 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 HTTP Strict Transport Security (HSTS) headers. + * Some basic consistency checks have been applied to the channel. Called + * from ProcessSecurityHeaders. + */ + [[nodiscard]] nsresult ProcessHSTSHeader(nsITransportSecurityInfo* aSecInfo); + + 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); + + void SetPushedStreamTransactionAndId( + HttpTransactionShell* aTransWithPushedStream, uint32_t aPushedStreamId); + + void SetOriginHeader(); + void SetDoNotTrack(); + void SetGlobalPrivacyControl(); + + already_AddRefed GetOrCreateChannelClassifier(); + + // Start an internal redirect to a new InterceptedHttpChannel which will + // resolve in firing a ServiceWorker FetchEvent. + [[nodiscard]] nsresult RedirectToInterceptedChannel(); + + // Start an internal redirect to a new channel for auth retry + [[nodiscard]] nsresult RedirectToNewChannelForAuthRetry(); + + // 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. + // auth specific data + nsCOMPtr mAuthProvider; + nsCOMPtr mRedirectURI; + nsCOMPtr mUnstrippedRedirectURI; + 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; + RefPtr mTransactionSticky; + + uint64_t mLogicalOffset{0}; + + // 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{0}; + uint32_t mRequestTime{0}; + + nsTArray mStreamFilterRequests; + + mozilla::TimeStamp mOnStartRequestTimestamp; + // Timestamp of the time the channel was suspended. + mozilla::TimeStamp mSuspendTimestamp; + mozilla::TimeStamp mOnCacheEntryCheckTimestamp; + + // Properties used for the profiler markers + // This keeps the timestamp for the start marker, to be reused for the end + // marker. + mozilla::TimeStamp mLastStatusReported; + // This is true when one end marker is output, so that we never output more + // than one. + bool mEndMarkerAdded = false; + + // Total time the channel spent suspended. This value is reported to + // telemetry in nsHttpChannel::OnStartRequest(). + uint32_t mSuspendTotalTime{0}; + + friend class AutoRedirectVetoNotifier; + friend class HttpAsyncAborter; + + uint32_t mRedirectType{0}; + + static const uint32_t WAIT_FOR_CACHE_ENTRY = 1; + + bool mCacheOpenWithPriority{false}; + uint32_t mCacheQueueSizeWhenOpen{0}; + + Atomic mCachedContentIsValid{false}; + Atomic mIsAuthChannel{false}; + Atomic mAuthRetryPending{false}; + + // 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 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, 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, WaitForCacheEntry, 1), + // 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 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), + (uint32_t, AuthRedirectedChannel, 1) + )) + // clang-format on + + nsTArray mRedirectFuncStack; + + // Needed for accurate DNS timing + RefPtr mDNSPrefetch; + + uint32_t mPushedStreamId{0}; + RefPtr mTransWithPushedStream; + + // True if the channel's principal was found on a phishing, malware, or + // tracking (if tracking protection is enabled) blocklist + bool mLocalBlocklist{false}; + + [[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. + using TailUnblockCallback = nsresult (nsHttpChannel::*)(); + TailUnblockCallback mOnTailUnblock{nullptr}; + // 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{false}; + + // nsITimerCallback is implemented on a subclass so that the name attribute + // doesn't conflict with the name attribute of the nsIRequest interface that + // might be present on the same object (as seen from JavaScript code). + class TimerCallback final : public nsITimerCallback, public nsINamed { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSITIMERCALLBACK + NS_DECL_NSINAMED + + explicit TimerCallback(nsHttpChannel* aChannel); + + private: + ~TimerCallback() = default; + + RefPtr mChannel; + }; + + // 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; + uint32_t mNetworkTriggerDelay = 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{RESPONSE_PENDING}; + + // Determines if it's possible and advisable to race the network request + // with the cache fetch, and proceeds to do so. + void 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); + + nsresult LogConsoleError(const char* aTag); + + void SetHTTPSSVCRecord(already_AddRefed&& aRecord); + + void RecordOnStartTelemetry(nsresult aStatus, bool aIsNavigation); + + // 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{false}; + uint32_t mRaceDelay{0}; + // If true then OnCacheEntryAvailable should ignore the entry, because + // SetupTransaction removed conditional headers and decisions made in + // OnCacheEntryCheck are no longer valid. + bool mIgnoreCacheEntry{false}; + // Lock preventing SetupTransaction/MaybeCreateCacheEntryWhenRCWN and + // OnCacheEntryCheck being called at the same time. + mozilla::Mutex mRCWNLock MOZ_UNANNOTATED{"nsHttpChannel.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{0}; + + // 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{false}; + + RefPtr mEarlyHintObserver; + Maybe mOpenerCallingScriptLocation; + RefPtr mWebTransportSessionEventListener; +}; + +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..8e7df0ae3d --- /dev/null +++ b/netwerk/protocol/http/nsHttpChannelAuthProvider.cpp @@ -0,0 +1,1913 @@ +/* -*- 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/StoragePrincipalHelper.h" +#include "mozilla/Tokenizer.h" +#include "MockHttpAuth.h" +#include "nsHttpChannelAuthProvider.h" +#include "nsCRT.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 "nsHttp.h" +#include "nsHttpBasicAuth.h" +#include "nsHttpDigestAuth.h" +#ifdef MOZ_AUTH_EXTENSION +# include "nsHttpNegotiateAuth.h" +#endif +#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() + : mProxyAuth(false), + mTriedProxyAuth(false), + mTriedHostAuth(false), + mSuppressDefensiveAuth(false), + mCrossOrigin(false), + mConnectionBased(false), + mHttpHandler(gHttpHandler) {} + +nsHttpChannelAuthProvider::~nsHttpChannelAuthProvider() { + MOZ_RELEASE_ASSERT(NS_IsMainThread()); + 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, 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 + if (!ProxyHost().IsEmpty() && UsingHttpProxy()) { + SetAuthorizationHeader(authCache, nsHttp::Proxy_Authorization, "http"_ns, + ProxyHost(), ProxyPort(), + ""_ns, // 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, Host(), + Port(), path, 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_SUPERFLUOS_AUTH); + return NS_ERROR_SUPERFLUOS_AUTH; + } + 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; +} + +// 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 nsACString& scheme, + const nsACString& host, int32_t port, const nsACString& directory, + const nsACString& realm, const nsACString& challenge, + const nsHttpAuthIdentity& ident, nsCOMPtr& sessionState, + nsACString& 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.BeginReading())); +#endif + + return UpdateCache(auth, scheme, host, port, directory, realm, challenge, + ident, result, generateFlags, sessionState, proxyAuth); +} + +nsresult nsHttpChannelAuthProvider::UpdateCache( + nsIHttpAuthenticator* auth, const nsACString& scheme, + const nsACString& host, int32_t port, const nsACString& directory, + const nsACString& realm, const nsACString& challenge, + const nsHttpAuthIdentity& ident, const nsACString& 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 : ""_ns, + saveChallenge ? challenge : ""_ns, 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, 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; +} + +class MOZ_STACK_CLASS ChallengeParser final : Tokenizer { + public: + explicit ChallengeParser(const nsACString& aChallenges) + : Tokenizer(aChallenges, nullptr, "") { + Record(); + } + + Maybe GetNext() { + Token t; + nsDependentCSubstring result; + + bool inQuote = false; + + while (Next(t)) { + if (t.Type() == TOKEN_EOL) { + Claim(result, ClaimInclusion::EXCLUDE_LAST); + SkipWhites(WhiteSkipping::INCLUDE_NEW_LINE); + Record(); + inQuote = false; + if (!result.IsEmpty()) { + return Some(result); + } + } else if (t.Equals(Token::Char(',')) && !inQuote) { + // Sometimes we get multiple challenges separated by a comma. + // This is not great, as it's slightly ambiguous. We check if something + // is a new challenge by matching agains = + // If the , isn't followed by a word and = then most likely + // it is the name of an authType. + + const char* prevCursorPos = mCursor; + const char* prevRollbackPos = mRollback; + + auto hasWordAndEqual = [&]() { + SkipWhites(); + nsDependentCSubstring word; + if (!ReadWord(word)) { + return false; + } + SkipWhites(); + return Check(Token::Char('=')); + }; + if (!hasWordAndEqual()) { + // This is not a parameter. It means the `,` character starts a + // different challenge. + // We'll revert the cursor and return the contents so far. + mCursor = prevCursorPos; + mRollback = prevRollbackPos; + Claim(result, ClaimInclusion::EXCLUDE_LAST); + SkipWhites(); + Record(); + if (!result.IsEmpty()) { + return Some(result); + } + } + } else if (t.Equals(Token::Char('"'))) { + inQuote = !inQuote; + } + } + + Claim(result, Tokenizer::ClaimInclusion::INCLUDE_LAST); + SkipWhites(); + Record(); + if (!result.IsEmpty()) { + return Some(result); + } + return Nothing{}; + } +}; + +enum ChallengeRank { + Unknown = 0, + Basic = 1, + Digest = 2, + NTLM = 3, + Negotiate = 4, +}; + +ChallengeRank Rank(const nsACString& aChallenge) { + if (StringBeginsWith(aChallenge, "Negotiate"_ns, + nsCaseInsensitiveCStringComparator)) { + return ChallengeRank::Negotiate; + } + + if (StringBeginsWith(aChallenge, "NTLM"_ns, + nsCaseInsensitiveCStringComparator)) { + return ChallengeRank::NTLM; + } + + if (StringBeginsWith(aChallenge, "Digest"_ns, + nsCaseInsensitiveCStringComparator)) { + return ChallengeRank::Digest; + } + + if (StringBeginsWith(aChallenge, "Basic"_ns, + nsCaseInsensitiveCStringComparator)) { + return ChallengeRank::Basic; + } + + return ChallengeRank::Unknown; +} + +nsresult nsHttpChannelAuthProvider::GetCredentials( + const nsACString& aChallenges, bool proxyAuth, nsCString& creds) { + LOG(("nsHttpChannelAuthProvider::GetCredentials")); + nsAutoCString challenges(aChallenges); + + using AuthChallenge = struct AuthChallenge { + nsDependentCSubstring challenge; + uint16_t algorithm = 0; + ChallengeRank rank = ChallengeRank::Unknown; + + void operator=(const AuthChallenge& aOther) { + challenge.Rebind(aOther.challenge, 0); + algorithm = aOther.algorithm; + rank = aOther.rank; + } + }; + + nsTArray cc; + + ChallengeParser p(challenges); + while (true) { + auto next = p.GetNext(); + if (next.isNothing()) { + break; + } + AuthChallenge ac{next.ref(), 0}; + nsAutoCString realm, domain, nonce, opaque; + bool stale = false; + uint16_t qop = 0; + ac.rank = Rank(ac.challenge); + if (StringBeginsWith(ac.challenge, "Digest"_ns, + nsCaseInsensitiveCStringComparator)) { + Unused << nsHttpDigestAuth::ParseChallenge(ac.challenge, realm, domain, + nonce, opaque, &stale, + &ac.algorithm, &qop); + } + cc.AppendElement(ac); + } + + cc.StableSort([](const AuthChallenge& lhs, const AuthChallenge& rhs) { + if (StaticPrefs::network_auth_choose_most_secure_challenge()) { + // Different auth types + if (lhs.rank != rhs.rank) { + return lhs.rank < rhs.rank ? 1 : -1; + } + + // If they're the same auth type, and not a Digest, then we treat them + // as equal (don't reorder them). + if (lhs.rank != ChallengeRank::Digest) { + return 0; + } + } else { + // Non-digest challenges should not be reordered when the pref is off. + if (lhs.algorithm == 0 || rhs.algorithm == 0) { + return 0; + } + } + + // Same algorithm. + if (lhs.algorithm == rhs.algorithm) { + return 0; + } + return lhs.algorithm < rhs.algorithm ? 1 : -1; + }); + + nsCOMPtr auth; + 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 (size_t i = 0; i < cc.Length(); i++) { + rv = GetAuthenticator(cc[i].challenge, authType, getter_AddRefs(auth)); + LOG(("trying auth for %s", authType.get())); + 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(cc[i].challenge, authType, 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 = cc[i].challenge; + // imperfect; does not save server-side preference ordering. + // instead, continues with remaining string as provided by client + mRemainingChallenges.Truncate(); + while (i + 1 < cc.Length()) { + i++; + mRemainingChallenges.Append(cc[i].challenge); + mRemainingChallenges.Append("\n"_ns); + } + 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, nsCString& 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 nsACString& aChallenge, const nsACString& aAuthType, bool proxyAuth, + nsIHttpAuthenticator* auth, nsCString& creds) { + LOG( + ("nsHttpChannelAuthProvider::GetCredentialsForChallenge " + "[this=%p channel=%p proxyAuth=%d challenges=%s]\n", + this, mAuthChannel, proxyAuth, nsCString(aChallenge).get())); + + // 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(aChallenge, 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 + nsAutoCString 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, host, port, realm, 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, aChallenge, 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, host, port, realm, suffix); + entry = nullptr; + ident->Clear(); + } + } else if (!identFromURI || + (ident->User() == entry->Identity().User() && + !(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. + *ident = entry->Identity(); + identFromURI = false; + if (entry->Creds()[0] != '\0') { + LOG((" using cached credentials!\n")); + creds.Assign(entry->Creds()); + return entry->AddPath(path); + } + } + } 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.Equals(aAuthType, nsCaseInsensitiveCStringComparator)) { + Telemetry::Accumulate( + Telemetry::HTTP_AUTH_TYPE_STATS, + UsingSSL() ? HTTP_AUTH_BASIC_SECURE : HTTP_AUTH_BASIC_INSECURE); + } else if ("digest"_ns.Equals(aAuthType, + nsCaseInsensitiveCStringComparator)) { + Telemetry::Accumulate( + Telemetry::HTTP_AUTH_TYPE_STATS, + UsingSSL() ? HTTP_AUTH_DIGEST_SECURE : HTTP_AUTH_DIGEST_INSECURE); + } else if ("ntlm"_ns.Equals(aAuthType, + nsCaseInsensitiveCStringComparator)) { + Telemetry::Accumulate( + Telemetry::HTTP_AUTH_TYPE_STATS, + UsingSSL() ? HTTP_AUTH_NTLM_SECURE : HTTP_AUTH_NTLM_INSECURE); + } else if ("negotiate"_ns.Equals(aAuthType, + nsCaseInsensitiveCStringComparator)) { + 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, aAuthType, 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, host, port, path, realm, + aChallenge, *ident, sessionStateGrip, creds); + 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); + mCrossOrigin = !loadingPrinc->IsSameOrigin(mURI); + } + } + + 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 nsACString& aChallenge, nsCString& authType) { + auto spaceIndex = aChallenge.FindChar(' '); + authType = Substring(aChallenge, 0, spaceIndex); + // normalize to lowercase + ToLowerCase(authType); +} + +nsresult nsHttpChannelAuthProvider::GetAuthenticator( + const nsACString& aChallenge, nsCString& authType, + nsIHttpAuthenticator** auth) { + LOG(("nsHttpChannelAuthProvider::GetAuthenticator [this=%p channel=%p]\n", + this, mAuthChannel)); + + GetAuthType(aChallenge, authType); + + nsCOMPtr authenticator; +#ifdef MOZ_AUTH_EXTENSION + if (authType.EqualsLiteral("negotiate")) { + authenticator = nsHttpNegotiateAuth::GetOrCreate(); + } else +#endif + if (authType.EqualsLiteral("basic")) { + authenticator = nsHttpBasicAuth::GetOrCreate(); + } else if (authType.EqualsLiteral("digest")) { + authenticator = nsHttpDigestAuth::GetOrCreate(); + } else if (authType.EqualsLiteral("ntlm")) { + authenticator = nsHttpNTLMAuth::GetOrCreate(); + } else if (authType.EqualsLiteral("mock_auth") && + PR_GetEnv("XPCSHELL_TEST_PROFILE_DIR")) { + authenticator = MockHttpAuth::Create(); + } else { + return NS_ERROR_FACTORY_NOT_REGISTERED; + } + + if (!authenticator) { + // If called during shutdown it's possible that the singleton authenticator + // was already cleared so we have a null one here. + return NS_ERROR_NOT_AVAILABLE; + } + + MOZ_ASSERT(authenticator); + authenticator.forget(auth); + + return NS_OK; +} + +// buf contains "domain\user" +static void ParseUserDomain(const nsAString& buf, nsDependentSubstring& user, + nsDependentSubstring& domain) { + auto backslashPos = buf.FindChar(u'\\'); + if (backslashPos != kNotFound) { + domain.Rebind(buf, 0, backslashPos); + user.Rebind(buf, backslashPos + 1); + } +} + +void nsHttpChannelAuthProvider::GetIdentityFromURI(uint32_t authFlags, + nsHttpAuthIdentity& ident) { + LOG(("nsHttpChannelAuthProvider::GetIdentityFromURI [this=%p channel=%p]\n", + this, mAuthChannel)); + + bool hasUserPass; + if (NS_FAILED(mURI->GetHasUserPass(&hasUserPass)) || !hasUserPass) { + return; + } + + nsAutoString userBuf; + nsAutoString passBuf; + + // XXX i18n + nsAutoCString buf; + nsresult rv = mURI->GetUsername(buf); + if (NS_FAILED(rv)) { + return; + } + NS_UnescapeURL(buf); + CopyUTF8toUTF16(buf, userBuf); + + rv = mURI->GetPassword(buf); + if (NS_FAILED(rv)) { + return; + } + NS_UnescapeURL(buf); + CopyUTF8toUTF16(buf, passBuf); + + nsDependentSubstring user(userBuf, 0); + nsDependentSubstring domain(u""_ns, 0); + + if (authFlags & nsIHttpAuthenticator::IDENTITY_INCLUDES_DOMAIN) { + ParseUserDomain(userBuf, user, domain); + } + + ident = nsHttpAuthIdentity(domain, user, passBuf); +} + +void nsHttpChannelAuthProvider::ParseRealm(const nsACString& aChallenge, + 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. + // + + Tokenizer t(aChallenge); + + // The challenge begins with the authType. + // If we can't find that something has probably gone wrong. + t.SkipWhites(); + nsDependentCSubstring authType; + if (!t.ReadWord(authType)) { + return; + } + + // Will return true if the tokenizer advanced the cursor - false otherwise. + auto readParam = [&](nsDependentCSubstring& key, nsAutoCString& value) { + key.Rebind(EmptyCString(), 0); + value.Truncate(); + + t.SkipWhites(); + if (!t.ReadWord(key)) { + return false; + } + t.SkipWhites(); + if (!t.CheckChar('=')) { + return true; + } + t.SkipWhites(); + + Tokenizer::Token token1; + + t.Record(); + if (!t.Next(token1)) { + return true; + } + nsDependentCSubstring sub; + bool hasQuote = false; + if (token1.Equals(Tokenizer::Token::Char('"'))) { + hasQuote = true; + } else { + t.Claim(sub, Tokenizer::ClaimInclusion::INCLUDE_LAST); + value.Append(sub); + } + t.Record(); + Tokenizer::Token token2; + while (t.Next(token2)) { + if (hasQuote && token2.Equals(Tokenizer::Token::Char('"')) && + !token1.Equals(Tokenizer::Token::Char('\\'))) { + break; + } + if (!hasQuote && (token2.Type() == Tokenizer::TokenType::TOKEN_WS || + token2.Type() == Tokenizer::TokenType::TOKEN_EOL)) { + break; + } + + t.Claim(sub, Tokenizer::ClaimInclusion::INCLUDE_LAST); + if (!sub.Equals(R"(\)")) { + value.Append(sub); + } + t.Record(); + token1 = token2; + } + return true; + }; + + while (!t.CheckEOF()) { + nsDependentCSubstring key; + nsAutoCString value; + // If we couldn't read anything, and the input isn't followed by a , + // then we exit. + if (!readParam(key, value) && !t.Check(Tokenizer::Token::Char(','))) { + break; + } + // When we find the first instance of realm we exit. + // Theoretically there should be only one instance and we should fail + // if there are more, but we're trying to preserve existing behaviour. + if (key.Equals("realm"_ns, nsCaseInsensitiveCStringComparator)) { + realm = value; + break; + } + } +} + +class nsHTTPAuthInformation : public nsAuthInformationHolder { + public: + nsHTTPAuthInformation(uint32_t aFlags, const nsString& aRealm, + const nsACString& aAuthType) + : nsAuthInformationHolder(aFlags, aRealm, aAuthType) {} + + void SetToHttpAuthIdentity(uint32_t authFlags, nsHttpAuthIdentity& identity); +}; + +void nsHTTPAuthInformation::SetToHttpAuthIdentity( + uint32_t authFlags, nsHttpAuthIdentity& identity) { + identity = nsHttpAuthIdentity(Domain(), User(), Password()); +} + +nsresult nsHttpChannelAuthProvider::PromptForIdentity( + uint32_t level, bool proxyAuth, const nsACString& realm, + const nsACString& 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, 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; + + nsAutoCString 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, 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, host, port, realm, suffix, + &entry); + + nsCOMPtr sessionStateGrip; + if (entry) sessionStateGrip = entry->mMetaData; + + nsAuthInformationHolder* holder = + static_cast(aAuthInfo); + *ident = + nsHttpAuthIdentity(holder->Domain(), holder->User(), holder->Password()); + + nsAutoCString unused; + nsCOMPtr auth; + rv = GetAuthenticator(mCurrentChallenge, 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, host, port, path, realm, + mCurrentChallenge, *ident, sessionStateGrip, 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, 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 nsACString& 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, unused, getter_AddRefs(auth)); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString host; + int32_t port; + nsHttpAuthIdentity* ident; + nsAutoCString directory, scheme; + nsISupports** unusedContinuationState; + + // Get realm from challenge + nsAutoCString realm; + ParseRealm(mCurrentChallenge, realm); + + rv = GetAuthorizationMembers(mProxyAuth, scheme, host, port, directory, ident, + unusedContinuationState); + if (NS_FAILED(rv)) return rv; + + rv = + UpdateCache(auth, scheme, host, port, directory, realm, mCurrentChallenge, + *ident, aGeneratedCreds, aFlags, aSessionState, mProxyAuth); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + mCurrentChallenge.Truncate(); + + rv = ContinueOnAuthAvailable(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/prompter;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, const nsHttpAtom& header, + const nsACString& scheme, const nsACString& host, int32_t port, + const nsACString& 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 (ident.User() == entry->User()) { + uint32_t loadFlags; + if (NS_SUCCEEDED(mAuthChannel->GetLoadFlags(&loadFlags)) && + !(loadFlags & nsIChannel::LOAD_EXPLICIT_CREDENTIALS)) { + ident.Clear(); + } + } + } + bool identFromURI; + if (ident.IsEmpty()) { + ident = entry->Identity(); + identFromURI = false; + } else { + identFromURI = true; + } + + nsCString temp; // this must have the same lifetime as creds + nsAutoCString creds(entry->Creds()); + // 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.IsEmpty() || identFromURI) && !entry->Challenge().IsEmpty()) { + nsCOMPtr auth; + nsAutoCString unused; + rv = GetAuthenticator(entry->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(), entry->Challenge(), ident, + entry->mMetaData, temp); + if (NS_SUCCEEDED(rv)) creds = temp; + + // make sure the continuation state is null since we do not + // support mixing preemptive and 'multirequest' authentication. + NS_IF_RELEASE(*continuationState); + } + } + if (!creds.IsEmpty()) { + LOG((" adding \"%s\" request header\n", header.get())); + if (header == nsHttp::Proxy_Authorization) { + rv = mAuthChannel->SetProxyCredentials(creds); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } else { + rv = mAuthChannel->SetWWWCredentials(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..22f9a27ede --- /dev/null +++ b/netwerk/protocol/http/nsHttpChannelAuthProvider.h @@ -0,0 +1,185 @@ +/* -*- 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 "nsICancelable.h" + +class nsIHttpAuthenticableChannel; +class nsIHttpAuthenticator; +class nsIURI; + +namespace mozilla { +namespace net { + +class nsHttpHandler; +struct nsHttpAtom; + +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 nsCString& ProxyHost() const { + return mProxyInfo ? mProxyInfo->Host() : EmptyCString(); + } + + int32_t ProxyPort() const { return mProxyInfo ? mProxyInfo->Port() : -1; } + + const nsCString& Host() const { return mHost; } + 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 nsACString& scheme, + const nsACString& host, int32_t port, const nsACString& dir, + const nsACString& realm, const nsACString& challenge, + const nsHttpAuthIdentity& ident, nsCOMPtr& session, + nsACString& result); + [[nodiscard]] nsresult GetAuthenticator(const nsACString& aChallenge, + nsCString& authType, + nsIHttpAuthenticator** auth); + void ParseRealm(const nsACString&, 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 nsACString& challenges, + bool proxyAuth, nsCString& creds); + [[nodiscard]] nsresult GetCredentialsForChallenge( + const nsACString& aChallenge, const nsACString& aAuthType, bool proxyAuth, + nsIHttpAuthenticator* auth, nsCString& creds); + [[nodiscard]] nsresult PromptForIdentity(uint32_t level, bool proxyAuth, + const nsACString& realm, + const nsACString& authType, + uint32_t authFlags, + nsHttpAuthIdentity&); + + bool ConfirmAuth(const char* bundleKey, bool doYesNoPrompt); + void SetAuthorizationHeader(nsHttpAuthCache*, const nsHttpAtom& header, + const nsACString& scheme, const nsACString& host, + int32_t port, const nsACString& 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, nsCString& 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 nsACString& aScheme, + const nsACString& aHost, int32_t aPort, const nsACString& aDirectory, + const nsACString& aRealm, const nsACString& aChallenge, + const nsHttpAuthIdentity& aIdent, const nsACString& aCreds, + uint32_t aGenerateFlags, nsISupports* aSessionState, bool aProxyAuth); + + private: + nsIHttpAuthenticableChannel* mAuthChannel{nullptr}; // weak ref + + nsCOMPtr mURI; + nsCOMPtr mProxyInfo; + nsCString mHost; + int32_t mPort{-1}; + bool mUsingSSL{false}; + bool mProxyUsingSSL{false}; + bool mIsPrivate{false}; + + nsISupports* mProxyAuthContinuationState{nullptr}; + nsCString mProxyAuthType; + nsISupports* mAuthContinuationState{nullptr}; + 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..5ee8abc088 --- /dev/null +++ b/netwerk/protocol/http/nsHttpChunkedDecoder.cpp @@ -0,0 +1,170 @@ +/* -*- 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 + +#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 = 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..0e627087dc --- /dev/null +++ b/netwerk/protocol/http/nsHttpChunkedDecoder.h @@ -0,0 +1,50 @@ +/* -*- 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() = default; + ~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* bytesConsumed); + + private: + UniquePtr mTrailers; + uint32_t mChunkRemaining{0}; + nsCString mLineBuf; // may hold a partial line + bool mReachedEOF{false}; + bool mWaitEOF{false}; +}; + +} // 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..023c06f3f3 --- /dev/null +++ b/netwerk/protocol/http/nsHttpConnection.cpp @@ -0,0 +1,2609 @@ +/* -*- 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() + +#include "ASpdySession.h" +#include "NSSErrorsService.h" +#include "TLSTransportLayer.h" +#include "mozilla/ChaosMode.h" +#include "mozilla/StaticPrefs_network.h" +#include "mozilla/Telemetry.h" +#include "mozpkix/pkixnss.h" +#include "nsCRT.h" +#include "nsHttpConnection.h" +#include "nsHttpHandler.h" +#include "nsHttpRequestHead.h" +#include "nsHttpResponseHead.h" +#include "nsIClassOfService.h" +#include "nsIOService.h" +#include "nsISocketTransport.h" +#include "nsISupportsPriority.h" +#include "nsITLSSocketControl.h" +#include "nsITransportSecurityInfo.h" +#include "nsPreloadedStream.h" +#include "nsProxyRelease.h" +#include "nsQueryObject.h" +#include "nsSocketTransport2.h" +#include "nsSocketTransportService2.h" +#include "nsStringStream.h" +#include "sslerr.h" +#include "sslt.h" + +namespace mozilla::net { + +enum TlsHandshakeResult : uint32_t { + EchConfigSuccessful = 0, + EchConfigFailed, + NoEchConfigSuccessful, + NoEchConfigFailed, +}; + +//----------------------------------------------------------------------------- +// nsHttpConnection +//----------------------------------------------------------------------------- + +nsHttpConnection::nsHttpConnection() : mHttpHandler(gHttpHandler) { + 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; + } + + auto ReleaseSocketTransport = + [socketTransport(std::move(mSocketTransport))]() mutable { + socketTransport = nullptr; + }; + if (OnSocketThread()) { + ReleaseSocketTransport(); + } else { + gSocketTransportService->Dispatch(NS_NewRunnableFunction( + "nsHttpConnection::~nsHttpConnection", ReleaseSocketTransport)); + } +} + +nsresult nsHttpConnection::Init( + nsHttpConnectionInfo* info, uint16_t maxHangTime, + nsISocketTransport* transport, nsIAsyncInputStream* instream, + nsIAsyncOutputStream* outstream, bool connectedTransport, nsresult status, + nsIInterfaceRequestor* callbacks, PRIntervalTime rtt, bool forWebSocket) { + LOG1(("nsHttpConnection::Init this=%p sockettransport=%p forWebSocket=%d", + this, transport, forWebSocket)); + NS_ENSURE_ARG_POINTER(info); + NS_ENSURE_TRUE(!mConnInfo, NS_ERROR_ALREADY_INITIALIZED); + MOZ_ASSERT(NS_SUCCEEDED(status) || !connectedTransport); + + mConnectedTransport = connectedTransport; + mConnInfo = info; + MOZ_ASSERT(mConnInfo); + + mLastWriteTime = mLastReadTime = PR_IntervalNow(); + mRtt = rtt; + mMaxHangTime = PR_SecondsToInterval(maxHangTime); + + mSocketTransport = transport; + mSocketIn = instream; + mSocketOut = outstream; + mForWebSocket = forWebSocket; + + // See explanation for non-strictness of this operation in + // SetSecurityCallbacks. + mCallbacks = new nsMainThreadPtrHolder( + "nsHttpConnection::mCallbacks", callbacks, false); + + mErrorBeforeConnect = status; + if (NS_SUCCEEDED(mErrorBeforeConnect)) { + mSocketTransport->SetEventSink(this, nullptr); + mSocketTransport->SetSecurityCallbacks(this); + ChangeConnectionState(ConnectionState::INITED); + } else { + SetCloseReason(ToCloseReason(mErrorBeforeConnect)); + } + + mTlsHandshaker = new TlsHandshaker(mConnInfo, this); + return NS_OK; +} + +void nsHttpConnection::ChangeState(HttpConnectionState newState) { + LOG(("nsHttpConnection::ChangeState %d -> %d [this=%p]", mState, newState, + this)); + mState = newState; +} + +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; +} + +void nsHttpConnection::ResetTransaction(RefPtr&& trans) { + MOZ_ASSERT(trans); + mSpdySession->SetConnection(trans->Connection()); + trans->SetConnection(nullptr); + trans->DoNotRemoveAltSvc(); + trans->Close(NS_ERROR_NET_RESET); +} + +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"); + + // If this transaction is used to drive websocket, we reset it to put it in + // the pending queue. Once we know if the server supports websocket or not, + // the pending queue will be processed. + nsHttpTransaction* trans = mTransaction->QueryHttpTransaction(); + if (trans && trans->IsWebsocketUpgrade()) { + LOG(("nsHttpConnection resetting transaction for websocket upgrade")); + // websocket upgrade needs NonSticky for transaction reset + mTransaction->MakeNonSticky(); + ResetTransaction(std::move(mTransaction)); + mTransaction = nullptr; + return NS_OK; + } + + // 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) { + RefPtr transaction = list[index]; + nsHttpTransaction* trans = transaction->QueryHttpTransaction(); + if (trans && trans->IsWebsocketUpgrade()) { + LOG(("nsHttpConnection resetting a transaction for websocket upgrade")); + // websocket upgrade needs NonSticky for transaction reset + transaction->MakeNonSticky(); + ResetTransaction(std::move(transaction)); + transaction = nullptr; + continue; + } + 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; + mEverUsedSpdy = true; + mSpdySession = + ASpdySession::NewSpdySession(spdyVersion, mSocketTransport, true); + + if (mTransaction) { + 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(nsITLSSocketControl* 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; + // See bug 1797729. + // It's possible that we already have a HTTP/3 connection that can be + // coleased with this connection. We should avoid coalescing with the + // existing HTTP/3 connection if the transaction doesn't allow to use + // HTTP/3. + gHttpHandler->ConnMgr()->ReportSpdyConnection(this, true, + mTransactionDisallowHttp3); + } + + // 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 && mTransaction) { + 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)); + SetTunnelSetupDone(); + } + + nsresult rv = NS_OK; + bool spdyProxy = mConnInfo->UsingHttpsProxy() && mConnInfo->UsingConnect() && + !mHasTLSTransportLayer; + 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 && mTransaction) { + if (spdyProxy) { + if (NS_FAILED(status)) { + // proxy upgrade needs Restartable for transaction reset + // note that using NonSticky here won't work because it breaks + // netwerk/test/unit/test_websocket_server.js - h1 ws with h2 proxy + mTransaction->MakeRestartable(); + ResetTransaction(std::move(mTransaction)); + mTransaction = nullptr; + } else { + for (auto trans : list) { + if (!mSpdySession->Connection()) { + mSpdySession->SetConnection(trans->Connection()); + } + trans->SetConnection(nullptr); + trans->DoNotRemoveAltSvc(); + trans->Close(NS_ERROR_NET_RESET); + } + } + } else { + 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; + + mTransaction = mSpdySession; + + if (mDontReuse) { + mSpdySession->DontReuse(); + } +} + +void nsHttpConnection::PostProcessNPNSetup(bool handshakeSucceeded, + bool hasSecurityInfo, + bool earlyDataUsed) { + if (mTransaction) { + 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 && mTransaction->QueryNullTransaction() && + (mBootstrappedTimings.secureConnectionStart.IsNull() || + mBootstrappedTimings.tcpConnectEnd.IsNull())) { + mBootstrappedTimings.secureConnectionStart = + mTransaction->QueryNullTransaction()->GetSecureConnectionStart(); + mBootstrappedTimings.tcpConnectEnd = + mTransaction->QueryNullTransaction()->GetTcpConnectEnd(); + } + + if (hasSecurityInfo) { + mBootstrappedTimings.connectEnd = TimeStamp::Now(); + } + + if (earlyDataUsed) { + // Didn't get 0RTT OK, back out of the "attempting 0RTT" state + LOG(("nsHttpConnection::PostProcessNPNSetup [this=%p] 0rtt failed", this)); + if (mTransaction && NS_FAILED(mTransaction->Finish0RTT(true, true))) { + mTransaction->Close(NS_ERROR_NET_RESET); + } + mContentBytesWritten0RTT = 0; + if (mDid0RTTSpdy) { + Reset0RttForSpdy(); + } + } + + if (hasSecurityInfo) { + // Telemetry for tls failure rate with and without esni; + bool echConfigUsed = false; + mSocketTransport->GetEchConfigUsed(&echConfigUsed); + TlsHandshakeResult result = + echConfigUsed + ? (handshakeSucceeded ? TlsHandshakeResult::EchConfigSuccessful + : TlsHandshakeResult::EchConfigFailed) + : (handshakeSucceeded ? TlsHandshakeResult::NoEchConfigSuccessful + : TlsHandshakeResult::NoEchConfigFailed); + Telemetry::Accumulate(Telemetry::ECHCONFIG_SUCCESS_RATE, result); + } +} + +void nsHttpConnection::Reset0RttForSpdy() { + // Reset the work done by Start0RTTSpdy + 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; +} + +// 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()) { + mHasFirstHttpTransaction = true; + if (mTlsHandshaker->NPNComplete()) { + mExperienced = true; + } + if (mBootstrappedTimingsSet) { + mBootstrappedTimingsSet = false; + nsHttpTransaction* hTrans = trans->QueryHttpTransaction(); + if (hTrans) { + hTrans->BootstrapTimings(mBootstrappedTimings); + SetUrgentStartPreferred(hTrans->GetClassOfService().Flags() & + nsIClassOfService::UrgentStart); + } + } + mBootstrappedTimings = TimingStruct(); + } + + if (caps & NS_HTTP_LARGE_KEEPALIVE) { + mDefaultTimeoutFactor = StaticPrefs::network_http_largeKeepaliveFactor(); + } + + mTransactionCaps = caps; + mPriority = pri; + + if (mHasFirstHttpTransaction && mExperienced) { + mHasFirstHttpTransaction = false; + mExperienceState |= ConnectionExperienceState::Experienced; + } + + if (mTransaction && (mUsingSpdyVersion != SpdyVersion::NONE)) { + return AddTransaction(trans, pri); + } + + NS_ENSURE_ARG_POINTER(trans); + NS_ENSURE_TRUE(!mTransaction, NS_ERROR_IN_PROGRESS); + + // 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 (NS_FAILED(mErrorBeforeConnect)) { + mSocketOutCondition = mErrorBeforeConnect; + mTransaction = trans; + CloseTransaction(mTransaction, mSocketOutCondition); + return mSocketOutCondition; + } + + 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); + mTlsHandshaker->SetupSSL(mInSpdyTunnel, mForcePlainText); + if (mTlsHandshaker->NPNComplete()) { + // For non-HTTPS connection, change the state to TRANSFERING directly. + ChangeConnectionState(ConnectionState::TRANSFERING); + } else { + ChangeConnectionState(ConnectionState::TLS_HANDSHAKING); + } + + // take ownership of the transaction + mTransaction = trans; + + nsCOMPtr tlsSocketControl; + if (NS_SUCCEEDED(mSocketTransport->GetTlsSocketControl( + getter_AddRefs(tlsSocketControl))) && + tlsSocketControl) { + tlsSocketControl->SetBrowserId(mTransaction->BrowserId()); + } + + 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); + + mTransactionDisallowHttp3 |= (caps & NS_HTTP_DISALLOW_HTTP3); + + // need to handle HTTP CONNECT tunnels if this is the first time if + // we are tunneling through a proxy + nsresult rv = CheckTunnelIsNeeded(); + if (NS_FAILED(rv)) goto failed_activation; + + // 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))); + } + + trans->OnActivated(); + + rv = OnOutputStreamReady(mSocketOut); + + if (NS_SUCCEEDED(rv) && mContinueHandshakeDone) { + mContinueHandshakeDone(); + } + mContinueHandshakeDone = nullptr; + +failed_activation: + if (NS_FAILED(rv)) { + mTransaction = nullptr; + } + + 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 && !mHasTLSTransportLayer; + 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 && mHasTLSTransportLayer) { + httpTransaction->OnProxyConnectComplete(200); + } + + LOG(("nsHttpConnection::AddTransaction [this=%p] for %s%s", this, + mSpdySession ? "SPDY" : "QUIC", needTunnel ? " over tunnel" : "")); + + if (mSpdySession) { + if (!mSpdySession->AddStream(httpTransaction, priority, mCallbacks)) { + MOZ_ASSERT(false); // this cannot happen! + httpTransaction->Close(NS_ERROR_ABORT); + return NS_ERROR_FAILURE; + } + } + + Unused << ResumeSend(); + return NS_OK; +} + +nsresult nsHttpConnection::CreateTunnelStream( + nsAHttpTransaction* httpTransaction, nsHttpConnection** aHttpConnection, + bool aIsWebSocket) { + if (!mSpdySession) { + return NS_ERROR_UNEXPECTED; + } + + RefPtr conn = mSpdySession->CreateTunnelStream( + httpTransaction, mCallbacks, mRtt, aIsWebSocket); + // We need to store the refrence of the Http2Session in the tunneled + // connection, so when nsHttpConnection::DontReuse is called the Http2Session + // can't be reused. + if (aIsWebSocket) { + LOG( + ("nsHttpConnection::CreateTunnelStream %p Set h2 session %p to " + "tunneled conn %p", + this, mSpdySession.get(), conn.get())); + conn->mWebSocketHttp2Session = mSpdySession; + } + conn.forget(aHttpConnection); + return NS_OK; +} + +void nsHttpConnection::Close(nsresult reason, bool aIsShutdown) { + LOG(("nsHttpConnection::Close [this=%p reason=%" PRIx32 + " mExperienceState=%x]\n", + this, static_cast(reason), + static_cast(mExperienceState))); + + if (mConnectionState != ConnectionState::CLOSED) { + RecordConnectionCloseTelemetry(reason); + ChangeConnectionState(ConnectionState::CLOSED); + } + + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + mTlsHandshaker->NotifyClose(); + mContinueHandshakeDone = nullptr; + mWebSocketHttp2Session = nullptr; + // 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()); + } + } + + nsCOMPtr tlsSocketControl; + GetTLSSocketControl(getter_AddRefs(tlsSocketControl)); + if (tlsSocketControl) { + tlsSocketControl->SetHandshakeCallbackListener(nullptr); + } + + if (NS_FAILED(reason)) { + if (mIdleMonitoring) EndIdleMonitoring(); + + // 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 (mTlsHandshaker->EarlyDataWasAvailable() && + SecurityErrorThatMayNeedRestart(reason)) { + gHttpHandler->Exclude0RttTcp(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 && !mInSpdyTunnel) { + 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; + } +} + +void nsHttpConnection::MarkAsDontReuse() { + LOG(("nsHttpConnection::MarkAsDontReuse %p\n", this)); + mKeepAliveMask = false; + mKeepAlive = false; + mDontReuse = true; + mIdleTimeout = 0; +} + +void nsHttpConnection::DontReuse() { + LOG(("nsHttpConnection::DontReuse %p spdysession=%p\n", this, + mSpdySession.get())); + MarkAsDontReuse(); + if (mSpdySession) { + mSpdySession->DontReuse(); + } else if (mWebSocketHttp2Session) { + LOG(("nsHttpConnection::DontReuse %p mWebSocketHttp2Session=%p\n", this, + mWebSocketHttp2Session.get())); + mWebSocketHttp2Session->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. + mTlsHandshaker->SetupSSL(mInSpdyTunnel, mForcePlainText); + + 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(); + if (responseStatus == 408) { + // 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 + mKeepAlive = explicitKeepAlive; + } 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 = nsCRT::strcasestr(keepAlive.get(), "timeout="); + if (cp) { + mIdleTimeout = PR_SecondsToInterval((uint32_t)atoi(cp + 8)); + } else { + mIdleTimeout = gHttpHandler->IdleTimeout() * mDefaultTimeoutFactor; + } + + cp = nsCRT::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; + } + + switch (mState) { + case HttpConnectionState::SETTING_UP_TUNNEL: { + nsHttpTransaction* trans = mTransaction->QueryHttpTransaction(); + // Distinguish SETTING_UP_TUNNEL for proxy or websocket via proxy + // See bug 1848013. Do not call HandleTunnelResponse for a tunnel + // connection created for WebSocket. + if (trans && trans->IsWebsocketUpgrade() && + (trans->GetProxyConnectResponseCode() == 200 || + (mForWebSocket && mInSpdyTunnel))) { + HandleWebSocketResponse(requestHead, responseHead, responseStatus); + } else { + HandleTunnelResponse(responseStatus, reset); + } + break; + } + default: + if (requestHead->HasHeader(nsHttp::Upgrade)) { + HandleWebSocketResponse(requestHead, responseHead, responseStatus); + } else if (responseStatus == 101) { + // We got an 101 but we are not asking of a WebSsocket? + Close(NS_ERROR_ABORT); + } + } + + mLastHttpResponseVersion = responseHead->Version(); + + return NS_OK; +} + +void nsHttpConnection::HandleTunnelResponse(uint16_t responseStatus, + bool* reset) { + LOG(("nsHttpConnection::HandleTunnelResponse()")); + MOZ_ASSERT(TunnelSetupInProgress()); + MOZ_ASSERT(mProxyConnectStream); + MOZ_ASSERT(mUsingSpdyVersion == SpdyVersion::NONE, + "SPDY NPN Complete while using proxy connect stream"); + // 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. + + if (responseStatus == 200) { + ChangeState(HttpConnectionState::REQUEST); + } + 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 = mTlsHandshaker->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. + mTlsHandshaker->SetNPNComplete(); + } + } + 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(); + } +} + +void nsHttpConnection::HandleWebSocketResponse(nsHttpRequestHead* requestHead, + nsHttpResponseHead* responseHead, + uint16_t responseStatus) { + LOG(("nsHttpConnection::HandleWebSocketResponse()")); + + // 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 (responseStatus != 401 && responseStatus != 407 && !mSpdySession) { + LOG(("HTTP Upgrade in play - disable keepalive for http/1.x\n")); + MarkAsDontReuse(); + } + + // the new Http2StreamWebSocket breaks wpt on + // h2 basic authentication 401, due to MakeSticky() work around + // so we DontReuse() in this circumstance + if (mInSpdyTunnel && (responseStatus == 401 || responseStatus == 407)) { + MarkAsDontReuse(); + return; + } + + if (responseStatus == 101) { + nsAutoCString upgradeReq; + bool hasUpgradeReq = + NS_SUCCEEDED(requestHead->GetHeader(nsHttp::Upgrade, upgradeReq)); + 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())); + } + } +} + +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))); + } + } + + if (mHasTLSTransportLayer) { + RefPtr tlsTransportLayer = + do_QueryObject(mSocketTransport); + if (tlsTransportLayer) { + // This transport layer is no longer owned by this connection. + tlsTransportLayer->ReleaseOwner(); + } + } + + mSocketTransport->SetSecurityCallbacks(nullptr); + mSocketTransport->SetEventSink(nullptr, 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; + SetCloseReason(ConnectionCloseReason::IDLE_TIMEOUT); + // 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); + } + + if (!mTlsHandshaker->NPNComplete()) { + // 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 + SetCloseReason(ConnectionCloseReason::TLS_TIMEOUT); + 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::GetTLSSocketControl( + nsITLSSocketControl** tlsSocketControl) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG(("nsHttpConnection::GetTLSSocketControl trans=%p socket=%p\n", + mTransaction.get(), mSocketTransport.get())); + + *tlsSocketControl = nullptr; + + if (mTransaction && NS_SUCCEEDED(mTransaction->GetTransactionTLSSocketControl( + tlsSocketControl))) { + return; + } + + if (mSocketTransport && + NS_SUCCEEDED(mSocketTransport->GetTlsSocketControl(tlsSocketControl))) { + return; + } +} + +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) + : Runnable("net::HttpConnectionForceIO"), 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 nsHttpConnection::ResumeSend() { + LOG(("nsHttpConnection::ResumeSend [this=%p]\n", this)); + + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + if (mSocketOut) { + return mSocketOut->AsyncWait(this, 0, 0, nullptr); + } + + 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"); + + // 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 (mHasTLSTransportLayer) { + RefPtr tlsTransportLayer = + do_QueryObject(mSocketTransport); + if (tlsTransportLayer) { + bool hasDataToRecv = tlsTransportLayer->HasDataToRecv(); + if (hasDataToRecv && NS_SUCCEEDED(ForceRecv())) { + return NS_OK; + } + Unused << mSocketIn->AsyncWait(this, 0, 0, nullptr); + // We have to return an error here to let the underlying layer know this + // connection doesn't read any data. + return NS_BASE_STREAM_WOULD_BLOCK; + } + } + return mSocketIn->AsyncWait(this, 0, 0, nullptr); + } + + 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)); +} + +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)); +} + +// trigger an asynchronous write +nsresult nsHttpConnection::ForceSend() { + LOG(("nsHttpConnection::ForceSend [this=%p]\n", this)); + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + 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; +} + +PRIntervalTime nsHttpConnection::LastWriteTime() { return mLastWriteTime; } + +//----------------------------------------------------------------------------- +// 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(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) { + 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; +} + +bool nsHttpConnection::CheckCanWrite0RTTData() { + MOZ_ASSERT(mTlsHandshaker->EarlyDataAvailable()); + nsCOMPtr tlsSocketControl; + GetTLSSocketControl(getter_AddRefs(tlsSocketControl)); + if (!tlsSocketControl) { + return false; + } + nsCOMPtr securityInfo; + if (NS_FAILED( + tlsSocketControl->GetSecurityInfo(getter_AddRefs(securityInfo)))) { + return false; + } + if (!securityInfo) { + return false; + } + nsAutoCString negotiatedNPN; + // If the following code fails means that the handshake is not done + // yet, so continue writing 0RTT data. + nsresult rv = securityInfo->GetNegotiatedNPN(negotiatedNPN); + if (NS_FAILED(rv)) { + return true; + } + bool earlyDataAccepted = false; + rv = tlsSocketControl->GetEarlyDataAccepted(&earlyDataAccepted); + // If 0RTT data is accepted we can continue writing data, + // if it is reject stop writing more data. + return NS_SUCCEEDED(rv) && earlyDataAccepted; +} + +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 + } + + // If we are waiting for 0RTT Response, check maybe nss has finished + // handshake already. + // IsAlive() calls drive the handshake and that may cause nss and necko + // to be out of sync. + if (mTlsHandshaker->EarlyDataAvailable() && !CheckCanWrite0RTTData()) { + MOZ_DIAGNOSTIC_ASSERT(mTlsHandshaker->TlsHandshakeComplitionPending()); + LOG( + ("nsHttpConnection::OnReadSegment Do not write any data, wait" + " for EnsureNPNComplete to be called [this=%p]", + this)); + *countRead = 0; + return NS_BASE_STREAM_WOULD_BLOCK; + } + + 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 (!TunnelSetupInProgress()) { + mTotalBytesWritten += *countRead; + mExperienceState |= ConnectionExperienceState::First_Request_Sent; + } + } + + 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; + + if (mTransactionCaps & NS_HTTP_CONNECT_ONLY) { + if (!mConnInfo->UsingConnect()) { + // 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 (mState == HttpConnectionState::REQUEST) { + // 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; + + switch (mState) { + case HttpConnectionState::SETTING_UP_TUNNEL: + if (mConnInfo->UsingHttpsProxy() && + !mTlsHandshaker->EnsureNPNComplete()) { + MOZ_DIAGNOSTIC_ASSERT(!mTlsHandshaker->EarlyDataAvailable()); + mSocketOutCondition = NS_BASE_STREAM_WOULD_BLOCK; + } else { + rv = SendConnectRequest(this, &transactionBytes); + } + break; + default: { + // 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 (!mTlsHandshaker->EnsureNPNComplete() && + (!mTlsHandshaker->EarlyDataUsed() || + mTlsHandshaker->TlsHandshakeComplitionPending())) { + // The handshake is not done and we cannot write 0RTT data or nss has + // already finished 0RTT data. + 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 && mTlsHandshaker->NPNComplete()) { + mReportedSpdy = true; + MOZ_ASSERT(!mEverUsedSpdy); + gHttpHandler->ConnMgr()->ReportSpdyConnection(this, false, false); + } + + LOG((" writing transaction request stream\n")); + rv = mTransaction->ReadSegmentsAgain(this, + nsIOService::gDefaultSegmentSize, + &transactionBytes, &again); + if (mTlsHandshaker->EarlyDataUsed()) { + mContentBytesWritten0RTT += transactionBytes; + if (NS_FAILED(rv) && rv != NS_BASE_STREAM_WOULD_BLOCK) { + // If an error happens while writting 0RTT data, restart + // the transactiions without 0RTT. + mTlsHandshaker->FinishNPNSetup(false, true); + } + } else { + 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 (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) { + if (!mTlsHandshaker->EarlyDataCanNotBeUsed()) { + // continue writing + // We are not going to poll for write if the handshake is in progress, + // but early data cannot be used. + rv = mSocketOut->AsyncWait(this, 0, 0, nullptr); + } + } else { + rv = mSocketOutCondition; + } + again = false; + } else if (!transactionBytes) { + rv = NS_OK; + + 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); + + rv = ResumeRecv(); // start reading + } + // When Spdy tunnel is used we need to explicitly set when a request is + // done. + if ((mState != HttpConnectionState::SETTING_UP_TUNNEL) && !mSpdySession) { + nsHttpTransaction* trans = mTransaction->QueryHttpTransaction(); + // needed for websocket over h2 (direct) + if (!trans || !trans->IsWebsocketUpgrade()) { + mRequestDone = true; + } + } + 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 + mExperienceState |= ConnectionExperienceState::First_Response_Received; + } + + 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) && !mConnInfo->UsingConnect()) { + // 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 = NS_OK; + uint32_t n; + bool again = true; + + do { + if (!TunnelSetupInProgress() && !mTlsHandshaker->EnsureNPNComplete()) { + // 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)); + if (mTlsHandshaker->EarlyDataAvailable() || mHasTLSTransportLayer) { + rv = ResumeRecv(); + } + break; + } + + mSocketInCondition = NS_OK; + if (!mTransaction) { + rv = NS_ERROR_FAILURE; + LOG((" No Transaction In OnSocketWritable\n")); + } else { + 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() { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + MOZ_ASSERT(!mHasTLSTransportLayer); + LOG(("nsHttpConnection %p SetupSecondaryTLS %s %d\n", this, + mConnInfo->Origin(), mConnInfo->OriginPort())); + + nsHttpConnectionInfo* ci = nullptr; + if (mTransaction) { + ci = mTransaction->ConnectionInfo(); + } + if (!ci) { + ci = mConnInfo; + } + MOZ_ASSERT(ci); + + RefPtr transportLayer = + new TLSTransportLayer(mSocketTransport, mSocketIn, mSocketOut, this); + if (transportLayer->Init(ci->Origin(), ci->OriginPort())) { + mSocketIn = transportLayer->GetInputStreamWrapper(); + mSocketOut = transportLayer->GetOutputStreamWrapper(); + mSocketTransport = transportLayer; + mHasTLSTransportLayer = true; + LOG(("Create mTLSTransportLayer %p", this)); + } +} + +void nsHttpConnection::SetInSpdyTunnel() { + mInSpdyTunnel = true; + mForcePlainText = true; +} + +// static +nsresult nsHttpConnection::MakeConnectString(nsAHttpTransaction* trans, + nsHttpRequestHead* request, + nsACString& result, bool h2ws, + bool aShouldResistFingerprinting) { + 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()); + + nsAutoCString val; + if (NS_SUCCEEDED(trans->RequestHead()->GetHeader( + nsHttp::Sec_WebSocket_Extensions, val))) { + rv = request->SetHeader(nsHttp::Sec_WebSocket_Extensions, val); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + if (NS_SUCCEEDED(trans->RequestHead()->GetHeader( + nsHttp::Sec_WebSocket_Protocol, val))) { + rv = request->SetHeader(nsHttp::Sec_WebSocket_Protocol, val); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + if (NS_SUCCEEDED(trans->RequestHead()->GetHeader( + nsHttp::Sec_WebSocket_Version, val))) { + rv = request->SetHeader(nsHttp::Sec_WebSocket_Version, val); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + } else { + request->SetRequestURI(result); + } + rv = request->SetHeader(nsHttp::User_Agent, + gHttpHandler->UserAgent(aShouldResistFingerprinting)); + 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 h2ws=%d[", + trans->QueryHttpTransaction(), h2ws)); + LogHeaders(result.BeginReading()); + LOG(("]")); + } + + result.AppendLiteral("\r\n"); + return NS_OK; +} + +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 (rv == NS_BASE_STREAM_WOULD_BLOCK) { + return rv; + } + + 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 (rv == NS_BASE_STREAM_WOULD_BLOCK) { + return NS_OK; + } + + 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; + } +} + +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; + 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 tlsSocketControl; + mSocketTransport->GetTlsSocketControl(getter_AddRefs(tlsSocketControl)); + if (!tlsSocketControl) { + return false; + } + + return !tlsSocketControl->GetClientCertSent(); +} + +WebSocketSupport nsHttpConnection::GetWebSocketSupport() { + LOG3(("nsHttpConnection::GetWebSocketSupport")); + if (!UsingSpdy()) { + return WebSocketSupport::SUPPORTED; + } + LOG3(("nsHttpConnection::GetWebSocketSupport checking spdy session")); + if (mSpdySession) { + return mSpdySession->GetWebSocketSupport(); + } + + return WebSocketSupport::NO_SUPPORT; +} + +bool nsHttpConnection::IsProxyConnectInProgress() { + return mState == SETTING_UP_TUNNEL; +} + +bool nsHttpConnection::LastTransactionExpectedNoContent() { + return mLastTransactionExpectedNoContent; +} + +void nsHttpConnection::SetLastTransactionExpectedNoContent(bool val) { + mLastTransactionExpectedNoContent = val; +} + +bool nsHttpConnection::IsPersistent() { return IsKeepAlive() && !mDontReuse; } + +nsAHttpTransaction* nsHttpConnection::Transaction() { return mTransaction; } + +nsresult nsHttpConnection::GetSelfAddr(NetAddr* addr) { + if (!mSocketTransport) { + return NS_ERROR_FAILURE; + } + return mSocketTransport->GetSelfAddr(addr); +} + +nsresult nsHttpConnection::GetPeerAddr(NetAddr* addr) { + if (!mSocketTransport) { + return NS_ERROR_FAILURE; + } + return mSocketTransport->GetPeerAddr(addr); +} + +bool nsHttpConnection::ResolvedByTRR() { + bool val = false; + if (mSocketTransport) { + mSocketTransport->ResolvedByTRR(&val); + } + return val; +} + +nsIRequest::TRRMode nsHttpConnection::EffectiveTRRMode() { + nsIRequest::TRRMode mode = nsIRequest::TRR_DEFAULT_MODE; + if (mSocketTransport) { + mSocketTransport->GetEffectiveTRRMode(&mode); + } + return mode; +} + +TRRSkippedReason nsHttpConnection::TRRSkipReason() { + TRRSkippedReason reason = nsITRRSkipReason::TRR_UNSET; + if (mSocketTransport) { + mSocketTransport->GetTrrSkipReason(&reason); + } + return reason; +} + +bool nsHttpConnection::GetEchConfigUsed() { + bool val = false; + if (mSocketTransport) { + mSocketTransport->GetEchConfigUsed(&val); + } + return val; +} + +void nsHttpConnection::HandshakeDoneInternal() { + LOG(("nsHttpConnection::HandshakeDoneInternal [this=%p]\n", this)); + if (mTlsHandshaker->NPNComplete()) { + return; + } + + ChangeConnectionState(ConnectionState::TRANSFERING); + + nsCOMPtr tlsSocketControl; + GetTLSSocketControl(getter_AddRefs(tlsSocketControl)); + if (!tlsSocketControl) { + mTlsHandshaker->FinishNPNSetup(false, false); + return; + } + + nsCOMPtr securityInfo; + if (NS_FAILED( + tlsSocketControl->GetSecurityInfo(getter_AddRefs(securityInfo)))) { + mTlsHandshaker->FinishNPNSetup(false, false); + return; + } + if (!securityInfo) { + mTlsHandshaker->FinishNPNSetup(false, false); + return; + } + + nsAutoCString negotiatedNPN; + DebugOnly rvDebug = securityInfo->GetNegotiatedNPN(negotiatedNPN); + MOZ_ASSERT(NS_SUCCEEDED(rvDebug)); + + bool earlyDataAccepted = false; + if (mTlsHandshaker->EarlyDataUsed()) { + // Check if early data has been accepted. + nsresult rvEarlyData = + tlsSocketControl->GetEarlyDataAccepted(&earlyDataAccepted); + LOG( + ("nsHttpConnection::HandshakeDone [this=%p] - early data " + "that was sent during 0RTT %s been accepted [rv=%" PRIx32 "].", + this, earlyDataAccepted ? "has" : "has not", + static_cast(rvEarlyData))); + + if (NS_FAILED(rvEarlyData) || + (mTransaction && + NS_FAILED(mTransaction->Finish0RTT( + !earlyDataAccepted, + negotiatedNPN != mTlsHandshaker->EarlyNegotiatedALPN())))) { + LOG( + ("nsHttpConection::HandshakeDone [this=%p] closing transaction " + "%p", + this, mTransaction.get())); + if (mTransaction) { + mTransaction->Close(NS_ERROR_NET_RESET); + } + mTlsHandshaker->FinishNPNSetup(false, true); + return; + } + if (mDid0RTTSpdy && + (negotiatedNPN != mTlsHandshaker->EarlyNegotiatedALPN())) { + Reset0RttForSpdy(); + } + } + + if (mTlsHandshaker->EarlyDataAvailable() && !earlyDataAccepted) { + // When the early-data were used but not accepted, we need to start + // from the begining here and start writing the request again. + // The same is true if 0RTT was available but not used. + if (mSocketIn) { + mSocketIn->AsyncWait(nullptr, 0, 0, nullptr); + } + Unused << ResumeSend(); + } + + int16_t tlsVersion; + tlsSocketControl->GetSSLVersionUsed(&tlsVersion); + mConnInfo->SetLessThanTls13( + (tlsVersion < nsITLSSocketControl::TLS_VERSION_1_3) && + (tlsVersion != nsITLSSocketControl::SSL_VERSION_UNKNOWN)); + + mTlsHandshaker->EarlyDataTelemetry(tlsVersion, earlyDataAccepted, + mContentBytesWritten0RTT); + mTlsHandshaker->EarlyDataDone(); + + if (!earlyDataAccepted) { + LOG( + ("nsHttpConnection::HandshakeDone [this=%p] early data not " + "accepted or early data were not used", + this)); + + const SpdyInformation* info = gHttpHandler->SpdyInfo(); + if (negotiatedNPN.Equals(info->VersionString)) { + if (mTransaction) { + StartSpdy(tlsSocketControl, info->Version); + } else { + LOG( + ("nsHttpConnection::HandshakeDone [this=%p] set " + "mContinueHandshakeDone", + this)); + RefPtr self = this; + mContinueHandshakeDone = [self = RefPtr{this}, + tlsSocketControl(tlsSocketControl), + info(info->Version)]() { + LOG(("nsHttpConnection do mContinueHandshakeDone [this=%p]", + self.get())); + self->StartSpdy(tlsSocketControl, info); + self->mTlsHandshaker->FinishNPNSetup(true, true); + }; + return; + } + } + } else { + LOG(("nsHttpConnection::HandshakeDone [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::HandshakeDone [this=%p] - finishing " + "StartSpdy for 0rtt spdy session %p", + this, mSpdySession.get())); + StartSpdy(tlsSocketControl, mSpdySession->SpdyVersion()); + } + } + + Telemetry::Accumulate(Telemetry::SPDY_NPN_CONNECT, UsingSpdy()); + + mTlsHandshaker->FinishNPNSetup(true, true); + Unused << ResumeSend(); +} + +void nsHttpConnection::SetTunnelSetupDone() { + MOZ_ASSERT(mProxyConnectStream); + MOZ_ASSERT(mState == HttpConnectionState::SETTING_UP_TUNNEL); + + ChangeState(HttpConnectionState::REQUEST); + mProxyConnectStream = nullptr; +} + +nsresult nsHttpConnection::CheckTunnelIsNeeded() { + switch (mState) { + case HttpConnectionState::UNINITIALIZED: { + // This is is called first time. Check if we need a tunnel. + if (!mTransaction->ConnectionInfo()->UsingConnect()) { + ChangeState(HttpConnectionState::REQUEST); + return NS_OK; + } + ChangeState(HttpConnectionState::SETTING_UP_TUNNEL); + } + [[fallthrough]]; + case HttpConnectionState::SETTING_UP_TUNNEL: { + // When a nsHttpConnection is in this state that means that an + // authentication was needed and we are resending a CONNECT + // request. This request will include authentication headers. + nsresult rv = SetupProxyConnectStream(); + if (NS_FAILED(rv)) { + ChangeState(HttpConnectionState::UNINITIALIZED); + } + return rv; + } + case HttpConnectionState::REQUEST: + return NS_OK; + } + return NS_OK; +} + +nsresult nsHttpConnection::SetupProxyConnectStream() { + LOG(("nsHttpConnection::SetupStream\n")); + NS_ENSURE_TRUE(!mProxyConnectStream, NS_ERROR_ALREADY_INITIALIZED); + MOZ_ASSERT(mState == HttpConnectionState::SETTING_UP_TUNNEL); + + nsAutoCString buf; + nsHttpRequestHead request; + nsresult rv = MakeConnectString(mTransaction, &request, buf, + mForWebSocket && mInSpdyTunnel, + mTransactionCaps & NS_HTTP_USE_RFP); + if (NS_FAILED(rv)) { + return rv; + } + rv = NS_NewCStringInputStream(getter_AddRefs(mProxyConnectStream), + std::move(buf)); + return rv; +} + +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::SendConnectRequest(void* closure, + uint32_t* transactionBytes) { + LOG((" writing CONNECT request stream\n")); + return mProxyConnectStream->ReadSegments(ReadFromStream, closure, + nsIOService::gDefaultSegmentSize, + transactionBytes); +} + +} // namespace mozilla::net diff --git a/netwerk/protocol/http/nsHttpConnection.h b/netwerk/protocol/http/nsHttpConnection.h new file mode 100644 index 0000000000..6f00591a2e --- /dev/null +++ b/netwerk/protocol/http/nsHttpConnection.h @@ -0,0 +1,388 @@ +/* -*- 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 +#include "HttpConnectionBase.h" +#include "nsHttpConnectionInfo.h" +#include "nsHttpResponseHead.h" +#include "nsAHttpTransaction.h" +#include "nsCOMPtr.h" +#include "nsProxyRelease.h" +#include "prinrval.h" +#include "mozilla/Mutex.h" +#include "ARefBase.h" +#include "TimingStruct.h" +#include "HttpTrafficAnalyzer.h" +#include "TlsHandshaker.h" + +#include "nsIAsyncInputStream.h" +#include "nsIAsyncOutputStream.h" +#include "nsIInterfaceRequestor.h" +#include "nsISocketTransport.h" +#include "nsISupportsPriority.h" +#include "nsITimer.h" +#include "nsITlsHandshakeListener.h" + +class nsISocketTransport; +class nsITLSSocketControl; + +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 { + 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 + + nsHttpConnection(); + + // 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, nsresult status, + nsIInterfaceRequestor*, PRIntervalTime, + bool forWebSocket); + + //------------------------------------------------------------------------- + // 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() && !mHasTLSTransportLayer && + 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; + friend class TlsHandshaker; + + // 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; } + + void SetupSecondaryTLS(); + void SetInSpdyTunnel(); + + // 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)); + } + + // 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; + + WebSocketSupport GetWebSocketSupport() override; + + int64_t BytesWritten() override { return mTotalBytesWritten; } + + nsISocketTransport* Transport() override { return mSocketTransport; } + + nsresult GetSelfAddr(NetAddr* addr) override; + nsresult GetPeerAddr(NetAddr* addr) override; + bool ResolvedByTRR() override; + bool GetEchConfigUsed() override; + nsIRequest::TRRMode EffectiveTRRMode() override; + TRRSkippedReason TRRSkipReason() override; + bool IsForWebSocket() { return mForWebSocket; } + + // The following functions are related to setting up a tunnel. + [[nodiscard]] static nsresult MakeConnectString( + nsAHttpTransaction* trans, nsHttpRequestHead* request, nsACString& result, + bool h2ws, bool aShouldResistFingerprinting); + [[nodiscard]] static nsresult ReadFromStream(nsIInputStream*, void*, + const char*, uint32_t, uint32_t, + uint32_t*); + + nsresult CreateTunnelStream(nsAHttpTransaction* httpTransaction, + nsHttpConnection** aHttpConnection, + bool aIsWebSocket = false); + + bool RequestDone() { return mRequestDone; } + + private: + enum HttpConnectionState { + UNINITIALIZED, + SETTING_UP_TUNNEL, + REQUEST, + } mState{HttpConnectionState::UNINITIALIZED}; + void ChangeState(HttpConnectionState newState); + + // Tunnel retated functions: + bool TunnelSetupInProgress() { return mState == SETTING_UP_TUNNEL; } + void SetTunnelSetupDone(); + nsresult CheckTunnelIsNeeded(); + nsresult SetupProxyConnectStream(); + nsresult SendConnectRequest(void* closure, uint32_t* transactionBytes); + + void HandleTunnelResponse(uint16_t responseStatus, bool* reset); + void HandleWebSocketResponse(nsHttpRequestHead* requestHead, + nsHttpResponseHead* responseHead, + uint16_t responseStatus); + void ResetTransaction(RefPtr&& trans); + + // Value (set in mTCPKeepaliveConfig) indicates which set of prefs to use. + enum TCPKeepaliveConfig { + kTCPKeepaliveDisabled = 0, + kTCPKeepaliveShortLivedConfig, + kTCPKeepaliveLongLivedConfig + }; + + [[nodiscard]] nsresult OnTransactionDone(nsresult reason); + [[nodiscard]] nsresult OnSocketWritable(); + [[nodiscard]] nsresult OnSocketReadable(); + + PRIntervalTime IdleTime(); + bool IsAlive(); + + // Start the Spdy transaction handler when NPN indicates spdy/* + void StartSpdy(nsITLSSocketControl* ssl, SpdyVersion spdyVersion); + // Like the above, but do the bare minimum to do 0RTT data, so we can back + // it out, if necessary + void Start0RTTSpdy(SpdyVersion spdyVersion); + + // 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(); + + bool CheckCanWrite0RTTData(); + void PostProcessNPNSetup(bool handshakeSucceeded, bool hasSecurityInfo, + bool earlyDataUsed); + void Reset0RttForSpdy(); + void HandshakeDoneInternal(); + uint32_t TransactionCaps() const { return mTransactionCaps; } + + void MarkAsDontReuse(); + + private: + // mTransaction only points to the HTTP Transaction callbacks if the + // transaction is open, otherwise it is null. + RefPtr mTransaction; + + RefPtr mTlsHandshaker; + + nsCOMPtr mSocketIn; + nsCOMPtr mSocketOut; + + nsresult mSocketInCondition{NS_ERROR_NOT_INITIALIZED}; + nsresult mSocketOutCondition{NS_ERROR_NOT_INITIALIZED}; + + RefPtr mHttpHandler; // keep gHttpHandler alive + + PRIntervalTime mLastReadTime{0}; + PRIntervalTime mLastWriteTime{0}; + // max download time before dropping keep-alive status + PRIntervalTime mMaxHangTime{0}; + PRIntervalTime mIdleTimeout; // value of keep-alive: timeout= + PRIntervalTime mConsiderReusedAfterInterval{0}; + PRIntervalTime mConsiderReusedAfterEpoch{0}; + int64_t mCurrentBytesRead{0}; // data read per activation + int64_t mMaxBytesRead{0}; // max read in 1 activation + int64_t mTotalBytesRead{0}; // total data read + int64_t mContentBytesWritten{0}; // 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{false}; + // A flag to prevent reset of mUrgentStartPreferred by subsequent transactions + bool mUrgentStartPreferredKnown{false}; + bool mConnectedTransport{false}; + // assume to keep-alive by default + bool mKeepAlive{true}; + bool mKeepAliveMask{true}; + bool mDontReuse{false}; + bool mIsReused{false}; + bool mLastTransactionExpectedNoContent{false}; + bool mIdleMonitoring{false}; + bool mInSpdyTunnel{false}; + bool mForcePlainText{false}; + + // A snapshot of current number of transfered bytes + int64_t mTrafficCount{0}; + bool mTrafficStamp{false}; // true then the above is set + + // The number of <= HTTP/1.1 transactions performed on this connection. This + // excludes spdy transactions. + uint32_t mHttp1xTransactionCount{0}; + + // 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{0xffffffff}; + + // version level in use, 0 if unused + SpdyVersion mUsingSpdyVersion{SpdyVersion::NONE}; + + RefPtr mSpdySession; + RefPtr mWebSocketHttp2Session; + int32_t mPriority{nsISupportsPriority::PRIORITY_NORMAL}; + bool mReportedSpdy{false}; + + // mUsingSpdyVersion is cleared when mSpdySession is freed, this is permanent + bool mEverUsedSpdy{false}; + + // mLastHttpResponseVersion stores the last response's http version seen. + HttpVersion mLastHttpResponseVersion{HttpVersion::v1_1}; + + // If a large keepalive has been requested for any trans, + // scale the default by this factor + uint32_t mDefaultTimeoutFactor{1}; + + bool mResponseTimeoutEnabled{false}; + + // Flag to indicate connection is in inital keepalive period (fast detect). + uint32_t mTCPKeepaliveConfig{kTCPKeepaliveDisabled}; + nsCOMPtr mTCPKeepaliveTransitionTimer; + + private: + // For ForceSend() + static void ForceSendIO(nsITimer* aTimer, void* aClosure); + [[nodiscard]] nsresult MaybeForceSendIO(); + bool mForceSendPending{false}; + nsCOMPtr mForceSendTimer; + + int64_t mContentBytesWritten0RTT{0}; + bool mDid0RTTSpdy{false}; + + nsresult mErrorBeforeConnect = NS_OK; + + nsCOMPtr mSocketTransport; + + // This flag indicates if the connection is used for WebSocket. + // - When true and mInSpdyTunnel is also true: WebSocket over HTTP/2. + // - When true and mInSpdyTunnel is false: WebSocket over HTTP/1.1. + bool mForWebSocket{false}; + + std::function mContinueHandshakeDone{nullptr}; + + private: + bool mThroughCaptivePortal; + int64_t mTotalBytesWritten = 0; // does not include CONNECT tunnel + + nsCOMPtr mProxyConnectStream; + + bool mRequestDone{false}; + bool mHasTLSTransportLayer{false}; + bool mTransactionDisallowHttp3{false}; +}; + +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..e91128d85a --- /dev/null +++ b/netwerk/protocol/http/nsHttpConnectionInfo.cpp @@ -0,0 +1,570 @@ +/* -*- 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, + nsProxyInfo* proxyInfo, const OriginAttributes& originAttributes, + bool endToEndSSL, bool aIsHttp3, bool aWebTransport) + : mRoutedPort(443), mLessThanTls13(false) { + Init(originHost, originPort, npnToken, username, proxyInfo, originAttributes, + endToEndSSL, aIsHttp3, aWebTransport); +} + +nsHttpConnectionInfo::nsHttpConnectionInfo( + const nsACString& originHost, int32_t originPort, + const nsACString& npnToken, const nsACString& username, + nsProxyInfo* proxyInfo, const OriginAttributes& originAttributes, + const nsACString& routedHost, int32_t routedPort, bool aIsHttp3, + bool aWebTransport) + : mLessThanTls13(false) { + mEndToEndSSL = true; // so DefaultPort() works + mRoutedPort = routedPort == -1 ? DefaultPort() : routedPort; + + if (!originHost.Equals(routedHost) || (originPort != routedPort) || + aIsHttp3) { + mRoutedHost = routedHost; + } + Init(originHost, originPort, npnToken, username, proxyInfo, originAttributes, + true, aIsHttp3, aWebTransport); +} + +void nsHttpConnectionInfo::Init(const nsACString& host, int32_t port, + const nsACString& npnToken, + const nsACString& username, + nsProxyInfo* proxyInfo, + const OriginAttributes& originAttributes, + bool e2eSSL, bool aIsHttp3, + bool aWebTransport) { + LOG(("Init nsHttpConnectionInfo @%p\n", this)); + + MOZ_RELEASE_ASSERT(!aWebTransport || aIsHttp3); + + mUsername = username; + mProxyInfo = proxyInfo; + mEndToEndSSL = e2eSSL; + mUsingConnect = false; + mNPNToken = npnToken; + mIsHttp3 = aIsHttp3; + mWebTransport = aWebTransport; + 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 9 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 B/. B is for allowing client certs on an anonymous channel + // byte 8 is F/. F is for indicating a fallback connection + // byte 9 is W/. W is for indicating a webTransport + // 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. + + const auto keyTemplate = + std::string(UnderlyingIndex(HashKeyIndex::End), '.') + + std::string("[tlsflags0x00000000]"); + mHashKey.Assign(keyTemplate.c_str()); + + mHashKey.Append(keyHost); + mHashKey.Append(':'); + mHashKey.AppendInt(keyPort); + if (!mUsername.IsEmpty()) { + mHashKey.Append('['); + mHashKey.Append(mUsername); + mHashKey.Append(']'); + } + + if (mUsingHttpsProxy) { + SetHashCharAt('T', HashKeyIndex::Proxy); + } else if (mUsingHttpProxy) { + SetHashCharAt('P', HashKeyIndex::Proxy); + } + if (mEndToEndSSL) { + SetHashCharAt('S', HashKeyIndex::EndToEndSSL); + } + + if (mWebTransport) { + SetHashCharAt('W', HashKeyIndex::WebTransport); + } + + // 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 (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(); + bool isAnonymousAllowClientCert = GetAnonymousAllowClientCert(); + bool isFallback = GetFallbackConnection(); + + BuildHashKey(); + + // Restore all of those properties. + SetAnonymous(isAnonymous); + SetPrivate(isPrivate); + SetInsecureScheme(isInsecureScheme); + SetNoSpdy(isNoSpdy); + SetBeConservative(isBeConservative); + SetAnonymousAllowClientCert(isAnonymousAllowClientCert); + SetFallbackConnection(isFallback); +} + +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, + mProxyInfo, mOriginAttributes, + mEndToEndSSL, mIsHttp3, mWebTransport); + } else { + MOZ_ASSERT(mEndToEndSSL); + clone = new nsHttpConnectionInfo(mOrigin, mOriginPort, mNPNToken, mUsername, + mProxyInfo, mOriginAttributes, mRoutedHost, + mRoutedPort, mIsHttp3, mWebTransport); + } + + // 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->SetAnonymousAllowClientCert(GetAnonymousAllowClientCert()); + clone->SetFallbackConnection(GetFallbackConnection()); + 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 ? mozilla::net::IsHttp3(std::get<1>(*alpn)) : false; + + LOG(("HTTPSSVC: use new routed host (%s) and new npnToken (%s)", name.get(), + alpn ? std::get<0>(*alpn).get() : "None")); + + RefPtr clone; + if (name.IsEmpty()) { + clone = new nsHttpConnectionInfo( + mOrigin, mOriginPort, alpn ? std::get<0>(*alpn) : EmptyCString(), + mUsername, mProxyInfo, mOriginAttributes, mEndToEndSSL, isHttp3); + } else { + MOZ_ASSERT(mEndToEndSSL); + clone = new nsHttpConnectionInfo( + mOrigin, mOriginPort, alpn ? std::get<0>(*alpn) : EmptyCString(), + mUsername, mProxyInfo, mOriginAttributes, name, + port ? *port : mOriginPort, isHttp3, mWebTransport); + } + + // 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->SetAnonymousAllowClientCert(GetAnonymousAllowClientCert()); + clone->SetFallbackConnection(GetFallbackConnection()); + 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.anonymousAllowClientCert() = aInfo->GetAnonymousAllowClientCert(); + aArgs.tlsFlags() = aInfo->GetTlsFlags(); + aArgs.isTrrServiceChannel() = aInfo->GetTRRMode(); + aArgs.trrMode() = aInfo->GetTRRMode(); + aArgs.isIPv4Disabled() = aInfo->GetIPv4Disabled(); + aArgs.isIPv6Disabled() = aInfo->GetIPv6Disabled(); + aArgs.isHttp3() = aInfo->IsHttp3(); + aArgs.hasIPHintAddress() = aInfo->HasIPHintAddress(); + aArgs.echConfig() = aInfo->GetEchConfig(); + aArgs.webTransport() = aInfo->GetWebTransport(); + + 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(), pi, aInfoArgs.originAttributes(), + aInfoArgs.endToEndSSL(), aInfoArgs.isHttp3(), aInfoArgs.webTransport()); + } else { + MOZ_ASSERT(aInfoArgs.endToEndSSL()); + cinfo = new nsHttpConnectionInfo( + aInfoArgs.host(), aInfoArgs.port(), aInfoArgs.npnToken(), + aInfoArgs.username(), pi, aInfoArgs.originAttributes(), + aInfoArgs.routedHost(), aInfoArgs.routedPort(), aInfoArgs.isHttp3(), + aInfoArgs.webTransport()); + } + + // 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->SetAnonymousAllowClientCert(aInfoArgs.anonymousAllowClientCert()); + cinfo->SetFallbackConnection(aInfoArgs.fallbackConnection()); + 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) { + // Explicitly use an empty npnToken when |mIsHttp3| is true, since we want to + // create a non-http3 connection info. + RefPtr clone = new nsHttpConnectionInfo( + mOrigin, mOriginPort, + (mRoutedHost.IsEmpty() && !mIsHttp3) ? mNPNToken : ""_ns, mUsername, + mProxyInfo, mOriginAttributes, mEndToEndSSL, false, mWebTransport); + // 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->SetAnonymousAllowClientCert(GetAnonymousAllowClientCert()); + clone->SetFallbackConnection(GetFallbackConnection()); + 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, mProxyInfo, + mOriginAttributes, true, mIsHttp3, + mWebTransport); + // 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::SetWebTransport(bool aWebTransport) { + if (mWebTransport != aWebTransport) { + mWebTransport = aWebTransport; + RebuildHashKey(); + } +} + +void nsHttpConnectionInfo::SetTlsFlags(uint32_t aTlsFlags) { + mTlsFlags = aTlsFlags; + const uint32_t tlsFlagsLength = 8; + const uint32_t tlsFlagsIndex = + UnderlyingIndex(HashKeyIndex::End) + strlen("[tlsflags0x"); + mHashKey.Replace(tlsFlagsIndex, tlsFlagsLength, + nsPrintfCString("%08x", mTlsFlags)); +} + +bool nsHttpConnectionInfo::UsingProxy() { + if (!mProxyInfo) return false; + return !mProxyInfo->IsDirect(); +} + +bool nsHttpConnectionInfo::HostIsLocalIPLiteral() const { + NetAddr netAddr; + // If the host/proxy host is not an IP address literal, return false. + nsAutoCString host(ProxyHost() ? ProxyHost() : Origin()); + if (NS_FAILED(netAddr.InitFromString(host))) { + return false; + } + 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..892d795b10 --- /dev/null +++ b/netwerk/protocol/http/nsHttpConnectionInfo.h @@ -0,0 +1,317 @@ +/* -*- 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" +#include "nsIRequest.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, + nsProxyInfo* proxyInfo, + const OriginAttributes& originAttributes, + bool endToEndSSL = false, bool aIsHttp3 = false, + bool aWebTransport = 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, + nsProxyInfo* proxyInfo, + const OriginAttributes& originAttributes, + const nsACString& routedHost, int32_t routedPort, + bool aIsHttp3, bool aWebTransport = false); + + 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(); + + // See comments in nsHttpConnectionInfo::BuildHashKey for the meaning of each + // field. + enum class HashKeyIndex : uint32_t { + Proxy = 0, + EndToEndSSL, + Anonymous, + Private, + InsecureScheme, + NoSpdy, + BeConservative, + AnonymousAllowClientCert, + FallbackConnection, + WebTransport, + End, + }; + constexpr inline auto UnderlyingIndex(HashKeyIndex aIndex) const { + return std::underlying_type_t(aIndex); + } + + 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** outCI); + [[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) { + SetHashCharAt(anon ? 'A' : '.', HashKeyIndex::Anonymous); + } + bool GetAnonymous() const { + return GetHashCharAt(HashKeyIndex::Anonymous) == 'A'; + } + void SetPrivate(bool priv) { + SetHashCharAt(priv ? 'P' : '.', HashKeyIndex::Private); + } + bool GetPrivate() const { + return GetHashCharAt(HashKeyIndex::Private) == 'P'; + } + void SetInsecureScheme(bool insecureScheme) { + SetHashCharAt(insecureScheme ? 'I' : '.', HashKeyIndex::InsecureScheme); + } + bool GetInsecureScheme() const { + return GetHashCharAt(HashKeyIndex::InsecureScheme) == 'I'; + } + + void SetNoSpdy(bool aNoSpdy) { + SetHashCharAt(aNoSpdy ? 'X' : '.', HashKeyIndex::NoSpdy); + if (aNoSpdy && mNPNToken == "h2"_ns) { + mNPNToken.Truncate(); + RebuildHashKey(); + } + } + bool GetNoSpdy() const { return GetHashCharAt(HashKeyIndex::NoSpdy) == 'X'; } + + void SetBeConservative(bool aBeConservative) { + SetHashCharAt(aBeConservative ? 'C' : '.', HashKeyIndex::BeConservative); + } + bool GetBeConservative() const { + return GetHashCharAt(HashKeyIndex::BeConservative) == 'C'; + } + + void SetAnonymousAllowClientCert(bool anon) { + SetHashCharAt(anon ? 'B' : '.', HashKeyIndex::AnonymousAllowClientCert); + } + bool GetAnonymousAllowClientCert() const { + return GetHashCharAt(HashKeyIndex::AnonymousAllowClientCert) == 'B'; + } + + void SetFallbackConnection(bool aFallback) { + SetHashCharAt(aFallback ? 'F' : '.', HashKeyIndex::FallbackConnection); + } + bool GetFallbackConnection() const { + return GetHashCharAt(HashKeyIndex::FallbackConnection) == 'F'; + } + + 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; } + + void SetWebTransport(bool aWebTransport); + bool GetWebTransport() const { return mWebTransport; } + + const nsCString& GetNPNToken() { return mNPNToken; } + const nsCString& GetUsername() { return mUsername; } + + 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: + void Init(const nsACString& host, int32_t port, const nsACString& npnToken, + const nsACString& username, nsProxyInfo* proxyInfo, + const OriginAttributes& originAttributes, bool e2eSSL, + bool aIsHttp3, bool aWebTransport); + void SetOriginServer(const nsACString& host, int32_t port); + nsCString::char_type GetHashCharAt(HashKeyIndex aIndex) const { + return mHashKey.CharAt(UnderlyingIndex(aIndex)); + } + void SetHashCharAt(nsCString::char_type aValue, HashKeyIndex aIndex) { + mHashKey.SetCharAt(aValue, UnderlyingIndex(aIndex)); + } + + nsCString mOrigin; + int32_t mOriginPort = 0; + nsCString mRoutedHost; + int32_t mRoutedPort; + + nsCString mHashKey; + nsCString mUsername; + nsCOMPtr mProxyInfo; + bool mUsingHttpProxy = false; + bool mUsingHttpsProxy = false; + bool mEndToEndSSL = false; + // if will use CONNECT with http proxy + bool mUsingConnect = false; + nsCString mNPNToken; + OriginAttributes mOriginAttributes; + nsIRequest::TRRMode mTRRMode; + + uint32_t mTlsFlags = 0; + 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 = false; + bool mWebTransport = false; + + 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..2e937d0f2a --- /dev/null +++ b/netwerk/protocol/http/nsHttpConnectionMgr.cpp @@ -0,0 +1,3863 @@ +/* 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 "ConnectionHandle.h" +#include "HttpConnectionUDP.h" +#include "NullHttpTransaction.h" +#include "SpeculativeTransaction.h" +#include "mozilla/Components.h" +#include "mozilla/PerfStats.h" +#include "mozilla/ProfilerMarkers.h" +#include "mozilla/SpinEventLoopUntil.h" +#include "mozilla/StaticPrefs_network.h" +#include "mozilla/Telemetry.h" +#include "mozilla/Unused.h" +#include "mozilla/glean/GleanMetrics.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 "nsIDNSListener.h" +#include "nsIDNSRecord.h" +#include "nsIDNSService.h" +#include "nsIHttpChannelInternal.h" +#include "nsIPipe.h" +#include "nsIRequestContext.h" +#include "nsISocketTransport.h" +#include "nsISocketTransportService.h" +#include "nsITransport.h" +#include "nsIXPConnect.h" +#include "nsInterfaceRequestorAgg.h" +#include "nsNetCID.h" +#include "nsNetSegmentUtils.h" +#include "nsNetUtil.h" +#include "nsQueryObject.h" +#include "nsSocketTransportService2.h" +#include "nsStreamUtils.h" + +using namespace mozilla; + +namespace geckoprofiler::markers { + +struct UrlMarker { + static constexpr Span MarkerTypeName() { + return MakeStringSpan("Url"); + } + static void StreamJSONMarkerData( + mozilla::baseprofiler::SpliceableJSONWriter& aWriter, + const mozilla::ProfilerString8View& aURL) { + if (aURL.Length() != 0) { + aWriter.StringProperty("url", aURL); + } + } + static MarkerSchema MarkerTypeDisplay() { + using MS = MarkerSchema; + MS schema(MS::Location::MarkerChart, MS::Location::MarkerTable); + schema.SetTableLabel("{marker.name} - {marker.data.url}"); + schema.AddKeyFormat("url", MS::Format::Url); + return schema; + } +}; + +} // namespace geckoprofiler::markers + +namespace mozilla::net { + +//----------------------------------------------------------------------------- + +NS_IMPL_ISUPPORTS(nsHttpConnectionMgr, nsIObserver, nsINamed) + +//----------------------------------------------------------------------------- + +nsHttpConnectionMgr::nsHttpConnectionMgr() { + 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 = components::IO::Service(); + if (ioService) { + nsCOMPtr realSTS = + components::SocketTransport::Service(); + 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() = default; + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(BoolWrapper, override) + + public: // intentional! + bool mBool{false}; + + 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("nsHttpConnectionMgr::Shutdown"_ns, + [&, 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(); + + nsCOMPtr target; + { + ReentrantMonitorAutoEnter mon(mReentrantMonitor); + target = mSocketThreadTarget; + } + + if (!target) { + NS_WARNING("cannot post event if not initialized"); + return NS_ERROR_NOT_INITIALIZED; + } + + nsCOMPtr event = new ConnEvent(this, handler, iparam, vparam); + return target->Dispatch(event, NS_DISPATCH_NORMAL); +} + +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 && StaticPrefs::network_http_http2_enabled())) { + 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::nsINamed +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsHttpConnectionMgr::GetName(nsACString& aName) { + aName.AssignLiteral("nsHttpConnectionMgr"); + return NS_OK; +} + +//----------------------------------------------------------------------------- +// 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)); + // Make sure a transaction is not in a pending queue. + CheckTransInPendingQueue(trans->AsHttpTransaction()); + 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)); + // Make sure a transaction is not in a pending queue. + CheckTransInPendingQueue(trans->AsHttpTransaction()); + + 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, const ClassOfService& classOfService) { + LOG( + ("nsHttpConnectionMgr::UpdateClassOfServiceOnTransaction [trans=%p " + "classOfService flags=%" PRIu32 " inc=%d]\n", + trans, static_cast(classOfService.Flags()), + classOfService.Incremental())); + + Unused << EnsureSocketThreadTarget(); + + nsCOMPtr target; + { + ReentrantMonitorAutoEnter mon(mReentrantMonitor); + target = mSocketThreadTarget; + } + + if (!target) { + NS_WARNING("cannot post event if not initialized"); + return; + } + + RefPtr self(this); + Unused << target->Dispatch(NS_NewRunnableFunction( + "nsHttpConnectionMgr::CallUpdateClassOfServiceOnTransaction", + [cos{classOfService}, self{std::move(self)}, trans = RefPtr{trans}]() { + self->OnMsgUpdateClassOfServiceOnTransaction( + cos, 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); +} + +nsresult nsHttpConnectionMgr::DoSingleConnectionCleanup( + nsHttpConnectionInfo* aCI) { + if (!aCI) { + return NS_ERROR_INVALID_ARG; + } + + RefPtr ci = aCI->Clone(); + return PostEvent(&nsHttpConnectionMgr::OnMsgDoSingleConnectionCleanup, 0, ci); +} + +class SpeculativeConnectArgs : public ARefBase { + public: + SpeculativeConnectArgs() = default; + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(SpeculativeConnectArgs, override) + + public: // intentional! + RefPtr mTrans; + + bool mFetchHTTPSRR{false}; + + 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; + } + + nsCString url = ci->EndToEndSSL() ? "https://"_ns : "http://"_ns; + url += ci->GetOrigin(); + PROFILER_MARKER("SpeculativeConnect", NETWORK, {}, UrlMarker, url); + + 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(); + + nsCOMPtr target; + { + ReentrantMonitorAutoEnter mon(mReentrantMonitor); + target = mSocketThreadTarget; + } + + if (!target) { + NS_WARNING("cannot post event if not initialized"); + return NS_ERROR_NOT_INITIALIZED; + } + + RefPtr connRef(conn); + RefPtr self(this); + return target->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->DnsAndConnectSocketsLength() == 0 && + ent->UrgentStartQueueLength() == 0 && ent->PendingQueueLength() == 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("~.:"); + } + if (ci->GetFallbackConnection()) { + newKey.AppendLiteral("~F:"); + } 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, bool aNoHttp3) { + 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); + LOG(("UpdateCoalescingForNewConn newConn=%p aNoHttp3=%d", newConn, aNoHttp3)); + if (newConn->ConnectionInfo()->GetWebTransport()) { + LOG(("Don't coalesce a WebTransport conn %p", newConn)); + // TODO: implement this properly in bug 1815735. + return; + } + + HttpConnectionBase* existingConn = + FindCoalescableConnection(ent, true, false, false); + if (existingConn) { + // Prefer http3 connection, but allow an HTTP/2 connection if it is used for + // WebSocket. + if (newConn->UsingHttp3() && existingConn->UsingSpdy()) { + RefPtr connTCP = do_QueryObject(existingConn); + if (connTCP && !connTCP->IsForWebSocket()) { + LOG( + ("UpdateCoalescingForNewConn() found existing active H2 conn that " + "could have served newConn, but new connection is H3, therefore " + "close the H2 conncetion")); + existingConn->SetCloseReason( + ConnectionCloseReason::CLOSE_EXISTING_CONN_FOR_COALESCING); + existingConn->DontReuse(); + } + } else if (existingConn->UsingHttp3() && newConn->UsingSpdy()) { + RefPtr connTCP = do_QueryObject(newConn); + if (connTCP && !connTCP->IsForWebSocket() && !aNoHttp3) { + LOG( + ("UpdateCoalescingForNewConn() found existing active H3 conn that " + "could have served H2 newConn graceful close of newConn=%p to " + "migrate to existingConn %p\n", + newConn, existingConn)); + existingConn->SetCloseReason( + ConnectionCloseReason::CLOSE_NEW_CONN_FOR_COALESCING); + newConn->DontReuse(); + return; + } + } 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)); + existingConn->SetCloseReason( + ConnectionCloseReason::CLOSE_NEW_CONN_FOR_COALESCING); + 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())); + + mCoalescingHash + .LookupOrInsertWith( + ent->mCoalescingKeys[i], + [] { + LOG(("UpdateCoalescingForNewConn() need new list element\n")); + return MakeUnique>(1); + }) + ->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, + bool disallowHttp3) { + 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, disallowHttp3); + + 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, false); + 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 alreadyDnsAndConnectSocketOrWaitingForTLS = + pendingTransInfo->IsAlreadyClaimedInitializingConn(); + + rv = TryDispatchTransaction(ent, alreadyDnsAndConnectSocketOrWaitingForTLS, + 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(mCurrentBrowserId, pendingQ, + maxFocusedWindowConnections); + + if (pendingQ.IsEmpty()) { + ent->AppendPendingQForNonFocusedWindows(mCurrentBrowserId, pendingQ, + availableConnections); + } + return; + } + + uint32_t maxNonFocusedWindowConnections = + availableConnections - maxFocusedWindowConnections; + nsTArray> remainingPendingQ; + + ent->AppendPendingQForFocusedWindow(mCurrentBrowserId, pendingQ, + maxFocusedWindowConnections); + + if (maxNonFocusedWindowConnections) { + ent->AppendPendingQForNonFocusedWindows( + mCurrentBrowserId, 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( + mCurrentBrowserId, pendingQ, + maxNonFocusedWindowConnections - remainingPendingQ.Length()); + } else if (pendingQ.Length() < maxFocusedWindowConnections) { + ent->AppendPendingQForNonFocusedWindows( + mCurrentBrowserId, 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()) { + if (ci->GetWebTransport()) { + // TODO: implement this properly in bug 1815735. + return false; + } + 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.ConstIter(); + 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 && + StaticPrefs::network_http_http2_enabled()) { + // 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 (const RefPtr& entry : mCT.Values()) { + 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 = ent->CreateDnsAndConnectSocket( + trans, trans->Caps(), false, false, + trans->GetClassOfService().Flags() & nsIClassOfService::UrgentStart, true, + pendingTransInfo); + if (NS_FAILED(rv)) { + /* hard failure */ + LOG( + ("nsHttpConnectionMgr::MakeNewConnection [ci = %s trans = %p] " + "CreateDnsAndConnectSocket() 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 onlyreused=%d active=%zu " + "idle=%zu]\n", + trans, ent->mConnInfo.get(), ent->mConnInfo->HashKey().get(), + uint32_t(trans->Caps()), 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, + (!StaticPrefs::network_http_http2_enabled() || + (caps & NS_HTTP_DISALLOW_SPDY)), + (!nsHttpHandler::IsHttp3Enabled() || (caps & NS_HTTP_DISALLOW_HTTP3))); + if (conn) { + LOG(("TryingDispatchTransaction: an active h2 connection exists")); + WebSocketSupport wsSupp = conn->GetWebSocketSupport(); + if (trans->IsWebsocketUpgrade()) { + LOG(("TryingDispatchTransaction: this is a websocket upgrade")); + if (wsSupp == WebSocketSupport::NO_SUPPORT) { + LOG(( + "TryingDispatchTransaction: no support for websockets over Http2")); + // 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; + trans->MakeSticky(); + } else if (wsSupp == WebSocketSupport::SUPPORTED) { + RefPtr connTCP = do_QueryObject(conn); + LOG(("TryingDispatchTransaction: websockets over Http2")); + + // No limit for number of websockets, dispatch transaction to the tunnel + RefPtr connToTunnel; + connTCP->CreateTunnelStream(trans, getter_AddRefs(connToTunnel), true); + ent->InsertIntoH2WebsocketConns(connToTunnel); + trans->SetConnection(nullptr); + connToTunnel->SetInSpdyTunnel(); // tells conn it is already in tunnel + trans->SetIsHttp2Websocket(true); + nsresult rv = DispatchTransaction(ent, trans, connToTunnel); + // need to undo NonSticky bypass for transaction reset to continue + // for correct websocket upgrade handling + trans->MakeSticky(); + return rv; + } else { + // if we aren't sure that websockets are supported yet or we are + // already at the connection limit then we queue the transaction + LOG(("TryingDispatchTransaction: unsure if websockets over Http2")); + return NS_ERROR_NOT_AVAILABLE; + } + } 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. + // This usually happens when the pref "network.dns.force_waiting_https_rr" is + // true or when echConfig is enabled. + if (trans->WaitingForHTTPSRR()) { + 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->GetClassOfService().Flags() & + 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; + } + } + } + + // 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->GetClassOfService().Flags() & 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); + + TimeStamp now = TimeStamp::Now(); + auto recordPendingTimeForHTTPSRR = [&](nsCString& aKey) { + uint32_t stage = trans->HTTPSSVCReceivedStage(); + TimeDuration elapsed = now - trans->GetPendingTime(); + if (HTTPS_RR_IS_USED(stage)) { + glean::networking::transaction_wait_time_https_rr.AccumulateRawDuration( + elapsed); + + } else { + glean::networking::transaction_wait_time.AccumulateRawDuration(elapsed); + } + PerfStats::RecordMeasurement(PerfStats::Metric::HttpTransactionWaitTime, + elapsed); + }; + + nsAutoCString httpVersionkey("h1"_ns); + 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); + if (NS_SUCCEEDED(rv) && !trans->GetPendingTime().IsNull()) { + if (conn->UsingSpdy()) { + httpVersionkey = "h2"_ns; + AccumulateTimeDelta(Telemetry::TRANSACTION_WAIT_TIME_SPDY, + trans->GetPendingTime(), now); + } else { + httpVersionkey = "h3"_ns; + AccumulateTimeDelta(Telemetry::TRANSACTION_WAIT_TIME_HTTP3, + trans->GetPendingTime(), now); + } + recordPendingTimeForHTTPSRR(httpVersionkey); + 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(), now); + recordPendingTimeForHTTPSRR(httpVersionkey); + trans->SetPendingTime(false); + } + return rv; +} + +// 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; + } + + // Make sure a transaction is not in a pending queue. + CheckTransInPendingQueue(trans); + + trans->SetPendingTime(); + + RefPtr pushedStreamWrapper = + trans->GetPushedStream(); + if (pushedStreamWrapper) { + Http2PushedStream* pushedStream = pushedStreamWrapper->GetStream(); + if (pushedStream) { + RefPtr session = pushedStream->Session(); + LOG((" ProcessNewTransaction %p tied to h2 session push %p\n", trans, + session.get())); + return session->AddStream(trans, trans->Priority(), 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)); + + bool isWildcard = false; + ConnectionEntry* ent = GetOrCreateConnectionEntry( + ci, trans->Caps() & NS_HTTP_DISALLOW_HTTP2_PROXY, + trans->Caps() & NS_HTTP_DISALLOW_SPDY, + trans->Caps() & NS_HTTP_DISALLOW_HTTP3, &isWildcard); + MOZ_ASSERT(ent); + + if (gHttpHandler->EchConfigEnabled(ci->IsHttp3())) { + 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 (isWildcard) { + // We have a HTTP/2 session to the proxy, create a new tunneled + // connection. + RefPtr conn = GetH2orH3ActiveConn(ent, false, true); + RefPtr connTCP = do_QueryObject(conn); + if (ci->UsingHttpsProxy() && ci->UsingConnect()) { + LOG(("About to create new tunnel conn from [%p]", connTCP.get())); + ConnectionEntry* specificEnt = mCT.GetWeak(ci->HashKey()); + + if (!specificEnt) { + RefPtr clone(ci->Clone()); + specificEnt = new ConnectionEntry(clone); + mCT.InsertOrUpdate(clone->HashKey(), RefPtr{specificEnt}); + } + + ent = specificEnt; + bool atLimit = AtActiveConnectionLimit(ent, trans->Caps()); + if (atLimit) { + rv = NS_ERROR_NOT_AVAILABLE; + } else { + RefPtr newTunnel; + connTCP->CreateTunnelStream(trans, getter_AddRefs(newTunnel)); + + ent->InsertIntoActiveConns(newTunnel); + trans->SetConnection(nullptr); + newTunnel->SetInSpdyTunnel(); + rv = DispatchTransaction(ent, trans, newTunnel); + // need to undo the bypass for transaction reset for proxy + trans->MakeNonRestartable(); + } + } else { + rv = DispatchTransaction(ent, trans, connTCP); + } + } else { + if (!ent->AllowHttp2()) { + trans->DisableSpdy(); + } + pendingTransInfo = new PendingTransactionInfo(trans); + rv = TryDispatchTransaction(ent, false, 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) { + MOZ_DIAGNOSTIC_ASSERT(mNumActiveConns > 0); + if (mNumActiveConns > 0) { + mNumActiveConns--; + } + + RefPtr connTCP = do_QueryObject(conn); + if (!connTCP || connTCP->EverUsedSpdy()) mNumSpdyHttp3ActiveConns--; + ConditionallyStopTimeoutTick(); +} + +void nsHttpConnectionMgr::StartedConnect() { + mNumActiveConns++; + ActivateTimeoutTick(); // likely disabled by RecvdConnect() +} + +void nsHttpConnectionMgr::RecvdConnect() { + MOZ_DIAGNOSTIC_ASSERT(mNumActiveConns > 0); + if (mNumActiveConns > 0) { + mNumActiveConns--; + } + + ConditionallyStopTimeoutTick(); +} + +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 (const auto& entry : mCT.Values()) { + ProcessSpdyPendingQ(entry.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 websocket "fake" connections + ent->CloseH2WebsocketConnections(); + + ent->ClosePendingConnections(); + + // Close all pending transactions. + ent->CancelAllTransactions(NS_ERROR_ABORT); + + // Close all half open tcp connections. + ent->CloseAllDnsAndConnectSockets(); + + MOZ_ASSERT(!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( + ClassOfService cos, ARefBase* param) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG( + ("nsHttpConnectionMgr::OnMsgUpdateClassOfServiceOnTransaction " + "[trans=%p]\n", + param)); + + nsHttpTransaction* trans = static_cast(param); + + ClassOfService previous = trans->GetClassOfService(); + trans->SetClassOfService(cos); + + // incremental change alone will not trigger a reschedule + if ((previous.Flags() ^ cos.Flags()) & + (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 (const auto& entry : mCT.Values()) { + Unused << ProcessPendingQForEntry(entry.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 (const auto& entry : mCT.Values()) { + if (ProcessPendingQForEntry(entry.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 && StaticPrefs::network_http_http2_enabled())) { + 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->DnsAndConnectSocketsLength() == 0 && + ent->PendingQueueLength() == 0 && + ent->UrgentStartQueueLength() == 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 (const RefPtr& ent : mCT.Values()) { + // Close the connections with no registered traffic. + 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; + } + + mCoalescingHash.Clear(); + + // Mark connections for traffic verification + for (const auto& entry : mCT.Values()) { + entry->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!"); + } + // Calling ActivateTimeoutTick to ensure the next timeout tick is 1s. + ActivateTimeoutTick(); +} + +void nsHttpConnectionMgr::OnMsgDoShiftReloadConnectionCleanup(int32_t, + ARefBase* param) { + LOG(("nsHttpConnectionMgr::OnMsgDoShiftReloadConnectionCleanup\n")); + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + mCoalescingHash.Clear(); + + nsHttpConnectionInfo* ci = static_cast(param); + + for (const auto& entry : mCT.Values()) { + entry->ClosePersistentConnections(); + } + + if (ci) ResetIPFamilyPreference(ci); +} + +void nsHttpConnectionMgr::OnMsgDoSingleConnectionCleanup(int32_t, + ARefBase* param) { + LOG(("nsHttpConnectionMgr::OnMsgDoSingleConnectionCleanup\n")); + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + nsHttpConnectionInfo* ci = static_cast(param); + + if (!ci) { + return; + } + + ConnectionEntry* entry = mCT.GetWeak(ci->HashKey()); + if (entry) { + entry->ClosePersistentConnections(); + } + + 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. + bool isWildcard = false; + ent = GetOrCreateConnectionEntry(conn->ConnectionInfo(), true, false, false, + &isWildcard); + 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)) || + NS_SUCCEEDED(ent->RemovePendingConnection(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 { + if (ent->IsInH2WebsocketConns(conn)) { + ent->RemoveH2WebsocketConns(conn); + } + LOG((" connection cannot be reused; closing connection\n")); + conn->SetCloseReason(ConnectionCloseReason::CANT_REUSED); + 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); + + nsCOMPtr socketIn; + nsCOMPtr socketOut; + + // If this is for JS, the input and output sockets need to be piped over the + // socket thread. Otherwise, the JS may attempt to read and/or write the + // sockets on the main thread, which could cause network I/O on the main + // thread. This is particularly bad in the case of TLS connections, because + // PSM and NSS rely on those connections only being used on the socket + // thread. + if (data->mJsWrapped) { + nsCOMPtr pipeIn; + uint32_t segsize = 0; + uint32_t segcount = 0; + net_ResolveSegmentParams(segsize, segcount); + if (NS_SUCCEEDED(rv)) { + NS_NewPipe2(getter_AddRefs(pipeIn), getter_AddRefs(socketOut), true, true, + segsize, segcount); + rv = NS_AsyncCopy(pipeIn, data->mSocketOut, gSocketTransportService, + NS_ASYNCCOPY_VIA_READSEGMENTS, segsize); + } + + nsCOMPtr pipeOut; + if (NS_SUCCEEDED(rv)) { + NS_NewPipe2(getter_AddRefs(socketIn), getter_AddRefs(pipeOut), true, true, + segsize, segcount); + rv = NS_AsyncCopy(data->mSocketIn, pipeOut, gSocketTransportService, + NS_ASYNCCOPY_VIA_WRITESEGMENTS, segsize); + } + } else { + socketIn = upgradeData->mSocketIn; + socketOut = upgradeData->mSocketOut; + } + + auto transportAvailableFunc = [upgradeData{std::move(upgradeData)}, socketIn, + socketOut, 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, socketIn, socketOut); + 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*) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + 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; + } + ReentrantMonitorAutoEnter mon(mReentrantMonitor); + if (!mSocketThreadTarget) { + NS_WARNING("cannot activate timout if not initialized or shutdown"); + return; + } + mTimeoutTick->SetTarget(mSocketThreadTarget); + } + + if (mIsShuttingDown) { // Atomic + // don't set a timer to generate an event if we're shutting down + // (and mSocketThreadTarget might be null or garbage if we're shutting down) + return; + } + 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::UpdateCurrentBrowserId(uint64_t aId) { + RefPtr idWrapper = new UINT64Wrapper(aId); + return PostEvent(&nsHttpConnectionMgr::OnMsgUpdateCurrentBrowserId, 0, + idWrapper); +} + +void nsHttpConnectionMgr::SetThrottlingEnabled(bool aEnable) { + LOG(("nsHttpConnectionMgr::SetThrottlingEnabled enable=%d", aEnable)); + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + 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(mCurrentBrowserId); + au = trs ? trs->Length() : 0; + trs = mActiveTransactions[true].Get(mCurrentBrowserId); + at = trs ? trs->Length() : 0; + + for (const auto& data : mActiveTransactions[false].Values()) { + bu += data->Length(); + } + bu -= au; + for (const auto& data : mActiveTransactions[true].Values()) { + bt += data->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->BrowserId(); + bool throttled = aTrans->EligibleForThrottling(); + + nsTArray>* transactions = + mActiveTransactions[throttled].GetOrInsertNew(tabId); + + MOZ_ASSERT(!transactions->Contains(aTrans)); + + transactions->AppendElement(aTrans); + + LOG(("nsHttpConnectionMgr::AddActiveTransaction t=%p tabid=%" PRIx64 + "(%d) thr=%d", + aTrans, tabId, tabId == mCurrentBrowserId, throttled)); + LogActiveTransactions('+'); + + if (tabId == mCurrentBrowserId) { + 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->BrowserId(); + bool forActiveTab = tabId == mCurrentBrowserId; + 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(mCurrentBrowserId)); + } + 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->BrowserId(); + bool forActiveTab = tabId == mCurrentBrowserId; + 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(mCurrentBrowserId) > 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('^'); +} + +// Can be called with or without the monitor held +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 (const auto& entry : hashtable) { + if (excludeForActiveTab && entry.GetKey() == mCurrentBrowserId) { + // These have never been throttled (never stopped reading) + continue; + } + ResumeReadOf(entry.GetWeak()); + } +} + +void nsHttpConnectionMgr::ResumeReadOf( + nsTArray>* transactions) { + MOZ_ASSERT(transactions); + + for (const auto& trans : *transactions) { + trans->ResumeReading(); + } +} + +void nsHttpConnectionMgr::NotifyConnectionOfBrowserIdChange( + uint64_t previousId) { + 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(previousId); + addConnectionHelper(transactions); + transactions = mActiveTransactions[false].Get(mCurrentBrowserId); + addConnectionHelper(transactions); + + // Get throttled transactions with the previous and current window id. + transactions = mActiveTransactions[true].Get(previousId); + addConnectionHelper(transactions); + transactions = mActiveTransactions[true].Get(mCurrentBrowserId); + addConnectionHelper(transactions); + + for (const auto& conn : connections) { + conn->CurrentBrowserIdChanged(mCurrentBrowserId); + } +} + +void nsHttpConnectionMgr::OnMsgUpdateCurrentBrowserId(int32_t aLoading, + ARefBase* param) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + uint64_t id = static_cast(param)->GetValue(); + + if (mCurrentBrowserId == id) { + // duplicate notification + return; + } + + bool activeTabWasLoading = mActiveTabTransactionsExist; + + uint64_t previousId = mCurrentBrowserId; + mCurrentBrowserId = id; + + if (gHttpHandler->ActiveTabPriority()) { + NotifyConnectionOfBrowserIdChange(previousId); + } + + LOG( + ("nsHttpConnectionMgr::OnMsgUpdateCurrentBrowserId" + " id=%" PRIx64 "\n", + mCurrentBrowserId)); + + 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(mCurrentBrowserId); + mActiveTabUnthrottledTransactionsExist = !!transactions; + + if (!mActiveTabUnthrottledTransactionsExist) { + transactions = mActiveTransactions[true].Get(mCurrentBrowserId); + } + 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 (const RefPtr& ent : mCT.Values()) { + 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, bool* aIsWildcard, bool* aAvailableForDispatchNow) { + if (aAvailableForDispatchNow) { + *aAvailableForDispatchNow = false; + } + *aIsWildcard = false; + + // step 1 + ConnectionEntry* specificEnt = mCT.GetWeak(specificCI->HashKey()); + if (specificEnt && specificEnt->AvailableForDispatchNow()) { + if (aAvailableForDispatchNow) { + *aAvailableForDispatchNow = true; + } + 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()) { + if (aAvailableForDispatchNow) { + *aAvailableForDispatchNow = true; + } + *aIsWildcard = true; + return wildCardEnt; + } + } + + // step 3 + if (!specificEnt) { + RefPtr clone(specificCI->Clone()); + specificEnt = new ConnectionEntry(clone); + mCT.InsertOrUpdate(clone->HashKey(), RefPtr{specificEnt}); + } + return specificEnt; +} + +void nsHttpConnectionMgr::DoSpeculativeConnection( + SpeculativeTransaction* aTrans, bool aFetchHTTPSRR) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + MOZ_ASSERT(aTrans); + + bool isWildcard = false; + ConnectionEntry* ent = GetOrCreateConnectionEntry( + aTrans->ConnectionInfo(), false, aTrans->Caps() & NS_HTTP_DISALLOW_SPDY, + aTrans->Caps() & NS_HTTP_DISALLOW_HTTP3, &isWildcard); + if (!aFetchHTTPSRR && + gHttpHandler->EchConfigEnabled(aTrans->ConnectionInfo()->IsHttp3())) { + // This happens when this is called from + // SpeculativeTransaction::OnHTTPSRRAvailable. We have to update this + // entry's echConfig so that the newly created connection can use the latest + // echConfig. + ent->MaybeUpdateEchConfig(aTrans->ConnectionInfo()); + } + DoSpeculativeConnectionInternal(ent, aTrans, aFetchHTTPSRR); +} + +void nsHttpConnectionMgr::DoSpeculativeConnectionInternal( + ConnectionEntry* aEnt, SpeculativeTransaction* aTrans, bool aFetchHTTPSRR) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + MOZ_ASSERT(aTrans); + MOZ_ASSERT(aEnt); + if (!gHttpHandler->Active()) { + // Do nothing if we are shutting down. + return; + } + + if (aFetchHTTPSRR && NS_SUCCEEDED(aTrans->FetchHTTPSRR())) { + // nsHttpConnectionMgr::DoSpeculativeConnection will be called again when + // HTTPS RR is available. + return; + } + + 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 (mNumDnsAndConnectSockets < parallelSpeculativeConnectLimit && + ((ignoreIdle && + (aEnt->IdleConnectionsLength() < parallelSpeculativeConnectLimit)) || + !aEnt->IdleConnectionsLength()) && + !(keepAlive && aEnt->RestrictConnections()) && + !AtActiveConnectionLimit(aEnt, aTrans->Caps())) { + nsresult rv = aEnt->CreateDnsAndConnectSocket(aTrans, aTrans->Caps(), true, + isFromPredictor, false, + allow1918, nullptr); + if (NS_FAILED(rv)) { + glean::networking::speculative_connect_outcome + .Get("aborted_socket_fail"_ns) + .Add(1); + LOG( + ("DoSpeculativeConnectionInternal Transport socket creation " + "failure: %" PRIx32 "\n", + static_cast(rv))); + } else { + glean::networking::speculative_connect_outcome.Get("successful"_ns) + .Add(1); + } + } else { + glean::networking::speculative_connect_outcome + .Get("aborted_socket_limit"_ns) + .Add(1); + LOG( + ("DoSpeculativeConnectionInternal Transport ci=%s " + "not created due to existing connection count:%d", + aEnt->mConnInfo->HashKey().get(), parallelSpeculativeConnectLimit)); + } +} + +void nsHttpConnectionMgr::DoFallbackConnection(SpeculativeTransaction* aTrans, + bool aFetchHTTPSRR) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + MOZ_ASSERT(aTrans); + + LOG(("nsHttpConnectionMgr::DoFallbackConnection")); + + bool availableForDispatchNow = false; + bool aIsWildcard = false; + ConnectionEntry* ent = GetOrCreateConnectionEntry( + aTrans->ConnectionInfo(), false, aTrans->Caps() & NS_HTTP_DISALLOW_SPDY, + aTrans->Caps() & NS_HTTP_DISALLOW_HTTP3, &aIsWildcard, + &availableForDispatchNow); + + if (availableForDispatchNow) { + LOG( + ("nsHttpConnectionMgr::DoFallbackConnection fallback connection is " + "ready for dispatching ent=%p", + ent)); + aTrans->InvokeCallback(); + return; + } + + DoSpeculativeConnectionInternal(ent, aTrans, aFetchHTTPSRR); +} + +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) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + 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); + mCoalescingHash.GetOrInsertNew(newKey, 1)->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 (const RefPtr& ent : mCT.Values()) { + 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; + } + + bool isWildcard = false; + ConnectionEntry* wcEnt = + GetOrCreateConnectionEntry(wildCardCI, true, false, false, &isWildcard); + 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->DnsAndConnectSocketsLength(), ent->PendingQueueLength())); + + LOG( + ("nsHttpConnectionMgr::MakeConnEntryWildCard wc-ent %p " + "idle=%zu active=%zu half=%zu pending=%zu\n", + wcEnt, wcEnt->IdleConnectionsLength(), wcEnt->ActiveConnsLength(), + wcEnt->DnsAndConnectSocketsLength(), wcEnt->PendingQueueLength())); + + ent->MoveConnection(proxyConn, wcEnt); +} + +bool nsHttpConnectionMgr::RemoveTransFromConnEntry(nsHttpTransaction* aTrans, + const nsACString& aHashKey) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + LOG(("nsHttpConnectionMgr::RemoveTransFromConnEntry: trans=%p ci=%s", aTrans, + PromiseFlatCString(aHashKey).get())); + + if (aHashKey.IsEmpty()) { + return false; + } + + // Step 1: Get the transaction's connection entry. + ConnectionEntry* entry = mCT.GetWeak(aHashKey); + if (!entry) { + return false; + } + + // Step 2: Try to find the undispatched transaction. + return entry->RemoveTransFromPendingQ(aTrans); +} + +void nsHttpConnectionMgr::IncreaseNumDnsAndConnectSockets() { + mNumDnsAndConnectSockets++; +} + +void nsHttpConnectionMgr::DecreaseNumDnsAndConnectSockets() { + MOZ_ASSERT(mNumDnsAndConnectSockets); + if (mNumDnsAndConnectSockets) { // just in case + mNumDnsAndConnectSockets--; + } +} + +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(); +} + +already_AddRefed nsHttpConnectionMgr::FindConnectionEntry( + const nsHttpConnectionInfo* ci) { + return mCT.Get(ci->HashKey()); +} + +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(); +} + +void nsHttpConnectionMgr::CheckTransInPendingQueue(nsHttpTransaction* aTrans) { +#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED + // We only do this check on socket thread. When this function is called on + // main thread, the transaction is newly created, so we can skip this check. + if (!OnSocketThread()) { + return; + } + + nsAutoCString hashKey; + aTrans->GetHashKeyOfConnectionEntry(hashKey); + if (hashKey.IsEmpty()) { + return; + } + + bool foundInPendingQ = RemoveTransFromConnEntry(aTrans, hashKey); + MOZ_DIAGNOSTIC_ASSERT(!foundInPendingQ); +#endif +} + +bool nsHttpConnectionMgr::AllowToRetryDifferentIPFamilyForHttp3( + nsHttpConnectionInfo* ci, nsresult aError) { + ConnectionEntry* ent = mCT.GetWeak(ci->HashKey()); + if (!ent) { + return false; + } + + return ent->AllowToRetryDifferentIPFamilyForHttp3(aError); +} + +void nsHttpConnectionMgr::SetRetryDifferentIPFamilyForHttp3( + nsHttpConnectionInfo* ci, uint16_t aIPFamily) { + ConnectionEntry* ent = mCT.GetWeak(ci->HashKey()); + if (!ent) { + return; + } + + ent->SetRetryDifferentIPFamilyForHttp3(aIPFamily); +} + +} // namespace mozilla::net diff --git a/netwerk/protocol/http/nsHttpConnectionMgr.h b/netwerk/protocol/http/nsHttpConnectionMgr.h new file mode 100644 index 0000000000..2cf4ab7568 --- /dev/null +++ b/netwerk/protocol/http/nsHttpConnectionMgr.h @@ -0,0 +1,469 @@ +/* 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 "DnsAndConnectSocket.h" +#include "HttpConnectionBase.h" +#include "HttpConnectionMgrShell.h" +#include "nsHttpConnection.h" +#include "nsHttpTransaction.h" +#include "nsTArray.h" +#include "nsThreadUtils.h" +#include "nsClassHashtable.h" +#include "mozilla/ReentrantMonitor.h" +#include "mozilla/TimeStamp.h" +#include "mozilla/Attributes.h" +#include "ARefBase.h" +#include "nsWeakReference.h" +#include "ConnectionEntry.h" + +#include "nsINamed.h" +#include "nsIObserver.h" +#include "nsITimer.h" + +class nsIHttpUpgradeListener; + +namespace mozilla::net { +class EventTokenBucket; +class NullHttpTransaction; +struct HttpRetParams; + +//----------------------------------------------------------------------------- + +// message handlers have this signature +class nsHttpConnectionMgr; +using nsConnEventHandler = void (nsHttpConnectionMgr::*)(int32_t, ARefBase*); + +class nsHttpConnectionMgr final : public HttpConnectionMgrShell, + public nsIObserver, + nsINamed { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_HTTPCONNECTIONMGRSHELL + NS_DECL_NSIOBSERVER + NS_DECL_NSINAMED + + //------------------------------------------------------------------------- + // 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 code); + + //------------------------------------------------------------------------- + // 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); + + // Remove a transaction from the pendingQ of it's connection entry. Returns + // true if the transaction is removed successfully, otherwise returns false. + bool RemoveTransFromConnEntry(nsHttpTransaction* aTrans, + const nsACString& aHashKey); + + // Directly dispatch the transaction or insert it in to the pendingQ. + [[nodiscard]] nsresult ProcessNewTransaction(nsHttpTransaction* aTrans); + + // 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*); + + // Close a single connection and prevent it from being reused. + [[nodiscard]] nsresult DoSingleConnectionCleanup(nsHttpConnectionInfo*); + + // 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, + bool disallowHttp3); + + 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 CurrentBrowserId() { return mCurrentBrowserId; } + + void DoFallbackConnection(SpeculativeTransaction* aTrans, bool aFetchHTTPSRR); + void DoSpeculativeConnection(SpeculativeTransaction* aTrans, + bool aFetchHTTPSRR); + + HttpConnectionBase* GetH2orH3ActiveConn(ConnectionEntry* ent, bool aNoHttp2, + bool aNoHttp3); + + void IncreaseNumDnsAndConnectSockets(); + void DecreaseNumDnsAndConnectSockets(); + + // 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); + + void DoSpeculativeConnectionInternal(ConnectionEntry* aEnt, + SpeculativeTransaction* aTrans, + bool aFetchHTTPSRR); + + already_AddRefed FindConnectionEntry( + const nsHttpConnectionInfo* ci); + + public: + void RegisterOriginCoalescingKey(HttpConnectionBase*, const nsACString& host, + int32_t port); + // A test if be-conservative should be used when proxy is setup for the + // connection + bool BeConservativeIfProxied(nsIProxyInfo* proxy); + + bool AllowToRetryDifferentIPFamilyForHttp3(nsHttpConnectionInfo* ci, + nsresult aError); + void SetRetryDifferentIPFamilyForHttp3(nsHttpConnectionInfo* ci, + uint16_t aIPFamily); + + protected: + friend class ConnectionEntry; + void IncrementActiveConnCount(); + void DecrementActiveConnCount(HttpConnectionBase*); + + private: + friend class DnsAndConnectSocket; + friend class PendingTransactionInfo; + + //------------------------------------------------------------------------- + // NOTE: these members may be accessed from any thread (use mReentrantMonitor) + //------------------------------------------------------------------------- + + ReentrantMonitor mReentrantMonitor{"nsHttpConnectionMgr.mReentrantMonitor"}; + // This is used as a flag that we're shut down, and no new events should be + // dispatched. + nsCOMPtr mSocketThreadTarget + MOZ_GUARDED_BY(mReentrantMonitor); + + Atomic mIsShuttingDown{false}; + + //------------------------------------------------------------------------- + // NOTE: these members are only accessed on the socket transport thread + //------------------------------------------------------------------------- + // connection limits + uint16_t mMaxUrgentExcessiveConns{0}; + uint16_t mMaxConns{0}; + uint16_t mMaxPersistConnsPerHost{0}; + uint16_t mMaxPersistConnsPerProxy{0}; + uint16_t mMaxRequestDelay{0}; // in seconds + bool mThrottleEnabled{false}; + uint32_t mThrottleVersion{2}; + uint32_t mThrottleSuspendFor{0}; + uint32_t mThrottleResumeFor{0}; + uint32_t mThrottleReadLimit{0}; + uint32_t mThrottleReadInterval{0}; + uint32_t mThrottleHoldTime{0}; + TimeDuration mThrottleMaxTime; + bool mBeConservativeForProxy{true}; + + [[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 EnsureSocketThreadTarget(); + void ReportProxyTelemetry(ConnectionEntry* ent); + void StartedConnect(); + void RecvdConnect(); + + ConnectionEntry* GetOrCreateConnectionEntry( + nsHttpConnectionInfo*, bool prohibitWildCard, bool aNoHttp2, + bool aNoHttp3, bool* aIsWildcard, + bool* aAvailableForDispatchNow = nullptr); + + [[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, bool aNoHttp3); + + 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(ClassOfService, 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 OnMsgDoSingleConnectionCleanup(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 OnMsgUpdateCurrentBrowserId(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{0}; + // Total number of idle connections in all of the ConnectionEntry objects + // that are accessed from mCT connection table. + uint16_t mNumIdleConns{0}; + // Total number of spdy or http3 connections which are a subset of the active + // conns + uint16_t mNumSpdyHttp3ActiveConns{0}; + // Total number of connections in DnsAndConnectSockets ConnectionEntry objects + // that are accessed from mCT connection table + uint32_t mNumDnsAndConnectSockets{0}; + + // Holds time in seconds for next wake-up to prune dead connections. + uint64_t mTimeOfNextWakeUp{UINT64_MAX}; + // Timer for next pruning of dead connections. + nsCOMPtr mTimer; + // Timer for pruning stalled connections after changed network. + nsCOMPtr mTrafficTimer; + bool mPruningNoTraffic{false}; + + // 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{false}; + uint32_t mTimeoutTickNext{1}; + + // + // 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 mCurrentBrowserId{0}; + + // 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{false}; + + 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 excludeForActiveTab = false); + void ResumeReadOf(nsTArray>*); + + // Cached status of the active tab active transactions existence, + // saves a lot of hashtable lookups + bool mActiveTabTransactionsExist{false}; + bool mActiveTabUnthrottledTransactionsExist{false}; + + void LogActiveTransactions(char); + + // When current active tab is changed, this function uses + // |previousId| to select background transactions and + // |mCurrentBrowserId| to select foreground transactions. + // Then, it notifies selected transactions' connection of the new active tab + // id. + void NotifyConnectionOfBrowserIdChange(uint64_t previousId); + + void CheckTransInPendingQueue(nsHttpTransaction* aTrans); +}; + +} // namespace mozilla::net + +#endif // !nsHttpConnectionMgr_h__ diff --git a/netwerk/protocol/http/nsHttpDigestAuth.cpp b/netwerk/protocol/http/nsHttpDigestAuth.cpp new file mode 100644 index 0000000000..2a98301942 --- /dev/null +++ b/netwerk/protocol/http/nsHttpDigestAuth.cpp @@ -0,0 +1,743 @@ +/* -*- 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" + +constexpr uint16_t DigestLength(uint16_t aAlgorithm) { + if (aAlgorithm & (ALGO_SHA256 | ALGO_SHA256_SESS)) { + return SHA256_DIGEST_LENGTH; + } + return MD5_DIGEST_LENGTH; +} + +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::DigestHash(const char* buf, uint32_t len, + uint16_t algorithm) { + 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; + } + } + + uint32_t dlen; + if (algorithm & (ALGO_SHA256 | ALGO_SHA256_SESS)) { + rv = mVerifier->Init(nsICryptoHash::SHA256); + dlen = SHA256_DIGEST_LENGTH; + } else { + rv = mVerifier->Init(nsICryptoHash::MD5); + dlen = MD5_DIGEST_LENGTH; + } + 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() == dlen); + 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 nsACString& 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 (!(algorithm & + (ALGO_MD5 | ALGO_MD5_SESS | ALGO_SHA256 | ALGO_SHA256_SESS))) { + // they asked for an algorithm that we do not support yet (like SHA-512/256) + NS_WARNING("unsupported algorithm requested by Digest authentication"); + return NS_ERROR_NOT_IMPLEMENTED; + } + + 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 nsACString& challenge, + bool isProxyAuth, const nsAString& domain, const nsAString& username, + const nsAString& password, nsISupports* sessionState, + nsISupports* continuationState, nsICancelable** aCancellable) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsHttpDigestAuth::GenerateCredentials( + nsIHttpAuthenticableChannel* authChannel, const nsACString& aChallenge, + bool isProxyAuth, const nsAString& userdomain, const nsAString& username, + const nsAString& password, nsISupports** sessionState, + nsISupports** continuationState, uint32_t* aFlags, nsACString& creds) + +{ + LOG(("nsHttpDigestAuth::GenerateCredentials [challenge=%s]\n", + aChallenge.BeginReading())); + + *aFlags = 0; + + bool isDigestAuth = StringBeginsWith(aChallenge, "digest "_ns, + nsCaseInsensitiveCStringComparator); + 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 = + !nsCRT::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(aChallenge, realm, domain, nonce, opaque, &stale, + &algorithm, &qop); + if (NS_FAILED(rv)) { + LOG( + ("nsHttpDigestAuth::GenerateCredentials [ParseChallenge failed " + "rv=%" PRIx32 "]\n", + static_cast(rv))); + return rv; + } + + const uint32_t dhexlen = 2 * DigestLength(algorithm) + 1; + char ha1_digest[dhexlen]; + char ha2_digest[dhexlen]; + char response_digest[dhexlen]; + char upload_data_digest[dhexlen]; + + 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 | ALGO_MD5_SESS | ALGO_SHA256 | ALGO_SHA256_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, algorithm, qop, upload_data_digest, + ha2_digest); + if (NS_FAILED(rv)) return rv; + + rv = CalculateResponse(ha1_digest, ha2_digest, algorithm, 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 if (algorithm & ALGO_SHA256) { + authString.AppendLiteral("SHA-256"); + } else if (algorithm & ALGO_SHA256_SESS) { + authString.AppendLiteral("SHA-256-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 = 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, uint16_t algorithm, + const nsCString& nonce, uint16_t qop, const char* nonce_count, + const nsCString& cnonce, char* result) { + const uint32_t dhexlen = 2 * DigestLength(algorithm); + uint32_t len = 2 * dhexlen + 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, dhexlen); + 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, dhexlen); + + nsresult rv = DigestHash(contents.get(), contents.Length(), algorithm); + if (NS_SUCCEEDED(rv)) rv = ExpandToHex(mHashBuf, result, algorithm); + return rv; +} + +nsresult nsHttpDigestAuth::ExpandToHex(const char* digest, char* result, + uint16_t algorithm) { + int16_t index, value; + const int16_t dlen = DigestLength(algorithm); + + for (index = 0; index < dlen; 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[2 * dlen] = 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) { + const int16_t dhexlen = 2 * DigestLength(algorithm); + int16_t len = username.Length() + password.Length() + realm.Length() + 2; + if (algorithm & (ALGO_MD5_SESS | ALGO_SHA256_SESS)) { + int16_t exlen = dhexlen + 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 = DigestHash(contents.get(), contents.Length(), algorithm); + if (NS_FAILED(rv)) return rv; + + if (algorithm & (ALGO_MD5_SESS | ALGO_SHA256_SESS)) { + char part1[dhexlen + 1]; + rv = ExpandToHex(mHashBuf, part1, algorithm); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + contents.Assign(part1, dhexlen); + contents.Append(':'); + contents.Append(nonce); + contents.Append(':'); + contents.Append(cnonce); + + rv = DigestHash(contents.get(), contents.Length(), algorithm); + if (NS_FAILED(rv)) return rv; + } + + return ExpandToHex(mHashBuf, result, algorithm); +} + +nsresult nsHttpDigestAuth::CalculateHA2(const nsCString& method, + const nsCString& path, + uint16_t algorithm, uint16_t qop, + const char* bodyDigest, char* result) { + uint16_t methodLen = method.Length(); + uint32_t pathLen = path.Length(); + uint32_t len = methodLen + pathLen + 1; + const uint32_t dhexlen = 2 * DigestLength(algorithm); + + if (qop & QOP_AUTH_INT) { + len += dhexlen + 1; + } + + nsAutoCString contents; + contents.SetCapacity(len); + + contents.Assign(method); + contents.Append(':'); + contents.Append(path); + + if (qop & QOP_AUTH_INT) { + contents.Append(':'); + contents.Append(bodyDigest, dhexlen); + } + + nsresult rv = DigestHash(contents.get(), contents.Length(), algorithm); + if (NS_FAILED(rv)) { + return rv; + } + return ExpandToHex(mHashBuf, result, algorithm); +} + +nsresult nsHttpDigestAuth::ParseChallenge(const nsACString& aChallenge, + 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 (aChallenge.Length() > 16000000) { + return NS_ERROR_INVALID_ARG; + } + + const char* challenge = aChallenge.BeginReading(); + const char* end = aChallenge.EndReading(); + const char* p = challenge + 6; // first 6 characters are "Digest" + if (p >= end) { + return NS_ERROR_INVALID_ARG; + } + + *stale = false; + *algorithm = ALGO_MD5; // default is MD5 + *qop = 0; + + for (;;) { + while (p < end && (*p == ',' || nsCRT::IsAsciiSpace(*p))) { + ++p; + } + if (p >= end) { + break; + } + + // name + int32_t nameStart = (p - challenge); + while (p < end && !nsCRT::IsAsciiSpace(*p) && *p != '=') { + ++p; + } + if (p >= end) { + return NS_ERROR_INVALID_ARG; + } + int32_t nameLength = (p - challenge) - nameStart; + + while (p < end && (nsCRT::IsAsciiSpace(*p) || *p == '=')) { + ++p; + } + if (p >= end) { + 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 < end && *p != '"') { + ++p; + } + if (p >= end || *p != '"') { + return NS_ERROR_INVALID_ARG; + } + valueLength = (p - challenge) - valueStart; + ++p; + } else { + while (p < end && !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 (valueLength == 7 && nsCRT::strncasecmp(challenge + valueStart, + "SHA-256", 7) == 0) { + *algorithm |= ALGO_SHA256; + } else if (valueLength == 12 && + nsCRT::strncasecmp(challenge + valueStart, "SHA-256-sess", + 12) == 0) { + *algorithm |= ALGO_SHA256_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..d9830a17dc --- /dev/null +++ b/netwerk/protocol/http/nsHttpDigestAuth.h @@ -0,0 +1,98 @@ +/* -*- 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 ALGO_SHA256 0x08 +#define ALGO_SHA256_SESS 0x10 +#define QOP_AUTH 0x01 +#define QOP_AUTH_INT 0x02 + +#define NONCE_COUNT_LENGTH 8 +#ifndef MD5_DIGEST_LENGTH +# define MD5_DIGEST_LENGTH 16 +#endif +#ifndef SHA256_DIGEST_LENGTH +# define SHA256_DIGEST_LENGTH 32 +#endif + +//----------------------------------------------------------------------------- +// nsHttpDigestAuth +//----------------------------------------------------------------------------- + +class nsHttpDigestAuth final : public nsIHttpAuthenticator { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIHTTPAUTHENTICATOR + + nsHttpDigestAuth() = default; + + static already_AddRefed GetOrCreate(); + + [[nodiscard]] static nsresult ParseChallenge( + const nsACString& aChallenge, nsACString& realm, nsACString& domain, + nsACString& nonce, nsACString& opaque, bool* stale, uint16_t* algorithm, + uint16_t* qop); + + protected: + ~nsHttpDigestAuth() = default; + + [[nodiscard]] nsresult ExpandToHex(const char* digest, char* result, + uint16_t algorithm); + + [[nodiscard]] nsresult CalculateResponse( + const char* ha1_digest, const char* ha2_digest, uint16_t algorithm, + 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 algorithm, uint16_t qop, + const char* bodyDigest, char* result); + + // result is in mHashBuf + [[nodiscard]] nsresult DigestHash(const char* buf, uint32_t len, + uint16_t algorithm); + + [[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[SHA256_DIGEST_LENGTH]{0}; + + 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..dcc5307984 --- /dev/null +++ b/netwerk/protocol/http/nsHttpHandler.cpp @@ -0,0 +1,2812 @@ +/* -*- 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 "nsCORSListenerProxy.h" +#include "nsError.h" +#include "nsHttp.h" +#include "nsHttpHandler.h" +#include "nsHttpChannel.h" +#include "nsHTTPCompressConv.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/AppShutdown.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/Components.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 "nsIHttpActivityObserver.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/net/SocketProcessChild.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" + +#include + +#if defined(XP_UNIX) +# include +#endif + +#if defined(MOZ_WIDGET_GTK) +# include "mozilla/WidgetUtilsGtk.h" +#endif + +#if defined(XP_WIN) +# include +# include "mozilla/WindowsVersion.h" +#endif + +#if defined(XP_MACOSX) +# include +#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 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 + +#ifdef XP_UNIX +static bool IsRunningUnderUbuntuSnap() { +# if defined(MOZ_WIDGET_GTK) + if (!widget::IsRunningUnderSnap()) { + return false; + } + + char version[100]; + if (PR_GetSystemInfo(PR_SI_RELEASE_BUILD, version, sizeof(version)) == + PR_SUCCESS) { + if (strstr(version, "Ubuntu")) { + return true; + } + } +# endif + return false; +} +#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::CCPostLastCycleCollection); + } + 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_jxl_enabled()) { + mimeTypes.Append("image/jxl,"); + } + + mimeTypes.Append("image/webp,*/*"); + + 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() + : mIdleTimeout(PR_SecondsToInterval(10)), + mSpdyTimeout( + PR_SecondsToInterval(StaticPrefs::network_http_http2_timeout())), + mResponseTimeout(PR_SecondsToInterval(300)), + mImageAcceptHeader(ImageAcceptHeader()), + mDocumentAcceptHeader(DocumentAcceptHeader(ImageAcceptHeader())), + mLastUniqueID(NowInSeconds()), + mDebugObservations(false), + mEnableAltSvc(false), + mEnableAltSvcOE(false), + mEnableOriginExtension(false), + mSpdyPingThreshold(PR_SecondsToInterval( + StaticPrefs::network_http_http2_ping_threshold())), + mSpdyPingTimeout(PR_SecondsToInterval( + StaticPrefs::network_http_http2_ping_timeout())) { + 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); + } +} + +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, + "image.http.accept", + "image.avif.enabled", + "image.jxl.enabled", + nullptr, +}; + +nsresult nsHttpHandler::Init() { + nsresult rv; + + LOG(("nsHttpHandler::Init\n")); + MOZ_ASSERT(NS_IsMainThread()); + + // We should not create nsHttpHandler during shutdown, but we have some + // xpcshell tests doing this. + if (MOZ_UNLIKELY(AppShutdown::IsInOrBeyond(ShutdownPhase::AppShutdown) && + !PR_GetEnv("XPCSHELL_TEST_PROFILE_DIR"))) { + MOZ_DIAGNOSTIC_ASSERT(false, "Try to init HttpHandler after shutdown"); + return NS_ERROR_ILLEGAL_DURING_SHUTDOWN; + } + + 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); + std::bitset<3> usageOfHTTPSRRPrefs; + usageOfHTTPSRRPrefs[0] = StaticPrefs::network_dns_upgrade_with_https_rr(); + usageOfHTTPSRRPrefs[1] = StaticPrefs::network_dns_use_https_rr_as_altsvc(); + usageOfHTTPSRRPrefs[2] = StaticPrefs::network_dns_echconfig_enabled(); + Telemetry::ScalarSet(Telemetry::ScalarID::NETWORKING_HTTPS_RR_PREFS_USAGE, + static_cast(usageOfHTTPSRRPrefs.to_ulong())); + mActivityDistributor = components::HttpActivityDistributor::Service(); + + auto initQLogDir = [&]() { + if (!StaticPrefs::network_http_http3_enable_qlog()) { + return EmptyCString(); + } + + 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, + StaticPrefs::network_http_http3_enable()); + + 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); + } + + mMisc.AssignLiteral("rv:" MOZILLA_UAVERSION); + + // 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(false).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); + obsService->AddObserver(this, "network:socket-process-crashed", true); + + if (!IsNeckoChild()) { + obsService->AddObserver(this, "net:current-browser-id", 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]() { + RefPtr parent = + self->mConnMgr->AsHttpConnectionMgrParent(); + Unused << SocketProcessParent::GetSingleton() + ->SendPHttpConnectionMgrConstructor( + parent, + HttpHandlerInitArgs( + 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, bool aShouldResistFingerprinting) { + nsresult rv; + + // Add the "User-Agent" header + rv = request->SetHeader(nsHttp::User_Agent, + UserAgent(aShouldResistFingerprinting), 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 && + (!nsCRT::strcasecmp(enc, "gzip") || !nsCRT::strcasecmp(enc, "deflate") || + !nsCRT::strcasecmp(enc, "x-gzip") || + !nsCRT::strcasecmp(enc, "x-deflate"))) { + rv = true; + } + LOG(("nsHttpHandler::IsAceptableEncoding %s https=%d %d\n", enc, isSecure, + rv)); + return rv; +} + +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 [this=%p chan=%p event=\"%s\"]\n", this, + 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); + + PrepareForAntiTrackingRedirectHeuristic(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(bool aShouldResistFingerprinting) { + if (aShouldResistFingerprinting && !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 +// Hardcode the reported Windows version to 10.0. This way, Microsoft doesn't +// get to change Web compat-sensitive values without our veto. The compat- +// sensitivity keeps going up as 10.0 stays as the current value for longer +// and longer. If the system-reported version ever changes, we'll be able to +// take our time to evaluate the Web compat impact instead of having to +// scramble to react like happened with macOS changing from 10.x to 11.x. +# define OSCPU_WINDOWS "Windows NT 10.0" +# 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; + } + + // 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 + ); + +#ifdef XP_UNIX + if (IsRunningUnderUbuntuSnap()) { + mPlatform.AppendLiteral("; Ubuntu"); + } +#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. + nsAutoString androidVersion; + rv = infoService->GetPropertyAsAString(u"release_version"_ns, androidVersion); + if (NS_SUCCEEDED(rv)) { + mPlatform += " "; + mPlatform += NS_LossyConvertUTF16toASCII(androidVersion); + } + + // Add the `Mobile` or `TV` token when running on device. + 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 + + // Gather OS/CPU. +#if defined(XP_WIN) + +# if defined _M_X64 || defined _M_AMD64 + mOscpu.AssignLiteral(OSCPU_WIN64); +# elif defined(_ARM64_) + // Report ARM64 Windows 11+ as x86_64 and Windows 10 as x86. Windows 11+ + // supports x86_64 emulation, but Windows 10 only supports x86 emulation. + if (IsWin11OrLater()) { + mOscpu.AssignLiteral(OSCPU_WIN64); + } else { + mOscpu.AssignLiteral(OSCPU_WINDOWS); + } +# else + BOOL isWow64 = FALSE; + if (!IsWow64Process(GetCurrentProcess(), &isWow64)) { + isWow64 = FALSE; + } + if (isWow64) { + mOscpu.AssignLiteral(OSCPU_WIN64); + } else { + mOscpu.AssignLiteral(OSCPU_WINDOWS); + } +# endif + +#elif defined(XP_MACOSX) + mOscpu.AssignLiteral("Intel Mac OS X 10.15"); +#elif defined(XP_UNIX) + if (mozilla::StaticPrefs::network_http_useragent_freezeCpu()) { +# ifdef ANDROID + mOscpu.AssignLiteral("Linux armv81"); +# else + mOscpu.AssignLiteral("Linux x86_64"); +# endif + } else { + 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 + + 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) || !strcmp(pref, p)) +#define MULTI_PREF_CHANGED(p) \ + ((pref == nullptr) || !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("http2.timeout"))) { + mSpdyTimeout = PR_SecondsToInterval( + clamped(StaticPrefs::network_http_http2_timeout(), 1, 0xffff)); + } + + if (PREF_CHANGED(HTTP_PREF("http2.chunk-size"))) { + // keep this within http/2 ranges of 1 to 2^24-1 + mSpdySendingChunkSize = (uint32_t)clamped( + StaticPrefs::network_http_http2_chunk_size(), 1, 0xffffff); + } + + // The amount of idle seconds on a http2 connection before initiating a + // server ping. 0 will disable. + if (PREF_CHANGED(HTTP_PREF("http2.ping-threshold"))) { + mSpdyPingThreshold = PR_SecondsToInterval((uint16_t)clamped( + StaticPrefs::network_http_http2_ping_threshold(), 0, 0x7fffffff)); + } + + // The amount of seconds to wait for a http2 ping response before + // closing the session. + if (PREF_CHANGED(HTTP_PREF("http2.ping-timeout"))) { + mSpdyPingTimeout = PR_SecondsToInterval((uint16_t)clamped( + StaticPrefs::network_http_http2_ping_timeout(), 0, 0x7fffffff)); + } + + 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("http2.push-allowance"))) { + mSpdyPushAllowance = static_cast( + clamped(StaticPrefs::network_http_http2_push_allowance(), 1024, + static_cast(ASpdySession::kInitialRwin))); + } + + if (PREF_CHANGED(HTTP_PREF("http2.pull-allowance"))) { + mSpdyPullAllowance = static_cast(clamped( + StaticPrefs::network_http_http2_pull_allowance(), 1024, 0x7fffffff)); + } + + if (PREF_CHANGED(HTTP_PREF("http2.default-concurrent"))) { + mDefaultSpdyConcurrent = static_cast(std::max( + std::min(StaticPrefs::network_http_http2_default_concurrent(), + 9999), + 1)); + } + + // If http2.send-buffer-size is non-zero, the size to set the TCP + // sendbuffer to once the stream has surpassed this number of bytes uploaded + if (PREF_CHANGED(HTTP_PREF("http2.send-buffer-size"))) { + mSpdySendBufferSize = (uint32_t)clamped( + StaticPrefs::network_http_http2_send_buffer_size(), 1500, 0x7fffffff); + } + + // 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(HTTP_PREF("http2.default-hpack-buffer"))) { + mDefaultHpackBuffer = + StaticPrefs::network_http_http2_default_hpack_buffer(); + } + + 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.jxl.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) { + mAltSvcMappingTemptativeMap.InsertOrUpdate( + Substring(token, 0, index), + MakeUnique(Substring(token, index + 1))); + } + } + } + } + + 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); +} + +// Ensure that we've fetched the AcceptLanguages setting +/* static */ +void nsHttpHandler::PresetAcceptLanguages() { + if (!gHttpHandler) { + RefPtr handler = nsHttpHandler::GetInstance(); + Unused << handler.get(); + } + [[maybe_unused]] nsresult rv = gHttpHandler->SetAcceptLanguages(); +} + +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, + 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::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; + + 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(), + 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(false); + return NS_OK; +} + +NS_IMETHODIMP +nsHttpHandler::GetRfpUserAgent(nsACString& value) { + value = UserAgent(true); + 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); + } + } + + mActivityDistributor = nullptr; + } 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(); + } + nsCORSListenerProxy::ClearPrivateBrowsingCache(); + } 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)) { + 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-browser-id")) { + // The window id will be updated by HttpConnectionMgrParent. + if (XRE_IsParentProcess()) { + nsCOMPtr wrapper = do_QueryInterface(subject); + MOZ_RELEASE_ASSERT(wrapper); + + uint64_t id = 0; + wrapper->GetData(&id); + MOZ_ASSERT(id); + + static uint64_t sCurrentBrowserId = 0; + if (sCurrentBrowserId != id) { + sCurrentBrowserId = id; + if (mConnMgr) { + mConnMgr->UpdateCurrentBrowserId(sCurrentBrowserId); + } + } + } + } 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, "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(); + } else if (!strcmp(topic, "network:socket-process-crashed")) { + ShutdownConnectionManager(); + mConnMgr = nullptr; + Unused << InitConnectionMgr(); + } + + return NS_OK; +} + +// nsISpeculativeConnect + +nsresult nsHttpHandler::SpeculativeConnectInternal( + nsIURI* aURI, nsIPrincipal* aPrincipal, + Maybe&& aOriginAttributes, + nsIInterfaceRequestor* aCallbacks, bool anonymous) { + if (IsNeckoChild()) { + gNeckoChild->SendSpeculativeConnect( + aURI, aPrincipal, std::move(aOriginAttributes), anonymous); + return NS_OK; + } + + if (!mHandlerActive) return NS_OK; + + MOZ_ASSERT(NS_IsMainThread()); + + nsISiteSecurityService* sss = gHttpHandler->GetSSService(); + bool isStsHost = false; + if (!sss) return NS_OK; + + nsCOMPtr loadContext = do_GetInterface(aCallbacks); + OriginAttributes originAttributes; + // If the principal is given, we use the originAttributes from this + // principal. Otherwise, we use the originAttributes from the loadContext. + if (aOriginAttributes) { + originAttributes = std::move(aOriginAttributes.ref()); + } else if (aPrincipal) { + originAttributes = aPrincipal->OriginAttributesRef(); + StoragePrincipalHelper::UpdateOriginAttributesForNetworkState( + aURI, originAttributes); + } else if (loadContext) { + loadContext->GetOriginAttributes(originAttributes); + StoragePrincipalHelper::UpdateOriginAttributesForNetworkState( + aURI, originAttributes); + } + + nsCOMPtr clone; + if (NS_SUCCEEDED(sss->IsSecureURI(aURI, originAttributes, &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; + } + + 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); + + RefPtr ci = + new nsHttpConnectionInfo(host, port, ""_ns, username, nullptr, + originAttributes, aURI->SchemeIs("https")); + ci->SetAnonymous(anonymous); + if (originAttributes.mPrivateBrowsingId > 0) { + ci->SetPrivate(true); + } + + if (mDebugObservations) { + // this is basically used for test coverage of an otherwise 'hintable' + // feature + + nsCOMPtr obsService = services::GetObserverService(); + if (obsService) { + // This is used to test if the speculative connection has the right + // connection info. + nsPrintfCString debugHashKey("%s", ci->HashKey().get()); + obsService->NotifyObservers(nullptr, "speculative-connect-request", + NS_ConvertUTF8toUTF16(debugHashKey).get()); + for (auto* cp : + dom::ContentParent::AllProcesses(dom::ContentParent::eLive)) { + PNeckoParent* neckoParent = + SingleManagedOrNull(cp->ManagedPNeckoParent()); + if (!neckoParent) { + continue; + } + Unused << neckoParent->SendSpeculativeConnectRequest(); + } + } + } + + return SpeculativeConnect(ci, aCallbacks); +} + +NS_IMETHODIMP +nsHttpHandler::SpeculativeConnect(nsIURI* aURI, nsIPrincipal* aPrincipal, + nsIInterfaceRequestor* aCallbacks, + bool aAnonymous) { + return SpeculativeConnectInternal(aURI, aPrincipal, Nothing(), aCallbacks, + aAnonymous); +} + +NS_IMETHODIMP nsHttpHandler::SpeculativeConnectWithOriginAttributes( + nsIURI* aURI, JS::Handle aOriginAttributes, + nsIInterfaceRequestor* aCallbacks, bool aAnonymous, JSContext* aCx) { + OriginAttributes attrs; + if (!aOriginAttributes.isObject() || !attrs.Init(aCx, aOriginAttributes)) { + return NS_ERROR_INVALID_ARG; + } + + SpeculativeConnectWithOriginAttributesNative(aURI, std::move(attrs), + aCallbacks, aAnonymous); + return NS_OK; +} + +NS_IMETHODIMP_(void) +nsHttpHandler::SpeculativeConnectWithOriginAttributesNative( + nsIURI* aURI, OriginAttributes&& aOriginAttributes, + nsIInterfaceRequestor* aCallbacks, bool aAnonymous) { + Maybe originAttributes; + originAttributes.emplace(aOriginAttributes); + Unused << SpeculativeConnectInternal( + aURI, nullptr, std::move(originAttributes), aCallbacks, aAnonymous); +} + +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::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, 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); +} + +NS_IMETHODIMP +nsHttpHandler::ClearCORSPreflightCache() { + nsCORSListenerProxy::ClearCache(); + return NS_OK; +} + +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 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::ExcludeHttp2OrHttp3Internal( + const nsHttpConnectionInfo* ci) { + LOG(("nsHttpHandler::ExcludeHttp2OrHttp3Internal ci=%s", + ci->HashKey().get())); + // The excluded list needs to be stayed synced between parent process and + // socket process, so we send this information to the parent process here. + if (XRE_IsSocketProcess()) { + MOZ_ASSERT(OnSocketThread()); + + RefPtr cinfo = ci->Clone(); + NS_DispatchToMainThread(NS_NewRunnableFunction( + "nsHttpHandler::ExcludeHttp2OrHttp3Internal", + [cinfo{std::move(cinfo)}]() { + HttpConnectionInfoCloneArgs connInfoArgs; + nsHttpConnectionInfo::SerializeHttpConnectionInfo(cinfo, + connInfoArgs); + Unused << SocketProcessChild::GetSingleton()->SendExcludeHttp2OrHttp3( + connInfoArgs); + })); + } + + MOZ_ASSERT_IF(nsIOService::UseSocketProcess() && XRE_IsParentProcess(), + NS_IsMainThread()); + MOZ_ASSERT_IF(!nsIOService::UseSocketProcess(), OnSocketThread()); + + if (ci->IsHttp3()) { + if (!mExcludedHttp3Origins.Contains(ci->GetRoutedHost())) { + MutexAutoLock lock(mHttpExclusionLock); + mExcludedHttp3Origins.Insert(ci->GetRoutedHost()); + } + mConnMgr->ExcludeHttp3(ci); + } else { + if (!mExcludedHttp2Origins.Contains(ci->GetOrigin())) { + MutexAutoLock lock(mHttpExclusionLock); + mExcludedHttp2Origins.Insert(ci->GetOrigin()); + } + mConnMgr->ExcludeHttp2(ci); + } +} + +void nsHttpHandler::ExcludeHttp2(const nsHttpConnectionInfo* ci) { + ExcludeHttp2OrHttp3Internal(ci); +} + +bool nsHttpHandler::IsHttp2Excluded(const nsHttpConnectionInfo* ci) { + MutexAutoLock lock(mHttpExclusionLock); + return mExcludedHttp2Origins.Contains(ci->GetOrigin()); +} + +void nsHttpHandler::ExcludeHttp3(const nsHttpConnectionInfo* ci) { + MOZ_ASSERT(ci->IsHttp3()); + ExcludeHttp2OrHttp3Internal(ci); +} + +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::IsHttp3Enabled() { + static const uint32_t TLS3_PREF_VALUE = 4; + + return StaticPrefs::network_http_http3_enable() && + (StaticPrefs::security_tls_version_max() >= TLS3_PREF_VALUE); +} + +bool nsHttpHandler::IsHttp3VersionSupported(const nsACString& version) { + if (!StaticPrefs::network_http_http3_support_version1() && + version.EqualsLiteral("h3")) { + return false; + } + for (uint32_t i = 0; i < kHttp3VersionCount; i++) { + if (version.Equals(kHttp3Versions[i])) { + return true; + } + } + return false; +} + +bool nsHttpHandler::IsHttp3SupportedByServer( + nsHttpResponseHead* aResponseHead) { + if ((aResponseHead->Version() != HttpVersion::v2_0) || + (aResponseHead->Status() >= 500) || (aResponseHead->Status() == 421)) { + return false; + } + + nsAutoCString altSvc; + Unused << aResponseHead->GetHeader(nsHttp::Alternate_Service, altSvc); + if (altSvc.IsEmpty() || !nsHttp::IsReasonableHeaderValue(altSvc)) { + return false; + } + + for (uint32_t i = 0; i < kHttp3VersionCount; i++) { + nsAutoCString value(kHttp3Versions[i]); + value.Append("="_ns); + if (strstr(altSvc.get(), value.get())) { + 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, const ClassOfService& 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.InsertOrUpdate(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()); + 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 nsACString& aModelId) { + MOZ_ASSERT(XRE_IsSocketProcess()); + mDeviceModelId = aModelId; +} + +void nsHttpHandler::MaybeAddAltSvcForTesting( + nsIURI* aUri, const nsACString& aUsername, bool aPrivateBrowsing, + nsIInterfaceRequestor* aCallbacks, + const OriginAttributes& aOriginAttributes) { + if (!IsHttp3Enabled() || mAltSvcMappingTemptativeMap.IsEmpty()) { + return; + } + + bool isHttps = false; + if (NS_FAILED(aUri->SchemeIs("https", &isHttps)) || !isHttps) { + // Only set for 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, + aPrivateBrowsing, aCallbacks, nullptr, 0, aOriginAttributes, true); + } +} + +bool nsHttpHandler::UseHTTPSRRAsAltSvcEnabled() const { + return StaticPrefs::network_dns_use_https_rr_as_altsvc(); +} + +bool nsHttpHandler::EchConfigEnabled(bool aIsHttp3) const { + if (!aIsHttp3) { + return StaticPrefs::network_dns_echconfig_enabled(); + } + + return StaticPrefs::network_dns_echconfig_enabled() && + StaticPrefs::network_dns_http3_echconfig_enabled(); +} + +bool nsHttpHandler::FallbackToOriginIfConfigsAreECHAndAllFailed() const { + return StaticPrefs:: + network_dns_echconfig_fallback_to_origin_when_all_failed(); +} + +void nsHttpHandler::ExcludeHTTPSRRHost(const nsACString& aHost) { + MOZ_ASSERT(NS_IsMainThread()); + + mExcludedHostsForHTTPSRRUpgrade.Insert(aHost); +} + +bool nsHttpHandler::IsHostExcludedForHTTPSRR(const nsACString& aHost) { + MOZ_ASSERT(NS_IsMainThread()); + + return mExcludedHostsForHTTPSRRUpgrade.Contains(aHost); +} + +void nsHttpHandler::Exclude0RttTcp(const nsHttpConnectionInfo* ci) { + MOZ_ASSERT(OnSocketThread()); + + if (!StaticPrefs::network_http_early_data_disable_on_error() || + (mExcluded0RttTcpOrigins.Count() >= + StaticPrefs::network_http_early_data_max_error())) { + return; + } + + mExcluded0RttTcpOrigins.Insert(ci->GetOrigin()); +} + +bool nsHttpHandler::Is0RttTcpExcluded(const nsHttpConnectionInfo* ci) { + MOZ_ASSERT(OnSocketThread()); + if (!StaticPrefs::network_http_early_data_disable_on_error()) { + return false; + } + + if (mExcluded0RttTcpOrigins.Count() >= + StaticPrefs::network_http_early_data_max_error()) { + return true; + } + + return mExcluded0RttTcpOrigins.Contains(ci->GetOrigin()); +} + +bool nsHttpHandler::HttpActivityDistributorActivated() { + if (!mActivityDistributor) { + return false; + } + + return mActivityDistributor->Activated(); +} + +void nsHttpHandler::ObserveHttpActivityWithArgs( + const HttpActivityArgs& aArgs, uint32_t aActivityType, + uint32_t aActivitySubtype, PRTime aTimestamp, uint64_t aExtraSizeData, + const nsACString& aExtraStringData) { + if (!HttpActivityDistributorActivated()) { + return; + } + + if (aActivitySubtype == NS_HTTP_ACTIVITY_SUBTYPE_PROXY_RESPONSE_HEADER && + !mActivityDistributor->ObserveProxyResponseEnabled()) { + return; + } + + if (aActivityType == NS_ACTIVITY_TYPE_HTTP_CONNECTION && + !mActivityDistributor->ObserveConnectionEnabled()) { + return; + } + + Unused << mActivityDistributor->ObserveActivityWithArgs( + aArgs, aActivityType, aActivitySubtype, aTimestamp, aExtraSizeData, + aExtraStringData); +} + +} // namespace mozilla::net diff --git a/netwerk/protocol/http/nsHttpHandler.h b/netwerk/protocol/http/nsHttpHandler.h new file mode 100644 index 0000000000..cf745347ee --- /dev/null +++ b/netwerk/protocol/http/nsHttpHandler.h @@ -0,0 +1,918 @@ +/* -*- 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 "nsTHashMap.h" +#include "nsTHashSet.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" +#include "nsSocketTransportService2.h" + +class nsIHttpActivityDistributor; +class nsIHttpUpgradeListener; +class nsIPrefBranch; +class nsICancelable; +class nsICookieService; +class nsIIOService; +class nsIRequestContextService; +class nsISiteSecurityService; +class nsIStreamConverterService; + +namespace mozilla::net { + +class ATokenBucketEvent; +class EventTokenBucket; +class Tickler; +class nsHttpConnection; +class nsHttpConnectionInfo; +class HttpBaseChannel; +class HttpHandlerInitArgs; +class HttpTransactionShell; +class AltSvcMapping; +class DNSUtils; +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, + bool aShouldResistFingerprinting); + [[nodiscard]] nsresult AddConnectionHeader(nsHttpRequestHead*, uint32_t caps); + bool IsAcceptableEncoding(const char* encoding, bool isSecure); + + const nsCString& UserAgent(bool aShouldResistFingerprinting); + + 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; + } + + 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 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; + } + + 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; + } + + // 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, + const ClassOfService& 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 | EchConfigEnabled()); + } + + [[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, + const OriginAttributes& originAttributes, bool aHttp2Allowed, + bool aHttp3Allowed) { + return mAltSvcCache->GetAltServiceMapping( + scheme, host, port, pb, originAttributes, aHttp2Allowed, aHttp3Allowed); + } + + // + // The HTTP handler caches pointers to specific XPCOM services, and + // provides the following helper routines for accessing those services: + // + [[nodiscard]] nsresult GetIOService(nsIIOService** result); + 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); + } + + // Same as OnModifyRequest but before cookie headers are written. + void OnModifyRequestBeforeCookies(nsIHttpChannel* chan) { + NotifyObservers(chan, NS_HTTP_ON_MODIFY_REQUEST_BEFORE_COOKIES_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; } + + static bool IsHttp3Enabled(); + bool IsHttp3VersionSupported(const nsACString& version); + + static bool IsHttp3SupportedByServer(nsHttpResponseHead* aResponseHead); + 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 GetLastActiveTabLoadOptimizationHit(); + void SetLastActiveTabLoadOptimizationHit(TimeStamp const& when); + bool IsBeforeLastActiveTabLoadOptimization(TimeStamp const& when); + + HttpTrafficAnalyzer* GetHttpTrafficAnalyzer(); + + bool GetThroughCaptivePortal() { return mThroughCaptivePortal; } + + nsresult CompleteUpgrade(HttpTransactionShell* aTrans, + nsIHttpUpgradeListener* aUpgradeListener); + + nsresult DoShiftReloadConnectionCleanupWithConnInfo( + nsHttpConnectionInfo* aCI); + + void MaybeAddAltSvcForTesting(nsIURI* aUri, const nsACString& aUsername, + bool aPrivateBrowsing, + nsIInterfaceRequestor* aCallbacks, + const OriginAttributes& aOriginAttributes); + + bool UseHTTPSRRAsAltSvcEnabled() const; + + bool EchConfigEnabled(bool aIsHttp3 = false) 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; + + // So we can ensure that this is done during process preallocation to + // avoid first-use overhead + static void PresetAcceptLanguages(); + + bool HttpActivityDistributorActivated(); + void ObserveHttpActivityWithArgs(const HttpActivityArgs& aArgs, + uint32_t aActivityType, + uint32_t aActivitySubtype, PRTime aTimestamp, + uint64_t aExtraSizeData, + const nsACString& aExtraStringData); + + 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); + + friend class SocketProcessChild; + void SetHttpHandlerInitArgs(const HttpHandlerInitArgs& aArgs); + void SetDeviceModelId(const nsACString& aModelId); + + // We only allow DNSUtils and TRRServiceChannel itself to create + // TRRServiceChannel. + friend class TRRServiceChannel; + friend class DNSUtils; + 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 mCookieService; + nsMainThreadPtrHandle mSSService; + + // the authentication credentials cache + nsHttpAuthCache mAuthCache; + nsHttpAuthCache mPrivateAuthCache; + + // the connection manager + RefPtr mConnMgr; + + UniquePtr mAltSvcCache; + + // + // prefs + // + + enum HttpVersion mHttpVersion { HttpVersion::v1_1 }; + enum HttpVersion mProxyHttpVersion { HttpVersion::v1_1 }; + uint32_t mCapabilities{NS_HTTP_ALLOW_KEEPALIVE}; + + bool mFastFallbackToIPv4{false}; + PRIntervalTime mIdleTimeout; + PRIntervalTime mSpdyTimeout; + PRIntervalTime mResponseTimeout; + Atomic mResponseTimeoutEnabled{false}; + uint32_t mNetworkChangedTimeout{5000}; // milliseconds + uint16_t mMaxRequestAttempts{6}; + uint16_t mMaxRequestDelay{10}; + uint16_t mIdleSynTimeout{250}; + uint16_t mFallbackSynTimeout{5}; // seconds + + bool mH2MandatorySuiteEnabled{false}; + uint16_t mMaxUrgentExcessiveConns{3}; + uint16_t mMaxConnections{24}; + uint8_t mMaxPersistentConnectionsPerServer{2}; + uint8_t mMaxPersistentConnectionsPerProxy{4}; + + bool mThrottleEnabled{true}; + uint32_t mThrottleVersion{2}; + uint32_t mThrottleSuspendFor{3000}; + uint32_t mThrottleResumeFor{200}; + uint32_t mThrottleReadLimit{8000}; + uint32_t mThrottleReadInterval{500}; + uint32_t mThrottleHoldTime{600}; + uint32_t mThrottleMaxTime{3000}; + + int32_t mSendWindowSize{1024}; + + bool mUrgentStartEnabled{true}; + bool mTailBlockingEnabled{true}; + uint32_t mTailDelayQuantum{600}; + uint32_t mTailDelayQuantumAfterDCL{100}; + uint32_t mTailDelayMax{6000}; + uint32_t mTailTotalMax{0}; + + uint8_t mRedirectionLimit{10}; + + bool mBeConservativeForProxy{true}; + + // 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{1}; + + uint8_t mQoSBits{0x00}; + + bool mEnforceAssocReq{false}; + + nsCString mImageAcceptHeader; + nsCString mDocumentAcceptHeader; + + nsCString mAcceptLanguages; + nsCString mHttpAcceptEncodings; + nsCString mHttpsAcceptEncodings; + + nsCString mDefaultSocketType; + + // cache support + uint32_t mLastUniqueID; + Atomic mSessionStartTime{0}; + + // useragent components + nsCString mLegacyAppName{"Mozilla"}; + nsCString mLegacyAppVersion{"5.0"}; + nsCString mPlatform; + nsCString mOscpu; + nsCString mMisc; + nsCString mProduct{"Gecko"}; + nsCString mProductSub; + nsCString mAppName; + nsCString mAppVersion; + nsCString mCompatFirefox; + bool mCompatFirefoxEnabled{false}; + nsCString mCompatDevice; + nsCString mDeviceModelId; + + nsCString mUserAgent; + nsCString mSpoofedUserAgent; + nsCString mUserAgentOverride; + bool mUserAgentIsDirty{true}; // true if mUserAgent should be rebuilt + bool mAcceptLanguagesIsDirty{true}; + + bool mPromptTempRedirect{true}; + + // Persistent HTTPS caching flag + bool mEnablePersistentHttpsCaching{false}; + + // for broadcasting safe hint; + bool mSafeHintEnabled{false}; + bool mParentalControlEnabled{false}; + + // true in between init and shutdown states + Atomic mHandlerActive{false}; + + // The value of 'hidden' network.http.debug-observations : 1; + uint32_t mDebugObservations : 1; + + uint32_t mEnableAltSvc : 1; + uint32_t mEnableAltSvcOE : 1; + uint32_t mEnableOriginExtension : 1; + + // Try to use SPDY features instead of HTTP/1.1 over SSL + SpdyInformation mSpdyInfo; + + uint32_t mSpdySendingChunkSize{ASpdySession::kSendingChunkSize}; + uint32_t mSpdySendBufferSize{ASpdySession::kTCPSendBufferSize}; + uint32_t mSpdyPushAllowance{ + ASpdySession::kInitialPushAllowance}; // match default pref + uint32_t mSpdyPullAllowance{ASpdySession::kInitialRwin}; + uint32_t mDefaultSpdyConcurrent{ASpdySession::kDefaultMaxConcurrent}; + PRIntervalTime mSpdyPingThreshold; + PRIntervalTime mSpdyPingTimeout; + + // The maximum amount of time to wait for socket transport to be + // established. In milliseconds. + uint32_t mConnectTimeout{90000}; + + // The maximum amount of time to wait for a tls handshake to be + // established. In milliseconds. + uint32_t mTLSHandshakeTimeout{30000}; + + // The maximum number of current global half open sockets allowable + // when starting a new speculative connection. + uint32_t mParallelSpeculativeConnectLimit{6}; + + // For Rate Pacing of HTTP/1 requests through a netwerk/base/EventTokenBucket + // Active requests <= *MinParallelism are not subject to the rate pacing + bool mRequestTokenBucketEnabled{true}; + uint16_t mRequestTokenBucketMinParallelism{6}; + uint32_t mRequestTokenBucketHz{100}; // EventTokenBucket HZ + uint32_t mRequestTokenBucketBurst{32}; // EventTokenBucket Burst + + // Whether or not to block requests for non head js/css items (e.g. media) + // while those elements load. + bool mCriticalRequestPrioritization{true}; + + // TCP Keepalive configuration values. + + // True if TCP keepalive is enabled for short-lived conns. + bool mTCPKeepaliveShortLivedEnabled{false}; + // Time (secs) indicating how long a conn is considered short-lived. + int32_t mTCPKeepaliveShortLivedTimeS{60}; + // Time (secs) before first keepalive probe; between successful probes. + int32_t mTCPKeepaliveShortLivedIdleTimeS{10}; + + // True if TCP keepalive is enabled for long-lived conns. + bool mTCPKeepaliveLongLivedEnabled{false}; + // Time (secs) before first keepalive probe; between successful probes. + int32_t mTCPKeepaliveLongLivedIdleTimeS{600}; + + // if true, generate NS_ERROR_PARTIAL_TRANSFER for h1 responses with + // incorrect content lengths or malformed chunked encodings + FrameCheckLevel mEnforceH1Framing{FRAMECHECK_BARELY}; + + nsCOMPtr mRequestContextService; + + // The default size (in bytes) of the HPACK decompressor table. + uint32_t mDefaultHpackBuffer{4096}; + + // Http3 parameters + Atomic mQpackTableSize{4096}; + // uint16_t is enough here, but Atomic only supports uint32_t or uint64_t. + Atomic mHttp3MaxBlockedStreams{10}; + + nsCString mHttp3QlogDir; + + // The max size (in bytes) for received Http response header. + uint32_t mMaxHttpResponseHeaderSize{393216}; + + // The ratio for dispatching transactions from the focused window. + float mFocusedWindowTransactionRatio{0.9f}; + + // If true, the transactions from active tab will be dispatched first. + bool mActiveTabPriority{true}; + + 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, + Maybe&& aOriginAttributes, + nsIInterfaceRequestor* aCallbacks, bool anonymous); + void ExcludeHttp2OrHttp3Internal(const nsHttpConnectionInfo* ci); + + // State for generating channelIds + uint32_t mProcessId{0}; + Atomic mNextChannelId{1}; + + // 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{ + "nsHttpConnectionMgr::LastActiveTabLoadOptimization"}; + TimeStamp mLastActiveTabLoadOptimizationHit; + + Mutex mHttpExclusionLock MOZ_UNANNOTATED{"nsHttpHandler::HttpExclusion"}; + + 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); + void Exclude0RttTcp(const nsHttpConnectionInfo* ci); + [[nodiscard]] bool Is0RttTcpExcluded(const nsHttpConnectionInfo* ci); + + void ExcludeHTTPSRRHost(const nsACString& aHost); + [[nodiscard]] bool IsHostExcludedForHTTPSRR(const nsACString& aHost); + + private: + nsTHashSet mExcludedHttp2Origins; + nsTHashSet mExcludedHttp3Origins; + nsTHashSet mExcluded0RttTcpOrigins; + // A set of hosts that we should not upgrade to HTTPS with HTTPS RR. + nsTHashSet mExcludedHostsForHTTPSRRUpgrade; + + Atomic mThroughCaptivePortal{false}; + + // The mapping of channel id and the weak pointer of nsHttpChannel. + nsTHashMap 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; + + nsCOMPtr mActivityDistributor; +}; + +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_IMETHOD SpeculativeConnect(nsIURI* aURI, nsIPrincipal* aPrincipal, + nsIInterfaceRequestor* aCallbacks, + bool aAnonymous) override { + return gHttpHandler->SpeculativeConnect(aURI, aPrincipal, aCallbacks, + aAnonymous); + } + + NS_IMETHOD SpeculativeConnectWithOriginAttributes( + nsIURI* aURI, JS::Handle originAttributes, + nsIInterfaceRequestor* aCallbacks, bool aAnonymous, + JSContext* cx) override { + return gHttpHandler->SpeculativeConnectWithOriginAttributes( + aURI, originAttributes, aCallbacks, aAnonymous, cx); + } + + NS_IMETHOD_(void) + SpeculativeConnectWithOriginAttributesNative( + nsIURI* aURI, mozilla::OriginAttributes&& originAttributes, + nsIInterfaceRequestor* aCallbacks, bool aAnonymous) override { + gHttpHandler->SpeculativeConnectWithOriginAttributesNative( + aURI, std::move(originAttributes), aCallbacks, aAnonymous); + } + + 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 mozilla::net + +#endif // nsHttpHandler_h__ diff --git a/netwerk/protocol/http/nsHttpHeaderArray.cpp b/netwerk/protocol/http/nsHttpHeaderArray.cpp new file mode 100644 index 0000000000..8da421e3fa --- /dev/null +++ b/netwerk/protocol/http/nsHttpHeaderArray.cpp @@ -0,0 +1,475 @@ +/* -*- 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(headerName); + if (!header) { + NS_WARNING("failed to resolve atom"); + return NS_ERROR_NOT_AVAILABLE; + } + return SetHeader(header, headerName, value, merge, variety); +} + +nsresult nsHttpHeaderArray::SetHeader( + const nsHttpAtom& header, const nsACString& value, bool merge, + nsHttpHeaderArray::HeaderVariety variety) { + return SetHeader(header, ""_ns, value, merge, variety); +} + +nsresult nsHttpHeaderArray::SetHeader( + const nsHttpAtom& header, const nsACString& headerName, + const nsACString& value, bool merge, + nsHttpHeaderArray::HeaderVariety variety) { + MOZ_ASSERT( + (variety == eVarietyResponse) || (variety == eVarietyRequestDefault) || + (variety == eVarietyRequestOverride) || + (variety == eVarietyRequestEnforceDefault), + "Net original headers can only be set using SetHeader_internal()."); + + nsEntry* entry = nullptr; + int32_t index = LookupEntry(header, &entry); + + // If an empty value is received and we aren't merging headers discard it + if (value.IsEmpty() && header != nsHttp::X_Frame_Options) { + if (!merge && entry) { + if (entry->variety == eVarietyResponseNetOriginalAndResponse) { + MOZ_ASSERT(variety == eVarietyResponse); + entry->variety = eVarietyResponseNetOriginal; + } else { + mHeaders.RemoveElementAt(index); + } + } + return NS_OK; + } + + MOZ_ASSERT((variety == eVarietyRequestEnforceDefault) || + (!entry || variety != eVarietyRequestDefault), + "Cannot set default entry which overrides existing entry!"); + + // Set the variety to default if we are enforcing it. + if (variety == eVarietyRequestEnforceDefault) { + variety = eVarietyRequestDefault; + } + if (!entry) { + return SetHeader_internal(header, headerName, value, variety); + } + if (merge && !IsSingletonHeader(header)) { + return MergeHeader(header, entry, value, variety); + } + 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( + const 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(headerName); + 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; + } + if (entry) { + MOZ_ASSERT(variety == eVarietyResponse); + entry->variety = eVarietyResponseNetOriginal; + } + + return SetHeader_internal(header, headerName, ""_ns, variety); +} + +nsresult nsHttpHeaderArray::SetHeaderFromNet( + const 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); + } + 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; + } + if (!IsIgnoreMultipleHeader(header)) { + // Multiple instances of non-mergeable header received from network + // - ignore if same value + if (header == nsHttp::Content_Length) { + // Content length header needs special handling. + // For e.g. for CL all the below headers evaluates to value of X + // Content-Length: X + // Content-Length: X, X, X + // Content-Length: X \n\r Content-Length: X + // remove duplicate values from the header-values for comparison + + nsAutoCString headerValue; + RemoveDuplicateHeaderValues(value, headerValue); + + nsAutoCString entryValue; + RemoveDuplicateHeaderValues(entry->value, entryValue); + if (entryValue != headerValue) { + // reply may be corrupt/hacked (ex: CLRF injection attacks) + return NS_ERROR_CORRUPTED_CONTENT; + } + } else if (!entry->value.Equals(value)) { // compare remaining headers + 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( + const 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 != + CopyableTArray::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 != + CopyableTArray::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(const 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(const nsHttpAtom& header) const { + const nsEntry* entry = nullptr; + LookupEntry(header, &entry); + return entry ? entry->value.get() : nullptr; +} + +nsresult nsHttpHeaderArray::GetHeader(const 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(const 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; + } + + const nsCString& hdr = entry.headerNameOriginal.IsEmpty() + ? entry.header.val() + : 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(const 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; + } + + const nsCString& hdr = entry.headerNameOriginal.IsEmpty() + ? entry.header.val() + : 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.val()); + } 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.val()); + } 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..e9aa0a8e7e --- /dev/null +++ b/netwerk/protocol/http/nsHttpHeaderArray.h @@ -0,0 +1,334 @@ +/* -*- 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(const 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, + eVarietyRequestEnforceDefault, + // 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(const nsHttpAtom& header, + const nsACString& value, bool merge, + HeaderVariety variety); + [[nodiscard]] nsresult SetHeader(const 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(const nsHttpAtom& header, + const nsACString& headerNameOriginal, + const nsACString& value, + bool response); + + [[nodiscard]] nsresult SetResponseHeaderFromCache( + const nsHttpAtom& header, const nsACString& headerNameOriginal, + const nsACString& value, HeaderVariety variety); + + [[nodiscard]] nsresult GetHeader(const nsHttpAtom& header, + nsACString& result) const; + [[nodiscard]] nsresult GetOriginalHeader(const nsHttpAtom& aHeader, + nsIHttpHeaderVisitor* aVisitor); + void ClearHeader(const nsHttpAtom& h); + + // Find the location of the given header value, or null if none exists. + const char* FindHeaderValue(const 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(const nsHttpAtom& header, const char* value) const { + return FindHeaderValue(header, value) != nullptr; + } + + bool HasHeader(const 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, the header name, and the + // header value + [[nodiscard]] static nsresult ParseHeaderLine( + const nsACString& line, nsHttpAtom* hdr = 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(const nsHttpAtom& header, const nsEntry**) const; + int32_t LookupEntry(const nsHttpAtom& header, nsEntry**); + [[nodiscard]] nsresult MergeHeader(const nsHttpAtom& header, nsEntry* entry, + const nsACString& value, + HeaderVariety variety); + [[nodiscard]] nsresult SetHeader_internal(const nsHttpAtom& header, + const nsACString& headerName, + const nsACString& value, + HeaderVariety variety); + + // Header cannot be merged: only one value possible + bool IsSingletonHeader(const nsHttpAtom& header); + // Header cannot be merged, and subsequent values should be ignored + bool IsIgnoreMultipleHeader(const 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(const nsHttpAtom& header); + + // Removes duplicate header values entries + // Will return unmodified header value if the header values contains + // non-duplicate entries + void RemoveDuplicateHeaderValues(const nsACString& aHeaderValue, + nsACString& aResult); + + // 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(const 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(const 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(const 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 || + header == nsHttp::GlobalPrivacyControl || + // 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( + const 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( + const nsHttpAtom& header, nsEntry* entry, const nsACString& value, + nsHttpHeaderArray::HeaderVariety variety) { + // merge of empty header = no-op + if (value.IsEmpty() && header != nsHttp::X_Frame_Options) { + return NS_OK; + } + + // x-frame-options having an empty header value still has an effect so we make + // sure that we retain encountering it + nsCString newValue = entry->value; + if (!newValue.IsEmpty() || header == nsHttp::X_Frame_Options) { + // 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( + const 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; +} + +inline void nsHttpHeaderArray::RemoveDuplicateHeaderValues( + const nsACString& aHeaderValue, nsACString& aResult) { + mozilla::Maybe result; + for (const nsACString& token : + nsCCharSeparatedTokenizer(aHeaderValue, ',').ToRange()) { + if (result.isNothing()) { + // assign the first value + result.emplace(token); + continue; + } + if (*result != token) { + // non-identical header values. Do not change the header values + result.reset(); + break; + } + } + + if (result.isSome()) { + aResult = *result; + } else { + // either header values do not have multiple values or + // has unequal multiple values + // for both the cases restore the original header value + aResult = aHeaderValue; + } +} + +} // 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..0d8f5f083a --- /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 "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 "nsCRT.h" +#include "nsNetUtil.h" +#include "nsIChannel.h" +#include "nsUnicharUtils.h" +#include "mozilla/net/HttpAuthUtils.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/net/DNS.h" +#include "mozilla/StaticPrefs_browser.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; + 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('.') && !HostIsIPLiteral(host); +} + +// 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; + } + + if (!StaticPrefs::browser_privatebrowsing_autostart()) { + 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 nsACString& 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 (challenge.Equals("NTLM"_ns, nsCaseInsensitiveCStringComparator)) { + nsCOMPtr module; + +#ifdef MOZ_AUTH_EXTENSION + // 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; + } +#endif + + // 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 nsACString& challenge, + bool isProxyAuth, const nsAString& domain, const nsAString& username, + const nsAString& password, nsISupports* sessionState, + nsISupports* continuationState, nsICancelable** aCancellable) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsHttpNTLMAuth::GenerateCredentials( + nsIHttpAuthenticableChannel* authChannel, const nsACString& aChallenge, + bool isProxyAuth, const nsAString& domain, const nsAString& user, + const nsAString& pass, nsISupports** sessionState, + nsISupports** continuationState, uint32_t* aFlags, nsACString& creds) + +{ + LOG(("nsHttpNTLMAuth::GenerateCredentials\n")); + + creds.Truncate(); + *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.IsEmpty() || pass.IsEmpty()) *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 (aChallenge.Equals("NTLM"_ns, nsCaseInsensitiveCStringComparator)) { + // 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, reqFlags, domain, user, pass); + if (NS_FAILED(rv)) return rv; + + inBufLen = 0; + inBuf = nullptr; +// 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. +// Extended protection update is just for Linux and Windows machines. +#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 securityInfo; + rv = channel->GetSecurityInfo(getter_AddRefs(securityInfo)); + if (NS_FAILED(rv)) return rv; + + if (mUseNative && securityInfo) { + nsCOMPtr cert; + rv = securityInfo->GetServerCert(getter_AddRefs(cert)); + if (NS_FAILED(rv)) return rv; + + if (cert) { + 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(); + } + } +#endif + } else { + // decode challenge; skip past "NTLM " to the start of the base64 + // encoded data. + if (aChallenge.Length() < 6) { + return NS_ERROR_UNEXPECTED; // bogus challenge + } + + // strip off any padding (see bug 230351) + nsDependentCSubstring challenge(aChallenge, 5); + uint32_t len = challenge.Length(); + while (len > 0 && challenge[len - 1] == '=') { + len--; + } + + // decode into the input secbuffer + rv = Base64Decode(challenge.BeginReading(), 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 { + nsAutoCString encoded; + (void)Base64Encode(nsDependentCSubstring((char*)outBuf, outBufLen), + encoded); + creds = nsPrintfCString("NTLM %s", encoded.get()); + } + + // 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..a1fafc4a10 --- /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() = default; + + 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{false}; + + 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..0ce7a14f8a --- /dev/null +++ b/netwerk/protocol/http/nsHttpRequestHead.cpp @@ -0,0 +1,367 @@ +/* -*- 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() { MOZ_COUNT_CTOR(nsHttpRequestHead); } + +nsHttpRequestHead::nsHttpRequestHead(const nsHttpRequestHead& aRequestHead) { + MOZ_COUNT_CTOR(nsHttpRequestHead); + 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(nsHttpRequestHead&& aRequestHead) { + MOZ_COUNT_CTOR(nsHttpRequestHead); + nsHttpRequestHead& other = aRequestHead; + RecursiveMutexAutoLock monitor(other.mRecursiveMutex); + + mHeaders = std::move(other.mHeaders); + mMethod = std::move(other.mMethod); + mVersion = std::move(other.mVersion); + mRequestURI = std::move(other.mRequestURI); + mPath = std::move(other.mPath); + mOrigin = std::move(other.mOrigin); + mParsedMethod = std::move(other.mParsedMethod); + mHTTPS = std::move(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(const 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( + const 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(const nsHttpAtom& h, nsACString& v) { + v.Truncate(); + RecursiveMutexAutoLock mon(mRecursiveMutex); + return mHeaders.GetHeader(h, v); +} + +nsresult nsHttpRequestHead::ClearHeader(const 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(const nsHttpAtom& h) { + RecursiveMutexAutoLock mon(mRecursiveMutex); + return mHeaders.HasHeader(h); +} + +bool nsHttpRequestHead::HasHeaderValue(const nsHttpAtom& h, const char* v) { + RecursiveMutexAutoLock mon(mRecursiveMutex); + return mHeaders.HasHeaderValue(h, v); +} + +nsresult nsHttpRequestHead::SetHeaderOnce(const 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..e308790bd4 --- /dev/null +++ b/netwerk/protocol/http/nsHttpRequestHead.h @@ -0,0 +1,155 @@ +/* -*- 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&& 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 MOZ_REQUIRES(mRecursiveMutex); + void Enter() const MOZ_CAPABILITY_ACQUIRE(mRecursiveMutex) { + mRecursiveMutex.Lock(); + } + void Exit() const MOZ_CAPABILITY_RELEASE(mRecursiveMutex) { + 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(const nsHttpAtom& h, const nsACString& v, + bool m = false); + [[nodiscard]] nsresult SetHeader(const nsHttpAtom& h, const nsACString& v, + bool m, + nsHttpHeaderArray::HeaderVariety variety); + [[nodiscard]] nsresult SetEmptyHeader(const nsACString& h); + [[nodiscard]] nsresult GetHeader(const nsHttpAtom& h, nsACString& v); + + [[nodiscard]] nsresult ClearHeader(const nsHttpAtom& h); + void ClearHeaders(); + + bool HasHeaderValue(const nsHttpAtom& h, const char* v); + // This function returns true if header is set even if it is an empty + // header. + bool HasHeader(const nsHttpAtom& h); + void Flatten(nsACString&, bool pruneProxyHeaders = false); + + // Don't allow duplicate values + [[nodiscard]] nsresult SetHeaderOnce(const 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 MOZ_GUARDED_BY(mRecursiveMutex); + nsCString mMethod MOZ_GUARDED_BY(mRecursiveMutex){"GET"_ns}; + HttpVersion mVersion MOZ_GUARDED_BY(mRecursiveMutex){HttpVersion::v1_1}; + + // mRequestURI and mPath are strings instead of an nsIURI + // because this is used off the main thread + // TODO: nsIURI is thread-safe now, should be fixable. + nsCString mRequestURI MOZ_GUARDED_BY(mRecursiveMutex); + nsCString mPath MOZ_GUARDED_BY(mRecursiveMutex); + + nsCString mOrigin MOZ_GUARDED_BY(mRecursiveMutex); + ParsedMethodType mParsedMethod MOZ_GUARDED_BY(mRecursiveMutex){kMethod_Get}; + bool mHTTPS MOZ_GUARDED_BY(mRecursiveMutex){false}; + + // We are using RecursiveMutex instead of a Mutex because VisitHeader + // function calls nsIHttpHeaderVisitor::VisitHeader while under lock. + mutable RecursiveMutex mRecursiveMutex MOZ_UNANNOTATED{ + "nsHttpRequestHead.mRecursiveMutex"}; + + // During VisitHeader we sould not allow call to SetHeader. + bool mInVisitHeaders MOZ_GUARDED_BY(mRecursiveMutex){false}; + + 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..18f7beed3a --- /dev/null +++ b/netwerk/protocol/http/nsHttpResponseHead.cpp @@ -0,0 +1,1220 @@ +/* -*- 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/dom/MimeType.h" +#include "mozilla/StaticPrefs_network.h" +#include "mozilla/TextUtils.h" +#include "mozilla/Unused.h" +#include "nsHttpResponseHead.h" +#include "nsIHttpHeaderVisitor.h" +#include "nsPrintfCString.h" +#include "prtime.h" +#include "nsCRT.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) { + 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(hdr); + if (!atom.get()) { + NS_WARNING("failed to resolve atom"); + return NS_ERROR_NOT_AVAILABLE; + } + + return SetHeader_locked(atom, hdr, val, merge); +} + +nsresult nsHttpResponseHead::SetHeader(const 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(const 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(const nsHttpAtom& h, + nsACString& v) const { + v.Truncate(); + RecursiveMutexAutoLock monitor(mRecursiveMutex); + return mHeaders.GetHeader(h, v); +} + +void nsHttpResponseHead::ClearHeader(const nsHttpAtom& h) { + RecursiveMutexAutoLock monitor(mRecursiveMutex); + mHeaders.ClearHeader(h); +} + +void nsHttpResponseHead::ClearHeaders() { + RecursiveMutexAutoLock monitor(mRecursiveMutex); + mHeaders.Clear(); +} + +bool nsHttpResponseHead::HasHeaderValue(const nsHttpAtom& h, const char* v) { + RecursiveMutexAutoLock monitor(mRecursiveMutex); + return mHeaders.HasHeaderValue(h, v); +} + +bool nsHttpResponseHead::HasHeader(const 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 ;-) + + const char* p = 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 = 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 = 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. + + net_GetDefaultStatusTextForCode(mStatus, mStatusText); +} + +nsresult nsHttpResponseHead::ParseStatusLine(const nsACString& line) { + RecursiveMutexAutoLock monitor(mRecursiveMutex); + return ParseStatusLine_locked(line); +} + +nsresult 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(); + + // HTTP-Version + ParseVersion(start); + + int32_t index = line.FindChar(' '); + + if ((mVersion == HttpVersion::v0_9) || (index == -1)) { + mStatus = 200; + AssignDefaultStatusText(); + } else if (StaticPrefs::network_http_strict_response_status_line_parsing()) { + // Status-Code: all ASCII digits after any whitespace + const char* p = start + index + 1; + while (p < end && NS_IsHTTPWhitespace(*p)) ++p; + if (p == end || !mozilla::IsAsciiDigit(*p)) { + return NS_ERROR_PARSING_HTTP_STATUS_LINE; + } + const char* codeStart = p; + while (p < end && mozilla::IsAsciiDigit(*p)) ++p; + + // Only accept 3 digits (including all leading zeros) + // Also if next char isn't whitespace, fail (ie, code is 0x2) + if (p - codeStart > 3 || (p < end && !NS_IsHTTPWhitespace(*p))) { + return NS_ERROR_PARSING_HTTP_STATUS_LINE; + } + + // At this point the code is between 0 and 999 inclusive + nsDependentCSubstring strCode(codeStart, p - codeStart); + nsresult rv; + mStatus = strCode.ToInteger(&rv); + if (NS_FAILED(rv)) { + return NS_ERROR_PARSING_HTTP_STATUS_LINE; + } + + // Reason-Phrase: whatever remains after any whitespace (even empty) + while (p < end && NS_IsHTTPWhitespace(*p)) ++p; + if (p != end) { + mStatusText = nsDependentCSubstring(p, end - p); + } + } else { + // Status-Code + const char* p = start + 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())); + return NS_OK; +} + +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; + } + + // reject the header if there are 0x00 bytes in the value. + // (see https://github.com/httpwg/http-core/issues/215 for details). + if (StaticPrefs::network_http_reject_NULs_in_response_header_values() && + val.FindChar('\0') >= 0) { + return NS_ERROR_DOM_INVALID_HEADER_VALUE; + } + + 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) { + rv = ParseResponseContentLength(val); + if (rv == NS_ERROR_ILLEGAL_VALUE) { + LOG(("illegal content-length! %s\n", val.get())); + return rv; + } + + if (rv == NS_ERROR_NOT_AVAILABLE) { + LOG(("content-length value ignored! %s\n", val.get())); + } + + } else if (hdr == nsHttp::Content_Type) { + if (StaticPrefs::network_standard_content_type_parsing_response_headers() && + CMimeType::Parse(val, mContentType, mContentCharset)) { + } else { + bool dummy; + net_ParseContentType(val, mContentType, mContentCharset, &dummy); + } + LOG(("ParseContentType [input=%s, type=%s, charset=%s]\n", val.get(), + mContentType.get(), mContentCharset.get())); + } 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; + } + + return now <= stallValidUntil.value(); +} + +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; + + if (!aOther->mHeaders.PeekHeaderAt(i, header, headerNameOriginal)) { + continue; + } + + nsAutoCString val; + if (NS_FAILED(aOther->GetHeader(header, 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.get())); + } else { + LOG(("new response header [%s: %s]\n", header.get(), val.get())); + + // overwrite the current header value with the new value... + DebugOnly rv = + SetHeader_locked(header, headerNameOriginal, 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(const 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 + MOZ_NO_THREAD_SAFETY_ANALYSIS { + 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 (nsCRT::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::ParseResponseContentLength( + const nsACString& aHeaderStr) { + int64_t contentLength = 0; + // Ref: https://fetch.spec.whatwg.org/#content-length-header + // Step 1. Let values be the result of getting, decoding, and splitting + // `Content - Length` from headers. + // Step 1 is done by the caller + // Step 2. If values is null, then return null. + if (aHeaderStr.IsEmpty()) { + return NS_ERROR_NOT_AVAILABLE; + } + + // Step 3 Let candidateValue be null. + Maybe candidateValue; + // Step 4 For each value of values + for (const nsACString& token : + nsCCharSeparatedTokenizerTemplate< + NS_IsAsciiWhitespace, nsTokenizerFlags::IncludeEmptyTokenAtEnd>( + aHeaderStr, ',') + .ToRange()) { + // Step 4.1 If candidateValue is null, then set candidateValue to value. + if (candidateValue.isNothing()) { + candidateValue.emplace(token); + } + // Step 4.2 Otherwise, if value is not candidateValue, return failure. + if (candidateValue.value() != token) { + return NS_ERROR_ILLEGAL_VALUE; + } + } + // Step 5 If candidateValue is the empty string or has a code point that is + // not an ASCII digit, then return null. + if (candidateValue.isNothing()) { + return NS_ERROR_NOT_AVAILABLE; + } + + // Step 6 Return candidateValue, interpreted as decimal number contentLength + const char* end = nullptr; + if (!net::nsHttp::ParseInt64(candidateValue->get(), &end, &contentLength)) { + return NS_ERROR_NOT_AVAILABLE; + } + + if (*end != '\0') { + // a number was parsed by ParseInt64 but candidateValue contains non-numeric + // characters + return NS_ERROR_NOT_AVAILABLE; + } + + mContentLength = contentLength; + return NS_OK; +} + +nsresult nsHttpResponseHead::VisitHeaders( + nsIHttpHeaderVisitor* visitor, nsHttpHeaderArray::VisitorFilter filter) { + RecursiveMutexAutoLock monitor(mRecursiveMutex); + mInVisitHeaders = true; + nsresult rv = mHeaders.VisitHeaders(visitor, filter); + mInVisitHeaders = false; + return rv; +} + +namespace { +class ContentTypeOptionsVisitor final : public nsIHttpHeaderVisitor { + public: + NS_DECL_ISUPPORTS + + ContentTypeOptionsVisitor() = default; + + NS_IMETHOD + VisitHeader(const nsACString& aHeader, const nsACString& aValue) override { + if (!mHeaderPresent) { + mHeaderPresent = true; + } else { + // multiple XCTO headers in response, merge them + mContentTypeOptionsHeader.Append(", "_ns); + } + mContentTypeOptionsHeader.Append(aValue); + return NS_OK; + } + + void GetMergedHeader(nsACString& aValue) { + aValue = mContentTypeOptionsHeader; + } + + private: + ~ContentTypeOptionsVisitor() = default; + bool mHeaderPresent{false}; + nsAutoCString mContentTypeOptionsHeader; +}; + +NS_IMPL_ISUPPORTS(ContentTypeOptionsVisitor, nsIHttpHeaderVisitor) +} // namespace + +nsresult nsHttpResponseHead::GetOriginalHeader(const 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; + // We need to fetch original headers and manually merge them because empty + // header values are not retrieved with GetHeader. Ref - Bug 1819642 + RefPtr visitor = new ContentTypeOptionsVisitor(); + Unused << GetOriginalHeader(nsHttp::X_Content_Type_Options, visitor); + visitor->GetMergedHeader(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..9caf92b3e1 --- /dev/null +++ b/netwerk/protocol/http/nsHttpResponseHead.h @@ -0,0 +1,239 @@ +/* -*- 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() = default; + + nsHttpResponseHead(const nsHttpResponseHead& aOther); + nsHttpResponseHead& operator=(const nsHttpResponseHead& aOther); + + void Enter() const MOZ_CAPABILITY_ACQUIRE(mRecursiveMutex) { + mRecursiveMutex.Lock(); + } + void Exit() const MOZ_CAPABILITY_RELEASE(mRecursiveMutex) { + mRecursiveMutex.Unlock(); + } + void AssertMutexOwned() const { mRecursiveMutex.AssertCurrentThreadIn(); } + + 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(const nsHttpAtom& h, const nsACString& v, + bool m = false); + [[nodiscard]] nsresult GetHeader(const nsHttpAtom& h, nsACString& v) const; + void ClearHeader(const nsHttpAtom& h); + void ClearHeaders(); + bool HasHeaderValue(const nsHttpAtom& h, const char* v); + bool HasHeader(const 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. + nsresult 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* aOther); + + // reset the response head to it's initial state + void Reset(); + + [[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(const nsHttpAtom& aHeader, + nsIHttpHeaderVisitor* aVisitor); + + bool HasContentType() const; + bool HasContentCharset(); + bool GetContentTypeOptionsHeader(nsACString& aOutput); + + private: + [[nodiscard]] nsresult SetHeader_locked(const nsHttpAtom& atom, + const nsACString& h, + const nsACString& v, bool m = false) + MOZ_REQUIRES(mRecursiveMutex); + void AssignDefaultStatusText() MOZ_REQUIRES(mRecursiveMutex); + void ParseVersion(const char*) MOZ_REQUIRES(mRecursiveMutex); + void ParseCacheControl(const char*) MOZ_REQUIRES(mRecursiveMutex); + void ParsePragma(const char*) MOZ_REQUIRES(mRecursiveMutex); + // Parses a content-length header-value as described in + // https://fetch.spec.whatwg.org/#content-length-header + nsresult ParseResponseContentLength(const nsACString& aHeaderStr) + MOZ_REQUIRES(mRecursiveMutex); + + nsresult ParseStatusLine_locked(const nsACString& line) + MOZ_REQUIRES(mRecursiveMutex); + [[nodiscard]] nsresult ParseHeaderLine_locked(const nsACString& line, + bool originalFromNetHeaders) + MOZ_REQUIRES(mRecursiveMutex); + + // these return failure if the header does not exist. + [[nodiscard]] nsresult ParseDateHeader(const nsHttpAtom& header, + uint32_t* result) const + MOZ_REQUIRES(mRecursiveMutex); + [[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); + + bool ExpiresInPast_locked() const MOZ_REQUIRES(mRecursiveMutex); + [[nodiscard]] nsresult GetAgeValue_locked(uint32_t* result) const + MOZ_REQUIRES(mRecursiveMutex); + [[nodiscard]] nsresult GetExpiresValue_locked(uint32_t* result) const + MOZ_REQUIRES(mRecursiveMutex); + [[nodiscard]] nsresult GetMaxAgeValue_locked(uint32_t* result) const + MOZ_REQUIRES(mRecursiveMutex); + [[nodiscard]] nsresult GetStaleWhileRevalidateValue_locked( + uint32_t* result) const MOZ_REQUIRES(mRecursiveMutex); + + [[nodiscard]] nsresult GetDateValue_locked(uint32_t* result) const + MOZ_REQUIRES(mRecursiveMutex) { + return ParseDateHeader(nsHttp::Date, result); + } + + [[nodiscard]] nsresult GetLastModifiedValue_locked(uint32_t* result) const + MOZ_REQUIRES(mRecursiveMutex) { + return ParseDateHeader(nsHttp::Last_Modified, result); + } + + bool NoCache_locked() const MOZ_REQUIRES(mRecursiveMutex) { + // 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 MOZ_GUARDED_BY(mRecursiveMutex); + HttpVersion mVersion MOZ_GUARDED_BY(mRecursiveMutex){HttpVersion::v1_1}; + uint16_t mStatus MOZ_GUARDED_BY(mRecursiveMutex){200}; + nsCString mStatusText MOZ_GUARDED_BY(mRecursiveMutex); + int64_t mContentLength MOZ_GUARDED_BY(mRecursiveMutex){-1}; + nsCString mContentType MOZ_GUARDED_BY(mRecursiveMutex); + nsCString mContentCharset MOZ_GUARDED_BY(mRecursiveMutex); + bool mHasCacheControl MOZ_GUARDED_BY(mRecursiveMutex){false}; + bool mCacheControlPublic MOZ_GUARDED_BY(mRecursiveMutex){false}; + bool mCacheControlPrivate MOZ_GUARDED_BY(mRecursiveMutex){false}; + bool mCacheControlNoStore MOZ_GUARDED_BY(mRecursiveMutex){false}; + bool mCacheControlNoCache MOZ_GUARDED_BY(mRecursiveMutex){false}; + bool mCacheControlImmutable MOZ_GUARDED_BY(mRecursiveMutex){false}; + bool mCacheControlStaleWhileRevalidateSet MOZ_GUARDED_BY(mRecursiveMutex){ + false}; + uint32_t mCacheControlStaleWhileRevalidate MOZ_GUARDED_BY(mRecursiveMutex){0}; + bool mCacheControlMaxAgeSet MOZ_GUARDED_BY(mRecursiveMutex){false}; + uint32_t mCacheControlMaxAge MOZ_GUARDED_BY(mRecursiveMutex){0}; + bool mPragmaNoCache MOZ_GUARDED_BY(mRecursiveMutex){false}; + + // We are using RecursiveMutex instead of a Mutex because VisitHeader + // function calls nsIHttpHeaderVisitor::VisitHeader while under lock. + mutable RecursiveMutex mRecursiveMutex{"nsHttpResponseHead.mRecursiveMutex"}; + // During VisitHeader we sould not allow call to SetHeader. + bool mInVisitHeaders MOZ_GUARDED_BY(mRecursiveMutex){false}; + + 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..580ea35841 --- /dev/null +++ b/netwerk/protocol/http/nsHttpTransaction.cpp @@ -0,0 +1,3628 @@ +/* -*- 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 "base/basictypes.h" +#include "mozilla/Components.h" +#include "mozilla/glean/GleanMetrics.h" +#include "mozilla/net/SSLTokensCache.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/Tokenizer.h" +#include "mozilla/StaticPrefs_network.h" +#include "MockHttpAuth.h" +#include "nsCRT.h" +#include "nsComponentManagerUtils.h" // do_CreateInstance +#include "nsHttpBasicAuth.h" +#include "nsHttpChannel.h" +#include "nsHttpChunkedDecoder.h" +#include "nsHttpDigestAuth.h" +#include "nsHttpHandler.h" +#include "nsHttpNTLMAuth.h" +#ifdef MOZ_AUTH_EXTENSION +# include "nsHttpNegotiateAuth.h" +#endif +#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 "nsITLSSocketControl.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" + +//----------------------------------------------------------------------------- + +// 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::net { + +//----------------------------------------------------------------------------- +// nsHttpTransaction +//----------------------------------------------------------------------------- + +nsHttpTransaction::nsHttpTransaction() { + 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 (mClassOfServiceFlags & + (nsIClassOfService::Throttleable | nsIClassOfService::DontThrottle | + nsIClassOfService::Leader | nsIClassOfService::Unblocked)) == + nsIClassOfService::Throttleable; +} + +void nsHttpTransaction::SetClassOfService(ClassOfService cos) { + if (mClosed) { + return; + } + + bool wasThrottling = EligibleForThrottling(); + mClassOfServiceFlags = cos.Flags(); + mClassOfServiceIncremental = cos.Incremental(); + 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 ReleaseOnSocketThread final : public mozilla::Runnable { + public: + explicit ReleaseOnSocketThread(nsTArray>&& aDoomed) + : Runnable("ReleaseOnSocketThread"), mDoomed(std::move(aDoomed)) {} + + NS_IMETHOD + Run() override { + mDoomed.Clear(); + return NS_OK; + } + + void Dispatch() { + nsCOMPtr sts = + do_GetService("@mozilla.org/network/socket-transport-service;1"); + Unused << sts->Dispatch(this, nsIEventTarget::DISPATCH_NORMAL); + } + + private: + virtual ~ReleaseOnSocketThread() = default; + + nsTArray> mDoomed; +}; + +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 + { + MutexAutoLock lock(mLock); + mCallbacks = nullptr; + } + + mEarlyHintObserver = nullptr; + + delete mResponseHead; + delete mChunkedDecoder; + ReleaseBlockingTransaction(); + + nsTArray> arrayToRelease; + if (mConnection) { + arrayToRelease.AppendElement(mConnection.forget()); + } + + if (!arrayToRelease.IsEmpty()) { + RefPtr r = + new ReleaseOnSocketThread(std::move(arrayToRelease)); + 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 browserId, HttpTrafficCategory trafficCategory, + nsIRequestContext* requestContext, ClassOfService 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); + mBrowserId = browserId; + + mTrafficCategory = trafficCategory; + + 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; + // eventsink is a nsHttpChannel when we expect "103 Early Hints" responses. + // We expect it in document requests and not e.g. in TRR requests. + mEarlyHintObserver = do_QueryInterface(eventsink); + + 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 (gHttpHandler->HttpActivityDistributorActivated()) { + nsCString requestBuf(mReqHeaderBuf); + NS_DispatchToMainThread(NS_NewRunnableFunction( + "ObserveHttpActivityWithArgs", [channelId(mChannelId), requestBuf]() { + if (!gHttpHandler) { + return; + } + gHttpHandler->ObserveHttpActivityWithArgs( + HttpActivityArgs(channelId), + NS_HTTP_ACTIVITY_TYPE_HTTP_TRANSACTION, + NS_HTTP_ACTIVITY_SUBTYPE_REQUEST_HEADER, PR_Now(), 0, requestBuf); + })); + } + + // 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(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 + NS_NewPipe2(getter_AddRefs(mPipeIn), getter_AddRefs(mPipeOut), true, true, + nsIOService::gDefaultSegmentSize, + nsIOService::gDefaultSegmentCount); + + if (transWithPushedStream && aPushedStreamId) { + RefPtr trans = + transWithPushedStream->AsHttpTransaction(); + MOZ_ASSERT(trans); + mPushedStream = trans->TakePushedStreamById(aPushedStreamId); + } + + bool forceUseHTTPSRR = StaticPrefs::network_dns_force_use_https_rr(); + if ((gHttpHandler->UseHTTPSRRAsAltSvcEnabled() && + !(mCaps & NS_HTTP_DISALLOW_HTTPS_RR)) || + forceUseHTTPSRR) { + nsCOMPtr target; + Unused << gHttpHandler->GetSocketThreadTarget(getter_AddRefs(target)); + if (target) { + if (StaticPrefs::network_dns_force_waiting_https_rr() || + StaticPrefs::network_dns_echconfig_enabled() || forceUseHTTPSRR) { + mCaps |= NS_HTTP_FORCE_WAIT_HTTP_RR; + } + + mResolver = new HTTPSRecordResolver(this); + nsCOMPtr dnsRequest; + rv = mResolver->FetchHTTPSRRInternal(target, getter_AddRefs(dnsRequest)); + if (NS_SUCCEEDED(rv)) { + mHTTPSSVCReceivedStage = HTTPSSVC_NOT_PRESENT; + } + + { + MutexAutoLock lock(mLock); + mDNSRequest.swap(dnsRequest); + if (NS_FAILED(rv)) { + MakeDontWaitHTTPSRR(); + } + } + } + } + + RefPtr httpChannel = do_QueryObject(eventsink); + RefPtr listener = + httpChannel ? httpChannel->GetWebTransportSessionEventListener() + : nullptr; + if (listener) { + mWebTransportSessionEventListener = std::move(listener); + } + + return NS_OK; +} + +static inline void CreateAndStartTimer(nsCOMPtr& aTimer, + nsITimerCallback* aCallback, + uint32_t aTimeout) { + MOZ_DIAGNOSTIC_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( + const nsACString& aConnectionHashKey) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + { + MutexAutoLock lock(mLock); + mHashKeyOfConnectionEntry.Assign(aConnectionHashKey); + } + + // Don't create mHttp3BackupTimer if HTTPS RR is in play. + if (mConnInfo->IsHttp3() && !mOrigConnInfo && !mConnInfo->GetWebTransport()) { + // 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; + } +} + +UniquePtr nsHttpTransaction::TakeResponseHead() { + MOZ_ASSERT(!mResponseHeadTaken, "TakeResponseHead called 2x"); + + // Lock TakeResponseHead() against main thread + MutexAutoLock lock(mLock); + + 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(mLock); + + 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) { + if (mConnection) { + MutexAutoLock lock(mLock); + mConnection->GetSelfAddr(&mSelfAddr); + mConnection->GetPeerAddr(&mPeerAddr); + mResolvedByTRR = mConnection->ResolvedByTRR(); + mEffectiveTRRMode = mConnection->EffectiveTRRMode(); + mTRRSkipReason = mConnection->TRRSkipReason(); + mEchConfigUsed = mConnection->GetEchConfigUsed(); + } + } + + // 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; + } + } 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. + + // upon STATUS_WAITING_FOR; report request body sent + if ((mHasRequestBody) && (status == NS_NET_STATUS_WAITING_FOR)) { + gHttpHandler->ObserveHttpActivityWithArgs( + HttpActivityArgs(mChannelId), NS_HTTP_ACTIVITY_TYPE_HTTP_TRANSACTION, + NS_HTTP_ACTIVITY_SUBTYPE_REQUEST_BODY_SENT, PR_Now(), 0, ""_ns); + } + + // report the status and progress + gHttpHandler->ObserveHttpActivityWithArgs( + HttpActivityArgs(mChannelId), NS_HTTP_ACTIVITY_TYPE_SOCKET_TRANSPORT, + static_cast(status), PR_Now(), progress, ""_ns); + + // 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 tellable = do_QueryInterface(mRequestStream); + if (!tellable) { + LOG1( + ("nsHttpTransaction::OnTransportStatus %p " + "SENDING_TO without tellable request stream\n", + this)); + MOZ_ASSERT( + !mRequestStream, + "mRequestStream should be tellable as it was wrapped in " + "nsBufferedInputStream, which provides the tellable interface even " + "when wrapping non-tellable streams."); + progress = 0; + } else { + int64_t prog = 0; + tellable->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)) { + trans->MaybeRefreshSecurityInfo(); + 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; + MaybeRefreshSecurityInfo(); + } + + mDeferredSendProgress = false; + mReader = reader; + nsresult rv = + mRequestStream->ReadSegments(ReadRequestSegment, this, count, countRead); + mReader = nullptr; + + if (m0RTTInProgress && (mEarlyDataDisposition == EARLY_NONE) && + NS_SUCCEEDED(rv) && (*countRead > 0)) { + LOG(("mEarlyDataDisposition = EARLY_SENT")); + mEarlyDataDisposition = EARLY_SENT; + } + + if (mDeferredSendProgress && mConnection) { + // 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)) { + trans->MaybeRefreshSecurityInfo(); + 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 (mClassOfServiceFlags & 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 (!(mClassOfServiceFlags & 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()->CurrentBrowserId() != mBrowserId) { + 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; + } + + mIDToStreamMap.WithEntryHandle(stream->StreamID(), [&](auto&& entry) { + MOZ_ASSERT(!entry); + entry.OrInsert(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, const nsACString& aFailedAlpn, + 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; + + bool unused; + nsTArray> records; + Unused << mHTTPSSVCRecord->GetAllRecordsWithEchConfig( + mCaps & NS_HTTP_DISALLOW_SPDY, noHttp3, &aAllRecordsHaveEchConfig, + &unused, 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); + nsAutoCString alpn; + nsresult rv = record->GetSelectedAlpn(alpn); + + if (name == aFailedDomainName) { + // If the record has no alpn or the alpn is already tried, we skip this + // record. + if (NS_FAILED(rv) || alpn == aFailedAlpn) { + 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 && aEchConfigUsed) { + nsAutoCString echConfig; + Unused << fastFallbackRecord->GetEchConfig(echConfig); + if (echConfig.IsEmpty()) { + fastFallbackRecord = nullptr; + } + } + + if (!fastFallbackRecord) { + if (aEchConfigUsed) { + LOG( + ("nsHttpTransaction::PrepareFastFallbackConnInfo [this=%p] no record " + "can be used", + this)); + return nullptr; + } + + if (mOrigConnInfo->IsHttp3()) { + mOrigConnInfo->CloneAsDirectRoute(getter_AddRefs(fallbackConnInfo)); + } else { + fallbackConnInfo = mOrigConnInfo; + } + return fallbackConnInfo.forget(); + } + + 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->IsHttp3()) && + !failedConnInfo->GetEchConfig().IsEmpty(); + + if (mFastFallbackTriggered) { + mFastFallbackTriggered = false; + MOZ_ASSERT(mBackupConnInfo); + mConnInfo.swap(mBackupConnInfo); + return; + } + + auto useOrigConnInfoToRetry = [&]() { + mOrigConnInfo.swap(mConnInfo); + if (mConnInfo->IsHttp3() && + ((mCaps & NS_HTTP_DISALLOW_HTTP3) || + gHttpHandler->IsHttp3Excluded(mConnInfo->GetRoutedHost().IsEmpty() + ? mConnInfo->GetOrigin() + : mConnInfo->GetRoutedHost()))) { + RefPtr ci; + mConnInfo->CloneAsDirectRoute(getter_AddRefs(ci)); + mConnInfo = ci; + } + }; + + if (!echConfigUsed) { + LOG((" echConfig is not used, fallback to origin conn info")); + useOrigConnInfoToRetry(); + return; + } + + Telemetry::HistogramID id = Telemetry::TRANSACTION_ECH_RETRY_OTHERS_COUNT; + auto updateCount = MakeScopeExit([&] { + auto entry = mEchRetryCounterMap.Lookup(id); + MOZ_ASSERT(entry, "table not initialized"); + if (entry) { + *entry += 1; + } + }); + + 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 socketControl; + if (mConnection) { + mConnection->GetTLSSocketControl(getter_AddRefs(socketControl)); + } + 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(), + failedConnInfo->GetNPNToken(), + allRecordsHaveEchConfig)) { + LOG( + (" Can't find other records with echConfig, " + "allRecordsHaveEchConfig=%d", + allRecordsHaveEchConfig)); + if (gHttpHandler->FallbackToOriginIfConfigsAreECHAndAllFailed() || + !allRecordsHaveEchConfig) { + useOrigConnInfoToRetry(); + } + return; + } + } else { + LOG((" No available records to retry")); + if (gHttpHandler->FallbackToOriginIfConfigsAreECHAndAllFailed()) { + useOrigConnInfoToRetry(); + } + return; + } + } + + if (LOG5_ENABLED()) { + LOG(("SvcDomainName to retry: [")); + for (const auto& r : mRecordsForRetry) { + nsAutoCString name; + r->GetName(name); + nsAutoCString alpn; + r->GetSelectedAlpn(alpn); + LOG((" name=%s alpn=%s", name.get(), alpn.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); + } +} + +bool nsHttpTransaction::ShouldRestartOn0RttError(nsresult reason) { + LOG( + ("nsHttpTransaction::ShouldRestartOn0RttError [this=%p, " + "mEarlyDataWasAvailable=%d error=%" PRIx32 "]\n", + this, mEarlyDataWasAvailable, static_cast(reason))); + return StaticPrefs::network_http_early_data_disable_on_error() && + mEarlyDataWasAvailable && SecurityErrorThatMayNeedRestart(reason); +} + +static void MaybeRemoveSSLToken(nsITransportSecurityInfo* aSecurityInfo) { + if (!StaticPrefs:: + network_http_remove_resumption_token_when_early_data_failed()) { + return; + } + if (!aSecurityInfo) { + return; + } + nsAutoCString key; + aSecurityInfo->GetPeerId(key); + nsresult rv = SSLTokensCache::RemoveAll(key); + LOG(("RemoveSSLToken [key=%s, rv=%" PRIx32 "]", key.get(), + static_cast(rv))); +} + +void nsHttpTransaction::Close(nsresult reason) { + LOG(("nsHttpTransaction::Close [this=%p reason=%" PRIx32 "]\n", this, + static_cast(reason))); + + { + MutexAutoLock lock(mLock); + mEarlyHintObserver = nullptr; + mWebTransportSessionEventListener = nullptr; + } + + 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; + } + + NotifyTransactionObserver(reason); + + if (mTokenBucketCancel) { + mTokenBucketCancel->Cancel(reason); + mTokenBucketCancel = nullptr; + } + + // report the reponse is complete if not already reported + if (!mResponseIsComplete) { + gHttpHandler->ObserveHttpActivityWithArgs( + HttpActivityArgs(mChannelId), NS_HTTP_ACTIVITY_TYPE_HTTP_TRANSACTION, + NS_HTTP_ACTIVITY_SUBTYPE_RESPONSE_COMPLETE, PR_Now(), + static_cast(mContentRead), ""_ns); + } + + // report that this transaction is closing + gHttpHandler->ObserveHttpActivityWithArgs( + HttpActivityArgs(mChannelId), NS_HTTP_ACTIVITY_TYPE_HTTP_TRANSACTION, + NS_HTTP_ACTIVITY_SUBTYPE_TRANSACTION_CLOSE, PR_Now(), 0, ""_ns); + + // 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; + if (!mConnected) { + MaybeRefreshSecurityInfo(); + } + } + mConnected = false; + + // When mDoNotRemoveAltSvc is true, this means we want to keep the AltSvc in + // in the conncetion info. In this case, let's not apply HTTPS RR retry logic + // to make sure this transaction can be restarted with the same conncetion + // info. + bool shouldRestartTransactionForHTTPSRR = + mOrigConnInfo && AllowedErrorForHTTPSRRFallback(reason) && + !mDoNotRemoveAltSvc; + + // + // 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) || + ShouldRestartOn0RttError(reason) || + shouldRestartTransactionForHTTPSRR) && + (!(mCaps & NS_HTTP_STICKY_CONNECTION) || + (mCaps & NS_HTTP_CONNECTION_RESTARTABLE) || + (mEarlyDataDisposition == EARLY_425))) { + if (mForceRestart) { + SetRestartReason(TRANSACTION_RESTART_FORCED); + if (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")); + return; + } + } + + mDoNotTryEarlyData = true; + + // 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. + shouldRestartTransactionForHTTPSRR &= !reallySentData; + + if (reason == + psm::GetXPCOMFromNSSError(SSL_ERROR_DOWNGRADE_WITH_EARLY_DATA) || + reason == psm::GetXPCOMFromNSSError(SSL_ERROR_PROTOCOL_VERSION_ALERT) || + (!mReceivedData && ((mRequestHead && mRequestHead->IsSafeMethod()) || + !reallySentData || connReused)) || + shouldRestartTransactionForHTTPSRR) { + if (shouldRestartTransactionForHTTPSRR) { + 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 (shouldRestartTransactionForHTTPSRR) { + auto toRestartReason = + [](nsresult aStatus) -> TRANSACTION_RESTART_REASON { + if (aStatus == NS_ERROR_NET_RESET) { + return TRANSACTION_RESTART_HTTPS_RR_NET_RESET; + } + if (aStatus == NS_ERROR_CONNECTION_REFUSED) { + return TRANSACTION_RESTART_HTTPS_RR_CONNECTION_REFUSED; + } + if (aStatus == NS_ERROR_UNKNOWN_HOST) { + return TRANSACTION_RESTART_HTTPS_RR_UNKNOWN_HOST; + } + if (aStatus == NS_ERROR_NET_TIMEOUT) { + return TRANSACTION_RESTART_HTTPS_RR_NET_TIMEOUT; + } + if (psm::IsNSSErrorCode(-1 * NS_ERROR_GET_CODE(aStatus))) { + return TRANSACTION_RESTART_HTTPS_RR_SEC_ERROR; + } + MOZ_ASSERT_UNREACHABLE("Unexpected reason"); + return TRANSACTION_RESTART_OTHERS; + }; + SetRestartReason(toRestartReason(reason)); + } else if (!reallySentData) { + SetRestartReason(TRANSACTION_RESTART_NO_DATA_SENT); + } else if (reason == psm::GetXPCOMFromNSSError( + SSL_ERROR_DOWNGRADE_WITH_EARLY_DATA)) { + SetRestartReason(TRANSACTION_RESTART_DOWNGRADE_WITH_EARLY_DATA); + } else if (reason == + psm::GetXPCOMFromNSSError(SSL_ERROR_PROTOCOL_VERSION_ALERT)) { + SetRestartReason(TRANSACTION_RESTART_PROTOCOL_VERSION_ALERT); + } + // 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())) { + 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); + } + } + } + + Telemetry::Accumulate(Telemetry::HTTP_TRANSACTION_RESTART_REASON, + mRestartReason); + + 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\n"; + uint32_t unused = 0; + // If we have a partial line already, we actually need two \ns to finish + // the headers section. + Unused << ParseHead(data, mLineBuf.IsEmpty() ? 1 : 2, &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()); + } + + // Accumulate download throughput telemetry + const int64_t TELEMETRY_DOWNLOAD_SIZE_GREATER_THAN_10MB = + (int64_t)10 * (int64_t)(1 << 20); + if ((mContentRead > TELEMETRY_DOWNLOAD_SIZE_GREATER_THAN_10MB) && + !timings.requestStart.IsNull() && !timings.responseEnd.IsNull()) { + TimeDuration elapsed = timings.responseEnd - timings.requestStart; + double megabits = static_cast(mContentRead) * 8.0 / 1000000.0; + uint32_t mpbs = static_cast(megabits / elapsed.ToSeconds()); + + switch (mHttpVersion) { + case HttpVersion::v1_0: + case HttpVersion::v1_1: + glean::networking::http_1_download_throughput.AccumulateSamples( + {mpbs}); + break; + case HttpVersion::v2_0: + glean::networking::http_2_download_throughput.AccumulateSamples( + {mpbs}); + break; + case HttpVersion::v3_0: + glean::networking::http_3_download_throughput.AccumulateSamples( + {mpbs}); + break; + default: + break; + } + } + } + + 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; + } + + if (isHttp2or3 && + reason == psm::GetXPCOMFromNSSError(SSL_ERROR_PROTOCOL_VERSION_ALERT)) { + // Change reason to NS_ERROR_ABORT, so we avoid showing a missleading + // error page tthat TLS1.0 is disabled. H2 or H3 is used here so the + // TLS version is not a problem. + reason = NS_ERROR_ABORT; + } + mStatus = reason; + mTransactionDone = true; // forcibly flag the transaction as complete + mClosed = true; + if (mResolver) { + mResolver->Close(); + mResolver = nullptr; + } + ReleaseBlockingTransaction(); + + // release some resources that we no longer need + mRequestStream = nullptr; + mReqHeaderBuf.Truncate(); + mLineBuf.Truncate(); + if (mChunkedDecoder) { + delete mChunkedDecoder; + mChunkedDecoder = nullptr; + } + + for (const auto& entry : mEchRetryCounterMap) { + Telemetry::Accumulate(static_cast(entry.GetKey()), + entry.GetData()); + } + + // 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 +//----------------------------------------------------------------------------- + +static inline void RemoveAlternateServiceUsedHeader( + nsHttpRequestHead* aRequestHead) { + if (aRequestHead) { + DebugOnly rv = + aRequestHead->SetHeader(nsHttp::Alternate_Service_Used, "0"_ns); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } +} + +void nsHttpTransaction::SetRestartReason(TRANSACTION_RESTART_REASON aReason) { + if (mRestartReason == TRANSACTION_RESTART_NONE) { + mRestartReason = aReason; + } +} + +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)); + + 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); + + if (mDoNotTryEarlyData) { + MutexAutoLock lock(mLock); + MaybeRemoveSSLToken(mSecurityInfo); + } + + // 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() || mConnInfo->IsHttp3()) && + !mDontRetryWithDirectRoute) { + RefPtr ci; + mConnInfo->CloneAsDirectRoute(getter_AddRefs(ci)); + mConnInfo = ci; + RemoveAlternateServiceUsedHeader(mRequestHead); + } + + // Reset mDoNotRemoveAltSvc for the next try. + mDoNotRemoveAltSvc = false; + mEarlyDataWasAvailable = false; + mRestarted = true; + + // If we weren't trying to do 'proper' ECH, disable ECH GREASE when retrying. + if (mConnInfo->GetEchConfig().IsEmpty() && + StaticPrefs::security_tls_ech_disable_grease_on_fallback()) { + mCaps |= NS_HTTP_DISALLOW_ECH; + } + + mCaps |= NS_HTTP_IS_RETRY; + + // Use TRANSACTION_RESTART_OTHERS as a catch-all. + SetRestartReason(TRANSACTION_RESTART_OTHERS); + + if (!mDoNotResetIPFamilyPreference) { + // Reset the IP family preferences, so the new connection can try to use + // another IPv4 or IPv6 address. + gHttpHandler->ConnMgr()->ResetIPFamilyPreference(mConnInfo); + } + + 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 (nsCRT::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 (nsCRT::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 (nsCRT::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 && + (nsCRT::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 && + (nsCRT::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 && + (nsCRT::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) { + rv = mResponseHead->ParseStatusLine(line); + if (NS_SUCCEEDED(rv)) { + 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 == 103) { + nsCString linkHeader; + nsresult rv = mResponseHead->GetHeader(nsHttp::Link, linkHeader); + + nsCString referrerPolicy; + Unused << mResponseHead->GetHeader(nsHttp::Referrer_Policy, + referrerPolicy); + + if (NS_SUCCEEDED(rv) && !linkHeader.IsEmpty()) { + nsCString cspHeader; + Unused << mResponseHead->GetHeader(nsHttp::Content_Security_Policy, + cspHeader); + + nsCOMPtr earlyHint; + { + MutexAutoLock lock(mLock); + earlyHint = mEarlyHintObserver; + } + if (earlyHint) { + DebugOnly rv = NS_DispatchToMainThread( + NS_NewRunnableFunction( + "nsIEarlyHintObserver->EarlyHint", + [obs{std::move(earlyHint)}, header{std::move(linkHeader)}, + referrerPolicy{std::move(referrerPolicy)}, + cspHeader{std::move(cspHeader)}]() { + obs->EarlyHint(header, referrerPolicy, cspHeader); + }), + NS_DISPATCH_NORMAL); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + } + } + if ((status != 101) && (status / 100 == 1)) { + LOG(("ignoring 1xx response except 101 and 103\n")); + mHaveStatusLine = false; + mHttpResponseMatched = false; + mConnection->SetLastTransactionExpectedNoContent(true); + mResponseHead->Reset(); + return NS_OK; + } + if (!mConnection->IsProxyConnectInProgress()) { + MutexAutoLock lock(mLock); + mEarlyHintObserver = nullptr; + } + 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 (!mReportedStart) { + mReportedStart = true; + gHttpHandler->ObserveHttpActivityWithArgs( + HttpActivityArgs(mChannelId), NS_HTTP_ACTIVITY_TYPE_HTTP_TRANSACTION, + NS_HTTP_ACTIVITY_SUBTYPE_RESPONSE_START, PR_Now(), 0, ""_ns); + } + } + + 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; + + if (NS_FAILED(mResponseHead->ParseStatusLine(""_ns))) { + return NS_ERROR_FAILURE; + } + 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; +} + +bool nsHttpTransaction::HandleWebTransportResponse(uint16_t aStatus) { + MOZ_ASSERT(mIsForWebTransport); + if (!(aStatus >= 200 && aStatus < 300)) { + return false; + } + + RefPtr wtSession = + mConnection->GetWebTransportSession(this); + if (!wtSession) { + return false; + } + + nsCOMPtr webTransportListener; + { + MutexAutoLock lock(mLock); + webTransportListener = mWebTransportSessionEventListener; + mWebTransportSessionEventListener = nullptr; + } + if (webTransportListener) { + webTransportListener->OnSessionReadyInternal(wtSession); + wtSession->SetWebTransportSessionEventListener(webTransportListener); + } + + return true; +} + +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 (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; + } + + bool responseChecked = false; + if (mIsForWebTransport) { + responseChecked = HandleWebTransportResponse(mResponseHead->Status()); + LOG(("HandleWebTransportResponse res=%d", responseChecked)); + if (responseChecked) { + mNoContent = true; + mPreserveStream = true; + } + } + + if (!responseChecked) { + // 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 408: + LOG(("408 Server Timeouts")); + + if (mConnection->Version() >= HttpVersion::v2_0) { + mForceRestart = true; + return NS_ERROR_NET_RESET; + } + + // 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. + LOG(("408 Server Timeouts now=%d lastWrite=%d", PR_IntervalNow(), + mConnection->LastWriteTime())); + if ((PR_IntervalNow() - mConnection->LastWriteTime()) >= + PR_MillisecondsToInterval(1000)) { + mForceRestart = true; + return NS_ERROR_NET_RESET; + } + break; + case 421: + LOG(("Misdirected Request.\n")); + gHttpHandler->ClearHostMapping(mConnInfo); + + m421Received = true; + mCaps |= NS_HTTP_REFRESH_DNS; + + // retry on a new connection - just in case + // See bug 1609410, we can't restart the transaction when + // NS_HTTP_STICKY_CONNECTION is set. In the case that a connection + // already passed NTLM authentication, restarting the transaction will + // cause the connection to be closed. + if (!mRestartCount && !(mCaps & NS_HTTP_STICKY_CONNECTION)) { + 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 + mSupportsHTTP3 = nsHttpHandler::IsHttp3SupportedByServer(mResponseHead); + + CollectTelemetryForUploads(); + + // 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; + } + + // preserve connection for tunnel setup - h2 websocket upgrade only + if (mIsHttp2Websocket && mResponseHead->Status() == 200) { + LOG(("nsHttpTransaction::HandleContentStart websocket upgrade resp 200")); + 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(mLock); + 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 + gHttpHandler->ObserveHttpActivityWithArgs( + HttpActivityArgs(mChannelId), NS_HTTP_ACTIVITY_TYPE_HTTP_TRANSACTION, + NS_HTTP_ACTIVITY_SUBTYPE_RESPONSE_COMPLETE, PR_Now(), + static_cast(mContentRead), ""_ns); + } + + 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); + + if (mResponseHead && mHaveAllHeaders) { + auto reportResponseHeader = [&](uint32_t aSubType) { + nsAutoCString completeResponseHeaders; + mResponseHead->Flatten(completeResponseHeaders, false); + completeResponseHeaders.AppendLiteral("\r\n"); + gHttpHandler->ObserveHttpActivityWithArgs( + HttpActivityArgs(mChannelId), + NS_HTTP_ACTIVITY_TYPE_HTTP_TRANSACTION, aSubType, PR_Now(), 0, + completeResponseHeaders); + }; + + if (mConnection->IsProxyConnectInProgress()) { + reportResponseHeader(NS_HTTP_ACTIVITY_SUBTYPE_PROXY_RESPONSE_HEADER); + } else if (!mReportedResponseHeader) { + mReportedResponseHeader = true; + reportResponseHeader(NS_HTTP_ACTIVITY_SUBTYPE_RESPONSE_HEADER); + } + } + } + + // 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::DisableHttp2ForProxy() { + mCaps |= NS_HTTP_DISALLOW_HTTP2_PROXY; +} + +void nsHttpTransaction::DisableHttp3(bool aAllowRetryHTTPSRR) { + 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 " + "aAllowRetryHTTPSRR=%d", + this, mOrigConnInfo->HashKey().get(), aAllowRetryHTTPSRR)); + if (!aAllowRetryHTTPSRR) { + mCaps |= NS_HTTP_DISALLOW_HTTP3; + } + return; + } + + mCaps |= NS_HTTP_DISALLOW_HTTP3; + + MOZ_ASSERT(mConnInfo); + if (mConnInfo) { + // After CloneAsDirectRoute(), http3 will be disabled. + RefPtr connInfo; + mConnInfo->CloneAsDirectRoute(getter_AddRefs(connInfo)); + RemoveAlternateServiceUsedHeader(mRequestHead); + 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")) { +#ifdef MOZ_AUTH_EXTENSION + authenticator = new nsHttpNegotiateAuth(); +#endif + } else if (schema.EqualsLiteral("basic")) { + authenticator = new nsHttpBasicAuth(); + } else if (schema.EqualsLiteral("digest")) { + authenticator = new nsHttpDigestAuth(); + } else if (schema.EqualsLiteral("ntlm")) { + authenticator = new nsHttpNTLMAuth(); + } else if (schema.EqualsLiteral("mock_auth") && + PR_GetEnv("XPCSHELL_TEST_PROFILE_DIR")) { + authenticator = new MockHttpAuth(); + } + 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; +} + +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, nsITimerCallback, nsINamed) + +//----------------------------------------------------------------------------- +// 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) && rv != NS_BASE_STREAM_WOULD_BLOCK) { + NS_ERROR("ResumeRecv failed"); + } + } + return NS_OK; +} + +void nsHttpTransaction::GetNetworkAddresses( + NetAddr& self, NetAddr& peer, bool& aResolvedByTRR, + nsIRequest::TRRMode& aEffectiveTRRMode, TRRSkippedReason& aSkipReason, + bool& aEchConfigUsed) { + MutexAutoLock lock(mLock); + self = mSelfAddr; + peer = mPeerAddr; + aResolvedByTRR = mResolvedByTRR; + aEffectiveTRRMode = mEffectiveTRRMode; + aSkipReason = mTRRSkipReason; + aEchConfigUsed = mEchConfigUsed; +} + +bool nsHttpTransaction::Do0RTT() { + LOG(("nsHttpTransaction::Do0RTT")); + mEarlyDataWasAvailable = true; + 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) { + // Not to use 0RTT when this transaction is restarted next time. + mDoNotTryEarlyData = true; + + // 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; + MaybeRefreshSecurityInfo(); + } + return NS_OK; +} + +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(mLock); + 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(mLock); + 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::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 socketControl; + mConnection->GetTLSSocketControl(getter_AddRefs(socketControl)); + 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); + MakeDontWaitHTTPSRR(); + mDNSRequest = nullptr; + } + + if (!mResolver) { + LOG(("The transaction is not interested in HTTPS record anymore.")); + return NS_OK; + } + + RefPtr deleteProtector(this); + + 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; + + // In the case that an HTTPS RR is unavailable, we should call + // ProcessPendingQ to make sure this transition to be processed soon. + if (!mHTTPSSVCRecord) { + gHttpHandler->ConnMgr()->ProcessPendingQ(mConnInfo); + } + }); + + 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 = newInfo->IsHttp3(); + bool foundInPendingQ = gHttpHandler->ConnMgr()->RemoveTransFromConnEntry( + this, mHashKeyOfConnectionEntry); + + // Adopt the new connection info, so this transaction will be added into the + // new connection entry. + UpdateConnectionInfo(newInfo); + + // If this transaction is sucessfully removed from a connection entry, we call + // ProcessNewTransaction to process it immediately. + // If not, this means that nsHttpTransaction::OnHTTPSRRAvailable happens + // before ProcessNewTransaction and this transaction will be processed later. + if (foundInPendingQ) { + if (NS_FAILED(gHttpHandler->ConnMgr()->ProcessNewTransaction(this))) { + LOG(("Failed to process this transaction.")); + return NS_ERROR_FAILURE; + } + } + + // In case we already have mHttp3BackupTimer, cancel it. + MaybeCancelFallbackTimer(); + + 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.InsertOrUpdate( + Telemetry::TRANSACTION_ECH_RETRY_WITH_ECH_COUNT, 0); + mEchRetryCounterMap.InsertOrUpdate( + Telemetry::TRANSACTION_ECH_RETRY_WITHOUT_ECH_COUNT, 0); + mEchRetryCounterMap.InsertOrUpdate( + Telemetry::TRANSACTION_ECH_RETRY_ECH_FAILED_COUNT, 0); + mEchRetryCounterMap.InsertOrUpdate( + Telemetry::TRANSACTION_ECH_RETRY_OTHERS_COUNT, 0); + } + + return NS_OK; +} + +uint32_t nsHttpTransaction::HTTPSSVCReceivedStage() { + return mHTTPSSVCReceivedStage; +} + +void nsHttpTransaction::MaybeCancelFallbackTimer() { + MOZ_DIAGNOSTIC_ASSERT(OnSocketThread(), "not on socket thread"); + + if (mFastFallbackTimer) { + mFastFallbackTimer->Cancel(); + mFastFallbackTimer = nullptr; + } + + if (mHttp3BackupTimer) { + mHttp3BackupTimer->Cancel(); + mHttp3BackupTimer = nullptr; + } +} + +void nsHttpTransaction::OnBackupConnectionReady(bool aTriggeredByHTTPSRR) { + LOG( + ("nsHttpTransaction::OnBackupConnectionReady [%p] mConnected=%d " + "aTriggeredByHTTPSRR=%d", + this, mConnected, aTriggeredByHTTPSRR)); + if (mConnected || mClosed || mRestarted) { + return; + } + + // If HTTPS RR is in play, don't mess up the fallback mechansim of HTTPS RR. + if (!aTriggeredByHTTPSRR && mOrigConnInfo) { + return; + } + + if (mConnection) { + // The transaction will only be restarted when we already have a connection. + // When there is no connection, this transaction will be moved to another + // connection entry. + SetRestartReason(aTriggeredByHTTPSRR + ? TRANSACTION_RESTART_HTTPS_RR_FAST_FALLBACK + : TRANSACTION_RESTART_HTTP3_FAST_FALLBACK); + } + + mCaps |= NS_HTTP_DISALLOW_HTTP3; + + // Need to backup the origin conn info, since UpdateConnectionInfo() will be + // called in HandleFallback() and mOrigConnInfo will be + // replaced. + RefPtr backup = mOrigConnInfo; + HandleFallback(mBackupConnInfo); + mOrigConnInfo.swap(backup); + + RemoveAlternateServiceUsedHeader(mRequestHead); + + if (mResolver) { + if (mBackupConnInfo) { + const nsCString& host = mBackupConnInfo->GetRoutedHost().IsEmpty() + ? mBackupConnInfo->GetOrigin() + : mBackupConnInfo->GetRoutedHost(); + mResolver->PrefetchAddrRecord(host, Caps() & NS_HTTP_REFRESH_DNS); + } + + if (!aTriggeredByHTTPSRR) { + // We are about to use this backup connection. We shoud not try to use + // HTTPS RR at this point. + mResolver->Close(); + mResolver = nullptr; + } + } +} + +static void CreateBackupConnection( + nsHttpConnectionInfo* aBackupConnInfo, nsIInterfaceRequestor* aCallbacks, + uint32_t aCaps, std::function&& aResultCallback) { + aBackupConnInfo->SetFallbackConnection(true); + RefPtr trans = new SpeculativeTransaction( + aBackupConnInfo, aCallbacks, aCaps | NS_HTTP_DISALLOW_HTTP3, + std::move(aResultCallback)); + uint32_t limit = + StaticPrefs::network_http_http3_parallel_fallback_conn_limit(); + if (limit) { + trans->SetParallelSpeculativeConnectLimit(limit); + trans->SetIgnoreIdle(true); + } + gHttpHandler->ConnMgr()->DoFallbackConnection(trans, false); +} + +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(false); + } + }; + + nsCOMPtr callbacks; + { + MutexAutoLock lock(mLock); + callbacks = mCallbacks; + } + CreateBackupConnection(mBackupConnInfo, callbacks, mCaps, + std::move(callback)); +} + +void nsHttpTransaction::OnFastFallbackTimer() { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG(("nsHttpTransaction::OnFastFallbackTimer [%p] mConnected=%d", this, + mConnected)); + + mFastFallbackTimer = nullptr; + + MOZ_ASSERT(mHTTPSSVCRecord && mOrigConnInfo); + if (!mHTTPSSVCRecord || !mOrigConnInfo) { + return; + } + + bool echConfigUsed = gHttpHandler->EchConfigEnabled(mConnInfo->IsHttp3()) && + !mConnInfo->GetEchConfig().IsEmpty(); + mBackupConnInfo = PrepareFastFallbackConnInfo(echConfigUsed); + if (!mBackupConnInfo) { + return; + } + + MOZ_ASSERT(!mBackupConnInfo->IsHttp3()); + + RefPtr self = this; + auto callback = [self](bool aSucceded) { + if (!aSucceded) { + return; + } + + self->mFastFallbackTriggered = true; + self->OnBackupConnectionReady(true); + }; + + nsCOMPtr callbacks; + { + MutexAutoLock lock(mLock); + callbacks = mCallbacks; + } + CreateBackupConnection(mBackupConnInfo, callbacks, mCaps, + std::move(callback)); +} + +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()->RemoveTransFromConnEntry( + this, mHashKeyOfConnectionEntry); + 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); + } + + UpdateConnectionInfo(aFallbackConnInfo); + Unused << gHttpHandler->ConnMgr()->ProcessNewTransaction(this); +} + +NS_IMETHODIMP +nsHttpTransaction::Notify(nsITimer* aTimer) { + MOZ_DIAGNOSTIC_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; +} + +NS_IMETHODIMP +nsHttpTransaction::GetName(nsACString& aName) { + aName.AssignLiteral("nsHttpTransaction"); + return NS_OK; +} + +bool nsHttpTransaction::GetSupportsHTTP3() { return mSupportsHTTP3; } + +const int64_t TELEMETRY_REQUEST_SIZE_10M = (int64_t)10 * (int64_t)(1 << 20); +const int64_t TELEMETRY_REQUEST_SIZE_50M = (int64_t)50 * (int64_t)(1 << 20); +const int64_t TELEMETRY_REQUEST_SIZE_100M = (int64_t)100 * (int64_t)(1 << 20); + +void nsHttpTransaction::CollectTelemetryForUploads() { + if ((mRequestSize < TELEMETRY_REQUEST_SIZE_10M) || + mTimings.requestStart.IsNull() || mTimings.responseStart.IsNull()) { + return; + } + + // We will briefly continue to collect HTTP_UPLOAD_BANDWIDTH_MBPS + // (a keyed histogram) while live experiments depend on it. + // Once complete, we can remove and use the glean probes, + // http_1/2/3_upload_throughput. + nsAutoCString protocolVersion(nsHttp::GetProtocolVersion(mHttpVersion)); + TimeDuration sendTime = mTimings.responseStart - mTimings.requestStart; + double megabits = static_cast(mRequestSize) * 8.0 / 1000000.0; + uint32_t mpbs = static_cast(megabits / sendTime.ToSeconds()); + Telemetry::Accumulate(Telemetry::HTTP_UPLOAD_BANDWIDTH_MBPS, protocolVersion, + mpbs); + + switch (mHttpVersion) { + case HttpVersion::v1_0: + case HttpVersion::v1_1: + glean::networking::http_1_upload_throughput.AccumulateSamples({mpbs}); + if (mRequestSize <= TELEMETRY_REQUEST_SIZE_50M) { + glean::networking::http_1_upload_throughput_10_50.AccumulateSamples( + {mpbs}); + } else if (mRequestSize <= TELEMETRY_REQUEST_SIZE_100M) { + glean::networking::http_1_upload_throughput_50_100.AccumulateSamples( + {mpbs}); + } else { + glean::networking::http_1_upload_throughput_100.AccumulateSamples( + {mpbs}); + } + break; + case HttpVersion::v2_0: + glean::networking::http_2_upload_throughput.AccumulateSamples({mpbs}); + if (mRequestSize <= TELEMETRY_REQUEST_SIZE_50M) { + glean::networking::http_2_upload_throughput_10_50.AccumulateSamples( + {mpbs}); + } else if (mRequestSize <= TELEMETRY_REQUEST_SIZE_100M) { + glean::networking::http_2_upload_throughput_50_100.AccumulateSamples( + {mpbs}); + } else { + glean::networking::http_2_upload_throughput_100.AccumulateSamples( + {mpbs}); + } + break; + case HttpVersion::v3_0: + glean::networking::http_3_upload_throughput.AccumulateSamples({mpbs}); + if (mRequestSize <= TELEMETRY_REQUEST_SIZE_50M) { + glean::networking::http_3_upload_throughput_10_50.AccumulateSamples( + {mpbs}); + } else if (mRequestSize <= TELEMETRY_REQUEST_SIZE_100M) { + glean::networking::http_3_upload_throughput_50_100.AccumulateSamples( + {mpbs}); + } else { + glean::networking::http_3_upload_throughput_100.AccumulateSamples( + {mpbs}); + } + break; + default: + break; + } +} + +void nsHttpTransaction::GetHashKeyOfConnectionEntry(nsACString& aResult) { + MutexAutoLock lock(mLock); + aResult.Assign(mHashKeyOfConnectionEntry); +} + +void nsHttpTransaction::SetIsForWebTransport(bool aIsForWebTransport) { + mIsForWebTransport = aIsForWebTransport; +} + +void nsHttpTransaction::RemoveConnection() { + MutexAutoLock lock(mLock); + mConnection = nullptr; +} + +} // namespace mozilla::net diff --git a/netwerk/protocol/http/nsHttpTransaction.h b/netwerk/protocol/http/nsHttpTransaction.h new file mode 100644 index 0000000000..354fa2c4d2 --- /dev/null +++ b/netwerk/protocol/http/nsHttpTransaction.h @@ -0,0 +1,599 @@ +/* -*- 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 "ARefBase.h" +#include "EventTokenBucket.h" +#include "Http2Push.h" +#include "HttpTransactionShell.h" +#include "TimingStruct.h" +#include "mozilla/StaticPrefs_security.h" +#include "mozilla/net/DNS.h" +#include "mozilla/net/NeckoChannelParams.h" +#include "nsAHttpConnection.h" +#include "nsAHttpTransaction.h" +#include "nsCOMPtr.h" +#include "nsHttp.h" +#include "nsIAsyncOutputStream.h" +#include "nsIClassOfService.h" +#include "nsIEarlyHintObserver.h" +#include "nsIInterfaceRequestor.h" +#include "nsITLSSocketControl.h" +#include "nsITimer.h" +#include "nsIWebTransport.h" +#include "nsTHashMap.h" +#include "nsThreadUtils.h" + +//----------------------------------------------------------------------------- + +class nsIDNSHTTPSSVCRecord; +class nsIEventTarget; +class nsIInputStream; +class nsIOutputStream; +class nsIRequestContext; +class nsISVCBRecord; + +namespace mozilla::net { + +class HTTPSRecordResolver; +class nsHttpChunkedDecoder; +class nsHttpHeaderArray; +class nsHttpRequestHead; +class nsHttpResponseHead; +class NullHttpTransaction; +class Http2ConnectTransaction; + +//----------------------------------------------------------------------------- +// 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 nsINamed { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSAHTTPTRANSACTION + NS_DECL_HTTPTRANSACTIONSHELL + NS_DECL_NSIINPUTSTREAMCALLBACK + NS_DECL_NSIOUTPUTSTREAMCALLBACK + NS_DECL_NSITIMERCALLBACK + NS_DECL_NSINAMED + + 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 MakeRestartable() override { mCaps |= NS_HTTP_CONNECTION_RESTARTABLE; } + void MakeNonRestartable() { mCaps &= ~NS_HTTP_CONNECTION_RESTARTABLE; } + void RemoveConnection(); + void SetIsHttp2Websocket(bool h2ws) override { mIsHttp2Websocket = h2ws; } + bool IsHttp2Websocket() override { return mIsHttp2Websocket; } + + void SetTRRInfo(nsIRequest::TRRMode aMode, + TRRSkippedReason aSkipReason) override { + mEffectiveTRRMode = aMode; + mTRRSkipReason = aSkipReason; + } + + bool WaitingForHTTPSRR() const { return mCaps & NS_HTTP_FORCE_WAIT_HTTP_RR; } + void MakeDontWaitHTTPSRR() { mCaps &= ~NS_HTTP_FORCE_WAIT_HTTP_RR; } + + // 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 mTimings.transactionPending to the current time stamp or to a null + // time stamp (if now is false) + void SetPendingTime(bool now = true) { + mozilla::MutexAutoLock lock(mLock); + if (!now && !mTimings.transactionPending.IsNull()) { + // Remember how long it took. We will use this value to record + // TRANSACTION_WAIT_TIME_HTTP2_SUP_HTTP3 telemetry, but we need to wait + // for the response headers. + mPendingDurationTime = TimeStamp::Now() - mTimings.transactionPending; + } + // Note that the transaction could be added in to a pending queue multiple + // times (when the transaction is restarted or moved to a new conn entry due + // to HTTPS RR), so we should only set the pending time once. + if (mTimings.transactionPending.IsNull()) { + mTimings.transactionPending = now ? TimeStamp::Now() : TimeStamp(); + } + } + TimeStamp GetPendingTime() override { + mozilla::MutexAutoLock lock(mLock); + return mTimings.transactionPending; + } + + // overload of nsAHttpTransaction::RequestContext() + nsIRequestContext* RequestContext() override { return mRequestContext.get(); } + void DispatchedAsBlocking(); + void RemoveDispatchedAsBlocking(); + + void DisableSpdy() override; + void DisableHttp2ForProxy() override; + void DoNotRemoveAltSvc() override { mDoNotRemoveAltSvc = true; } + void DoNotResetIPFamilyPreference() override { + mDoNotResetIPFamilyPreference = true; + } + void DisableHttp3(bool aAllowRetryHTTPSRR) 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(); + + uint64_t BrowserId() override { return mBrowserId; } + + void SetHttpTrailers(nsCString& aTrailers); + + bool IsWebsocketUpgrade(); + + 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(ClassOfService cos); + + virtual nsresult OnHTTPSRRAvailable( + nsIDNSHTTPSSVCRecord* aHTTPSSVCRecord, + nsISVCBRecord* aHighestPriorityRecord) override; + + void GetHashKeyOfConnectionEntry(nsACString& aResult); + + bool IsForWebTransport() { return mIsForWebTransport; } + + 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, + const nsACString& aFailedAlpn, + 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(bool aTriggeredByHTTPSRR); + void OnFastFallbackTimer(); + void HandleFallback(nsHttpConnectionInfo* aFallbackConnInfo); + void MaybeCancelFallbackTimer(); + + // 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, // The transaction was forced to restart. + TRANSACTION_RESTART_NO_DATA_SENT, + TRANSACTION_RESTART_DOWNGRADE_WITH_EARLY_DATA, + TRANSACTION_RESTART_HTTPS_RR_NET_RESET, + TRANSACTION_RESTART_HTTPS_RR_CONNECTION_REFUSED, + TRANSACTION_RESTART_HTTPS_RR_UNKNOWN_HOST, + TRANSACTION_RESTART_HTTPS_RR_NET_TIMEOUT, + TRANSACTION_RESTART_HTTPS_RR_SEC_ERROR, + TRANSACTION_RESTART_HTTPS_RR_FAST_FALLBACK, + TRANSACTION_RESTART_HTTP3_FAST_FALLBACK, + TRANSACTION_RESTART_OTHERS, + TRANSACTION_RESTART_PROTOCOL_VERSION_ALERT, + }; + void SetRestartReason(TRANSACTION_RESTART_REASON aReason); + + bool HandleWebTransportResponse(uint16_t aStatus); + + void MaybeRefreshSecurityInfo() { + MutexAutoLock lock(mLock); + if (mConnection) { + nsCOMPtr tlsSocketControl; + mConnection->GetTLSSocketControl(getter_AddRefs(tlsSocketControl)); + if (tlsSocketControl) { + tlsSocketControl->GetSecurityInfo(getter_AddRefs(mSecurityInfo)); + } + } + } + + 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 MOZ_UNANNOTATED{"transaction lock"}; + + nsCOMPtr mCallbacks; + nsCOMPtr mTransportSink; + nsCOMPtr mConsumerTarget; + nsCOMPtr mSecurityInfo; + nsCOMPtr mPipeIn; + nsCOMPtr mPipeOut; + nsCOMPtr mRequestContext; + + uint64_t mChannelId{0}; + + nsCString mReqHeaderBuf; // flattened request headers + nsCOMPtr mRequestStream; + int64_t mRequestSize{0}; + + 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{nullptr}; // weak ref + nsHttpResponseHead* mResponseHead{nullptr}; // owning pointer + + nsAHttpSegmentReader* mReader{nullptr}; + nsAHttpSegmentWriter* mWriter{nullptr}; + + nsCString mLineBuf; // may contain a partial line + + int64_t mContentLength{-1}; // equals -1 if unknown + int64_t mContentRead{0}; // count of consumed content bytes + Atomic mTransferSize{0}; // 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{0}; + + RefPtr mPushedStream; + uint32_t mInitialRwin{0}; + + nsHttpChunkedDecoder* mChunkedDecoder{nullptr}; + + TimingStruct mTimings; + + nsresult mStatus{NS_OK}; + + int16_t mPriority{0}; + + // the number of times this transaction has been restarted + uint16_t mRestartCount{0}; + Atomic mCaps{0}; + + HttpVersion mHttpVersion{HttpVersion::UNKNOWN}; + uint16_t mHttpResponseCode{0}; + nsCString mFlat407Headers; + + uint32_t mCurrentHttpResponseHeaderSize{0}; + + 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{THROTTLE_NO_LIMIT}; + + // 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{0}; + Atomic mResponseIsComplete{false}; + Atomic mClosed{false}; + Atomic mIsHttp3Used{false}; + + // 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{false}; + + // state flags, all logically boolean, but not packed together into a + // bitfield so as to avoid bitfield-induced races. See bug 560579. + bool mConnected{false}; + bool mActivated{false}; + bool mHaveStatusLine{false}; + bool mHaveAllHeaders{false}; + bool mTransactionDone{false}; + bool mDidContentStart{false}; + bool mNoContent{false}; // expecting an empty entity body + bool mSentData{false}; + bool mReceivedData{false}; + bool mStatusEventPending{false}; + bool mHasRequestBody{false}; + bool mProxyConnectFailed{false}; + bool mHttpResponseMatched{false}; + bool mPreserveStream{false}; + bool mDispatchedAsBlocking{false}; + bool mResponseTimeoutEnabled{true}; + bool mForceRestart{false}; + bool mReuseOnRestart{false}; + bool mContentDecoding{false}; + bool mContentDecodingCheck{false}; + bool mDeferredSendProgress{false}; + bool mWaitingOnPipeOut{false}; + bool mDoNotRemoveAltSvc{false}; + bool mDoNotResetIPFamilyPreference{false}; + bool mIsHttp2Websocket{false}; + + // 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{false}; + bool mReportedResponseHeader{false}; + + // protected by nsHttp::GetLock() + bool mResponseHeadTaken{false}; + UniquePtr mForTakeResponseTrailers; + bool mResponseTrailersTaken{false}; + + // Set when this transaction was restarted by call to Restart(). Used to tell + // the http channel to reset proxy authentication. + Atomic mRestarted{false}; + + // The time when the transaction was submitted to the Connection Manager + TimeDuration mPendingDurationTime; + + uint64_t mBrowserId{0}; + + // 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{false}; + bool mPassedRatePacing{false}; + bool mSynchronousRatePaceRequest{false}; + nsCOMPtr mTokenBucketCancel; + + void CollectTelemetryForUploads(); + + public: + ClassOfService GetClassOfService() { + return {mClassOfServiceFlags, mClassOfServiceIncremental}; + } + + private: + Atomic mClassOfServiceFlags{0}; + Atomic mClassOfServiceIncremental{false}; + + public: + nsIInterfaceRequestor* SecurityCallbacks() { return mCallbacks; } + // Called when this transaction is inserted in the pending queue. + void OnPendingQueueInserted(const nsACString& aConnectionHashKey); + + private: + TransactionObserverFunc mTransactionObserver; + NetAddr mSelfAddr; + NetAddr mPeerAddr; + bool mResolvedByTRR{false}; + Atomic mEffectiveTRRMode{ + nsIRequest::TRR_DEFAULT_MODE}; + Atomic mTRRSkipReason{nsITRRSkipReason::TRR_UNSET}; + bool mEchConfigUsed = false; + + bool m0RTTInProgress{false}; + bool mDoNotTryEarlyData{false}; + enum { + EARLY_NONE, + EARLY_SENT, + EARLY_ACCEPTED, + EARLY_425 + } mEarlyDataDisposition{EARLY_NONE}; + + HttpTrafficCategory mTrafficCategory{HttpTrafficCategory::eInvalid}; + bool mThroughCaptivePortal; + Atomic mProxyConnectResponseCode{0}; + + OnPushCallback mOnPushCallback; + nsTHashMap> mIDToStreamMap; + + nsCOMPtr mDNSRequest; + Atomic mHTTPSSVCReceivedStage{HTTPSSVC_NOT_USED}; + bool m421Received = false; + nsCOMPtr mHTTPSSVCRecord; + nsTArray> mRecordsForRetry; + bool mDontRetryWithDirectRoute = false; + bool mFastFallbackTriggered = false; + bool mHttp3BackupTimerCreated = false; + nsCOMPtr mFastFallbackTimer; + nsCOMPtr mHttp3BackupTimer; + RefPtr mBackupConnInfo; + RefPtr mResolver; + TRANSACTION_RESTART_REASON mRestartReason = TRANSACTION_RESTART_NONE; + + nsTHashMap mEchRetryCounterMap; + + bool mSupportsHTTP3 = false; + Atomic mIsForWebTransport{false}; + + bool mEarlyDataWasAvailable = false; + bool ShouldRestartOn0RttError(nsresult reason); + + nsCOMPtr mEarlyHintObserver; + // This hash key is set when a transaction is inserted into the connection + // entry's pending queue. + // See nsHttpConnectionMgr::GetOrCreateConnectionEntry(). A transaction could + // be associated with the connection entry whose hash key is not the same as + // this transaction's. + nsCString mHashKeyOfConnectionEntry; + + nsCOMPtr mWebTransportSessionEventListener; +}; + +} // namespace mozilla::net + +#endif // nsHttpTransaction_h__ diff --git a/netwerk/protocol/http/nsIBackgroundChannelRegistrar.idl b/netwerk/protocol/http/nsIBackgroundChannelRegistrar.idl new file mode 100644 index 0000000000..025dcebf51 --- /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. + */ +[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/nsIBinaryHttp.idl b/netwerk/protocol/http/nsIBinaryHttp.idl new file mode 100644 index 0000000000..895f25f2d2 --- /dev/null +++ b/netwerk/protocol/http/nsIBinaryHttp.idl @@ -0,0 +1,40 @@ +/* -*- Mode: IDL; 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" + +[scriptable, uuid(f6f899cc-683a-43da-9206-0eb0c09cc758)] +interface nsIBinaryHttpRequest : nsISupports { + readonly attribute ACString method; + readonly attribute ACString scheme; + readonly attribute ACString authority; + readonly attribute ACString path; + readonly attribute Array headerNames; + readonly attribute Array headerValues; + readonly attribute Array content; +}; + +[scriptable, uuid(6ca85d9c-cdc5-45d4-9adc-005abedce9c9)] +interface nsIBinaryHttpResponse : nsISupports { + readonly attribute uint16_t status; + readonly attribute Array headerNames; + readonly attribute Array headerValues; + readonly attribute Array content; +}; + +// Implements Binary Representation of HTTP Messages (RFC 9292). +// In normal operation, encodeRequest and decodeResponse are expected to be +// used. For testing, decodeRequest and encodeResponse are available as well. +// Thread safety: this interface may be used on any thread, but objects +// returned by it are not inherently thread-safe and should only be used on the +// threads they were created on. +[scriptable, builtinclass, uuid(b43b3f73-8160-4ab2-9f5d-4129a9708081)] +interface nsIBinaryHttp : nsISupports { + Array encodeRequest(in nsIBinaryHttpRequest request); + nsIBinaryHttpRequest decodeRequest(in Array request); + + nsIBinaryHttpResponse decodeResponse(in Array response); + Array encodeResponse(in nsIBinaryHttpResponse response); +}; 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/nsIEarlyHintObserver.idl b/netwerk/protocol/http/nsIEarlyHintObserver.idl new file mode 100644 index 0000000000..1cc206ae56 --- /dev/null +++ b/netwerk/protocol/http/nsIEarlyHintObserver.idl @@ -0,0 +1,16 @@ +/* -*- 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" + +[scriptable, uuid(97b6be6f-283c-45dd-81a7-4bb2d87d42f8)] +interface nsIEarlyHintObserver : nsISupports +{ + /** + * This method is called when the transaction has early hint (i.e. the + * '103 Early Hint' informational response) headers. + */ + void earlyHint(in ACString linkHeader, in ACString referrerPolicy, in ACString cspHeader); +}; diff --git a/netwerk/protocol/http/nsIHttpActivityObserver.idl b/netwerk/protocol/http/nsIHttpActivityObserver.idl new file mode 100644 index 0000000000..533e6af135 --- /dev/null +++ b/netwerk/protocol/http/nsIHttpActivityObserver.idl @@ -0,0 +1,214 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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); + + /** + * This function is for testing only. We use this function to observe the + * activities of HTTP connections. To receive this notification, + * observeConnection should be set to true. + */ + [must_use] + void observeConnectionActivity(in ACString aHost, + in int32_t aPort, + in boolean aSSL, + in boolean aHasECH, + in boolean aIsHttp3, + in uint32_t aActivityType, + in uint32_t aActivitySubtype, + in PRTime aTimestamp, + in ACString aExtraStringData); + + const unsigned long ACTIVITY_TYPE_SOCKET_TRANSPORT = 0x0001; + const unsigned long ACTIVITY_TYPE_HTTP_TRANSACTION = 0x0002; + const unsigned long ACTIVITY_TYPE_HTTP_CONNECTION = 0x0003; + + 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; + const unsigned long ACTIVITY_SUBTYPE_PROXY_RESPONSE_HEADER = 0x5007; + const unsigned long ACTIVITY_SUBTYPE_DNSANDSOCKET_CREATED = 0x5008; + const unsigned long ACTIVITY_SUBTYPE_SPECULATIVE_DNSANDSOCKET_CREATED = 0x5009; + const unsigned long ACTIVITY_SUBTYPE_ECH_SET = 0x500A; + const unsigned long ACTIVITY_SUBTYPE_CONNECTION_CREATED = 0x500B; + + /** + * 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_ACTIVITY_TYPE_HTTP_CONNECTION \ + nsIHttpActivityObserver::ACTIVITY_TYPE_HTTP_CONNECTION + +#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 +#define NS_HTTP_ACTIVITY_SUBTYPE_PROXY_RESPONSE_HEADER \ + nsIHttpActivityObserver::ACTIVITY_SUBTYPE_PROXY_RESPONSE_HEADER +#define NS_HTTP_ACTIVITY_SUBTYPE_DNSANDSOCKET_CREATED \ + nsIHttpActivityObserver::ACTIVITY_SUBTYPE_DNSANDSOCKET_CREATED +#define NS_HTTP_ACTIVITY_SUBTYPE_SPECULATIVE_DNSANDSOCKET_CREATED \ + nsIHttpActivityObserver::ACTIVITY_SUBTYPE_SPECULATIVE_DNSANDSOCKET_CREATED +#define NS_HTTP_ACTIVITY_SUBTYPE_ECH_SET \ + nsIHttpActivityObserver::ACTIVITY_SUBTYPE_ECH_SET +#define NS_HTTP_ACTIVITY_SUBTYPE_CONNECTION_CREATED \ + nsIHttpActivityObserver::ACTIVITY_SUBTYPE_CONNECTION_CREATED +%} + +/** + * 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); + + /** + * C++ friendly getter + */ + [noscript, notxpcom] bool Activated(); + [noscript, notxpcom] bool ObserveProxyResponseEnabled(); + [noscript, notxpcom] bool ObserveConnectionEnabled(); + + /** + * When true, the ACTIVITY_SUBTYPE_PROXY_RESPONSE_HEADER will be sent to + * the observers. + */ + [must_use] attribute boolean observeProxyResponse; + + /** + * When true, the ACTIVITY_TYPE_HTTP_CONNECTION will be sent to + * the observers. + */ + [must_use] attribute boolean observeConnection; +}; 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..f479b6ff58 --- /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; + +[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..222bd3837c --- /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. + */ +[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 ACString 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 ACString aChallenge, + in boolean aProxyAuth, + in AString aDomain, + in AString aUser, + in AString 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] + ACString generateCredentials(in nsIHttpAuthenticableChannel aChannel, + in ACString aChallenge, + in boolean aProxyAuth, + in AString aDomain, + in AString aUser, + in AString 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..7ed707eacf --- /dev/null +++ b/netwerk/protocol/http/nsIHttpChannel.idl @@ -0,0 +1,497 @@ +/* -*- 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" +%} + +native UniqueProfileChunkedBuffer(mozilla::UniquePtr); + +/** + * 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 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; + + /** + * ID of the browser for this channel. + * + * NOTE: The setter of this attribute is currently for xpcshell test only. + * Don't alter it otherwise. + */ + [must_use] attribute uint64_t browserId; + + /** + * 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. + * + * @param aIsWarning + * When true, this is a warning message. + */ + void logBlockedCORSRequest(in AString aMessage, + in ACString aCategory, + in boolean aIsWarning); + + void logMimeTypeMismatch(in ACString aMessageName, + in boolean aWarning, + in AString aURL, + in AString aContentType); + + [notxpcom, nostdcall] void setSource(in UniqueProfileChunkedBuffer aSource); + + [must_use] attribute AString classicScriptHintCharset; + + [must_use] attribute AString documentCharacterSet; +}; diff --git a/netwerk/protocol/http/nsIHttpChannelAuthProvider.idl b/netwerk/protocol/http/nsIHttpChannelAuthProvider.idl new file mode 100644 index 0000000000..67a1ae217d --- /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. + */ + +[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..13ff5661b8 --- /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; + +[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..1650b8f35c --- /dev/null +++ b/netwerk/protocol/http/nsIHttpChannelInternal.idl @@ -0,0 +1,522 @@ +/* -*- 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" +#include "nsIRequest.idl" +#include "nsITRRSkipReason.idl" + +%{C++ +#include "nsStringFwd.h" +#include "nsTArrayForwardDeclare.h" +template class nsCOMArray; +namespace mozilla { +class TimeStamp; +namespace net { +class nsHttpConnectionInfo; +class WebSocketConnectionBase; +class EarlyHintConnectArgs; +} +namespace dom { +enum class RequestMode : uint8_t; +} +} +%} +[ptr] native nsHttpConnectionInfo(mozilla::net::nsHttpConnectionInfo); +[ptr] native StringArray(nsTArray); +[ref] native CStringArrayRef(const nsTArray); +[ref] native securityMessagesArray(nsCOMArray); +[ptr] native WebSocketConnectionBase(mozilla::net::WebSocketConnectionBase); + +native TimeStamp(mozilla::TimeStamp); +native RequestMode(mozilla::dom::RequestMode); + +interface nsIAsyncInputStream; +interface nsIAsyncOutputStream; +interface nsIEarlyHintObserver; +interface nsIPrincipal; +interface nsIProxyInfo; +interface nsISecurityConsoleMessage; +interface nsISocketTransport; +interface nsIURI; +interface WebTransportSessionEventListener; + +/** + * 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); + + void onWebSocketConnectionAvailable(in WebSocketConnectionBase aConnection); +}; + +/** + * 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); + + /** + * 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.http2.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.enable 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; + + /** + * If true, do not resolve any proxy for this request. Intended only for use with + * critical infra like the updater. + * default is false. + */ + [must_use] attribute boolean bypassProxy; + + /** + * 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. + */ + [must_use] readonly attribute boolean isResolvedByTRR; + + + /** + * The effective TRR mode used to resolve this channel. + * This is computed by taking the value returned by nsIRequest.getTRRMode() + * and the state of the TRRService. If the domain is excluded from TRR + * or the TRRService is disabled, the effective mode would be TRR_DISABLED_MODE + * even if the initial mode set on the request was TRR_ONLY_MODE. + */ + [must_use] readonly attribute nsIRequest_TRRMode effectiveTRRMode; + + /** + * If the DNS request triggered by this channel didn't use TRR, this value + * contains the reason why that was skipped. + */ + [must_use] readonly attribute nsITRRSkipReason_value trrSkipReason; + + /** + * True if channel is loaded by socket process. + */ + [must_use] readonly attribute boolean isLoadedBySocketProcess; + + /** + * Set to true if the channel is an OCSP check. + * Channels with this flag set will skip TRR in mode3 (because the circular + * dependency with checking OCSP for the TRR server will cause a failure) + */ + [must_use] attribute boolean isOCSP; + + /** + * 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; + + /** + * Set by nsCORSListenerProxy to indicate CORS load type. Defaults to CORS_MODE_NO_CORS. + */ + [must_use, noscript] attribute RequestMode requestMode; + + 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. + */ + void setIPv4Disabled(); + + /** + * The channel will be loaded over IPv4, disabling IPv6. + */ + 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(in boolean aIsOriginTrialCoepCredentiallessEnabled); + + [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; + + /** + * This attribute indicates if the HTTPS RR is used for this channel. + */ + [must_use] readonly attribute boolean hasHTTPSRR; + + /** + * Set Early Hint Observer. + */ + [must_use] void setEarlyHintObserver(in nsIEarlyHintObserver aObserver); + + /* + * id of the EarlyHintPreloader to connect back from PreloadService to + * EarlyHintPreloader. + */ + [must_use] attribute unsigned long long earlyHintPreloaderId; + + [notxpcom, nostdcall] void setConnectionInfo(in nsHttpConnectionInfo aInfo); + + /* + * This attribute indicates if the channel was loaded via Proxy. + */ + [must_use] readonly attribute boolean isProxyUsed; + + /** + * Set mWebTransportSessionEventListener. + */ + [must_use] void setWebTransportSessionEventListener( + in WebTransportSessionEventListener aListener); + + /** + * This attribute indicates the type of Link header in the received + * 103 response. + */ + [must_use] attribute unsigned long earlyHintLinkType; + + /** + * Indicates whether the User-Agent request header has been modified since + * the channel was created. This value will be used to decide if we need to + * recalculate the User-Agent header for fingerprinting protection. We won't + * recalculate the User-Agent header if it has been modified to preserve the + * overridden header value. + */ + [must_use] attribute boolean isUserAgentHeaderModified; +}; 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..d6a6f8a0c1 --- /dev/null +++ b/netwerk/protocol/http/nsIHttpProtocolHandler.idl @@ -0,0 +1,215 @@ +/* -*- 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 HTTP advertised user agent string. + */ + [must_use] readonly attribute ACString rfpUserAgent; + + /** + * 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); + + /* + * Clears the CORS preflight cache. + */ + void clearCORSPreflightCache(); +}; + +%{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" + +/** + * Same as http-on-modify-request, but called before the cookie header is set on + * the channel. + * This allows observers to set cookies via the cookie service to be included in + * the request header. + * http-on-modify-request is too late for this use-case since the cookie header + * has already been populated at that point. + */ +#define NS_HTTP_ON_MODIFY_REQUEST_BEFORE_COOKIES_TOPIC "http-on-modify-request-before-cookies" + +/** + * 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/nsIObliviousHttp.idl b/netwerk/protocol/http/nsIObliviousHttp.idl new file mode 100644 index 0000000000..84bc30d640 --- /dev/null +++ b/netwerk/protocol/http/nsIObliviousHttp.idl @@ -0,0 +1,78 @@ +/* -*- Mode: IDL; 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 nsIChannel; +interface nsIURI; + +[scriptable, builtinclass, uuid(f2a4aaa4-046a-439e-beef-893b15a90cff)] +interface nsIObliviousHttpClientResponse : nsISupports { + // Decrypt an encrypted response ("enc_response" in the RFC). + // Can only be called once. + Array decapsulate(in Array encResponse); +}; + +[scriptable, builtinclass, uuid(403af7f9-4a76-49fc-a622-38d6ba3ee496)] +interface nsIObliviousHttpClientRequest : nsISupports { + // The encrypted request ("enc_request" in the RFC). + readonly attribute Array encRequest; + // The context for decrypting the eventual response. + readonly attribute nsIObliviousHttpClientResponse response; +}; + +[scriptable, builtinclass, uuid(105deb62-45b4-407a-b330-550433279111)] +interface nsIObliviousHttpServerResponse : nsISupports { + readonly attribute Array request; + + Array encapsulate(in Array response); +}; + +[scriptable, builtinclass, uuid(fb1abc56-b525-4e1a-a4c6-341a9b32084e)] +interface nsIObliviousHttpServer : nsISupports { + readonly attribute Array encodedConfig; + + nsIObliviousHttpServerResponse decapsulate(in Array encRequest); +}; + + +// IDL bindings for the rust implementation of oblivious http. +// Client code will generally call `encapsulateRequest` given an encoded +// oblivious gateway key configuration and an encoded binary http request. +// This function returns a nsIObliviousHttpClientRequest. The `encRequest` +// attribute of that object is the encapsulated request that can be sent to an +// oblivious relay to be forwarded on to the oblivious gateway and then to the +// actual target. The `response` attribute is used to decapsulate the response +// returned by the oblivious relay. +// For tests, this implementation provides a facility for decapsulating +// requests and encapsulating responses. Call `server` to get an +// `nsIObliviousHttpServer`, which has an attribute `encodedConfig` for use +// with `encapsulateRequest`. It also has a function `decapsulate`, which +// decapsulates an encapsulated client request and returns an +// `nsIObliviousHttpServerResponse`. This object can `encapsulate` a response, +// which the `nsIObliviousHttpClientResponse` from the original request should +// be able to `decapsulate`. +// Thread safety: nsIObliviousHttp may be used on any thread, but any objects +// created by it must only be used on the threads they are created on. +[scriptable, builtinclass, uuid(d581149e-3319-4563-b95e-46c64af5c4e8)] +interface nsIObliviousHttp : nsISupports +{ + nsIObliviousHttpClientRequest encapsulateRequest( + in Array encodedConfig, + in Array request); + + nsIObliviousHttpServer server(); +}; + +[scriptable, builtinclass, uuid(b1f08d56-fca6-4290-9500-d5168dc9d8c3)] +interface nsIObliviousHttpService : nsISupports +{ + nsIChannel newChannel(in nsIURI relayURI, in nsIURI targetURI, in Array encodedConfig); + + void getTRRSettings(out nsIURI relayURI, out Array encodedConfig); + + // Clears the config + void clearTRRConfig(); +}; diff --git a/netwerk/protocol/http/nsIObliviousHttpChannel.idl b/netwerk/protocol/http/nsIObliviousHttpChannel.idl new file mode 100644 index 0000000000..0373693294 --- /dev/null +++ b/netwerk/protocol/http/nsIObliviousHttpChannel.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 "nsIHttpChannel.idl" + +/** + * nsIObliviousHttpChannel + * + * This interface allows consumers to differentiate between the + * relayChannel request that transports the OHTTP payload + * and the virtual OHTTP channel represented by the + * nsIObliviousHttpChannel implementation. + */ +[builtinclass, scriptable, uuid(f829f761-0744-4d1c-9c2d-8931c22ae8d5)] +interface nsIObliviousHttpChannel: nsIHttpChannel +{ + /** + * Returns the channel used to transport the binary serialization + * of the request and response to and from the OHTTP relay. + * This can be useful to determine if an HTTP status code or failure + * is due to the relay or the gateway response. + */ + readonly attribute nsIHttpChannel relayChannel; +}; diff --git a/netwerk/protocol/http/nsIRaceCacheWithNetwork.idl b/netwerk/protocol/http/nsIRaceCacheWithNetwork.idl new file mode 100644 index 0000000000..93f361b198 --- /dev/null +++ b/netwerk/protocol/http/nsIRaceCacheWithNetwork.idl @@ -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/. */ + +#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 asyncOpen has already been called. + * Otherwise the delayed timer will be set when the normal call to + * TriggerNetwork is made. 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/nsITlsHandshakeListener.idl b/netwerk/protocol/http/nsITlsHandshakeListener.idl new file mode 100644 index 0000000000..0d5806be24 --- /dev/null +++ b/netwerk/protocol/http/nsITlsHandshakeListener.idl @@ -0,0 +1,14 @@ +/* -*- 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" + +[uuid(b4bbe824-ec4c-48be-9a40-6a7339347f40)] +interface nsITlsHandshakeCallbackListener : nsISupports { + [noscript] void handshakeDone(); + [noscript] void certVerificationDone(); + [noscript] void clientAuthCertificateSelected(); +}; 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..2db1887b80 --- /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 = 0; + 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/http/oblivious_http/Cargo.toml b/netwerk/protocol/http/oblivious_http/Cargo.toml new file mode 100644 index 0000000000..e15cd2f74f --- /dev/null +++ b/netwerk/protocol/http/oblivious_http/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "oblivious_http" +version = "0.1.0" +edition = "2021" + +[dependencies] +nserror = { path = "../../../../xpcom/rust/nserror" } +ohttp = { version = "0.3", default-features = false, features = ["gecko", "nss", "client", "server"] } +rand = "0.8" +thin-vec = { version = "0.2.1", features = ["gecko-ffi"] } +xpcom = { path = "../../../../xpcom/rust/xpcom" } diff --git a/netwerk/protocol/http/oblivious_http/src/lib.rs b/netwerk/protocol/http/oblivious_http/src/lib.rs new file mode 100644 index 0000000000..948139ab68 --- /dev/null +++ b/netwerk/protocol/http/oblivious_http/src/lib.rs @@ -0,0 +1,188 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +extern crate nserror; +extern crate ohttp; +extern crate rand; +extern crate thin_vec; +#[macro_use] +extern crate xpcom; + +use nserror::{nsresult, NS_ERROR_FAILURE, NS_ERROR_NOT_AVAILABLE, NS_OK}; +use ohttp::hpke::{Aead, Kdf, Kem}; +use ohttp::{ + ClientRequest, ClientResponse, KeyConfig, KeyId, Server, ServerResponse, SymmetricSuite, +}; +use thin_vec::ThinVec; +use xpcom::interfaces::{ + nsIObliviousHttpClientRequest, nsIObliviousHttpClientResponse, nsIObliviousHttpServer, + nsIObliviousHttpServerResponse, +}; +use xpcom::{xpcom_method, RefPtr}; + +use std::cell::RefCell; + +#[xpcom(implement(nsIObliviousHttpClientResponse), atomic)] +struct ObliviousHttpClientResponse { + response: RefCell>, +} + +impl ObliviousHttpClientResponse { + xpcom_method!(decapsulate => Decapsulate(enc_response: *const ThinVec) -> ThinVec); + fn decapsulate(&self, enc_response: &ThinVec) -> Result, nsresult> { + let response = self + .response + .borrow_mut() + .take() + .ok_or(NS_ERROR_NOT_AVAILABLE)?; + let decapsulated = response + .decapsulate(enc_response) + .map_err(|_| NS_ERROR_FAILURE)?; + Ok(decapsulated.into_iter().collect()) + } +} + +#[xpcom(implement(nsIObliviousHttpClientRequest), atomic)] +struct ObliviousHttpClientRequest { + enc_request: Vec, + response: RefPtr, +} + +impl ObliviousHttpClientRequest { + xpcom_method!(get_enc_request => GetEncRequest() -> ThinVec); + fn get_enc_request(&self) -> Result, nsresult> { + Ok(self.enc_request.clone().into_iter().collect()) + } + + xpcom_method!(get_response => GetResponse() -> *const nsIObliviousHttpClientResponse); + fn get_response(&self) -> Result, nsresult> { + Ok(self.response.clone()) + } +} + +#[xpcom(implement(nsIObliviousHttpServerResponse), atomic)] +struct ObliviousHttpServerResponse { + request: Vec, + server_response: RefCell>, +} + +impl ObliviousHttpServerResponse { + xpcom_method!(get_request => GetRequest() -> ThinVec); + fn get_request(&self) -> Result, nsresult> { + Ok(self.request.clone().into_iter().collect()) + } + + xpcom_method!(encapsulate => Encapsulate(response: *const ThinVec) -> ThinVec); + fn encapsulate(&self, response: &ThinVec) -> Result, nsresult> { + let server_response = self + .server_response + .borrow_mut() + .take() + .ok_or(NS_ERROR_NOT_AVAILABLE)?; + Ok(server_response + .encapsulate(response) + .map_err(|_| NS_ERROR_FAILURE)? + .into_iter() + .collect()) + } +} + +#[xpcom(implement(nsIObliviousHttpServer), atomic)] +struct ObliviousHttpServer { + server: RefCell, +} + +impl ObliviousHttpServer { + xpcom_method!(get_encoded_config => GetEncodedConfig() -> ThinVec); + fn get_encoded_config(&self) -> Result, nsresult> { + let server = self.server.borrow_mut(); + Ok(server + .config() + .encode() + .map_err(|_| NS_ERROR_FAILURE)? + .into_iter() + .collect()) + } + + xpcom_method!(decapsulate => Decapsulate(enc_request: *const ThinVec) -> *const nsIObliviousHttpServerResponse); + fn decapsulate( + &self, + enc_request: &ThinVec, + ) -> Result, nsresult> { + let mut server = self.server.borrow_mut(); + let (request, server_response) = server + .decapsulate(enc_request) + .map_err(|_| NS_ERROR_FAILURE)?; + let oblivious_http_server_response = + ObliviousHttpServerResponse::allocate(InitObliviousHttpServerResponse { + request, + server_response: RefCell::new(Some(server_response)), + }); + oblivious_http_server_response + .query_interface::() + .ok_or(NS_ERROR_FAILURE) + } +} + +#[xpcom(implement(nsIObliviousHttp), atomic)] +struct ObliviousHttp {} + +impl ObliviousHttp { + xpcom_method!(encapsulate_request => EncapsulateRequest(encoded_config: *const ThinVec, + request: *const ThinVec) -> *const nsIObliviousHttpClientRequest); + fn encapsulate_request( + &self, + encoded_config: &ThinVec, + request: &ThinVec, + ) -> Result, nsresult> { + ohttp::init(); + + let client = ClientRequest::new(encoded_config).map_err(|_| NS_ERROR_FAILURE)?; + let (enc_request, response) = client.encapsulate(request).map_err(|_| NS_ERROR_FAILURE)?; + let oblivious_http_client_response = + ObliviousHttpClientResponse::allocate(InitObliviousHttpClientResponse { + response: RefCell::new(Some(response)), + }); + let response = oblivious_http_client_response + .query_interface::() + .ok_or(NS_ERROR_FAILURE)?; + let oblivious_http_client_request = + ObliviousHttpClientRequest::allocate(InitObliviousHttpClientRequest { + enc_request, + response, + }); + oblivious_http_client_request + .query_interface::() + .ok_or(NS_ERROR_FAILURE) + } + + xpcom_method!(server => Server() -> *const nsIObliviousHttpServer); + fn server(&self) -> Result, nsresult> { + ohttp::init(); + + let key_id: KeyId = rand::random::(); + let kem: Kem = Kem::X25519Sha256; + let symmetric = vec![ + SymmetricSuite::new(Kdf::HkdfSha256, Aead::Aes128Gcm), + SymmetricSuite::new(Kdf::HkdfSha256, Aead::ChaCha20Poly1305), + ]; + let key_config = KeyConfig::new(key_id, kem, symmetric).map_err(|_| NS_ERROR_FAILURE)?; + let server = Server::new(key_config).map_err(|_| NS_ERROR_FAILURE)?; + let oblivious_http_server = ObliviousHttpServer::allocate(InitObliviousHttpServer { + server: RefCell::new(server), + }); + oblivious_http_server + .query_interface::() + .ok_or(NS_ERROR_FAILURE) + } +} + +#[no_mangle] +pub extern "C" fn oblivious_http_constructor( + iid: *const xpcom::nsIID, + result: *mut *mut xpcom::reexports::libc::c_void, +) -> nserror::nsresult { + let oblivious_http = ObliviousHttp::allocate(InitObliviousHttp {}); + unsafe { oblivious_http.QueryInterface(iid, result) } +} diff --git a/netwerk/protocol/http/oblivious_http/src/oblivious_http.h b/netwerk/protocol/http/oblivious_http/src/oblivious_http.h new file mode 100644 index 0000000000..176b4909c1 --- /dev/null +++ b/netwerk/protocol/http/oblivious_http/src/oblivious_http.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/. */ + +#ifndef _oblivious_http_h_ +#define _oblivious_http_h_ + +#include "nsISupportsUtils.h" // for nsresult, etc. + +// {d581149e-3319-4563-b95e-46c64af5c4e8} +#define NS_OBLIVIOUS_HTTP_CID \ + { \ + 0xd581149e, 0x3319, 0x4563, { \ + 0xb9, 0x5e, 0x46, 0xc6, 0x4a, 0xf5, 0xc4, 0xe8 \ + } \ + } + +extern "C" { +nsresult oblivious_http_constructor(REFNSIID iid, void** result); +}; + +#endif // _oblivious_http_h_ diff --git a/netwerk/protocol/moz.build b/netwerk/protocol/moz.build new file mode 100644 index 0000000000..aa3200c856 --- /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"] +if CONFIG["MOZ_WIDGET_TOOLKIT"] == "gtk": + DIRS += ["gio"] +DIRS += ["http", "res", "viewsource", "websocket", "webtransport"] diff --git a/netwerk/protocol/res/ExtensionProtocolHandler.cpp b/netwerk/protocol/res/ExtensionProtocolHandler.cpp new file mode 100644 index 0000000000..d3b1a39164 --- /dev/null +++ b/netwerk/protocol/res/ExtensionProtocolHandler.cpp @@ -0,0 +1,1039 @@ +/* -*- 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/Promise.h" +#include "mozilla/dom/Promise-inl.h" +#include "mozilla/ExtensionPolicyService.h" +#include "mozilla/FileUtils.h" +#include "mozilla/ipc/FileDescriptor.h" +#include "mozilla/ipc/IPCStreamUtils.h" +#include "mozilla/ipc/URIUtils.h" +#include "mozilla/net/NeckoChild.h" +#include "mozilla/Omnijar.h" +#include "mozilla/RefPtr.h" +#include "mozilla/ResultExtensions.h" +#include "mozilla/Try.h" + +#include "FileDescriptorFile.h" +#include "LoadInfo.h" +#include "nsContentUtils.h" +#include "nsServiceManagerUtils.h" +#include "nsDirectoryServiceDefs.h" +#include "nsICancelable.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 final : public nsICancelable { + NS_DECL_ISUPPORTS + NS_DECL_NSICANCELABLE + + 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(); + } + + void SetupEventTarget() { + mMainThreadEventTarget = GetMainThreadSerialEventTarget(); + } + + // Get an input stream or file descriptor from the parent asynchronously. + RequestOrReason 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); + + private: + ~ExtensionStreamGetter() = default; + + nsCOMPtr mURI; + nsCOMPtr mLoadInfo; + nsCOMPtr mJarChannel; + nsCOMPtr mPump; + nsCOMPtr mJarFile; + nsCOMPtr mListener; + nsCOMPtr mChannel; + nsCOMPtr mMainThreadEventTarget; + bool mIsJarChannel; + bool mCanceled{false}; + nsresult mStatus{NS_OK}; +}; + +NS_IMPL_ISUPPORTS(ExtensionStreamGetter, nsICancelable) + +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, + getter_Transfers(prFileDesc)); + } +#else + nsresult rv = + mFile->OpenNSPRFileDesc(PR_RDONLY, 0, getter_Transfers(prFileDesc)); +#endif /* XP_WIN */ + + if (NS_SUCCEEDED(rv)) { + mFD = FileDescriptor(FileDescriptor::PlatformHandleType( + PR_FileDesc2NativeHandle(prFileDesc.get()))); + } + + 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. +RequestOrReason ExtensionStreamGetter::GetAsync(nsIStreamListener* aListener, + nsIChannel* aChannel) { + MOZ_ASSERT(IsNeckoChild()); + MOZ_ASSERT(mMainThreadEventTarget); + + mListener = aListener; + mChannel = aChannel; + + nsCOMPtr cancelableRequest(this); + + 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 RequestOrCancelable(WrapNotNull(cancelableRequest)); + } + + // 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 RequestOrCancelable(WrapNotNull(cancelableRequest)); +} + +// Called to cancel the ongoing async request. +NS_IMETHODIMP +ExtensionStreamGetter::Cancel(nsresult aStatus) { + if (mCanceled) { + return NS_OK; + } + + mCanceled = true; + mStatus = aStatus; + + if (mPump) { + mPump->CancelWithReason(aStatus, "ExtensionStreamGetter::Cancel"_ns); + mPump = nullptr; + } + + if (mIsJarChannel && mJarChannel) { + mJarChannel->CancelWithReason(aStatus, "ExtensionStreamGetter::Cancel"_ns); + } + + return NS_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->CancelWithReason(NS_BINDING_ABORTED, + "ExtensionStreamGetter::CancelRequest"_ns); +} + +// Handle an input stream sent from the parent. +void ExtensionStreamGetter::OnStream(already_AddRefed aStream) { + MOZ_ASSERT(IsNeckoChild()); + MOZ_ASSERT(mChannel); + MOZ_ASSERT(mListener); + MOZ_ASSERT(mMainThreadEventTarget); + + nsCOMPtr stream = std::move(aStream); + nsCOMPtr channel = std::move(mChannel); + + // We must keep an owning reference to the listener + // until we pass it on to AsyncRead. + nsCOMPtr listener = std::move(mListener); + + if (mCanceled) { + // The channel that has created this stream getter has been canceled. + CancelRequest(listener, channel, mStatus); + return; + } + + if (!stream) { + // The parent didn't send us back a stream. + CancelRequest(listener, channel, 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, channel, rv); + return; + } + + rv = pump->AsyncRead(listener); + if (NS_FAILED(rv)) { + CancelRequest(listener, channel, rv); + return; + } + + mPump = pump; +} + +// Handle an FD sent from the parent. +void ExtensionStreamGetter::OnFD(const FileDescriptor& aFD) { + MOZ_ASSERT(IsNeckoChild()); + MOZ_ASSERT(mChannel); + MOZ_ASSERT(mListener); + + nsCOMPtr channel = std::move(mChannel); + + // We must keep an owning reference to the listener + // until we pass it on to AsyncOpen. + nsCOMPtr listener = std::move(mListener); + + if (mCanceled) { + // The channel that has created this stream getter has been canceled. + CancelRequest(listener, channel, mStatus); + return; + } + + if (!aFD.IsValid()) { + // The parent didn't send us back a valid file descriptor. + CancelRequest(listener, channel, NS_ERROR_FILE_ACCESS_DENIED); + return; + } + + RefPtr fdFile = new FileDescriptorFile(aFD, mJarFile); + mJarChannel->SetJarFile(fdFile); + nsresult rv = mJarChannel->AsyncOpen(listener); + if (NS_FAILED(rv)) { + CancelRequest(listener, channel, 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) { + // 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 an + // allowlist subset are web-accessible (and cross-origin fetchable). + // The allowlist is checked using EPS.SourceMayLoadExtensionURI in + // BasePrincipal and nsScriptSecurityManager. + if (policy->IsWebAccessiblePath(url.FilePath())) { + if (policy->ManifestVersion() < 3) { + flags |= URI_LOADABLE_BY_ANYONE | URI_FETCHABLE_BY_ANYONE; + } else { + flags |= WEBEXT_URI_WEB_ACCESSIBLE; + } + } else if (policy->Type() == nsGkAtoms::theme) { + // Static themes cannot set web accessible resources, however using this + // flag here triggers SourceMayAccessPath calls necessary to allow another + // extension to access static theme resources in this extension. + flags |= WEBEXT_URI_WEB_ACCESSIBLE; + } 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::Handle aValue, ErrorResult& aRv, + 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)); + } + nsCOMPtr request(origChannel); + return RequestOrCancelable(WrapNotNull(request)); + }); + } else if (readyPromise) { + size_t matchIdx; + if (BinarySearchIf( + sStaticFileExtensions, 0, ArrayLength(sStaticFileExtensions), + [&ext](const char* aOther) { + return Compare(ext, nsDependentCString(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); + }); + + nsCOMPtr request(origChannel); + return RequestOrCancelable(WrapNotNull(request)); + }); + } 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, non-package builds don't use symlinks so we never need to + // allow a resource from outside of the extension dir. + return false; +#else + if (mozilla::IsPackagedBuild()) { + 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::IsPackagedBuild()); + 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::IsPackagedBuild()); + 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"); + } + + 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 { + return getter->GetAsync(listener, simpleChannel); + }); + + 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); + } + nsCOMPtr request(origChannel); + return RequestOrCancelable(WrapNotNull(request)); + }); + + 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..0824d7dd99 --- /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{false}; +#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{false}; +#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..5918b40fad --- /dev/null +++ b/netwerk/protocol/res/PageThumbProtocolHandler.cpp @@ -0,0 +1,361 @@ +/* -*- 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 "mozilla/Try.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" +#include "nsICancelable.h" + +#ifdef MOZ_PLACES +# include "nsIPlacesPreviewsHelperService.h" +#endif + +#define PAGE_THUMB_HOST "thumbnails" +#define PLACES_PREVIEWS_HOST "places-previews" +#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; + +NS_IMPL_QUERY_INTERFACE(PageThumbProtocolHandler, + nsISubstitutingProtocolHandler, nsIProtocolHandler, + 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); +} + +// 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. +PageThumbProtocolHandler::PageThumbProtocolHandler() + : SubstitutingProtocolHandler(PAGE_THUMB_SCHEME) {} + +RefPtr PageThumbProtocolHandler::NewStream( + nsIURI* aChildURI, bool* aTerminateSender) { + MOZ_ASSERT(!IsNeckoChild()); + MOZ_ASSERT(NS_IsMainThread()); + + if (!aChildURI || !aTerminateSender) { + return RemoteStreamPromise::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 RemoteStreamPromise::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) || + host.EqualsLiteral(PLACES_PREVIEWS_HOST))) { + return RemoteStreamPromise::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 RemoteStreamPromise::CreateAndReject(rv, __func__); + } + + nsAutoCString resolvedScheme; + rv = net_ExtractURLScheme(resolvedSpec, resolvedScheme); + if (NS_FAILED(rv) || !resolvedScheme.EqualsLiteral("file")) { + return RemoteStreamPromise::CreateAndReject(NS_ERROR_UNEXPECTED, __func__); + } + + nsCOMPtr ioService = do_GetIOService(&rv); + if (NS_FAILED(rv)) { + return RemoteStreamPromise::CreateAndReject(rv, __func__); + } + + nsCOMPtr resolvedURI; + rv = ioService->NewURI(resolvedSpec, nullptr, nullptr, + getter_AddRefs(resolvedURI)); + if (NS_FAILED(rv)) { + return RemoteStreamPromise::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 RemoteStreamPromise::CreateAndReject(rv, __func__); + } + + auto promiseHolder = MakeUnique>(); + RefPtr promise = promiseHolder->Ensure(__func__); + + nsCOMPtr mime = do_GetService("@mozilla.org/mime;1", &rv); + if (NS_FAILED(rv)) { + return RemoteStreamPromise::CreateAndReject(rv, __func__); + } + + nsAutoCString contentType; + rv = mime->GetTypeFromURI(aChildURI, contentType); + if (NS_FAILED(rv)) { + return RemoteStreamPromise::CreateAndReject(rv, __func__); + } + + rv = NS_DispatchBackgroundTask( + NS_NewRunnableFunction( + "PageThumbProtocolHandler::NewStream", + [contentType, channel, holder = std::move(promiseHolder)]() { + nsresult rv; + + nsCOMPtr fileChannel = + do_QueryInterface(channel, &rv); + if (NS_FAILED(rv)) { + holder->Reject(rv, __func__); + return; + } + + 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; + } + + RemoteStreamInfo info(inputStream, contentType, -1); + + holder->Resolve(std::move(info), __func__); + }), + NS_DISPATCH_EVENT_MAY_BLOCK); + + if (NS_FAILED(rv)) { + return RemoteStreamPromise::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) && + !aHost.EqualsLiteral(PLACES_PREVIEWS_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, aHost, 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 RemoteStreamGetter(aURI, aLoadInfo); + + NewSimpleChannel(aURI, aLoadInfo, streamGetter, aRetVal); + return Ok(); +} + +nsresult PageThumbProtocolHandler::GetThumbnailPath(const nsACString& aPath, + const nsACString& aHost, + 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; + } + + // 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; + } + + nsresult rv; + if (aHost.EqualsLiteral(PAGE_THUMB_HOST)) { + nsCOMPtr pageThumbsStorage = + do_GetService("@mozilla.org/thumbnails/pagethumbs-service;1", &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + // Use PageThumbsStorageService to get the local file path of the screenshot + // for the given URL. + rv = pageThumbsStorage->GetFilePathForURL(url, aThumbnailPath); +#ifdef MOZ_PLACES + } else if (aHost.EqualsLiteral(PLACES_PREVIEWS_HOST)) { + nsCOMPtr helper = + do_GetService("@mozilla.org/places/previews-helper;1", &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + rv = helper->GetFilePathForURL(url, aThumbnailPath); +#endif + } else { + MOZ_ASSERT_UNREACHABLE("Unknown thumbnail host"); + return NS_ERROR_UNEXPECTED; + } + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + return NS_OK; +} + +// static +void PageThumbProtocolHandler::NewSimpleChannel( + nsIURI* aURI, nsILoadInfo* aLoadinfo, RemoteStreamGetter* aStreamGetter, + nsIChannel** aRetVal) { + nsCOMPtr channel = NS_NewSimpleChannel( + aURI, aLoadinfo, aStreamGetter, + [](nsIStreamListener* listener, nsIChannel* simpleChannel, + RemoteStreamGetter* getter) -> RequestOrReason { + return getter->GetAsync(listener, simpleChannel, + &NeckoChild::SendGetPageThumbStream); + }); + + 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..da670d52d4 --- /dev/null +++ b/netwerk/protocol/res/PageThumbProtocolHandler.h @@ -0,0 +1,120 @@ +/* -*- 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 "mozilla/MozPromise.h" +#include "mozilla/net/RemoteStreamGetter.h" +#include "SubstitutingProtocolHandler.h" +#include "nsIInputStream.h" +#include "nsWeakReference.h" + +namespace mozilla { +namespace net { + +class RemoteStreamGetter; + +class PageThumbProtocolHandler final : public nsISubstitutingProtocolHandler, + public SubstitutingProtocolHandler, + public nsSupportsWeakReference { + public: + NS_DECL_ISUPPORTS_INHERITED + 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 RemoteStreamPromise + * The RemoteStreamPromise will resolve with an RemoteStreamInfo 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 aHost the host 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, const nsACString& aHost, + 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; + + // Gets a SimpleChannel that wraps the provided channel. + static void NewSimpleChannel(nsIURI* aURI, nsILoadInfo* aLoadinfo, + RemoteStreamGetter* aStreamGetter, + nsIChannel** aRetVal); +}; + +} // namespace net +} // namespace mozilla + +#endif /* PageThumbProtocolHandler_h___ */ diff --git a/netwerk/protocol/res/RemoteStreamGetter.cpp b/netwerk/protocol/res/RemoteStreamGetter.cpp new file mode 100644 index 0000000000..8bad84ef5e --- /dev/null +++ b/netwerk/protocol/res/RemoteStreamGetter.cpp @@ -0,0 +1,138 @@ +/* -*- 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 "RemoteStreamGetter.h" +#include "mozilla/MozPromise.h" +#include "mozilla/net/NeckoChild.h" +#include "mozilla/RefPtr.h" +#include "mozilla/ResultExtensions.h" +#include "nsContentUtils.h" +#include "nsIInputStreamPump.h" + +namespace mozilla { +namespace net { + +NS_IMPL_ISUPPORTS(RemoteStreamGetter, nsICancelable) + +RemoteStreamGetter::RemoteStreamGetter(nsIURI* aURI, nsILoadInfo* aLoadInfo) + : mURI(aURI), mLoadInfo(aLoadInfo) { + MOZ_ASSERT(aURI); + MOZ_ASSERT(aLoadInfo); +} + +// Request an input stream from the parent. +RequestOrReason RemoteStreamGetter::GetAsync(nsIStreamListener* aListener, + nsIChannel* aChannel, + Method aMethod) { + MOZ_ASSERT(IsNeckoChild()); + MOZ_ASSERT(aMethod); + + mListener = aListener; + mChannel = aChannel; + + nsCOMPtr cancelableRequest(this); + + RefPtr self = this; + LoadInfoArgs loadInfoArgs; + nsresult rv = ipc::LoadInfoToLoadInfoArgs(mLoadInfo, &loadInfoArgs); + if (NS_FAILED(rv)) { + return Err(rv); + } + + (gNeckoChild->*aMethod)(mURI, loadInfoArgs) + ->Then( + GetMainThreadSerialEventTarget(), __func__, + [self](const Maybe& info) { self->OnStream(info); }, + [self](const mozilla::ipc::ResponseRejectReason) { + self->OnStream(Nothing()); + }); + return RequestOrCancelable(WrapNotNull(cancelableRequest)); +} + +// Called to cancel the ongoing async request. +NS_IMETHODIMP +RemoteStreamGetter::Cancel(nsresult aStatus) { + if (mCanceled) { + return NS_OK; + } + + mCanceled = true; + mStatus = aStatus; + + if (mPump) { + mPump->Cancel(aStatus); + mPump = nullptr; + } + + return NS_OK; +} + +// static +void RemoteStreamGetter::CancelRequest(nsIStreamListener* aListener, + nsIChannel* aChannel, nsresult aResult) { + MOZ_ASSERT(aListener); + MOZ_ASSERT(aChannel); + + aListener->OnStartRequest(aChannel); + aListener->OnStopRequest(aChannel, aResult); + aChannel->CancelWithReason(NS_BINDING_ABORTED, + "RemoteStreamGetter::CancelRequest"_ns); +} + +// Handle an input stream sent from the parent. +void RemoteStreamGetter::OnStream(const Maybe& aStreamInfo) { + MOZ_ASSERT(IsNeckoChild()); + MOZ_ASSERT(mChannel); + MOZ_ASSERT(mListener); + + nsCOMPtr channel = std::move(mChannel); + + // We must keep an owning reference to the listener until we pass it on + // to AsyncRead. + nsCOMPtr listener = mListener.forget(); + + if (aStreamInfo.isNothing()) { + // The parent didn't send us back a stream. + CancelRequest(listener, channel, NS_ERROR_FILE_ACCESS_DENIED); + return; + } + + if (mCanceled) { + // The channel that has created this stream getter has been canceled. + CancelRequest(listener, channel, mStatus); + return; + } + + nsCOMPtr stream = std::move(aStreamInfo.ref().inputStream()); + if (!stream) { + // We somehow failed to get a stream, so just cancel the request. + CancelRequest(listener, channel, mStatus); + return; + } + + nsCOMPtr pump; + nsresult rv = + NS_NewInputStreamPump(getter_AddRefs(pump), stream.forget(), 0, 0, false, + GetMainThreadSerialEventTarget()); + if (NS_FAILED(rv)) { + CancelRequest(listener, channel, rv); + return; + } + + channel->SetContentType(aStreamInfo.ref().contentType()); + channel->SetContentLength(aStreamInfo.ref().contentLength()); + + rv = pump->AsyncRead(listener); + if (NS_FAILED(rv)) { + CancelRequest(listener, channel, rv); + return; + } + + mPump = pump; +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/res/RemoteStreamGetter.h b/netwerk/protocol/res/RemoteStreamGetter.h new file mode 100644 index 0000000000..de2bd6b601 --- /dev/null +++ b/netwerk/protocol/res/RemoteStreamGetter.h @@ -0,0 +1,68 @@ +/* -*- 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 RemoteStreamGetter_h___ +#define RemoteStreamGetter_h___ + +#include "nsIChannel.h" +#include "nsIInputStreamPump.h" +#include "nsIStreamListener.h" +#include "nsIInputStream.h" +#include "nsICancelable.h" +#include "SimpleChannel.h" +#include "mozilla/net/NeckoChannelParams.h" +#include "mozilla/net/NeckoChild.h" +#include "mozilla/Maybe.h" + +class nsILoadInfo; + +namespace mozilla { +namespace net { + +using RemoteStreamPromise = + mozilla::MozPromise; +using Method = RefPtr< + MozPromise, ipc::ResponseRejectReason, true>> ( + PNeckoChild::*)(nsIURI*, const LoadInfoArgs&); + +/** + * Helper class used with SimpleChannel to asynchronously obtain an input + * stream and metadata from the parent for a remote protocol load from the + * child. + */ +class RemoteStreamGetter final : public nsICancelable { + NS_DECL_ISUPPORTS + NS_DECL_NSICANCELABLE + + public: + RemoteStreamGetter(nsIURI* aURI, nsILoadInfo* aLoadInfo); + + // Get an input stream from the parent asynchronously. + RequestOrReason GetAsync(nsIStreamListener* aListener, nsIChannel* aChannel, + Method aMethod); + + // Handle an input stream being returned from the parent + void OnStream(const Maybe& aStreamInfo); + + static void CancelRequest(nsIStreamListener* aListener, nsIChannel* aChannel, + nsresult aResult); + + private: + ~RemoteStreamGetter() = default; + + nsCOMPtr mURI; + nsCOMPtr mLoadInfo; + nsCOMPtr mListener; + nsCOMPtr mChannel; + nsCOMPtr mPump; + bool mCanceled{false}; + nsresult mStatus{NS_OK}; +}; + +} // namespace net +} // namespace mozilla + +#endif /* RemoteStreamGetter_h___ */ diff --git a/netwerk/protocol/res/SubstitutingJARURI.h b/netwerk/protocol/res/SubstitutingJARURI.h new file mode 100644 index 0000000000..85976d4908 --- /dev/null +++ b/netwerk/protocol/res/SubstitutingJARURI.h @@ -0,0 +1,229 @@ +/* -*- 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 "nsIStandardURL.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 GetHasUserPass(bool* aHasUserPass) override { + return !mSource ? NS_ERROR_NULL_POINTER + : mSource->GetHasUserPass(aHasUserPass); + } + NS_IMETHOD GetHasQuery(bool* aHasQuery) override { + return !mSource ? NS_ERROR_NULL_POINTER : mSource->GetHasQuery(aHasQuery); + } + 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; + NS_IMETHOD_(void) Serialize(mozilla::ipc::URIParams& aParams) override; + + private: + nsresult Clone(nsIURI** aURI); + nsresult SetSpecInternal(const nsACString& input) { + return NS_ERROR_NOT_IMPLEMENTED; + } + nsresult SetScheme(const nsACString& input) { + return NS_ERROR_NOT_IMPLEMENTED; + } + nsresult SetUserPass(const nsACString& aInput); + nsresult SetUsername(const nsACString& input) { + return NS_ERROR_NOT_IMPLEMENTED; + } + nsresult SetPassword(const nsACString& input) { + return NS_ERROR_NOT_IMPLEMENTED; + } + nsresult SetHostPort(const nsACString& aValue) { + return NS_ERROR_NOT_IMPLEMENTED; + } + nsresult SetHost(const nsACString& input) { return NS_ERROR_NOT_IMPLEMENTED; } + nsresult SetPort(int32_t aPort); + nsresult SetPathQueryRef(const nsACString& input) { + return NS_ERROR_NOT_IMPLEMENTED; + } + nsresult SetRef(const nsACString& input) { return NS_ERROR_NOT_IMPLEMENTED; } + nsresult SetFilePath(const nsACString& input) { + return NS_ERROR_NOT_IMPLEMENTED; + } + nsresult SetQuery(const nsACString& input) { + return NS_ERROR_NOT_IMPLEMENTED; + } + nsresult SetQueryWithEncoding(const nsACString& input, + const mozilla::Encoding* encoding) { + return NS_ERROR_NOT_IMPLEMENTED; + } + bool Deserialize(const mozilla::ipc::URIParams& aParams); + nsresult ReadPrivate(nsIObjectInputStream* aStream); + + public: + class Mutator final : public nsIURIMutator, + public BaseURIMutator, + public nsISerializable { + NS_DECL_ISUPPORTS + NS_FORWARD_SAFE_NSIURISETTERS_RET(mURI) + NS_DEFINE_NSIMUTATOR_COMMON + + // nsISerializable overrides + NS_IMETHOD + Write(nsIObjectOutputStream* aOutputStream) override { + return NS_ERROR_NOT_IMPLEMENTED; + } + + NS_IMETHOD Read(nsIObjectInputStream* aStream) override { + return InitFromInputStream(aStream); + } + + explicit Mutator() = default; + + private: + virtual ~Mutator() = default; + + friend SubstitutingJARURI; + }; + + friend BaseURIMutator; +}; + +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..523aaa435d --- /dev/null +++ b/netwerk/protocol/res/SubstitutingProtocolHandler.cpp @@ -0,0 +1,723 @@ +/* -*- 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 "mozilla/ipc/URIUtils.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(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); + if (!substHandler) { + return NS_ERROR_NO_INTERFACE; + } + + 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); +} + +NS_IMETHODIMP +SubstitutingJARURI::Mutate(nsIURIMutator** aMutator) { + RefPtr mutator = + new SubstitutingJARURI::Mutator(); + nsresult rv = mutator->InitFromURI(this); + if (NS_FAILED(rv)) { + return rv; + } + mutator.forget(aMutator); + return NS_OK; +} + +void SubstitutingJARURI::Serialize(mozilla::ipc::URIParams& aParams) { + using namespace mozilla::ipc; + + SubstitutingJARURIParams params; + URIParams source; + URIParams resolved; + + mSource->Serialize(source); + mResolved->Serialize(resolved); + params.source() = source; + params.resolved() = resolved; + aParams = params; +} + +// 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; +} + +nsresult SubstitutingJARURI::Clone(nsIURI** aURI) { + RefPtr uri = new SubstitutingJARURI(); + // SubstitutingJARURI's mSource/mResolved isn't mutable. + uri->mSource = mSource; + uri->mResolved = mResolved; + uri.forget(aURI); + + return NS_OK; +} + +nsresult SubstitutingJARURI::SetUserPass(const nsACString& aInput) { + // If setting same value in mSource, return NS_OK; + if (!mSource) { + return NS_ERROR_NULL_POINTER; + } + + nsAutoCString sourceUserPass; + nsresult rv = mSource->GetUserPass(sourceUserPass); + if (NS_FAILED(rv)) { + return rv; + } + if (aInput.Equals(sourceUserPass)) { + return NS_OK; + } + return NS_ERROR_FAILURE; +} + +nsresult SubstitutingJARURI::SetPort(int32_t aPort) { + // If setting same value in mSource, return NS_OK; + if (!mSource) { + return NS_ERROR_NULL_POINTER; + } + + int32_t sourcePort = -1; + nsresult rv = mSource->GetPort(&sourcePort); + if (NS_FAILED(rv)) { + return rv; + } + if (aPort == sourcePort) { + return NS_OK; + } + return NS_ERROR_FAILURE; +} + +bool SubstitutingJARURI::Deserialize(const mozilla::ipc::URIParams& aParams) { + using namespace mozilla::ipc; + + if (aParams.type() != URIParams::TSubstitutingJARURIParams) { + NS_ERROR("Received unknown parameters from the other process!"); + return false; + } + + const SubstitutingJARURIParams& jarUriParams = + aParams.get_SubstitutingJARURIParams(); + + nsCOMPtr source = DeserializeURI(jarUriParams.source()); + nsresult rv; + mSource = do_QueryInterface(source, &rv); + if (NS_FAILED(rv)) { + return false; + } + nsCOMPtr jarUri = DeserializeURI(jarUriParams.resolved()); + mResolved = do_QueryInterface(jarUri, &rv); + return NS_SUCCEEDED(rv); +} + +nsresult SubstitutingJARURI::ReadPrivate(nsIObjectInputStream* aStream) { + return Read(aStream); +} + +NS_IMPL_CLASSINFO(SubstitutingJARURI, nullptr, 0, 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) + +NS_IMPL_NSIURIMUTATOR_ISUPPORTS(SubstitutingJARURI::Mutator, nsIURISetters, + nsIURIMutator, nsISerializable) + +SubstitutingProtocolHandler::SubstitutingProtocolHandler(const char* aScheme, + bool aEnforceFileOrJar) + : mScheme(aScheme), + mSubstitutionsLock("SubstitutingProtocolHandler::mSubstitutions"), + mSubstitutions(16), + mEnforceFileOrJar(aEnforceFileOrJar) { + 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 (const auto& substitutionEntry : mSubstitutions) { + const SubstitutionEntry& entry = substitutionEntry.GetData(); + nsCOMPtr uri = entry.baseURI; + SerializedURI serialized; + if (uri) { + nsresult rv = uri->GetSpec(serialized.spec); + NS_ENSURE_SUCCESS(rv, rv); + } + SubstitutionMapping substitution = {mScheme, + nsCString(substitutionEntry.GetKey()), + 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::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(&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); + mSubstitutions.InsertOrUpdate(root, SubstitutionEntry{baseURI, 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); + mSubstitutions.InsertOrUpdate(root, SubstitutionEntry{newBaseURI, 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..6bdb27f38a --- /dev/null +++ b/netwerk/protocol/res/SubstitutingProtocolHandler.h @@ -0,0 +1,129 @@ +/* -*- 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 "nsTHashMap.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: + explicit SubstitutingProtocolHandler(const char* aScheme, + bool aEnforceFileOrJar = true); + + 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& aMappings); + + 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 { + nsCOMPtr baseURI; + uint32_t flags = 0; + }; + + // 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; + + RWLock mSubstitutionsLock; + nsTHashMap mSubstitutions + MOZ_GUARDED_BY(mSubstitutionsLock); + 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..63407eebd8 --- /dev/null +++ b/netwerk/protocol/res/moz.build @@ -0,0 +1,42 @@ +# -*- 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", + "RemoteStreamGetter.h", + "SubstitutingJARURI.h", + "SubstitutingProtocolHandler.h", + "SubstitutingURL.h", +] + +EXPORTS += [ + "nsResProtocolHandler.h", +] + +UNIFIED_SOURCES += [ + "ExtensionProtocolHandler.cpp", + "nsResProtocolHandler.cpp", + "PageThumbProtocolHandler.cpp", + "RemoteStreamGetter.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..208baedf2b --- /dev/null +++ b/netwerk/protocol/res/nsResProtocolHandler.cpp @@ -0,0 +1,195 @@ +/* -*- 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/ClearOnShutdown.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..50e790a53a --- /dev/null +++ b/netwerk/protocol/res/nsResProtocolHandler.h @@ -0,0 +1,78 @@ +/* -*- 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", + /* 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..036142ba3d --- /dev/null +++ b/netwerk/protocol/viewsource/nsIViewSourceChannel.idl @@ -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/. */ + +#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..f2428a5744 --- /dev/null +++ b/netwerk/protocol/viewsource/nsViewSourceChannel.cpp @@ -0,0 +1,1220 @@ +/* -*- 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 "nsIExternalProtocolHandler.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(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 + +static nsresult WillUseExternalProtocolHandler(nsIIOService* aIOService, + const char* aScheme) { + nsCOMPtr handler; + nsresult rv = + aIOService->GetProtocolHandler(aScheme, getter_AddRefs(handler)); + if (NS_FAILED(rv)) { + return rv; + } + + nsCOMPtr externalHandler = + do_QueryInterface(handler); + // We should not allow view-source to open any external app. + if (externalHandler) { + NS_WARNING(nsPrintfCString("blocking view-source:%s:", aScheme).get()); + return NS_ERROR_MALFORMED_URI; + } + + return NS_OK; +} + +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; + } + + rv = WillUseExternalProtocolHandler(pService, scheme.get()); + if (NS_FAILED(rv)) { + return rv; + } + + 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); + 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::SetCanceledReason( + const nsACString& aReason) { + return nsIViewSourceChannel::SetCanceledReasonImpl(aReason); +} + +NS_IMETHODIMP nsViewSourceChannel::GetCanceledReason(nsACString& aReason) { + return nsIViewSourceChannel::GetCanceledReasonImpl(aReason); +} + +NS_IMETHODIMP nsViewSourceChannel::CancelWithReason(nsresult aStatus, + const nsACString& aReason) { + return nsIViewSourceChannel::CancelWithReasonImpl(aStatus, aReason); +} + +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) != 0; + + 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.IsEmpty()) { + 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(nsITransportSecurityInfo** 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 = do_AddRef(mBaseURI).take(); + 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::GetBrowserId(uint64_t* aId) { + return !mHttpChannel ? NS_ERROR_NULL_POINTER + : mHttpChannel->GetBrowserId(aId); +} + +NS_IMETHODIMP +nsViewSourceChannel::SetBrowserId(uint64_t aId) { + return !mHttpChannel ? NS_ERROR_NULL_POINTER + : mHttpChannel->SetBrowserId(aId); +} + +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::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); +} + +NS_IMETHODIMP nsViewSourceChannel::SetClassicScriptHintCharset( + const nsAString& aClassicScriptHintCharset) { + return !mHttpChannel ? NS_ERROR_NULL_POINTER + : mHttpChannel->SetClassicScriptHintCharset( + aClassicScriptHintCharset); +} + +NS_IMETHODIMP nsViewSourceChannel::GetClassicScriptHintCharset( + nsAString& aClassicScriptHintCharset) { + return !mHttpChannel ? NS_ERROR_NULL_POINTER + : mHttpChannel->GetClassicScriptHintCharset( + aClassicScriptHintCharset); +} + +NS_IMETHODIMP nsViewSourceChannel::SetDocumentCharacterSet( + const nsAString& aDocumentCharacterSet) { + return !mHttpChannel + ? NS_ERROR_NULL_POINTER + : mHttpChannel->SetDocumentCharacterSet(aDocumentCharacterSet); +} + +NS_IMETHODIMP nsViewSourceChannel::GetDocumentCharacterSet( + nsAString& aDocumentCharacterSet) { + return !mHttpChannel + ? NS_ERROR_NULL_POINTER + : mHttpChannel->GetDocumentCharacterSet(aDocumentCharacterSet); +} +// 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, + bool aIsWarning) { + if (!mHttpChannel) { + NS_WARNING( + "nsViewSourceChannel::LogBlockedCORSRequest mHttpChannel is null"); + return NS_ERROR_UNEXPECTED; + } + return mHttpChannel->LogBlockedCORSRequest(aMessage, aCategory, aIsWarning); +} + +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); +} + +// FIXME: Should this forward to mHttpChannel? This was previously handled by a +// default empty implementation. +void nsViewSourceChannel::SetSource( + mozilla::UniquePtr aSource) {} + +const nsTArray& +nsViewSourceChannel::PreferredAlternativeDataTypes() { + if (mCacheInfoChannel) { + return mCacheInfoChannel->PreferredAlternativeDataTypes(); + } + return mEmptyArray; +} + +void nsViewSourceChannel::DoDiagnosticAssertWhenOnStopNotCalledOnDestroy() { + if (mHttpChannelInternal) { + mHttpChannelInternal->DoDiagnosticAssertWhenOnStopNotCalledOnDestroy(); + } +} + +// FIXME: Should this forward to mHttpChannelInternal? This was previously +// handled by a default empty implementation. +void nsViewSourceChannel::SetConnectionInfo( + mozilla::net::nsHttpConnectionInfo* aInfo) {} + +// 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); + + nsAutoCString scheme; + rv = newChannelOrigURI->GetScheme(scheme); + if (NS_FAILED(rv)) { + return rv; + } + + nsCOMPtr ioService(do_GetIOService(&rv)); + if (NS_FAILED(rv)) { + return rv; + } + + rv = WillUseExternalProtocolHandler(ioService, scheme.get()); + if (NS_FAILED(rv)) { + return 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..87c490f6b7 --- /dev/null +++ b/netwerk/protocol/viewsource/nsViewSourceChannel.h @@ -0,0 +1,98 @@ +/* -*- 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 "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 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_NSIUPLOADCHANNEL(mUploadChannel) + NS_FORWARD_SAFE_NSIFORMPOSTACTIONCHANNEL(mPostChannel) + NS_FORWARD_SAFE_NSIHTTPCHANNELINTERNAL(mHttpChannelInternal) + + // nsViewSourceChannel methods: + nsViewSourceChannel() = default; + + [[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 mUploadChannel; + nsCOMPtr mPostChannel; + nsCOMPtr mChildChannel; + nsCOMPtr mListener; + nsCOMPtr mOriginalURI; + nsCOMPtr mBaseURI; + nsCString mContentType; + bool mIsDocument{false}; // keeps track of the LOAD_DOCUMENT_URI flag + bool mOpened{false}; + bool mIsSrcdocChannel{false}; +}; + +#endif /* nsViewSourceChannel_h___ */ diff --git a/netwerk/protocol/viewsource/nsViewSourceHandler.cpp b/netwerk/protocol/viewsource/nsViewSourceHandler.cpp new file mode 100644 index 0000000000..f7b440d4d7 --- /dev/null +++ b/netwerk/protocol/viewsource/nsViewSourceHandler.cpp @@ -0,0 +1,141 @@ +/* -*- 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::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(&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..6e9589afb2 --- /dev/null +++ b/netwerk/protocol/websocket/BaseWebSocketChannel.cpp @@ -0,0 +1,378 @@ +/* -*- 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::~BaseWebSocketChannel() { + NS_ReleaseOnMainThread("BaseWebSocketChannel::mLoadGroup", + mLoadGroup.forget()); + NS_ReleaseOnMainThread("BaseWebSocketChannel::mLoadInfo", mLoadInfo.forget()); + nsCOMPtr target; + { + auto lock = mTargetThread.Lock(); + target.swap(*lock); + } + NS_ReleaseOnMainThread("BaseWebSocketChannel::mTargetThread", + target.forget()); +} + +//----------------------------------------------------------------------------- +// 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)); + *aNotificationCallbacks = do_AddRef(mCallbacks).take(); + 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)); + *aLoadGroup = do_AddRef(mLoadGroup).take(); + 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) { + *aLoadInfo = do_AddRef(mLoadInfo).take(); + 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::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(nsISerialEventTarget* aTargetThread) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aTargetThread); + MOZ_ASSERT(!mWasOpened, "Should not be called after AsyncOpen!"); + MOZ_ASSERT(aTargetThread); + + auto lock = mTargetThread.Lock(); + MOZ_ASSERT(!lock.ref(), + "Delivery target should be set once, before AsyncOpen"); + lock.ref() = aTargetThread; + return NS_OK; +} + +NS_IMETHODIMP +BaseWebSocketChannel::GetDeliveryTarget(nsISerialEventTarget** aTargetThread) { + MOZ_ASSERT(NS_IsMainThread()); + + nsCOMPtr target = GetTargetThread(); + if (!target) { + target = GetCurrentSerialEventTarget(); + } + target.forget(aTargetThread); + return NS_OK; +} + +already_AddRefed BaseWebSocketChannel::GetTargetThread() { + nsCOMPtr target; + auto lock = mTargetThread.Lock(); + target = *lock; + return target.forget(); +} + +bool BaseWebSocketChannel::IsOnTargetThread() { + nsCOMPtr target = GetTargetThread(); + if (!target) { + MOZ_ASSERT(false); + return false; + } + + bool isOnTargetThread = false; + nsresult rv = target->IsOnCurrentThread(&isOnTargetThread); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + return NS_FAILED(rv) ? false : isOnTargetThread; +} + +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..ee05e5bf4c --- /dev/null +++ b/netwerk/protocol/websocket/BaseWebSocketChannel.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_BaseWebSocketChannel_h +#define mozilla_net_BaseWebSocketChannel_h + +#include "mozilla/DataMutex.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; + + already_AddRefed GetTargetThread(); + bool IsOnTargetThread(); + + class ListenerAndContextContainer final { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(ListenerAndContextContainer) + + ListenerAndContextContainer(nsIWebSocketListener* aListener, + nsISupports* aContext); + + nsCOMPtr mListener; + nsCOMPtr mContext; + + private: + ~ListenerAndContextContainer(); + }; + + protected: + virtual ~BaseWebSocketChannel(); + nsCOMPtr mOriginalURI; + nsCOMPtr mURI; + RefPtr mListenerMT; + nsCOMPtr mCallbacks; + nsCOMPtr mLoadGroup; + nsCOMPtr mLoadInfo; + nsCOMPtr mServerTransportProvider; + + // Used to ensure atomicity of mTargetThread. + // Set before AsyncOpen via RetargetDeliveryTo or in AsyncOpen, never changed + // after AsyncOpen + DataMutex> mTargetThread{ + "BaseWebSocketChannel::EventTargetMutex"}; + + nsCString mProtocol; + nsCString mOrigin; + + nsCString mNegotiatedExtensions; + + uint32_t mWasOpened : 1; + uint32_t mClientSetPingInterval : 1; + uint32_t mClientSetPingTimeout : 1; + + Atomic mEncrypted; + bool mPingForced; + bool mIsServerSide; + + Atomic 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..ff902ae835 --- /dev/null +++ b/netwerk/protocol/websocket/IPCTransportProvider.cpp @@ -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/. */ + +#include "mozilla/net/IPCTransportProvider.h" + +#include "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; } + +NS_IMETHODIMP +TransportProviderParent::OnWebSocketConnectionAvailable( + WebSocketConnectionBase* aConnection) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +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..de54be127a --- /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..42ff3589a5 --- /dev/null +++ b/netwerk/protocol/websocket/PTransportProvider.ipdl @@ -0,0 +1,30 @@ +/* -*- 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; + +include "mozilla/net/IPCTransportProvider.h"; + +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. + */ + +[ManualDealloc, ChildImpl="TransportProviderChild", ParentImpl="TransportProviderParent"] +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..bb263049f0 --- /dev/null +++ b/netwerk/protocol/websocket/PWebSocket.ipdl @@ -0,0 +1,67 @@ +/* -*- 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 "mozilla/net/WebSocketChannelParent.h"; +include "mozilla/net/WebSocketChannelChild.h"; + +using class IPC::SerializedLoadContext from "SerializedLoadContext.h"; +[RefCounted] using class nsIURI from "mozilla/ipc/URIUtils.h"; + +namespace mozilla { +namespace net { + +[ManualDealloc, ChildImpl="WebSocketChannelChild", ParentImpl="WebSocketChannelParent"] +async protocol PWebSocket +{ + manager PNecko; + +parent: + // Forwarded methods corresponding to methods on nsIWebSocketChannel + async AsyncOpen(nullable nsIURI aURI, + nsCString aOrigin, + OriginAttributes aOriginAttributes, + 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(nsCString aMsg, bool aMoreData); + async OnBinaryMessageAvailable(nsCString 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/PWebSocketConnection.ipdl b/netwerk/protocol/websocket/PWebSocketConnection.ipdl new file mode 100644 index 0000000000..b378320a2b --- /dev/null +++ b/netwerk/protocol/websocket/PWebSocketConnection.ipdl @@ -0,0 +1,33 @@ +/* -*- 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 "mozilla/ipc/TransportSecurityInfoUtils.h"; + +[RefCounted] using class nsITransportSecurityInfo from "nsITransportSecurityInfo.h"; + +namespace mozilla { +namespace net { + +[ChildProc=Socket] +protocol PWebSocketConnection +{ +parent: + async OnTransportAvailable(nullable nsITransportSecurityInfo aSecurityInfo); + async OnError(nsresult aStatus); + async OnTCPClosed(); + async OnDataReceived(uint8_t[] aData); + async OnUpgradeFailed(nsresult aReason); + +child: + async WriteOutputData(uint8_t[] aData); + async StartReading(); + async DrainSocketData(); + + 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..d48224b337 --- /dev/null +++ b/netwerk/protocol/websocket/PWebSocketEventListener.ipdl @@ -0,0 +1,53 @@ +/* -*- 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 { + +[ManualDealloc] +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..a1ebf85d8e --- /dev/null +++ b/netwerk/protocol/websocket/WebSocketChannel.cpp @@ -0,0 +1,4336 @@ +/* -*- 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 + +#include "WebSocketChannel.h" + +#include "WebSocketConnectionBase.h" +#include "WebSocketFrame.h" +#include "WebSocketLog.h" +#include "mozilla/Atomics.h" +#include "mozilla/Attributes.h" +#include "mozilla/Base64.h" +#include "mozilla/EndianUtils.h" +#include "mozilla/MathAlgorithms.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/StaticMutex.h" +#include "mozilla/StaticPrefs_privacy.h" +#include "mozilla/Telemetry.h" +#include "mozilla/TimeStamp.h" +#include "mozilla/Utf8.h" +#include "mozilla/net/WebSocketEventService.h" +#include "nsAlgorithm.h" +#include "nsCRT.h" +#include "nsCharSeparatedTokenizer.h" +#include "nsComponentManagerUtils.h" +#include "nsError.h" +#include "nsIAsyncVerifyRedirectCallback.h" +#include "nsICancelable.h" +#include "nsIChannel.h" +#include "nsIClassOfService.h" +#include "nsICryptoHash.h" +#include "nsIDNSRecord.h" +#include "nsIDNSService.h" +#include "nsIDashboardEventNotifier.h" +#include "nsIEventTarget.h" +#include "nsIHttpChannel.h" +#include "nsIIOService.h" +#include "nsINSSErrorsService.h" +#include "nsINetworkLinkService.h" +#include "nsINode.h" +#include "nsIObserverService.h" +#include "nsIPrefBranch.h" +#include "nsIProtocolHandler.h" +#include "nsIProtocolProxyService.h" +#include "nsIProxiedChannel.h" +#include "nsIProxyInfo.h" +#include "nsIRandomGenerator.h" +#include "nsIRunnable.h" +#include "nsISocketTransport.h" +#include "nsITLSSocketControl.h" +#include "nsITransportProvider.h" +#include "nsITransportSecurityInfo.h" +#include "nsIURI.h" +#include "nsIURIMutator.h" +#include "nsNetCID.h" +#include "nsNetUtil.h" +#include "nsProxyRelease.h" +#include "nsServiceManagerUtils.h" +#include "nsSocketTransportService2.h" +#include "nsStringStream.h" +#include "nsThreadUtils.h" +#include "plbase64.h" +#include "prmem.h" +#include "prnetdb.h" +#include "zlib.h" + +// 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::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/path/port. +class FailDelay { + public: + FailDelay(nsCString address, nsCString path, int32_t port) + : mAddress(std::move(address)), mPath(std::move(path)), 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, path=%s, port=%d: incremented delay " + "to " + "%" PRIu32, + mAddress.get(), mPath.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) + nsCString mPath; + 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, nsCString& path, int32_t port) { + if (mDelaysDisabled) return; + + UniquePtr record(new FailDelay(address, path, 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, nsCString& path, 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->mPath.Equals(path) && + 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; + } + 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->mPath, 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; + MutexAutoLock lock(ws->mMutex); + 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, nsCString& path, 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->mPath.Equals(path) && + 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 hostFound = (sManager->IndexOf(ws->mAddress, ws->mOriginSuffix) >= 0); + + uint32_t failIndex = 0; + FailDelay* fail = sManager->mFailures.Lookup(ws->mAddress, ws->mPath, + ws->mPort, &failIndex); + bool existingFail = fail != nullptr; + + // Always add ourselves to queue, even if we'll connect immediately + UniquePtr newdata( + new nsOpenConn(ws->mAddress, ws->mOriginSuffix, existingFail, ws)); + + // If a connection has not previously failed then prioritize it over + // connections that have + if (existingFail) { + sManager->mQueue.AppendElement(std::move(newdata)); + } else { + uint32_t insertionIndex = sManager->IndexOfFirstFailure(); + MOZ_ASSERT(insertionIndex <= sManager->mQueue.Length(), + "Insertion index outside bounds"); + sManager->mQueue.InsertElementAt(insertionIndex, std::move(newdata)); + } + + if (hostFound) { + 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->mPath, + 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, aChannel->mOriginSuffix); + } + + // 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->mPath, 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, %s, %d completed" + " [this=%p]", + aChannel->mAddress.get(), aChannel->mPath.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, %s, %d failed: [this=%p]", + aChannel->mAddress.get(), aChannel->mPath.get(), + (int)aChannel->mPort, aChannel)); + sManager->mFailures.Add(aChannel->mAddress, aChannel->mPath, + aChannel->mPort); + } + } + + if (NS_IsMainThread()) { + ContinueOnStopSession(aChannel, aReason); + } else { + NS_DispatchToMainThread(NS_NewRunnableFunction( + "nsWSAdmissionManager::ContinueOnStopSession", + [channel = RefPtr{aChannel}, reason = aReason]() { + StaticMutexAutoLock lock(sLock); + if (!sManager) { + return; + } + + nsWSAdmissionManager::ContinueOnStopSession(channel, reason); + })); + } + } + + static void ContinueOnStopSession(WebSocketChannel* aChannel, + nsresult aReason) { + sLock.AssertCurrentThreadOwns(); + MOZ_ASSERT(NS_IsMainThread(), "not main thread"); + + if (!aChannel->mConnecting) { + return; + } + + // 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. +#ifdef DEBUG + { + MutexAutoLock lock(aChannel->mMutex); + MOZ_ASSERT( + NS_FAILED(aReason) || aChannel->mScriptCloseCode == CLOSE_GOING_AWAY, + "websocket closed while connecting w/o failing?"); + } +#endif + Unused << aReason; + + 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, aChannel->mOriginSuffix); + } + } + + 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, nsCString& originSuffix, bool failed, + WebSocketChannel* channel) + : mAddress(addr), + mOriginSuffix(originSuffix), + mFailed(failed), + mChannel(channel) { + MOZ_COUNT_CTOR(nsOpenConn); + } + MOZ_COUNTED_DTOR(nsOpenConn) + + nsCString mAddress; + nsCString mOriginSuffix; + bool mFailed = false; + RefPtr mChannel; + }; + + void ConnectNext(nsCString& hostName, nsCString& originSuffix) { + MOZ_ASSERT(NS_IsMainThread(), "not main thread"); + + int32_t index = IndexOf(hostName, originSuffix); + 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& aAddress, nsCString& aOriginSuffix) { + for (uint32_t i = 0; i < mQueue.Length(); i++) { + bool isPartitioned = StaticPrefs::privacy_partition_network_state() || + StaticPrefs::privacy_firstparty_isolate(); + if (aAddress == (mQueue[i])->mAddress && + (!isPartitioned || aOriginSuffix == (mQueue[i])->mOriginSuffix)) { + 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; + } + + // Returns the index of the first entry that failed, or else the last entry if + // none found + uint32_t IndexOfFirstFailure() { + for (uint32_t i = 0; i < mQueue.Length(); i++) { + if (mQueue[i]->mFailed) return i; + } + return mQueue.Length(); + } + + // 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 MOZ_GUARDED_BY(sLock); + 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]{0}; +}; + +//----------------------------------------------------------------------------- +// 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=%zd, " + "original=%zd]", + 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), + 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), + mCompressorMutex("WebSocketChannel::mCompressorMutex"), + 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::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 (!mIOThread) { + // there has not been an asyncopen yet on the object and then we need + // no ping. + LOG(("WebSocket: early object, no ping needed")); + } else { + mIOThread->Dispatch( + NewRunnableMethod("net::WebSocketChannel::OnNetworkChanged", this, + &WebSocketChannel::OnNetworkChanged), + NS_DISPATCH_NORMAL); + } + } + } + + return NS_OK; +} + +nsresult WebSocketChannel::OnNetworkChanged() { + if (!mDataStarted) { + LOG(("WebSocket: data not started yet, no ping needed")); + return NS_OK; + } + + MOZ_ASSERT(mIOThread->IsOnCurrentThread(), "not on right 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(); } + +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(); + } +} + +// MainThread +void WebSocketChannel::BeginOpenInternal() { + LOG(("WebSocketChannel::BeginOpenInternal() %p\n", this)); + MOZ_ASSERT(NS_IsMainThread(), "not main thread"); + + 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 = localChannel->AsyncOpen(this); + + if (NS_FAILED(rv)) { + LOG(("WebSocketChannel::BeginOpenInternal: cannot async open\n")); + AbortSession(NS_ERROR_WEBSOCKET_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(mIOThread->IsOnCurrentThread(), "not on right 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 + MutexAutoLock lock(mCompressorMutex); + 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) { + if (mListenerMT) { + nsCString utf8Data; + { + MutexAutoLock lock(mCompressorMutex); + bool isDeflated = + mPMCECompressor && mPMCECompressor->IsMessageDeflated(); + LOG(("WebSocketChannel:: %stext frame received\n", + isDeflated ? "deflated " : "")); + + if (isDeflated) { + rv = mPMCECompressor->Inflate(payload, payloadLength, utf8Data); + if (NS_FAILED(rv)) { + return rv; + } + LOG( + ("WebSocketChannel:: message successfully inflated " + "[origLength=%d, newLength=%zd]\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()); + } + + if (nsCOMPtr target = GetTargetThread()) { + target->Dispatch(new CallOnMessageAvailable(this, utf8Data, -1), + NS_DISPATCH_NORMAL); + } else { + return NS_ERROR_UNEXPECTED; + } + 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) { + if (nsCOMPtr target = GetTargetThread()) { + target->Dispatch(new CallOnServerClose(this, mServerCloseCode, + mServerCloseReason), + NS_DISPATCH_NORMAL); + } else { + return NS_ERROR_UNEXPECTED; + } + } + + 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) { + if (mListenerMT) { + nsCString binaryData; + { + MutexAutoLock lock(mCompressorMutex); + bool isDeflated = + mPMCECompressor && mPMCECompressor->IsMessageDeflated(); + LOG(("WebSocketChannel:: %sbinary frame received\n", + isDeflated ? "deflated " : "")); + + if (isDeflated) { + rv = mPMCECompressor->Inflate(payload, payloadLength, binaryData); + if (NS_FAILED(rv)) { + return rv; + } + LOG( + ("WebSocketChannel:: message successfully inflated " + "[origLength=%d, newLength=%zd]\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()); + } + + if (nsCOMPtr target = GetTargetThread()) { + target->Dispatch( + new CallOnMessageAvailable(this, binaryData, binaryData.Length()), + NS_DISPATCH_NORMAL); + } else { + return NS_ERROR_UNEXPECTED; + } + // 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(mIOThread->IsOnCurrentThread(), "not on right thread"); + + LOG( + ("WebSocketChannel::EnqueueOutgoingMessage %p " + "queueing msg %p [type=%s len=%d]\n", + this, aMsg, msgNames[aMsg->GetMsgType()], aMsg->Length())); + + aQueue.Push(aMsg); + if (mSocketOut) { + OnOutputStreamReady(mSocketOut); + } else { + DoEnqueueOutgoingMessage(); + } +} + +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(mIOThread->IsOnCurrentThread(), "not on right 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)) { + MutexAutoLock lock(mMutex); + 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 + MutexAutoLock lock(mCompressorMutex); + 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 { + static_assert(4 == sizeof(mask), "Size of the mask should be equal to 4"); + nsresult rv = mRandomGenerator->GenerateRandomBytesInto(mask); + if (NS_FAILED(rv)) { + LOG( + ("WebSocketChannel::PrimeNewOutgoingMessage(): " + "GenerateRandomBytes failure %" PRIx32 "\n", + static_cast(rv))); + AbortSession(rv); + return; + } + } 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() { + // normally this should be called on socket thread, but it may be called + // on MainThread + + LOG(("WebSocketChannel::CleanupConnection() %p", this)); + // This needs to run on the IOThread so we don't need to lock a bunch of these + if (!mIOThread->IsOnCurrentThread()) { + mIOThread->Dispatch( + NewRunnableMethod("net::WebSocketChannel::CleanupConnection", this, + &WebSocketChannel::CleanupConnection), + NS_DISPATCH_NORMAL); + return; + } + + 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 (mConnection) { + mConnection->Close(); + mConnection = nullptr; + } + + if (mConnectionLogService && !mPrivateBrowsing) { + mConnectionLogService->RemoveHost(mHost, mSerial); + } + + // 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. + // If mDataStarted is false, this is called on MainThread for Close(). + // Otherwise it should be called on the IO thread + + MOZ_ASSERT(mStopped); + MOZ_ASSERT(mIOThread->IsOnCurrentThread() || 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; + } + + // mOpenTimer must be null if mDataStarted is true and we're not on MainThread + if (mOpenTimer) { + MOZ_ASSERT(NS_IsMainThread(), "not main thread"); + mOpenTimer->Cancel(); + mOpenTimer = nullptr; + } + + { + MutexAutoLock lock(mMutex); + if (mReconnectDelayTimer) { + mReconnectDelayTimer->Cancel(); + NS_ReleaseOnMainThread("WebSocketChannel::mMutex", + mReconnectDelayTimer.forget()); + } + } + + if (mPingTimer) { + mPingTimer->Cancel(); + mPingTimer = nullptr; + } + + if (!mTCPClosed && mDataStarted) { + if (mSocketIn) { + // 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); + } else if (mConnection) { + mConnection->DrainSocketData(); + } + } + + int32_t sessionCount = kLingeringCloseThreshold; + nsWSAdmissionManager::GetSessionCount(sessionCount); + + if (!mTCPClosed && (mTransport || mConnection) && + 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(); + } + + { + MutexAutoLock lock(mMutex); + if (mCancelable) { + mCancelable->Cancel(NS_ERROR_UNEXPECTED); + mCancelable = nullptr; + } + } + + { + MutexAutoLock lock(mCompressorMutex); + mPMCECompressor = nullptr; + } + if (!mCalledOnStop) { + mCalledOnStop = true; + + nsWSAdmissionManager::OnStopSession(this, reason); + + RefPtr runnable = new CallOnStop(this, reason); + if (nsCOMPtr target = GetTargetThread()) { + target->Dispatch(runnable, NS_DISPATCH_NORMAL); + } + } +} + +// Called from MainThread, and called from IOThread in +// PrimeNewOutgoingMessage +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 + MOZ_ASSERT(mIOThread->IsOnCurrentThread() || !mDataStarted); + + // 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 || mConnection) && reason != NS_BASE_STREAM_CLOSED && + !mRequestedClose && !mClientClosed && !mServerClosed && mDataStarted) { + mRequestedClose = true; + mStopOnClose = reason; + mIOThread->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(mIOThread->IsOnCurrentThread(), "not on right 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; + } + + MutexAutoLock lock(mCompressorMutex); + 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; + nsCString path; + rv = mURI->GetFilePath(path); + NS_ENSURE_SUCCESS(rv, rv); + mPath = path; + 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 = GetMainThreadSerialEventTarget(); + nsCOMPtr cancelable; + rv = dns->AsyncResolveNative(hostName, nsIDNSService::RESOLVE_TYPE_DEFAULT, + nsIDNSService::RESOLVE_DEFAULT_FLAGS, nullptr, + this, main, mLoadInfo->GetOriginAttributes(), + getter_AddRefs(cancelable)); + if (NS_FAILED(rv)) { + return rv; + } + + MutexAutoLock lock(mMutex); + MOZ_ASSERT(!mCancelable); + mCancelable = std::move(cancelable); + return rv; +} + +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(); + } + + nsresult rv; + nsCOMPtr cancelable; + rv = pps->AsyncResolve( + mHttpChannel, + nsIProtocolProxyService::RESOLVE_PREFER_SOCKS_PROXY | + nsIProtocolProxyService::RESOLVE_PREFER_HTTPS_PROXY | + nsIProtocolProxyService::RESOLVE_ALWAYS_TUNNEL, + this, nullptr, getter_AddRefs(cancelable)); + + MutexAutoLock lock(mMutex); + MOZ_ASSERT(!mCancelable); + mCancelable = std::move(cancelable); + 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; + } + + nsCOMPtr target = GetTargetThread(); + if (target && !target->IsOnCurrentThread()) { + return target->Dispatch( + NewRunnableMethod("net::WebSocketChannel::StartWebsocketData", this, + &WebSocketChannel::StartWebsocketData), + NS_DISPATCH_NORMAL); + } + + return StartWebsocketData(); +} + +nsresult WebSocketChannel::StartWebsocketData() { + { + 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; + } + } + + RefPtr self = this; + mIOThread->Dispatch(NS_NewRunnableFunction( + "WebSocketChannel::StartWebsocketData", [self{std::move(self)}] { + LOG(("WebSocketChannel::DoStartWebsocketData() %p", self.get())); + + NS_DispatchToMainThread( + NewRunnableMethod("net::WebSocketChannel::NotifyOnStart", self, + &WebSocketChannel::NotifyOnStart), + NS_DISPATCH_NORMAL); + + nsresult rv = self->mConnection ? self->mConnection->StartReading() + : self->mSocketIn->AsyncWait( + self, 0, 0, self->mIOThread); + if (NS_FAILED(rv)) { + self->AbortSession(rv); + } + + if (self->mPingInterval) { + rv = self->StartPinging(); + if (NS_FAILED(rv)) { + LOG(( + "WebSocketChannel::StartWebsocketData Could not start pinging, " + "rv=0x%08" PRIx32, + static_cast(rv))); + self->AbortSession(rv); + } + } + })); + + return NS_OK; +} + +void WebSocketChannel::NotifyOnStart() { + LOG(("WebSocketChannel::NotifyOnStart Notifying Listener %p", + mListenerMT ? mListenerMT->mListener.get() : nullptr)); + mDataStarted = true; + if (mListenerMT) { + nsresult rv = mListenerMT->mListener->OnStart(mListenerMT->mContext); + if (NS_FAILED(rv)) { + LOG( + ("WebSocketChannel::NotifyOnStart " + "mListenerMT->mListener->OnStart() failed with error 0x%08" PRIx32, + static_cast(rv))); + } + } +} + +nsresult WebSocketChannel::StartPinging() { + LOG(("WebSocketChannel::StartPinging() %p", this)); + MOZ_ASSERT(mIOThread->IsOnCurrentThread(), "not on right 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", + (uint32_t)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"); + + { + MutexAutoLock lock(mMutex); + mCancelable = nullptr; + } + + if (mStopped) { + LOG(("WebSocketChannel::OnLookupComplete: Request Already Stopped\n")); + return NS_OK; + } + + // 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) { + { + MutexAutoLock lock(mMutex); + MOZ_ASSERT(!mCancelable || (aRequest == mCancelable)); + mCancelable = nullptr; + } + + if (mStopped) { + LOG(("WebSocketChannel::OnProxyAvailable: [%p] Request Already Stopped\n", + this)); + return NS_OK; + } + + 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); + } + } + + // notify listener of OnProxyAvailable + LOG(("WebSocketChannel::OnProxyAvailable Notifying Listener %p", + mListenerMT ? mListenerMT->mListener.get() : nullptr)); + nsresult rv; + nsCOMPtr ppc( + do_QueryInterface(mListenerMT->mListener, &rv)); + if (NS_SUCCEEDED(rv)) { + rv = ppc->OnProxyAvailable(aRequest, aChannel, pi, status); + if (NS_FAILED(rv)) { + LOG( + ("WebSocketChannel::OnProxyAvailable notify" + " failed with error 0x%08" PRIx32, + static_cast(rv))); + } + } + + 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"); + + // allow insecure->secure redirects 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(mIOThread->IsOnCurrentThread(), "not on right 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_EXTERNAL); + } 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_EXTERNAL); + MOZ_PUSH_IGNORE_THREAD_SAFETY + // mReconnectDelayTimer is only modified on MainThread, we can read it + // without a lock, but ONLY if we're on MainThread! And if we're not + // on MainThread, it can't be mReconnectDelayTimer + } else if (NS_IsMainThread() && timer == mReconnectDelayTimer) { + MOZ_POP_THREAD_SAFETY + MOZ_ASSERT(mConnecting == CONNECTING_DELAYED, + "woke up from delay w/o being delayed?"); + + { + MutexAutoLock lock(mMutex); + mReconnectDelayTimer = nullptr; + } + LOG(("WebSocketChannel: connecting [this=%p] after reconnect delay", this)); + BeginOpen(false); + } else if (timer == mPingTimer) { + MOZ_ASSERT(mIOThread->IsOnCurrentThread(), "not on right 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_EXTERNAL); + } + } 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(nsITransportSecurityInfo** aSecurityInfo) { + LOG(("WebSocketChannel::GetSecurityInfo() %p\n", this)); + MOZ_ASSERT(NS_IsMainThread(), "not main thread"); + + *aSecurityInfo = nullptr; + + if (mConnection) { + nsresult rv = mConnection->GetSecurityInfo(aSecurityInfo); + if (NS_FAILED(rv)) { + return rv; + } + return NS_OK; + } + + if (mTransport) { + nsCOMPtr tlsSocketControl; + nsresult rv = + mTransport->GetTlsSocketControl(getter_AddRefs(tlsSocketControl)); + if (NS_FAILED(rv)) { + return rv; + } + nsCOMPtr securityInfo( + do_QueryInterface(tlsSocketControl)); + if (securityInfo) { + securityInfo.forget(aSecurityInfo); + } + } + return NS_OK; +} + +NS_IMETHODIMP +WebSocketChannel::AsyncOpen(nsIURI* aURI, const nsACString& aOrigin, + JS::Handle aOriginAttributes, + uint64_t aInnerWindowID, + nsIWebSocketListener* aListener, + nsISupports* aContext, JSContext* aCx) { + OriginAttributes attrs; + if (!aOriginAttributes.isObject() || !attrs.Init(aCx, aOriginAttributes)) { + return NS_ERROR_INVALID_ARG; + } + return AsyncOpenNative(aURI, aOrigin, attrs, aInnerWindowID, aListener, + aContext); +} + +NS_IMETHODIMP +WebSocketChannel::AsyncOpenNative(nsIURI* aURI, const nsACString& aOrigin, + const OriginAttributes& aOriginAttributes, + uint64_t aInnerWindowID, + nsIWebSocketListener* aListener, + nsISupports* aContext) { + LOG(("WebSocketChannel::AsyncOpen() %p\n", this)); + + aOriginAttributes.CreateSuffix(mOriginSuffix); + + 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 RetargetDeliveryTo isn't called + { + auto lock = mTargetThread.Lock(); + if (!lock.ref()) { + lock.ref() = GetMainThreadSerialEventTarget(); + } + } + + mIOThread = 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->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 mIOThread->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=%zu\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 mIOThread->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; + + return OnTransportAvailableInternal(); +} + +NS_IMETHODIMP +WebSocketChannel::OnWebSocketConnectionAvailable( + WebSocketConnectionBase* aConnection) { + if (!NS_IsMainThread()) { + RefPtr self = this; + RefPtr connection = aConnection; + return NS_DispatchToMainThread(NS_NewRunnableFunction( + "WebSocketChannel::OnWebSocketConnectionAvailable", + [self, connection]() { + self->OnWebSocketConnectionAvailable(connection); + })); + } + + LOG( + ("WebSocketChannel::OnWebSocketConnectionAvailable %p [%p] " + "rcvdonstart=%d\n", + this, aConnection, mGotUpgradeOK)); + + MOZ_ASSERT(NS_IsMainThread(), "not main thread"); + MOZ_ASSERT(!mRecvdHttpUpgradeTransport, + "OnWebSocketConnectionAvailable duplicated"); + MOZ_ASSERT(aConnection); + + if (mStopped) { + LOG(("WebSocketChannel::OnWebSocketConnectionAvailable: Already stopped")); + aConnection->Close(); + return NS_OK; + } + + nsresult rv = aConnection->Init(this); + if (NS_FAILED(rv)) { + return rv; + } + + mConnection = aConnection; + // Note: mIOThread will be IPDL background thread. + mConnection->GetIoTarget(getter_AddRefs(mIOThread)); + return OnTransportAvailableInternal(); +} + +nsresult WebSocketChannel::OnTransportAvailableInternal() { + MOZ_ASSERT(NS_IsMainThread(), "not main thread"); + MOZ_ASSERT(!mRecvdHttpUpgradeTransport, + "OnWebSocketConnectionAvailable duplicated"); + MOZ_ASSERT(mSocketIn || mConnection); + + 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; + + nsresult 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; + } + + MutexAutoLock lock(mCompressorMutex); + 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) { + // When socket process is enabled, this could be called on background 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_WEBSOCKET_CONNECTION_REFUSED); + return NS_ERROR_WEBSOCKET_CONNECTION_REFUSED; + } + + nsresult rv; + uint32_t status; + char *val, *token; + + rv = mHttpChannel->GetResponseStatus(&status); + if (NS_FAILED(rv)) { + nsresult httpStatus; + rv = NS_ERROR_WEBSOCKET_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_WEBSOCKET_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_WEBSOCKET_CONNECTION_REFUSED); + return NS_ERROR_WEBSOCKET_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 (nsCRT::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 (nsCRT::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 (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_DIAGNOSTIC_ASSERT(mIOThread->IsOnCurrentThread(), "not on right 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, sizeof(buffer), &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, mIOThread); + 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_DIAGNOSTIC_ASSERT(mIOThread->IsOnCurrentThread(), "not on right 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 [%p]: " + "Try to send %u of data\n", + this, toSend)); + } + } + + if (toSend == 0) { + amtSent = 0; + } else { + rv = mSocketOut->Write(sndBuf, toSend, &amtSent); + LOG(("WebSocketChannel::OnOutputStreamReady [%p]: write %u rv %" PRIx32 + "\n", + this, amtSent, static_cast(rv))); + + if (rv == NS_BASE_STREAM_WOULD_BLOCK) { + mSocketOut->AsyncWait(this, 0, 0, mIOThread); + 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, mIOThread); + } + } else { + if (amtSent == toSend) { + if (!mStopped) { + if (nsCOMPtr target = GetTargetThread()) { + target->Dispatch( + new CallAcknowledge(this, mCurrentOut->OrigLength()), + NS_DISPATCH_NORMAL); + } else { + return NS_ERROR_UNEXPECTED; + } + } + DeleteCurrentOutGoingMessage(); + PrimeNewOutgoingMessage(); + } else { + mCurrentOutSent += amtSent; + mSocketOut->AsyncWait(this, 0, 0, mIOThread); + } + } + } + + 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; +} + +void WebSocketChannel::DoEnqueueOutgoingMessage() { + LOG(("WebSocketChannel::DoEnqueueOutgoingMessage() %p\n", this)); + MOZ_ASSERT(mIOThread->IsOnCurrentThread(), "not on right thread"); + + if (!mCurrentOut) { + PrimeNewOutgoingMessage(); + } + + while (mCurrentOut && mConnection) { + nsresult rv = NS_OK; + if (mCurrentOut->Length() - mCurrentOutSent == 0) { + LOG( + ("WebSocketChannel::DoEnqueueOutgoingMessage: " + "Try to send %u of hdr/copybreak\n", + mHdrOutToSend)); + rv = mConnection->WriteOutputData(mOutHeader, mHdrOutToSend, nullptr, 0); + } else { + LOG( + ("WebSocketChannel::DoEnqueueOutgoingMessage: " + "Try to send %u of hdr and %u of data\n", + mHdrOutToSend, mCurrentOut->Length())); + rv = mConnection->WriteOutputData(mOutHeader, mHdrOutToSend, + (uint8_t*)mCurrentOut->BeginReading(), + mCurrentOut->Length()); + } + + LOG(("WebSocketChannel::DoEnqueueOutgoingMessage: rv %" PRIx32 "\n", + static_cast(rv))); + if (NS_FAILED(rv)) { + AbortSession(rv); + return; + } + + if (!mStopped) { + // TODO: Currently, we assume that data is completely written to the + // socket after sending it to socket process, but it's not true. The data + // could be queued in socket process and waiting for the socket to be able + // to write. We should implement flow control for this in bug 1726552. + if (nsCOMPtr target = GetTargetThread()) { + target->Dispatch(new CallAcknowledge(this, mCurrentOut->OrigLength()), + NS_DISPATCH_NORMAL); + } else { + AbortSession(NS_ERROR_UNEXPECTED); + return; + } + } + DeleteCurrentOutGoingMessage(); + PrimeNewOutgoingMessage(); + } + + if (mReleaseOnTransmit) { + ReleaseSession(); + } +} + +void WebSocketChannel::OnError(nsresult aStatus) { AbortSession(aStatus); } + +void WebSocketChannel::OnTCPClosed() { mTCPClosed = true; } + +nsresult WebSocketChannel::OnDataReceived(uint8_t* aData, uint32_t aCount) { + return ProcessInput(aData, aCount); +} + +} // namespace mozilla::net + +#undef CLOSE_GOING_AWAY diff --git a/netwerk/protocol/websocket/WebSocketChannel.h b/netwerk/protocol/websocket/WebSocketChannel.h new file mode 100644 index 0000000000..83b1fc6cd2 --- /dev/null +++ b/netwerk/protocol/websocket/WebSocketChannel.h @@ -0,0 +1,377 @@ +/* -*- 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 "mozilla/net/WebSocketConnectionListener.h" +#include "mozilla/Mutex.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; +class WebSocketConnectionBase; + +[[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, + public WebSocketConnectionListener { + 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, + JS::Handle aOriginAttributes, + uint64_t aWindowID, nsIWebSocketListener* aListener, + nsISupports* aContext, JSContext* aCx) override; + NS_IMETHOD AsyncOpenNative(nsIURI* aURI, const nsACString& aOrigin, + const OriginAttributes& aOriginAttributes, + 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(nsITransportSecurityInfo** aSecurityInfo) override; + + WebSocketChannel(); + static void Shutdown(); + + // Off main thread URI access. + void GetEffectiveURL(nsAString& aEffectiveURL) const override; + bool IsEncrypted() const override; + + nsresult OnTransportAvailableInternal(); + void OnError(nsresult aStatus) override; + void OnTCPClosed() override; + nsresult OnDataReceived(uint8_t* aData, uint32_t aCount) 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: + ~WebSocketChannel() override; + + 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 DoEnqueueOutgoingMessage(); + + 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); + } + } + } + + void NotifyOnStart(); + + nsCOMPtr mIOThread; + // Set in AsyncOpenNative and AsyncOnChannelRedirect, modified in + // DoStopSession on IO thread (.forget()). Probably ok... + nsCOMPtr mChannel; + nsCOMPtr mHttpChannel; + + nsCOMPtr mCancelable MOZ_GUARDED_BY(mMutex); + // Mainthread only + nsCOMPtr mRedirectCallback; + // Set on Mainthread during AsyncOpen, used on IO thread and Mainthread + nsCOMPtr mRandomGenerator; + + nsCString mHashedSecret; // MainThread only + + // 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) + // MainThread only + nsCString mAddress; + nsCString mPath; + int32_t mPort; // WS server port + // Secondary key for the connection queue. Used by nsWSAdmissionManager. + nsCString mOriginSuffix; // MainThread only + + // Used for off main thread access to the URI string. + // Set on MainThread in AsyncOpenNative, used on TargetThread and IO thread + nsCString mHost; + nsString mEffectiveURL; + + // Set on MainThread before multithread use, used on IO thread, cleared on + // IOThread + nsCOMPtr mTransport; + nsCOMPtr mSocketIn; + nsCOMPtr mSocketOut; + RefPtr mConnection; + + // Only used on IO Thread (accessed when known-to-be-null in DoStopSession + // on MainThread before mDataStarted) + nsCOMPtr mCloseTimer; + // set in AsyncOpenInternal on MainThread, used on IO thread. + // No multithread use before it's set, no changes after that. + uint32_t mCloseTimeout; /* milliseconds */ + + nsCOMPtr mOpenTimer; /* Mainthread only */ + uint32_t mOpenTimeout; /* milliseconds, MainThread only */ + wsConnectingState mConnecting; /* 0 if not connecting, MainThread only */ + // Set on MainThread, deleted on MainThread, used on MainThread or + // IO Thread (in DoStopSession). Mutex required to access off-main-thread. + nsCOMPtr mReconnectDelayTimer MOZ_GUARDED_BY(mMutex); + + // Only touched on IOThread (DoStopSession reads it on MainThread if + // we haven't connected yet (mDataStarted==false), and it's always null + // until mDataStarted=true) + nsCOMPtr mPingTimer; + + // Created in DoStopSession on IO thread (mDataStarted=true), accessed + // only from IO Thread + nsCOMPtr mLingeringCloseTimer; + const static int32_t kLingeringCloseTimeout = 1000; + const static int32_t kLingeringCloseThreshold = 50; + + RefPtr mService; // effectively const + + int32_t + mMaxConcurrentConnections; // only used in AsyncOpenNative on MainThread + + // Set on MainThread in AsyncOpenNative; then used on IO thread + uint64_t mInnerWindowID; + + // following members are accessed only on the main thread + uint32_t mGotUpgradeOK : 1; + uint32_t mRecvdHttpUpgradeTransport : 1; + uint32_t mAllowPMCE : 1; + uint32_t : 0; // ensure these aren't mixed with the next set + + // following members are accessed only on the IO thread + uint32_t mPingOutstanding : 1; + uint32_t mReleaseOnTransmit : 1; + uint32_t : 0; + + Atomic mDataStarted; + // All changes to mRequestedClose happen under mutex, but since it's atomic, + // it can be read anywhere without a lock + Atomic mRequestedClose; + // mServer/ClientClosed are only modified on IOThread + Atomic mClientClosed; + Atomic mServerClosed; + // All changes to mStopped happen under mutex, but since it's atomic, it + // can be read anywhere without a lock + Atomic mStopped; + Atomic mCalledOnStop; + Atomic mTCPClosed; + Atomic mOpenedHttpChannel; + Atomic mIncrementedSessionCount; + Atomic mDecrementedSessionCount; + + int32_t mMaxMessageSize; // set on MainThread in AsyncOpenNative, read on IO + // thread + // Set on IOThread, or on MainThread before mDataStarted. Used on IO Thread + // (after mDataStarted) + nsresult mStopOnClose; + uint16_t mServerCloseCode; // only used on IO thread + nsCString mServerCloseReason; // only used on IO thread + uint16_t mScriptCloseCode MOZ_GUARDED_BY(mMutex); + nsCString mScriptCloseReason MOZ_GUARDED_BY(mMutex); + + // 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; + + // Set at creation, used/modified only on IO thread + 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; + + // Only used on IO thread + OutboundMessage* mCurrentOut; + uint32_t mCurrentOutSent; + nsDeque mOutgoingMessages; + nsDeque mOutgoingPingMessages; + nsDeque mOutgoingPongMessages; + uint32_t mHdrOutToSend; + uint8_t* mHdrOut; + uint8_t mOutHeader[kCopyBreak + 16]{0}; + + // Set on MainThread in OnStartRequest (before mDataStarted), or in + // HandleExtensions() or OnTransportAvailableInternal(),used on IO Thread + // (after mDataStarted), cleared in DoStopSession on IOThread or on + // MainThread (if mDataStarted == false). + Mutex mCompressorMutex; + UniquePtr mPMCECompressor MOZ_GUARDED_BY(mCompressorMutex); + + // Used by EnsureHdrOut, which isn't called anywhere + uint32_t mDynamicOutputSize; + uint8_t* mDynamicOutput; + // Set on creation and AsyncOpen, read on both threads + Atomic mPrivateBrowsing; + + nsCOMPtr + mConnectionLogService; // effectively const + + 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..2931b47065 --- /dev/null +++ b/netwerk/protocol/websocket/WebSocketChannelChild.cpp @@ -0,0 +1,732 @@ +/* -*- 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" +#include "mozilla/dom/ContentChild.h" +#include "nsITransportProvider.h" + +using namespace mozilla::ipc; +using mozilla::dom::ContentChild; + +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)); + mEventQ->NotifyReleasingOwner(); +} + +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) + : mChild(aChild), + mWebSocketEvent(aWebSocketEvent), + mEventTarget(mChild->GetTargetThread()) {} + + void Run() override { + if (mEventTarget) { + mEventTarget->Dispatch( + new WrappedWebSocketEvent(mChild, std::move(mWebSocketEvent)), + NS_DISPATCH_NORMAL); + return; + } + } + + already_AddRefed GetEventTarget() override { + nsCOMPtr target = mEventTarget; + if (!target) { + target = GetMainThreadSerialEventTarget(); + } + 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 nsACString& aProtocol, const nsACString& aExtensions, + const nsAString& 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 nsACString& aProtocol, const nsACString& aExtensions, + const nsAString& aEffectiveURL, const bool& aEncrypted, + const uint64_t& aHttpChannelId) { + mEventQ->RunOrEnqueue(new EventTargetDispatcher( + this, new StartEvent(aProtocol, aExtensions, aEffectiveURL, aEncrypted, + aHttpChannelId))); + + return IPC_OK(); +} + +void WebSocketChannelChild::OnStart(const nsACString& aProtocol, + const nsACString& aExtensions, + const nsAString& 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))); + + 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 nsACString& 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 nsACString& 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))); + 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 nsACString& aMsg, const bool& aMoreData) { + if (!RecvOnMessageAvailableInternal(aMsg, aMoreData, false)) { + LOG(("WebSocketChannelChild %p append message failed", this)); + mEventQ->RunOrEnqueue(new EventTargetDispatcher(this, new OnErrorEvent())); + } + return IPC_OK(); +} + +void WebSocketChannelChild::OnMessageAvailable(const nsACString& 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 nsACString& aMsg, const bool& aMoreData) { + if (!RecvOnMessageAvailableInternal(aMsg, aMoreData, true)) { + LOG(("WebSocketChannelChild %p append message failed", this)); + mEventQ->RunOrEnqueue(new EventTargetDispatcher(this, new OnErrorEvent())); + } + return IPC_OK(); +} + +void WebSocketChannelChild::OnBinaryMessageAvailable(const nsACString& 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))); + + 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 nsACString& 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 nsACString& aReason) { + mEventQ->RunOrEnqueue( + new EventTargetDispatcher(this, new ServerCloseEvent(aCode, aReason))); + + return IPC_OK(); +} + +void WebSocketChannelChild::OnServerClose(const uint16_t& aCode, + const nsACString& 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 = GetMainThreadSerialEventTarget(); +} + +NS_IMETHODIMP +WebSocketChannelChild::AsyncOpen(nsIURI* aURI, const nsACString& aOrigin, + JS::Handle aOriginAttributes, + uint64_t aInnerWindowID, + nsIWebSocketListener* aListener, + nsISupports* aContext, JSContext* aCx) { + OriginAttributes attrs; + if (!aOriginAttributes.isObject() || !attrs.Init(aCx, aOriginAttributes)) { + return NS_ERROR_INVALID_ARG; + } + return AsyncOpenNative(aURI, aOrigin, attrs, aInnerWindowID, aListener, + aContext); +} + +NS_IMETHODIMP +WebSocketChannelChild::AsyncOpenNative( + nsIURI* aURI, const nsACString& aOrigin, + const OriginAttributes& aOriginAttributes, 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; + LoadInfoArgs 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(WrapNotNull(ipcChild)); + } + + // This must be called before sending constructor message. + SetupNeckoTarget(); + + if (!gNeckoChild->SendPWebSocketConstructor( + this, browserChild, IPC::SerializedLoadContext(this), mSerial)) { + return NS_ERROR_UNEXPECTED; + } + if (!SendAsyncOpen(uri, aOrigin, aOriginAttributes, 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(IsOnTargetThread()); + 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, 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(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(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(IsOnTargetThread()); + nsCOMPtr target = GetNeckoTarget(); + return target->Dispatch(new BinaryStreamEvent(this, aStream, aLength), + NS_DISPATCH_NORMAL); + } + + LOG(("WebSocketChannelChild::SendBinaryStream() %p\n", this)); + + IPCStream ipcStream; + if (NS_WARN_IF(!mozilla::ipc::SerializeIPCStream(do_AddRef(aStream), + ipcStream, + /* aAllowLazy */ false))) { + return NS_ERROR_UNEXPECTED; + } + + { + MutexAutoLock lock(mMutex); + if (mIPCState != Opened) { + return NS_ERROR_UNEXPECTED; + } + } + + if (!SendSendBinaryStream(ipcStream, aLength)) { + return NS_ERROR_UNEXPECTED; + } + + return NS_OK; +} + +NS_IMETHODIMP +WebSocketChannelChild::GetSecurityInfo( + nsITransportSecurityInfo** aSecurityInfo) { + LOG(("WebSocketChannelChild::GetSecurityInfo() %p\n", this)); + return NS_ERROR_NOT_AVAILABLE; +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/websocket/WebSocketChannelChild.h b/netwerk/protocol/websocket/WebSocketChannelChild.h new file mode 100644 index 0000000000..0221b9fdde --- /dev/null +++ b/netwerk/protocol/websocket/WebSocketChannelChild.h @@ -0,0 +1,116 @@ +/* -*- 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 MessageEvent; + +class WebSocketChannelChild final : public BaseWebSocketChannel, + public PWebSocketChild, + public NeckoTargetHolder { + friend class PWebSocketChild; + + public: + explicit WebSocketChannelChild(bool aEncrypted); + + NS_DECL_THREADSAFE_ISUPPORTS + + // nsIWebSocketChannel methods BaseWebSocketChannel didn't implement for us + // + NS_IMETHOD AsyncOpen(nsIURI* aURI, const nsACString& aOrigin, + JS::Handle aOriginAttributes, + uint64_t aInnerWindowID, nsIWebSocketListener* aListener, + nsISupports* aContext, JSContext* aCx) override; + NS_IMETHOD AsyncOpenNative(nsIURI* aURI, const nsACString& aOrigin, + const OriginAttributes& aOriginAttributes, + 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(nsITransportSecurityInfo** 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 nsACString& aProtocol, + const nsACString& aExtensions, + const nsAString& aEffectiveURL, + const bool& aEncrypted, + const uint64_t& aHttpChannelId); + mozilla::ipc::IPCResult RecvOnStop(const nsresult& aStatusCode); + mozilla::ipc::IPCResult RecvOnMessageAvailable(const nsACString& aMsg, + const bool& aMoreData); + mozilla::ipc::IPCResult RecvOnBinaryMessageAvailable(const nsACString& aMsg, + const bool& aMoreData); + mozilla::ipc::IPCResult RecvOnAcknowledge(const uint32_t& aSize); + mozilla::ipc::IPCResult RecvOnServerClose(const uint16_t& aCode, + const nsACString& aReason); + + void OnStart(const nsACString& aProtocol, const nsACString& aExtensions, + const nsAString& aEffectiveURL, const bool& aEncrypted, + const uint64_t& aHttpChannelId); + void OnStop(const nsresult& aStatusCode); + void OnMessageAvailable(const nsACString& aMsg); + void OnBinaryMessageAvailable(const nsACString& aMsg); + void OnAcknowledge(const uint32_t& aSize); + void OnServerClose(const uint16_t& aCode, const nsACString& aReason); + void AsyncOpenFailed(); + + void MaybeReleaseIPCObject(); + + // This function tries to get a labeled event target for |mNeckoTarget|. + void SetupNeckoTarget(); + + bool RecvOnMessageAvailableInternal(const nsACString& 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 MOZ_UNANNOTATED; + + 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..b690f3ac27 --- /dev/null +++ b/netwerk/protocol/websocket/WebSocketChannelParent.cpp @@ -0,0 +1,360 @@ +/* -*- 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/dom/ContentParent.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" +#include "nsComponentManagerUtils.h" +#include "IPCTransportProvider.h" +#include "mozilla/net/ChannelEventQueue.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 OriginAttributes& aOriginAttributes, 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 LoadInfoArgs& aLoadInfoArgs, + const Maybe& aTransportProvider, + const nsCString& aNegotiatedExtensions) { + LOG(("WebSocketChannelParent::RecvAsyncOpen() %p\n", this)); + + nsresult rv; + nsCOMPtr loadInfo; + nsCOMPtr uri; + + rv = LoadInfoArgsToLoadInfo( + aLoadInfoArgs, + mozilla::dom::ContentParent::Cast(Manager()->Manager())->GetRemoteType(), + 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->AsyncOpenNative(uri, aOrigin, aOriginAttributes, + 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, 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..534a737dcb --- /dev/null +++ b/netwerk/protocol/websocket/WebSocketChannelParent.h @@ -0,0 +1,70 @@ +/* -*- 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 OriginAttributes& aOriginAttributes, 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 LoadInfoArgs& 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/WebSocketConnection.cpp b/netwerk/protocol/websocket/WebSocketConnection.cpp new file mode 100644 index 0000000000..b57f4fc115 --- /dev/null +++ b/netwerk/protocol/websocket/WebSocketConnection.cpp @@ -0,0 +1,261 @@ +/* -*- 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 "nsIInterfaceRequestor.h" +#include "WebSocketConnection.h" + +#include "WebSocketLog.h" +#include "mozilla/net/WebSocketConnectionListener.h" +#include "nsIOService.h" +#include "nsITLSSocketControl.h" +#include "nsISocketTransport.h" +#include "nsITransportSecurityInfo.h" +#include "nsSocketTransportService2.h" + +namespace mozilla::net { + +NS_IMPL_ISUPPORTS(WebSocketConnection, nsIInputStreamCallback, + nsIOutputStreamCallback) + +WebSocketConnection::WebSocketConnection(nsISocketTransport* aTransport, + nsIAsyncInputStream* aInputStream, + nsIAsyncOutputStream* aOutputStream) + : mTransport(aTransport), + mSocketIn(aInputStream), + mSocketOut(aOutputStream) { + LOG(("WebSocketConnection ctor %p\n", this)); +} + +WebSocketConnection::~WebSocketConnection() { + LOG(("WebSocketConnection dtor %p\n", this)); +} + +nsresult WebSocketConnection::Init(WebSocketConnectionListener* aListener) { + NS_ENSURE_ARG_POINTER(aListener); + + mListener = aListener; + nsresult rv; + mSocketThread = do_GetService(NS_SOCKETTRANSPORTSERVICE_CONTRACTID, &rv); + if (NS_FAILED(rv)) { + return rv; + } + + if (!mTransport) { + return NS_ERROR_FAILURE; + } + + if (XRE_IsParentProcess()) { + nsCOMPtr callbacks = do_QueryInterface(mListener); + mTransport->SetSecurityCallbacks(callbacks); + } else { + // NOTE: we don't use security callbacks in socket process. + mTransport->SetSecurityCallbacks(nullptr); + } + return mTransport->SetEventSink(nullptr, nullptr); +} + +void WebSocketConnection::GetIoTarget(nsIEventTarget** aTarget) { + nsCOMPtr target = mSocketThread; + return target.forget(aTarget); +} + +void WebSocketConnection::Close() { + LOG(("WebSocketConnection::Close %p\n", this)); + MOZ_ASSERT(OnSocketThread()); + + if (mTransport) { + mTransport->SetSecurityCallbacks(nullptr); + mTransport->SetEventSink(nullptr, nullptr); + mTransport->Close(NS_BASE_STREAM_CLOSED); + mTransport = nullptr; + } + + if (mSocketIn) { + if (mStartReadingCalled) { + mSocketIn->AsyncWait(nullptr, 0, 0, nullptr); + } + mSocketIn = nullptr; + } + + if (mSocketOut) { + mSocketOut->AsyncWait(nullptr, 0, 0, nullptr); + mSocketOut = nullptr; + } +} + +nsresult WebSocketConnection::WriteOutputData(nsTArray&& aData) { + MOZ_ASSERT(OnSocketThread()); + + if (!mSocketOut) { + return NS_ERROR_NOT_AVAILABLE; + } + + mOutputQueue.emplace_back(std::move(aData)); + return OnOutputStreamReady(mSocketOut); +} + +nsresult WebSocketConnection::WriteOutputData(const uint8_t* aHdrBuf, + uint32_t aHdrBufLength, + const uint8_t* aPayloadBuf, + uint32_t aPayloadBufLength) { + MOZ_ASSERT_UNREACHABLE("Should not be called"); + return NS_ERROR_NOT_IMPLEMENTED; +} + +nsresult WebSocketConnection::StartReading() { + MOZ_ASSERT(OnSocketThread()); + + if (!mSocketIn) { + return NS_ERROR_NOT_AVAILABLE; + } + + MOZ_ASSERT(!mStartReadingCalled, "StartReading twice"); + mStartReadingCalled = true; + return mSocketIn->AsyncWait(this, 0, 0, mSocketThread); +} + +void WebSocketConnection::DrainSocketData() { + MOZ_ASSERT(OnSocketThread()); + + if (!mSocketIn || !mListener) { + return; + } + + // 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)) { + mListener->OnTCPClosed(); + } + } while (NS_SUCCEEDED(rv) && count > 0 && total < 32000); +} + +nsresult WebSocketConnection::GetSecurityInfo( + nsITransportSecurityInfo** aSecurityInfo) { + LOG(("WebSocketConnection::GetSecurityInfo() %p\n", this)); + MOZ_ASSERT(OnSocketThread()); + *aSecurityInfo = nullptr; + + if (mTransport) { + nsCOMPtr tlsSocketControl; + nsresult rv = + mTransport->GetTlsSocketControl(getter_AddRefs(tlsSocketControl)); + if (NS_FAILED(rv)) { + return rv; + } + nsCOMPtr securityInfo( + do_QueryInterface(tlsSocketControl)); + if (securityInfo) { + securityInfo.forget(aSecurityInfo); + } + } + return NS_OK; +} + +NS_IMETHODIMP +WebSocketConnection::OnInputStreamReady(nsIAsyncInputStream* aStream) { + LOG(("WebSocketConnection::OnInputStreamReady() %p\n", this)); + MOZ_ASSERT(OnSocketThread()); + MOZ_ASSERT(mListener); + + // did we we clean up the socket after scheduling InputReady? + if (!mSocketIn) { + return NS_OK; + } + + // this is after the http upgrade - so we are speaking websockets + uint8_t buffer[2048]; + uint32_t count; + nsresult rv; + + do { + rv = mSocketIn->Read((char*)buffer, 2048, &count); + LOG(("WebSocketConnection::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)) { + mListener->OnError(rv); + return rv; + } + + if (count == 0) { + mListener->OnError(NS_BASE_STREAM_CLOSED); + return NS_OK; + } + + rv = mListener->OnDataReceived(buffer, count); + if (NS_FAILED(rv)) { + mListener->OnError(rv); + return rv; + } + } while (NS_SUCCEEDED(rv) && mSocketIn && mListener); + + return NS_OK; +} + +NS_IMETHODIMP +WebSocketConnection::OnOutputStreamReady(nsIAsyncOutputStream* aStream) { + LOG(("WebSocketConnection::OnOutputStreamReady() %p\n", this)); + MOZ_ASSERT(OnSocketThread()); + MOZ_ASSERT(mListener); + + if (!mSocketOut) { + return NS_OK; + } + + while (!mOutputQueue.empty()) { + const OutputData& data = mOutputQueue.front(); + + char* buffer = reinterpret_cast( + const_cast(data.GetData().Elements())) + + mWriteOffset; + uint32_t toWrite = data.GetData().Length() - mWriteOffset; + + uint32_t wrote = 0; + nsresult rv = mSocketOut->Write(buffer, toWrite, &wrote); + LOG(("WebSocketConnection::OnOutputStreamReady: write %u rv %" PRIx32, + wrote, static_cast(rv))); + + if (rv == NS_BASE_STREAM_WOULD_BLOCK) { + mSocketOut->AsyncWait(this, 0, 0, mSocketThread); + return rv; + } + + if (NS_FAILED(rv)) { + LOG(("WebSocketConnection::OnOutputStreamReady %p failed %u\n", this, + static_cast(rv))); + mListener->OnError(rv); + return NS_OK; + } + + mWriteOffset += wrote; + + if (toWrite == wrote) { + mWriteOffset = 0; + mOutputQueue.pop_front(); + } else { + mSocketOut->AsyncWait(this, 0, 0, mSocketThread); + } + } + + return NS_OK; +} + +} // namespace mozilla::net diff --git a/netwerk/protocol/websocket/WebSocketConnection.h b/netwerk/protocol/websocket/WebSocketConnection.h new file mode 100644 index 0000000000..4e6a53b013 --- /dev/null +++ b/netwerk/protocol/websocket/WebSocketConnection.h @@ -0,0 +1,79 @@ +/* -*- 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_WebSocketConnection_h +#define mozilla_net_WebSocketConnection_h + +#include + +#include "nsIStreamListener.h" +#include "nsIAsyncInputStream.h" +#include "nsIAsyncOutputStream.h" +#include "mozilla/net/WebSocketConnectionBase.h" +#include "nsTArray.h" +#include "nsISocketTransport.h" + +class nsISocketTransport; + +namespace mozilla { +namespace net { + +class WebSocketConnectionListener; + +class WebSocketConnection : public nsIInputStreamCallback, + public nsIOutputStreamCallback, + public WebSocketConnectionBase { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIINPUTSTREAMCALLBACK + NS_DECL_NSIOUTPUTSTREAMCALLBACK + + explicit WebSocketConnection(nsISocketTransport* aTransport, + nsIAsyncInputStream* aInputStream, + nsIAsyncOutputStream* aOutputStream); + + nsresult Init(WebSocketConnectionListener* aListener) override; + void GetIoTarget(nsIEventTarget** aTarget) override; + void Close() override; + nsresult WriteOutputData(const uint8_t* aHdrBuf, uint32_t aHdrBufLength, + const uint8_t* aPayloadBuf, + uint32_t aPayloadBufLength) override; + nsresult WriteOutputData(nsTArray&& aData); + nsresult StartReading() override; + void DrainSocketData() override; + nsresult GetSecurityInfo(nsITransportSecurityInfo** aSecurityInfo) override; + + private: + virtual ~WebSocketConnection(); + + class OutputData { + public: + explicit OutputData(nsTArray&& aData) : mData(std::move(aData)) { + MOZ_COUNT_CTOR(OutputData); + } + + ~OutputData() { MOZ_COUNT_DTOR(OutputData); } + + const nsTArray& GetData() const { return mData; } + + private: + nsTArray mData; + }; + + RefPtr mListener; + nsCOMPtr mTransport; + nsCOMPtr mSocketIn; + nsCOMPtr mSocketOut; + nsCOMPtr mSocketThread; + size_t mWriteOffset{0}; + std::list mOutputQueue; + bool mStartReadingCalled{false}; +}; + +} // namespace net +} // namespace mozilla + +#endif // mozilla_net_WebSocketConnection_h diff --git a/netwerk/protocol/websocket/WebSocketConnectionBase.h b/netwerk/protocol/websocket/WebSocketConnectionBase.h new file mode 100644 index 0000000000..a462b8a346 --- /dev/null +++ b/netwerk/protocol/websocket/WebSocketConnectionBase.h @@ -0,0 +1,86 @@ +/* -*- 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_WebSocketConnectionBase_h +#define mozilla_net_WebSocketConnectionBase_h + +// Architecture view: +// Parent Process Socket Process +// ┌─────────────────┐ IPC ┌────────────────────────┐ +// │ WebSocketChannel│ ┌─────►WebSocketConnectionChild│ +// └─────────┬───────┘ │ └─────────┬──────────────┘ +// │ │ │ +// ┌──────────▼───────────────┐ │ ┌─────────▼─────────┐ +// │ WebSocketConnectionParent├──┘ │WebSocketConnection│ +// └──────────────────────────┘ └─────────┬─────────┘ +// │ +// ┌─────────▼─────────┐ +// │ nsSocketTransport │ +// └───────────────────┘ +// The main reason that we need WebSocketConnectionBase is that we need to +// encapsulate nsSockTransport, nsSocketOutoutStream, and nsSocketInputStream. +// These three objects only live in socket process, so we provide the necessary +// interfaces in WebSocketConnectionBase and WebSocketConnectionListener for +// reading/writing the real socket. +// +// The output path when WebSocketChannel wants to write data to socket: +// - WebSocketConnectionParent::WriteOutputData +// - WebSocketConnectionParent::SendWriteOutputData +// - WebSocketConnectionChild::RecvWriteOutputData +// - WebSocketConnection::WriteOutputData +// - WebSocketConnection::OnOutputStreamReady (writes data to the real socket) +// +// The input path when data is able to read from the socket +// - WebSocketConnection::OnInputStreamReady +// - WebSocketConnectionChild::OnDataReceived +// - WebSocketConnectionChild::SendOnDataReceived +// - WebSocketConnectionParent::RecvOnDataReceived +// - WebSocketChannel::OnDataReceived +// +// The path that WebSocketConnection is constructed. +// - nsHttpChannel::OnStopRequest +// - HttpConnectionMgrShell::CompleteUpgrade +// - HttpConnectionMgrParent::CompleteUpgrade (we store the +// nsIHttpUpgradeListener in a table and send an id to socket process) +// - HttpConnectionMgrParent::SendStartWebSocketConnection +// - HttpConnectionMgrChild::RecvStartWebSocketConnection (the listener id is +// saved in WebSocketConnectionChild and will be used when the socket +// transport is available) +// - WebSocketConnectionChild::Init (an IPC channel between socket thread in +// socket process and background thread in parent process is created) +// - nsHttpConnectionMgr::CompleteUpgrade +// - WebSocketConnectionChild::OnTransportAvailable (WebSocketConnection is +// created) +// - WebSocketConnectionChild::SendOnTransportAvailable +// - WebSocketConnectionParent::RecvOnTransportAvailable +// - WebSocketChannel::OnWebSocketConnectionAvailable + +class nsITransportSecurityInfo; + +namespace mozilla { +namespace net { + +class WebSocketConnectionListener; + +class WebSocketConnectionBase : public nsISupports { + public: + virtual nsresult Init(WebSocketConnectionListener* aListener) = 0; + virtual void GetIoTarget(nsIEventTarget** aTarget) = 0; + virtual void Close() = 0; + virtual nsresult WriteOutputData(const uint8_t* aHdrBuf, + uint32_t aHdrBufLength, + const uint8_t* aPayloadBuf, + uint32_t aPayloadBufLength) = 0; + virtual nsresult StartReading() = 0; + virtual void DrainSocketData() = 0; + virtual nsresult GetSecurityInfo( + nsITransportSecurityInfo** aSecurityInfo) = 0; +}; + +} // namespace net +} // namespace mozilla + +#endif // mozilla_net_WebSocketConnectionBase_h diff --git a/netwerk/protocol/websocket/WebSocketConnectionChild.cpp b/netwerk/protocol/websocket/WebSocketConnectionChild.cpp new file mode 100644 index 0000000000..045c3763b7 --- /dev/null +++ b/netwerk/protocol/websocket/WebSocketConnectionChild.cpp @@ -0,0 +1,221 @@ +/* -*- 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 "WebSocketLog.h" +#include "WebSocketConnectionChild.h" + +#include "WebSocketConnection.h" +#include "mozilla/ipc/Endpoint.h" +#include "mozilla/net/SocketProcessBackgroundChild.h" +#include "nsISerializable.h" +#include "nsITLSSocketControl.h" +#include "nsITransportSecurityInfo.h" +#include "nsNetCID.h" +#include "nsSerializationHelper.h" +#include "nsSocketTransportService2.h" +#include "nsThreadUtils.h" + +namespace mozilla { +namespace net { + +NS_IMPL_ISUPPORTS(WebSocketConnectionChild, nsIHttpUpgradeListener) + +WebSocketConnectionChild::WebSocketConnectionChild() { + LOG(("WebSocketConnectionChild ctor %p\n", this)); +} + +WebSocketConnectionChild::~WebSocketConnectionChild() { + LOG(("WebSocketConnectionChild dtor %p\n", this)); +} + +void WebSocketConnectionChild::Init(uint32_t aListenerId) { + MOZ_ASSERT(NS_IsMainThread()); + + nsresult rv; + mSocketThread = do_GetService(NS_SOCKETTRANSPORTSERVICE_CONTRACTID, &rv); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + if (!mSocketThread) { + return; + } + + ipc::Endpoint parentEndpoint; + ipc::Endpoint childEndpoint; + PWebSocketConnection::CreateEndpoints(&parentEndpoint, &childEndpoint); + + if (NS_FAILED(SocketProcessBackgroundChild::WithActor( + "SendInitWebSocketConnection", + [aListenerId, endpoint = std::move(parentEndpoint)]( + SocketProcessBackgroundChild* aActor) mutable { + Unused << aActor->SendInitWebSocketConnection(std::move(endpoint), + aListenerId); + }))) { + return; + } + + mSocketThread->Dispatch(NS_NewRunnableFunction( + "BindWebSocketConnectionChild", + [self = RefPtr{this}, endpoint = std::move(childEndpoint)]() mutable { + endpoint.Bind(self); + })); +} + +// nsIHttpUpgradeListener +NS_IMETHODIMP +WebSocketConnectionChild::OnTransportAvailable( + nsISocketTransport* aTransport, nsIAsyncInputStream* aSocketIn, + nsIAsyncOutputStream* aSocketOut) { + LOG(("WebSocketConnectionChild::OnTransportAvailable %p\n", this)); + if (!OnSocketThread()) { + nsCOMPtr transport = aTransport; + nsCOMPtr inputStream = aSocketIn; + nsCOMPtr outputStream = aSocketOut; + RefPtr self = this; + return mSocketThread->Dispatch( + NS_NewRunnableFunction("WebSocketConnectionChild::OnTransportAvailable", + [self, transport, inputStream, outputStream]() { + Unused << self->OnTransportAvailable( + transport, inputStream, outputStream); + }), + NS_DISPATCH_NORMAL); + } + + LOG(("WebSocketConnectionChild::OnTransportAvailable %p\n", this)); + MOZ_ASSERT(OnSocketThread()); + MOZ_ASSERT(!mConnection, "already called"); + MOZ_ASSERT(aTransport); + + if (!CanSend()) { + return NS_ERROR_NOT_AVAILABLE; + } + + nsCOMPtr tlsSocketControl; + aTransport->GetTlsSocketControl(getter_AddRefs(tlsSocketControl)); + nsCOMPtr securityInfo( + do_QueryInterface(tlsSocketControl)); + + RefPtr connection = + new WebSocketConnection(aTransport, aSocketIn, aSocketOut); + nsresult rv = connection->Init(this); + if (NS_FAILED(rv)) { + Unused << OnUpgradeFailed(rv); + return NS_OK; + } + + mConnection = std::move(connection); + + Unused << SendOnTransportAvailable(securityInfo); + return NS_OK; +} + +NS_IMETHODIMP +WebSocketConnectionChild::OnUpgradeFailed(nsresult aReason) { + if (!OnSocketThread()) { + return mSocketThread->Dispatch(NewRunnableMethod( + "WebSocketConnectionChild::OnUpgradeFailed", this, + &WebSocketConnectionChild::OnUpgradeFailed, aReason)); + } + + if (CanSend()) { + Unused << SendOnUpgradeFailed(aReason); + } + return NS_OK; +} + +NS_IMETHODIMP +WebSocketConnectionChild::OnWebSocketConnectionAvailable( + WebSocketConnectionBase* aConnection) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +mozilla::ipc::IPCResult WebSocketConnectionChild::RecvWriteOutputData( + nsTArray&& aData) { + LOG(("WebSocketConnectionChild::RecvWriteOutputData %p\n", this)); + + if (!mConnection) { + OnError(NS_ERROR_NOT_AVAILABLE); + return IPC_OK(); + } + + mConnection->WriteOutputData(std::move(aData)); + return IPC_OK(); +} + +mozilla::ipc::IPCResult WebSocketConnectionChild::RecvStartReading() { + LOG(("WebSocketConnectionChild::RecvStartReading %p\n", this)); + + if (!mConnection) { + OnError(NS_ERROR_NOT_AVAILABLE); + return IPC_OK(); + } + + mConnection->StartReading(); + return IPC_OK(); +} + +mozilla::ipc::IPCResult WebSocketConnectionChild::RecvDrainSocketData() { + LOG(("WebSocketConnectionChild::RecvDrainSocketData %p\n", this)); + + if (!mConnection) { + OnError(NS_ERROR_NOT_AVAILABLE); + return IPC_OK(); + } + + mConnection->DrainSocketData(); + return IPC_OK(); +} + +mozilla::ipc::IPCResult WebSocketConnectionChild::Recv__delete__() { + LOG(("WebSocketConnectionChild::Recv__delete__ %p\n", this)); + + if (!mConnection) { + OnError(NS_ERROR_NOT_AVAILABLE); + return IPC_OK(); + } + + mConnection->Close(); + mConnection = nullptr; + return IPC_OK(); +} + +void WebSocketConnectionChild::OnError(nsresult aStatus) { + LOG(("WebSocketConnectionChild::OnError %p\n", this)); + + if (CanSend()) { + Unused << SendOnError(aStatus); + } +} + +void WebSocketConnectionChild::OnTCPClosed() { + LOG(("WebSocketConnectionChild::OnTCPClosed %p\n", this)); + + if (CanSend()) { + Unused << SendOnTCPClosed(); + } +} + +nsresult WebSocketConnectionChild::OnDataReceived(uint8_t* aData, + uint32_t aCount) { + LOG(("WebSocketConnectionChild::OnDataReceived %p\n", this)); + + if (CanSend()) { + nsTArray data; + data.AppendElements(aData, aCount); + Unused << SendOnDataReceived(data); + } + + return NS_OK; +} + +void WebSocketConnectionChild::ActorDestroy(ActorDestroyReason aWhy) { + LOG(("WebSocketConnectionChild::ActorDestroy %p\n", this)); + if (mConnection) { + mConnection->Close(); + mConnection = nullptr; + } +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/websocket/WebSocketConnectionChild.h b/netwerk/protocol/websocket/WebSocketConnectionChild.h new file mode 100644 index 0000000000..a0f16b7fa8 --- /dev/null +++ b/netwerk/protocol/websocket/WebSocketConnectionChild.h @@ -0,0 +1,54 @@ +/* -*- 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_WebSocketConnectionChild_h +#define mozilla_net_WebSocketConnectionChild_h + +#include "mozilla/net/PWebSocketConnectionChild.h" +#include "mozilla/net/WebSocketConnectionListener.h" +#include "nsIHttpChannelInternal.h" + +namespace mozilla { +namespace net { + +class WebSocketConnection; + +// WebSocketConnectionChild only lives in socket process and uses +// WebSocketConnection to send/read data from socket. Only IPDL holds a strong +// reference to WebSocketConnectionChild, so the life time of +// WebSocketConnectionChild is bound to the IPC actor. + +class WebSocketConnectionChild final : public PWebSocketConnectionChild, + public nsIHttpUpgradeListener, + public WebSocketConnectionListener { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIHTTPUPGRADELISTENER + + WebSocketConnectionChild(); + void Init(uint32_t aListenerId); + mozilla::ipc::IPCResult RecvWriteOutputData(nsTArray&& aData); + mozilla::ipc::IPCResult RecvStartReading(); + mozilla::ipc::IPCResult RecvDrainSocketData(); + mozilla::ipc::IPCResult Recv__delete__() override; + + void ActorDestroy(ActorDestroyReason aWhy) override; + + void OnError(nsresult aStatus) override; + void OnTCPClosed() override; + nsresult OnDataReceived(uint8_t* aData, uint32_t aCount) override; + + private: + virtual ~WebSocketConnectionChild(); + + RefPtr mConnection; + nsCOMPtr mSocketThread; +}; + +} // namespace net +} // namespace mozilla + +#endif // mozilla_net_WebSocketConnectionChild_h diff --git a/netwerk/protocol/websocket/WebSocketConnectionListener.h b/netwerk/protocol/websocket/WebSocketConnectionListener.h new file mode 100644 index 0000000000..31663f17f6 --- /dev/null +++ b/netwerk/protocol/websocket/WebSocketConnectionListener.h @@ -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 : */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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_WebSocketConnectionListener_h +#define mozilla_net_WebSocketConnectionListener_h + +namespace mozilla { +namespace net { + +class WebSocketConnectionListener : public nsISupports { + public: + virtual void OnError(nsresult aStatus) = 0; + virtual void OnTCPClosed() = 0; + virtual nsresult OnDataReceived(uint8_t* aData, uint32_t aCount) = 0; +}; + +} // namespace net +} // namespace mozilla + +#endif // mozilla_net_WebSocketConnectionListener_h diff --git a/netwerk/protocol/websocket/WebSocketConnectionParent.cpp b/netwerk/protocol/websocket/WebSocketConnectionParent.cpp new file mode 100644 index 0000000000..3fdf85337e --- /dev/null +++ b/netwerk/protocol/websocket/WebSocketConnectionParent.cpp @@ -0,0 +1,210 @@ +/* -*- 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 "WebSocketLog.h" +#include "WebSocketConnectionParent.h" + +#include "nsIHttpChannelInternal.h" +#include "nsITransportSecurityInfo.h" +#include "nsSerializationHelper.h" +#include "nsThreadUtils.h" +#include "WebSocketConnectionListener.h" + +namespace mozilla { +namespace net { + +NS_IMPL_ISUPPORTS0(WebSocketConnectionParent) + +WebSocketConnectionParent::WebSocketConnectionParent( + nsIHttpUpgradeListener* aListener) + : mUpgradeListener(aListener), + mBackgroundThread(GetCurrentSerialEventTarget()) { + LOG(("WebSocketConnectionParent ctor %p\n", this)); + MOZ_ASSERT(mUpgradeListener); +} + +WebSocketConnectionParent::~WebSocketConnectionParent() { + LOG(("WebSocketConnectionParent dtor %p\n", this)); +} + +mozilla::ipc::IPCResult WebSocketConnectionParent::RecvOnTransportAvailable( + nsITransportSecurityInfo* aSecurityInfo) { + LOG(("WebSocketConnectionParent::RecvOnTransportAvailable %p\n", this)); + MOZ_ASSERT(mBackgroundThread->IsOnCurrentThread()); + + if (aSecurityInfo) { + MutexAutoLock lock(mMutex); + mSecurityInfo = aSecurityInfo; + } + + if (mUpgradeListener) { + Unused << mUpgradeListener->OnWebSocketConnectionAvailable(this); + mUpgradeListener = nullptr; + } + return IPC_OK(); +} + +mozilla::ipc::IPCResult WebSocketConnectionParent::RecvOnError( + const nsresult& aStatus) { + LOG(("WebSocketConnectionParent::RecvOnError %p\n", this)); + MOZ_ASSERT(mBackgroundThread->IsOnCurrentThread()); + + MOZ_ASSERT(mListener); + mListener->OnError(aStatus); + return IPC_OK(); +} + +mozilla::ipc::IPCResult WebSocketConnectionParent::RecvOnUpgradeFailed( + const nsresult& aReason) { + MOZ_ASSERT(mBackgroundThread->IsOnCurrentThread()); + + if (mUpgradeListener) { + Unused << mUpgradeListener->OnUpgradeFailed(aReason); + mUpgradeListener = nullptr; + } + return IPC_OK(); +} + +mozilla::ipc::IPCResult WebSocketConnectionParent::RecvOnTCPClosed() { + LOG(("WebSocketConnectionParent::RecvOnTCPClosed %p\n", this)); + MOZ_ASSERT(mBackgroundThread->IsOnCurrentThread()); + + MOZ_ASSERT(mListener); + mListener->OnTCPClosed(); + return IPC_OK(); +} + +mozilla::ipc::IPCResult WebSocketConnectionParent::RecvOnDataReceived( + nsTArray&& aData) { + LOG(("WebSocketConnectionParent::RecvOnDataReceived %p\n", this)); + MOZ_ASSERT(mBackgroundThread->IsOnCurrentThread()); + + MOZ_ASSERT(mListener); + uint8_t* buffer = const_cast(aData.Elements()); + nsresult rv = mListener->OnDataReceived(buffer, aData.Length()); + if (NS_FAILED(rv)) { + mListener->OnError(rv); + } + + return IPC_OK(); +} + +void WebSocketConnectionParent::ActorDestroy(ActorDestroyReason aWhy) { + LOG(("WebSocketConnectionParent::ActorDestroy %p aWhy=%d\n", this, aWhy)); + if (!mClosed) { + // Treat this as an error when IPC is closed before + // WebSocketConnectionParent::Close() is called. + RefPtr listener; + listener.swap(mListener); + if (listener) { + listener->OnError(NS_ERROR_FAILURE); + } + } + mBackgroundThread->Dispatch(NS_NewRunnableFunction( + "WebSocketConnectionParent::DefereredDestroy", [self = RefPtr{this}]() { + LOG(("WebSocketConnectionParent::DefereredDestroy")); + })); +}; + +nsresult WebSocketConnectionParent::Init( + WebSocketConnectionListener* aListener) { + NS_ENSURE_ARG_POINTER(aListener); + + mListener = aListener; + return NS_OK; +} + +void WebSocketConnectionParent::GetIoTarget(nsIEventTarget** aTarget) { + nsCOMPtr target = mBackgroundThread; + return target.forget(aTarget); +} + +void WebSocketConnectionParent::Close() { + LOG(("WebSocketConnectionParent::Close %p\n", this)); + + mClosed = true; + + auto task = [self = RefPtr{this}]() { + self->PWebSocketConnectionParent::Close(); + }; + + if (mBackgroundThread->IsOnCurrentThread()) { + task(); + } else { + mBackgroundThread->Dispatch(NS_NewRunnableFunction( + "WebSocketConnectionParent::Close", std::move(task))); + } +} + +nsresult WebSocketConnectionParent::WriteOutputData( + const uint8_t* aHdrBuf, uint32_t aHdrBufLength, const uint8_t* aPayloadBuf, + uint32_t aPayloadBufLength) { + LOG(("WebSocketConnectionParent::WriteOutputData %p", this)); + MOZ_ASSERT(mBackgroundThread->IsOnCurrentThread()); + + if (!CanSend()) { + return NS_ERROR_NOT_AVAILABLE; + } + + nsTArray data; + data.AppendElements(aHdrBuf, aHdrBufLength); + data.AppendElements(aPayloadBuf, aPayloadBufLength); + return SendWriteOutputData(data) ? NS_OK : NS_ERROR_FAILURE; +} + +nsresult WebSocketConnectionParent::StartReading() { + LOG(("WebSocketConnectionParent::StartReading %p\n", this)); + + RefPtr self = this; + auto task = [self{std::move(self)}]() { + if (!self->CanSend()) { + if (self->mListener) { + self->mListener->OnError(NS_ERROR_NOT_AVAILABLE); + } + return; + } + + Unused << self->SendStartReading(); + }; + + if (mBackgroundThread->IsOnCurrentThread()) { + task(); + } else { + mBackgroundThread->Dispatch(NS_NewRunnableFunction( + "WebSocketConnectionParent::SendStartReading", std::move(task))); + } + + return NS_OK; +} + +void WebSocketConnectionParent::DrainSocketData() { + LOG(("WebSocketConnectionParent::DrainSocketData %p\n", this)); + MOZ_ASSERT(mBackgroundThread->IsOnCurrentThread()); + + if (!CanSend()) { + MOZ_ASSERT(mListener); + mListener->OnError(NS_ERROR_NOT_AVAILABLE); + + return; + } + + Unused << SendDrainSocketData(); +} + +nsresult WebSocketConnectionParent::GetSecurityInfo( + nsITransportSecurityInfo** aSecurityInfo) { + LOG(("WebSocketConnectionParent::GetSecurityInfo() %p\n", this)); + MOZ_ASSERT(NS_IsMainThread()); + + NS_ENSURE_ARG_POINTER(aSecurityInfo); + + MutexAutoLock lock(mMutex); + NS_IF_ADDREF(*aSecurityInfo = mSecurityInfo); + return NS_OK; +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/protocol/websocket/WebSocketConnectionParent.h b/netwerk/protocol/websocket/WebSocketConnectionParent.h new file mode 100644 index 0000000000..45b031e9cb --- /dev/null +++ b/netwerk/protocol/websocket/WebSocketConnectionParent.h @@ -0,0 +1,68 @@ +/* -*- 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_WebSocketConnectionParent_h +#define mozilla_net_WebSocketConnectionParent_h + +#include "mozilla/net/PWebSocketConnectionParent.h" +#include "mozilla/net/WebSocketConnectionBase.h" +#include "nsISupportsImpl.h" +#include "WebSocketConnectionBase.h" + +class nsIHttpUpgradeListener; + +namespace mozilla { +namespace net { + +class WebSocketConnectionListener; + +// WebSocketConnectionParent implements WebSocketConnectionBase and provides +// interface for WebSocketChannel to send/receive data. The ownership model for +// WebSocketConnectionParent is that IPDL and WebSocketChannel hold strong +// reference of this object. When Close() is called, a __delete__ message will +// be sent and the IPC actor will be deallocated as well. + +class WebSocketConnectionParent final : public PWebSocketConnectionParent, + public WebSocketConnectionBase { + public: + NS_DECL_THREADSAFE_ISUPPORTS + + explicit WebSocketConnectionParent(nsIHttpUpgradeListener* aListener); + + mozilla::ipc::IPCResult RecvOnTransportAvailable( + nsITransportSecurityInfo* aSecurityInfo); + mozilla::ipc::IPCResult RecvOnError(const nsresult& aStatus); + mozilla::ipc::IPCResult RecvOnTCPClosed(); + mozilla::ipc::IPCResult RecvOnDataReceived(nsTArray&& aData); + mozilla::ipc::IPCResult RecvOnUpgradeFailed(const nsresult& aReason); + + void ActorDestroy(ActorDestroyReason aWhy) override; + + nsresult Init(WebSocketConnectionListener* aListener) override; + void GetIoTarget(nsIEventTarget** aTarget) override; + void Close() override; + nsresult WriteOutputData(const uint8_t* aHdrBuf, uint32_t aHdrBufLength, + const uint8_t* aPayloadBuf, + uint32_t aPayloadBufLength) override; + nsresult StartReading() override; + void DrainSocketData() override; + nsresult GetSecurityInfo(nsITransportSecurityInfo** aSecurityInfo) override; + + private: + virtual ~WebSocketConnectionParent(); + + nsCOMPtr mUpgradeListener; + RefPtr mListener; + nsCOMPtr mBackgroundThread; + nsCOMPtr mSecurityInfo; + Atomic mClosed{false}; + Mutex mMutex MOZ_UNANNOTATED{"WebSocketConnectionParent::mMutex"}; +}; + +} // namespace net +} // namespace mozilla + +#endif // mozilla_net_WebSocketConnectionParent_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..5b0ea16838 --- /dev/null +++ b/netwerk/protocol/websocket/WebSocketEventListenerParent.cpp @@ -0,0 +1,117 @@ +/* -*- 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" +#include "WebSocketFrame.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, aURI, 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, aEffectiveURI, aProtocols, + 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, aReason); + return NS_OK; +} + +NS_IMETHODIMP +WebSocketEventListenerParent::WebSocketMessageAvailable( + uint32_t aWebSocketSerialID, const nsACString& aData, + uint16_t aMessageType) { + Unused << SendWebSocketMessageAvailable(aWebSocketSerialID, 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..6e9a89a005 --- /dev/null +++ b/netwerk/protocol/websocket/WebSocketEventService.cpp @@ -0,0 +1,566 @@ +/* -*- 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.InsertOrUpdate(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; + + mWindows + .LookupOrInsertWith( + aInnerWindowID, + [&] { + auto listener = MakeUnique(); + + if (IsChildProcess()) { + PWebSocketEventListenerChild* actor = + gNeckoChild->SendPWebSocketEventListenerConstructor( + aInnerWindowID); + + listener->mActor = + static_cast(actor); + MOZ_ASSERT(listener->mActor); + } + + return 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..0f15e5058b --- /dev/null +++ b/netwerk/protocol/websocket/WebSocketEventService.h @@ -0,0 +1,125 @@ +/* -*- 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" +#include "nsIWeakReferenceUtils.h" +#include "nsTHashMap.h" + +class nsIWebSocketImpl; +class nsIEventTarget; + +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(); + + using WindowListeners = nsTArray>; + + nsTHashMap 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..745b598ba9 --- /dev/null +++ b/netwerk/protocol/websocket/WebSocketFrame.cpp @@ -0,0 +1,133 @@ +/* -*- 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) { + // This could be called on the background thread when socket process is + // enabled. + 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() + : mFinBit(false), + mRsvBit1(false), + mRsvBit2(false), + mRsvBit3(false), + mMaskBit(false) {} + +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) {} + +void WebSocketFrameData::WriteIPCParams(IPC::MessageWriter* aWriter) const { + WriteParam(aWriter, mTimeStamp); + WriteParam(aWriter, mFinBit); + WriteParam(aWriter, mRsvBit1); + WriteParam(aWriter, mRsvBit2); + WriteParam(aWriter, mRsvBit3); + WriteParam(aWriter, mMaskBit); + WriteParam(aWriter, mOpCode); + WriteParam(aWriter, mMask); + WriteParam(aWriter, mPayload); +} + +bool WebSocketFrameData::ReadIPCParams(IPC::MessageReader* aReader) { + if (!ReadParam(aReader, &mTimeStamp)) { + return false; + } + +#define ReadParamHelper(x) \ + { \ + bool bit; \ + if (!ReadParam(aReader, &bit)) { \ + return false; \ + } \ + (x) = bit; \ + } + + ReadParamHelper(mFinBit); + ReadParamHelper(mRsvBit1); + ReadParamHelper(mRsvBit2); + ReadParamHelper(mRsvBit3); + ReadParamHelper(mMaskBit); + +#undef ReadParamHelper + + return ReadParam(aReader, &mOpCode) && ReadParam(aReader, &mMask) && + ReadParam(aReader, &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..8bcb672692 --- /dev/null +++ b/netwerk/protocol/websocket/WebSocketFrame.h @@ -0,0 +1,104 @@ +/* -*- 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. +using DOMHighResTimeStamp = double; + +namespace IPC { +class Message; +class MessageReader; +class MessageWriter; +template +struct ParamTraits; +} // namespace IPC + +namespace mozilla { +namespace net { + +class WebSocketFrameData final { + public: + WebSocketFrameData(); + + explicit WebSocketFrameData(const WebSocketFrameData&) = default; + WebSocketFrameData(WebSocketFrameData&&) = default; + WebSocketFrameData& operator=(WebSocketFrameData&&) = default; + WebSocketFrameData& operator=(const WebSocketFrameData&) = default; + + WebSocketFrameData(DOMHighResTimeStamp aTimeStamp, bool aFinBit, + bool aRsvBit1, bool aRsvBit2, bool aRsvBit3, + uint8_t aOpCode, bool aMaskBit, uint32_t aMask, + const nsCString& aPayload); + + ~WebSocketFrameData() = default; + + // For IPC serialization + void WriteIPCParams(IPC::MessageWriter* aWriter) const; + bool ReadIPCParams(IPC::MessageReader* aReader); + + DOMHighResTimeStamp mTimeStamp{0}; + + bool mFinBit : 1; + bool mRsvBit1 : 1; + bool mRsvBit2 : 1; + bool mRsvBit3 : 1; + bool mMaskBit : 1; + uint8_t mOpCode{0}; + + uint32_t mMask{0}; + + 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 { + using paramType = mozilla::net::WebSocketFrameData; + + static void Write(MessageWriter* aWriter, const paramType& aParam) { + aParam.WriteIPCParams(aWriter); + } + + static bool Read(MessageReader* aReader, paramType* aResult) { + return aResult->ReadIPCParams(aReader); + } +}; + +} // 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..123d364b70 --- /dev/null +++ b/netwerk/protocol/websocket/moz.build @@ -0,0 +1,67 @@ +# -*- 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", + "WebSocketConnectionBase.h", + "WebSocketConnectionChild.h", + "WebSocketConnectionListener.h", + "WebSocketConnectionParent.h", + "WebSocketEventListenerChild.h", + "WebSocketEventListenerParent.h", + "WebSocketEventService.h", + "WebSocketFrame.h", +] + +UNIFIED_SOURCES += [ + "BaseWebSocketChannel.cpp", + "IPCTransportProvider.cpp", + "WebSocketChannel.cpp", + "WebSocketChannelChild.cpp", + "WebSocketChannelParent.cpp", + "WebSocketConnection.cpp", + "WebSocketConnectionChild.cpp", + "WebSocketConnectionParent.cpp", + "WebSocketEventListenerChild.cpp", + "WebSocketEventListenerParent.cpp", + "WebSocketEventService.cpp", + "WebSocketFrame.cpp", +] + +IPDL_SOURCES += [ + "PTransportProvider.ipdl", + "PWebSocket.ipdl", + "PWebSocketConnection.ipdl", + "PWebSocketEventListener.ipdl", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" + +LOCAL_INCLUDES += [ + "/dom/base", + "/netwerk/base", +] + +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..7092c3c6b1 --- /dev/null +++ b/netwerk/protocol/websocket/nsIWebSocketChannel.idl @@ -0,0 +1,268 @@ +/* -*- 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 nsIInputStream; +interface nsIInterfaceRequestor; +interface nsILoadGroup; +interface nsILoadInfo; +interface nsIPrincipal; +interface nsITransportProvider; +interface nsITransportSecurityInfo; +interface nsIURI; +interface nsIWebSocketListener; + +webidl Node; + +#include "nsISupports.idl" +#include "nsIContentPolicy.idl" + + + +[ref] native OriginAttributes(const mozilla::OriginAttributes); + +/** + * 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 nsITransportSecurityInfo 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 aOriginAttributes attributes 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 + */ + [implicit_jscontext] + void asyncOpen(in nsIURI aURI, + in ACString aOrigin, + in jsval aOriginAttributes, + in unsigned long long aInnerWindowID, + in nsIWebSocketListener aListener, + in nsISupports aContext); + + [must_use] + void asyncOpenNative(in nsIURI aURI, + in ACString aOrigin, + in OriginAttributes aOriginAttributes, + 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..31c588630b --- /dev/null +++ b/netwerk/protocol/websocket/nsIWebSocketListener.idl @@ -0,0 +1,94 @@ +/* -*- 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(); + +}; diff --git a/netwerk/protocol/webtransport/WebTransportHash.cpp b/netwerk/protocol/webtransport/WebTransportHash.cpp new file mode 100644 index 0000000000..f53fbacbcf --- /dev/null +++ b/netwerk/protocol/webtransport/WebTransportHash.cpp @@ -0,0 +1,24 @@ +/* -*- 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 "WebTransportHash.h" + +namespace mozilla::net { + +NS_IMPL_ISUPPORTS(WebTransportHash, nsIWebTransportHash); + +NS_IMETHODIMP +WebTransportHash::GetValue(nsTArray& aValue) { + aValue.Clear(); + aValue.AppendElements(mValue); + return NS_OK; +} + +NS_IMETHODIMP +WebTransportHash::GetAlgorithm(nsACString& algorithm) { + algorithm.Assign(mAlgorithm); + return NS_OK; +} +} // namespace mozilla::net diff --git a/netwerk/protocol/webtransport/WebTransportHash.h b/netwerk/protocol/webtransport/WebTransportHash.h new file mode 100644 index 0000000000..b3037f560a --- /dev/null +++ b/netwerk/protocol/webtransport/WebTransportHash.h @@ -0,0 +1,30 @@ +/* -*- 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 mozilla_net_WebTransportHash_h +#define mozilla_net_WebTransportHash_h + +#include "nsIWebTransport.h" + +namespace mozilla::net { + +class WebTransportHash final : public nsIWebTransportHash { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIWEBTRANSPORTHASH + + explicit WebTransportHash(const nsACString& algorithm, + const nsTArray& value) + : mAlgorithm(algorithm), mValue(Span(value)) {} + + private: + ~WebTransportHash() = default; + nsCString mAlgorithm; + nsTArray mValue; +}; + +} // namespace mozilla::net + +#endif // mozilla_net_WebTransportHash_h diff --git a/netwerk/protocol/webtransport/WebTransportLog.h b/netwerk/protocol/webtransport/WebTransportLog.h new file mode 100644 index 0000000000..99f211b893 --- /dev/null +++ b/netwerk/protocol/webtransport/WebTransportLog.h @@ -0,0 +1,21 @@ +/* -*- 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 WebTransportLog_h +#define WebTransportLog_h + +#include "mozilla/Logging.h" +#include "mozilla/net/NeckoChild.h" + +namespace mozilla::net { +extern LazyLogModule webTransportLog; +} // namespace mozilla::net + +#undef LOG +#define LOG(args) \ + MOZ_LOG(mozilla::net::webTransportLog, mozilla::LogLevel::Debug, args) + +#endif diff --git a/netwerk/protocol/webtransport/WebTransportSessionProxy.cpp b/netwerk/protocol/webtransport/WebTransportSessionProxy.cpp new file mode 100644 index 0000000000..e650a74d5b --- /dev/null +++ b/netwerk/protocol/webtransport/WebTransportSessionProxy.cpp @@ -0,0 +1,1269 @@ +/* -*- 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 "WebTransportLog.h" +#include "Http3WebTransportSession.h" +#include "Http3WebTransportStream.h" +#include "ScopedNSSTypes.h" +#include "WebTransportSessionProxy.h" +#include "WebTransportStreamProxy.h" +#include "nsIAsyncVerifyRedirectCallback.h" +#include "nsIHttpChannel.h" +#include "nsIHttpChannelInternal.h" +#include "nsIRequest.h" +#include "nsITransportSecurityInfo.h" +#include "nsIX509Cert.h" +#include "nsNetUtil.h" +#include "nsProxyRelease.h" +#include "nsSocketTransportService2.h" +#include "mozilla/Logging.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/StaticPrefs_network.h" + +namespace mozilla::net { + +LazyLogModule webTransportLog("nsWebTransport"); + +NS_IMPL_ISUPPORTS(WebTransportSessionProxy, WebTransportSessionEventListener, + nsIWebTransport, nsIRedirectResultListener, nsIStreamListener, + nsIChannelEventSink, nsIInterfaceRequestor); + +WebTransportSessionProxy::WebTransportSessionProxy() + : mMutex("WebTransportSessionProxy::mMutex"), + mTarget(GetMainThreadSerialEventTarget()) { + LOG(("WebTransportSessionProxy constructor")); +} + +WebTransportSessionProxy::~WebTransportSessionProxy() { + if (OnSocketThread()) { + return; + } + + MutexAutoLock lock(mMutex); + if ((mState != WebTransportSessionProxyState::NEGOTIATING_SUCCEEDED) && + (mState != WebTransportSessionProxyState::ACTIVE) && + (mState != WebTransportSessionProxyState::SESSION_CLOSE_PENDING)) { + return; + } + + MOZ_ASSERT(mState != WebTransportSessionProxyState::SESSION_CLOSE_PENDING, + "We can not be in the SESSION_CLOSE_PENDING state in destructor, " + "because should e an runnable that holds reference to this" + "object."); + + Unused << gSocketTransportService->Dispatch(NS_NewRunnableFunction( + "WebTransportSessionProxy::ProxyHttp3WebTransportSessionRelease", + [self{std::move(mWebTransportSession)}]() {})); +} + +//----------------------------------------------------------------------------- +// WebTransportSessionProxy::nsIWebTransport +//----------------------------------------------------------------------------- + +nsresult WebTransportSessionProxy::AsyncConnect( + nsIURI* aURI, + const nsTArray>& aServerCertHashes, + nsIPrincipal* aPrincipal, uint32_t aSecurityFlags, + WebTransportSessionEventListener* aListener) { + return AsyncConnectWithClient(aURI, std::move(aServerCertHashes), aPrincipal, + aSecurityFlags, aListener, + Maybe()); +} + +nsresult WebTransportSessionProxy::AsyncConnectWithClient( + nsIURI* aURI, + const nsTArray>& aServerCertHashes, + nsIPrincipal* aPrincipal, uint32_t aSecurityFlags, + WebTransportSessionEventListener* aListener, + const Maybe& aClientInfo) { + MOZ_ASSERT(NS_IsMainThread()); + + LOG(("WebTransportSessionProxy::AsyncConnect")); + { + MutexAutoLock lock(mMutex); + mListener = aListener; + } + auto cleanup = MakeScopeExit([self = RefPtr(this)] { + MutexAutoLock lock(self->mMutex); + self->mListener->OnSessionClosed(false, 0, + ""_ns); // TODO: find a better error. + self->mChannel = nullptr; + self->mListener = nullptr; + self->ChangeState(WebTransportSessionProxyState::DONE); + }); + + nsSecurityFlags flags = nsILoadInfo::SEC_COOKIES_OMIT | aSecurityFlags; + nsLoadFlags loadFlags = nsIRequest::LOAD_NORMAL | + nsIRequest::LOAD_BYPASS_CACHE | + nsIRequest::INHIBIT_CACHING; + nsresult rv = NS_ERROR_FAILURE; + + if (aClientInfo.isSome()) { + rv = NS_NewChannel(getter_AddRefs(mChannel), aURI, aPrincipal, + aClientInfo.ref(), Maybe(), + flags, nsContentPolicyType::TYPE_WEB_TRANSPORT, + /* aCookieJarSettings */ nullptr, + /* aPerformanceStorage */ nullptr, + /* aLoadGroup */ nullptr, + /* aCallbacks */ this, loadFlags); + } else { + rv = NS_NewChannel(getter_AddRefs(mChannel), aURI, aPrincipal, flags, + nsContentPolicyType::TYPE_WEB_TRANSPORT, + /* aCookieJarSettings */ nullptr, + /* aPerformanceStorage */ nullptr, + /* aLoadGroup */ nullptr, + /* aCallbacks */ this, loadFlags); + } + + NS_ENSURE_SUCCESS(rv, rv); + + // configure HTTP specific stuff + nsCOMPtr httpChannel = do_QueryInterface(mChannel); + if (!httpChannel) { + mChannel = nullptr; + return NS_ERROR_ABORT; + } + + if (!aServerCertHashes.IsEmpty()) { + mServerCertHashes.AppendElements(aServerCertHashes); + } + + { + MutexAutoLock lock(mMutex); + ChangeState(WebTransportSessionProxyState::NEGOTIATING); + } + + // https://www.ietf.org/archive/id/draft-ietf-webtrans-http3-04.html#section-6 + rv = httpChannel->SetRequestHeader("Sec-Webtransport-Http3-Draft02"_ns, + "1"_ns, false); + if (NS_FAILED(rv)) { + return rv; + } + + // To establish a WebTransport session with an origin origin, follow + // [WEB-TRANSPORT-HTTP3] section 3.3, with using origin, serialized and + // isomorphic encoded, as the `Origin` header of the request. + // https://www.w3.org/TR/webtransport/#protocol-concepts + nsAutoCString serializedOrigin; + if (NS_FAILED( + aPrincipal->GetWebExposedOriginSerialization(serializedOrigin))) { + // origin/URI will be missing for system principals + // assign null origin + serializedOrigin = "null"_ns; + } + + rv = httpChannel->SetRequestHeader("Origin"_ns, serializedOrigin, false); + if (NS_FAILED(rv)) { + return rv; + } + + nsCOMPtr internalChannel = + do_QueryInterface(mChannel); + if (!internalChannel) { + mChannel = nullptr; + return NS_ERROR_ABORT; + } + Unused << internalChannel->SetWebTransportSessionEventListener(this); + + rv = mChannel->AsyncOpen(this); + if (NS_SUCCEEDED(rv)) { + cleanup.release(); + } + return rv; +} + +NS_IMETHODIMP +WebTransportSessionProxy::RetargetTo(nsIEventTarget* aTarget) { + if (!aTarget) { + return NS_ERROR_INVALID_ARG; + } + + { + MutexAutoLock lock(mMutex); + LOG(("WebTransportSessionProxy::RetargetTo mState=%d", mState)); + // RetargetTo should be only called after the session is ready. + if (mState != WebTransportSessionProxyState::ACTIVE) { + return NS_ERROR_UNEXPECTED; + } + + mTarget = aTarget; + } + + return NS_OK; +} + +NS_IMETHODIMP +WebTransportSessionProxy::GetStats() { return NS_ERROR_NOT_IMPLEMENTED; } + +NS_IMETHODIMP +WebTransportSessionProxy::CloseSession(uint32_t status, + const nsACString& reason) { + MutexAutoLock lock(mMutex); + MOZ_ASSERT(mTarget->IsOnCurrentThread()); + mCloseStatus = status; + mReason = reason; + mListener = nullptr; + mPendingEvents.Clear(); + mServerCertHashes.Clear(); + switch (mState) { + case WebTransportSessionProxyState::INIT: + case WebTransportSessionProxyState::DONE: + return NS_ERROR_NOT_INITIALIZED; + case WebTransportSessionProxyState::NEGOTIATING: + mChannel->Cancel(NS_ERROR_ABORT); + mChannel = nullptr; + ChangeState(WebTransportSessionProxyState::DONE); + break; + case WebTransportSessionProxyState::NEGOTIATING_SUCCEEDED: + mChannel->Cancel(NS_ERROR_ABORT); + mChannel = nullptr; + ChangeState(WebTransportSessionProxyState::SESSION_CLOSE_PENDING); + CloseSessionInternal(); + break; + case WebTransportSessionProxyState::ACTIVE: + ChangeState(WebTransportSessionProxyState::SESSION_CLOSE_PENDING); + CloseSessionInternal(); + break; + case WebTransportSessionProxyState::CLOSE_CALLBACK_PENDING: + ChangeState(WebTransportSessionProxyState::DONE); + break; + case SESSION_CLOSE_PENDING: + break; + } + return NS_OK; +} + +void WebTransportSessionProxy::CloseSessionInternalLocked() { + MutexAutoLock lock(mMutex); + CloseSessionInternal(); +} + +void WebTransportSessionProxy::CloseSessionInternal() { + if (!OnSocketThread()) { + mMutex.AssertCurrentThreadOwns(); + RefPtr self(this); + Unused << gSocketTransportService->Dispatch(NS_NewRunnableFunction( + "WebTransportSessionProxy::CallCloseWebTransportSession", + [self{std::move(self)}]() { self->CloseSessionInternalLocked(); })); + return; + } + + mMutex.AssertCurrentThreadOwns(); + + RefPtr wt; + uint32_t closeStatus = 0; + nsCString reason; + + if (mState == WebTransportSessionProxyState::SESSION_CLOSE_PENDING) { + MOZ_ASSERT(mWebTransportSession); + wt = mWebTransportSession; + mWebTransportSession = nullptr; + closeStatus = mCloseStatus; + reason = mReason; + ChangeState(WebTransportSessionProxyState::DONE); + } else { + MOZ_ASSERT(mState == WebTransportSessionProxyState::DONE); + } + + if (wt) { + MutexAutoUnlock unlock(mMutex); + wt->CloseSession(closeStatus, reason); + } +} + +class WebTransportStreamCallbackWrapper final { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(WebTransportStreamCallbackWrapper) + + explicit WebTransportStreamCallbackWrapper( + nsIWebTransportStreamCallback* aCallback, bool aBidi) + : mCallback(aCallback), + mTarget(GetCurrentSerialEventTarget()), + mBidi(aBidi) {} + + void CallOnError(nsresult aError) { + if (!mTarget->IsOnCurrentThread()) { + RefPtr self(this); + Unused << mTarget->Dispatch(NS_NewRunnableFunction( + "WebTransportStreamCallbackWrapper::CallOnError", + [self{std::move(self)}, error{aError}]() { + self->CallOnError(error); + })); + return; + } + + LOG(("WebTransportStreamCallbackWrapper::OnError aError=0x%" PRIx32, + static_cast(aError))); + Unused << mCallback->OnError(nsIWebTransport::INVALID_STATE_ERROR); + } + + void CallOnStreamReady(WebTransportStreamProxy* aStream) { + if (!mTarget->IsOnCurrentThread()) { + RefPtr self(this); + RefPtr stream = aStream; + Unused << mTarget->Dispatch(NS_NewRunnableFunction( + "WebTransportStreamCallbackWrapper::CallOnStreamReady", + [self{std::move(self)}, stream{std::move(stream)}]() { + self->CallOnStreamReady(stream); + })); + return; + } + + if (mBidi) { + Unused << mCallback->OnBidirectionalStreamReady(aStream); + return; + } + + Unused << mCallback->OnUnidirectionalStreamReady(aStream); + } + + private: + virtual ~WebTransportStreamCallbackWrapper() { + NS_ProxyRelease( + "WebTransportStreamCallbackWrapper::~WebTransportStreamCallbackWrapper", + mTarget, mCallback.forget()); + } + + nsCOMPtr mCallback; + nsCOMPtr mTarget; + bool mBidi = false; +}; + +void WebTransportSessionProxy::CreateStreamInternal( + nsIWebTransportStreamCallback* callback, bool aBidi) { + mMutex.AssertCurrentThreadOwns(); + LOG( + ("WebTransportSessionProxy::CreateStreamInternal %p " + "mState=%d, bidi=%d", + this, mState, aBidi)); + switch (mState) { + case WebTransportSessionProxyState::INIT: + case WebTransportSessionProxyState::NEGOTIATING: + case WebTransportSessionProxyState::NEGOTIATING_SUCCEEDED: + case WebTransportSessionProxyState::ACTIVE: { + RefPtr wrapper = + new WebTransportStreamCallbackWrapper(callback, aBidi); + if (mState == WebTransportSessionProxyState::ACTIVE && + mWebTransportSession) { + DoCreateStream(wrapper, mWebTransportSession, aBidi); + } else { + LOG( + ("WebTransportSessionProxy::CreateStreamInternal %p " + " queue create stream event", + this)); + auto task = [self = RefPtr{this}, wrapper{std::move(wrapper)}, + bidi(aBidi)](nsresult aStatus) { + if (NS_FAILED(aStatus)) { + wrapper->CallOnError(aStatus); + return; + } + + self->DoCreateStream(wrapper, nullptr, bidi); + }; + // TODO: we should do this properly in bug 1830362. + mPendingCreateStreamEvents.AppendElement(std::move(task)); + } + } break; + case WebTransportSessionProxyState::SESSION_CLOSE_PENDING: + case WebTransportSessionProxyState::CLOSE_CALLBACK_PENDING: + case WebTransportSessionProxyState::DONE: { + nsCOMPtr cb(callback); + NS_DispatchToCurrentThread(NS_NewRunnableFunction( + "WebTransportSessionProxy::CreateStreamInternal", + [cb{std::move(cb)}]() { + cb->OnError(nsIWebTransport::INVALID_STATE_ERROR); + })); + } break; + } +} + +void WebTransportSessionProxy::DoCreateStream( + WebTransportStreamCallbackWrapper* aCallback, + Http3WebTransportSession* aSession, bool aBidi) { + if (!OnSocketThread()) { + RefPtr self(this); + RefPtr wrapper(aCallback); + Unused << gSocketTransportService->Dispatch(NS_NewRunnableFunction( + "WebTransportSessionProxy::DoCreateStream", + [self{std::move(self)}, wrapper{std::move(wrapper)}, bidi(aBidi)]() { + self->DoCreateStream(wrapper, nullptr, bidi); + })); + return; + } + + LOG(("WebTransportSessionProxy::DoCreateStream %p bidi=%d", this, aBidi)); + + RefPtr session = aSession; + // Having no session here means that this is called by dispatching tasks. + // The mState may be already changed, so we need to check it again. + if (!aSession) { + MutexAutoLock lock(mMutex); + switch (mState) { + case WebTransportSessionProxyState::INIT: + case WebTransportSessionProxyState::NEGOTIATING: + case WebTransportSessionProxyState::NEGOTIATING_SUCCEEDED: + MOZ_ASSERT(false, "DoCreateStream called with invalid state"); + aCallback->CallOnError(NS_ERROR_UNEXPECTED); + return; + case WebTransportSessionProxyState::ACTIVE: { + session = mWebTransportSession; + } break; + case WebTransportSessionProxyState::SESSION_CLOSE_PENDING: + case WebTransportSessionProxyState::CLOSE_CALLBACK_PENDING: + case WebTransportSessionProxyState::DONE: + // Session is going to be closed. + aCallback->CallOnError(NS_ERROR_NOT_AVAILABLE); + return; + } + } + + if (!session) { + MOZ_ASSERT_UNREACHABLE("This should not happen"); + aCallback->CallOnError(NS_ERROR_UNEXPECTED); + return; + } + + RefPtr wrapper(aCallback); + auto callback = + [wrapper{std::move(wrapper)}]( + Result, nsresult>&& aResult) { + if (aResult.isErr()) { + wrapper->CallOnError(aResult.unwrapErr()); + return; + } + + RefPtr stream = aResult.unwrap(); + RefPtr streamProxy = + new WebTransportStreamProxy(stream); + wrapper->CallOnStreamReady(streamProxy); + }; + + if (aBidi) { + session->CreateOutgoingBidirectionalStream(std::move(callback)); + } else { + session->CreateOutgoingUnidirectionalStream(std::move(callback)); + } +} + +NS_IMETHODIMP +WebTransportSessionProxy::CreateOutgoingUnidirectionalStream( + nsIWebTransportStreamCallback* callback) { + if (!callback) { + return NS_ERROR_INVALID_ARG; + } + + MutexAutoLock lock(mMutex); + CreateStreamInternal(callback, false); + return NS_OK; +} + +NS_IMETHODIMP +WebTransportSessionProxy::CreateOutgoingBidirectionalStream( + nsIWebTransportStreamCallback* callback) { + if (!callback) { + return NS_ERROR_INVALID_ARG; + } + + MutexAutoLock lock(mMutex); + CreateStreamInternal(callback, true); + return NS_OK; +} + +void WebTransportSessionProxy::SendDatagramInternal( + const RefPtr& aSession, nsTArray&& aData, + uint64_t aTrackingId) { + MOZ_ASSERT(OnSocketThread()); + + aSession->SendDatagram(std::move(aData), aTrackingId); +} + +NS_IMETHODIMP +WebTransportSessionProxy::SendDatagram(const nsTArray& aData, + uint64_t aTrackingId) { + RefPtr session; + { + MutexAutoLock lock(mMutex); + if (mState != WebTransportSessionProxyState::ACTIVE || + !mWebTransportSession) { + return NS_ERROR_NOT_AVAILABLE; + } + session = mWebTransportSession; + } + + nsTArray copied; + copied.Assign(aData); + if (!OnSocketThread()) { + return gSocketTransportService->Dispatch(NS_NewRunnableFunction( + "WebTransportSessionProxy::SendDatagramInternal", + [self = RefPtr{this}, session{std::move(session)}, + data{std::move(copied)}, trackingId(aTrackingId)]() mutable { + self->SendDatagramInternal(session, std::move(data), trackingId); + })); + } + + SendDatagramInternal(session, std::move(copied), aTrackingId); + return NS_OK; +} + +void WebTransportSessionProxy::GetMaxDatagramSizeInternal( + const RefPtr& aSession) { + MOZ_ASSERT(OnSocketThread()); + + aSession->GetMaxDatagramSize(); +} + +NS_IMETHODIMP +WebTransportSessionProxy::GetMaxDatagramSize() { + RefPtr session; + { + MutexAutoLock lock(mMutex); + if (mState != WebTransportSessionProxyState::ACTIVE || + !mWebTransportSession) { + return NS_ERROR_NOT_AVAILABLE; + } + session = mWebTransportSession; + } + + if (!OnSocketThread()) { + return gSocketTransportService->Dispatch(NS_NewRunnableFunction( + "WebTransportSessionProxy::GetMaxDatagramSizeInternal", + [self = RefPtr{this}, session{std::move(session)}]() { + self->GetMaxDatagramSizeInternal(session); + })); + } + + GetMaxDatagramSizeInternal(session); + return NS_OK; +} + +//----------------------------------------------------------------------------- +// WebTransportSessionProxy::nsIStreamListener +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +WebTransportSessionProxy::OnStartRequest(nsIRequest* aRequest) { + MOZ_ASSERT(NS_IsMainThread()); + LOG(("WebTransportSessionProxy::OnStartRequest\n")); + nsCOMPtr listener; + nsAutoCString reason; + uint32_t closeStatus = 0; + { + MutexAutoLock lock(mMutex); + switch (mState) { + case WebTransportSessionProxyState::INIT: + case WebTransportSessionProxyState::DONE: + case WebTransportSessionProxyState::ACTIVE: + case WebTransportSessionProxyState::SESSION_CLOSE_PENDING: + MOZ_ASSERT(false, "OnStartRequest cannot be called in this state."); + break; + case WebTransportSessionProxyState::NEGOTIATING: + case WebTransportSessionProxyState::CLOSE_CALLBACK_PENDING: + listener = mListener; + mListener = nullptr; + mChannel = nullptr; + reason = mReason; + closeStatus = mCloseStatus; + ChangeState(WebTransportSessionProxyState::DONE); + break; + case WebTransportSessionProxyState::NEGOTIATING_SUCCEEDED: { + uint32_t status; + + nsCOMPtr httpChannel = do_QueryInterface(mChannel); + if (!httpChannel || + NS_FAILED(httpChannel->GetResponseStatus(&status)) || + !(status >= 200 && status < 300) || + !CheckServerCertificateIfNeeded()) { + listener = mListener; + mListener = nullptr; + mChannel = nullptr; + mReason = ""_ns; + reason = ""_ns; + mCloseStatus = + 0; // TODO: find a better error. Currently error code 0 is used + ChangeState(WebTransportSessionProxyState::SESSION_CLOSE_PENDING); + CloseSessionInternal(); // TODO: find a better error. Currently error + // code 0 is used. + } + // The success cases will be handled in OnStopRequest. + } break; + } + } + if (listener) { + listener->OnSessionClosed(false, closeStatus, reason); + } + return NS_OK; +} + +NS_IMETHODIMP +WebTransportSessionProxy::OnDataAvailable(nsIRequest* aRequest, + nsIInputStream* aStream, + uint64_t aOffset, uint32_t aCount) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_RELEASE_ASSERT( + false, "WebTransportSessionProxy::OnDataAvailable should not be called"); + return NS_OK; +} + +NS_IMETHODIMP +WebTransportSessionProxy::OnStopRequest(nsIRequest* aRequest, + nsresult aStatus) { + MOZ_ASSERT(NS_IsMainThread()); + mChannel = nullptr; + nsCOMPtr listener; + nsAutoCString reason; + uint32_t closeStatus = 0; + uint64_t sessionId; + bool succeeded = false; + nsTArray> pendingEvents; + nsTArray> pendingCreateStreamEvents; + { + MutexAutoLock lock(mMutex); + switch (mState) { + case WebTransportSessionProxyState::INIT: + case WebTransportSessionProxyState::ACTIVE: + case WebTransportSessionProxyState::NEGOTIATING: + MOZ_ASSERT(false, "OnStopRequest cannot be called in this state."); + break; + case WebTransportSessionProxyState::CLOSE_CALLBACK_PENDING: + reason = mReason; + closeStatus = mCloseStatus; + listener = mListener; + mListener = nullptr; + ChangeState(WebTransportSessionProxyState::DONE); + break; + case WebTransportSessionProxyState::NEGOTIATING_SUCCEEDED: + if (NS_FAILED(aStatus)) { + listener = mListener; + mListener = nullptr; + mReason = ""_ns; + reason = ""_ns; + mCloseStatus = 0; + ChangeState(WebTransportSessionProxyState::SESSION_CLOSE_PENDING); + CloseSessionInternal(); // TODO: find a better error. Currently error + // code 0 is used. + } else { + succeeded = true; + sessionId = mSessionId; + listener = mListener; + ChangeState(WebTransportSessionProxyState::ACTIVE); + } + break; + case WebTransportSessionProxyState::SESSION_CLOSE_PENDING: + case WebTransportSessionProxyState::DONE: + break; + } + pendingEvents = std::move(mPendingEvents); + pendingCreateStreamEvents = std::move(mPendingCreateStreamEvents); + if (!pendingCreateStreamEvents.IsEmpty()) { + if (NS_SUCCEEDED(aStatus) && + (mState == WebTransportSessionProxyState::DONE || + mState == WebTransportSessionProxyState::CLOSE_CALLBACK_PENDING || + mState == WebTransportSessionProxyState::SESSION_CLOSE_PENDING)) { + aStatus = NS_ERROR_FAILURE; + } + } + + mStopRequestCalled = true; + } + + if (!pendingCreateStreamEvents.IsEmpty()) { + Unused << gSocketTransportService->Dispatch(NS_NewRunnableFunction( + "WebTransportSessionProxy::DispatchPendingCreateStreamEvents", + [pendingCreateStreamEvents = std::move(pendingCreateStreamEvents), + status(aStatus)]() { + for (const auto& event : pendingCreateStreamEvents) { + event(status); + } + })); + } // otherwise let the CreateStreams just go away + + if (listener) { + if (succeeded) { + listener->OnSessionReady(sessionId); + if (!pendingEvents.IsEmpty()) { + Unused << gSocketTransportService->Dispatch(NS_NewRunnableFunction( + "WebTransportSessionProxy::DispatchPendingEvents", + [pendingEvents = std::move(pendingEvents)]() { + for (const auto& event : pendingEvents) { + event(); + } + })); + } + } else { + listener->OnSessionClosed(false, closeStatus, + reason); // TODO: find a better error. + // Currently error code 0 is used. + } + } + return NS_OK; +} + +//----------------------------------------------------------------------------- +// WebTransportSessionProxy::nsIChannelEventSink +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +WebTransportSessionProxy::AsyncOnChannelRedirect( + nsIChannel* aOldChannel, nsIChannel* aNewChannel, uint32_t aFlags, + nsIAsyncVerifyRedirectCallback* callback) { + // Currently implementation we do not reach this part of the code + // as location headers are not forwarded by the http3 stack to the applicaion. + // Hence, the channel is aborted due to the location header check in + // nsHttpChannel::AsyncProcessRedirection This comment must be removed after + // the following neqo bug is resolved + // https://github.com/mozilla/neqo/issues/1364 + if (!StaticPrefs::network_webtransport_redirect_enabled()) { + LOG(("Channel Redirects are disabled for WebTransport sessions")); + return NS_ERROR_ABORT; + } + + nsCOMPtr newURI; + nsresult rv = NS_GetFinalChannelURI(aNewChannel, getter_AddRefs(newURI)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = aNewChannel->GetURI(getter_AddRefs(newURI)); + if (NS_FAILED(rv)) { + callback->OnRedirectVerifyCallback(rv); + return NS_OK; + } + + // abort the request if redirecting to insecure context + if (!newURI->SchemeIs("https")) { + callback->OnRedirectVerifyCallback(NS_ERROR_ABORT); + return NS_OK; + } + + // Assign to mChannel after we get notification about success of the + // redirect in OnRedirectResult. + mRedirectChannel = aNewChannel; + + callback->OnRedirectVerifyCallback(NS_OK); + return NS_OK; +} + +//----------------------------------------------------------------------------- +// WebTransportSessionProxy::nsIRedirectResultListener +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +WebTransportSessionProxy::OnRedirectResult(nsresult aStatus) { + if (NS_SUCCEEDED(aStatus) && mRedirectChannel) { + mChannel = mRedirectChannel; + } + + mRedirectChannel = nullptr; + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// WebTransportSessionProxy::nsIInterfaceRequestor +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +WebTransportSessionProxy::GetInterface(const nsIID& aIID, void** aResult) { + if (aIID.Equals(NS_GET_IID(nsIChannelEventSink))) { + NS_ADDREF_THIS(); + *aResult = static_cast(this); + return NS_OK; + } + + if (aIID.Equals(NS_GET_IID(nsIRedirectResultListener))) { + NS_ADDREF_THIS(); + *aResult = static_cast(this); + return NS_OK; + } + + return NS_ERROR_NO_INTERFACE; +} + +//----------------------------------------------------------------------------- +// WebTransportSessionProxy::WebTransportSessionEventListener +//----------------------------------------------------------------------------- + +// This function is called when the Http3WebTransportSession is ready. After +// this call WebTransportSessionProxy is responsible for the +// Http3WebTransportSession, i.e. it is responsible for closing it. +// The listener of the WebTransportSessionProxy will be informed during +// OnStopRequest call. +NS_IMETHODIMP +WebTransportSessionProxy::OnSessionReadyInternal( + Http3WebTransportSession* aSession) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG(("WebTransportSessionProxy::OnSessionReadyInternal")); + MutexAutoLock lock(mMutex); + switch (mState) { + case WebTransportSessionProxyState::INIT: + case WebTransportSessionProxyState::CLOSE_CALLBACK_PENDING: + case WebTransportSessionProxyState::ACTIVE: + case WebTransportSessionProxyState::SESSION_CLOSE_PENDING: + case WebTransportSessionProxyState::NEGOTIATING_SUCCEEDED: + MOZ_ASSERT(false, + "OnSessionReadyInternal cannot be called in this state."); + return NS_ERROR_ABORT; + case WebTransportSessionProxyState::NEGOTIATING: + mWebTransportSession = aSession; + mSessionId = aSession->StreamId(); + ChangeState(WebTransportSessionProxyState::NEGOTIATING_SUCCEEDED); + break; + case WebTransportSessionProxyState::DONE: + // The session has been canceled. We do not need to set + // mWebTransportSession. + break; + } + return NS_OK; +} + +NS_IMETHODIMP +WebTransportSessionProxy::OnIncomingStreamAvailableInternal( + Http3WebTransportStream* aStream) { + nsCOMPtr listener; + { + MutexAutoLock lock(mMutex); + + LOG( + ("WebTransportSessionProxy::OnIncomingStreamAvailableInternal %p " + "mState=%d " + "mStopRequestCalled=%d", + this, mState, mStopRequestCalled)); + // Since OnSessionReady on the listener is called on the main thread, + // OnIncomingStreamAvailableInternal and OnSessionReady can be racy. If + // OnStopRequest is not called yet, OnIncomingStreamAvailableInternal needs + // to wait. + if (!mStopRequestCalled) { + mPendingEvents.AppendElement( + [self = RefPtr{this}, stream = RefPtr{aStream}]() { + self->OnIncomingStreamAvailableInternal(stream); + }); + return NS_OK; + } + + if (!mTarget->IsOnCurrentThread()) { + RefPtr self(this); + RefPtr stream = aStream; + Unused << mTarget->Dispatch(NS_NewRunnableFunction( + "WebTransportSessionProxy::OnIncomingStreamAvailableInternal", + [self{std::move(self)}, stream{std::move(stream)}]() { + self->OnIncomingStreamAvailableInternal(stream); + })); + return NS_OK; + } + + LOG( + ("WebTransportSessionProxy::OnIncomingStreamAvailableInternal %p " + "mState=%d mListener=%p", + this, mState, mListener.get())); + if (mState == WebTransportSessionProxyState::ACTIVE) { + listener = mListener; + } + } + + if (!listener) { + // Session can be already closed. + return NS_OK; + } + + RefPtr streamProxy = + new WebTransportStreamProxy(aStream); + if (aStream->StreamType() == WebTransportStreamType::BiDi) { + Unused << listener->OnIncomingBidirectionalStreamAvailable(streamProxy); + } else { + Unused << listener->OnIncomingUnidirectionalStreamAvailable(streamProxy); + } + return NS_OK; +} + +NS_IMETHODIMP +WebTransportSessionProxy::OnIncomingBidirectionalStreamAvailable( + nsIWebTransportBidirectionalStream* aStream) { + return NS_OK; +} + +NS_IMETHODIMP +WebTransportSessionProxy::OnIncomingUnidirectionalStreamAvailable( + nsIWebTransportReceiveStream* aStream) { + return NS_OK; +} + +NS_IMETHODIMP +WebTransportSessionProxy::OnSessionReady(uint64_t ready) { + MOZ_ASSERT(false, "Should not b called"); + return NS_OK; +} + +NS_IMETHODIMP +WebTransportSessionProxy::OnSessionClosed(bool aCleanly, uint32_t aStatus, + const nsACString& aReason) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + MutexAutoLock lock(mMutex); + LOG( + ("WebTransportSessionProxy::OnSessionClosed %p mState=%d " + "mStopRequestCalled=%d", + this, mState, mStopRequestCalled)); + // Since OnSessionReady on the listener is called on the main thread, + // OnSessionClosed and OnSessionReady can be racy. If OnStopRequest is not + // called yet, OnSessionClosed needs to wait. + if (!mStopRequestCalled) { + nsCString closeReason(aReason); + mPendingEvents.AppendElement([self = RefPtr{this}, status(aStatus), + closeReason(std::move(closeReason)), + cleanly(aCleanly)]() { + Unused << self->OnSessionClosed(cleanly, status, closeReason); + }); + return NS_OK; + } + + switch (mState) { + case WebTransportSessionProxyState::INIT: + case WebTransportSessionProxyState::NEGOTIATING: + case WebTransportSessionProxyState::CLOSE_CALLBACK_PENDING: + MOZ_ASSERT(false, "OnSessionClosed cannot be called in this state."); + return NS_ERROR_ABORT; + case WebTransportSessionProxyState::NEGOTIATING_SUCCEEDED: + case WebTransportSessionProxyState::ACTIVE: { + mCleanly = aCleanly; + mCloseStatus = aStatus; + mReason = aReason; + mWebTransportSession = nullptr; + ChangeState(WebTransportSessionProxyState::CLOSE_CALLBACK_PENDING); + CallOnSessionClosed(); + } break; + case WebTransportSessionProxyState::SESSION_CLOSE_PENDING: + ChangeState(WebTransportSessionProxyState::DONE); + break; + case WebTransportSessionProxyState::DONE: + // The session has been canceled. We do not need to set + // mWebTransportSession. + break; + } + return NS_OK; +} + +void WebTransportSessionProxy::CallOnSessionClosedLocked() { + MutexAutoLock lock(mMutex); + CallOnSessionClosed(); +} + +void WebTransportSessionProxy::CallOnSessionClosed() { + mMutex.AssertCurrentThreadOwns(); + + if (!mTarget->IsOnCurrentThread()) { + RefPtr self(this); + Unused << mTarget->Dispatch(NS_NewRunnableFunction( + "WebTransportSessionProxy::CallOnSessionClosed", + [self{std::move(self)}]() { self->CallOnSessionClosedLocked(); })); + return; + } + + MOZ_ASSERT(mTarget->IsOnCurrentThread()); + nsCOMPtr listener; + bool cleanly = false; + nsAutoCString reason; + uint32_t closeStatus = 0; + + switch (mState) { + case WebTransportSessionProxyState::INIT: + case WebTransportSessionProxyState::NEGOTIATING: + case WebTransportSessionProxyState::NEGOTIATING_SUCCEEDED: + case WebTransportSessionProxyState::ACTIVE: + case WebTransportSessionProxyState::SESSION_CLOSE_PENDING: + MOZ_ASSERT(false, "CallOnSessionClosed cannot be called in this state."); + break; + case WebTransportSessionProxyState::CLOSE_CALLBACK_PENDING: + listener = mListener; + mListener = nullptr; + cleanly = mCleanly; + reason = mReason; + closeStatus = mCloseStatus; + ChangeState(WebTransportSessionProxyState::DONE); + break; + case WebTransportSessionProxyState::DONE: + break; + } + + if (listener) { + // Don't invoke the callback under the lock. + MutexAutoUnlock unlock(mMutex); + listener->OnSessionClosed(cleanly, closeStatus, reason); + } +} + +bool WebTransportSessionProxy::CheckServerCertificateIfNeeded() { + if (mServerCertHashes.IsEmpty()) { + return true; + } + + nsCOMPtr httpChannel = do_QueryInterface(mChannel); + MOZ_ASSERT(httpChannel, "Not a http channel ?"); + nsCOMPtr tsi; + httpChannel->GetSecurityInfo(getter_AddRefs(tsi)); + MOZ_ASSERT(tsi, + "We shouln't reach this code before setting the security info."); + nsCOMPtr cert; + nsresult rv = tsi->GetServerCert(getter_AddRefs(cert)); + if (!cert || NS_WARN_IF(NS_FAILED(rv))) return true; + nsTArray certDER; + if (NS_FAILED(cert->GetRawDER(certDER))) { + return false; + } + // https://w3c.github.io/webtransport/#compute-a-certificate-hash + nsTArray certHash; + if (NS_FAILED(Digest::DigestBuf(SEC_OID_SHA256, certDER.Elements(), + certDER.Length(), certHash)) || + certHash.Length() != SHA256_LENGTH) { + return false; + } + auto verifyCertDer = [&certHash](const auto& hash) { + return certHash.Length() == hash.Length() && + memcmp(certHash.Elements(), hash.Elements(), certHash.Length()) == 0; + }; + + // https://w3c.github.io/webtransport/#verify-a-certificate-hash + for (const auto& hash : mServerCertHashes) { + nsCString algorithm; + if (NS_FAILED(hash->GetAlgorithm(algorithm)) || algorithm != "sha-256") { + continue; + LOG(("Unexpected non-SHA-256 hash")); + } + + nsTArray value; + if (NS_FAILED(hash->GetValue(value))) { + continue; + LOG(("Unexpected corrupted hash")); + } + + if (verifyCertDer(value)) { + return true; + } + } + return false; +} + +void WebTransportSessionProxy::ChangeState( + WebTransportSessionProxyState newState) { + mMutex.AssertCurrentThreadOwns(); + LOG(("WebTransportSessionProxy::ChangeState %d -> %d [this=%p]", mState, + newState, this)); + switch (newState) { + case WebTransportSessionProxyState::INIT: + MOZ_ASSERT(false, "Cannot change into INIT sate."); + break; + case WebTransportSessionProxyState::NEGOTIATING: + MOZ_ASSERT(mState == WebTransportSessionProxyState::INIT, + "Only from INIT can be change into NEGOTIATING"); + MOZ_ASSERT(mChannel); + MOZ_ASSERT(mListener); + break; + case WebTransportSessionProxyState::NEGOTIATING_SUCCEEDED: + MOZ_ASSERT( + mState == WebTransportSessionProxyState::NEGOTIATING, + "Only from NEGOTIATING can be change into NEGOTIATING_SUCCEEDED"); + MOZ_ASSERT(mChannel); + MOZ_ASSERT(mWebTransportSession); + MOZ_ASSERT(mListener); + break; + case WebTransportSessionProxyState::ACTIVE: + MOZ_ASSERT(mState == WebTransportSessionProxyState::NEGOTIATING_SUCCEEDED, + "Only from NEGOTIATING_SUCCEEDED can be change into ACTIVE"); + MOZ_ASSERT(!mChannel); + MOZ_ASSERT(mWebTransportSession); + MOZ_ASSERT(mListener); + break; + case WebTransportSessionProxyState::SESSION_CLOSE_PENDING: + MOZ_ASSERT( + (mState == WebTransportSessionProxyState::NEGOTIATING_SUCCEEDED) || + (mState == WebTransportSessionProxyState::ACTIVE), + "Only from NEGOTIATING_SUCCEEDED and ACTIVE can be change into" + " SESSION_CLOSE_PENDING"); + MOZ_ASSERT(!mChannel); + MOZ_ASSERT(mWebTransportSession); + MOZ_ASSERT(!mListener); + break; + case WebTransportSessionProxyState::CLOSE_CALLBACK_PENDING: + MOZ_ASSERT( + (mState == WebTransportSessionProxyState::NEGOTIATING_SUCCEEDED) || + (mState == WebTransportSessionProxyState::ACTIVE), + "Only from NEGOTIATING_SUCCEEDED and ACTIVE can be change into" + " CLOSE_CALLBACK_PENDING"); + MOZ_ASSERT(!mWebTransportSession); + MOZ_ASSERT(mListener); + break; + case WebTransportSessionProxyState::DONE: + MOZ_ASSERT( + (mState == WebTransportSessionProxyState::NEGOTIATING) || + (mState == + WebTransportSessionProxyState::SESSION_CLOSE_PENDING) || + (mState == WebTransportSessionProxyState::CLOSE_CALLBACK_PENDING), + "Only from NEGOTIATING, SESSION_CLOSE_PENDING and " + "CLOSE_CALLBACK_PENDING can be change into DONE"); + MOZ_ASSERT(!mChannel); + MOZ_ASSERT(!mWebTransportSession); + MOZ_ASSERT(!mListener); + break; + } + mState = newState; +} + +void WebTransportSessionProxy::NotifyDatagramReceived( + nsTArray&& aData) { + nsCOMPtr listener; + { + MutexAutoLock lock(mMutex); + MOZ_ASSERT(mTarget->IsOnCurrentThread()); + + if (!mStopRequestCalled) { + CopyableTArray copied(aData); + mPendingEvents.AppendElement( + [self = RefPtr{this}, data = std::move(copied)]() mutable { + self->NotifyDatagramReceived(std::move(data)); + }); + return; + } + + if (mState != WebTransportSessionProxyState::ACTIVE || !mListener) { + return; + } + listener = mListener; + } + + listener->OnDatagramReceived(aData); +} + +NS_IMETHODIMP WebTransportSessionProxy::OnDatagramReceivedInternal( + nsTArray&& aData) { + MOZ_ASSERT(OnSocketThread()); + + { + MutexAutoLock lock(mMutex); + if (!mTarget->IsOnCurrentThread()) { + return mTarget->Dispatch(NS_NewRunnableFunction( + "WebTransportSessionProxy::OnDatagramReceived", + [self = RefPtr{this}, data{std::move(aData)}]() mutable { + self->NotifyDatagramReceived(std::move(data)); + })); + } + } + + NotifyDatagramReceived(std::move(aData)); + return NS_OK; +} + +NS_IMETHODIMP WebTransportSessionProxy::OnDatagramReceived( + const nsTArray& aData) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +void WebTransportSessionProxy::OnMaxDatagramSizeInternal(uint64_t aSize) { + nsCOMPtr listener; + { + MutexAutoLock lock(mMutex); + MOZ_ASSERT(mTarget->IsOnCurrentThread()); + + if (!mStopRequestCalled) { + mPendingEvents.AppendElement([self = RefPtr{this}, size(aSize)]() { + self->OnMaxDatagramSizeInternal(size); + }); + return; + } + + if (mState != WebTransportSessionProxyState::ACTIVE || !mListener) { + return; + } + listener = mListener; + } + + listener->OnMaxDatagramSize(aSize); +} + +NS_IMETHODIMP WebTransportSessionProxy::OnMaxDatagramSize(uint64_t aSize) { + MOZ_ASSERT(OnSocketThread()); + + { + MutexAutoLock lock(mMutex); + if (!mTarget->IsOnCurrentThread()) { + return mTarget->Dispatch( + NS_NewRunnableFunction("WebTransportSessionProxy::OnMaxDatagramSize", + [self = RefPtr{this}, size(aSize)] { + self->OnMaxDatagramSizeInternal(size); + })); + } + } + + OnMaxDatagramSizeInternal(aSize); + return NS_OK; +} + +void WebTransportSessionProxy::OnOutgoingDatagramOutComeInternal( + uint64_t aId, WebTransportSessionEventListener::DatagramOutcome aOutCome) { + nsCOMPtr listener; + { + MutexAutoLock lock(mMutex); + MOZ_ASSERT(mTarget->IsOnCurrentThread()); + if (mState != WebTransportSessionProxyState::ACTIVE || !mListener) { + return; + } + listener = mListener; + } + + listener->OnOutgoingDatagramOutCome(aId, aOutCome); +} + +NS_IMETHODIMP +WebTransportSessionProxy::OnOutgoingDatagramOutCome( + uint64_t aId, WebTransportSessionEventListener::DatagramOutcome aOutCome) { + MOZ_ASSERT(OnSocketThread()); + + { + MutexAutoLock lock(mMutex); + if (!mTarget->IsOnCurrentThread()) { + return mTarget->Dispatch(NS_NewRunnableFunction( + "WebTransportSessionProxy::OnOutgoingDatagramOutCome", + [self = RefPtr{this}, id(aId), outcome(aOutCome)] { + self->OnOutgoingDatagramOutComeInternal(id, outcome); + })); + } + } + + OnOutgoingDatagramOutComeInternal(aId, aOutCome); + return NS_OK; +} + +NS_IMETHODIMP WebTransportSessionProxy::OnStopSending(uint64_t aStreamId, + nsresult aError) { + MOZ_ASSERT(OnSocketThread()); + nsCOMPtr listener; + { + MutexAutoLock lock(mMutex); + MOZ_ASSERT(mTarget->IsOnCurrentThread()); + + if (mState != WebTransportSessionProxyState::ACTIVE || !mListener) { + return NS_OK; + } + listener = mListener; + } + + listener->OnStopSending(aStreamId, aError); + return NS_OK; +} + +NS_IMETHODIMP WebTransportSessionProxy::OnResetReceived(uint64_t aStreamId, + nsresult aError) { + MOZ_ASSERT(OnSocketThread()); + nsCOMPtr listener; + { + MutexAutoLock lock(mMutex); + MOZ_ASSERT(mTarget->IsOnCurrentThread()); + + if (mState != WebTransportSessionProxyState::ACTIVE || !mListener) { + return NS_OK; + } + listener = mListener; + } + + listener->OnResetReceived(aStreamId, aError); + return NS_OK; +} + +} // namespace mozilla::net diff --git a/netwerk/protocol/webtransport/WebTransportSessionProxy.h b/netwerk/protocol/webtransport/WebTransportSessionProxy.h new file mode 100644 index 0000000000..aebb446e21 --- /dev/null +++ b/netwerk/protocol/webtransport/WebTransportSessionProxy.h @@ -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/. */ + +#ifndef mozilla_net_WebTransportProxy_h +#define mozilla_net_WebTransportProxy_h + +#include +#include "nsIChannelEventSink.h" +#include "nsIInterfaceRequestor.h" +#include "nsIRedirectResultListener.h" +#include "nsIStreamListener.h" +#include "nsIWebTransport.h" + +/* + * WebTransportSessionProxy is introduced to enable the creation of a + * Http3WebTransportSession and coordination of actions that are performed on + * the main thread and on the socket thread. + * + * mChannel, mRedirectChannel, and mListener are used only on the main thread. + * + * mWebTransportSession is used only on the socket thread. + * + * mState and mSessionId are used on both threads, socket and main thread and it + * is only used with a lock. + * + * + * WebTransportSessionProxyState: + * - INIT: before AsyncConnect is called. + * + * - NEGOTIATING: It is set during AsyncConnect. During this state HttpChannel + * is open but OnStartRequest has not been called yet. This state can + * transfer into: + * - NEGOTIATING_SUCCEEDED: when a Http3WebTransportSession has been + * negotiated. + * - DONE: when a WebTransport session has been canceled. + * + * - NEGOTIATING_SUCCEEDED: It is set during parsing of + * Http3WebTransportSession response when the response has been successful. + * mWebTransport is set to the Http3WebTransportSession at the same time the + * session changes to this state. This state can transfer into: + * - ACTIVE: during the OnStopRequest call if the WebTransport has not been + * canceled or failed for other reason, e.g. a browser shutdown or content + * blocking policies. + * - SESSION_CLOSE_PENDING: if the WebTransport has been canceled via an API + * call or content blocking policies. (the main thread initiated close). + * - CLOSE_CALLBACK_PENDING: if Http3WebTransportSession has been canceled + * due to a shutdown or a server closing a session. (the socket thread + * initiated close). + * + * - ACTIVE: In this state the session is negotiated and ready to use. This + * state can transfer into: + * - SESSION_CLOSE_PENDING: if the WebTransport has been canceled via an API + * call(nsIWebTransport::closeSession) or content blocking policies. (the + * main thread initiated close). + * - CLOSE_CALLBACK_PENDING: if Http3WebTransportSession has been canceled + * due to a shutdown or a server closing a session. (the socket thread + * initiated close). + * + * - CLOSE_CALLBACK_PENDING: This is the socket thread initiated close. In this + * state, the Http3WebTransportSession has been closed and a + * CallOnSessionClosed call is dispatched to the main thread to call the + * appropriate listener. + * + * - SESSION_CLOSE_PENDING: This is the main thread initiated close. In this + * state, the WebTransport has been closed via an API call + * (nsIWebTransport::closeSession) and a CloseSessionInternal call is + * dispatched to the socket thread to close the appropriate + * Http3WebTransportSession. + * + * - DONE: everything has been cleaned up on both threads. + * + * + * AsyncConnect creates mChannel on the main thread. Redirect callbacks are also + * performed on the main thread (mRedirectChannel set and access only on the + * main thread). Before this point, there are no activities on the socket thread + * and Http3WebTransportSession is nullptr. mChannel is going to create a + * nsHttpTransaction. The transaction will be dispatched on a nsAHttpConnection, + * i.e. currently only the HTTP/3 version is implemented, therefore this will be + * a HttpConnectionUDP and a Http3Session. The Http3Session creates a + * Http3WebTransportSession. Until a response is received + * Http3WebTransportSession is only accessed by Http3Session. During parsing of + * a successful received from a server on the socket thread, + * WebTransportSessionProxy::mWebTransportSession will take a reference to + * Http3WebTransportSession and mState will be set to NEGOTIATING_SUCCEEDED. + * From now on WebTransportSessionProxy is responsible for closing + * Http3WebTransportSession if the closing of the session is initiated on the + * main thread. OnStartRequest and OnStopRequest will be called on the main + * thread. The session negotiation can have 2 outcomes: + * - If both calls, i.e. OnStartRequest an OnStopRequest, indicate that the + * request has succeeded and mState is NEGOTIATING_SUCCEEDED, the + * mListener->OnSessionReady will be called during OnStopRequest. + * - Otherwise, mListener->OnSessionClosed will be called, the state transferred + * into SESSION_CLOSE_PENDING, and CloseSessionInternal will be dispatched to + * the socket thread. + * + * CloseSession is called on the main thread. If the session is already closed + * it returns an error. If the session is in state NEGOTIATING or + * NEGOTIATING_SUCCEEDED mChannel will be canceled. If the session is in state + * NEGOTIATING_SUCCEEDED or ACTIVE the state transferred into + * SESSION_CLOSE_PENDING, and CloseSessionInternal will be dispatched to the + * socket thread + * + * OnSessionReadyInternal is called on the socket thread. If mState is + * NEGOTIATING the state will be set to NEGOTIATING_SUCCEEDED and mWebTransport + * will be set to the newly negotiated Http3WebTransportSession. If mState is + * DONE, the Http3WebTransportSession will be close. + * + * OnSessionClosed is called on the socket thread. mState will be set to + * CLOSE_CALLBACK_PENDING and CallOnSessionClosed will be dispatched to the main + * thread. + * + * mWebTransport is set during states NEGOTIATING_SUCCEEDED, ACTIVE and + * SESSION_CLOSE_PENDING. + */ + +namespace mozilla::net { + +class WebTransportStreamCallbackWrapper; + +class WebTransportSessionProxy final : public nsIWebTransport, + public WebTransportSessionEventListener, + public nsIStreamListener, + public nsIChannelEventSink, + public nsIRedirectResultListener, + public nsIInterfaceRequestor { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIWEBTRANSPORT + NS_DECL_WEBTRANSPORTSESSIONEVENTLISTENER + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMLISTENER + NS_DECL_NSICHANNELEVENTSINK + NS_DECL_NSIREDIRECTRESULTLISTENER + NS_DECL_NSIINTERFACEREQUESTOR + + WebTransportSessionProxy(); + + private: + ~WebTransportSessionProxy(); + + void CloseSessionInternal(); + void CloseSessionInternalLocked(); + void CallOnSessionClosed(); + void CallOnSessionClosedLocked(); + + enum WebTransportSessionProxyState { + INIT, + NEGOTIATING, + NEGOTIATING_SUCCEEDED, + ACTIVE, + CLOSE_CALLBACK_PENDING, + SESSION_CLOSE_PENDING, + DONE, + }; + mozilla::Mutex mMutex; + WebTransportSessionProxyState mState MOZ_GUARDED_BY(mMutex) = + WebTransportSessionProxyState::INIT; + void ChangeState(WebTransportSessionProxyState newState); + void CreateStreamInternal(nsIWebTransportStreamCallback* callback, + bool aBidi); + void DoCreateStream(WebTransportStreamCallbackWrapper* aCallback, + Http3WebTransportSession* aSession, bool aBidi); + void SendDatagramInternal(const RefPtr& aSession, + nsTArray&& aData, uint64_t aTrackingId); + void NotifyDatagramReceived(nsTArray&& aData); + void GetMaxDatagramSizeInternal( + const RefPtr& aSession); + void OnMaxDatagramSizeInternal(uint64_t aSize); + void OnOutgoingDatagramOutComeInternal( + uint64_t aId, WebTransportSessionEventListener::DatagramOutcome aOutCome); + bool CheckServerCertificateIfNeeded(); + + nsCOMPtr mChannel; + nsCOMPtr mRedirectChannel; + nsTArray> mServerCertHashes; + nsCOMPtr mListener MOZ_GUARDED_BY(mMutex); + RefPtr mWebTransportSession MOZ_GUARDED_BY(mMutex); + uint64_t mSessionId MOZ_GUARDED_BY(mMutex) = UINT64_MAX; + uint32_t mCloseStatus MOZ_GUARDED_BY(mMutex) = 0; + nsCString mReason MOZ_GUARDED_BY(mMutex); + bool mCleanly MOZ_GUARDED_BY(mMutex) = false; + bool mStopRequestCalled MOZ_GUARDED_BY(mMutex) = false; + // This is used to store events happened before OnSessionReady. + // Note that these events will be dispatched to the socket thread. + nsTArray> mPendingEvents MOZ_GUARDED_BY(mMutex); + nsTArray> mPendingCreateStreamEvents + MOZ_GUARDED_BY(mMutex); + nsCOMPtr mTarget MOZ_GUARDED_BY(mMutex); +}; + +} // namespace mozilla::net + +#endif // mozilla_net_WebTransportProxy_h diff --git a/netwerk/protocol/webtransport/WebTransportStreamProxy.cpp b/netwerk/protocol/webtransport/WebTransportStreamProxy.cpp new file mode 100644 index 0000000000..6192bda0f7 --- /dev/null +++ b/netwerk/protocol/webtransport/WebTransportStreamProxy.cpp @@ -0,0 +1,386 @@ +/* -*- 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 "WebTransportStreamProxy.h" + +#include "WebTransportLog.h" +#include "Http3WebTransportStream.h" +#include "nsProxyRelease.h" +#include "nsSocketTransportService2.h" + +namespace mozilla::net { + +NS_IMPL_ADDREF(WebTransportStreamProxy) +NS_IMPL_RELEASE(WebTransportStreamProxy) + +NS_INTERFACE_MAP_BEGIN(WebTransportStreamProxy) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIWebTransportReceiveStream) + NS_INTERFACE_MAP_ENTRY(nsIWebTransportReceiveStream) + NS_INTERFACE_MAP_ENTRY(nsIWebTransportSendStream) + NS_INTERFACE_MAP_ENTRY(nsIWebTransportBidirectionalStream) +NS_INTERFACE_MAP_END + +WebTransportStreamProxy::WebTransportStreamProxy( + Http3WebTransportStream* aStream) + : mWebTransportStream(aStream) { + nsCOMPtr inputStream; + nsCOMPtr outputStream; + mWebTransportStream->GetWriterAndReader(getter_AddRefs(outputStream), + getter_AddRefs(inputStream)); + if (outputStream) { + mWriter = new AsyncOutputStreamWrapper(outputStream); + } + if (inputStream) { + mReader = new AsyncInputStreamWrapper(inputStream, mWebTransportStream); + } +} + +WebTransportStreamProxy::~WebTransportStreamProxy() { + // mWebTransportStream needs to be destroyed on the socket thread. + NS_ProxyRelease("WebTransportStreamProxy::~WebTransportStreamProxy", + gSocketTransportService, mWebTransportStream.forget()); +} + +NS_IMETHODIMP WebTransportStreamProxy::SendStopSending(uint8_t aError) { + if (!OnSocketThread()) { + RefPtr self(this); + return gSocketTransportService->Dispatch( + NS_NewRunnableFunction("WebTransportStreamProxy::SendStopSending", + [self{std::move(self)}, error(aError)]() { + self->SendStopSending(error); + })); + } + + mWebTransportStream->SendStopSending(aError); + return NS_OK; +} + +NS_IMETHODIMP WebTransportStreamProxy::SendFin(void) { + if (!mWriter) { + return NS_ERROR_UNEXPECTED; + } + + mWriter->Close(); + + if (!OnSocketThread()) { + RefPtr self(this); + return gSocketTransportService->Dispatch(NS_NewRunnableFunction( + "WebTransportStreamProxy::SendFin", + [self{std::move(self)}]() { self->mWebTransportStream->SendFin(); })); + } + + mWebTransportStream->SendFin(); + return NS_OK; +} + +NS_IMETHODIMP WebTransportStreamProxy::Reset(uint8_t aErrorCode) { + if (!mWriter) { + return NS_ERROR_UNEXPECTED; + } + + mWriter->Close(); + + if (!OnSocketThread()) { + RefPtr self(this); + return gSocketTransportService->Dispatch( + NS_NewRunnableFunction("WebTransportStreamProxy::Reset", + [self{std::move(self)}, error(aErrorCode)]() { + self->mWebTransportStream->Reset(error); + })); + } + + mWebTransportStream->Reset(aErrorCode); + return NS_OK; +} + +namespace { + +class StatsCallbackWrapper : public nsIWebTransportStreamStatsCallback { + public: + NS_DECL_THREADSAFE_ISUPPORTS + + explicit StatsCallbackWrapper(nsIWebTransportStreamStatsCallback* aCallback) + : mCallback(aCallback), mTarget(GetCurrentSerialEventTarget()) {} + + NS_IMETHOD OnSendStatsAvailable( + nsIWebTransportSendStreamStats* aStats) override { + if (!mTarget->IsOnCurrentThread()) { + RefPtr self(this); + nsCOMPtr stats = aStats; + Unused << mTarget->Dispatch(NS_NewRunnableFunction( + "StatsCallbackWrapper::OnSendStatsAvailable", + [self{std::move(self)}, stats{std::move(stats)}]() { + self->OnSendStatsAvailable(stats); + })); + return NS_OK; + } + + mCallback->OnSendStatsAvailable(aStats); + return NS_OK; + } + + NS_IMETHOD OnReceiveStatsAvailable( + nsIWebTransportReceiveStreamStats* aStats) override { + if (!mTarget->IsOnCurrentThread()) { + RefPtr self(this); + nsCOMPtr stats = aStats; + Unused << mTarget->Dispatch(NS_NewRunnableFunction( + "StatsCallbackWrapper::OnReceiveStatsAvailable", + [self{std::move(self)}, stats{std::move(stats)}]() { + self->OnReceiveStatsAvailable(stats); + })); + return NS_OK; + } + + mCallback->OnReceiveStatsAvailable(aStats); + return NS_OK; + } + + private: + virtual ~StatsCallbackWrapper() { + NS_ProxyRelease("StatsCallbackWrapper::~StatsCallbackWrapper", mTarget, + mCallback.forget()); + } + + nsCOMPtr mCallback; + nsCOMPtr mTarget; +}; + +NS_IMPL_ISUPPORTS(StatsCallbackWrapper, nsIWebTransportStreamStatsCallback) + +} // namespace + +NS_IMETHODIMP WebTransportStreamProxy::GetSendStreamStats( + nsIWebTransportStreamStatsCallback* aCallback) { + if (!OnSocketThread()) { + RefPtr self(this); + nsCOMPtr callback = + new StatsCallbackWrapper(aCallback); + return gSocketTransportService->Dispatch(NS_NewRunnableFunction( + "WebTransportStreamProxy::GetSendStreamStats", + [self{std::move(self)}, callback{std::move(callback)}]() { + self->GetSendStreamStats(callback); + })); + } + + nsCOMPtr stats = + mWebTransportStream->GetSendStreamStats(); + aCallback->OnSendStatsAvailable(stats); + return NS_OK; +} + +NS_IMETHODIMP WebTransportStreamProxy::GetReceiveStreamStats( + nsIWebTransportStreamStatsCallback* aCallback) { + if (!OnSocketThread()) { + RefPtr self(this); + nsCOMPtr callback = + new StatsCallbackWrapper(aCallback); + return gSocketTransportService->Dispatch(NS_NewRunnableFunction( + "WebTransportStreamProxy::GetReceiveStreamStats", + [self{std::move(self)}, callback{std::move(callback)}]() { + self->GetReceiveStreamStats(callback); + })); + } + + nsCOMPtr stats = + mWebTransportStream->GetReceiveStreamStats(); + aCallback->OnReceiveStatsAvailable(stats); + return NS_OK; +} + +NS_IMETHODIMP WebTransportStreamProxy::GetHasReceivedFIN( + bool* aHasReceivedFIN) { + *aHasReceivedFIN = mWebTransportStream->RecvDone(); + return NS_OK; +} + +NS_IMETHODIMP WebTransportStreamProxy::GetInputStream( + nsIAsyncInputStream** aOut) { + if (!mReader) { + return NS_ERROR_NOT_AVAILABLE; + } + + RefPtr stream = mReader; + stream.forget(aOut); + return NS_OK; +} + +NS_IMETHODIMP WebTransportStreamProxy::GetOutputStream( + nsIAsyncOutputStream** aOut) { + if (!mWriter) { + return NS_ERROR_NOT_AVAILABLE; + } + + RefPtr stream = mWriter; + stream.forget(aOut); + return NS_OK; +} + +NS_IMETHODIMP WebTransportStreamProxy::GetStreamId(uint64_t* aId) { + *aId = mWebTransportStream->StreamId(); + return NS_OK; +} + +NS_IMETHODIMP WebTransportStreamProxy::SetSendOrder(Maybe aSendOrder) { + if (!OnSocketThread()) { + return gSocketTransportService->Dispatch(NS_NewRunnableFunction( + "SetSendOrder", [stream = mWebTransportStream, aSendOrder]() { + stream->SetSendOrder(aSendOrder); + })); + } + mWebTransportStream->SetSendOrder(aSendOrder); + return NS_OK; +} + +//------------------------------------------------------------------------------ +// WebTransportStreamProxy::AsyncInputStreamWrapper +//------------------------------------------------------------------------------ + +NS_IMPL_ISUPPORTS(WebTransportStreamProxy::AsyncInputStreamWrapper, + nsIInputStream, nsIAsyncInputStream) + +WebTransportStreamProxy::AsyncInputStreamWrapper::AsyncInputStreamWrapper( + nsIAsyncInputStream* aStream, Http3WebTransportStream* aWebTransportStream) + : mStream(aStream), mWebTransportStream(aWebTransportStream) {} + +WebTransportStreamProxy::AsyncInputStreamWrapper::~AsyncInputStreamWrapper() = + default; + +void WebTransportStreamProxy::AsyncInputStreamWrapper::MaybeCloseStream() { + if (!mWebTransportStream->RecvDone()) { + return; + } + + uint64_t available = 0; + Unused << Available(&available); + if (available) { + // Don't close the InputStream if there's unread data available, since it + // would be lost. We exit above unless we know no more data will be received + // for the stream. + return; + } + + LOG( + ("AsyncInputStreamWrapper::MaybeCloseStream close stream due to FIN " + "stream=%p", + mWebTransportStream.get())); + Close(); +} + +NS_IMETHODIMP WebTransportStreamProxy::AsyncInputStreamWrapper::Close() { + return mStream->CloseWithStatus(NS_BASE_STREAM_CLOSED); +} + +NS_IMETHODIMP WebTransportStreamProxy::AsyncInputStreamWrapper::Available( + uint64_t* aAvailable) { + return mStream->Available(aAvailable); +} + +NS_IMETHODIMP WebTransportStreamProxy::AsyncInputStreamWrapper::StreamStatus() { + return mStream->StreamStatus(); +} + +NS_IMETHODIMP WebTransportStreamProxy::AsyncInputStreamWrapper::Read( + char* aBuf, uint32_t aCount, uint32_t* aResult) { + LOG(("WebTransportStreamProxy::AsyncInputStreamWrapper::Read %p", this)); + nsresult rv = mStream->Read(aBuf, aCount, aResult); + MaybeCloseStream(); + return rv; +} + +NS_IMETHODIMP WebTransportStreamProxy::AsyncInputStreamWrapper::ReadSegments( + nsWriteSegmentFun aWriter, void* aClosure, uint32_t aCount, + uint32_t* aResult) { + LOG(("WebTransportStreamProxy::AsyncInputStreamWrapper::ReadSegments %p", + this)); + nsresult rv = mStream->ReadSegments(aWriter, aClosure, aCount, aResult); + if (*aResult > 0) { + LOG((" Read %u bytes", *aResult)); + } + MaybeCloseStream(); + return rv; +} + +NS_IMETHODIMP WebTransportStreamProxy::AsyncInputStreamWrapper::IsNonBlocking( + bool* aResult) { + return mStream->IsNonBlocking(aResult); +} + +NS_IMETHODIMP WebTransportStreamProxy::AsyncInputStreamWrapper::CloseWithStatus( + nsresult aStatus) { + return mStream->CloseWithStatus(aStatus); +} + +NS_IMETHODIMP WebTransportStreamProxy::AsyncInputStreamWrapper::AsyncWait( + nsIInputStreamCallback* aCallback, uint32_t aFlags, + uint32_t aRequestedCount, nsIEventTarget* aEventTarget) { + return mStream->AsyncWait(aCallback, aFlags, aRequestedCount, aEventTarget); +} + +//------------------------------------------------------------------------------ +// WebTransportStreamProxy::AsyncOutputStreamWrapper +//------------------------------------------------------------------------------ + +NS_IMPL_ISUPPORTS(WebTransportStreamProxy::AsyncOutputStreamWrapper, + nsIOutputStream, nsIAsyncOutputStream) + +WebTransportStreamProxy::AsyncOutputStreamWrapper::AsyncOutputStreamWrapper( + nsIAsyncOutputStream* aStream) + : mStream(aStream) {} + +WebTransportStreamProxy::AsyncOutputStreamWrapper::~AsyncOutputStreamWrapper() = + default; + +NS_IMETHODIMP WebTransportStreamProxy::AsyncOutputStreamWrapper::Flush() { + return mStream->Flush(); +} + +NS_IMETHODIMP +WebTransportStreamProxy::AsyncOutputStreamWrapper::StreamStatus() { + return mStream->StreamStatus(); +} + +NS_IMETHODIMP WebTransportStreamProxy::AsyncOutputStreamWrapper::Write( + const char* aBuf, uint32_t aCount, uint32_t* aResult) { + LOG( + ("WebTransportStreamProxy::AsyncOutputStreamWrapper::Write %p %u bytes, " + "first byte %c", + this, aCount, aBuf[0])); + return mStream->Write(aBuf, aCount, aResult); +} + +NS_IMETHODIMP WebTransportStreamProxy::AsyncOutputStreamWrapper::WriteFrom( + nsIInputStream* aFromStream, uint32_t aCount, uint32_t* aResult) { + return mStream->WriteFrom(aFromStream, aCount, aResult); +} + +NS_IMETHODIMP WebTransportStreamProxy::AsyncOutputStreamWrapper::WriteSegments( + nsReadSegmentFun aReader, void* aClosure, uint32_t aCount, + uint32_t* aResult) { + return mStream->WriteSegments(aReader, aClosure, aCount, aResult); +} + +NS_IMETHODIMP WebTransportStreamProxy::AsyncOutputStreamWrapper::AsyncWait( + nsIOutputStreamCallback* aCallback, uint32_t aFlags, + uint32_t aRequestedCount, nsIEventTarget* aEventTarget) { + return mStream->AsyncWait(aCallback, aFlags, aRequestedCount, aEventTarget); +} + +NS_IMETHODIMP +WebTransportStreamProxy::AsyncOutputStreamWrapper::CloseWithStatus( + nsresult aStatus) { + return mStream->CloseWithStatus(aStatus); +} + +NS_IMETHODIMP WebTransportStreamProxy::AsyncOutputStreamWrapper::Close() { + return mStream->Close(); +} + +NS_IMETHODIMP WebTransportStreamProxy::AsyncOutputStreamWrapper::IsNonBlocking( + bool* aResult) { + return mStream->IsNonBlocking(aResult); +} + +} // namespace mozilla::net diff --git a/netwerk/protocol/webtransport/WebTransportStreamProxy.h b/netwerk/protocol/webtransport/WebTransportStreamProxy.h new file mode 100644 index 0000000000..e6ccc573fa --- /dev/null +++ b/netwerk/protocol/webtransport/WebTransportStreamProxy.h @@ -0,0 +1,84 @@ +/* -*- 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 mozilla_net_WebTransportStreamProxy_h +#define mozilla_net_WebTransportStreamProxy_h + +#include "nsIAsyncInputStream.h" +#include "nsIAsyncOutputStream.h" +#include "nsIWebTransportStream.h" +#include "nsCOMPtr.h" + +namespace mozilla::net { + +class Http3WebTransportStream; + +class WebTransportStreamProxy final + : public nsIWebTransportReceiveStream, + public nsIWebTransportSendStream, + public nsIWebTransportBidirectionalStream { + public: + NS_DECL_THREADSAFE_ISUPPORTS + + explicit WebTransportStreamProxy(Http3WebTransportStream* aStream); + + NS_IMETHOD SendStopSending(uint8_t aError) override; + NS_IMETHOD SendFin() override; + NS_IMETHOD Reset(uint8_t aErrorCode) override; + NS_IMETHOD GetSendStreamStats( + nsIWebTransportStreamStatsCallback* aCallback) override; + NS_IMETHOD GetReceiveStreamStats( + nsIWebTransportStreamStatsCallback* aCallback) override; + + NS_IMETHOD GetHasReceivedFIN(bool* aHasReceivedFIN) override; + + NS_IMETHOD GetInputStream(nsIAsyncInputStream** aOut) override; + NS_IMETHOD GetOutputStream(nsIAsyncOutputStream** aOut) override; + + NS_IMETHOD GetStreamId(uint64_t* aId) override; + NS_IMETHOD SetSendOrder(Maybe aSendOrder) override; + + private: + virtual ~WebTransportStreamProxy(); + + class AsyncInputStreamWrapper : public nsIAsyncInputStream { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIINPUTSTREAM + NS_DECL_NSIASYNCINPUTSTREAM + + AsyncInputStreamWrapper(nsIAsyncInputStream* aStream, + Http3WebTransportStream* aWebTransportStream); + + private: + virtual ~AsyncInputStreamWrapper(); + void MaybeCloseStream(); + + nsCOMPtr mStream; + RefPtr mWebTransportStream; + }; + + class AsyncOutputStreamWrapper : public nsIAsyncOutputStream { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIOUTPUTSTREAM + NS_DECL_NSIASYNCOUTPUTSTREAM + + explicit AsyncOutputStreamWrapper(nsIAsyncOutputStream* aStream); + + private: + virtual ~AsyncOutputStreamWrapper(); + + nsCOMPtr mStream; + }; + + RefPtr mWebTransportStream; + RefPtr mWriter; + RefPtr mReader; +}; + +} // namespace mozilla::net + +#endif diff --git a/netwerk/protocol/webtransport/moz.build b/netwerk/protocol/webtransport/moz.build new file mode 100644 index 0000000000..2dd99978ac --- /dev/null +++ b/netwerk/protocol/webtransport/moz.build @@ -0,0 +1,36 @@ +# -*- 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: WebTransport") + +XPIDL_SOURCES += [ + "nsIWebTransport.idl", + "nsIWebTransportStream.idl", +] + +XPIDL_MODULE = "necko_webtransport" + +EXPORTS.mozilla.net += [ + "WebTransportHash.h", + "WebTransportSessionProxy.h", + "WebTransportStreamProxy.h", +] + +UNIFIED_SOURCES += [ + "WebTransportHash.cpp", + "WebTransportSessionProxy.cpp", + "WebTransportStreamProxy.cpp", +] + +FINAL_LIBRARY = "xul" + +LOCAL_INCLUDES += [ + "/netwerk/base", + "/netwerk/protocol/http", +] + +include("/ipc/chromium/chromium-config.mozbuild") diff --git a/netwerk/protocol/webtransport/nsIWebTransport.idl b/netwerk/protocol/webtransport/nsIWebTransport.idl new file mode 100644 index 0000000000..faf99e61b6 --- /dev/null +++ b/netwerk/protocol/webtransport/nsIWebTransport.idl @@ -0,0 +1,132 @@ +/* -*- 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 "nsIURI.idl" +#include "nsIPrincipal.idl" + +interface WebTransportSessionEventListener; +interface nsIWebTransportStreamCallback; +interface nsIWebTransportBidirectionalStream; +interface nsIWebTransportSendStream; +interface nsIWebTransportReceiveStream; +interface nsIWebTransportHash; + +%{C++ +namespace mozilla::dom { +class ClientInfo; +} +namespace mozilla::net { +class Http3WebTransportSession; +class Http3WebTransportStream; +} +%} + +[ptr] native Http3WebTransportSessionPtr(mozilla::net::Http3WebTransportSession); +[ptr] native Http3WebTransportStreamPtr(mozilla::net::Http3WebTransportStream); +native Datagram(nsTArray&&); +[ref] native const_MaybeClientInfoRef(const mozilla::Maybe); + +[builtinclass, scriptable, uuid(c20d6e77-8cb1-4838-a88d-fff826080aa3)] +interface nsIWebTransport : nsISupports { + cenum WebTransportError : 16 { + UNKNOWN_ERROR, + INVALID_STATE_ERROR, + }; + + // When called, perform steps in "Initialization WebTransport over HTTP". + void asyncConnect(in nsIURI aURI, + in Array aServerCertHashes, + in nsIPrincipal aLoadingPrincipal, + in unsigned long aSecurityFlags, + in WebTransportSessionEventListener aListener); + + void asyncConnectWithClient(in nsIURI aURI, + in Array aServerCertHashes, + in nsIPrincipal aLoadingPrincipal, + in unsigned long aSecurityFlags, + in WebTransportSessionEventListener aListener, + in const_MaybeClientInfoRef aClientInfo); + + // Asynchronously get stats. + void getStats(); + + // Close the session. + void closeSession(in uint32_t aErrorCode, + in ACString aReason); + + // Create and open a new WebTransport stream. + void createOutgoingBidirectionalStream(in nsIWebTransportStreamCallback aListener); + void createOutgoingUnidirectionalStream(in nsIWebTransportStreamCallback aListener); + + void sendDatagram(in Array aData, in uint64_t aTrackingId); + + void getMaxDatagramSize(); + + // This can be only called after onSessionReady(). + // After this point, we can retarget the underlying WebTransportSessionProxy + // object off main thread. + [noscript] void retargetTo(in nsIEventTarget aTarget); +}; + +// Events related to a WebTransport session. +[scriptable, uuid(0e3cb269-f318-43c8-959e-897f57894b71)] +interface WebTransportSessionEventListener : nsISupports { + // This is used to let the consumer of nsIWebTransport know that the + // underlying WebTransportSession object is ready to use. + void onSessionReady(in uint64_t aSessionId); + // This is used internally to pass the reference of WebTransportSession + // object to WebTransportSessionProxy. + void onSessionReadyInternal(in Http3WebTransportSessionPtr aSession); + void onSessionClosed(in bool aCleanly, + in uint32_t aErrorCode, + in ACString aReason); + + // When a new stream has been received. + void onIncomingBidirectionalStreamAvailable(in nsIWebTransportBidirectionalStream aStream); + void onIncomingUnidirectionalStreamAvailable(in nsIWebTransportReceiveStream aStream); + + // This is used internally to pass the reference of Http3WebTransportStream + // object to WebTransportSessionProxy. + void onIncomingStreamAvailableInternal(in Http3WebTransportStreamPtr aStream); + + void onStopSending(in uint64_t aStreamId, in nsresult aError); + void onResetReceived(in uint64_t aStreamId, in nsresult aError); + + // When a new datagram has been received. + void onDatagramReceived(in Array aData); + + // This is used internally to pass the datagram to WebTransportSessionProxy. + void onDatagramReceivedInternal(in Datagram aData); + + void onMaxDatagramSize(in uint64_t aSize); + + cenum DatagramOutcome: 32 { + UNKNOWN = 0, + DROPPED_TOO_MUCH_DATA = 1, + SENT = 2, + }; + + void onOutgoingDatagramOutCome( + in uint64_t aId, + in WebTransportSessionEventListener_DatagramOutcome aOutCome); + + // void onStatsAvailable(in WebTransportStats aStats); +}; + +// This interface is used as a callback when creating an outgoing +// unidirectional or bidirectional stream. +[scriptable, uuid(c6eeff1d-599b-40a8-9157-c7a40c3d51a2)] +interface nsIWebTransportStreamCallback : nsISupports { + void onBidirectionalStreamReady(in nsIWebTransportBidirectionalStream aStream); + void onUnidirectionalStreamReady(in nsIWebTransportSendStream aStream); + void onError(in uint8_t aError); +}; + +[scriptable, uuid(2523a26e-94be-4de6-8c27-9b4ffff742f0)] +interface nsIWebTransportHash : nsISupports { + readonly attribute ACString algorithm; + readonly attribute Array value; +}; diff --git a/netwerk/protocol/webtransport/nsIWebTransportStream.idl b/netwerk/protocol/webtransport/nsIWebTransportStream.idl new file mode 100644 index 0000000000..9283f4aa30 --- /dev/null +++ b/netwerk/protocol/webtransport/nsIWebTransportStream.idl @@ -0,0 +1,89 @@ +/* -*- 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 nsIAsyncInputStream; +interface nsIAsyncOutputStream; +interface nsIInputStreamCallback; +interface nsIEventTarget; + +%{C++ +#include "mozilla/Maybe.h" + +namespace mozilla { +class TimeStamp; +} +%} + +native TimeStamp(mozilla::TimeStamp); +native MaybeInt64(mozilla::Maybe); + +[builtinclass, scriptable, uuid(ccc3e685-8411-48f0-8b3e-ff6d1fae4809)] +interface nsIWebTransportSendStreamStats : nsISupports { + [noscript] readonly attribute TimeStamp timestamp; + readonly attribute unsigned long long bytesSent; + readonly attribute unsigned long long bytesAcknowledged; +}; + +[builtinclass, scriptable, uuid(43ce1145-30ef-41a7-b97d-fa797f7f7d18)] +interface nsIWebTransportReceiveStreamStats : nsISupports { + [noscript] readonly attribute TimeStamp timestamp; + readonly attribute unsigned long long bytesReceived; +}; + +[scriptable, uuid(9c1df3f5-bf04-46b6-9977-eb6389076db8)] +interface nsIWebTransportStreamStatsCallback : nsISupports { + void onSendStatsAvailable(in nsIWebTransportSendStreamStats aStats); + void onReceiveStatsAvailable(in nsIWebTransportReceiveStreamStats aStats); +}; + +[builtinclass, scriptable, uuid(d461b235-6291-4817-adcc-a2a3b3dfc10b)] +interface nsIWebTransportReceiveStream : nsISupports { + // Sends the STOP_SENDING on the stream. + void sendStopSending(in uint8_t aError); + + void getReceiveStreamStats( + in nsIWebTransportStreamStatsCallback aCallback); + + // When true, this indicates that FIN had been received. + readonly attribute boolean hasReceivedFIN; + readonly attribute nsIAsyncInputStream inputStream; + readonly attribute uint64_t streamId; +}; + +[builtinclass, scriptable, uuid(804f245c-52ea-403c-8a78-f751533bdd70)] +interface nsIWebTransportSendStream : nsISupports { + // Sends the FIN on the stream. + void sendFin(); + + // Reset the stream with the specified error code. + void reset(in uint8_t aErrorCode); + + void getSendStreamStats(in nsIWebTransportStreamStatsCallback aCallback); + readonly attribute nsIAsyncOutputStream outputStream; + readonly attribute uint64_t streamId; + [noscript] void setSendOrder(in MaybeInt64 aSendOrder); +}; + +[builtinclass, scriptable, uuid(f9ecb509-36db-4689-97d6-137639a08750)] +interface nsIWebTransportBidirectionalStream : nsISupports { + // Sends the STOP_SENDING on the stream. + void sendStopSending(in uint8_t aError); + + // Sends the FIN on the stream. + void sendFin(); + + // Reset the stream with the specified error code. + void reset(in uint8_t aErrorCode); + + // When true, this indicates that FIN had been received. + readonly attribute boolean hasReceivedFIN; + + readonly attribute nsIAsyncInputStream inputStream; + readonly attribute nsIAsyncOutputStream outputStream; + readonly attribute uint64_t streamId; + [noscript] void setSendOrder(in MaybeInt64 aSendOrder); +}; -- cgit v1.2.3