Source code

Revision control

Other Tools

/* -*- 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 <algorithm>
#include <utility>
#include "HttpLog.h"
#include "NSSErrorsService.h"
#include "TCPFastOpenLayer.h"
#include "TunnelUtils.h"
#include "base/basictypes.h"
#include "mozilla/Tokenizer.h"
#include "nsCRT.h"
#include "nsComponentManagerUtils.h" // do_CreateInstance
#include "nsHttpBasicAuth.h"
#include "nsHttpChunkedDecoder.h"
#include "nsHttpDigestAuth.h"
#include "nsHttpHandler.h"
#include "nsHttpNTLMAuth.h"
#include "nsHttpNegotiateAuth.h"
#include "nsHttpRequestHead.h"
#include "nsHttpResponseHead.h"
#include "nsICancelable.h"
#include "nsIClassOfService.h"
#include "nsIDNSByTypeRecord.h"
#include "nsIDNSRecord.h"
#include "nsIDNSService.h"
#include "nsIEventTarget.h"
#include "nsIHttpActivityObserver.h"
#include "nsIHttpAuthenticator.h"
#include "nsIInputStream.h"
#include "nsIInputStreamPriority.h"
#include "nsIMultiplexInputStream.h"
#include "nsIOService.h"
#include "nsIPipe.h"
#include "nsIRequestContext.h"
#include "nsISeekableStream.h"
#include "nsISSLSocketControl.h"
#include "nsIThrottledInputChannel.h"
#include "nsITransport.h"
#include "nsMultiplexInputStream.h"
#include "nsNetCID.h"
#include "nsNetUtil.h"
#include "nsQueryObject.h"
#include "nsSocketTransportService2.h"
#include "nsStringStream.h"
#include "nsTransportUtils.h"
#include "sslerr.h"
//-----------------------------------------------------------------------------
static NS_DEFINE_CID(kMultiplexInputStream, NS_MULTIPLEXINPUTSTREAM_CID);
// Place a limit on how much non-compliant HTTP can be skipped while
// looking for a response header
#define MAX_INVALID_RESPONSE_BODY_SIZE (1024 * 128)
using namespace mozilla::net;
namespace mozilla {
namespace net {
//-----------------------------------------------------------------------------
// nsHttpTransaction <public>
//-----------------------------------------------------------------------------
nsHttpTransaction::nsHttpTransaction()
: mLock("transaction lock"),
mChannelId(0),
mRequestSize(0),
mRequestHead(nullptr),
mResponseHead(nullptr),
mReader(nullptr),
mWriter(nullptr),
mContentLength(-1),
mContentRead(0),
mTransferSize(0),
mInvalidResponseBytesRead(0),
mPushedStream(nullptr),
mInitialRwin(0),
mChunkedDecoder(nullptr),
mStatus(NS_OK),
mPriority(0),
mRestartCount(0),
mCaps(0),
mHttpVersion(HttpVersion::UNKNOWN),
mHttpResponseCode(0),
mCurrentHttpResponseHeaderSize(0),
mThrottlingReadAllowance(THROTTLE_NO_LIMIT),
mCapsToClear(0),
mResponseIsComplete(false),
mReadingStopped(false),
mClosed(false),
mConnected(false),
mActivated(false),
mHaveStatusLine(false),
mHaveAllHeaders(false),
mTransactionDone(false),
mDidContentStart(false),
mNoContent(false),
mSentData(false),
mReceivedData(false),
mStatusEventPending(false),
mHasRequestBody(false),
mProxyConnectFailed(false),
mHttpResponseMatched(false),
mPreserveStream(false),
mDispatchedAsBlocking(false),
mResponseTimeoutEnabled(true),
mForceRestart(false),
mReuseOnRestart(false),
mContentDecoding(false),
mContentDecodingCheck(false),
mDeferredSendProgress(false),
mWaitingOnPipeOut(false),
mDoNotRemoveAltSvc(false),
mReportedStart(false),
mReportedResponseHeader(false),
mResponseHeadTaken(false),
mForTakeResponseTrailers(nullptr),
mResponseTrailersTaken(false),
mRestarted(false),
mTopLevelOuterContentWindowId(0),
mSubmittedRatePacing(false),
mPassedRatePacing(false),
mSynchronousRatePaceRequest(false),
mClassOfService(0),
mResolvedByTRR(false),
m0RTTInProgress(false),
mDoNotTryEarlyData(false),
mEarlyDataDisposition(EARLY_NONE),
mFastOpenStatus(TFO_NOT_TRIED),
mTrafficCategory(HttpTrafficCategory::eInvalid),
mProxyConnectResponseCode(0),
m421Received(false) {
this->mSelfAddr.inet = {};
this->mPeerAddr.inet = {};
LOG(("Creating nsHttpTransaction @%p\n", this));
#ifdef MOZ_VALGRIND
memset(&mSelfAddr, 0, sizeof(NetAddr));
memset(&mPeerAddr, 0, sizeof(NetAddr));
#endif
mSelfAddr.raw.family = PR_AF_UNSPEC;
mPeerAddr.raw.family = PR_AF_UNSPEC;
mThroughCaptivePortal = gHttpHandler->GetThroughCaptivePortal();
}
void nsHttpTransaction::ResumeReading() {
MOZ_ASSERT(OnSocketThread(), "not on socket thread");
if (!mReadingStopped) {
return;
}
LOG(("nsHttpTransaction::ResumeReading %p", this));
mReadingStopped = false;
// This with either reengage the limit when still throttled in WriteSegments
// or simply reset to allow unlimeted reading again.
mThrottlingReadAllowance = THROTTLE_NO_LIMIT;
if (mConnection) {
mConnection->TransactionHasDataToRecv(this);
nsresult rv = mConnection->ResumeRecv();
if (NS_FAILED(rv)) {
LOG((" resume failed with rv=%" PRIx32, static_cast<uint32_t>(rv)));
}
}
}
bool nsHttpTransaction::EligibleForThrottling() const {
return (mClassOfService &
(nsIClassOfService::Throttleable | nsIClassOfService::DontThrottle |
nsIClassOfService::Leader | nsIClassOfService::Unblocked)) ==
nsIClassOfService::Throttleable;
}
void nsHttpTransaction::SetClassOfService(uint32_t cos) {
bool wasThrottling = EligibleForThrottling();
mClassOfService = cos;
bool isThrottling = EligibleForThrottling();
if (mConnection && wasThrottling != isThrottling) {
// Do nothing until we are actually activated. For now
// only remember the throttle flag. Call to UpdateActiveTransaction
// would add this transaction to the list too early.
gHttpHandler->ConnMgr()->UpdateActiveTransaction(this);
if (mReadingStopped && !isThrottling) {
ResumeReading();
}
}
}
class ReleaseH2WSTrans final : public Runnable {
public:
explicit ReleaseH2WSTrans(RefPtr<SpdyConnectTransaction>&& trans)
: Runnable("ReleaseH2WSTrans"), mTrans(std::move(trans)) {}
NS_IMETHOD Run() override {
mTrans = nullptr;
return NS_OK;
}
void Dispatch() {
nsCOMPtr<nsIEventTarget> sts =
do_GetService("@mozilla.org/network/socket-transport-service;1");
Unused << sts->Dispatch(this, nsIEventTarget::DISPATCH_NORMAL);
}
private:
RefPtr<SpdyConnectTransaction> mTrans;
};
nsHttpTransaction::~nsHttpTransaction() {
LOG(("Destroying nsHttpTransaction @%p\n", this));
if (mPushedStream) {
mPushedStream->OnPushFailed();
mPushedStream = nullptr;
}
if (mTokenBucketCancel) {
mTokenBucketCancel->Cancel(NS_ERROR_ABORT);
mTokenBucketCancel = nullptr;
}
// Force the callbacks and connection to be released right now
mCallbacks = nullptr;
mConnection = nullptr;
delete mResponseHead;
delete mChunkedDecoder;
ReleaseBlockingTransaction();
if (mH2WSTransaction) {
RefPtr<ReleaseH2WSTrans> r =
new ReleaseH2WSTrans(std::move(mH2WSTransaction));
r->Dispatch();
}
}
nsresult nsHttpTransaction::Init(
uint32_t caps, nsHttpConnectionInfo* cinfo, nsHttpRequestHead* requestHead,
nsIInputStream* requestBody, uint64_t requestContentLength,
bool requestBodyHasHeaders, nsIEventTarget* target,
nsIInterfaceRequestor* callbacks, nsITransportEventSink* eventsink,
uint64_t topLevelOuterContentWindowId, HttpTrafficCategory trafficCategory,
nsIRequestContext* requestContext, uint32_t classOfService,
uint32_t initialRwin, bool responseTimeoutEnabled, uint64_t channelId,
TransactionObserverFunc&& transactionObserver,
OnPushCallback&& aOnPushCallback,
HttpTransactionShell* transWithPushedStream, uint32_t aPushedStreamId) {
nsresult rv;
LOG1(("nsHttpTransaction::Init [this=%p caps=%x]\n", this, caps));
MOZ_ASSERT(cinfo);
MOZ_ASSERT(requestHead);
MOZ_ASSERT(target);
MOZ_ASSERT(target->IsOnCurrentThread());
mChannelId = channelId;
mTransactionObserver = std::move(transactionObserver);
mOnPushCallback = std::move(aOnPushCallback);
mTopLevelOuterContentWindowId = topLevelOuterContentWindowId;
LOG((" window-id = %" PRIx64, mTopLevelOuterContentWindowId));
mTrafficCategory = trafficCategory;
mActivityDistributor = services::GetHttpActivityDistributor();
if (!mActivityDistributor) {
return NS_ERROR_NOT_AVAILABLE;
}
bool activityDistributorActive;
rv = mActivityDistributor->GetIsActive(&activityDistributorActive);
if (NS_SUCCEEDED(rv) && activityDistributorActive) {
// there are some observers registered at activity distributor, gather
// nsISupports for the channel that called Init()
LOG(
("nsHttpTransaction::Init() "
"mActivityDistributor is active "
"this=%p",
this));
} else {
// there is no observer, so don't use it
activityDistributorActive = false;
mActivityDistributor = nullptr;
}
LOG1(("nsHttpTransaction %p SetRequestContext %p\n", this, requestContext));
mRequestContext = requestContext;
SetClassOfService(classOfService);
mResponseTimeoutEnabled = responseTimeoutEnabled;
mInitialRwin = initialRwin;
// create transport event sink proxy. it coalesces consecutive
// events of the same status type.
rv = net_NewTransportEventSinkProxy(getter_AddRefs(mTransportSink), eventsink,
target);
if (NS_FAILED(rv)) return rv;
mConnInfo = cinfo;
mCallbacks = callbacks;
mConsumerTarget = target;
mCaps = caps;
if (requestHead->IsHead()) {
mNoContent = true;
}
// grab a weak reference to the request head
mRequestHead = requestHead;
mReqHeaderBuf = nsHttp::ConvertRequestHeadToString(
*requestHead, !!requestBody, requestBodyHasHeaders,
cinfo->UsingConnect());
if (LOG1_ENABLED()) {
LOG1(("http request [\n"));
LogHeaders(mReqHeaderBuf.get());
LOG1(("]\n"));
}
// report the request header
if (mActivityDistributor) {
RefPtr<nsHttpTransaction> self = this;
nsCString requestBuf(mReqHeaderBuf);
NS_DispatchToMainThread(
NS_NewRunnableFunction("ObserveActivityWithArgs", [self, requestBuf]() {
nsresult rv = self->mActivityDistributor->ObserveActivityWithArgs(
HttpActivityArgs(self->mChannelId),
NS_HTTP_ACTIVITY_TYPE_HTTP_TRANSACTION,
NS_HTTP_ACTIVITY_SUBTYPE_REQUEST_HEADER, PR_Now(), 0, requestBuf);
if (NS_FAILED(rv)) {
LOG3(("ObserveActivity failed (%08x)", static_cast<uint32_t>(rv)));
}
}));
}
// Create a string stream for the request header buf (the stream holds
// a non-owning reference to the request header data, so we MUST keep
// mReqHeaderBuf around).
nsCOMPtr<nsIInputStream> 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<nsIMultiplexInputStream> multi;
rv = nsMultiplexInputStreamConstructor(
nullptr, NS_GET_IID(nsIMultiplexInputStream), getter_AddRefs(multi));
if (NS_FAILED(rv)) return rv;
rv = multi->AppendStream(headers);
if (NS_FAILED(rv)) return rv;
rv = multi->AppendStream(requestBody);
if (NS_FAILED(rv)) return rv;
// wrap the multiplexed input stream with a buffered input stream, so
// that we write data in the largest chunks possible. this is actually
// necessary to workaround some common server bugs (see bug 137155).
nsCOMPtr<nsIInputStream> stream(do_QueryInterface(multi));
rv = NS_NewBufferedInputStream(getter_AddRefs(mRequestStream),
stream.forget(),
nsIOService::gDefaultSegmentSize);
if (NS_FAILED(rv)) return rv;
} else {
mRequestStream = headers;
}
nsCOMPtr<nsIThrottledInputChannel> throttled = do_QueryInterface(eventsink);
if (throttled) {
nsCOMPtr<nsIInputChannelThrottleQueue> queue;
rv = throttled->GetThrottleQueue(getter_AddRefs(queue));
// In case of failure, just carry on without throttling.
if (NS_SUCCEEDED(rv) && queue) {
nsCOMPtr<nsIAsyncInputStream> 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<int64_t>(requestContentLength)
: -1;
// create pipe for response stream
rv = NS_NewPipe2(getter_AddRefs(mPipeIn), getter_AddRefs(mPipeOut), true,
true, nsIOService::gDefaultSegmentSize,
nsIOService::gDefaultSegmentCount);
if (NS_FAILED(rv)) return rv;
if (transWithPushedStream && aPushedStreamId) {
RefPtr<nsHttpTransaction> trans =
transWithPushedStream->AsHttpTransaction();
MOZ_ASSERT(trans);
mPushedStream = trans->TakePushedStreamById(aPushedStreamId);
}
if (gHttpHandler->UseHTTPSRRAsAltSvcEnabled()) {
mHTTPSSVCReceivedStage.emplace(HTTPSSVC_NOT_PRESENT);
nsCOMPtr<nsIDNSService> dns = do_GetService(NS_DNSSERVICE_CONTRACTID);
nsCOMPtr<nsIEventTarget> target;
Unused << gHttpHandler->GetSocketThreadTarget(getter_AddRefs(target));
if (dns && target) {
uint32_t flags =
nsIDNSService::GetFlagsFromTRRMode(mConnInfo->GetTRRMode());
if (mCaps & NS_HTTP_REFRESH_DNS) {
flags |= nsIDNSService::RESOLVE_BYPASS_CACHE;
}
nsCOMPtr<nsICancelable> dnsRequest;
rv = dns->AsyncResolveNative(
mConnInfo->GetOrigin(), nsIDNSService::RESOLVE_TYPE_HTTPSSVC, flags,
nullptr, this, target, mConnInfo->GetOriginAttributes(),
getter_AddRefs(dnsRequest));
if (NS_FAILED(rv) && (mCaps & NS_HTTP_WAIT_HTTPSSVC_RESULT)) {
return rv;
}
{
MutexAutoLock lock(mLock);
mDNSRequest.swap(dnsRequest);
}
}
}
return NS_OK;
}
nsresult nsHttpTransaction::AsyncRead(nsIStreamListener* listener,
nsIRequest** pump) {
RefPtr<nsInputStreamPump> 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<nsIRunnable> event =
NewRunnableMethod("nsHttpTransaction::SetH2WSConnRefTaken", this,
&nsHttpTransaction::SetH2WSConnRefTaken);
gSocketTransportService->Dispatch(event, NS_DISPATCH_NORMAL);
return;
}
if (mH2WSTransaction) {
// Need to let the websocket transaction/connection know we've reached
// this point so it can stop forwarding information through us and
// instead communicate directly with the websocket channel.
mH2WSTransaction->SetConnRefTaken();
mH2WSTransaction = nullptr;
}
}
UniquePtr<nsHttpResponseHead> nsHttpTransaction::TakeResponseHead() {
MOZ_ASSERT(!mResponseHeadTaken, "TakeResponseHead called 2x");
// Lock TakeResponseHead() against main thread
MutexAutoLock lock(*nsHttp::GetLock());
mResponseHeadTaken = true;
// Even in OnStartRequest() the headers won't be available if we were
// canceled
if (!mHaveAllHeaders) {
NS_WARNING("response headers not available or incomplete");
return nullptr;
}
return WrapUnique(std::exchange(mResponseHead, nullptr));
}
UniquePtr<nsHttpHeaderArray> nsHttpTransaction::TakeResponseTrailers() {
MOZ_ASSERT(!mResponseTrailersTaken, "TakeResponseTrailers called 2x");
// Lock TakeResponseTrailers() against main thread
MutexAutoLock lock(*nsHttp::GetLock());
mResponseTrailersTaken = true;
return std::move(mForTakeResponseTrailers);
}
void nsHttpTransaction::SetProxyConnectFailed() { mProxyConnectFailed = true; }
nsHttpRequestHead* nsHttpTransaction::RequestHead() { return mRequestHead; }
uint32_t nsHttpTransaction::Http1xTransactionCount() { return 1; }
nsresult nsHttpTransaction::TakeSubTransactions(
nsTArray<RefPtr<nsAHttpTransaction> >& outTransactions) {
return NS_ERROR_NOT_IMPLEMENTED;
}
//----------------------------------------------------------------------------
// nsHttpTransaction::nsAHttpTransaction
//----------------------------------------------------------------------------
void nsHttpTransaction::SetConnection(nsAHttpConnection* conn) {
{
MutexAutoLock lock(mLock);
mConnection = conn;
}
}
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<nsIInterfaceRequestor> tmp(mCallbacks);
tmp.forget(cb);
}
void nsHttpTransaction::SetSecurityCallbacks(
nsIInterfaceRequestor* aCallbacks) {
{
MutexAutoLock lock(mLock);
mCallbacks = aCallbacks;
}
if (gSocketTransportService) {
RefPtr<UpdateSecurityCallbacks> 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<uint32_t>(status), progress));
if (status == NS_NET_STATUS_CONNECTED_TO ||
status == NS_NET_STATUS_WAITING_FOR) {
nsISocketTransport* socketTransport =
mConnection ? mConnection->Transport() : nullptr;
if (socketTransport) {
MutexAutoLock lock(mLock);
socketTransport->GetSelfAddr(&mSelfAddr);
socketTransport->GetPeerAddr(&mPeerAddr);
socketTransport->ResolvedByTRR(&mResolvedByTRR);
}
}
// If the timing is enabled, and we are not using a persistent connection
// then the requestStart timestamp will be null, so we mark the timestamps
// for domainLookupStart/End and connectStart/End
// If we are using a persistent connection they will remain null,
// and the correct value will be returned in Performance.
if (TimingEnabled() && GetRequestStart().IsNull()) {
if (status == NS_NET_STATUS_RESOLVING_HOST) {
SetDomainLookupStart(TimeStamp::Now(), true);
} else if (status == NS_NET_STATUS_RESOLVED_HOST) {
SetDomainLookupEnd(TimeStamp::Now());
} else if (status == NS_NET_STATUS_CONNECTING_TO) {
SetConnectStart(TimeStamp::Now());
} else if (status == NS_NET_STATUS_CONNECTED_TO) {
TimeStamp tnow = TimeStamp::Now();
SetConnectEnd(tnow, true);
{
MutexAutoLock lock(mLock);
mTimings.tcpConnectEnd = tnow;
// After a socket is connected we know for sure whether data
// has been sent on SYN packet and if not we should update TLS
// start timing.
if ((mFastOpenStatus != TFO_DATA_SENT) &&
!mTimings.secureConnectionStart.IsNull()) {
mTimings.secureConnectionStart = tnow;
}
}
} else if (status == NS_NET_STATUS_TLS_HANDSHAKE_STARTING) {
{
MutexAutoLock lock(mLock);
mTimings.secureConnectionStart = TimeStamp::Now();
}
} else if (status == NS_NET_STATUS_TLS_HANDSHAKE_ENDED) {
SetConnectEnd(TimeStamp::Now(), false);
} else if (status == NS_NET_STATUS_SENDING_TO) {
// Set the timestamp to Now(), only if it null
SetRequestStart(TimeStamp::Now(), true);
}
}
if (!mTransportSink) return;
MOZ_ASSERT(OnSocketThread(), "not on socket thread");
// Need to do this before the STATUS_RECEIVING_FROM check below, to make
// sure that the activity distributor gets told about all status events.
if (mActivityDistributor) {
// upon STATUS_WAITING_FOR; report request body sent
if ((mHasRequestBody) && (status == NS_NET_STATUS_WAITING_FOR)) {
nsresult rv = mActivityDistributor->ObserveActivityWithArgs(
HttpActivityArgs(mChannelId), NS_HTTP_ACTIVITY_TYPE_HTTP_TRANSACTION,
NS_HTTP_ACTIVITY_SUBTYPE_REQUEST_BODY_SENT, PR_Now(), 0,
EmptyCString());
if (NS_FAILED(rv)) {
LOG3(("ObserveActivity failed (%08x)", static_cast<uint32_t>(rv)));
}
}
// report the status and progress
nsresult rv = mActivityDistributor->ObserveActivityWithArgs(
HttpActivityArgs(mChannelId), NS_HTTP_ACTIVITY_TYPE_SOCKET_TRANSPORT,
static_cast<uint32_t>(status), PR_Now(), progress, EmptyCString());
if (NS_FAILED(rv)) {
LOG3(("ObserveActivity failed (%08x)", static_cast<uint32_t>(rv)));
}
}
// nsHttpChannel synthesizes progress events in OnDataAvailable
if (status == NS_NET_STATUS_RECEIVING_FROM) return;
int64_t progressMax;
if (status == NS_NET_STATUS_SENDING_TO) {
// suppress progress when only writing request headers
if (!mHasRequestBody) {
LOG1(
("nsHttpTransaction::OnTransportStatus %p "
"SENDING_TO without request body\n",
this));
return;
}
if (mReader) {
// A mRequestStream method is on the stack - wait.
LOG(
("nsHttpTransaction::OnSocketStatus [this=%p] "
"Skipping Re-Entrant NS_NET_STATUS_SENDING_TO\n",
this));
// its ok to coalesce several of these into one deferred event
mDeferredSendProgress = true;
return;
}
nsCOMPtr<nsISeekableStream> seekable = do_QueryInterface(mRequestStream);
if (!seekable) {
LOG1(
("nsHttpTransaction::OnTransportStatus %p "
"SENDING_TO without seekable request stream\n",
this));
progress = 0;
} else {
int64_t prog = 0;
seekable->Tell(&prog);
progress = prog;
}
// when uploading, we include the request headers in the progress
// notifications.
progressMax = mRequestSize;
} else {
progress = 0;
progressMax = 0;
}
mTransportSink->OnTransportStatus(transport, status, progress, progressMax);
}
bool nsHttpTransaction::IsDone() { return mTransactionDone; }
nsresult nsHttpTransaction::Status() { return mStatus; }
uint32_t nsHttpTransaction::Caps() { return mCaps & ~mCapsToClear; }
void nsHttpTransaction::SetDNSWasRefreshed() {
MOZ_ASSERT(mConsumerTarget->IsOnCurrentThread(),
"SetDNSWasRefreshed on target thread only!");
mCapsToClear |= NS_HTTP_REFRESH_DNS;
}
nsresult nsHttpTransaction::ReadRequestSegment(nsIInputStream* stream,
void* closure, const char* buf,
uint32_t offset, uint32_t count,
uint32_t* countRead) {
// For the tracking of sent bytes that we used to do for the networkstats
// API, please see bug 1318883 where it was removed.
nsHttpTransaction* trans = (nsHttpTransaction*)closure;
nsresult rv = trans->mReader->OnReadSegment(buf, count, countRead);
if (NS_FAILED(rv)) return rv;
LOG(("nsHttpTransaction::ReadRequestSegment %p read=%u", trans, *countRead));
trans->mSentData = true;
return NS_OK;
}
nsresult nsHttpTransaction::ReadSegments(nsAHttpSegmentReader* reader,
uint32_t count, uint32_t* countRead) {
LOG(("nsHttpTransaction::ReadSegments %p", this));
MOZ_ASSERT(OnSocketThread(), "not on socket thread");
if (mTransactionDone) {
*countRead = 0;
return mStatus;
}
if (!mConnected && !m0RTTInProgress) {
mConnected = true;
mConnection->GetSecurityInfo(getter_AddRefs(mSecurityInfo));
}
mDeferredSendProgress = false;
mReader = reader;
nsresult rv =
mRequestStream->ReadSegments(ReadRequestSegment, this, count, countRead);
mReader = nullptr;
if (m0RTTInProgress && (mEarlyDataDisposition == EARLY_NONE) &&
NS_SUCCEEDED(rv) && (*countRead > 0)) {
mEarlyDataDisposition = EARLY_SENT;
}
if (mDeferredSendProgress && mConnection && mConnection->Transport()) {
// to avoid using mRequestStream concurrently, OnTransportStatus()
// did not report upload status off the ReadSegments() stack from
// nsSocketTransport do it now.
OnTransportStatus(mConnection->Transport(), NS_NET_STATUS_SENDING_TO, 0);
}
mDeferredSendProgress = false;
if (mForceRestart) {
// The forceRestart condition was dealt with on the stack, but it did not
// clear the flag because nsPipe in the readsegment stack clears out
// return codes, so we need to use the flag here as a cue to return
// ERETARGETED
if (NS_SUCCEEDED(rv)) {
rv = NS_BINDING_RETARGETED;
}
mForceRestart = false;
}
// if read would block then we need to AsyncWait on the request stream.
// have callback occur on socket thread so we stay synchronized.
if (rv == NS_BASE_STREAM_WOULD_BLOCK) {
nsCOMPtr<nsIAsyncInputStream> asyncIn = do_QueryInterface(mRequestStream);
if (asyncIn) {
nsCOMPtr<nsIEventTarget> target;
Unused << gHttpHandler->GetSocketThreadTarget(getter_AddRefs(target));
if (target)
asyncIn->AsyncWait(this, 0, 0, target);
else {
NS_ERROR("no socket thread event target");
rv = NS_ERROR_UNEXPECTED;
}
}
}
return rv;
}
nsresult nsHttpTransaction::WritePipeSegment(nsIOutputStream* stream,
void* closure, char* buf,
uint32_t offset, uint32_t count,
uint32_t* countWritten) {
nsHttpTransaction* trans = (nsHttpTransaction*)closure;
if (trans->mTransactionDone) return NS_BASE_STREAM_CLOSED; // stop iterating
if (trans->TimingEnabled()) {
// Set the timestamp to Now(), only if it null
trans->SetResponseStart(TimeStamp::Now(), true);
}
// Bug 1153929 - add checks to fix windows crash
MOZ_ASSERT(trans->mWriter);
if (!trans->mWriter) {
return NS_ERROR_UNEXPECTED;
}
nsresult rv;
//
// OK, now let the caller fill this segment with data.
//
rv = trans->mWriter->OnWriteSegment(buf, count, countWritten);
if (NS_FAILED(rv)) return rv; // caller didn't want to write anything
LOG(("nsHttpTransaction::WritePipeSegment %p written=%u", trans,
*countWritten));
MOZ_ASSERT(*countWritten > 0, "bad writer");
trans->mReceivedData = true;
trans->mTransferSize += *countWritten;
// Let the transaction "play" with the buffer. It is free to modify
// the contents of the buffer and/or modify countWritten.
// - Bytes in HTTP headers don't count towards countWritten, so the input
// side of pipe (aka nsHttpChannel's mTransactionPump) won't hit
// OnInputStreamReady until all headers have been parsed.
//
rv = trans->ProcessData(buf, *countWritten, countWritten);
if (NS_FAILED(rv)) trans->Close(rv);
return rv; // failure code only stops WriteSegments; it is not propagated.
}
bool nsHttpTransaction::ShouldThrottle() {
if (mClassOfService & nsIClassOfService::DontThrottle) {
// We deliberately don't touch the throttling window here since
// DontThrottle requests are expected to be long-standing media
// streams and would just unnecessarily block running downloads.
// If we want to ballance bandwidth for media responses against
// running downloads, we need to find something smarter like
// changing the suspend/resume throttling intervals at-runtime.
return false;
}
if (!gHttpHandler->ConnMgr()->ShouldThrottle(this)) {
// We are not obligated to throttle
return false;
}
if (mContentRead < 16000) {
// Let the first bytes go, it may also well be all the content we get
LOG(("nsHttpTransaction::ShouldThrottle too few content (%" PRIi64
") this=%p",
mContentRead, this));
return false;
}
if (!(mClassOfService & nsIClassOfService::Throttleable) &&
gHttpHandler->ConnMgr()->IsConnEntryUnderPressure(mConnInfo)) {
LOG(("nsHttpTransaction::ShouldThrottle entry pressure this=%p", this));
// This is expensive to check (two hashtable lookups) but may help
// freeing connections for active tab transactions.
// Checking this only for transactions that are not explicitly marked
// as throttleable because trackers and (specially) downloads should
// keep throttling even under pressure.
return false;
}
return true;
}
void nsHttpTransaction::DontReuseConnection() {
LOG(("nsHttpTransaction::DontReuseConnection %p\n", this));
if (!OnSocketThread()) {
LOG(("DontReuseConnection %p not on socket thread\n", this));
nsCOMPtr<nsIRunnable> event =
NewRunnableMethod("nsHttpTransaction::DontReuseConnection", this,
&nsHttpTransaction::DontReuseConnection);
gSocketTransportService->Dispatch(event, NS_DISPATCH_NORMAL);
return;
}
if (mConnection) {
mConnection->DontReuse();
}
}
nsresult nsHttpTransaction::WriteSegments(nsAHttpSegmentWriter* writer,
uint32_t count,
uint32_t* countWritten) {
LOG(("nsHttpTransaction::WriteSegments %p", this));
MOZ_ASSERT(OnSocketThread(), "not on socket thread");
if (mTransactionDone) {
return NS_SUCCEEDED(mStatus) ? NS_BASE_STREAM_CLOSED : mStatus;
}
if (ShouldThrottle()) {
if (mThrottlingReadAllowance == THROTTLE_NO_LIMIT) { // no limit set
// V1: ThrottlingReadLimit() returns 0
mThrottlingReadAllowance = gHttpHandler->ThrottlingReadLimit();
}
} else {
mThrottlingReadAllowance = THROTTLE_NO_LIMIT; // don't limit
}
if (mThrottlingReadAllowance == 0) { // depleted
if (gHttpHandler->ConnMgr()->CurrentTopLevelOuterContentWindowId() !=
mTopLevelOuterContentWindowId) {
nsHttp::NotifyActiveTabLoadOptimization();
}
// Must remember that we have to call ResumeRecv() on our connection when
// called back by the conn manager to resume reading.
LOG(("nsHttpTransaction::WriteSegments %p response throttled", this));
mReadingStopped = true;
// This makes the underlaying connection or stream wait for explicit resume.
// For h1 this means we stop reading from the socket.
// For h2 this means we stop updating recv window for the stream.
return NS_BASE_STREAM_WOULD_BLOCK;
}
mWriter = writer;
if (!mPipeOut) {
return NS_ERROR_UNEXPECTED;
}
if (mThrottlingReadAllowance > 0) {
LOG(("nsHttpTransaction::WriteSegments %p limiting read from %u to %d",
this, count, mThrottlingReadAllowance));
count = std::min(count, static_cast<uint32_t>(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<nsIEventTarget> 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; }
nsISupports* nsHttpTransaction::SecurityInfo() { return 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; }
already_AddRefed<Http2PushedStreamWrapper>
nsHttpTransaction::TakePushedStreamById(uint32_t aStreamId) {
MOZ_ASSERT(mConsumerTarget->IsOnCurrentThread());
MOZ_ASSERT(aStreamId);
auto entry = mIDToStreamMap.Lookup(aStreamId);
if (entry) {
RefPtr<Http2PushedStreamWrapper> 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<Http2PushedStreamWrapper> stream = aStream;
if (!mConsumerTarget->IsOnCurrentThread()) {
RefPtr<nsHttpTransaction> self = this;
if (NS_FAILED(mConsumerTarget->Dispatch(
NS_NewRunnableFunction("nsHttpTransaction::OnPush",
[self, stream]() { self->OnPush(stream); }),
NS_DISPATCH_NORMAL))) {
stream->OnPushFailed();
}
return;
}
auto entry = mIDToStreamMap.LookupForAdd(stream->StreamID());
MOZ_ASSERT(!entry);
if (!entry) {
entry.OrInsert([&stream]() { return stream; });
}
if (NS_FAILED(mOnPushCallback(stream->StreamID(), stream->GetResourceUrl(),
stream->GetRequestString(), this))) {
stream->OnPushFailed();
mIDToStreamMap.Remove(stream->StreamID());
}
}
nsHttpTransaction* nsHttpTransaction::AsHttpTransaction() { return this; }
HttpTransactionParent* nsHttpTransaction::AsHttpTransactionParent() {
return nullptr;
}
nsHttpTransaction::HTTPSSVC_CONNECTION_FAILED_REASON
nsHttpTransaction::ErrorCodeToFailedReason(nsresult aErrorCode) {
HTTPSSVC_CONNECTION_FAILED_REASON reason = HTTPSSVC_CONNECTION_OTHERS;
switch (aErrorCode) {
case NS_ERROR_UNKNOWN_HOST:
reason = HTTPSSVC_CONNECTION_UNKNOWN_HOST;
break;
case NS_ERROR_CONNECTION_REFUSED:
reason = HTTPSSVC_CONNECTION_UNREACHABLE;
break;
default:
if (m421Received) {
reason = HTTPSSVC_CONNECTION_421_RECEIVED;
} else if (NS_ERROR_GET_MODULE(aErrorCode) == NS_ERROR_MODULE_SECURITY) {
reason = HTTPSSVC_CONNECTION_SECURITY_ERROR;
}
break;
}
return reason;
}
void nsHttpTransaction::Close(nsresult reason) {
LOG(("nsHttpTransaction::Close [this=%p reason=%" PRIx32 "]\n", this,
static_cast<uint32_t>(reason)));
if (!mClosed) {
gHttpHandler->ConnMgr()->RemoveActiveTransaction(this);
mActivated = false;
}
if (mDNSRequest) {
mDNSRequest->Cancel(NS_ERROR_ABORT);
mDNSRequest = nullptr;
}
MOZ_ASSERT(OnSocketThread(), "not on socket thread");
if (reason == NS_BINDING_RETARGETED) {
LOG((" close %p skipped due to ERETARGETED\n", this));
return;
}
if (mClosed) {
LOG((" already closed\n"));
return;
}
// When we capture 407 from H2 proxy via CONNECT, prepare the response headers
// for authentication in http channel.
if (mTunnelProvider && reason == NS_ERROR_PROXY_AUTHENTICATION_FAILED) {
MOZ_ASSERT(mProxyConnectResponseCode == 407, "non-407 proxy auth failed");
MOZ_ASSERT(!mFlat407Headers.IsEmpty(), "Contain status line at least");
uint32_t unused = 0;
// Reset the reason to avoid nsHttpChannel::ProcessFallback
reason = ProcessData(mFlat407Headers.BeginWriting(),
mFlat407Headers.Length(), &unused);
if (NS_SUCCEEDED(reason)) {
// prevent restarting the transaction
mReceivedData = true;
}
LOG(("nsHttpTransaction::Close [this=%p] overwrite reason to %" PRIx32
" for 407 proxy via CONNECT\n",
this, static_cast<uint32_t>(reason)));
}
NotifyTransactionObserver(reason);
if (mTokenBucketCancel) {
mTokenBucketCancel->Cancel(reason);
mTokenBucketCancel = nullptr;
}
if (mActivityDistributor) {
// report the reponse is complete if not already reported
if (!mResponseIsComplete) {
nsresult rv = mActivityDistributor->ObserveActivityWithArgs(
HttpActivityArgs(mChannelId), NS_HTTP_ACTIVITY_TYPE_HTTP_TRANSACTION,
NS_HTTP_ACTIVITY_SUBTYPE_RESPONSE_COMPLETE, PR_Now(),
static_cast<uint64_t>(mContentRead), EmptyCString());
if (NS_FAILED(rv)) {
LOG3(("ObserveActivity failed (%08x)", static_cast<uint32_t>(rv)));
}
}
// report that this transaction is closing
nsresult rv = mActivityDistributor->ObserveActivityWithArgs(
HttpActivityArgs(mChannelId), NS_HTTP_ACTIVITY_TYPE_HTTP_TRANSACTION,
NS_HTTP_ACTIVITY_SUBTYPE_TRANSACTION_CLOSE, PR_Now(), 0,
EmptyCString());
if (NS_FAILED(rv)) {
LOG3(("ObserveActivity failed (%08x)", static_cast<uint32_t>(rv)));
}
}
// we must no longer reference the connection! find out if the
// connection was being reused before letting it go.
bool connReused = false;
bool isHttp2 = false;
if (mConnection) {
connReused = mConnection->IsReused();
isHttp2 = mConnection->Version() >= HttpVersion::v2_0;
}
mConnected = false;
mTunnelProvider = nullptr;
//
// if the connection was reset or closed before we wrote any part of the
// request or if we wrote the request but didn't receive any part of the
// response and the connection was being reused, then we can (and really
// should) assume that we wrote to a stale connection and we must therefore
// repeat the request over a new connection.
//
// We have decided to retry not only in case of the reused connections, but
// all safe methods(bug 1236277).
//
// NOTE: the conditions under which we will automatically retry the HTTP
// request have to be carefully selected to avoid duplication of the
// request from the point-of-view of the server. such duplication could
// have dire consequences including repeated purchases, etc.
//
// NOTE: because of the way SSL proxy CONNECT is implemented, it is
// possible that the transaction may have received data without having
// sent any data. for this reason, mSendData == FALSE does not imply
// mReceivedData == FALSE. (see bug 203057 for more info.)
//
// Never restart transactions that are marked as sticky to their conenction.
// We use that capability to identify transactions bound to connection based
// authentication. Reissuing them on a different connections will break
// this bondage. Major issue may arise when there is an NTLM message auth
// header on the transaction and we send it to a different NTLM authenticated
// connection. It will break that connection and also confuse the channel's
// auth provider, beliving the cached credentials are wrong and asking for
// the password mistakenly again from the user.
if ((reason == NS_ERROR_NET_RESET || reason == NS_OK ||
reason ==
psm::GetXPCOMFromNSSError(SSL_ERROR_DOWNGRADE_WITH_EARLY_DATA) ||
mFallbackConnInfo) &&
(!(mCaps & NS_HTTP_STICKY_CONNECTION) ||
(mCaps & NS_HTTP_CONNECTION_RESTARTABLE) ||
(mEarlyDataDisposition == EARLY_425))) {
if (mForceRestart && NS_SUCCEEDED(Restart())) {
if (mResponseHead) {
mResponseHead->Reset();
}
mContentRead = 0;
mContentLength = -1;
delete mChunkedDecoder;
mChunkedDecoder = nullptr;
mHaveStatusLine = false;
mHaveAllHeaders = false;
mHttpResponseMatched = false;
mResponseIsComplete = false;
mDidContentStart = false;
mNoContent = false;
mSentData = false;
mReceivedData = false;
LOG(("transaction force restarted\n"));
return;
}
// reallySentData is meant to separate the instances where data has
// been sent by this transaction but buffered at a higher level while
// a TLS session (perhaps via a tunnel) is setup.
bool reallySentData =
mSentData && (!mConnection || mConnection->BytesWritten());
// If this is true, it means we failed to use the HTTPSSVC connection info
// to connect to the server. We need to retry with the original connection
// info.
bool restartToFallbackConnInfo = !reallySentData && mFallbackConnInfo;
if (reason ==
psm::GetXPCOMFromNSSError(SSL_ERROR_DOWNGRADE_WITH_EARLY_DATA) ||
(!mReceivedData && ((mRequestHead && mRequestHead->IsSafeMethod()) ||
!reallySentData || connReused)) ||
restartToFallbackConnInfo) {
if (restartToFallbackConnInfo) {
Telemetry::Accumulate(Telemetry::DNS_HTTPSSVC_CONNECTION_FAILED_REASON,
ErrorCodeToFailedReason(reason));
nsCOMPtr<nsIDNSService> dns = do_GetService(NS_DNSSERVICE_CONTRACTID);
if (dns) {
LOG(("add failed domain name [%s] -> [%s] to exclusion list",
mConnInfo->GetOrigin().get(), mConnInfo->GetRoutedHost().get()));
Unused << dns->ReportFailedSVCDomainName(mConnInfo->GetOrigin(),
mConnInfo->GetRoutedHost());
}
mConnInfo = nullptr;
mFallbackConnInfo.swap(mConnInfo);
LOG(
("transaction will be restarted with the fallback connection info "
"key=%s",
mConnInfo->HashKey().get()));
}
// if restarting fails, then we must proceed to close the pipe,
// which will notify the channel that the transaction failed.
if (NS_SUCCEEDED(Restart())) return;
}
}
if (!mResponseIsComplete && NS_SUCCEEDED(reason) && isHttp2) {
// Responses without content-length header field are still complete if
// they are transfered over http2 and the stream is properly closed.
mResponseIsComplete = true;
}
if ((mChunkedDecoder || (mContentLength >= int64_t(0))) &&
(NS_SUCCEEDED(reason) && !mResponseIsComplete)) {
NS_WARNING("Partial transfer, incomplete HTTP response received");
if ((mHttpResponseCode / 100 == 2) && (mHttpVersion >= HttpVersion::v1_1)) {
FrameCheckLevel clevel = gHttpHandler->GetEnforceH1Framing();
if (clevel >= FRAMECHECK_BARELY) {
// If clevel == FRAMECHECK_STRICT mark any incomplete response as
// partial.
// if clevel == FRAMECHECK_BARELY: 1) mark a chunked-encoded response
// that do not ends on exactly a chunk boundary as partial; We are not
// strict about the last 0-size chunk and do not mark as parial
// responses that do not have the last 0-size chunk but do end on a
// chunk boundary. (check mChunkedDecoder->GetChunkRemaining() != 0)
// 2) mark a transfer that is partial and it is not chunk-encoded or
// gzip-encoded or other content-encoding as partial. (check
// !mChunkedDecoder && !mContentDecoding && mContentDecodingCheck))
// if clevel == FRAMECHECK_STRICT_CHUNKED mark a chunked-encoded
// response that ends on exactly a chunk boundary also as partial.
// Here a response must have the last 0-size chunk.
if ((clevel == FRAMECHECK_STRICT) ||
(mChunkedDecoder && (mChunkedDecoder->GetChunkRemaining() ||
(clevel == FRAMECHECK_STRICT_CHUNKED))) ||
(!mChunkedDecoder && !mContentDecoding && mContentDecodingCheck)) {
reason = NS_ERROR_NET_PARTIAL_TRANSFER;
LOG(("Partial transfer, incomplete HTTP response received: %s",
mChunkedDecoder ? "broken chunk" : "c-l underrun"));
}
}
}
if (mConnection) {
// whether or not we generate an error for the transaction
// bad framing means we don't want a pconn
mConnection->DontReuse();
}
}
bool relConn = true;
if (NS_SUCCEEDED(reason)) {
// the server has not sent the final \r\n terminating the header
// section, and there may still be a header line unparsed. let's make
// sure we parse the remaining header line, and then hopefully, the
// response will be usable (see bug 88792).
if (!mHaveAllHeaders) {
char data = '\n';
uint32_t unused = 0;
Unused << ParseHead(&data, 1, &unused);
if (mResponseHead->Version() == HttpVersion::v0_9) {
// Reject 0 byte HTTP/0.9 Responses - bug 423506
LOG(("nsHttpTransaction::Close %p 0 Byte 0.9 Response", this));
reason = NS_ERROR_NET_RESET;
}
}
// honor the sticky connection flag...
if (mCaps & NS_HTTP_STICKY_CONNECTION) {
LOG((" keeping the connection because of STICKY_CONNECTION flag"));
relConn = false;
}
// if the proxy connection has failed, we want the connection be held
// to allow the upper layers (think nsHttpChannel) to close it when
// the failure is unrecoverable.
// we can't just close it here, because mProxyConnectFailed is to a general
// flag and is also set for e.g. 407 which doesn't mean to kill the
// connection, specifically when connection oriented auth may be involved.
if (mProxyConnectFailed) {
LOG((" keeping the connection because of mProxyConnectFailed"));
relConn = false;
}
// Use mFallbackConnInfo as an indicator that this transaction is completed
// successfully with an HTTPSSVC record.
if (mFallbackConnInfo) {
Telemetry::Accumulate(Telemetry::DNS_HTTPSSVC_CONNECTION_FAILED_REASON,
HTTPSSVC_CONNECTION_OK);
}
}
// mTimings.responseEnd is normally recorded based on the end of a
// HTTP delimiter such as chunked-encodings or content-length. However,
// EOF or an error still require an end time be recorded.
if (TimingEnabled()) {
const TimingStruct timings = Timings();
if (timings.responseEnd.IsNull() && !timings.responseStart.IsNull()) {
SetResponseEnd(TimeStamp::Now());
}
}
if (mTrafficCategory != HttpTrafficCategory::eInvalid) {
HttpTrafficAnalyzer* hta = gHttpHandler->GetHttpTrafficAnalyzer();
if (hta) {
hta->AccumulateHttpTransferredSize(mTrafficCategory, mTransferSize,
mContentRead);
}
}
if (mThroughCaptivePortal) {
Telemetry::ScalarAdd(
Telemetry::ScalarID::NETWORKING_HTTP_TRANSACTIONS_CAPTIVE_PORTAL, 1);
}
if (relConn && mConnection) {
MutexAutoLock lock(mLock);
mConnection = nullptr;
}
mStatus = reason;
mTransactionDone = true; // forcibly flag the transaction as complete
mClosed = true;
ReleaseBlockingTransaction();
// release some resources that we no longer need
mRequestStream = nullptr;
mReqHeaderBuf.Truncate();
mLineBuf.Truncate();
if (mChunkedDecoder) {
delete mChunkedDecoder;
mChunkedDecoder = nullptr;
}
// 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 <private>
//-----------------------------------------------------------------------------
nsresult nsHttpTransaction::Restart() {
MOZ_ASSERT(OnSocketThread(), "not on socket thread");
// limit the number of restart attempts - bug 92224
if (++mRestartCount >= gHttpHandler->MaxRequestAttempts()) {
LOG(("reached max request attempts, failing transaction @%p\n", this));
return NS_ERROR_NET_RESET;
}
LOG(("restarting transaction @%p\n", this));
mTunnelProvider = nullptr;
if (mRequestHead) {
// Dispatching on a new connection better w/o an ambient connection proxy
// auth request header to not confuse the proxy authenticator.
nsAutoCString proxyAuth;
if (NS_SUCCEEDED(
mRequestHead->GetHeader(nsHttp::Proxy_Authorization, proxyAuth)) &&
IsStickyAuthSchemeAt(proxyAuth)) {
Unused << mRequestHead->ClearHeader(nsHttp::Proxy_Authorization);
}
}
// rewind streams in case we already wrote out the request
nsCOMPtr<nsISeekableStream> seekable = do_QueryInterface(mRequestStream);
if (seekable) seekable->Seek(nsISeekableStream::NS_SEEK_SET, 0);
// clear old connection state...
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()) {
MutexAutoLock lock(*nsHttp::GetLock());
RefPtr<nsHttpConnectionInfo> ci;
mConnInfo->CloneAsDirectRoute(getter_AddRefs(ci));
mConnInfo = ci;
if (mRequestHead) {
DebugOnly<nsresult> rv =
mRequestHead->SetHeader(nsHttp::Alternate_Service_Used, "0"_ns);
MOZ_ASSERT(NS_SUCCEEDED(rv));
}
}
// Reset mDoNotRemoveAltSvc for the next try.
mDoNotRemoveAltSvc = false;
mRestarted = true;
return gHttpHandler->InitiateTransaction(this, mPriority);
}
bool nsHttpTransaction::TakeRestartedState() {
// This return true if the transaction has been restarted internally. Used to
// let the consuming nsHttpChannel reset proxy authentication. The flag is
// reset to false by this method.
return mRestarted.exchange(false);
}
char* nsHttpTransaction::LocateHttpStart(char* buf, uint32_t len,
bool aAllowPartialMatch) {
MOZ_ASSERT(!aAllowPartialMatch || mLineBuf.IsEmpty());
static const char HTTPHeader[] = "HTTP/1.";
static const uint32_t HTTPHeaderLen = sizeof(HTTPHeader) - 1;
static const char HTTP2Header[] = "HTTP/2";
static const uint32_t HTTP2HeaderLen = sizeof(HTTP2Header) - 1;
static const char HTTP3Header[] = "HTTP/3";
static const uint32_t HTTP3HeaderLen = sizeof(HTTP3Header) - 1;
// ShoutCast ICY is treated as HTTP/1.0
static const char ICYHeader[] = "ICY ";
static const uint32_t ICYHeaderLen = sizeof(ICYHeader) - 1;
if (aAllowPartialMatch && (len < HTTPHeaderLen))
return (PL_strncasecmp(buf, HTTPHeader, len) == 0) ? buf : nullptr;
// mLineBuf can contain partial match from previous search
if (!mLineBuf.IsEmpty()) {
MOZ_ASSERT(mLineBuf.Length() < HTTPHeaderLen);
int32_t checkChars = std::min(len, HTTPHeaderLen - mLineBuf.Length());
if (PL_strncasecmp(buf, HTTPHeader + mLineBuf.Length(), checkChars) == 0) {
mLineBuf.Append(buf, checkChars);
if (mLineBuf.Length() == HTTPHeaderLen) {
// We've found whole HTTPHeader sequence. Return pointer at the
// end of matched sequence since it is stored in mLineBuf.
return (buf + checkChars);
}
// Response matches pattern but is still incomplete.
return nullptr;
}
// Previous partial match together with new data doesn't match the
// pattern. Start the search again.
mLineBuf.Truncate();
}
bool firstByte = true;
while (len > 0) {
if (PL_strncasecmp(buf, HTTPHeader,
std::min<uint32_t>(len, HTTPHeaderLen)) == 0) {
if (len < HTTPHeaderLen) {
// partial HTTPHeader sequence found
// save partial match to mLineBuf
mLineBuf.Assign(buf, len);
return nullptr;
}
// whole HTTPHeader sequence found
return buf;
}
// At least "SmarterTools/2.0.3974.16813" generates nonsensical
// HTTP/2.0 responses to our HTTP/1 requests. Treat the minimal case of
// it as HTTP/1.1 to be compatible with old versions of ourselves and
// other browsers
if (firstByte && !mInvalidResponseBytesRead && len >= HTTP2HeaderLen &&
(PL_strncasecmp(buf, HTTP2Header, HTTP2HeaderLen) == 0)) {
LOG(("nsHttpTransaction:: Identified HTTP/2.0 treating as 1.x\n"));
return buf;
}
// HTTP/3.0 responses to our HTTP/1 requests. Treat the minimal case of
// it as HTTP/1.1 to be compatible with old versions of ourselves and
// other browsers
if (firstByte && !mInvalidResponseBytesRead && len >= HTTP3HeaderLen &&
(PL_strncasecmp(buf, HTTP3Header, HTTP3HeaderLen) == 0)) {
LOG(("nsHttpTransaction:: Identified HTTP/3.0 treating as 1.x\n"));
return buf;
}
// Treat ICY (AOL/Nullsoft ShoutCast) non-standard header in same fashion
// as HTTP/2.0 is treated above. This will allow "ICY " to be interpretted
// as HTTP/1.0 in nsHttpResponseHead::ParseVersion
if (firstByte && !mInvalidResponseBytesRead && len >= ICYHeaderLen &&
(PL_strncasecmp(buf, ICYHeader, ICYHeaderLen) == 0)) {
LOG(("nsHttpTransaction:: Identified ICY treating as HTTP/1.0\n"));
return buf;
}
if (!nsCRT::IsAsciiSpace(*buf)) firstByte = false;
buf++;
len--;
}
return nullptr;
}
nsresult nsHttpTransaction::ParseLine(nsACString& line) {
LOG1(("nsHttpTransaction::ParseLine [%s]\n", PromiseFlatCString(line).get()));
nsresult rv = NS_OK;
if (!mHaveStatusLine) {
mResponseHead->ParseStatusLine(line);
mHaveStatusLine = true;
// XXX this should probably never happen
if (mResponseHead->Version() == HttpVersion::v0_9) mHaveAllHeaders = true;
} else {
rv = mResponseHead->ParseHeaderLine(line);
}
return rv;
}
nsresult nsHttpTransaction::ParseLineSegment(char* segment, uint32_t len) {
MOZ_ASSERT(!mHaveAllHeaders, "already have all headers");
if (!mLineBuf.IsEmpty() && mLineBuf.Last() == '\n') {
// trim off the new line char, and if this segment is
// not a continuation of the previous or if we haven't
// parsed the status line yet, then parse the contents
// of mLineBuf.
mLineBuf.Truncate(mLineBuf.Length() - 1);
if (!mHaveStatusLine || (*segment != ' ' && *segment != '\t')) {
nsresult rv = ParseLine(mLineBuf);
mLineBuf.Truncate();
if (NS_FAILED(rv)) {
return rv;
}
}
}
// append segment to mLineBuf...
mLineBuf.Append(segment, len);
// a line buf with only a new line char signifies the end of headers.
if (mLineBuf.First() == '\n') {
mLineBuf.Truncate();
// discard this response if it is a 100 continue or other 1xx status.
uint16_t status = mResponseHead->Status();
if ((status != 101) && (status / 100 == 1)) {
LOG(("ignoring 1xx response\n"));
mHaveStatusLine = false;
mHttpResponseMatched = false;
mConnection->SetLastTransactionExpectedNoContent(true);
mResponseHead->Reset();
return NS_OK;
}
mHaveAllHeaders = true;
}
return NS_OK;
}
nsresult nsHttpTransaction::ParseHead(char* buf, uint32_t count,
uint32_t* countRead) {
nsresult rv;
uint32_t len;
char* eol;
LOG(("nsHttpTransaction::ParseHead [count=%u]\n", count));
*countRead = 0;
MOZ_ASSERT(!mHaveAllHeaders, "oops");
// allocate the response head object if necessary
if (!mResponseHead) {
mResponseHead = new nsHttpResponseHead();
if (!mResponseHead) return NS_ERROR_OUT_OF_MEMORY;
// report that we have a least some of the response
if (mActivityDistributor && !mReportedStart) {
mReportedStart = true;
rv = mActivityDistributor->ObserveActivityWithArgs(
HttpActivityArgs(mChannelId), NS_HTTP_ACTIVITY_TYPE_HTTP_TRANSACTION,
NS_HTTP_ACTIVITY_SUBTYPE_RESPONSE_START, PR_Now(), 0, EmptyCString());
if (NS_FAILED(rv)) {
LOG3(("ObserveActivity failed (%08x)", static_cast<uint32_t>(rv)));
}
}
}
if (!mHttpResponseMatched) {
// Normally we insist on seeing HTTP/1.x in the first few bytes,
// but if we are on a persistent connection and the previous transaction
// was not supposed to have any content then we need to be prepared
// to skip over a response body that the server may have sent even
// though it wasn't allowed.
if (!mConnection || !mConnection->LastTransactionExpectedNoContent()) {
// tolerate only minor junk before the status line
mHttpResponseMatched = true;
char* p = LocateHttpStart(buf, std::min<uint32_t>(count, 11), true);
if (!p) {
// Treat any 0.9 style response of a put as a failure.
if (mRequestHead->IsPut()) return NS_ERROR_ABORT;
mResponseHead->ParseStatusLine(EmptyCString());
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<char*>(memchr(buf, '\n', count - *countRead))) !=
nullptr) {
// found line in range [buf:eol]
len = eol - buf + 1;
*countRead += len;
// actually, the line is in the range [buf:eol-1]
if ((eol > buf) && (*(eol - 1) == '\r')) len--;
buf[len - 1] = '\n';
rv = ParseLineSegment(buf, len);
if (NS_FAILED(rv)) return rv;
if (mHaveAllHeaders) return NS_OK;
// skip over line
buf = eol + 1;
if (!mHttpResponseMatched) {
// a 100 class response has caused us to throw away that set of
// response headers and look for the next response
return NS_ERROR_NET_INTERRUPT;
}
}
// do something about a partial header line
if (!mHaveAllHeaders && (len = count - *countRead)) {
*countRead = count;
// ignore a trailing carriage return, and don't bother calling
// ParseLineSegment if buf only contains a carriage return.
if ((buf[len - 1] == '\r') && (--len == 0)) return NS_OK;
rv = ParseLineSegment(buf, len);
if (NS_FAILED(rv)) return rv;
}
return NS_OK;
}
nsresult nsHttpTransaction::HandleContentStart() {
LOG(("nsHttpTransaction::HandleContentStart [this=%p]\n", this));
MOZ_ASSERT(OnSocketThread(), "not on socket thread");
if (mResponseHead) {
if (mEarlyDataDisposition == EARLY_ACCEPTED) {
if (mResponseHead->Status() == 425) {
// We will report this state when the final responce arrives.
mEarlyDataDisposition = EARLY_425;
} else {
Unused << mResponseHead->SetHeader(nsHttp::X_Firefox_Early_Data,
"accepted"_ns);
}
} else if (mEarlyDataDisposition == EARLY_SENT) {
Unused << mResponseHead->SetHeader(nsHttp::X_Firefox_Early_Data,
"sent"_ns);
} else if (mEarlyDataDisposition == EARLY_425) {
Unused << mResponseHead->SetHeader(nsHttp::X_Firefox_Early_Data,
"received 425"_ns);
mEarlyDataDisposition = EARLY_NONE;
} // no header on NONE case
if (mFastOpenStatus == TFO_DATA_SENT) {
Unused << mResponseHead->SetHeader(nsHttp::X_Firefox_TCP_Fast_Open,
"data sent"_ns);
} else if (mFastOpenStatus == TFO_TRIED) {
Unused << mResponseHead->SetHeader(nsHttp::X_Firefox_TCP_Fast_Open,
"tried negotiating"_ns);
} else if (mFastOpenStatus == TFO_FAILED) {
Unused << mResponseHead->SetHeader(nsHttp::X_Firefox_TCP_Fast_Open,
"failed"_ns);
} // no header on TFO_NOT_TRIED case
if (LOG3_ENABLED()) {
LOG3(("http response [\n"));
nsAutoCString headers;
mResponseHead->Flatten(headers, false);
headers.AppendLiteral(" OriginalHeaders");
headers.AppendLiteral("\r\n");
mResponseHead->FlattenNetworkOriginalHeaders(headers);
LogHeaders(headers.get());
LOG3(("]\n"));
}
CheckForStickyAuthScheme();
// Save http version, mResponseHead isn't available anymore after
// TakeResponseHead() is called
mHttpVersion = mResponseHead->Version();
mHttpResponseCode = mResponseHead->Status();
// notify the connection, give it a chance to cause a reset.
bool reset = false;
nsresult rv = mConnection->OnHeadersAvailable(this, mRequestHead,
mResponseHead, &reset);
NS_ENSURE_SUCCESS(rv, rv);
// looks like we should ignore this response, resetting...
if (reset) {
LOG(("resetting transaction's response head\n"));
mHaveAllHeaders = false;
mHaveStatusLine = false;
mReceivedData = false;
mSentData = false;
mHttpResponseMatched = false;
mResponseHead->Reset();
// wait to be called again...