/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ /* vim:set expandtab ts=4 sw=4 sts=4 cin: */ /* ***** BEGIN LICENSE BLOCK ***** * Version: MPL 1.1/GPL 2.0/LGPL 2.1 * * The contents of this file are subject to the Mozilla Public License Version * 1.1 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * http://www.mozilla.org/MPL/ * * Software distributed under the License is distributed on an "AS IS" basis, * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License * for the specific language governing rights and limitations under the * License. * * The Original Code is Mozilla. * * The Initial Developer of the Original Code is * Netscape Communications. * Portions created by the Initial Developer are Copyright (C) 2001 * the Initial Developer. All Rights Reserved. * * Contributor(s): * Darin Fisher (original author) * Christian Biesinger * Google Inc. * Jan Wrobel * Jan Odvarko * Dave Camp * Honza Bambas * * Alternatively, the contents of this file may be used under the terms of * either the GNU General Public License Version 2 or later (the "GPL"), or * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), * in which case the provisions of the GPL or the LGPL are applicable instead * of those above. If you wish to allow use of your version of this file only * under the terms of either the GPL or the LGPL, and not to allow others to * use your version of this file under the terms of the MPL, indicate your * decision by deleting the provisions above and replace them with the notice * and other provisions required by the GPL or the LGPL. If you do not delete * the provisions above, a recipient may use your version of this file under * the terms of any one of the MPL, the GPL or the LGPL. * * ***** END LICENSE BLOCK ***** */ #include "nsHttpChannel.h" #include "nsHttpTransaction.h" #include "nsHttpConnection.h" #include "nsHttpHandler.h" #include "nsHttpAuthCache.h" #include "nsHttpResponseHead.h" #include "nsHttp.h" #include "nsIHttpAuthenticator.h" #include "nsIApplicationCacheService.h" #include "nsIApplicationCacheContainer.h" #include "nsIAuthInformation.h" #include "nsIAuthPrompt2.h" #include "nsIAuthPromptProvider.h" #include "nsIStringBundle.h" #include "nsXPCOM.h" #include "nsISupportsPrimitives.h" #include "nsIURL.h" #include "nsIIDNService.h" #include "nsIStreamListenerTee.h" #include "nsISeekableStream.h" #include "nsMimeTypes.h" #include "nsNetUtil.h" #include "nsString.h" #include "nsPrintfCString.h" #include "nsReadableUtils.h" #include "nsUnicharUtils.h" #include "nsAutoPtr.h" #include "plstr.h" #include "prprf.h" #include "nsEscape.h" #include "nsICookieService.h" #include "nsIResumableChannel.h" #include "nsInt64.h" #include "nsIVariant.h" #include "nsChannelProperties.h" #include "nsStreamUtils.h" #include "nsIOService.h" #include "nsAuthInformationHolder.h" #include "nsICacheService.h" #include "nsDNSPrefetch.h" #include "nsChannelClassifier.h" // True if the local cache should be bypassed when processing a request. #define BYPASS_LOCAL_CACHE(loadFlags) \ (loadFlags & (nsIRequest::LOAD_BYPASS_CACHE | \ nsICachingChannel::LOAD_BYPASS_LOCAL_CACHE)) static NS_DEFINE_CID(kStreamListenerTeeCID, NS_STREAMLISTENERTEE_CID); //----------------------------------------------------------------------------- // nsHttpChannel //----------------------------------------------------------------------------- nsHttpChannel::nsHttpChannel() : mResponseHead(nsnull) , mTransaction(nsnull) , mConnectionInfo(nsnull) , mLoadFlags(LOAD_NORMAL) , mStatus(NS_OK) , mLogicalOffset(0) , mCaps(0) , mPriority(PRIORITY_NORMAL) , mCachedResponseHead(nsnull) , mCacheAccess(0) , mPostID(0) , mRequestTime(0) , mProxyAuthContinuationState(nsnull) , mAuthContinuationState(nsnull) , mStartPos(LL_MAXUINT) , mPendingAsyncCallOnResume(nsnull) , mSuspendCount(0) , mRedirectionLimit(gHttpHandler->RedirectionLimit()) , mIsPending(PR_FALSE) , mWasOpened(PR_FALSE) , mApplyConversion(PR_TRUE) , mAllowPipelining(PR_TRUE) , mCachedContentIsValid(PR_FALSE) , mCachedContentIsPartial(PR_FALSE) , mResponseHeadersModified(PR_FALSE) , mCanceled(PR_FALSE) , mTransactionReplaced(PR_FALSE) , mUploadStreamHasHeaders(PR_FALSE) , mAuthRetryPending(PR_FALSE) , mProxyAuth(PR_FALSE) , mTriedProxyAuth(PR_FALSE) , mTriedHostAuth(PR_FALSE) , mSuppressDefensiveAuth(PR_FALSE) , mResuming(PR_FALSE) , mInitedCacheEntry(PR_FALSE) , mCacheForOfflineUse(PR_FALSE) , mCachingOpportunistically(PR_FALSE) , mFallbackChannel(PR_FALSE) , mInheritApplicationCache(PR_TRUE) , mChooseApplicationCache(PR_FALSE) , mLoadedFromApplicationCache(PR_FALSE) , mTracingEnabled(PR_TRUE) , mForceAllowThirdPartyCookie(PR_FALSE) , mCustomConditionalRequest(PR_FALSE) { LOG(("Creating nsHttpChannel [this=%p]\n", this)); // grab a reference to the handler to ensure that it doesn't go away. nsHttpHandler *handler = gHttpHandler; NS_ADDREF(handler); } nsHttpChannel::~nsHttpChannel() { LOG(("Destroying nsHttpChannel [this=%p]\n", this)); NS_IF_RELEASE(mConnectionInfo); NS_IF_RELEASE(mTransaction); NS_IF_RELEASE(mProxyAuthContinuationState); NS_IF_RELEASE(mAuthContinuationState); delete mResponseHead; delete mCachedResponseHead; // release our reference to the handler nsHttpHandler *handler = gHttpHandler; NS_RELEASE(handler); } nsresult nsHttpChannel::Init(nsIURI *uri, PRUint8 caps, nsProxyInfo *proxyInfo) { LOG(("nsHttpChannel::Init [this=%p]\n", this)); NS_PRECONDITION(uri, "null uri"); nsresult rv = nsHashPropertyBag::Init(); if (NS_FAILED(rv)) return rv; mURI = uri; mOriginalURI = uri; mDocumentURI = nsnull; mCaps = caps; // // Construct connection info object // nsCAutoString host; PRInt32 port = -1; PRBool usingSSL = PR_FALSE; rv = mURI->SchemeIs("https", &usingSSL); if (NS_FAILED(rv)) return rv; 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; LOG(("host=%s port=%d\n", host.get(), port)); rv = mURI->GetAsciiSpec(mSpec); if (NS_FAILED(rv)) return rv; LOG(("uri=%s\n", mSpec.get())); mConnectionInfo = new nsHttpConnectionInfo(host, port, proxyInfo, usingSSL); if (!mConnectionInfo) return NS_ERROR_OUT_OF_MEMORY; NS_ADDREF(mConnectionInfo); // Set default request method mRequestHead.SetMethod(nsHttp::Get); // // Set request headers // nsCAutoString 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.Headers(), caps, !mConnectionInfo->UsingSSL() && mConnectionInfo->UsingHttpProxy()); return rv; } //----------------------------------------------------------------------------- // nsHttpChannel //----------------------------------------------------------------------------- nsresult nsHttpChannel::AsyncCall(nsAsyncCallback funcPtr, nsRunnableMethod **retval) { nsresult rv; nsRefPtr > event = NS_NewRunnableMethod(this, funcPtr); rv = NS_DispatchToCurrentThread(event); if (NS_SUCCEEDED(rv) && retval) { *retval = event; } return rv; } nsresult nsHttpChannel::Connect(PRBool firstTime) { nsresult rv; LOG(("nsHttpChannel::Connect [this=%p]\n", this)); // ensure that we are using a valid hostname if (!net_IsValidHostName(nsDependentCString(mConnectionInfo->Host()))) return NS_ERROR_UNKNOWN_HOST; // true when called from AsyncOpen if (firstTime) { PRBool delayed = PR_FALSE; // are we offline? PRBool offline = gIOService->IsOffline(); if (offline) mLoadFlags |= LOAD_ONLY_FROM_CACHE; else if (PL_strcmp(mConnectionInfo->ProxyType(), "unknown") == 0) return ResolveProxy(); // Lazily resolve proxy info // Don't allow resuming when cache must be used if (mResuming && (mLoadFlags & LOAD_ONLY_FROM_CACHE)) { LOG(("Resuming from cache is not supported yet")); return NS_ERROR_DOCUMENT_NOT_CACHED; } // open a cache entry for this channel... rv = OpenCacheEntry(offline, &delayed); if (NS_FAILED(rv)) { LOG(("OpenCacheEntry failed [rv=%x]\n", rv)); // if this channel is only allowed to pull from the cache, then // we must fail if we were unable to open a cache entry. if (mLoadFlags & LOAD_ONLY_FROM_CACHE) { // If we have a fallback URI (and we're not already // falling back), process the fallback asynchronously. if (!mFallbackChannel && !mFallbackKey.IsEmpty()) { return AsyncCall(&nsHttpChannel::HandleAsyncFallback); } return NS_ERROR_DOCUMENT_NOT_CACHED; } // otherwise, let's just proceed without using the cache. } // if cacheForOfflineUse has been set, open up an offline cache // entry to update if (mCacheForOfflineUse) { rv = OpenOfflineCacheEntryForWriting(); if (NS_FAILED(rv)) return rv; } if (NS_SUCCEEDED(rv) && delayed) return NS_OK; } // we may or may not have a cache entry at this point if (mCacheEntry) { // inspect the cache entry to determine whether or not we need to go // out to net to validate it. this call sets mCachedContentIsValid // and may set request headers as required for cache validation. rv = CheckCache(); if (NS_FAILED(rv)) NS_WARNING("cache check failed"); // read straight from the cache if possible... if (mCachedContentIsValid) { nsRunnableMethod *event = nsnull; if (!mCachedContentIsPartial) { AsyncCall(&nsHttpChannel::AsyncOnExamineCachedResponse, &event); } rv = ReadFromCache(); if (NS_FAILED(rv) && event) { event->Revoke(); } return rv; } else 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. return NS_ERROR_DOCUMENT_NOT_CACHED; } } // check to see if authorization headers should be included AddAuthorizationHeaders(); if (mLoadFlags & LOAD_NO_NETWORK_IO) { return NS_ERROR_DOCUMENT_NOT_CACHED; } // hit the net... rv = SetupTransaction(); if (NS_FAILED(rv)) return rv; rv = gHttpHandler->InitiateTransaction(mTransaction, mPriority); if (NS_FAILED(rv)) return rv; return mTransactionPump->AsyncRead(this, nsnull); } // called when Connect fails nsresult nsHttpChannel::AsyncAbort(nsresult status) { LOG(("nsHttpChannel::AsyncAbort [this=%p status=%x]\n", this, status)); mStatus = status; mIsPending = PR_FALSE; nsresult rv = AsyncCall(&nsHttpChannel::HandleAsyncNotifyListener); // And if that fails? Callers ignore our return value anyway.... // finally remove ourselves from the load group. if (mLoadGroup) mLoadGroup->RemoveRequest(this, nsnull, status); return rv; } void nsHttpChannel::HandleAsyncNotifyListener() { NS_PRECONDITION(!mPendingAsyncCallOnResume, "How did that happen?"); if (mSuspendCount) { LOG(("Waiting until resume to do async notification [this=%p]\n", this)); mPendingAsyncCallOnResume = &nsHttpChannel::HandleAsyncNotifyListener; return; } DoNotifyListener(); } void nsHttpChannel::DoNotifyListener() { if (mListener) { mListener->OnStartRequest(this, mListenerContext); mListener->OnStopRequest(this, mListenerContext, mStatus); mListener = 0; mListenerContext = 0; } // We have to make sure to drop the reference to the callbacks too mCallbacks = nsnull; mProgressSink = nsnull; } void nsHttpChannel::HandleAsyncRedirect() { NS_PRECONDITION(!mPendingAsyncCallOnResume, "How did that happen?"); if (mSuspendCount) { LOG(("Waiting until resume to do async redirect [this=%p]\n", this)); mPendingAsyncCallOnResume = &nsHttpChannel::HandleAsyncRedirect; 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)) { rv = ProcessRedirection(mResponseHead->Status()); if (NS_FAILED(rv)) { // If ProcessRedirection fails, then we have to send out the // OnStart/OnStop notifications. LOG(("ProcessRedirection failed [rv=%x]\n", rv)); mStatus = rv; DoNotifyListener(); } } // close the cache entry. Blow it away if we couldn't process the redirect // for some reason (the cache entry might be corrupt). if (mCacheEntry) { if (NS_FAILED(rv)) mCacheEntry->Doom(); CloseCacheEntry(PR_FALSE); } mIsPending = PR_FALSE; if (mLoadGroup) mLoadGroup->RemoveRequest(this, nsnull, mStatus); } void nsHttpChannel::HandleAsyncNotModified() { NS_PRECONDITION(!mPendingAsyncCallOnResume, "How did that happen?"); if (mSuspendCount) { LOG(("Waiting until resume to do async not-modified [this=%p]\n", this)); mPendingAsyncCallOnResume = &nsHttpChannel::HandleAsyncNotModified; return; } LOG(("nsHttpChannel::HandleAsyncNotModified [this=%p]\n", this)); DoNotifyListener(); CloseCacheEntry(PR_TRUE); mIsPending = PR_FALSE; if (mLoadGroup) mLoadGroup->RemoveRequest(this, nsnull, mStatus); } void nsHttpChannel::HandleAsyncFallback() { NS_PRECONDITION(!mPendingAsyncCallOnResume, "How did that happen?"); if (mSuspendCount) { LOG(("Waiting until resume to do async fallback [this=%p]\n", this)); mPendingAsyncCallOnResume = &nsHttpChannel::HandleAsyncFallback; return; } nsresult rv = NS_OK; LOG(("nsHttpChannel::HandleAsyncFallback [this=%p]\n", this)); // since this event is handled asynchronously, it is possible that this // channel could have been canceled, in which case there would be no point // in processing the fallback. if (!mCanceled) { PRBool fallingBack; rv = ProcessFallback(&fallingBack); if (NS_FAILED(rv) || !fallingBack) { // If ProcessFallback fails, then we have to send out the // OnStart/OnStop notifications. LOG(("ProcessFallback failed [rv=%x, %d]\n", rv, fallingBack)); mStatus = NS_FAILED(rv) ? rv : NS_ERROR_DOCUMENT_NOT_CACHED; DoNotifyListener(); } } mIsPending = PR_FALSE; if (mLoadGroup) mLoadGroup->RemoveRequest(this, nsnull, mStatus); } nsresult nsHttpChannel::SetupTransaction() { LOG(("nsHttpChannel::SetupTransaction [this=%p]\n", this)); NS_ENSURE_TRUE(!mTransaction, NS_ERROR_ALREADY_INITIALIZED); nsresult rv; if (mCaps & NS_HTTP_ALLOW_PIPELINING) { // // disable pipelining if: // (1) pipelining has been explicitly disabled // (2) request corresponds to a top-level document load (link click) // (3) request method is non-idempotent // // XXX does the toplevel document check really belong here? or, should // we push it out entirely to necko consumers? // if (!mAllowPipelining || (mLoadFlags & LOAD_INITIAL_DOCUMENT_URI) || !(mRequestHead.Method() == nsHttp::Get || mRequestHead.Method() == nsHttp::Head || mRequestHead.Method() == nsHttp::Propfind || mRequestHead.Method() == nsHttp::Proppatch)) { LOG((" pipelining disallowed\n")); mCaps &= ~NS_HTTP_ALLOW_PIPELINING; } } // use the URI path if not proxying (transparent proxying such as SSL proxy // does not count here). also, figure out what version we should be speaking. nsCAutoString buf, path; nsCString* requestURI; if (mConnectionInfo->UsingSSL() || !mConnectionInfo->UsingHttpProxy()) { rv = mURI->GetPath(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, buf)) requestURI = &buf; else requestURI = &path; mRequestHead.SetVersion(gHttpHandler->HttpVersion()); } else { 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; rv = mURI->Clone(getter_AddRefs(tempURI)); if (NS_FAILED(rv)) return rv; rv = tempURI->SetUserPass(EmptyCString()); if (NS_FAILED(rv)) return rv; rv = tempURI->GetAsciiSpec(path); if (NS_FAILED(rv)) return rv; requestURI = &path; } else requestURI = &mSpec; mRequestHead.SetVersion(gHttpHandler->ProxyHttpVersion()); } // trim off the #ref portion if any... PRInt32 ref = requestURI->FindChar('#'); if (ref != kNotFound) requestURI->SetLength(ref); mRequestHead.SetRequestURI(*requestURI); // set the request time for cache expiration calculations mRequestTime = NowInSeconds(); // 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. mRequestHead.SetHeader(nsHttp::Pragma, NS_LITERAL_CSTRING("no-cache"), PR_TRUE); // If we're configured to speak HTTP/1.1 then also send 'Cache-control: // no-cache' if (mRequestHead.Version() >= NS_HTTP_VERSION_1_1) mRequestHead.SetHeader(nsHttp::Cache_Control, NS_LITERAL_CSTRING("no-cache"), PR_TRUE); } else if ((mLoadFlags & VALIDATE_ALWAYS) && (mCacheAccess & nsICache::ACCESS_READ)) { // 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() >= NS_HTTP_VERSION_1_1) mRequestHead.SetHeader(nsHttp::Cache_Control, NS_LITERAL_CSTRING("max-age=0"), PR_TRUE); else mRequestHead.SetHeader(nsHttp::Pragma, NS_LITERAL_CSTRING("no-cache"), PR_TRUE); } if (mResuming) { char byteRange[32]; PR_snprintf(byteRange, sizeof(byteRange), "bytes=%llu-", mStartPos); mRequestHead.SetHeader(nsHttp::Range, nsDependentCString(byteRange)); 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)) { nsCAutoString ifMatch; mRequestHead.SetHeader(nsHttp::If_Match, NS_UnescapeURL(Substring(start, slash), 0, ifMatch)); ++slash; // Incrementing, so that searching for '/' won't find // the same slash again } if (FindCharInReadable('/', slash, end)) { mRequestHead.SetHeader(nsHttp::If_Unmodified_Since, Substring(++slash, end)); } } } // create wrapper for this channel's notification callbacks nsCOMPtr callbacks; NS_NewNotificationCallbacksAggregation(mCallbacks, mLoadGroup, getter_AddRefs(callbacks)); if (!callbacks) return NS_ERROR_OUT_OF_MEMORY; // create the transaction object mTransaction = new nsHttpTransaction(); if (!mTransaction) return NS_ERROR_OUT_OF_MEMORY; NS_ADDREF(mTransaction); // See bug #466080. Transfer LOAD_ANONYMOUS flag to socket-layer. if (mLoadFlags & LOAD_ANONYMOUS) mCaps |= NS_HTTP_LOAD_ANONYMOUS; mConnectionInfo->SetAnonymous((mLoadFlags & LOAD_ANONYMOUS) != 0); nsCOMPtr responseStream; rv = mTransaction->Init(mCaps, mConnectionInfo, &mRequestHead, mUploadStream, mUploadStreamHasHeaders, NS_GetCurrentThread(), callbacks, this, getter_AddRefs(responseStream)); if (NS_FAILED(rv)) { NS_RELEASE(mTransaction); return rv; } rv = nsInputStreamPump::Create(getter_AddRefs(mTransactionPump), responseStream); return rv; } void nsHttpChannel::AddCookiesToRequest() { if (mLoadFlags & LOAD_ANONYMOUS) { return; } nsXPIDLCString cookie; nsICookieService *cs = gHttpHandler->GetCookieService(); if (cs) cs->GetCookieStringFromHttp(mURI, mDocumentURI ? mDocumentURI : mOriginalURI, this, getter_Copies(cookie)); if (cookie.IsEmpty()) cookie = mUserSetCookieHeader; else if (!mUserSetCookieHeader.IsEmpty()) cookie.Append(NS_LITERAL_CSTRING("; ") + mUserSetCookieHeader); // overwrite any existing cookie headers. be sure to clear any // existing cookies if we have no cookies to set or if the cookie // service is unavailable. mRequestHead.SetHeader(nsHttp::Cookie, cookie, PR_FALSE); } nsresult nsHttpChannel::ApplyContentConversions() { if (!mResponseHead) return NS_OK; LOG(("nsHttpChannel::ApplyContentConversions [this=%p]\n", this)); if (!mApplyConversion) { LOG(("not applying conversion per mApplyConversion\n")); return NS_OK; } const char *val = mResponseHead->PeekHeader(nsHttp::Content_Encoding); if (gHttpHandler->IsAcceptableEncoding(val)) { nsCOMPtr serv; nsresult rv = gHttpHandler-> GetStreamConverterService(getter_AddRefs(serv)); // we won't fail to load the page just because we couldn't load the // stream converter service.. carry on.. if (NS_SUCCEEDED(rv)) { nsCOMPtr converter; nsCAutoString from(val); ToLowerCase(from); rv = serv->AsyncConvertData(from.get(), "uncompressed", mListener, mListenerContext, getter_AddRefs(converter)); if (NS_SUCCEEDED(rv)) { LOG(("converter installed from \'%s\' to \'uncompressed\'\n", val)); mListener = converter; } } } else if (val != nsnull) { LOG(("Unknown content encoding '%s', ignoring\n", val)); } return NS_OK; } // NOTE: This function duplicates code from nsBaseChannel. This will go away // once HTTP uses nsBaseChannel (part of bug 312760) static void CallTypeSniffers(void *aClosure, const PRUint8 *aData, PRUint32 aCount) { nsIChannel *chan = static_cast(aClosure); const nsCOMArray& sniffers = gIOService->GetContentSniffers(); PRUint32 length = sniffers.Count(); for (PRUint32 i = 0; i < length; ++i) { nsCAutoString newType; nsresult rv = sniffers[i]->GetMIMETypeFromContent(chan, aData, aCount, newType); if (NS_SUCCEEDED(rv) && !newType.IsEmpty()) { chan->SetContentType(newType); break; } } } nsresult nsHttpChannel::CallOnStartRequest() { mTracingEnabled = PR_FALSE; if (mResponseHead && mResponseHead->ContentType().IsEmpty()) { if (!mContentTypeHint.IsEmpty()) mResponseHead->SetContentType(mContentTypeHint); else { // Uh-oh. We had better find out what type we are! // XXX This does not work with content-encodings... but // neither does applying the conversion from the URILoader nsCOMPtr serv; nsresult rv = gHttpHandler-> GetStreamConverterService(getter_AddRefs(serv)); // If we failed, we just fall through to the "normal" case if (NS_SUCCEEDED(rv)) { nsCOMPtr converter; rv = serv->AsyncConvertData(UNKNOWN_CONTENT_TYPE, "*/*", mListener, mListenerContext, getter_AddRefs(converter)); if (NS_SUCCEEDED(rv)) { mListener = converter; } } } } if (mResponseHead && mResponseHead->ContentCharset().IsEmpty()) mResponseHead->SetContentCharset(mContentCharsetHint); if (mResponseHead) SetPropertyAsInt64(NS_CHANNEL_PROP_CONTENT_LENGTH, mResponseHead->ContentLength()); // Allow consumers to override our content type if ((mLoadFlags & LOAD_CALL_CONTENT_SNIFFERS) && gIOService->GetContentSniffers().Count() != 0) { // 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); PRBool typeSniffersCalled = PR_FALSE; if (mCachePump) { typeSniffersCalled = NS_SUCCEEDED(mCachePump->PeekStream(CallTypeSniffers, thisChannel)); } if (!typeSniffersCalled && mTransactionPump) { mTransactionPump->PeekStream(CallTypeSniffers, thisChannel); } } LOG((" calling mListener->OnStartRequest\n")); nsresult rv = mListener->OnStartRequest(this, mListenerContext); if (NS_FAILED(rv)) return rv; // install stream converter if required rv = ApplyContentConversions(); if (NS_FAILED(rv)) return rv; if (!mCanceled) { // create offline cache entry if offline caching was requested if (mCacheForOfflineUse) { PRBool shouldCacheForOfflineUse; rv = ShouldUpdateOfflineCacheEntry(&shouldCacheForOfflineUse); if (NS_FAILED(rv)) return rv; if (shouldCacheForOfflineUse) { LOG(("writing to the offline cache")); rv = InitOfflineCacheEntry(); if (NS_FAILED(rv)) return rv; if (mOfflineCacheEntry) { rv = InstallOfflineCacheListener(); if (NS_FAILED(rv)) return rv; } } else { LOG(("offline cache is up to date, not updating")); CloseOfflineCacheEntry(); } } } return NS_OK; } nsresult nsHttpChannel::ProcessFailedSSLConnect(PRUint32 httpStatus) { // Failure to set up SSL proxy tunnel 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 noticed our proxy CONNECT, and is replying with malicious HTML. // // Our current architecture will parse 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. NS_ABORT_IF_FALSE(mConnectionInfo->UsingSSL(), "SSL connect failed but not using SSL?"); nsresult rv; switch (httpStatus) { case 300: case 301: case 302: case 303: case 307: // 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; case 403: // HTTP/1.1: "Forbidden" case 407: // ProcessAuthentication() failed case 501: // HTTP/1.1: "Not Implemented" // user sees boilerplate Mozilla "Proxy Refused Connection" page. rv = NS_ERROR_PROXY_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" /* User sees: "Address Not Found: Firefox can't find the server at * www.foo.com." */ rv = NS_ERROR_UNKNOWN_HOST; break; case 502: // HTTP/1.1: "Bad Gateway" (invalid resp from target server) // Squid returns 503 if target request fails for anything but DNS. case 503: // HTTP/1.1: "Service Unavailable" /* 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: // HTTP/1.1: "Gateway Timeout" // user sees: "Network Timeout: The server at www.foo.com // is taking too long to respond." rv = NS_ERROR_NET_TIMEOUT; break; // Confused proxy server or malicious response default: rv = NS_ERROR_PROXY_CONNECTION_REFUSED; break; } LOG(("Cancelling failed SSL proxy connection [this=%p httpStatus=%u]\n", this, httpStatus)); Cancel(rv); CallOnStartRequest(); return rv; } PRBool nsHttpChannel::ShouldSSLProxyResponseContinue(PRUint32 httpStatus) { // When SSL connect has failed, allow proxy reply to continue only if it's // an auth request, or a redirect of a non-POST top-level document load. switch (httpStatus) { case 407: return PR_TRUE; case 300: case 301: case 302: case 303: case 307: { return ( (mLoadFlags & nsIChannel::LOAD_DOCUMENT_URI) && mURI == mDocumentURI && mRequestHead.Method() != nsHttp::Post); } } return PR_FALSE; } nsresult nsHttpChannel::ProcessResponse() { nsresult rv; PRUint32 httpStatus = mResponseHead->Status(); LOG(("nsHttpChannel::ProcessResponse [this=%p httpStatus=%u]\n", this, httpStatus)); if (mTransaction->SSLConnectFailed() && !ShouldSSLProxyResponseContinue(httpStatus)) return ProcessFailedSSLConnect(httpStatus); // notify "http-on-examine-response" observers gHttpHandler->OnExamineResponse(this); // set cookies, if any exist; done after OnExamineResponse to allow those // observers to modify the cookie response headers SetCookie(mResponseHead->PeekHeader(nsHttp::Set_Cookie)); // handle unused username and password in url (see bug 232567) if (httpStatus != 401 && httpStatus != 407) { CheckForSuperfluousAuth(); if (mCanceled) return CallOnStartRequest(); if (mAuthContinuationState) { // reset the current continuation state because our last // authentication attempt has been completed successfully NS_RELEASE(mAuthContinuationState); LOG((" continuation state has been reset")); } } // 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 (mResuming && 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 (mCachedContentIsPartial) // an internal byte range request... rv = ProcessPartialContent(); else rv = ProcessNormal(); break; case 300: case 301: case 302: case 307: case 303: #if 0 case 305: // disabled as a security measure (see bug 187996). #endif // don't store the response body for redirects MaybeInvalidateCacheEntryForSubsequentGet(); rv = ProcessRedirection(httpStatus); if (NS_SUCCEEDED(rv)) { InitCacheEntry(); CloseCacheEntry(PR_FALSE); if (mCacheForOfflineUse) { // Store response in the offline cache InitOfflineCacheEntry(); CloseOfflineCacheEntry(); } } else { LOG(("ProcessRedirection failed [rv=%x]\n", rv)); if (mTransaction->SSLConnectFailed()) return ProcessFailedSSLConnect(httpStatus); rv = ProcessNormal(); } break; case 304: rv = ProcessNotModified(); if (NS_FAILED(rv)) { LOG(("ProcessNotModified failed [rv=%x]\n", rv)); rv = ProcessNormal(); } break; case 401: case 407: rv = ProcessAuthentication(httpStatus); if (NS_FAILED(rv)) { LOG(("ProcessAuthentication failed [rv=%x]\n", rv)); if (mTransaction->SSLConnectFailed()) return ProcessFailedSSLConnect(httpStatus); CheckForSuperfluousAuth(); rv = ProcessNormal(); } break; default: rv = ProcessNormal(); MaybeInvalidateCacheEntryForSubsequentGet(); break; } return rv; } nsresult nsHttpChannel::ProcessNormal() { nsresult rv; LOG(("nsHttpChannel::ProcessNormal [this=%p]\n", this)); PRBool succeeded; rv = GetRequestSucceeded(&succeeded); if (NS_SUCCEEDED(rv) && !succeeded) { PRBool fallingBack; rv = ProcessFallback(&fallingBack); if (NS_FAILED(rv)) { DoNotifyListener(); return rv; } if (fallingBack) { // Do not continue with normal processing, fallback is in // progress now. return NS_OK; } } // if we're here, then any byte-range requests failed to result in a partial // response. we must clear this flag to prevent BufferPartialContent from // being called inside our OnDataAvailable (see bug 136678). mCachedContentIsPartial = PR_FALSE; ClearBogusContentEncodingIfNeeded(); // 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(PR_TRUE); } // Check that the server sent us what we were asking for if (mResuming) { // Create an entity id from the response nsCAutoString 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 && (mCacheAccess & nsICache::ACCESS_WRITE)) { 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; nsXPIDLString messageString; rv = stringBundle->GetStringFromName(NS_LITERAL_STRING("RepostFormData").get(), getter_Copies(messageString)); // GetStringFromName can return NS_OK and NULL messageString. if (NS_SUCCEEDED(rv) && messageString) { PRBool repost = PR_FALSE; nsCOMPtr prompt; GetCallback(prompt); if (!prompt) return NS_ERROR_NO_INTERFACE; prompt->Confirm(nsnull, messageString, &repost); if (!repost) return NS_ERROR_FAILURE; } return rv; } nsresult nsHttpChannel::ProxyFailover() { LOG(("nsHttpChannel::ProxyFailover [this=%p]\n", this)); nsresult rv; nsCOMPtr pps = do_GetService(NS_PROTOCOLPROXYSERVICE_CONTRACTID, &rv); if (NS_FAILED(rv)) return rv; nsCOMPtr pi; rv = pps->GetFailoverForProxy(mConnectionInfo->ProxyInfo(), mURI, mStatus, getter_AddRefs(pi)); if (NS_FAILED(rv)) return rv; // XXXbz so where does this codepath remove us from the loadgroup, // exactly? return DoReplaceWithProxy(pi); } void nsHttpChannel::HandleAsyncReplaceWithProxy() { NS_PRECONDITION(!mPendingAsyncCallOnResume, "How did that happen?"); if (mSuspendCount) { LOG(("Waiting until resume to do async proxy replacement [this=%p]\n", this)); mPendingAsyncCallOnResume = &nsHttpChannel::HandleAsyncReplaceWithProxy; return; } nsresult status = mStatus; nsCOMPtr pi; pi.swap(mTargetProxyInfo); if (!mCanceled) { status = DoReplaceWithProxy(pi); if (mLoadGroup && NS_SUCCEEDED(status)) { mLoadGroup->RemoveRequest(this, nsnull, mStatus); } } if (NS_FAILED(status)) { AsyncAbort(status); } } nsresult nsHttpChannel::DoReplaceWithProxy(nsIProxyInfo* pi) { LOG(("nsHttpChannel::DoReplaceWithProxy [this=%p pi=%p]", this, pi)); nsresult rv; nsCOMPtr newChannel; rv = gHttpHandler->NewProxiedChannel(mURI, pi, getter_AddRefs(newChannel)); if (NS_FAILED(rv)) return rv; rv = SetupReplacementChannel(mURI, newChannel, PR_TRUE); if (NS_FAILED(rv)) return rv; // Inform consumers about this fake redirect PRUint32 flags = nsIChannelEventSink::REDIRECT_INTERNAL; rv = gHttpHandler->OnChannelRedirect(this, newChannel, flags); if (NS_FAILED(rv)) return rv; // Make sure to do this _after_ calling OnChannelRedirect newChannel->SetOriginalURI(mOriginalURI); // open new channel rv = newChannel->AsyncOpen(mListener, mListenerContext); if (NS_FAILED(rv)) return rv; mStatus = NS_BINDING_REDIRECTED; // disconnect from the old listeners... mListener = nsnull; mListenerContext = nsnull; // ...and the old callbacks mCallbacks = nsnull; mProgressSink = nsnull; 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; return pps->AsyncResolve(mURI, 0, this, getter_AddRefs(mProxyRequest)); } PRBool nsHttpChannel::ResponseWouldVary() { PRBool result = PR_FALSE; nsCAutoString buf, metaKey; mCachedResponseHead->GetHeader(nsHttp::Vary, buf); if (!buf.IsEmpty()) { NS_NAMED_LITERAL_CSTRING(prefix, "request-"); // enumerate the elements of the Vary header... char *val = buf.BeginWriting(); // going to munge buf char *token = nsCRT::strtok(val, NS_HTTP_HEADER_SEPS, &val); while (token) { // // if "*", then assume response would vary. technically speaking, // "Vary: header, *" is not permitted, but we allow it anyways. // // if the response depends on the value of the "Cookie" header, then // bail since we do not store cookies in the cache. this is done // 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.) // // this implementation is obviously not fully standards compliant, but // it is perhaps most prudent given the above issues. // if ((*token == '*') || (PL_strcasecmp(token, "cookie") == 0)) { result = PR_TRUE; break; } else { // build cache meta data key... metaKey = prefix + nsDependentCString(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. nsXPIDLCString lastVal; mCacheEntry->GetMetaDataElement(metaKey.get(), getter_Copies(lastVal)); if (lastVal) { nsHttpAtom atom = nsHttp::ResolveAtom(token); const char *newVal = mRequestHead.PeekHeader(atom); if (newVal && (strcmp(newVal, lastVal) != 0)) { result = PR_TRUE; // yes, response would vary break; } } // next token... token = nsCRT::strtok(val, NS_HTTP_HEADER_SEPS, &val); } } } return result; } //----------------------------------------------------------------------------- // nsHttpChannel //----------------------------------------------------------------------------- nsresult nsHttpChannel::SetupByteRangeRequest(PRUint32 partialLen) { // cached content has been found to be partial, add necessary request // headers to complete cache entry. // use strongest validator available... const char *val = mCachedResponseHead->PeekHeader(nsHttp::ETag); if (!val) val = mCachedResponseHead->PeekHeader(nsHttp::Last_Modified); if (!val) { // if we hit this code it means mCachedResponseHead->IsResumable() is // either broken or not being called. NS_NOTREACHED("no cache validator"); return NS_ERROR_FAILURE; } char buf[32]; PR_snprintf(buf, sizeof(buf), "bytes=%u-", partialLen); mRequestHead.SetHeader(nsHttp::Range, nsDependentCString(buf)); mRequestHead.SetHeader(nsHttp::If_Range, nsDependentCString(val)); return NS_OK; } nsresult nsHttpChannel::ProcessPartialContent() { // ok, we've just received a 206 // // we need to stream whatever data is in the cache out first, and then // pick up whatever data is on the wire, writing it into the cache. LOG(("nsHttpChannel::ProcessPartialContent [this=%p]\n", this)); NS_ENSURE_TRUE(mCachedResponseHead, NS_ERROR_NOT_INITIALIZED); NS_ENSURE_TRUE(mCacheEntry, NS_ERROR_NOT_INITIALIZED); // Make sure to clear bogus content-encodings before looking at the header ClearBogusContentEncodingIfNeeded(); // Check if the content-encoding we now got is different from the one we // got before if (PL_strcasecmp(mResponseHead->PeekHeader(nsHttp::Content_Encoding), mCachedResponseHead->PeekHeader(nsHttp::Content_Encoding)) != 0) { Cancel(NS_ERROR_INVALID_CONTENT_ENCODING); return CallOnStartRequest(); } // suspend the current transaction nsresult rv = mTransactionPump->Suspend(); if (NS_FAILED(rv)) return rv; // merge any new headers with the cached response headers rv = mCachedResponseHead->UpdateHeaders(mResponseHead->Headers()); if (NS_FAILED(rv)) return rv; // update the cached response head nsCAutoString head; mCachedResponseHead->Flatten(head, PR_TRUE); rv = mCacheEntry->SetMetaDataElement("response-head", head.get()); if (NS_FAILED(rv)) return rv; // make the cached response be the current response delete mResponseHead; mResponseHead = mCachedResponseHead; mCachedResponseHead = 0; 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); // the cached content is valid, although incomplete. mCachedContentIsValid = PR_TRUE; return ReadFromCache(); } nsresult nsHttpChannel::OnDoneReadingPartialCacheEntry(PRBool *streamDone) { nsresult rv; LOG(("nsHttpChannel::OnDoneReadingPartialCacheEntry [this=%p]", this)); // by default, assume we would have streamed all data or failed... *streamDone = PR_TRUE; // setup cache listener to append to cache entry PRUint32 size; rv = mCacheEntry->GetDataSize(&size); if (NS_FAILED(rv)) return rv; rv = InstallCacheListener(size); 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. mCachedContentIsPartial = PR_FALSE; // 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 = PR_FALSE; } else NS_NOTREACHED("no transaction"); return rv; } //----------------------------------------------------------------------------- // nsHttpChannel //----------------------------------------------------------------------------- nsresult nsHttpChannel::ProcessNotModified() { nsresult rv; LOG(("nsHttpChannel::ProcessNotModified [this=%p]\n", this)); if (mCustomConditionalRequest) { LOG(("Bypassing ProcessNotModified due to custom conditional headers")); return NS_ERROR_FAILURE; } NS_ENSURE_TRUE(mCachedResponseHead, NS_ERROR_NOT_INITIALIZED); NS_ENSURE_TRUE(mCacheEntry, NS_ERROR_NOT_INITIALIZED); // merge any new headers with the cached response headers rv = mCachedResponseHead->UpdateHeaders(mResponseHead->Headers()); if (NS_FAILED(rv)) return rv; // update the cached response head nsCAutoString head; mCachedResponseHead->Flatten(head, PR_TRUE); rv = mCacheEntry->SetMetaDataElement("response-head", head.get()); if (NS_FAILED(rv)) return rv; // make the cached response be the current response delete mResponseHead; mResponseHead = mCachedResponseHead; mCachedResponseHead = 0; rv = UpdateExpirationTime(); 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 = PR_TRUE; rv = ReadFromCache(); if (NS_FAILED(rv)) return rv; mTransactionReplaced = PR_TRUE; return NS_OK; } nsresult nsHttpChannel::ProcessFallback(PRBool *fallingBack) { LOG(("nsHttpChannel::ProcessFallback [this=%p]\n", this)); nsresult rv; *fallingBack = PR_FALSE; // At this point a load has failed (either due to network problems // or an error returned on the server). Perform an application // cache fallback if we have a URI to fall back to. if (!mApplicationCache || mFallbackKey.IsEmpty() || mFallbackChannel) { LOG((" choosing not to fallback [%p,%s,%d]", mApplicationCache.get(), mFallbackKey.get(), mFallbackChannel)); return NS_OK; } // Make sure the fallback entry hasn't been marked as a foreign // entry. PRUint32 fallbackEntryType; rv = mApplicationCache->GetTypes(mFallbackKey, &fallbackEntryType); NS_ENSURE_SUCCESS(rv, rv); if (fallbackEntryType & nsIApplicationCache::ITEM_FOREIGN) { // This cache points to a fallback that refers to a different // manifest. Refuse to fall back. return NS_OK; } NS_ASSERTION(fallbackEntryType & nsIApplicationCache::ITEM_FALLBACK, "Fallback entry not marked correctly!"); // Kill any opportunistic cache entry, and disable opportunistic // caching for the fallback. if (mOfflineCacheEntry) { mOfflineCacheEntry->Doom(); mOfflineCacheEntry = 0; mOfflineCacheAccess = 0; } mCacheForOfflineUse = PR_FALSE; mCachingOpportunistically = PR_FALSE; mOfflineCacheClientID.Truncate(); mOfflineCacheEntry = 0; mOfflineCacheAccess = 0; // Close the current cache entry. if (mCacheEntry) CloseCacheEntry(PR_TRUE); // Create a new channel to load the fallback entry. nsRefPtr newChannel; rv = gHttpHandler->NewChannel(mURI, getter_AddRefs(newChannel)); NS_ENSURE_SUCCESS(rv, rv); rv = SetupReplacementChannel(mURI, newChannel, PR_TRUE); NS_ENSURE_SUCCESS(rv, rv); // Make sure the new channel loads from the fallback key. nsCOMPtr httpInternal = do_QueryInterface(newChannel, &rv); NS_ENSURE_SUCCESS(rv, rv); rv = httpInternal->SetupFallbackChannel(mFallbackKey.get()); NS_ENSURE_SUCCESS(rv, rv); // ... and fallbacks should only load from the cache. PRUint32 newLoadFlags = mLoadFlags | LOAD_REPLACE | LOAD_ONLY_FROM_CACHE; rv = newChannel->SetLoadFlags(newLoadFlags); // Inform consumers about this fake redirect PRUint32 redirectFlags = nsIChannelEventSink::REDIRECT_INTERNAL; rv = gHttpHandler->OnChannelRedirect(this, newChannel, redirectFlags); if (NS_FAILED(rv)) return rv; // Make sure to do this _after_ calling OnChannelRedirect newChannel->SetOriginalURI(mOriginalURI); rv = newChannel->AsyncOpen(mListener, mListenerContext); NS_ENSURE_SUCCESS(rv, rv); // close down this channel Cancel(NS_BINDING_REDIRECTED); // disconnect from our listener mListener = 0; mListenerContext = 0; // and from our callbacks mCallbacks = nsnull; mProgressSink = nsnull; *fallingBack = PR_TRUE; return NS_OK; } // Determines if a request is a byte range request for a subrange, // i.e. is a byte range request, but not a 0- byte range request. static PRBool IsSubRangeRequest(nsHttpRequestHead &aRequestHead) { if (!aRequestHead.PeekHeader(nsHttp::Range)) return PR_FALSE; nsCAutoString byteRange; aRequestHead.GetHeader(nsHttp::Range, byteRange); return !byteRange.EqualsLiteral("bytes=0-"); } nsresult nsHttpChannel::OpenCacheEntry(PRBool offline, PRBool *delayed) { nsresult rv; *delayed = PR_FALSE; mLoadedFromApplicationCache = PR_FALSE; LOG(("nsHttpChannel::OpenCacheEntry [this=%p]", this)); // make sure we're not abusing this function NS_PRECONDITION(!mCacheEntry, "cache entry already open"); nsCAutoString cacheKey; if (mRequestHead.Method() == nsHttp::Post) { // 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.Method() != nsHttp::Get) && (mRequestHead.Method() != nsHttp::Head)) { // don't use the cache for other types of requests return NS_OK; } if (mResuming) { // 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; GenerateCacheKey(mPostID, cacheKey); // Get a cache session with appropriate storage policy nsCacheStoragePolicy storagePolicy = DetermineStoragePolicy(); // Set the desired cache access mode accordingly... nsCacheAccessMode accessRequested; if (offline || (mLoadFlags & INHIBIT_CACHING)) { // If we have been asked to bypass the cache and not write to the // cache, then don't use the cache at all. Unless we're actually // offline, which takes precedence over BYPASS_LOCAL_CACHE. if (BYPASS_LOCAL_CACHE(mLoadFlags) && !offline) return NS_ERROR_NOT_AVAILABLE; accessRequested = nsICache::ACCESS_READ; } else if (BYPASS_LOCAL_CACHE(mLoadFlags)) accessRequested = nsICache::ACCESS_WRITE; // replace cache entry else accessRequested = nsICache::ACCESS_READ_WRITE; // normal browsing if (!mApplicationCache && mInheritApplicationCache) { // Pick up an application cache from the notification // callbacks if available nsCOMPtr appCacheContainer; GetCallback(appCacheContainer); if (appCacheContainer) { appCacheContainer->GetApplicationCache(getter_AddRefs(mApplicationCache)); } } if (!mApplicationCache && (mChooseApplicationCache || (mLoadFlags & LOAD_CHECK_OFFLINE_CACHE))) { // We're supposed to load from an application cache, but // one was not supplied by the load group. Ask the // application cache service to choose one for us. nsCOMPtr appCacheService = do_GetService(NS_APPLICATIONCACHESERVICE_CONTRACTID); if (appCacheService) { nsresult rv = appCacheService->ChooseApplicationCache (cacheKey, getter_AddRefs(mApplicationCache)); NS_ENSURE_SUCCESS(rv, rv); } } nsCOMPtr session; // Will be set to true if we've found the right session, but need // to open the cache entry asynchronously. PRBool waitingForValidation = PR_FALSE; // If we have an application cache, we check it first. if (mApplicationCache) { nsCAutoString appCacheClientID; mApplicationCache->GetClientID(appCacheClientID); nsCOMPtr serv = do_GetService(NS_CACHESERVICE_CONTRACTID, &rv); NS_ENSURE_SUCCESS(rv, rv); rv = serv->CreateSession(appCacheClientID.get(), nsICache::STORE_OFFLINE, nsICache::STREAM_BASED, getter_AddRefs(session)); NS_ENSURE_SUCCESS(rv, rv); // we'll try to synchronously open the cache entry... however, // it may be in use and not yet validated, in which case we'll // try asynchronously opening the cache entry. // // We open with ACCESS_READ only, because we don't want to // overwrite the offline cache entry non-atomically. // ACCESS_READ will prevent us from writing to the offline // cache as a normal cache entry. rv = session->OpenCacheEntry(cacheKey, nsICache::ACCESS_READ, PR_FALSE, getter_AddRefs(mCacheEntry)); if (rv == NS_ERROR_CACHE_WAIT_FOR_VALIDATION) { accessRequested = nsICache::ACCESS_READ; waitingForValidation = PR_TRUE; rv = NS_OK; } if (NS_FAILED(rv) && !mCacheForOfflineUse && !mFallbackChannel) { // Check for namespace match. nsCOMPtr namespaceEntry; rv = mApplicationCache->GetMatchingNamespace (cacheKey, getter_AddRefs(namespaceEntry)); NS_ENSURE_SUCCESS(rv, rv); PRUint32 namespaceType = 0; if (!namespaceEntry || NS_FAILED(namespaceEntry->GetItemType(&namespaceType)) || (namespaceType & (nsIApplicationCacheNamespace::NAMESPACE_FALLBACK | nsIApplicationCacheNamespace::NAMESPACE_OPPORTUNISTIC | nsIApplicationCacheNamespace::NAMESPACE_BYPASS)) == 0) { // When loading from an application cache, only items // on the whitelist or matching a // fallback/opportunistic namespace should hit the // network... mLoadFlags |= LOAD_ONLY_FROM_CACHE; // ... and if there were an application cache entry, // we would have found it earlier. return NS_ERROR_CACHE_KEY_NOT_FOUND; } if (namespaceType & nsIApplicationCacheNamespace::NAMESPACE_FALLBACK) { rv = namespaceEntry->GetData(mFallbackKey); NS_ENSURE_SUCCESS(rv, rv); } if ((namespaceType & nsIApplicationCacheNamespace::NAMESPACE_OPPORTUNISTIC) && mLoadFlags & LOAD_DOCUMENT_URI) { // Document loads for items in an opportunistic namespace // should be placed in the offline cache. nsCString clientID; mApplicationCache->GetClientID(clientID); mCacheForOfflineUse = !clientID.IsEmpty(); SetOfflineCacheClientID(clientID); mCachingOpportunistically = PR_TRUE; } } else if (NS_SUCCEEDED(rv)) { // We successfully opened an offline cache session and the entry, // now indiciate we load from the offline cache. mLoadedFromApplicationCache = PR_TRUE; } } if (!mCacheEntry && !waitingForValidation) { rv = gHttpHandler->GetCacheSession(storagePolicy, getter_AddRefs(session)); if (NS_FAILED(rv)) return rv; rv = session->OpenCacheEntry(cacheKey, accessRequested, PR_FALSE, getter_AddRefs(mCacheEntry)); if (rv == NS_ERROR_CACHE_WAIT_FOR_VALIDATION) { waitingForValidation = PR_TRUE; rv = NS_OK; } if (NS_FAILED(rv)) return rv; } if (waitingForValidation) { // access to the cache entry has been denied (because the // cache entry is probably in use by another channel). if (mLoadFlags & LOAD_BYPASS_LOCAL_CACHE_IF_BUSY) { LOG(("bypassing local cache since it is busy\n")); return NS_ERROR_NOT_AVAILABLE; } rv = session->AsyncOpenCacheEntry(cacheKey, accessRequested, this); if (NS_FAILED(rv)) return rv; // we'll have to wait for the cache entry *delayed = PR_TRUE; } else if (NS_SUCCEEDED(rv)) { mCacheEntry->GetAccessGranted(&mCacheAccess); LOG(("nsHttpChannel::OpenCacheEntry [this=%p grantedAccess=%d]", this, mCacheAccess)); } return rv; } nsresult nsHttpChannel::OpenOfflineCacheEntryForWriting() { nsresult rv; LOG(("nsHttpChannel::OpenOfflineCacheEntryForWriting [this=%p]", this)); // make sure we're not abusing this function NS_PRECONDITION(!mOfflineCacheEntry, "cache entry already open"); PRBool offline = gIOService->IsOffline(); if (offline) { // only put things in the offline cache while online return NS_OK; } if (mRequestHead.Method() != nsHttp::Get) { // only cache complete documents offline return NS_OK; } // Don't cache byte range requests which are subranges, only cache 0- // byte range requests. if (IsSubRangeRequest(mRequestHead)) return NS_OK; nsCAutoString cacheKey; GenerateCacheKey(mPostID, cacheKey); NS_ENSURE_TRUE(!mOfflineCacheClientID.IsEmpty(), NS_ERROR_NOT_AVAILABLE); nsCOMPtr session; nsCOMPtr serv = do_GetService(NS_CACHESERVICE_CONTRACTID, &rv); if (NS_FAILED(rv)) return rv; rv = serv->CreateSession(mOfflineCacheClientID.get(), nsICache::STORE_OFFLINE, nsICache::STREAM_BASED, getter_AddRefs(session)); if (NS_FAILED(rv)) return rv; rv = session->OpenCacheEntry(cacheKey, nsICache::ACCESS_READ_WRITE, PR_FALSE, getter_AddRefs(mOfflineCacheEntry)); if (rv == NS_ERROR_CACHE_WAIT_FOR_VALIDATION) { // access to the cache entry has been denied (because the cache entry // is probably in use by another channel). Either the cache is being // read from (we're offline) or it's being updated elsewhere. return NS_OK; } if (NS_SUCCEEDED(rv)) { mOfflineCacheEntry->GetAccessGranted(&mOfflineCacheAccess); LOG(("got offline cache entry [access=%x]\n", mOfflineCacheAccess)); } return rv; } nsresult nsHttpChannel::GenerateCacheKey(PRUint32 postID, nsACString &cacheKey) { cacheKey.Truncate(); if (mLoadFlags & LOAD_ANONYMOUS) { cacheKey.AssignLiteral("anon&"); } if (postID) { char buf[32]; PR_snprintf(buf, sizeof(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 *spec = mFallbackChannel ? mFallbackKey.get() : mSpec.get(); const char *p = strchr(spec, '#'); if (p) cacheKey.Append(spec, p - spec); else cacheKey.Append(spec); return NS_OK; } // 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() { NS_ENSURE_TRUE(mResponseHead, NS_ERROR_FAILURE); nsresult rv; PRUint32 expirationTime = 0; if (!mResponseHead->MustValidate()) { PRUint32 freshnessLifetime = 0; rv = mResponseHead->ComputeFreshnessLifetime(&freshnessLifetime); if (NS_FAILED(rv)) return rv; if (freshnessLifetime > 0) { PRUint32 now = NowInSeconds(), currentAge = 0; rv = mResponseHead->ComputeCurrentAge(now, mRequestTime, ¤tAge); if (NS_FAILED(rv)) return rv; LOG(("freshnessLifetime = %u, currentAge = %u\n", freshnessLifetime, currentAge)); if (freshnessLifetime > currentAge) { PRUint32 timeRemaining = freshnessLifetime - currentAge; // be careful... now + timeRemaining may overflow if (now + timeRemaining < now) expirationTime = PRUint32(-1); else expirationTime = now + timeRemaining; } else expirationTime = now; } } rv = mCacheEntry->SetExpirationTime(expirationTime); NS_ENSURE_SUCCESS(rv, rv); if (mOfflineCacheEntry) { rv = mOfflineCacheEntry->SetExpirationTime(expirationTime); NS_ENSURE_SUCCESS(rv, rv); } return NS_OK; } // CheckCache is called from Connect after a cache entry has been opened for // this URL but before going out to net. It's purpose is to set or clear the // mCachedContentIsValid flag, and to configure an If-Modified-Since request // if validation is required. nsresult nsHttpChannel::CheckCache() { nsresult rv = NS_OK; LOG(("nsHTTPChannel::CheckCache enter [this=%p entry=%p access=%d]", this, mCacheEntry.get(), mCacheAccess)); // Be pessimistic: assume the cache entry has no useful data. mCachedContentIsValid = PR_FALSE; // Don't proceed unless we have opened a cache entry for reading. if (!mCacheEntry || !(mCacheAccess & nsICache::ACCESS_READ)) return NS_OK; nsXPIDLCString buf; // Get the method that was used to generate the cached response rv = mCacheEntry->GetMetaDataElement("request-method", getter_Copies(buf)); NS_ENSURE_SUCCESS(rv, rv); nsHttpAtom method = nsHttp::ResolveAtom(buf); if (method == nsHttp::Head) { // The cached response does not contain an entity. We can only reuse // the response if the current request is also HEAD. if (mRequestHead.Method() != nsHttp::Head) return NS_OK; } buf.Adopt(0); // We'll need this value in later computations... PRUint32 lastModifiedTime; rv = mCacheEntry->GetLastModified(&lastModifiedTime); NS_ENSURE_SUCCESS(rv, rv); // Determine if this is the first time that this cache entry // has been accessed during this session. PRBool fromPreviousSession = (gHttpHandler->SessionStartTime() > lastModifiedTime); // Get the cached HTTP response headers rv = mCacheEntry->GetMetaDataElement("response-head", getter_Copies(buf)); NS_ENSURE_SUCCESS(rv, rv); // Parse the cached HTTP response headers NS_ASSERTION(!mCachedResponseHead, "memory leak detected"); mCachedResponseHead = new nsHttpResponseHead(); if (!mCachedResponseHead) return NS_ERROR_OUT_OF_MEMORY; rv = mCachedResponseHead->Parse((char *) buf.get()); NS_ENSURE_SUCCESS(rv, rv); buf.Adopt(0); // Don't bother to validate items that are read-only, // unless they are read-only because of INHIBIT_CACHING or because // we're updating the offline cache. // Don't bother to validate if this is a fallback entry. if (!mCacheForOfflineUse && (mLoadedFromApplicationCache || (mCacheAccess == nsICache::ACCESS_READ && !(mLoadFlags & INHIBIT_CACHING)) || mFallbackChannel)) { mCachedContentIsValid = PR_TRUE; return NS_OK; } PRUint16 isCachedRedirect = mCachedResponseHead->Status()/100 == 3; if (method != nsHttp::Head && !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. nsInt64 contentLength = mCachedResponseHead->ContentLength(); if (contentLength != nsInt64(-1)) { PRUint32 size; rv = mCacheEntry->GetDataSize(&size); NS_ENSURE_SUCCESS(rv, rv); if (nsInt64(size) != contentLength) { LOG(("Cached data size does not match the Content-Length header " "[content-length=%lld size=%u]\n", PRInt64(contentLength), size)); if ((nsInt64(size) < contentLength) && mCachedResponseHead->IsResumable()) { // looks like a partial entry. rv = SetupByteRangeRequest(size); NS_ENSURE_SUCCESS(rv, rv); mCachedContentIsPartial = PR_TRUE; } return NS_OK; } } } PRBool doValidation = PR_FALSE; PRBool canAddImsHeader = PR_TRUE; mCustomConditionalRequest = mRequestHead.PeekHeader(nsHttp::If_Modified_Since) || mRequestHead.PeekHeader(nsHttp::If_None_Match) || mRequestHead.PeekHeader(nsHttp::If_Unmodified_Since) || mRequestHead.PeekHeader(nsHttp::If_Match) || mRequestHead.PeekHeader(nsHttp::If_Range); // If the LOAD_FROM_CACHE flag is set, any cached data can simply be used. if (mLoadFlags & LOAD_FROM_CACHE) { LOG(("NOT validating based on LOAD_FROM_CACHE load flag\n")); doValidation = PR_FALSE; } // If the VALIDATE_ALWAYS flag is set, any cached data won't be used until // it's revalidated with the server. else if (mLoadFlags & VALIDATE_ALWAYS) { LOG(("Validating based on VALIDATE_ALWAYS load flag\n")); doValidation = PR_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. else if (mLoadFlags & VALIDATE_NEVER) { LOG(("VALIDATE_NEVER set\n")); // if no-store or if no-cache and ssl, validate cached response (see // bug 112564 for an explanation of this logic) if (mCachedResponseHead->NoStore() || (mCachedResponseHead->NoCache() && mConnectionInfo->UsingSSL())) { LOG(("Validating based on (no-store || (no-cache && ssl)) logic\n")); doValidation = PR_TRUE; } else { LOG(("NOT validating based on VALIDATE_NEVER load flag\n")); doValidation = PR_FALSE; } } // check if validation is strictly required... else if (mCachedResponseHead->MustValidate()) { LOG(("Validating based on MustValidate() returning TRUE\n")); doValidation = PR_TRUE; } else if (ResponseWouldVary()) { LOG(("Validating based on Vary headers returning TRUE\n")); canAddImsHeader = PR_FALSE; doValidation = PR_TRUE; } else if (MustValidateBasedOnQueryUrl()) { LOG(("Validating based on RFC 2616 section 13.9 " "(query-url w/o explicit expiration-time)\n")); doValidation = PR_TRUE; } // Check if the cache entry has expired... else { PRUint32 time = 0; // a temporary variable for storing time values... rv = mCacheEntry->GetExpirationTime(&time); NS_ENSURE_SUCCESS(rv, rv); if (NowInSeconds() <= time) doValidation = PR_FALSE; else if (mCachedResponseHead->MustValidateIfExpired()) doValidation = PR_TRUE; else if (mLoadFlags & 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. rv = mCachedResponseHead->ComputeFreshnessLifetime(&time); NS_ENSURE_SUCCESS(rv, rv); if (time == 0) doValidation = PR_TRUE; else doValidation = fromPreviousSession; } else doValidation = PR_TRUE; LOG(("%salidating based on expiration time\n", doValidation ? "V" : "Not v")); } if (!doValidation && mRequestHead.PeekHeader(nsHttp::If_Match) && (method == nsHttp::Get || method == nsHttp::Head)) { const char *requestedETag, *cachedETag; cachedETag = mCachedResponseHead->PeekHeader(nsHttp::ETag); requestedETag = mRequestHead.PeekHeader(nsHttp::If_Match); if (cachedETag && (!strncmp(cachedETag, "W/", 2) || strcmp(requestedETag, 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 = PR_TRUE; } } 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. // mCacheEntry->GetMetaDataElement("auth", getter_Copies(buf)); doValidation = (fromPreviousSession && !buf.IsEmpty()) || (buf.IsEmpty() && mRequestHead.PeekHeader(nsHttp::Authorization)); } if (!doValidation) { // Sites redirect back to the original URI after setting a session/tracking // cookie. In such cases, force revalidation so that we hit the net and do not // cycle thru cached responses. if (isCachedRedirect && mRequestHead.PeekHeader(nsHttp::Cookie)) doValidation = PR_TRUE; } mCachedContentIsValid = !doValidation; if (doValidation) { // // now, we are definitely going to issue a HTTP request to the server. // make it conditional if possible. // // do not attempt to validate no-store content, since servers will not // expect it to be cached. (we only keep it in our cache for the // purposes of back/forward, etc.) // // the request method MUST be either GET or HEAD (see bug 175641). // // do not override conditional headers when consumer has defined its own if (!mCachedResponseHead->NoStore() && (mRequestHead.Method() == nsHttp::Get || mRequestHead.Method() == nsHttp::Head) && !mCustomConditionalRequest) { const char *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) { val = mCachedResponseHead->PeekHeader(nsHttp::Last_Modified); if (val) mRequestHead.SetHeader(nsHttp::If_Modified_Since, nsDependentCString(val)); } // Add If-None-Match header if an ETag was given in the response val = mCachedResponseHead->PeekHeader(nsHttp::ETag); if (val) mRequestHead.SetHeader(nsHttp::If_None_Match, nsDependentCString(val)); } } LOG(("nsHTTPChannel::CheckCache exit [this=%p doValidation=%d]\n", this, doValidation)); return NS_OK; } PRBool nsHttpChannel::MustValidateBasedOnQueryUrl() { // RFC 2616, section 13.9 states that GET-requests with a query-url // MUST NOT be treated as fresh unless the server explicitly provides // an expiration-time in the response. See bug #468594 // Section 13.2.1 (6th paragraph) defines "explicit expiration time" if (mRequestHead.Method() == nsHttp::Get) { nsCAutoString query; nsCOMPtr url = do_QueryInterface(mURI); nsresult rv = url->GetQuery(query); if (NS_SUCCEEDED(rv) && !query.IsEmpty()) { PRUint32 tmp; // we don't need the value, just whether it's set rv = mCachedResponseHead->GetExpiresValue(&tmp); if (NS_FAILED(rv)) { rv = mCachedResponseHead->GetMaxAgeValue(&tmp); if (NS_FAILED(rv)) { return PR_TRUE; } } } } return PR_FALSE; } nsresult nsHttpChannel::ShouldUpdateOfflineCacheEntry(PRBool *shouldCacheForOfflineUse) { *shouldCacheForOfflineUse = PR_FALSE; if (!mOfflineCacheEntry) { return NS_OK; } // if we're updating the cache entry, update the offline cache entry too if (mCacheEntry && (mCacheAccess & nsICache::ACCESS_WRITE)) { *shouldCacheForOfflineUse = PR_TRUE; return NS_OK; } // if there's nothing in the offline cache, add it if (mOfflineCacheEntry && (mOfflineCacheAccess == nsICache::ACCESS_WRITE)) { *shouldCacheForOfflineUse = PR_TRUE; return NS_OK; } // if the document is newer than the offline entry, update it PRUint32 docLastModifiedTime; nsresult rv = mResponseHead->GetLastModifiedValue(&docLastModifiedTime); if (NS_FAILED(rv)) { *shouldCacheForOfflineUse = PR_TRUE; return NS_OK; } PRUint32 offlineLastModifiedTime; rv = mOfflineCacheEntry->GetLastModified(&offlineLastModifiedTime); NS_ENSURE_SUCCESS(rv, rv); if (docLastModifiedTime > offlineLastModifiedTime) { *shouldCacheForOfflineUse = PR_TRUE; return NS_OK; } return NS_OK; } // If the data in the cache hasn't expired, then there's no need to // talk with the server, not even to do an if-modified-since. This // method creates a stream from the cache, synthesizing all the various // channel-related events. nsresult nsHttpChannel::ReadFromCache() { nsresult rv; NS_ENSURE_TRUE(mCacheEntry, NS_ERROR_FAILURE); NS_ENSURE_TRUE(mCachedContentIsValid, NS_ERROR_FAILURE); LOG(("nsHttpChannel::ReadFromCache [this=%p] " "Using cached copy of: %s\n", this, mSpec.get())); if (mCachedResponseHead) { NS_ASSERTION(!mResponseHead, "memory leak"); mResponseHead = mCachedResponseHead; mCachedResponseHead = 0; } // 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) mCacheEntry->GetSecurityInfo(getter_AddRefs(mSecurityInfo)); if ((mCacheAccess & nsICache::ACCESS_WRITE) && !mCachedContentIsPartial) { // We have write access to the cache, but we don't need to go to the // server to validate at this time, so just mark the cache entry as // valid in order to allow others access to this cache entry. mCacheEntry->MarkValid(); } // if this is a cached redirect, we must process the redirect asynchronously // since AsyncOpen may not have returned yet. Make sure there is a Location // header, otherwise we'll have to treat this like a normal 200 response. if (mResponseHead && (mResponseHead->Status() / 100 == 3) && (mResponseHead->PeekHeader(nsHttp::Location))) return AsyncCall(&nsHttpChannel::HandleAsyncRedirect); // have we been configured to skip reading from the cache? if ((mLoadFlags & LOAD_ONLY_IF_MODIFIED) && !mCachedContentIsPartial) { // if offline caching has been requested and the offline cache needs // updating, complete the call even if the main cache entry is // up-to-date PRBool shouldUpdateOffline; if (!mCacheForOfflineUse || NS_FAILED(ShouldUpdateOfflineCacheEntry(&shouldUpdateOffline)) || !shouldUpdateOffline) { LOG(("skipping read from cache based on LOAD_ONLY_IF_MODIFIED " "load flag\n")); return AsyncCall(&nsHttpChannel::HandleAsyncNotModified); } } // open input stream for reading... nsCOMPtr stream; rv = mCacheEntry->OpenInputStream(0, getter_AddRefs(stream)); if (NS_FAILED(rv)) return rv; rv = nsInputStreamPump::Create(getter_AddRefs(mCachePump), stream, nsInt64(-1), nsInt64(-1), 0, 0, PR_TRUE); if (NS_FAILED(rv)) return rv; return mCachePump->AsyncRead(this, mListenerContext); } void nsHttpChannel::CloseCacheEntry(PRBool doomOnFailure) { if (!mCacheEntry) return; LOG(("nsHttpChannel::CloseCacheEntry [this=%p] mStatus=%x mCacheAccess=%x", this, mStatus, mCacheAccess)); // 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. PRBool doom = PR_FALSE; if (mInitedCacheEntry) { NS_ASSERTION(mResponseHead, "oops"); if (NS_FAILED(mStatus) && doomOnFailure && (mCacheAccess & nsICache::ACCESS_WRITE) && !mResponseHead->IsResumable()) doom = PR_TRUE; } else if (mCacheAccess == nsICache::ACCESS_WRITE) doom = PR_TRUE; if (doom) { LOG((" dooming cache entry!!")); mCacheEntry->Doom(); } if (mCachedResponseHead) { delete mCachedResponseHead; mCachedResponseHead = 0; } mCachePump = 0; mCacheEntry = 0; mCacheAccess = 0; mInitedCacheEntry = PR_FALSE; } void nsHttpChannel::CloseOfflineCacheEntry() { if (!mOfflineCacheEntry) return; LOG(("nsHttpChannel::CloseOfflineCacheEntry [this=%p]", this)); if (NS_FAILED(mStatus)) { mOfflineCacheEntry->Doom(); } else { PRBool succeeded; if (NS_SUCCEEDED(GetRequestSucceeded(&succeeded)) && !succeeded) mOfflineCacheEntry->Doom(); } mOfflineCacheEntry = 0; mOfflineCacheAccess = 0; if (mCachingOpportunistically) { nsCOMPtr appCacheService = do_GetService(NS_APPLICATIONCACHESERVICE_CONTRACTID); if (appCacheService) { nsCAutoString cacheKey; GenerateCacheKey(mPostID, cacheKey); appCacheService->CacheOpportunistically(mApplicationCache, cacheKey); } } } // 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 (mCacheAccess == nsICache::ACCESS_READ) 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())); // 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() && mConnectionInfo->UsingSSL()) mLoadFlags |= INHIBIT_PERSISTENT_CACHING; if (mLoadFlags & INHIBIT_PERSISTENT_CACHING) { rv = mCacheEntry->SetStoragePolicy(nsICache::STORE_IN_MEMORY); if (NS_FAILED(rv)) return rv; } // Set the expiration time for this cache entry rv = UpdateExpirationTime(); if (NS_FAILED(rv)) return rv; rv = AddCacheEntryHeaders(mCacheEntry); if (NS_FAILED(rv)) return rv; mInitedCacheEntry = PR_TRUE; return NS_OK; } nsresult nsHttpChannel::InitOfflineCacheEntry() { // This function can be called even when we fail to connect (bug 551990) if (!mOfflineCacheEntry) { return NS_OK; } if (mResponseHead && mResponseHead->NoStore()) { CloseOfflineCacheEntry(); return NS_OK; } // This entry's expiration time should match the main entry's expiration // time. UpdateExpirationTime() will keep it in sync once the offline // cache entry has been created. if (mCacheEntry) { PRUint32 expirationTime; nsresult rv = mCacheEntry->GetExpirationTime(&expirationTime); NS_ENSURE_SUCCESS(rv, rv); mOfflineCacheEntry->SetExpirationTime(expirationTime); } return AddCacheEntryHeaders(mOfflineCacheEntry); } nsresult nsHttpChannel::AddCacheEntryHeaders(nsICacheEntryDescriptor *entry) { nsresult rv; // Store secure data in memory only if (mSecurityInfo) entry->SetSecurityInfo(mSecurityInfo); // Store the HTTP request method with the cache entry so we can distinguish // for example GET and HEAD responses. rv = entry->SetMetaDataElement("request-method", mRequestHead.Method().get()); if (NS_FAILED(rv)) return rv; // Store the HTTP authorization scheme used if any... rv = StoreAuthorizationMetaData(entry); 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 not to store "Cookie" headers though. 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. { nsCAutoString buf, metaKey; mResponseHead->GetHeader(nsHttp::Vary, buf); if (!buf.IsEmpty()) { NS_NAMED_LITERAL_CSTRING(prefix, "request-"); char *val = buf.BeginWriting(); // going to munge buf char *token = nsCRT::strtok(val, NS_HTTP_HEADER_SEPS, &val); while (token) { if ((*token != '*') && (PL_strcasecmp(token, "cookie") != 0)) { nsHttpAtom atom = nsHttp::ResolveAtom(token); const char *requestVal = mRequestHead.PeekHeader(atom); if (requestVal) { // build cache meta data key and set meta data element... metaKey = prefix + nsDependentCString(token); entry->SetMetaDataElement(metaKey.get(), requestVal); } } token = nsCRT::strtok(val, NS_HTTP_HEADER_SEPS, &val); } } } // Store the received HTTP head with the cache entry as an element of // the meta data. nsCAutoString head; mResponseHead->Flatten(head, PR_TRUE); rv = entry->SetMetaDataElement("response-head", head.get()); return rv; } inline void GetAuthType(const char *challenge, nsCString &authType) { const char *p; // get the challenge type if ((p = strchr(challenge, ' ')) != nsnull) authType.Assign(challenge, p - challenge); else authType.Assign(challenge); } nsresult nsHttpChannel::StoreAuthorizationMetaData(nsICacheEntryDescriptor *entry) { // Not applicable to proxy authorization... const char *val = mRequestHead.PeekHeader(nsHttp::Authorization); if (!val) return NS_OK; // eg. [Basic realm="wally world"] nsCAutoString buf; GetAuthType(val, 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)); if (mResponseHead && mResponseHeadersModified) { // 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(PRUint32 offset) { nsresult rv; LOG(("Preparing to write data into the cache [uri=%s]\n", mSpec.get())); NS_ASSERTION(mCacheEntry, "no cache entry"); NS_ASSERTION(mListener, "no listener"); nsCOMPtr out; rv = mCacheEntry->OpenOutputStream(offset, getter_AddRefs(out)); if (NS_FAILED(rv)) return rv; // 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; nsCacheStoragePolicy policy; rv = mCacheEntry->GetStoragePolicy(&policy); if (!gHttpHandler->mCacheWriteThread || NS_FAILED(rv) || policy == nsICache::STORE_ON_DISK_AS_FILE) { LOG(("nsHttpChannel::InstallCacheListener sync tee %p\n", tee.get())); rv = tee->Init(mListener, out, nsnull); } else { LOG(("nsHttpChannel::InstallCacheListener async tee %p\n", tee.get())); rv = tee->InitAsync(mListener, gHttpHandler->mCacheWriteThread, out, nsnull); } if (NS_FAILED(rv)) return rv; mListener = tee; return NS_OK; } nsresult nsHttpChannel::InstallOfflineCacheListener() { nsresult rv; LOG(("Preparing to write data into the offline cache [uri=%s]\n", mSpec.get())); NS_ASSERTION(mOfflineCacheEntry, "no offline cache entry"); NS_ASSERTION(mListener, "no listener"); nsCOMPtr out; rv = mOfflineCacheEntry->OpenOutputStream(0, getter_AddRefs(out)); if (NS_FAILED(rv)) return rv; nsCOMPtr tee = do_CreateInstance(kStreamListenerTeeCID, &rv); if (NS_FAILED(rv)) return rv; rv = tee->Init(mListener, out, nsnull); if (NS_FAILED(rv)) return rv; mListener = tee; return NS_OK; } void nsHttpChannel::ClearBogusContentEncodingIfNeeded() { // For .gz files, apache sends both a Content-Type: application/x-gzip // as well as Content-Encoding: gzip, which is completely wrong. In // this case, we choose to ignore the rogue Content-Encoding header. We // must do this early on so as to prevent it from being seen up stream. // The same problem exists for Content-Encoding: compress in default // Apache installs. if (mResponseHead->HasHeaderValue(nsHttp::Content_Encoding, "gzip") && ( mResponseHead->ContentType().EqualsLiteral(APPLICATION_GZIP) || mResponseHead->ContentType().EqualsLiteral(APPLICATION_GZIP2) || mResponseHead->ContentType().EqualsLiteral(APPLICATION_GZIP3))) { // clear the Content-Encoding header mResponseHead->ClearHeader(nsHttp::Content_Encoding); } else if (mResponseHead->HasHeaderValue(nsHttp::Content_Encoding, "compress") && ( mResponseHead->ContentType().EqualsLiteral(APPLICATION_COMPRESS) || mResponseHead->ContentType().EqualsLiteral(APPLICATION_COMPRESS2))) { // clear the Content-Encoding header mResponseHead->ClearHeader(nsHttp::Content_Encoding); } } //----------------------------------------------------------------------------- // nsHttpChannel //----------------------------------------------------------------------------- static PLDHashOperator CopyProperties(const nsAString& aKey, nsIVariant *aData, void *aClosure) { nsIWritablePropertyBag* bag = static_cast (aClosure); bag->SetProperty(aKey, aData); return PL_DHASH_NEXT; } nsresult nsHttpChannel::SetupReplacementChannel(nsIURI *newURI, nsIChannel *newChannel, PRBool preserveMethod) { LOG(("nsHttpChannel::SetupReplacementChannel " "[this=%p newChannel=%p preserveMethod=%d]", this, newChannel, preserveMethod)); PRUint32 newLoadFlags = mLoadFlags | 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 (mConnectionInfo->UsingSSL()) newLoadFlags &= ~INHIBIT_PERSISTENT_CACHING; // Do not pass along LOAD_CHECK_OFFLINE_CACHE newLoadFlags &= ~LOAD_CHECK_OFFLINE_CACHE; newChannel->SetLoadGroup(mLoadGroup); newChannel->SetNotificationCallbacks(mCallbacks); newChannel->SetLoadFlags(newLoadFlags); nsCOMPtr httpChannel = do_QueryInterface(newChannel); if (!httpChannel) return NS_OK; // no other options to set if (preserveMethod) { nsCOMPtr uploadChannel = do_QueryInterface(httpChannel); nsCOMPtr uploadChannel2 = do_QueryInterface(httpChannel); if (mUploadStream && (uploadChannel2 || uploadChannel)) { // rewind upload stream nsCOMPtr seekable = do_QueryInterface(mUploadStream); if (seekable) seekable->Seek(nsISeekableStream::NS_SEEK_SET, 0); // replicate original call to SetUploadStream... if (uploadChannel2) { const char *ctype = mRequestHead.PeekHeader(nsHttp::Content_Type); if (!ctype) ctype = ""; const char *clen = mRequestHead.PeekHeader(nsHttp::Content_Length); PRInt64 len = clen ? nsCRT::atoll(clen) : -1; uploadChannel2->ExplicitSetUploadStream( mUploadStream, nsDependentCString(ctype), len, nsDependentCString(mRequestHead.Method()), mUploadStreamHasHeaders); } else { if (mUploadStreamHasHeaders) uploadChannel->SetUploadStream(mUploadStream, EmptyCString(), -1); else { const char *ctype = mRequestHead.PeekHeader(nsHttp::Content_Type); const char *clen = mRequestHead.PeekHeader(nsHttp::Content_Length); if (!ctype) { ctype = "application/octet-stream"; } if (clen) { uploadChannel->SetUploadStream(mUploadStream, nsDependentCString(ctype), atoi(clen)); } } } } // 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. httpChannel->SetRequestMethod(nsDependentCString(mRequestHead.Method())); } // convey the referrer if one was used for this channel to the next one if (mReferrer) httpChannel->SetReferrer(mReferrer); // convey the mAllowPipelining flag httpChannel->SetAllowPipelining(mAllowPipelining); // convey the new redirection limit httpChannel->SetRedirectionLimit(mRedirectionLimit - 1); nsCOMPtr httpInternal = do_QueryInterface(newChannel); if (httpInternal) { // convey the mForceAllowThirdPartyCookie flag httpInternal->SetForceAllowThirdPartyCookie(mForceAllowThirdPartyCookie); // 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)) httpInternal->SetDocumentURI(newURI); else httpInternal->SetDocumentURI(mDocumentURI); } // convey the mApplyConversion flag (bug 91862) nsCOMPtr encodedChannel = do_QueryInterface(httpChannel); if (encodedChannel) encodedChannel->SetApplyConversion(mApplyConversion); // transfer the resume information if (mResuming) { 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); } // transfer application cache information nsCOMPtr appCacheChannel = do_QueryInterface(newChannel); if (appCacheChannel) { appCacheChannel->SetApplicationCache(mApplicationCache); appCacheChannel->SetInheritApplicationCache(mInheritApplicationCache); // We purposely avoid transfering mChooseApplicationCache. } // transfer any properties nsCOMPtr bag(do_QueryInterface(newChannel)); if (bag) mPropertyHash.EnumerateRead(CopyProperties, bag.get()); return NS_OK; } nsresult nsHttpChannel::ProcessRedirection(PRUint32 redirectType) { LOG(("nsHttpChannel::ProcessRedirection [this=%p type=%u]\n", this, redirectType)); const char *location = mResponseHead->PeekHeader(nsHttp::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 (!location) return NS_ERROR_FAILURE; // make sure non-ASCII characters in the location header are escaped. nsCAutoString locationBuf; if (NS_EscapeURL(location, -1, esc_OnlyNonASCII, locationBuf)) location = locationBuf.get(); if (mRedirectionLimit == 0) { LOG(("redirection limit reached!\n")); // this error code is fatal, and should be conveyed to our listener. Cancel(NS_ERROR_REDIRECT_LOOP); return NS_ERROR_REDIRECT_LOOP; } LOG(("redirecting to: %s [redirection-limit=%u]\n", location, PRUint32(mRedirectionLimit))); nsresult rv; nsCOMPtr newChannel; nsCOMPtr newURI; // create a new URI using the location header and the current URL // as a base... nsCOMPtr ioService; rv = gHttpHandler->GetIOService(getter_AddRefs(ioService)); if (NS_FAILED(rv)) return rv; // the new uri should inherit the origin charset of the current uri nsCAutoString originCharset; rv = mURI->GetOriginCharset(originCharset); if (NS_FAILED(rv)) originCharset.Truncate(); rv = ioService->NewURI(nsDependentCString(location), originCharset.get(), mURI, getter_AddRefs(newURI)); if (NS_FAILED(rv)) return rv; if (mApplicationCache) { // if we are redirected to a different origin check if there is a fallback // cache entry to fall back to. we don't care about file strict // checking, at least mURI is not a file URI. if (!NS_SecurityCompareURIs(mURI, newURI, PR_FALSE)) { PRBool fallingBack; rv = ProcessFallback(&fallingBack); if (NS_SUCCEEDED(rv) && fallingBack) { // do not continue with redirect processing, fallback is in // progress now. return NS_OK; } } } // Kill the current cache entry if we are redirecting // back to ourself. PRBool redirectingBackToSameURI = PR_FALSE; if (mCacheEntry && (mCacheAccess & nsICache::ACCESS_WRITE) && NS_SUCCEEDED(mURI->Equals(newURI, &redirectingBackToSameURI)) && redirectingBackToSameURI) mCacheEntry->Doom(); // move the reference of the old location to the new one if the new // one has none. nsCOMPtr newURL = do_QueryInterface(newURI); if (newURL) { nsCAutoString ref; rv = newURL->GetRef(ref); if (NS_SUCCEEDED(rv) && ref.IsEmpty()) { nsCOMPtr baseURL(do_QueryInterface(mURI)); if (baseURL) { baseURL->GetRef(ref); if (!ref.IsEmpty()) newURL->SetRef(ref); } } } // if we need to re-send POST data then be sure to ask the user first. PRBool preserveMethod = (redirectType == 307); if (preserveMethod && mUploadStream) { rv = PromptTempRedirect(); if (NS_FAILED(rv)) return rv; } rv = ioService->NewChannelFromURI(newURI, getter_AddRefs(newChannel)); if (NS_FAILED(rv)) return rv; rv = SetupReplacementChannel(newURI, newChannel, preserveMethod); if (NS_FAILED(rv)) return rv; PRUint32 redirectFlags; if (redirectType == 301) // Moved Permanently redirectFlags = nsIChannelEventSink::REDIRECT_PERMANENT; else redirectFlags = nsIChannelEventSink::REDIRECT_TEMPORARY; // verify that this is a legal redirect rv = gHttpHandler->OnChannelRedirect(this, newChannel, redirectFlags); if (NS_FAILED(rv)) return rv; // Make sure to do this _after_ calling OnChannelRedirect newChannel->SetOriginalURI(mOriginalURI); // And now, the deprecated way nsCOMPtr httpEventSink; GetCallback(httpEventSink); if (httpEventSink) { // NOTE: nsIHttpEventSink is only used for compatibility with pre-1.8 // versions. rv = httpEventSink->OnRedirect(this, newChannel); if (NS_FAILED(rv)) return rv; } // 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 = newChannel->AsyncOpen(mListener, mListenerContext); if (NS_FAILED(rv)) return rv; // close down this channel Cancel(NS_BINDING_REDIRECTED); // disconnect from our listener mListener = 0; mListenerContext = 0; // and from our callbacks mCallbacks = nsnull; mProgressSink = nsnull; return NS_OK; } //----------------------------------------------------------------------------- // nsHttpChannel //----------------------------------------------------------------------------- // buf contains "domain\user" static void ParseUserDomain(PRUnichar *buf, const PRUnichar **user, const PRUnichar **domain) { PRUnichar *p = buf; while (*p && *p != '\\') ++p; if (!*p) return; *p = '\0'; *domain = buf; *user = p + 1; } // helper function for setting identity from raw user:pass static void SetIdent(nsHttpAuthIdentity &ident, PRUint32 authFlags, PRUnichar *userBuf, PRUnichar *passBuf) { const PRUnichar *user = userBuf; const PRUnichar *domain = nsnull; if (authFlags & nsIHttpAuthenticator::IDENTITY_INCLUDES_DOMAIN) ParseUserDomain(userBuf, &user, &domain); ident.Set(domain, user, passBuf); } // helper function for getting an auth prompt from an interface requestor static void GetAuthPrompt(nsIInterfaceRequestor *ifreq, PRBool proxyAuth, nsIAuthPrompt2 **result) { if (!ifreq) return; PRUint32 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 nsHttpChannel::GenCredsAndSetEntry(nsIHttpAuthenticator *auth, PRBool proxyAuth, const char *scheme, const char *host, PRInt32 port, const char *directory, const char *realm, const char *challenge, const nsHttpAuthIdentity &ident, nsCOMPtr &sessionState, char **result) { nsresult rv; PRUint32 authFlags; rv = auth->GetAuthFlags(&authFlags); if (NS_FAILED(rv)) return 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; } PRUint32 generateFlags; rv = auth->GenerateCredentials(this, challenge, proxyAuth, ident.Domain(), ident.User(), ident.Password(), &ss, &*continuationState, &generateFlags, result); sessionState.swap(ss); if (NS_FAILED(rv)) return rv; // don't log this in release build since it could contain sensitive info. #ifdef DEBUG LOG(("generated creds: %s\n", *result)); #endif // find out if this authenticator allows reuse of credentials and/or // challenge. PRBool saveCreds = 0 != (authFlags & nsIHttpAuthenticator::REUSABLE_CREDENTIALS); PRBool saveChallenge = 0 != (authFlags & nsIHttpAuthenticator::REUSABLE_CHALLENGE); PRBool saveIdentity = 0 == (generateFlags & nsIHttpAuthenticator::USING_INTERNAL_IDENTITY); // this getter never fails nsHttpAuthCache *authCache = gHttpHandler->AuthCache(); // 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 ? *result : nsnull, saveChallenge ? challenge : nsnull, saveIdentity ? &ident : nsnull, sessionState); return rv; } nsresult nsHttpChannel::ProcessAuthentication(PRUint32 httpStatus) { LOG(("nsHttpChannel::ProcessAuthentication [this=%p code=%u]\n", this, httpStatus)); if (mLoadFlags & LOAD_ANONYMOUS) { return NS_ERROR_NOT_AVAILABLE; } const char *challenges; mProxyAuth = (httpStatus == 407); nsresult 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 inadvertantly 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 (!mConnectionInfo->UsingHttpProxy()) { LOG(("rejecting 407 when proxy server not configured!\n")); return NS_ERROR_UNEXPECTED; } if (mConnectionInfo->UsingSSL() && !mTransaction->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; } challenges = mResponseHead->PeekHeader(nsHttp::Proxy_Authenticate); } else challenges = mResponseHead->PeekHeader(nsHttp::WWW_Authenticate); NS_ENSURE_TRUE(challenges, NS_ERROR_UNEXPECTED); nsCAutoString creds; rv = GetCredentials(challenges, mProxyAuth, creds); if (rv == NS_ERROR_IN_PROGRESS) { // authentication prompt has been invoked and result // is expected asynchronously mAuthRetryPending = PR_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(); return NS_OK; } else if (NS_FAILED(rv)) LOG(("unable to authenticate\n")); else { // set the authentication credentials if (mProxyAuth) mRequestHead.SetHeader(nsHttp::Proxy_Authorization, creds); else mRequestHead.SetHeader(nsHttp::Authorization, creds); mAuthRetryPending = PR_TRUE; // see DoAuthRetry } return rv; } nsresult nsHttpChannel::PrepareForAuthentication(PRBool proxyAuth) { LOG(("nsHttpChannel::PrepareForAuthentication [this=%p]\n", this)); 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 (!mConnectionInfo->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). nsCAutoString contractId; contractId.Assign(NS_HTTP_AUTHENTICATOR_CONTRACTID_PREFIX); contractId.Append(mProxyAuthType); nsresult rv; nsCOMPtr precedingAuth = do_GetService(contractId.get(), &rv); if (NS_FAILED(rv)) return rv; PRUint32 precedingAuthFlags; rv = precedingAuth->GetAuthFlags(&precedingAuthFlags); if (NS_FAILED(rv)) return rv; if (!(precedingAuthFlags & nsIHttpAuthenticator::REQUEST_BASED)) { const char *challenges = mResponseHead->PeekHeader(nsHttp::Proxy_Authenticate); if (!challenges) { // delete the proxy authorization header because we weren't // asked to authenticate mRequestHead.ClearHeader(nsHttp::Proxy_Authorization); LOG((" cleared proxy authorization header")); } } return NS_OK; } nsresult nsHttpChannel::GetCredentials(const char *challenges, PRBool proxyAuth, nsAFlatCString &creds) { nsCOMPtr auth; nsCAutoString challenge; nsCString authType; // force heap allocation to enable string sharing since // we'll be assigning this value into mAuthType. // set informations that depend on whether we're authenticating against a // proxy or a webserver nsISupports **currentContinuationState; nsCString *currentAuthType; if (proxyAuth) { currentContinuationState = &mProxyAuthContinuationState; currentAuthType = &mProxyAuthType; } else { currentContinuationState = &mAuthContinuationState; currentAuthType = &mAuthType; } nsresult rv = NS_ERROR_NOT_AVAILABLE; PRBool gotCreds = PR_FALSE; // figure out which challenge we can handle and which authenticator to use. for (const char *eol = challenges - 1; eol; ) { const char *p = eol + 1; // get the challenge string (LF separated -- see nsHttpHeaderArray) if ((eol = strchr(p, '\n')) != nsnull) challenge.Assign(p, eol - p); else challenge.Assign(p); rv = GetAuthenticator(challenge.get(), authType, getter_AddRefs(auth)); if (NS_SUCCEEDED(rv)) { // // if we've already selected an auth type from a previous challenge // received while processing this channel, then skip others until // we find a challenge corresponding to the previously tried auth // type. // if (!currentAuthType->IsEmpty() && authType != *currentAuthType) continue; // // we allow the routines to run all the way through before we // decide if they are valid. // // we don't worry about the auth cache being altered because that // would have been the last step, and if the error is from updating // the authcache it wasn't really altered anyway. -CTN // // at this point the code is really only useful for client side // errors (it will not automatically fail over to do a different // auth type if the server keeps rejecting what is being sent, even // if a particular auth method only knows 1 thing, like a // non-identity based authentication method) // rv = GetCredentialsForChallenge(challenge.get(), authType.get(), proxyAuth, auth, creds); if (NS_SUCCEEDED(rv)) { gotCreds = PR_TRUE; *currentAuthType = authType; break; } else if (rv == NS_ERROR_IN_PROGRESS) { // authentication prompt has been invoked and result is // expected asynchronously, save current challenge being // processed and all remaining challenges to use later in // OnAuthAvailable and now immediately return mCurrentChallenge = challenge; mRemainingChallenges = eol ? eol+1 : nsnull; 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 nsHttpChannel::GetAuthorizationMembers(PRBool proxyAuth, nsCSubstring& scheme, const char*& host, PRInt32& port, nsCSubstring& path, nsHttpAuthIdentity*& ident, nsISupports**& continuationState) { if (proxyAuth) { NS_ASSERTION (mConnectionInfo->UsingHttpProxy(), "proxyAuth is true, but no HTTP proxy is configured!"); host = mConnectionInfo->ProxyHost(); port = mConnectionInfo->ProxyPort(); ident = &mProxyIdent; scheme.AssignLiteral("http"); continuationState = &mProxyAuthContinuationState; } else { host = mConnectionInfo->Host(); port = mConnectionInfo->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 nsHttpChannel::GetCredentialsForChallenge(const char *challenge, const char *authType, PRBool proxyAuth, nsIHttpAuthenticator *auth, nsAFlatCString &creds) { LOG(("nsHttpChannel::GetCredentialsForChallenge [this=%p proxyAuth=%d challenges=%s]\n", this, proxyAuth, challenge)); // this getter never fails nsHttpAuthCache *authCache = gHttpHandler->AuthCache(); PRUint32 authFlags; nsresult rv = auth->GetAuthFlags(&authFlags); if (NS_FAILED(rv)) return rv; nsCAutoString realm; ParseRealm(challenge, realm); // if no realm, then use the auth type as the realm. ToUpperCase so the // ficticious realm stands out a bit more. // XXX this will cause some single signon misses! // XXX this was meant to be used with NTLM, which supplies no realm. /* if (realm.IsEmpty()) { realm = authType; ToUpperCase(realm); } */ // set informations that depend on whether // we're authenticating against a proxy // or a webserver const char *host; PRInt32 port; nsHttpAuthIdentity *ident; nsCAutoString path, scheme; PRBool identFromURI = PR_FALSE; nsISupports **continuationState; rv = GetAuthorizationMembers(proxyAuth, scheme, host, port, path, ident, continuationState); if (NS_FAILED(rv)) return rv; if (!proxyAuth) { // 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 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 = nsnull; authCache->GetAuthEntryForDomain(scheme.get(), host, port, realm.get(), &entry); // hold reference to the auth session state (in case we clear our // reference to the entry). nsCOMPtr sessionStateGrip; if (entry) sessionStateGrip = entry->mMetaData; // for digest auth, maybe our cached nonce value simply timed out... PRBool identityInvalid; nsISupports *sessionState = sessionStateGrip; rv = auth->ChallengeReceived(this, challenge, proxyAuth, &sessionState, &*continuationState, &identityInvalid); sessionStateGrip.swap(sessionState); if (NS_FAILED(rv)) return rv; LOG((" identity invalid = %d\n", identityInvalid)); if (identityInvalid) { if (entry) { if (ident->Equals(entry->Identity())) { LOG((" clearing bad auth cache entry\n")); // ok, we've already tried this user identity, so clear the // corresponding entry from the auth cache. authCache->ClearAuthEntry(scheme.get(), host, port, realm.get()); entry = nsnull; ident->Clear(); } else if (!identFromURI || nsCRT::strcmp(ident->User(), entry->Identity().User()) == 0) { 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->Set(entry->Identity()); identFromURI = PR_FALSE; if (entry->Creds()[0] != '\0') { LOG((" using cached credentials!\n")); creds.Assign(entry->Creds()); return entry->AddPath(path.get()); } } } else if (!identFromURI) { // hmm... identity invalid, but no auth entry! the realm probably // changed (see bug 201986). ident->Clear(); } if (!entry && ident->IsEmpty()) { PRUint32 level = nsIAuthPrompt2::LEVEL_NONE; if (scheme.EqualsLiteral("https")) level = nsIAuthPrompt2::LEVEL_SECURE; else if (authFlags & nsIHttpAuthenticator::IDENTITY_ENCRYPTED) level = nsIAuthPrompt2::LEVEL_PW_ENCRYPTED; // at this point we are forced to interact with the user to get // their username and password for this domain. rv = PromptForIdentity(level, proxyAuth, realm.get(), authType, authFlags, *ident); if (NS_FAILED(rv)) return rv; identFromURI = PR_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(NS_LITERAL_STRING("AutomaticAuth"), PR_FALSE)) { // calling cancel here sets our mStatus and aborts the HTTP // transaction, which prevents OnDataAvailable events. Cancel(NS_ERROR_ABORT); // 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. // nsXPIDLCString result; rv = GenCredsAndSetEntry(auth, proxyAuth, scheme.get(), host, port, path.get(), realm.get(), challenge, *ident, sessionStateGrip, getter_Copies(result)); if (NS_SUCCEEDED(rv)) creds = result; return rv; } nsresult nsHttpChannel::GetAuthenticator(const char *challenge, nsCString &authType, nsIHttpAuthenticator **auth) { LOG(("nsHttpChannel::GetAuthenticator [this=%p]\n", this)); GetAuthType(challenge, authType); // normalize to lowercase ToLowerCase(authType); nsCAutoString contractid; contractid.Assign(NS_HTTP_AUTHENTICATOR_CONTRACTID_PREFIX); contractid.Append(authType); return CallGetService(contractid.get(), auth); } void nsHttpChannel::GetIdentityFromURI(PRUint32 authFlags, nsHttpAuthIdentity &ident) { LOG(("nsHttpChannel::GetIdentityFromURI [this=%p]\n", this)); nsAutoString userBuf; nsAutoString passBuf; // XXX i18n nsCAutoString buf; mURI->GetUsername(buf); if (!buf.IsEmpty()) { NS_UnescapeURL(buf); CopyASCIItoUTF16(buf, userBuf); mURI->GetPassword(buf); if (!buf.IsEmpty()) { NS_UnescapeURL(buf); CopyASCIItoUTF16(buf, passBuf); } } if (!userBuf.IsEmpty()) SetIdent(ident, authFlags, (PRUnichar *) userBuf.get(), (PRUnichar *) passBuf.get()); } void nsHttpChannel::ParseRealm(const char *challenge, nsACString &realm) { // // From RFC2617 section 1.2, the realm value is defined as such: // // realm = "realm" "=" realm-value // realm-value = quoted-string // // but, we'll accept anything after the the "=" up to the first space, or // end-of-line, if the string is not quoted. // const char *p = PL_strcasestr(challenge, "realm="); if (p) { PRBool has_quote = PR_FALSE; p += 6; if (*p == '"') { has_quote = PR_TRUE; p++; } const char *end = p; while (*end && has_quote) { // Loop through all the string characters to find the closing // quote, ignoring escaped quotes. if (*end == '"' && end[-1] != '\\') break; ++end; } if (!has_quote) end = strchr(p, ' '); if (end) realm.Assign(p, end - p); else realm.Assign(p); } } class nsHTTPAuthInformation : public nsAuthInformationHolder { public: nsHTTPAuthInformation(PRUint32 aFlags, const nsString& aRealm, const nsCString& aAuthType) : nsAuthInformationHolder(aFlags, aRealm, aAuthType) {} void SetToHttpAuthIdentity(PRUint32 authFlags, nsHttpAuthIdentity& identity); }; void nsHTTPAuthInformation::SetToHttpAuthIdentity(PRUint32 authFlags, nsHttpAuthIdentity& identity) { identity.Set(Domain().get(), User().get(), Password().get()); } nsresult nsHttpChannel::PromptForIdentity(PRUint32 level, PRBool proxyAuth, const char *realm, const char *authType, PRUint32 authFlags, nsHttpAuthIdentity &ident) { LOG(("nsHttpChannel::PromptForIdentity [this=%p]\n", this)); nsCOMPtr authPrompt; GetAuthPrompt(mCallbacks, proxyAuth, getter_AddRefs(authPrompt)); if (!authPrompt && mLoadGroup) { nsCOMPtr cbs; mLoadGroup->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); nsresult rv; // prompt the user... PRUint32 promptFlags = 0; if (proxyAuth) { promptFlags |= nsIAuthInformation::AUTH_PROXY; if (mTriedProxyAuth) promptFlags |= nsIAuthInformation::PREVIOUS_FAILED; mTriedProxyAuth = PR_TRUE; } else { promptFlags |= nsIAuthInformation::AUTH_HOST; if (mTriedHostAuth) promptFlags |= nsIAuthInformation::PREVIOUS_FAILED; mTriedHostAuth = PR_TRUE; } if (authFlags & nsIHttpAuthenticator::IDENTITY_INCLUDES_DOMAIN) promptFlags |= nsIAuthInformation::NEED_DOMAIN; nsRefPtr holder = new nsHTTPAuthInformation(promptFlags, realmU, nsDependentCString(authType)); if (!holder) return NS_ERROR_OUT_OF_MEMORY; rv = authPrompt->AsyncPromptAuth(this, this, nsnull, 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 PRBool retval = PR_FALSE; rv = authPrompt->PromptAuth(this, 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 = PR_TRUE; return rv; } NS_IMETHODIMP nsHttpChannel::OnAuthAvailable(nsISupports *aContext, nsIAuthInformation *aAuthInfo) { LOG(("nsHttpChannel::OnAuthAvailable [this=%p]", this)); mAsyncPromptAuthCancelable = nsnull; nsresult rv; const char *host; PRInt32 port; nsHttpAuthIdentity *ident; nsCAutoString path, scheme; nsISupports **continuationState; rv = GetAuthorizationMembers(mProxyAuth, scheme, host, port, path, ident, continuationState); if (NS_FAILED(rv)) OnAuthCancelled(aContext, PR_FALSE); nsCAutoString realm; ParseRealm(mCurrentChallenge.get(), realm); nsHttpAuthCache *authCache = gHttpHandler->AuthCache(); nsHttpAuthEntry *entry = nsnull; authCache->GetAuthEntryForDomain(scheme.get(), host, port, realm.get(), &entry); nsCOMPtr sessionStateGrip; if (entry) sessionStateGrip = entry->mMetaData; nsAuthInformationHolder* holder = static_cast(aAuthInfo); ident->Set(holder->Domain().get(), holder->User().get(), holder->Password().get()); nsCAutoString unused; nsCOMPtr auth; rv = GetAuthenticator(mCurrentChallenge.get(), unused, getter_AddRefs(auth)); if (NS_FAILED(rv)) { NS_ASSERTION(PR_FALSE, "GetAuthenticator failed"); OnAuthCancelled(aContext, PR_TRUE); return NS_OK; } nsXPIDLCString creds; rv = GenCredsAndSetEntry(auth, mProxyAuth, scheme.get(), host, port, path.get(), realm.get(), mCurrentChallenge.get(), *ident, sessionStateGrip, getter_Copies(creds)); mCurrentChallenge.Truncate(); if (NS_FAILED(rv)) { OnAuthCancelled(aContext, PR_TRUE); return NS_OK; } return ContinueOnAuthAvailable(creds); } NS_IMETHODIMP nsHttpChannel::OnAuthCancelled(nsISupports *aContext, PRBool userCancel) { LOG(("nsHttpChannel::OnAuthCancelled [this=%p]", this)); mAsyncPromptAuthCancelable = nsnull; if (userCancel) { if (!mRemainingChallenges.IsEmpty()) { // there are still some challenges to process, do so nsresult rv; nsCAutoString creds; rv = GetCredentials(mRemainingChallenges.get(), mProxyAuth, creds); if (NS_SUCCEEDED(rv)) { // GetCredentials loaded the credentials from the cache or // some other way in a synchronous manner, process those // credentials now mRemainingChallenges.Truncate(); return ContinueOnAuthAvailable(creds); } else 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(); // ensure call of OnStartRequest of the current listener here, // it would not be called otherwise at all nsresult rv = CallOnStartRequest(); // drop mAuthRetryPending flag and resume the transaction // this resumes load of the unauthenticated content data mAuthRetryPending = PR_FALSE; LOG(("Resuming the transaction, user cancelled the auth dialog")); mTransactionPump->Resume(); if (NS_FAILED(rv)) mTransactionPump->Cancel(rv); } return NS_OK; } nsresult nsHttpChannel::ContinueOnAuthAvailable(const nsCSubstring& creds) { if (mProxyAuth) mRequestHead.SetHeader(nsHttp::Proxy_Authorization, creds); else mRequestHead.SetHeader(nsHttp::Authorization, creds); // 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(); // setting mAuthRetryPending flag and resuming the transaction // triggers process of throwing away the unauthenticated data already // coming from the network mAuthRetryPending = PR_TRUE; LOG(("Resuming the transaction, we got credentials from user")); mTransactionPump->Resume(); return NS_OK; } PRBool nsHttpChannel::ConfirmAuth(const nsString &bundleKey, PRBool doYesNoPrompt) { // skip prompting the user if // 1) we've already prompted the user // 2) we're not a toplevel channel // 3) the userpass length is less than the "phishy" threshold if (mSuppressDefensiveAuth || !(mLoadFlags & LOAD_INITIAL_DOCUMENT_URI)) return PR_TRUE; nsresult rv; nsCAutoString userPass; rv = mURI->GetUserPass(userPass); if (NS_FAILED(rv) || (userPass.Length() < gHttpHandler->PhishyUserPassLength())) return PR_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 PR_TRUE; nsCOMPtr bundle; bundleService->CreateBundle(NECKO_MSGS_URL, getter_AddRefs(bundle)); if (!bundle) return PR_TRUE; nsCAutoString host; rv = mURI->GetHost(host); if (NS_FAILED(rv)) return PR_TRUE; nsCAutoString user; rv = mURI->GetUsername(user); if (NS_FAILED(rv)) return PR_TRUE; NS_ConvertUTF8toUTF16 ucsHost(host), ucsUser(user); const PRUnichar *strs[2] = { ucsHost.get(), ucsUser.get() }; nsXPIDLString msg; bundle->FormatStringFromName(bundleKey.get(), strs, 2, getter_Copies(msg)); if (!msg) return PR_TRUE; nsCOMPtr prompt; GetCallback(prompt); if (!prompt) return PR_TRUE; // do not prompt again mSuppressDefensiveAuth = PR_TRUE; PRBool confirmed; if (doYesNoPrompt) { PRInt32 choice; PRBool checkState; rv = prompt->ConfirmEx(nsnull, msg, nsIPrompt::BUTTON_POS_1_DEFAULT + nsIPrompt::STD_YES_NO_BUTTONS, nsnull, nsnull, nsnull, nsnull, &checkState, &choice); if (NS_FAILED(rv)) return PR_TRUE; confirmed = choice == 0; } else { rv = prompt->Confirm(nsnull, msg, &confirmed); if (NS_FAILED(rv)) return PR_TRUE; } return confirmed; } void nsHttpChannel::CheckForSuperfluousAuth() { // 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 (!mAuthRetryPending) { // ask user... if (!ConfirmAuth(NS_LITERAL_STRING("SuperfluousAuth"), PR_TRUE)) { // calling cancel here sets our mStatus and aborts the HTTP // transaction, which prevents OnDataAvailable events. Cancel(NS_ERROR_ABORT); } } } void nsHttpChannel::SetAuthorizationHeader(nsHttpAuthCache *authCache, nsHttpAtom header, const char *scheme, const char *host, PRInt32 port, const char *path, nsHttpAuthIdentity &ident) { nsHttpAuthEntry *entry = nsnull; nsresult rv; // set informations that depend on whether // we're authenticating against a proxy // or a webserver nsISupports **continuationState; if (header == nsHttp::Proxy_Authorization) { continuationState = &mProxyAuthContinuationState; } else { continuationState = &mAuthContinuationState; } rv = authCache->GetAuthEntryForPath(scheme, host, port, path, &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. if (nsCRT::strcmp(ident.User(), entry->User()) == 0) ident.Clear(); } PRBool identFromURI; if (ident.IsEmpty()) { ident.Set(entry->Identity()); identFromURI = PR_FALSE; } else identFromURI = PR_TRUE; nsXPIDLCString temp; const char *creds = entry->Creds(); const char *challenge = entry->Challenge(); // we can only send a preemptive Authorization header if we have either // stored credentials or a stored challenge from which to derive // credentials. if the identity is from the URI, then we cannot use // the stored credentials. if ((!creds[0] || identFromURI) && challenge[0]) { nsCOMPtr auth; nsCAutoString unused; rv = GetAuthenticator(challenge, unused, getter_AddRefs(auth)); if (NS_SUCCEEDED(rv)) { PRBool proxyAuth = (header == nsHttp::Proxy_Authorization); rv = GenCredsAndSetEntry(auth, proxyAuth, scheme, host, port, path, entry->Realm(), challenge, ident, entry->mMetaData, getter_Copies(temp)); if (NS_SUCCEEDED(rv)) creds = temp.get(); // make sure the continuation state is null since we do not // support mixing preemptive and 'multirequest' authentication. NS_IF_RELEASE(*continuationState); } } if (creds[0]) { LOG((" adding \"%s\" request header\n", header.get())); mRequestHead.SetHeader(header, nsDependentCString(creds)); // 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 = PR_TRUE; } else ident.Clear(); // don't remember the identity } } void nsHttpChannel::AddAuthorizationHeaders() { LOG(("nsHttpChannel::AddAuthorizationHeaders? [this=%p]\n", this)); if (mLoadFlags & LOAD_ANONYMOUS) { return; } // this getter never fails nsHttpAuthCache *authCache = gHttpHandler->AuthCache(); // check if proxy credentials should be sent const char *proxyHost = mConnectionInfo->ProxyHost(); if (proxyHost && mConnectionInfo->UsingHttpProxy()) SetAuthorizationHeader(authCache, nsHttp::Proxy_Authorization, "http", proxyHost, mConnectionInfo->ProxyPort(), nsnull, // proxy has no path mProxyIdent); // check if server credentials should be sent nsCAutoString path, scheme; if (NS_SUCCEEDED(GetCurrentPath(path)) && NS_SUCCEEDED(mURI->GetScheme(scheme))) { SetAuthorizationHeader(authCache, nsHttp::Authorization, scheme.get(), mConnectionInfo->Host(), mConnectionInfo->Port(), path.get(), mIdent); } } nsresult nsHttpChannel::GetCurrentPath(nsACString &path) { nsresult rv; nsCOMPtr url = do_QueryInterface(mURI); if (url) rv = url->GetDirectory(path); else rv = mURI->GetPath(path); return rv; } //----------------------------------------------------------------------------- // nsHttpChannel::nsISupports //----------------------------------------------------------------------------- NS_IMPL_ADDREF_INHERITED(nsHttpChannel, nsHashPropertyBag) NS_IMPL_RELEASE_INHERITED(nsHttpChannel, nsHashPropertyBag) 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(nsICachingChannel) NS_INTERFACE_MAP_ENTRY(nsIUploadChannel) NS_INTERFACE_MAP_ENTRY(nsIUploadChannel2) NS_INTERFACE_MAP_ENTRY(nsICacheListener) NS_INTERFACE_MAP_ENTRY(nsIEncodedChannel) 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(nsITraceableChannel) NS_INTERFACE_MAP_ENTRY(nsIApplicationCacheContainer) NS_INTERFACE_MAP_ENTRY(nsIApplicationCacheChannel) NS_INTERFACE_MAP_ENTRY(nsIAuthPromptCallback) NS_INTERFACE_MAP_END_INHERITING(nsHashPropertyBag) //----------------------------------------------------------------------------- // nsHttpChannel::nsIRequest //----------------------------------------------------------------------------- NS_IMETHODIMP nsHttpChannel::GetName(nsACString &aName) { aName = mSpec; return NS_OK; } NS_IMETHODIMP nsHttpChannel::IsPending(PRBool *value) { NS_ENSURE_ARG_POINTER(value); *value = mIsPending; return NS_OK; } NS_IMETHODIMP nsHttpChannel::GetStatus(nsresult *aStatus) { NS_ENSURE_ARG_POINTER(aStatus); *aStatus = mStatus; return NS_OK; } NS_IMETHODIMP nsHttpChannel::Cancel(nsresult status) { LOG(("nsHttpChannel::Cancel [this=%p status=%x]\n", this, status)); if (mCanceled) { LOG((" ignoring; already canceled\n")); return NS_OK; } mCanceled = PR_TRUE; mStatus = status; if (mProxyRequest) mProxyRequest->Cancel(status); if (mTransaction) gHttpHandler->CancelTransaction(mTransaction, status); if (mTransactionPump) mTransactionPump->Cancel(status); if (mCachePump) mCachePump->Cancel(status); if (mAsyncPromptAuthCancelable) mAsyncPromptAuthCancelable->Cancel(status); return NS_OK; } NS_IMETHODIMP nsHttpChannel::Suspend() { NS_ENSURE_TRUE(mIsPending, NS_ERROR_NOT_AVAILABLE); LOG(("nsHttpChannel::Suspend [this=%p]\n", this)); ++mSuspendCount; if (mTransactionPump) return mTransactionPump->Suspend(); if (mCachePump) return mCachePump->Suspend(); return NS_OK; } NS_IMETHODIMP nsHttpChannel::Resume() { NS_ENSURE_TRUE(mSuspendCount > 0, NS_ERROR_UNEXPECTED); LOG(("nsHttpChannel::Resume [this=%p]\n", this)); if (--mSuspendCount == 0 && mPendingAsyncCallOnResume) { nsresult rv = AsyncCall(mPendingAsyncCallOnResume); mPendingAsyncCallOnResume = nsnull; NS_ENSURE_SUCCESS(rv, rv); } if (mTransactionPump) return mTransactionPump->Resume(); if (mCachePump) return mCachePump->Resume(); return NS_OK; } NS_IMETHODIMP nsHttpChannel::GetLoadGroup(nsILoadGroup **aLoadGroup) { NS_ENSURE_ARG_POINTER(aLoadGroup); *aLoadGroup = mLoadGroup; NS_IF_ADDREF(*aLoadGroup); return NS_OK; } NS_IMETHODIMP nsHttpChannel::SetLoadGroup(nsILoadGroup *aLoadGroup) { mLoadGroup = aLoadGroup; mProgressSink = nsnull; return NS_OK; } NS_IMETHODIMP nsHttpChannel::GetLoadFlags(nsLoadFlags *aLoadFlags) { NS_ENSURE_ARG_POINTER(aLoadFlags); *aLoadFlags = mLoadFlags; return NS_OK; } NS_IMETHODIMP nsHttpChannel::SetLoadFlags(nsLoadFlags aLoadFlags) { mLoadFlags = aLoadFlags; return NS_OK; } //----------------------------------------------------------------------------- // nsHttpChannel::nsIChannel //----------------------------------------------------------------------------- NS_IMETHODIMP nsHttpChannel::GetOriginalURI(nsIURI **originalURI) { NS_ENSURE_ARG_POINTER(originalURI); *originalURI = mOriginalURI; NS_ADDREF(*originalURI); return NS_OK; } NS_IMETHODIMP nsHttpChannel::SetOriginalURI(nsIURI *originalURI) { NS_ENSURE_ARG_POINTER(originalURI); mOriginalURI = originalURI; return NS_OK; } NS_IMETHODIMP nsHttpChannel::GetURI(nsIURI **URI) { NS_ENSURE_ARG_POINTER(URI); *URI = mURI; NS_IF_ADDREF(*URI); return NS_OK; } NS_IMETHODIMP nsHttpChannel::GetOwner(nsISupports **owner) { NS_ENSURE_ARG_POINTER(owner); *owner = mOwner; NS_IF_ADDREF(*owner); return NS_OK; } NS_IMETHODIMP nsHttpChannel::SetOwner(nsISupports *owner) { mOwner = owner; return NS_OK; } NS_IMETHODIMP nsHttpChannel::GetNotificationCallbacks(nsIInterfaceRequestor **callbacks) { NS_IF_ADDREF(*callbacks = mCallbacks); return NS_OK; } NS_IMETHODIMP nsHttpChannel::SetNotificationCallbacks(nsIInterfaceRequestor *callbacks) { mCallbacks = callbacks; mProgressSink = nsnull; return NS_OK; } NS_IMETHODIMP nsHttpChannel::GetSecurityInfo(nsISupports **securityInfo) { NS_ENSURE_ARG_POINTER(securityInfo); *securityInfo = mSecurityInfo; NS_IF_ADDREF(*securityInfo); return NS_OK; } NS_IMETHODIMP nsHttpChannel::GetContentType(nsACString &value) { if (!mResponseHead) { // We got no data, we got no headers, we got nothing value.Truncate(); return NS_ERROR_NOT_AVAILABLE; } if (!mResponseHead->ContentType().IsEmpty()) { value = mResponseHead->ContentType(); return NS_OK; } value.AssignLiteral(UNKNOWN_CONTENT_TYPE); return NS_OK; } NS_IMETHODIMP nsHttpChannel::SetContentType(const nsACString &value) { if (mListener || mWasOpened) { if (!mResponseHead) return NS_ERROR_NOT_AVAILABLE; nsCAutoString contentTypeBuf, charsetBuf; PRBool hadCharset; net_ParseContentType(value, 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. PRBool dummy; net_ParseContentType(value, mContentTypeHint, mContentCharsetHint, &dummy); } return NS_OK; } NS_IMETHODIMP nsHttpChannel::GetContentCharset(nsACString &value) { if (!mResponseHead) return NS_ERROR_NOT_AVAILABLE; value = mResponseHead->ContentCharset(); return NS_OK; } NS_IMETHODIMP nsHttpChannel::SetContentCharset(const nsACString &value) { if (mListener) { if (!mResponseHead) return NS_ERROR_NOT_AVAILABLE; mResponseHead->SetContentCharset(value); } else { // Charset hint mContentCharsetHint = value; } return NS_OK; } NS_IMETHODIMP nsHttpChannel::GetContentLength(PRInt32 *value) { NS_ENSURE_ARG_POINTER(value); if (!mResponseHead) return NS_ERROR_NOT_AVAILABLE; // XXX truncates to 32 bit LL_L2I(*value, mResponseHead->ContentLength()); return NS_OK; } NS_IMETHODIMP nsHttpChannel::SetContentLength(PRInt32 value) { NS_NOTYETIMPLEMENTED("nsHttpChannel::SetContentLength"); return NS_ERROR_NOT_IMPLEMENTED; } NS_IMETHODIMP nsHttpChannel::Open(nsIInputStream **_retval) { NS_ENSURE_TRUE(!mWasOpened, NS_ERROR_IN_PROGRESS); return NS_ImplementChannelOpen(this, _retval); } NS_IMETHODIMP nsHttpChannel::AsyncOpen(nsIStreamListener *listener, nsISupports *context) { LOG(("nsHttpChannel::AsyncOpen [this=%p]\n", this)); NS_ENSURE_ARG_POINTER(listener); NS_ENSURE_TRUE(!mIsPending, NS_ERROR_IN_PROGRESS); NS_ENSURE_TRUE(!mWasOpened, NS_ERROR_ALREADY_OPENED); nsresult rv; rv = NS_CheckPortSafety(mURI); if (NS_FAILED(rv)) return rv; if (!(mConnectionInfo && mConnectionInfo->UsingHttpProxy())) { // 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. nsRefPtr prefetch = new nsDNSPrefetch(mURI); if (prefetch) { prefetch->PrefetchHigh(); } } // Remember the cookie header that was set, if any const char *cookieHeader = mRequestHead.PeekHeader(nsHttp::Cookie); if (cookieHeader) mUserSetCookieHeader = cookieHeader; // fetch cookies, and add them to the request header AddCookiesToRequest(); // notify "http-on-modify-request" observers gHttpHandler->OnModifyRequest(this); // 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 | NS_HTTP_ALLOW_PIPELINING); if ((mLoadFlags & VALIDATE_ALWAYS) || (BYPASS_LOCAL_CACHE(mLoadFlags))) mCaps |= NS_HTTP_REFRESH_DNS; mIsPending = PR_TRUE; mWasOpened = PR_TRUE; mListener = listener; mListenerContext = context; // add ourselves to the load group. from this point forward, we'll report // all failures asynchronously. if (mLoadGroup) mLoadGroup->AddRequest(this, nsnull); // We may have been cancelled already, either by on-modify-request // listeners or by load group observers; in that case, we should // not send the request to the server if (mCanceled) rv = mStatus; else rv = Connect(); if (NS_FAILED(rv)) { LOG(("Calling AsyncAbort [rv=%x mCanceled=%i]\n", rv, mCanceled)); CloseCacheEntry(PR_TRUE); AsyncAbort(rv); } if (mLoadFlags & LOAD_CLASSIFY_URI) { nsRefPtr classifier = new nsChannelClassifier(); if (!classifier) { Cancel(NS_ERROR_OUT_OF_MEMORY); return NS_OK; } rv = classifier->Start(this); if (NS_FAILED(rv)) { Cancel(rv); } } return NS_OK; } //----------------------------------------------------------------------------- // nsHttpChannel::nsIHttpChannel //----------------------------------------------------------------------------- NS_IMETHODIMP nsHttpChannel::GetRequestMethod(nsACString &method) { method = mRequestHead.Method(); return NS_OK; } NS_IMETHODIMP nsHttpChannel::SetRequestMethod(const nsACString &method) { NS_ENSURE_TRUE(!mIsPending, NS_ERROR_IN_PROGRESS); const nsCString &flatMethod = PromiseFlatCString(method); // Method names are restricted to valid HTTP tokens. if (!nsHttp::IsValidToken(flatMethod)) return NS_ERROR_INVALID_ARG; nsHttpAtom atom = nsHttp::ResolveAtom(flatMethod.get()); if (!atom) return NS_ERROR_FAILURE; mRequestHead.SetMethod(atom); return NS_OK; } NS_IMETHODIMP nsHttpChannel::GetReferrer(nsIURI **referrer) { NS_ENSURE_ARG_POINTER(referrer); *referrer = mReferrer; NS_IF_ADDREF(*referrer); return NS_OK; } NS_IMETHODIMP nsHttpChannel::SetReferrer(nsIURI *referrer) { NS_ENSURE_TRUE(!mIsPending, NS_ERROR_IN_PROGRESS); // clear existing referrer, if any mReferrer = nsnull; mRequestHead.ClearHeader(nsHttp::Referer); if (!referrer) return NS_OK; // check referrer blocking pref PRUint32 referrerLevel; if (mLoadFlags & LOAD_INITIAL_DOCUMENT_URI) referrerLevel = 1; // user action else referrerLevel = 2; // inline content if (gHttpHandler->ReferrerLevel() < referrerLevel) return NS_OK; nsCOMPtr referrerGrip; nsresult rv; PRBool match; // // Strip off "wyciwyg://123/" from wyciwyg referrers. // // XXX this really belongs elsewhere since wyciwyg URLs aren't part of necko. // perhaps some sort of generic nsINestedURI could be used. then, if an URI // fails the whitelist test, then we could check for an inner URI and try // that instead. though, that might be too automatic. // rv = referrer->SchemeIs("wyciwyg", &match); if (NS_FAILED(rv)) return rv; if (match) { nsCAutoString path; rv = referrer->GetPath(path); if (NS_FAILED(rv)) return rv; PRUint32 pathLength = path.Length(); if (pathLength <= 2) return NS_ERROR_FAILURE; // Path is of the form "//123/http://foo/bar", with a variable number of digits. // To figure out where the "real" URL starts, search path for a '/', starting at // the third character. PRInt32 slashIndex = path.FindChar('/', 2); if (slashIndex == kNotFound) return NS_ERROR_FAILURE; // Get the charset of the original URI so we can pass it to our fixed up URI. nsCAutoString charset; referrer->GetOriginCharset(charset); // Replace |referrer| with a URI without wyciwyg://123/. rv = NS_NewURI(getter_AddRefs(referrerGrip), Substring(path, slashIndex + 1, pathLength - slashIndex - 1), charset.get()); if (NS_FAILED(rv)) return rv; referrer = referrerGrip.get(); } // // block referrer if not on our white list... // static const char *const referrerWhiteList[] = { "http", "https", "ftp", "gopher", nsnull }; match = PR_FALSE; const char *const *scheme = referrerWhiteList; for (; *scheme && !match; ++scheme) { rv = referrer->SchemeIs(*scheme, &match); if (NS_FAILED(rv)) return rv; } if (!match) return NS_OK; // kick out.... // // Handle secure referrals. // // Support referrals from a secure server if this is a secure site // and (optionally) if the host names are the same. // rv = referrer->SchemeIs("https", &match); if (NS_FAILED(rv)) return rv; if (match) { rv = mURI->SchemeIs("https", &match); if (NS_FAILED(rv)) return rv; if (!match) return NS_OK; if (!gHttpHandler->SendSecureXSiteReferrer()) { nsCAutoString referrerHost; nsCAutoString host; rv = referrer->GetAsciiHost(referrerHost); if (NS_FAILED(rv)) return rv; rv = mURI->GetAsciiHost(host); if (NS_FAILED(rv)) return rv; // GetAsciiHost returns lowercase hostname. if (!referrerHost.Equals(host)) return NS_OK; } } nsCOMPtr clone; // // we need to clone the referrer, so we can: // (1) modify it // (2) keep a reference to it after returning from this function // rv = referrer->Clone(getter_AddRefs(clone)); if (NS_FAILED(rv)) return rv; // strip away any userpass; we don't want to be giving out passwords ;-) clone->SetUserPass(EmptyCString()); // strip away any fragment per RFC 2616 section 14.36 nsCOMPtr url = do_QueryInterface(clone); if (url) url->SetRef(EmptyCString()); nsCAutoString spec; rv = clone->GetAsciiSpec(spec); if (NS_FAILED(rv)) return rv; // finally, remember the referrer URI and set the Referer header. mReferrer = clone; mRequestHead.SetHeader(nsHttp::Referer, spec); return NS_OK; } NS_IMETHODIMP nsHttpChannel::GetRequestHeader(const nsACString &header, nsACString &value) { // XXX might be better to search the header list directly instead of // hitting the http atom hash table. nsHttpAtom atom = nsHttp::ResolveAtom(header); if (!atom) return NS_ERROR_NOT_AVAILABLE; return mRequestHead.GetHeader(atom, value); } NS_IMETHODIMP nsHttpChannel::SetRequestHeader(const nsACString &header, const nsACString &value, PRBool merge) { NS_ENSURE_TRUE(!mIsPending, NS_ERROR_IN_PROGRESS); const nsCString &flatHeader = PromiseFlatCString(header); const nsCString &flatValue = PromiseFlatCString(value); LOG(("nsHttpChannel::SetRequestHeader [this=%p header=\"%s\" value=\"%s\" merge=%u]\n", this, flatHeader.get(), flatValue.get(), merge)); // Header names are restricted to valid HTTP tokens. if (!nsHttp::IsValidToken(flatHeader)) return NS_ERROR_INVALID_ARG; // 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. if (flatValue.FindCharInSet("\r\n") != kNotFound || flatValue.Length() != strlen(flatValue.get())) return NS_ERROR_INVALID_ARG; nsHttpAtom atom = nsHttp::ResolveAtom(flatHeader.get()); if (!atom) { NS_WARNING("failed to resolve atom"); return NS_ERROR_NOT_AVAILABLE; } return mRequestHead.SetHeader(atom, flatValue, merge); } NS_IMETHODIMP nsHttpChannel::VisitRequestHeaders(nsIHttpHeaderVisitor *visitor) { return mRequestHead.Headers().VisitHeaders(visitor); } NS_IMETHODIMP nsHttpChannel::GetUploadStream(nsIInputStream **stream) { NS_ENSURE_ARG_POINTER(stream); *stream = mUploadStream; NS_IF_ADDREF(*stream); return NS_OK; } NS_IMETHODIMP nsHttpChannel::SetUploadStream(nsIInputStream *stream, const nsACString &contentType, PRInt32 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) { if (!contentType.IsEmpty()) { if (contentLength < 0) { stream->Available((PRUint32 *) &contentLength); if (contentLength < 0) { NS_ERROR("unable to determine content length"); return NS_ERROR_FAILURE; } } mRequestHead.SetHeader(nsHttp::Content_Length, nsPrintfCString("%d", contentLength)); mRequestHead.SetHeader(nsHttp::Content_Type, contentType); mUploadStreamHasHeaders = PR_FALSE; mRequestHead.SetMethod(nsHttp::Put); // PUT request } else { mUploadStreamHasHeaders = PR_TRUE; mRequestHead.SetMethod(nsHttp::Post); // POST request } } else { mUploadStreamHasHeaders = PR_FALSE; mRequestHead.SetMethod(nsHttp::Get); // revert to GET request } mUploadStream = stream; return NS_OK; } NS_IMETHODIMP nsHttpChannel::ExplicitSetUploadStream(nsIInputStream *aStream, const nsACString &aContentType, PRInt64 aContentLength, const nsACString &aMethod, PRBool aStreamHasHeaders) { // Ensure stream is set and method is valid NS_ENSURE_TRUE(aStream, NS_ERROR_FAILURE); if (aContentLength < 0 && !aStreamHasHeaders) { PRUint32 streamLength; aStream->Available(&streamLength); aContentLength = streamLength; if (aContentLength < 0) { NS_ERROR("unable to determine content length"); return NS_ERROR_FAILURE; } } nsresult rv = SetRequestMethod(aMethod); NS_ENSURE_SUCCESS(rv, rv); if (!aStreamHasHeaders) { mRequestHead.SetHeader(nsHttp::Content_Length, nsPrintfCString("%lld", aContentLength)); mRequestHead.SetHeader(nsHttp::Content_Type, aContentType); } mUploadStreamHasHeaders = aStreamHasHeaders; mUploadStream = aStream; return NS_OK; } NS_IMETHODIMP nsHttpChannel::GetResponseStatus(PRUint32 *value) { NS_ENSURE_ARG_POINTER(value); if (!mResponseHead) return NS_ERROR_NOT_AVAILABLE; *value = mResponseHead->Status(); return NS_OK; } NS_IMETHODIMP nsHttpChannel::GetResponseStatusText(nsACString &value) { if (!mResponseHead) return NS_ERROR_NOT_AVAILABLE; value = mResponseHead->StatusText(); return NS_OK; } NS_IMETHODIMP nsHttpChannel::GetRequestSucceeded(PRBool *value) { NS_PRECONDITION(value, "Don't ever pass a null arg to this function"); if (!mResponseHead) return NS_ERROR_NOT_AVAILABLE; PRUint32 status = mResponseHead->Status(); *value = (status / 100 == 2); return NS_OK; } NS_IMETHODIMP nsHttpChannel::GetResponseHeader(const nsACString &header, nsACString &value) { 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 nsHttpChannel::SetResponseHeader(const nsACString &header, const nsACString &value, PRBool merge) { LOG(("nsHttpChannel::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; mResponseHeadersModified = PR_TRUE; return mResponseHead->SetHeader(atom, value, merge); } NS_IMETHODIMP nsHttpChannel::VisitResponseHeaders(nsIHttpHeaderVisitor *visitor) { if (!mResponseHead) return NS_ERROR_NOT_AVAILABLE; return mResponseHead->Headers().VisitHeaders(visitor); } NS_IMETHODIMP nsHttpChannel::IsNoStoreResponse(PRBool *value) { if (!mResponseHead) return NS_ERROR_NOT_AVAILABLE; *value = mResponseHead->NoStore(); return NS_OK; } NS_IMETHODIMP nsHttpChannel::IsNoCacheResponse(PRBool *value) { if (!mResponseHead) return NS_ERROR_NOT_AVAILABLE; *value = mResponseHead->NoCache(); if (!*value) *value = mResponseHead->ExpiresInPast(); return NS_OK; } NS_IMETHODIMP nsHttpChannel::GetApplyConversion(PRBool *value) { NS_ENSURE_ARG_POINTER(value); *value = mApplyConversion; return NS_OK; } NS_IMETHODIMP nsHttpChannel::SetApplyConversion(PRBool value) { LOG(("nsHttpChannel::SetApplyConversion [this=%p value=%d]\n", this, value)); mApplyConversion = value; return NS_OK; } NS_IMETHODIMP nsHttpChannel::GetAllowPipelining(PRBool *value) { NS_ENSURE_ARG_POINTER(value); *value = mAllowPipelining; return NS_OK; } NS_IMETHODIMP nsHttpChannel::SetAllowPipelining(PRBool value) { if (mIsPending) return NS_ERROR_FAILURE; mAllowPipelining = value; return NS_OK; } NS_IMETHODIMP nsHttpChannel::GetRedirectionLimit(PRUint32 *value) { NS_ENSURE_ARG_POINTER(value); *value = PRUint32(mRedirectionLimit); return NS_OK; } NS_IMETHODIMP nsHttpChannel::SetRedirectionLimit(PRUint32 value) { mRedirectionLimit = PR_MIN(value, 0xff); return NS_OK; } NS_IMETHODIMP nsHttpChannel::GetContentEncodings(nsIUTF8StringEnumerator** aEncodings) { NS_PRECONDITION(aEncodings, "Null out param"); if (!mResponseHead) { *aEncodings = nsnull; return NS_OK; } const char *encoding = mResponseHead->PeekHeader(nsHttp::Content_Encoding); if (!encoding) { *aEncodings = nsnull; return NS_OK; } nsContentEncodings* enumerator = new nsContentEncodings(this, encoding); if (!enumerator) return NS_ERROR_OUT_OF_MEMORY; NS_ADDREF(*aEncodings = enumerator); return NS_OK; } //----------------------------------------------------------------------------- // nsHttpChannel::nsIHttpChannelInternal //----------------------------------------------------------------------------- NS_IMETHODIMP nsHttpChannel::GetDocumentURI(nsIURI **aDocumentURI) { NS_ENSURE_ARG_POINTER(aDocumentURI); *aDocumentURI = mDocumentURI; NS_IF_ADDREF(*aDocumentURI); return NS_OK; } NS_IMETHODIMP nsHttpChannel::SetDocumentURI(nsIURI *aDocumentURI) { mDocumentURI = aDocumentURI; return NS_OK; } NS_IMETHODIMP nsHttpChannel::GetForceAllowThirdPartyCookie(PRBool *aForceAllowThirdPartyCookie) { *aForceAllowThirdPartyCookie = mForceAllowThirdPartyCookie; return NS_OK; } NS_IMETHODIMP nsHttpChannel::SetForceAllowThirdPartyCookie(PRBool aForceAllowThirdPartyCookie) { mForceAllowThirdPartyCookie = aForceAllowThirdPartyCookie; return NS_OK; } NS_IMETHODIMP nsHttpChannel::GetRequestVersion(PRUint32 *major, PRUint32 *minor) { int version = mRequestHead.Version(); if (major) { *major = version / 10; } if (minor) { *minor = version % 10; } return NS_OK; } NS_IMETHODIMP nsHttpChannel::GetResponseVersion(PRUint32 *major, PRUint32 *minor) { if (!mResponseHead) { *major = *minor = 0; // we should at least be kind about it return NS_ERROR_NOT_AVAILABLE; } int version = mResponseHead->Version(); if (major) { *major = version / 10; } if (minor) { *minor = version % 10; } return NS_OK; } NS_IMETHODIMP nsHttpChannel::SetCookie(const char *aCookieHeader) { if (mLoadFlags & LOAD_ANONYMOUS) { return NS_OK; } // empty header isn't an error if (!(aCookieHeader && *aCookieHeader)) return NS_OK; nsICookieService *cs = gHttpHandler->GetCookieService(); NS_ENSURE_TRUE(cs, NS_ERROR_FAILURE); nsCOMPtr prompt; GetCallback(prompt); return cs->SetCookieStringFromHttp(mURI, mDocumentURI ? mDocumentURI : mOriginalURI, prompt, aCookieHeader, mResponseHead->PeekHeader(nsHttp::Date), this); } NS_IMETHODIMP nsHttpChannel::SetupFallbackChannel(const char *aFallbackKey) { LOG(("nsHttpChannel::SetupFallbackChannel [this=%p, key=%s]", this, aFallbackKey)); mFallbackChannel = PR_TRUE; mFallbackKey = aFallbackKey; return NS_OK; } //----------------------------------------------------------------------------- // nsHttpChannel::nsISupportsPriority //----------------------------------------------------------------------------- NS_IMETHODIMP nsHttpChannel::GetPriority(PRInt32 *value) { *value = mPriority; return NS_OK; } NS_IMETHODIMP nsHttpChannel::SetPriority(PRInt32 value) { PRInt16 newValue = NS_CLAMP(value, PR_INT16_MIN, PR_INT16_MAX); if (mPriority == newValue) return NS_OK; mPriority = newValue; if (mTransaction) gHttpHandler->RescheduleTransaction(mTransaction, mPriority); return NS_OK; } NS_IMETHODIMP nsHttpChannel::AdjustPriority(PRInt32 delta) { return SetPriority(mPriority + delta); } //----------------------------------------------------------------------------- // nsHttpChannel::nsIProtocolProxyCallback //----------------------------------------------------------------------------- NS_IMETHODIMP nsHttpChannel::OnProxyAvailable(nsICancelable *request, nsIURI *uri, nsIProxyInfo *pi, nsresult status) { mProxyRequest = nsnull; // 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). // Need to replace this channel with a new one. It would be complex to try // to change the value of mConnectionInfo since so much of our state may // depend on its state. mTargetProxyInfo = pi; HandleAsyncReplaceWithProxy(); return NS_OK; } //----------------------------------------------------------------------------- // nsHttpChannel::nsIProxiedChannel //----------------------------------------------------------------------------- NS_IMETHODIMP nsHttpChannel::GetProxyInfo(nsIProxyInfo **result) { if (!mConnectionInfo) *result = nsnull; else { *result = mConnectionInfo->ProxyInfo(); NS_IF_ADDREF(*result); } return NS_OK; } //----------------------------------------------------------------------------- // nsHttpChannel::nsIRequestObserver //----------------------------------------------------------------------------- NS_IMETHODIMP nsHttpChannel::OnStartRequest(nsIRequest *request, nsISupports *ctxt) { if (!(mCanceled || NS_FAILED(mStatus))) { // capture the request's status, so our consumers will know ASAP of any // connection failures, etc - bug 93581 request->GetStatus(&mStatus); } LOG(("nsHttpChannel::OnStartRequest [this=%p request=%p status=%x]\n", this, request, mStatus)); // Make sure things are what we expect them to be... NS_ASSERTION(request == mCachePump || request == mTransactionPump, "Unexpected request"); NS_ASSERTION(!(mTransactionPump && mCachePump) || mCachedContentIsPartial, "If we have both pumps, the cache content must be partial"); if (!mSecurityInfo && !mCachePump && mTransaction) { // grab the security info from the connection object; the transaction // is guaranteed to own a reference to the connection. mSecurityInfo = mTransaction->SecurityInfo(); } // don't enter this block if we're reading from the cache... if (NS_SUCCEEDED(mStatus) && !mCachePump && mTransaction) { NS_ASSERTION(mResponseHead == nsnull, "leaking mResponseHead"); // all of the response headers have been acquired, so we can take ownership // of them from the transaction. mResponseHead = mTransaction->TakeResponseHead(); // 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"); } // avoid crashing if mListener happens to be null... if (!mListener) { NS_NOTREACHED("mListener is null"); 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)) { if (NS_SUCCEEDED(ProxyFailover())) return NS_OK; } // on other request errors, try to fall back PRBool fallingBack; if (NS_FAILED(mStatus) && NS_SUCCEEDED(ProcessFallback(&fallingBack)) && fallingBack) { return NS_OK; } return CallOnStartRequest(); } NS_IMETHODIMP nsHttpChannel::OnStopRequest(nsIRequest *request, nsISupports *ctxt, nsresult status) { LOG(("nsHttpChannel::OnStopRequest [this=%p request=%p status=%x]\n", this, request, status)); // honor the cancelation status even if the underlying transaction completed. if (mCanceled || NS_FAILED(mStatus)) status = mStatus; if (mCachedContentIsPartial) { if (NS_SUCCEEDED(status)) { // mTransactionPump should be suspended NS_ASSERTION(request != mTransactionPump, "byte-range transaction finished prematurely"); if (request == mCachePump) { PRBool streamDone; status = OnDoneReadingPartialCacheEntry(&streamDone); if (NS_SUCCEEDED(status) && !streamDone) return status; // otherwise, fall through and fire OnStopRequest... } else NS_NOTREACHED("unexpected request"); } // Do not to leave the transaction in a suspended state in error cases. if (NS_FAILED(status) && mTransaction) gHttpHandler->CancelTransaction(mTransaction, status); } if (mTransaction) { // determine if we should call DoAuthRetry PRBool authRetry = mAuthRetryPending && NS_SUCCEEDED(status); // // grab reference to connection in case we need to retry an // authentication request over it. this applies to connection based // authentication schemes only. for request based schemes, conn is not // needed, so it may be null. // // 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. // nsRefPtr conn; if (authRetry && (mCaps & NS_HTTP_STICKY_CONNECTION)) { conn = mTransaction->Connection(); // This is so far a workaround to fix leak when reusing unpersistent // connection for authentication retry. See bug 459620 comment 4 // for details. if (conn && !conn->IsPersistent()) conn = nsnull; } // at this point, we're done with the transaction NS_RELEASE(mTransaction); mTransactionPump = 0; // handle auth retry... if (authRetry) { mAuthRetryPending = PR_FALSE; status = DoAuthRetry(conn); if (NS_SUCCEEDED(status)) 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 (authRetry || (mAuthRetryPending && NS_FAILED(status))) { NS_ASSERTION(NS_FAILED(status), "should have a failure code here"); // NOTE: since we have a failure status, we can ignore the return // value from onStartRequest. mListener->OnStartRequest(this, mListenerContext); } // if this transaction has been replaced, then bail. if (mTransactionReplaced) return NS_OK; } mIsPending = PR_FALSE; mStatus = status; // perform any final cache operations before we close the cache entry. if (mCacheEntry && (mCacheAccess & nsICache::ACCESS_WRITE)) FinalizeCacheEntry(); if (mListener) { LOG((" calling OnStopRequest\n")); mListener->OnStopRequest(this, mListenerContext, status); mListener = 0; mListenerContext = 0; } if (mCacheEntry) CloseCacheEntry(PR_TRUE); if (mOfflineCacheEntry) CloseOfflineCacheEntry(); if (mLoadGroup) mLoadGroup->RemoveRequest(this, nsnull, status); mCallbacks = nsnull; mProgressSink = nsnull; return NS_OK; } //----------------------------------------------------------------------------- // nsHttpChannel::nsIStreamListener //----------------------------------------------------------------------------- NS_IMETHODIMP nsHttpChannel::OnDataAvailable(nsIRequest *request, nsISupports *ctxt, nsIInputStream *input, PRUint32 offset, PRUint32 count) { LOG(("nsHttpChannel::OnDataAvailable [this=%p request=%p offset=%u count=%u]\n", this, request, offset, count)); // don't send out OnDataAvailable notifications if we've been canceled. if (mCanceled) return mStatus; NS_ASSERTION(mResponseHead, "No response head in ODA!!"); NS_ASSERTION(!(mCachedContentIsPartial && (request == mTransactionPump)), "transaction pump not suspended"); if (mAuthRetryPending || (request == mTransactionPump && mTransactionReplaced)) { PRUint32 n; return input->ReadSegments(NS_DiscardSegment, nsnull, count, &n); } 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 = nsITransport::STATUS_READING; else transportStatus = nsISocketTransport::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. PRUint64 progressMax(PRUint64(mResponseHead->ContentLength())); PRUint64 progress = mLogicalOffset + PRUint64(count); NS_ASSERTION(progress <= progressMax, "unexpected progress values"); OnTransportStatus(nsnull, transportStatus, progress, progressMax); // // 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). // nsresult rv = mListener->OnDataAvailable(this, mListenerContext, input, mLogicalOffset, count); if (NS_SUCCEEDED(rv)) mLogicalOffset = progress; return rv; } return NS_ERROR_ABORT; } //----------------------------------------------------------------------------- // nsHttpChannel::nsITransportEventSink //----------------------------------------------------------------------------- NS_IMETHODIMP nsHttpChannel::OnTransportStatus(nsITransport *trans, nsresult status, PRUint64 progress, PRUint64 progressMax) { // cache the progress sink so we don't have to query for it each time. if (!mProgressSink) GetCallback(mProgressSink); // block socket status event after Cancel or OnStopRequest has been called. if (mProgressSink && NS_SUCCEEDED(mStatus) && mIsPending && !(mLoadFlags & LOAD_BACKGROUND)) { LOG(("sending status notification [this=%p status=%x progress=%llu/%llu]\n", this, status, progress, progressMax)); nsCAutoString host; mURI->GetHost(host); mProgressSink->OnStatus(this, nsnull, status, NS_ConvertUTF8toUTF16(host).get()); if (progress > 0) { NS_ASSERTION(progress <= progressMax, "unexpected progress values"); mProgressSink->OnProgress(this, nsnull, progress, progressMax); } } #ifdef DEBUG else LOG(("skipping status notification [this=%p sink=%p pending=%u background=%x]\n", this, mProgressSink.get(), mIsPending, (mLoadFlags & LOAD_BACKGROUND))); #endif return NS_OK; } //----------------------------------------------------------------------------- // nsHttpChannel::nsICachingChannel //----------------------------------------------------------------------------- NS_IMETHODIMP nsHttpChannel::GetCacheToken(nsISupports **token) { NS_ENSURE_ARG_POINTER(token); if (!mCacheEntry) return NS_ERROR_NOT_AVAILABLE; return CallQueryInterface(mCacheEntry, token); } NS_IMETHODIMP nsHttpChannel::SetCacheToken(nsISupports *token) { return NS_ERROR_NOT_IMPLEMENTED; } NS_IMETHODIMP nsHttpChannel::GetOfflineCacheToken(nsISupports **token) { NS_ENSURE_ARG_POINTER(token); if (!mOfflineCacheEntry) return NS_ERROR_NOT_AVAILABLE; return CallQueryInterface(mOfflineCacheEntry, token); } NS_IMETHODIMP nsHttpChannel::SetOfflineCacheToken(nsISupports *token) { return NS_ERROR_NOT_IMPLEMENTED; } class nsHttpChannelCacheKey : public nsISupportsPRUint32, public nsISupportsCString { NS_DECL_ISUPPORTS NS_DECL_NSISUPPORTSPRIMITIVE NS_FORWARD_NSISUPPORTSPRUINT32(mSupportsPRUint32->) // Both interfaces declares toString method with the same signature. // Thus we have to delegate only to nsISupportsPRUint32 implementation. NS_SCRIPTABLE NS_IMETHOD GetData(nsACString & aData) { return mSupportsCString->GetData(aData); } NS_SCRIPTABLE NS_IMETHOD SetData(const nsACString & aData) { return mSupportsCString->SetData(aData); } public: nsresult SetData(PRUint32 aPostID, const nsACString& aKey); protected: nsCOMPtr mSupportsPRUint32; nsCOMPtr mSupportsCString; }; NS_IMPL_ADDREF(nsHttpChannelCacheKey) NS_IMPL_RELEASE(nsHttpChannelCacheKey) NS_INTERFACE_TABLE_HEAD(nsHttpChannelCacheKey) NS_INTERFACE_TABLE_BEGIN NS_INTERFACE_TABLE_ENTRY_AMBIGUOUS(nsHttpChannelCacheKey, nsISupports, nsISupportsPRUint32) NS_INTERFACE_TABLE_ENTRY_AMBIGUOUS(nsHttpChannelCacheKey, nsISupportsPrimitive, nsISupportsPRUint32) NS_INTERFACE_TABLE_ENTRY(nsHttpChannelCacheKey, nsISupportsPRUint32) NS_INTERFACE_TABLE_ENTRY(nsHttpChannelCacheKey, nsISupportsCString) NS_INTERFACE_TABLE_END NS_INTERFACE_TABLE_TAIL NS_IMETHODIMP nsHttpChannelCacheKey::GetType(PRUint16 *aType) { NS_ENSURE_ARG_POINTER(aType); *aType = TYPE_PRUINT32; return NS_OK; } nsresult nsHttpChannelCacheKey::SetData(PRUint32 aPostID, const nsACString& aKey) { nsresult rv; mSupportsCString = do_CreateInstance(NS_SUPPORTS_CSTRING_CONTRACTID, &rv); if (NS_FAILED(rv)) return rv; mSupportsCString->SetData(aKey); if (NS_FAILED(rv)) return rv; mSupportsPRUint32 = do_CreateInstance(NS_SUPPORTS_PRUINT32_CONTRACTID, &rv); if (NS_FAILED(rv)) return rv; mSupportsPRUint32->SetData(aPostID); if (NS_FAILED(rv)) return rv; return NS_OK; } NS_IMETHODIMP nsHttpChannel::GetCacheKey(nsISupports **key) { nsresult rv; NS_ENSURE_ARG_POINTER(key); LOG(("nsHttpChannel::GetCacheKey [this=%p]\n", this)); *key = nsnull; nsRefPtr container = new nsHttpChannelCacheKey(); if (!container) return NS_ERROR_OUT_OF_MEMORY; nsCAutoString cacheKey; rv = GenerateCacheKey(mPostID, cacheKey); if (NS_FAILED(rv)) return rv; rv = container->SetData(mPostID, cacheKey); if (NS_FAILED(rv)) return rv; return CallQueryInterface(container.get(), key); } NS_IMETHODIMP nsHttpChannel::SetCacheKey(nsISupports *key) { nsresult rv; LOG(("nsHttpChannel::SetCacheKey [this=%p key=%p]\n", this, key)); // can only set the cache key if a load is not in progress NS_ENSURE_TRUE(!mIsPending, NS_ERROR_IN_PROGRESS); if (!key) mPostID = 0; else { // extract the post id nsCOMPtr container = do_QueryInterface(key, &rv); if (NS_FAILED(rv)) return rv; rv = container->GetData(&mPostID); if (NS_FAILED(rv)) return rv; } return NS_OK; } NS_IMETHODIMP nsHttpChannel::GetCacheAsFile(PRBool *value) { NS_ENSURE_ARG_POINTER(value); if (!mCacheEntry) return NS_ERROR_NOT_AVAILABLE; nsCacheStoragePolicy storagePolicy; mCacheEntry->GetStoragePolicy(&storagePolicy); *value = (storagePolicy == nsICache::STORE_ON_DISK_AS_FILE); return NS_OK; } NS_IMETHODIMP nsHttpChannel::SetCacheAsFile(PRBool value) { if (!mCacheEntry || mLoadFlags & INHIBIT_PERSISTENT_CACHING) return NS_ERROR_NOT_AVAILABLE; nsCacheStoragePolicy policy; if (value) policy = nsICache::STORE_ON_DISK_AS_FILE; else policy = nsICache::STORE_ANYWHERE; return mCacheEntry->SetStoragePolicy(policy); } NS_IMETHODIMP nsHttpChannel::GetCacheForOfflineUse(PRBool *value) { *value = mCacheForOfflineUse; return NS_OK; } NS_IMETHODIMP nsHttpChannel::SetCacheForOfflineUse(PRBool value) { mCacheForOfflineUse = value; return NS_OK; } NS_IMETHODIMP nsHttpChannel::GetOfflineCacheClientID(nsACString &value) { value = mOfflineCacheClientID; return NS_OK; } NS_IMETHODIMP nsHttpChannel::SetOfflineCacheClientID(const nsACString &value) { mOfflineCacheClientID = value; return NS_OK; } NS_IMETHODIMP nsHttpChannel::GetCacheFile(nsIFile **cacheFile) { if (!mCacheEntry) return NS_ERROR_NOT_AVAILABLE; return mCacheEntry->GetFile(cacheFile); } NS_IMETHODIMP nsHttpChannel::IsFromCache(PRBool *value) { if (!mIsPending) return NS_ERROR_NOT_AVAILABLE; // return false if reading a partial cache entry; the data isn't entirely // from the cache! *value = (mCachePump || (mLoadFlags & LOAD_ONLY_IF_MODIFIED)) && mCachedContentIsValid && !mCachedContentIsPartial; return NS_OK; } //----------------------------------------------------------------------------- // nsHttpChannel::nsIResumableChannel //----------------------------------------------------------------------------- NS_IMETHODIMP nsHttpChannel::ResumeAt(PRUint64 aStartPos, const nsACString& aEntityID) { LOG(("nsHttpChannel::ResumeAt [this=%p startPos=%llu id='%s']\n", this, aStartPos, PromiseFlatCString(aEntityID).get())); mEntityID = aEntityID; mStartPos = aStartPos; mResuming = PR_TRUE; return NS_OK; } NS_IMETHODIMP nsHttpChannel::GetEntityID(nsACString& aEntityID) { // Don't return an entity ID for Non-GET requests which require // additional data if (mRequestHead.Method() != nsHttp::Get) { return NS_ERROR_NOT_RESUMABLE; } // 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. const char* acceptRanges = mResponseHead->PeekHeader(nsHttp::Accept_Ranges); if (acceptRanges && !nsHttp::FindToken(acceptRanges, "bytes", HTTP_HEADER_VALUE_SEPS)) { return NS_ERROR_NOT_RESUMABLE; } PRUint64 size = LL_MAXUINT; nsCAutoString etag, lastmod; if (mResponseHead) { size = mResponseHead->TotalEntitySize(); const char* cLastMod = mResponseHead->PeekHeader(nsHttp::Last_Modified); if (cLastMod) lastmod = cLastMod; const char* cEtag = mResponseHead->PeekHeader(nsHttp::ETag); if (cEtag) etag = cEtag; } nsCString entityID; NS_EscapeURL(etag.BeginReading(), etag.Length(), esc_AlwaysCopy | esc_FileBaseName | esc_Forced, entityID); entityID.Append('/'); entityID.AppendInt(PRInt64(size)); entityID.Append('/'); entityID.Append(lastmod); // NOTE: Appending lastmod as the last part avoids having to escape it aEntityID = entityID; return NS_OK; } //----------------------------------------------------------------------------- // nsHttpChannel::nsICacheListener //----------------------------------------------------------------------------- NS_IMETHODIMP nsHttpChannel::OnCacheEntryAvailable(nsICacheEntryDescriptor *entry, nsCacheAccessMode access, nsresult status) { LOG(("nsHttpChannel::OnCacheEntryAvailable [this=%p entry=%p " "access=%x status=%x]\n", this, entry, access, status)); // if the channel's already fired onStopRequest, then we should ignore // this event. if (!mIsPending) return NS_OK; // otherwise, we have to handle this event. if (NS_SUCCEEDED(status)) { mCacheEntry = entry; mCacheAccess = access; } nsresult rv; if (mCanceled && NS_FAILED(mStatus)) { LOG(("channel was canceled [this=%p status=%x]\n", this, mStatus)); rv = mStatus; } else if ((mLoadFlags & LOAD_ONLY_FROM_CACHE) && NS_FAILED(status)) // if this channel is only allowed to pull from the cache, then // we must fail if we were unable to open a cache entry. rv = NS_ERROR_DOCUMENT_NOT_CACHED; else // advance to the next state... rv = Connect(PR_FALSE); // a failure from Connect means that we have to abort the channel. if (NS_FAILED(rv)) { CloseCacheEntry(PR_TRUE); AsyncAbort(rv); } return NS_OK; } nsresult nsHttpChannel::DoAuthRetry(nsAHttpConnection *conn) { LOG(("nsHttpChannel::DoAuthRetry [this=%p]\n", this)); NS_ASSERTION(!mTransaction, "should not have a transaction"); nsresult rv; // toggle mIsPending to allow nsIObserver implementations to modify // the request headers (bug 95044). mIsPending = PR_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). AddCookiesToRequest(); // notify "http-on-modify-request" observers gHttpHandler->OnModifyRequest(this); mIsPending = PR_TRUE; // get rid of the old response headers delete mResponseHead; mResponseHead = nsnull; // set sticky connection flag and disable pipelining. mCaps |= NS_HTTP_STICKY_CONNECTION; mCaps &= ~NS_HTTP_ALLOW_PIPELINING; // and create a new one... rv = SetupTransaction(); if (NS_FAILED(rv)) return rv; // transfer ownership of connection to transaction if (conn) mTransaction->SetConnection(conn); // rewind the upload stream if (mUploadStream) { nsCOMPtr seekable = do_QueryInterface(mUploadStream); if (seekable) seekable->Seek(nsISeekableStream::NS_SEEK_SET, 0); } rv = gHttpHandler->InitiateTransaction(mTransaction, mPriority); if (NS_FAILED(rv)) return rv; return mTransactionPump->AsyncRead(this, nsnull); } //----------------------------------------------------------------------------- // nsHttpChannel::nsIApplicationCacheChannel //----------------------------------------------------------------------------- NS_IMETHODIMP nsHttpChannel::GetApplicationCache(nsIApplicationCache **out) { NS_IF_ADDREF(*out = mApplicationCache); return NS_OK; } NS_IMETHODIMP nsHttpChannel::SetApplicationCache(nsIApplicationCache *appCache) { NS_ENSURE_TRUE(!mWasOpened, NS_ERROR_ALREADY_OPENED); mApplicationCache = appCache; return NS_OK; } NS_IMETHODIMP nsHttpChannel::GetLoadedFromApplicationCache(PRBool *aLoadedFromApplicationCache) { *aLoadedFromApplicationCache = mLoadedFromApplicationCache; return NS_OK; } NS_IMETHODIMP nsHttpChannel::GetInheritApplicationCache(PRBool *aInherit) { *aInherit = mInheritApplicationCache; return NS_OK; } NS_IMETHODIMP nsHttpChannel::SetInheritApplicationCache(PRBool aInherit) { NS_ENSURE_TRUE(!mWasOpened, NS_ERROR_ALREADY_OPENED); mInheritApplicationCache = aInherit; return NS_OK; } NS_IMETHODIMP nsHttpChannel::GetChooseApplicationCache(PRBool *aChoose) { *aChoose = mChooseApplicationCache; return NS_OK; } NS_IMETHODIMP nsHttpChannel::SetChooseApplicationCache(PRBool aChoose) { NS_ENSURE_TRUE(!mWasOpened, NS_ERROR_ALREADY_OPENED); mChooseApplicationCache = aChoose; return NS_OK; } //----------------------------------------------------------------------------- // nsHttpChannel::nsContentEncodings //----------------------------------------------------------------------------- nsHttpChannel::nsContentEncodings::nsContentEncodings(nsIHttpChannel* aChannel, const char* aEncodingHeader) : mEncodingHeader(aEncodingHeader), mChannel(aChannel), mReady(PR_FALSE) { mCurEnd = aEncodingHeader + strlen(aEncodingHeader); mCurStart = mCurEnd; } nsHttpChannel::nsContentEncodings::~nsContentEncodings() { } //----------------------------------------------------------------------------- // nsHttpChannel::nsContentEncodings::nsISimpleEnumerator //----------------------------------------------------------------------------- NS_IMETHODIMP nsHttpChannel::nsContentEncodings::HasMore(PRBool* aMoreEncodings) { if (mReady) { *aMoreEncodings = PR_TRUE; return NS_OK; } nsresult rv = PrepareForNext(); *aMoreEncodings = NS_SUCCEEDED(rv); return NS_OK; } NS_IMETHODIMP nsHttpChannel::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); PRBool haveType = PR_FALSE; if (CaseInsensitiveFindInReadable(NS_LITERAL_CSTRING("gzip"), start, end)) { aNextEncoding.AssignLiteral(APPLICATION_GZIP); haveType = PR_TRUE; } if (!haveType) { encoding.BeginReading(start); if (CaseInsensitiveFindInReadable(NS_LITERAL_CSTRING("compress"), start, end)) { aNextEncoding.AssignLiteral(APPLICATION_COMPRESS); haveType = PR_TRUE; } } if (! haveType) { encoding.BeginReading(start); if (CaseInsensitiveFindInReadable(NS_LITERAL_CSTRING("deflate"), start, end)) { aNextEncoding.AssignLiteral(APPLICATION_ZIP); haveType = PR_TRUE; } } // Prepare to fetch the next encoding mCurEnd = mCurStart; mReady = PR_FALSE; if (haveType) return NS_OK; NS_WARNING("Unknown encoding type"); return NS_ERROR_FAILURE; } //----------------------------------------------------------------------------- // nsHttpChannel::nsContentEncodings::nsISupports //----------------------------------------------------------------------------- NS_IMPL_ISUPPORTS1(nsHttpChannel::nsContentEncodings, nsIUTF8StringEnumerator) //----------------------------------------------------------------------------- // nsHttpChannel::nsContentEncodings //----------------------------------------------------------------------------- nsresult nsHttpChannel::nsContentEncodings::PrepareForNext(void) { NS_PRECONDITION(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 = PR_TRUE; return NS_OK; } //----------------------------------------------------------------------------- // nsStreamListenerWrapper //----------------------------------------------------------------------------- // Wrapper class to make replacement of nsHttpChannel's listener // from JavaScript possible. It is workaround for bug 433711. class nsStreamListenerWrapper : public nsIStreamListener { public: nsStreamListenerWrapper(nsIStreamListener *listener); NS_DECL_ISUPPORTS NS_FORWARD_NSIREQUESTOBSERVER(mListener->) NS_FORWARD_NSISTREAMLISTENER(mListener->) private: ~nsStreamListenerWrapper() {} nsCOMPtr mListener; }; nsStreamListenerWrapper::nsStreamListenerWrapper(nsIStreamListener *listener) : mListener(listener) { NS_ASSERTION(mListener, "no stream listener specified"); } NS_IMPL_ISUPPORTS2(nsStreamListenerWrapper, nsIStreamListener, nsIRequestObserver) //----------------------------------------------------------------------------- // nsHttpChannel::nsITraceableChannel //----------------------------------------------------------------------------- NS_IMETHODIMP nsHttpChannel::SetNewListener(nsIStreamListener *aListener, nsIStreamListener **_retval) { if (!mTracingEnabled) return NS_ERROR_FAILURE; NS_ENSURE_ARG_POINTER(aListener); nsCOMPtr wrapper = new nsStreamListenerWrapper(mListener); if (!wrapper) return NS_ERROR_OUT_OF_MEMORY; wrapper.forget(_retval); mListener = aListener; return NS_OK; } 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.Method() == nsHttp::Options || mRequestHead.Method() == nsHttp::Get || mRequestHead.Method() == nsHttp::Head || mRequestHead.Method() == nsHttp::Trace || mRequestHead.Method() == nsHttp::Connect) return; // 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. LOG(("MaybeInvalidateCacheEntryForSubsequentGet [this=%p]\n", this)); nsCAutoString tmpCacheKey; // passing 0 in first param gives the cache-key for a GET to my resource GenerateCacheKey(0, tmpCacheKey); // Now, find the session holding the cache-entry nsCOMPtr session; nsCacheStoragePolicy storagePolicy = DetermineStoragePolicy(); nsresult rv; rv = gHttpHandler->GetCacheSession(storagePolicy, getter_AddRefs(session)); if (NS_FAILED(rv)) return; // Finally, find the actual cache-entry nsCOMPtr tmpCacheEntry; rv = session->OpenCacheEntry(tmpCacheKey, nsICache::ACCESS_READ, PR_FALSE, getter_AddRefs(tmpCacheEntry)); // If entry was found, set its expiration-time = 0 if(NS_SUCCEEDED(rv)) { tmpCacheEntry->SetExpirationTime(0); } } nsCacheStoragePolicy nsHttpChannel::DetermineStoragePolicy() { nsCacheStoragePolicy policy = nsICache::STORE_ANYWHERE; if (mLoadFlags & INHIBIT_PERSISTENT_CACHING) policy = nsICache::STORE_IN_MEMORY; return policy; } void nsHttpChannel::AsyncOnExamineCachedResponse() { gHttpHandler->OnExamineCachedResponse(this); }