Bug 1367551 - Cancel pushes when we already have the item in cache. r=mayhemer,mcmanus

MozReview-Commit-ID: 24N0Jm85wcC

--HG--
extra : rebase_source : e3babc6e569ce723c1f35bd587bb5f0b59f79cfd
This commit is contained in:
Nicholas Hurley 2017-06-28 10:34:55 -07:00
parent 01440593f7
commit 17f1c3bcb9
9 changed files with 616 additions and 192 deletions

View File

@ -37,6 +37,11 @@
#include "mozilla/Sprintf.h"
#include "nsSocketTransportService2.h"
#include "nsNetUtil.h"
#include "nsICacheEntry.h"
#include "nsICacheStorageService.h"
#include "nsICacheStorage.h"
#include "CacheControlParser.h"
#include "LoadContextInfo.h"
namespace mozilla {
namespace net {
@ -1809,15 +1814,15 @@ Http2Session::RecvPushPromise(Http2Session *self)
// does the pushed origin belong on this connection?
LOG3(("Http2Session::RecvPushPromise %p origin check %s", self,
pushedStream->Origin().get()));
RefPtr<nsStandardURL> pushedURL;
rv = Http2Stream::MakeOriginURL(pushedStream->Origin(), pushedURL);
RefPtr<nsStandardURL> pushedOrigin;
rv = Http2Stream::MakeOriginURL(pushedStream->Origin(), pushedOrigin);
nsAutoCString pushedHostName;
int32_t pushedPort = -1;
if (NS_SUCCEEDED(rv)) {
rv = pushedURL->GetHost(pushedHostName);
rv = pushedOrigin->GetHost(pushedHostName);
}
if (NS_SUCCEEDED(rv)) {
rv = pushedURL->GetPort(&pushedPort);
rv = pushedOrigin->GetPort(&pushedPort);
}
if (NS_FAILED(rv) ||
!self->TestJoinConnection(pushedHostName, pushedPort)) {
@ -1840,6 +1845,35 @@ Http2Session::RecvPushPromise(Http2Session *self)
self->ResetDownstreamState();
return NS_OK;
}
// Kick off a lookup into the HTTP cache so we can cancel the push if it's
// unneeded (we already have it in our local regular cache). See bug 1367551.
nsCOMPtr<nsICacheStorageService> css =
do_GetService("@mozilla.org/netwerk/cache-storage-service;1");
mozilla::OriginAttributes oa;
pushedStream->GetOriginAttributes(&oa);
RefPtr<LoadContextInfo> lci = GetLoadContextInfo(false, oa);
nsCOMPtr<nsICacheStorage> ds;
css->DiskCacheStorage(lci, false, getter_AddRefs(ds));
// Build up our full URL for the cache lookup
nsAutoCString spec;
spec.Assign(pushedStream->Origin());
spec.Append(pushedStream->Path());
RefPtr<nsStandardURL> pushedURL;
// Nifty trick: this doesn't actually do anything origin-specific, it's just
// named that way. So by passing it the full spec here, we get a URL with
// the full path.
// Another nifty trick! Even though this is using nsIURIs (which are not
// generally ok off the main thread), since we're not using the protocol
// handler to create any URIs, this will work just fine here. Don't try this
// at home, though, kids. I'm a trained professional.
if (NS_SUCCEEDED(Http2Stream::MakeOriginURL(spec, pushedURL))) {
LOG3(("Http2Session::RecvPushPromise %p check disk cache for entry", self));
RefPtr<CachePushCheckCallback> cpcc = new CachePushCheckCallback(self, promisedID, pushedStream->GetRequestString());
if (NS_FAILED(ds->AsyncOpenURI(pushedURL, EmptyCString(), nsICacheStorage::OPEN_READONLY|nsICacheStorage::OPEN_SECRETLY, cpcc))) {
LOG3(("Http2Session::RecvPushPromise %p failed to open cache entry for push check", self));
}
}
}
pushedStream->SetHTTPState(Http2Stream::RESERVED_BY_REMOTE);
@ -1853,6 +1887,167 @@ Http2Session::RecvPushPromise(Http2Session *self)
return NS_OK;
}
NS_IMPL_ISUPPORTS(Http2Session::CachePushCheckCallback, nsICacheEntryOpenCallback);
Http2Session::CachePushCheckCallback::CachePushCheckCallback(Http2Session *session, uint32_t promisedID, const nsACString &requestString)
:mPromisedID(promisedID)
{
mSession = session;
mRequestHead.ParseHeaderSet(requestString.BeginReading());
}
NS_IMETHODIMP
Http2Session::CachePushCheckCallback::OnCacheEntryCheck(nsICacheEntry *entry, nsIApplicationCache *appCache, uint32_t *result)
{
MOZ_ASSERT(OnSocketThread(), "Not on socket thread?!");
// We never care to fully open the entry, since we won't actually use it.
// We just want to be able to do all our checks to see if a future channel can
// use this entry, or if we need to accept the push.
*result = nsICacheEntryOpenCallback::ENTRY_NOT_WANTED;
bool isForcedValid = false;
entry->GetIsForcedValid(&isForcedValid);
nsHttpResponseHead cachedResponseHead;
nsresult rv = nsHttp::GetHttpResponseHeadFromCacheEntry(entry, &cachedResponseHead);
if (NS_FAILED(rv)) {
// Couldn't make sense of what's in the cache entry, go ahead and accept
// the push.
return NS_OK;
}
if ((cachedResponseHead.Status() / 100) != 2) {
// Assume the push is sending us a success, while we don't have one in the
// cache, so we'll accept the push.
return NS_OK;
}
// Get the method that was used to generate the cached response
nsXPIDLCString buf;
rv = entry->GetMetaDataElement("request-method", getter_Copies(buf));
if (NS_FAILED(rv)) {
// Can't check request method, accept the push
return NS_OK;
}
nsAutoCString pushedMethod;
mRequestHead.Method(pushedMethod);
if (!buf.Equals(pushedMethod)) {
// Methods don't match, accept the push
return NS_OK;
}
int64_t size, contentLength;
rv = nsHttp::CheckPartial(entry, &size, &contentLength, &cachedResponseHead);
if (NS_FAILED(rv)) {
// Couldn't figure out if this was partial or not, accept the push.
return NS_OK;
}
if (size == int64_t(-1) || contentLength != size) {
// This is partial content in the cache, accept the push.
return NS_OK;
}
nsAutoCString requestedETag;
if (NS_FAILED(mRequestHead.GetHeader(nsHttp::If_Match, requestedETag))) {
// Can't check etag
return NS_OK;
}
if (!requestedETag.IsEmpty()) {
nsAutoCString cachedETag;
if (NS_FAILED(cachedResponseHead.GetHeader(nsHttp::ETag, cachedETag))) {
// Can't check etag
return NS_OK;
}
if (!requestedETag.Equals(cachedETag)) {
// ETags don't match, accept the push.
return NS_OK;
}
}
nsAutoCString imsString;
Unused << mRequestHead.GetHeader(nsHttp::If_Modified_Since, imsString);
if (!buf.IsEmpty()) {
uint32_t ims = buf.ToInteger(&rv);
uint32_t lm;
rv = cachedResponseHead.GetLastModifiedValue(&lm);
if (NS_SUCCEEDED(rv) && lm && lm < ims) {
// The push appears to be newer than what's in our cache, accept it.
return NS_OK;
}
}
nsAutoCString cacheControlRequestHeader;
Unused << mRequestHead.GetHeader(nsHttp::Cache_Control, cacheControlRequestHeader);
CacheControlParser cacheControlRequest(cacheControlRequestHeader);
if (cacheControlRequest.NoStore()) {
// Don't use a no-store cache entry, accept the push.
return NS_OK;
}
nsXPIDLCString cachedAuth;
rv = entry->GetMetaDataElement("auth", getter_Copies(cachedAuth));
if (NS_SUCCEEDED(rv)) {
uint32_t lastModifiedTime;
rv = entry->GetLastModified(&lastModifiedTime);
if (NS_SUCCEEDED(rv)) {
if ((gHttpHandler->SessionStartTime() > lastModifiedTime) && !cachedAuth.IsEmpty()) {
// Need to revalidate this, as the auth is old. Accept the push.
return NS_OK;
}
if (cachedAuth.IsEmpty() && mRequestHead.HasHeader(nsHttp::Authorization)) {
// They're pushing us something with auth, but we didn't cache anything
// with auth. Accept the push.
return NS_OK;
}
}
}
bool weaklyFramed, isImmutable;
nsHttp::DetermineFramingAndImmutability(entry, &cachedResponseHead, true,
&weaklyFramed, &isImmutable);
// We'll need this value in later computations...
uint32_t lastModifiedTime;
rv = entry->GetLastModified(&lastModifiedTime);
if (NS_FAILED(rv)) {
// Ugh, this really sucks. OK, accept the push.
return NS_OK;
}
// Determine if this is the first time that this cache entry
// has been accessed during this session.
bool fromPreviousSession =
(gHttpHandler->SessionStartTime() > lastModifiedTime);
bool validationRequired = nsHttp::ValidationRequired(isForcedValid,
&cachedResponseHead, 0/*NWGH: ??? - loadFlags*/, false, isImmutable, false, mRequestHead, entry,
cacheControlRequest, fromPreviousSession);
if (validationRequired) {
// A real channel would most likely hit the net at this point, so let's
// accept the push.
return NS_OK;
}
// If we get here, then we would be able to use this cache entry. Cancel the
// push so as not to waste any more bandwidth.
mSession->CleanupStream(mPromisedID, NS_ERROR_FAILURE, Http2Session::REFUSED_STREAM_ERROR);
return NS_OK;
}
NS_IMETHODIMP
Http2Session::CachePushCheckCallback::OnCacheEntryAvailable(
nsICacheEntry *entry, bool isNew, nsIApplicationCache *appCache,
nsresult result)
{
// Nothing to do here, all the work is in OnCacheEntryCheck.
return NS_OK;
}
nsresult
Http2Session::RecvPing(Http2Session *self)
{

View File

@ -17,6 +17,8 @@
#include "nsDataHashtable.h"
#include "nsDeque.h"
#include "nsHashKeys.h"
#include "nsHttpRequestHead.h"
#include "nsICacheEntryOpenCallback.h"
#include "Http2Compression.h"
@ -529,6 +531,22 @@ private:
nsDataHashtable<nsCStringHashKey, bool> mJoinConnectionCache;
class CachePushCheckCallback final : public nsICacheEntryOpenCallback
{
public:
CachePushCheckCallback(Http2Session *session, uint32_t promisedID, const nsACString &requestString);
NS_DECL_ISUPPORTS
NS_DECL_NSICACHEENTRYOPENCALLBACK
private:
~CachePushCheckCallback() { }
RefPtr<Http2Session> mSession;
uint32_t mPromisedID;
nsHttpRequestHead mRequestHead;
};
private:
/// connect tunnels
void DispatchOnTunnel(nsAHttpTransaction *, nsIInterfaceRequestor *);

View File

@ -1557,5 +1557,15 @@ Http2Stream::Finish0RTT(bool aRestart, bool aAlpnChanged)
return rv;
}
nsresult
Http2Stream::GetOriginAttributes(mozilla::OriginAttributes *oa)
{
if (!mSocketTransport) {
return NS_ERROR_UNEXPECTED;
}
return mSocketTransport->GetOriginAttributes(oa);
}
} // namespace net
} // namespace mozilla

View File

@ -168,6 +168,8 @@ public:
bool Do0RTT();
nsresult Finish0RTT(bool aRestart, bool aAlpnIgnored);
nsresult GetOriginAttributes(mozilla::OriginAttributes *oa);
protected:
static void CreatePushHashKey(const nsCString &scheme,
const nsCString &hostHeader,

View File

@ -8,19 +8,26 @@
#include "HttpLog.h"
#include "nsHttp.h"
#include "CacheControlParser.h"
#include "PLDHashTable.h"
#include "mozilla/Mutex.h"
#include "mozilla/HashFunctions.h"
#include "nsCRT.h"
#include "nsHttpRequestHead.h"
#include "nsHttpResponseHead.h"
#include "nsICacheEntry.h"
#include "nsIRequest.h"
#include <errno.h>
namespace mozilla {
namespace net {
// define storage for all atoms
#define HTTP_ATOM(_name, _value) nsHttpAtom nsHttp::_name = { _value };
namespace nsHttp {
#define HTTP_ATOM(_name, _value) nsHttpAtom _name = { _value };
#include "nsHttpAtomList.h"
#undef HTTP_ATOM
}
// find out how many atoms we have
#define HTTP_ATOM(_name, _value) Unused_ ## _name,
@ -89,8 +96,9 @@ static const PLDHashTableOps ops = {
};
// We put the atoms in a hash table for speedy lookup.. see ResolveAtom.
namespace nsHttp {
nsresult
nsHttp::CreateAtomTable()
CreateAtomTable()
{
MOZ_ASSERT(!sAtomTable, "atom table already initialized");
@ -106,7 +114,7 @@ nsHttp::CreateAtomTable()
// fill the table with our known atoms
const char *const atoms[] = {
#define HTTP_ATOM(_name, _value) nsHttp::_name._val,
#define HTTP_ATOM(_name, _value) _name._val,
#include "nsHttpAtomList.h"
#undef HTTP_ATOM
nullptr
@ -126,7 +134,7 @@ nsHttp::CreateAtomTable()
}
void
nsHttp::DestroyAtomTable()
DestroyAtomTable()
{
delete sAtomTable;
sAtomTable = nullptr;
@ -142,14 +150,14 @@ nsHttp::DestroyAtomTable()
}
Mutex *
nsHttp::GetLock()
GetLock()
{
return sLock;
}
// this function may be called from multiple threads
nsHttpAtom
nsHttp::ResolveAtom(const char *str)
ResolveAtom(const char *str)
{
nsHttpAtom atom = { nullptr };
@ -213,7 +221,7 @@ static const char kValidTokenMap[128] = {
1, 1, 1, 0, 1, 0, 1, 0 // 120
};
bool
nsHttp::IsValidToken(const char *start, const char *end)
IsValidToken(const char *start, const char *end)
{
if (start == end)
return false;
@ -228,7 +236,7 @@ nsHttp::IsValidToken(const char *start, const char *end)
}
const char*
nsHttp::GetProtocolVersion(uint32_t pv)
GetProtocolVersion(uint32_t pv)
{
switch (pv) {
case HTTP_VERSION_2:
@ -247,7 +255,7 @@ nsHttp::GetProtocolVersion(uint32_t pv)
// static
void
nsHttp::TrimHTTPWhitespace(const nsACString& aSource, nsACString& aDest)
TrimHTTPWhitespace(const nsACString& aSource, nsACString& aDest)
{
nsAutoCString str(aSource);
@ -259,7 +267,7 @@ nsHttp::TrimHTTPWhitespace(const nsACString& aSource, nsACString& aDest)
// static
bool
nsHttp::IsReasonableHeaderValue(const nsACString &s)
IsReasonableHeaderValue(const nsACString &s)
{
// Header values MUST NOT contain line-breaks. RFC 2616 technically
// permits CTL characters, including CR and LF, in header values provided
@ -276,7 +284,7 @@ nsHttp::IsReasonableHeaderValue(const nsACString &s)
}
const char *
nsHttp::FindToken(const char *input, const char *token, const char *seps)
FindToken(const char *input, const char *token, const char *seps)
{
if (!input)
return nullptr;
@ -303,7 +311,7 @@ nsHttp::FindToken(const char *input, const char *token, const char *seps)
}
bool
nsHttp::ParseInt64(const char *input, const char **next, int64_t *r)
ParseInt64(const char *input, const char **next, int64_t *r)
{
MOZ_ASSERT(input);
MOZ_ASSERT(r);
@ -328,11 +336,212 @@ nsHttp::ParseInt64(const char *input, const char **next, int64_t *r)
}
bool
nsHttp::IsPermanentRedirect(uint32_t httpStatus)
IsPermanentRedirect(uint32_t httpStatus)
{
return httpStatus == 301 || httpStatus == 308;
}
bool
ValidationRequired(bool isForcedValid, nsHttpResponseHead *cachedResponseHead,
uint32_t loadFlags, bool allowStaleCacheContent,
bool isImmutable, bool customConditionalRequest,
nsHttpRequestHead &requestHead,
nsICacheEntry *entry, CacheControlParser &cacheControlRequest,
bool fromPreviousSession)
{
// Check isForcedValid to see if it is possible to skip validation.
// Don't skip validation if we have serious reason to believe that this
// content is invalid (it's expired).
// See netwerk/cache2/nsICacheEntry.idl for details
if (isForcedValid &&
(!cachedResponseHead->ExpiresInPast() ||
!cachedResponseHead->MustValidateIfExpired())) {
LOG(("NOT validating based on isForcedValid being true.\n"));
return false;
}
// If the LOAD_FROM_CACHE flag is set, any cached data can simply be used
if (loadFlags & nsIRequest::LOAD_FROM_CACHE || allowStaleCacheContent) {
LOG(("NOT validating based on LOAD_FROM_CACHE load flag\n"));
return false;
}
// If the VALIDATE_ALWAYS flag is set, any cached data won't be used until
// it's revalidated with the server.
if ((loadFlags & nsIRequest::VALIDATE_ALWAYS) && !isImmutable) {
LOG(("Validating based on VALIDATE_ALWAYS load flag\n"));
return true;
}
// Even if the VALIDATE_NEVER flag is set, there are still some cases in
// which we must validate the cached response with the server.
if (loadFlags & nsIRequest::VALIDATE_NEVER) {
LOG(("VALIDATE_NEVER set\n"));
// if no-store validate cached response (see bug 112564)
if (cachedResponseHead->NoStore()) {
LOG(("Validating based on no-store logic\n"));
return true;
} else {
LOG(("NOT validating based on VALIDATE_NEVER load flag\n"));
return false;
}
}
// check if validation is strictly required...
if (cachedResponseHead->MustValidate()) {
LOG(("Validating based on MustValidate() returning TRUE\n"));
return true;
}
// possibly serve from cache for a custom If-Match/If-Unmodified-Since
// conditional request
if (customConditionalRequest &&
!requestHead.HasHeader(nsHttp::If_Match) &&
!requestHead.HasHeader(nsHttp::If_Unmodified_Since)) {
LOG(("Validating based on a custom conditional request\n"));
return true;
}
// previously we also checked for a query-url w/out expiration
// and didn't do heuristic on it. but defacto that is allowed now.
//
// Check if the cache entry has expired...
bool doValidation = true;
uint32_t now = NowInSeconds();
uint32_t age = 0;
nsresult rv = cachedResponseHead->ComputeCurrentAge(now, now, &age);
if (NS_FAILED(rv)) {
return true;
}
uint32_t freshness = 0;
rv = cachedResponseHead->ComputeFreshnessLifetime(&freshness);
if (NS_FAILED(rv)) {
return true;
}
uint32_t expiration = 0;
rv = entry->GetExpirationTime(&expiration);
if (NS_FAILED(rv)) {
return true;
}
uint32_t maxAgeRequest, maxStaleRequest, minFreshRequest;
LOG((" NowInSeconds()=%u, expiration time=%u, freshness lifetime=%u, age=%u",
now, expiration, freshness, age));
if (cacheControlRequest.NoCache()) {
LOG((" validating, no-cache request"));
doValidation = true;
} else if (cacheControlRequest.MaxStale(&maxStaleRequest)) {
uint32_t staleTime = age > freshness ? age - freshness : 0;
doValidation = staleTime > maxStaleRequest;
LOG((" validating=%d, max-stale=%u requested", doValidation, maxStaleRequest));
} else if (cacheControlRequest.MaxAge(&maxAgeRequest)) {
doValidation = age > maxAgeRequest;
LOG((" validating=%d, max-age=%u requested", doValidation, maxAgeRequest));
} else if (cacheControlRequest.MinFresh(&minFreshRequest)) {
uint32_t freshTime = freshness > age ? freshness - age : 0;
doValidation = freshTime < minFreshRequest;
LOG((" validating=%d, min-fresh=%u requested", doValidation, minFreshRequest));
} else if (now <= expiration) {
doValidation = false;
LOG((" not validating, expire time not in the past"));
} else if (cachedResponseHead->MustValidateIfExpired()) {
doValidation = true;
} else if (loadFlags & nsIRequest::VALIDATE_ONCE_PER_SESSION) {
// If the cached response does not include expiration infor-
// mation, then we must validate the response, despite whether
// or not this is the first access this session. This behavior
// is consistent with existing browsers and is generally expected
// by web authors.
if (freshness == 0)
doValidation = true;
else
doValidation = fromPreviousSession;
} else {
doValidation = true;
}
LOG(("%salidating based on expiration time\n", doValidation ? "V" : "Not v"));
return doValidation;
}
nsresult
GetHttpResponseHeadFromCacheEntry(nsICacheEntry *entry, nsHttpResponseHead *cachedResponseHead)
{
nsXPIDLCString buf;
// A "original-response-headers" metadata element holds network original headers,
// i.e. the headers in the form as they arrieved from the network.
// We need to get the network original headers first, because we need to keep them
// in order.
nsresult rv = entry->GetMetaDataElement("original-response-headers", getter_Copies(buf));
if (NS_SUCCEEDED(rv)) {
rv = cachedResponseHead->ParseCachedOriginalHeaders((char *) buf.get());
if (NS_FAILED(rv)) {
LOG((" failed to parse original-response-headers\n"));
}
}
buf.Adopt(0);
// A "response-head" metadata element holds response head, e.g. response status
// line and headers in the form Firefox uses them internally (no dupicate
// headers, etc.).
rv = entry->GetMetaDataElement("response-head", getter_Copies(buf));
NS_ENSURE_SUCCESS(rv, rv);
// Parse string stored in a "response-head" metadata element.
// These response headers will be merged with the orignal headers (i.e. the
// headers stored in a "original-response-headers" metadata element).
rv = cachedResponseHead->ParseCachedHead(buf.get());
NS_ENSURE_SUCCESS(rv, rv);
buf.Adopt(0);
return NS_OK;
}
nsresult
CheckPartial(nsICacheEntry* aEntry, int64_t *aSize, int64_t *aContentLength,
nsHttpResponseHead *responseHead)
{
nsresult rv;
rv = aEntry->GetDataSize(aSize);
if (NS_ERROR_IN_PROGRESS == rv) {
*aSize = -1;
rv = NS_OK;
}
NS_ENSURE_SUCCESS(rv, rv);
if (!responseHead) {
return NS_ERROR_UNEXPECTED;
}
*aContentLength = responseHead->ContentLength();
return NS_OK;
}
void
DetermineFramingAndImmutability(nsICacheEntry *entry,
nsHttpResponseHead *responseHead, bool isHttps,
bool *weaklyFramed, bool *isImmutable)
{
nsXPIDLCString framedBuf;
nsresult rv = entry->GetMetaDataElement("strongly-framed", getter_Copies(framedBuf));
// describe this in terms of explicitly weakly framed so as to be backwards
// compatible with old cache contents which dont have strongly-framed makers
*weaklyFramed = NS_SUCCEEDED(rv) && framedBuf.EqualsLiteral("0");
*isImmutable = !*weaklyFramed && isHttps && responseHead->Immutable();
}
} // namespace nsHttp
template<typename T> void
localEnsureBuffer(UniquePtr<T[]> &buf, uint32_t newSize,

View File

@ -22,11 +22,17 @@
#define NS_HTTP_VERSION_1_1 11
#define NS_HTTP_VERSION_2_0 20
class nsICacheEntry;
namespace mozilla {
class Mutex;
namespace net {
class nsHttpResponseHead;
class nsHttpRequestHead;
class CacheControlParser;
enum {
// SPDY_VERSION_2 = 2, REMOVED
// SPDY_VERSION_3 = 3, REMOVED
@ -125,46 +131,46 @@ struct nsHttpAtom
const char *_val;
};
struct nsHttp
namespace nsHttp
{
static MOZ_MUST_USE nsresult CreateAtomTable();
static void DestroyAtomTable();
MOZ_MUST_USE nsresult CreateAtomTable();
void DestroyAtomTable();
// The mutex is valid any time the Atom Table is valid
// This mutex is used in the unusual case that the network thread and
// main thread might access the same data
static Mutex *GetLock();
Mutex *GetLock();
// will dynamically add atoms to the table if they don't already exist
static nsHttpAtom ResolveAtom(const char *);
static nsHttpAtom ResolveAtom(const nsACString &s)
nsHttpAtom ResolveAtom(const char *);
inline nsHttpAtom ResolveAtom(const nsACString &s)
{
return ResolveAtom(PromiseFlatCString(s).get());
}
// returns true if the specified token [start,end) is valid per RFC 2616
// section 2.2
static bool IsValidToken(const char *start, const char *end);
bool IsValidToken(const char *start, const char *end);
static inline bool IsValidToken(const nsACString &s) {
inline bool IsValidToken(const nsACString &s) {
return IsValidToken(s.BeginReading(), s.EndReading());
}
// Strip the leading or trailing HTTP whitespace per fetch spec section 2.2.
static void TrimHTTPWhitespace(const nsACString& aSource,
void TrimHTTPWhitespace(const nsACString& aSource,
nsACString& aDest);
// Returns true if the specified value is reasonable given the defintion
// in RFC 2616 section 4.2. Full strict validation is not performed
// currently as it would require full parsing of the value.
static bool IsReasonableHeaderValue(const nsACString &s);
bool IsReasonableHeaderValue(const nsACString &s);
// find the first instance (case-insensitive comparison) of the given
// |token| in the |input| string. the |token| is bounded by elements of
// |separators| and may appear at the beginning or end of the |input|
// string. null is returned if the |token| is not found. |input| may be
// null, in which case null is returned.
static const char *FindToken(const char *input, const char *token,
const char *FindToken(const char *input, const char *token,
const char *separators);
// This function parses a string containing a decimal-valued, non-negative
@ -175,22 +181,40 @@ struct nsHttp
//
// TODO(darin): Replace this with something generic.
//
static MOZ_MUST_USE bool ParseInt64(const char *input, const char **next,
MOZ_MUST_USE bool ParseInt64(const char *input, const char **next,
int64_t *result);
// Variant on ParseInt64 that expects the input string to contain nothing
// more than the value being parsed.
static inline MOZ_MUST_USE bool ParseInt64(const char *input,
inline MOZ_MUST_USE bool ParseInt64(const char *input,
int64_t *result) {
const char *next;
return ParseInt64(input, &next, result) && *next == '\0';
}
// Return whether the HTTP status code represents a permanent redirect
static bool IsPermanentRedirect(uint32_t httpStatus);
bool IsPermanentRedirect(uint32_t httpStatus);
// Returns the APLN token which represents the used protocol version.
static const char* GetProtocolVersion(uint32_t pv);
const char* GetProtocolVersion(uint32_t pv);
bool ValidationRequired(bool isForcedValid, nsHttpResponseHead *cachedResponseHead,
uint32_t loadFlags, bool allowStaleCacheContent,
bool isImmutable, bool customConditionalRequest,
nsHttpRequestHead &requestHead,
nsICacheEntry *entry, CacheControlParser &cacheControlRequest,
bool fromPreviousSession);
nsresult GetHttpResponseHeadFromCacheEntry(nsICacheEntry *entry,
nsHttpResponseHead *cachedResponseHead);
nsresult CheckPartial(nsICacheEntry* aEntry, int64_t *aSize,
int64_t *aContentLength,
nsHttpResponseHead *responseHead);
void DetermineFramingAndImmutability(nsICacheEntry *entry, nsHttpResponseHead *cachedResponseHead,
bool isHttps, bool *weaklyFramed,
bool *isImmutable);
// Declare all atoms
//
@ -198,10 +222,10 @@ struct nsHttp
// to you by the magic of C preprocessing. Add new atoms to nsHttpAtomList
// and all support logic will be auto-generated.
//
#define HTTP_ATOM(_name, _value) static nsHttpAtom _name;
#define HTTP_ATOM(_name, _value) extern nsHttpAtom _name;
#include "nsHttpAtomList.h"
#undef HTTP_ATOM
};
}
//-----------------------------------------------------------------------------
// utilities...

View File

@ -3955,27 +3955,9 @@ bypassCacheEntryOpen:
nsresult
nsHttpChannel::CheckPartial(nsICacheEntry* aEntry, int64_t *aSize, int64_t *aContentLength)
{
nsresult rv;
rv = aEntry->GetDataSize(aSize);
if (NS_ERROR_IN_PROGRESS == rv) {
*aSize = -1;
rv = NS_OK;
}
NS_ENSURE_SUCCESS(rv, rv);
nsHttpResponseHead* responseHead = mCachedResponseHead
? mCachedResponseHead
: mResponseHead;
if (!responseHead)
return NS_ERROR_UNEXPECTED;
*aContentLength = responseHead->ContentLength();
return NS_OK;
return nsHttp::CheckPartial(aEntry, aSize, aContentLength,
mCachedResponseHead ? mCachedResponseHead
: mResponseHead);
}
void
@ -4057,32 +4039,9 @@ nsHttpChannel::OnCacheEntryCheck(nsICacheEntry* entry, nsIApplicationCache* appC
// Get the cached HTTP response headers
mCachedResponseHead = new nsHttpResponseHead();
// A "original-response-headers" metadata element holds network original headers,
// i.e. the headers in the form as they arrieved from the network.
// We need to get the network original headers first, because we need to keep them
// in order.
rv = entry->GetMetaDataElement("original-response-headers", getter_Copies(buf));
if (NS_SUCCEEDED(rv)) {
rv = mCachedResponseHead->ParseCachedOriginalHeaders((char *) buf.get());
if (NS_FAILED(rv)) {
LOG((" failed to parse original-response-headers\n"));
}
}
buf.Adopt(0);
// A "response-head" metadata element holds response head, e.g. response status
// line and headers in the form Firefox uses them internally (no dupicate
// headers, etc.).
rv = entry->GetMetaDataElement("response-head", getter_Copies(buf));
rv = nsHttp::GetHttpResponseHeadFromCacheEntry(entry, mCachedResponseHead);
NS_ENSURE_SUCCESS(rv, rv);
// Parse string stored in a "response-head" metadata element.
// These response headers will be merged with the orignal headers (i.e. the
// headers stored in a "original-response-headers" metadata element).
rv = mCachedResponseHead->ParseCachedHead(buf.get());
NS_ENSURE_SUCCESS(rv, rv);
buf.Adopt(0);
bool isCachedRedirect = WillRedirect(mCachedResponseHead);
// Do not return 304 responses from the cache, and also do not return
@ -4188,126 +4147,20 @@ nsHttpChannel::OnCacheEntryCheck(nsICacheEntry* entry, nsIApplicationCache* appC
bool isForcedValid = false;
entry->GetIsForcedValid(&isForcedValid);
nsXPIDLCString framedBuf;
rv = entry->GetMetaDataElement("strongly-framed", getter_Copies(framedBuf));
// describe this in terms of explicitly weakly framed so as to be backwards
// compatible with old cache contents which dont have strongly-framed makers
bool weaklyFramed = NS_SUCCEEDED(rv) && framedBuf.EqualsLiteral("0");
bool isImmutable = !weaklyFramed && isHttps && mCachedResponseHead->Immutable();
bool weaklyFramed, isImmutable;
nsHttp::DetermineFramingAndImmutability(entry, mCachedResponseHead, isHttps,
&weaklyFramed, &isImmutable);
// Cached entry is not the entity we request (see bug #633743)
if (ResponseWouldVary(entry)) {
LOG(("Validating based on Vary headers returning TRUE\n"));
canAddImsHeader = false;
doValidation = true;
}
// Check isForcedValid to see if it is possible to skip validation.
// Don't skip validation if we have serious reason to believe that this
// content is invalid (it's expired).
// See netwerk/cache2/nsICacheEntry.idl for details
else if (isForcedValid &&
(!mCachedResponseHead->ExpiresInPast() ||
!mCachedResponseHead->MustValidateIfExpired())) {
LOG(("NOT validating based on isForcedValid being true.\n"));
Telemetry::AutoCounter<Telemetry::PREDICTOR_TOTAL_PREFETCHES_USED> used;
++used;
doValidation = false;
}
// If the LOAD_FROM_CACHE flag is set, any cached data can simply be used
else if (mLoadFlags & nsIRequest::LOAD_FROM_CACHE || mAllowStaleCacheContent) {
LOG(("NOT validating based on LOAD_FROM_CACHE load flag\n"));
doValidation = 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 & nsIRequest::VALIDATE_ALWAYS) && !isImmutable) {
LOG(("Validating based on VALIDATE_ALWAYS load flag\n"));
doValidation = 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 & nsIRequest::VALIDATE_NEVER) {
LOG(("VALIDATE_NEVER set\n"));
// if no-store validate cached response (see bug 112564)
if (mCachedResponseHead->NoStore()) {
LOG(("Validating based on no-store logic\n"));
doValidation = true;
}
else {
LOG(("NOT validating based on VALIDATE_NEVER load flag\n"));
doValidation = false;
}
}
// check if validation is strictly required...
else if (mCachedResponseHead->MustValidate()) {
LOG(("Validating based on MustValidate() returning TRUE\n"));
doValidation = true;
// possibly serve from cache for a custom If-Match/If-Unmodified-Since
// conditional request
} else if (mCustomConditionalRequest &&
!mRequestHead.HasHeader(nsHttp::If_Match) &&
!mRequestHead.HasHeader(nsHttp::If_Unmodified_Since)) {
LOG(("Validating based on a custom conditional request\n"));
doValidation = true;
} else {
// previously we also checked for a query-url w/out expiration
// and didn't do heuristic on it. but defacto that is allowed now.
//
// Check if the cache entry has expired...
uint32_t now = NowInSeconds();
uint32_t age = 0;
rv = mCachedResponseHead->ComputeCurrentAge(now, now, &age);
NS_ENSURE_SUCCESS(rv, rv);
uint32_t freshness = 0;
rv = mCachedResponseHead->ComputeFreshnessLifetime(&freshness);
NS_ENSURE_SUCCESS(rv, rv);
uint32_t expiration = 0;
rv = entry->GetExpirationTime(&expiration);
NS_ENSURE_SUCCESS(rv, rv);
uint32_t maxAgeRequest, maxStaleRequest, minFreshRequest;
LOG((" NowInSeconds()=%u, expiration time=%u, freshness lifetime=%u, age=%u",
now, expiration, freshness, age));
if (cacheControlRequest.NoCache()) {
LOG((" validating, no-cache request"));
doValidation = true;
} else if (cacheControlRequest.MaxStale(&maxStaleRequest)) {
uint32_t staleTime = age > freshness ? age - freshness : 0;
doValidation = staleTime > maxStaleRequest;
LOG((" validating=%d, max-stale=%u requested", doValidation, maxStaleRequest));
} else if (cacheControlRequest.MaxAge(&maxAgeRequest)) {
doValidation = age > maxAgeRequest;
LOG((" validating=%d, max-age=%u requested", doValidation, maxAgeRequest));
} else if (cacheControlRequest.MinFresh(&minFreshRequest)) {
uint32_t freshTime = freshness > age ? freshness - age : 0;
doValidation = freshTime < minFreshRequest;
LOG((" validating=%d, min-fresh=%u requested", doValidation, minFreshRequest));
} else if (now <= expiration) {
doValidation = false;
LOG((" not validating, expire time not in the past"));
} else if (mCachedResponseHead->MustValidateIfExpired()) {
doValidation = true;
} else if (mLoadFlags & nsIRequest::VALIDATE_ONCE_PER_SESSION) {
// If the cached response does not include expiration infor-
// mation, then we must validate the response, despite whether
// or not this is the first access this session. This behavior
// is consistent with existing browsers and is generally expected
// by web authors.
if (freshness == 0)
doValidation = true;
else
doValidation = fromPreviousSession;
}
else
doValidation = true;
LOG(("%salidating based on expiration time\n", doValidation ? "V" : "Not v"));
doValidation = nsHttp::ValidationRequired(
isForcedValid, mCachedResponseHead, mLoadFlags,
mAllowStaleCacheContent, isImmutable, mCustomConditionalRequest,
mRequestHead, entry, cacheControlRequest, fromPreviousSession);
}

View File

@ -958,6 +958,102 @@ function test_http2_status_phrase() {
chan.asyncOpen2(listener);
}
var PulledDiskCacheListener = function() {};
PulledDiskCacheListener.prototype = new Http2CheckListener();
PulledDiskCacheListener.prototype.EXPECTED_DATA = "this was pulled via h2";
PulledDiskCacheListener.prototype.readData = "";
PulledDiskCacheListener.prototype.onDataAvailable = function testOnDataAvailable(request, ctx, stream, off, cnt) {
this.onDataAvailableFired = true;
this.isHttp2Connection = checkIsHttp2(request);
this.accum += cnt;
this.readData += read_stream(stream, cnt);
};
PulledDiskCacheListener.prototype.onStopRequest = function testOnStopRequest(request, ctx, status) {
do_check_eq(this.EXPECTED_DATA, this.readData);
Http2CheckListener.prorotype.onStopRequest.call(this, request, ctx, status);
};
const DISK_CACHE_DATA = "this is from disk cache";
var FromDiskCacheListener = function() {};
FromDiskCacheListener.prototype = {
onStartRequestFired: false,
onDataAvailableFired: false,
readData: "",
onStartRequest: function testOnStartRequest(request, ctx) {
this.onStartRequestFired = true;
if (!Components.isSuccessCode(request.status)) {
do_throw("Channel should have a success code! (" + request.status + ")");
}
do_check_true(request instanceof Components.interfaces.nsIHttpChannel);
do_check_true(request.requestSucceeded);
do_check_eq(request.responseStatus, 200);
},
onDataAvailable: function testOnDataAvailable(request, ctx, stream, off, cnt) {
this.onDataAvailableFired = true;
this.readData += read_stream(stream, cnt);
},
onStopRequest: function testOnStopRequest(request, ctx, status) {
do_check_true(this.onStartRequestFired);
do_check_true(Components.isSuccessCode(status));
do_check_true(this.onDataAvailableFired);
do_check_eq(this.readData, DISK_CACHE_DATA);
evict_cache_entries("disk");
syncWithCacheIOThread(() => {
// Now that we know the entry is out of the disk cache, check to make sure
// we don't have this hiding in the push cache somewhere - if we do, it
// didn't get cancelled, and we have a bug.
var chan = makeChan("https://localhost:" + serverPort + "/diskcache");
chan.listener = new PulledDiskCacheListener();
chan.loadGroup = loadGroup;
chan.asyncOpen2(listener);
});
}
};
var Http2DiskCachePushListener = function() {};
Http2DiskCachePushListener.prototype = new Http2CheckListener();
Http2DiskCachePushListener.onStopRequest = function(request, ctx, status) {
do_check_true(this.onStartRequestFired);
do_check_true(Components.isSuccessCode(status));
do_check_true(this.onDataAvailableFired);
do_check_true(this.isHttp2Connection == this.shouldBeHttp2);
// Now we need to open a channel to ensure we get data from the disk cache
// for the pushed item, instead of from the push cache.
var chan = makeChan("https://localhost:" + serverPort + "/diskcache");
chan.listener = new FromDiskCacheListener();
chan.loadGroup = loadGroup;
chan.asyncOpen2(listener);
};
function continue_test_http2_disk_cache_push(status, entry, appCache) {
// TODO - store stuff in cache entry, then open an h2 channel that will push
// this, once that completes, open a channel for the cache entry we made and
// ensure it came from disk cache, not the push cache.
var outputStream = entry.openOutputStream(0);
outputStream.write(DISK_CACHE_DATA, DISK_CACHE_DATA.length);
// Now we open our URL that will push data for the URL above
var chan = makeChan("https://localhost:" + serverPort + "/pushindisk");
var listener = new Http2DiskCachePushListener();
chan.loadGroup = loadGroup;
chan.asyncOpen2(listener);
}
function test_http2_disk_cache_push() {
asyncOpenCacheEntry("https://localhost:" + serverPort + "/diskcache",
"disk", Ci.nsICacheStorage.OPEN_NORMALLY, null,
continue_test_http2_disk_cache_push, false);
}
function test_complete() {
resetPrefs();
do_test_pending();
@ -1044,6 +1140,7 @@ var tests = [ test_http2_post_big
, test_http2_empty_data
, test_http2_status_phrase
, test_http2_doublepush
, test_http2_disk_cache_push
// Add new tests above here - best to add new tests before h1
// streams get too involved
// These next two must always come in this order

View File

@ -831,6 +831,22 @@ function handleRequest(req, res) {
content = 'not pushed';
}
else if (u.pathname === "/diskcache") {
content = "this was pulled via h2";
}
else if (u.pathname === "/pushindisk") {
var pushedContent = "this was pushed via h2";
push = res.push('/diskcache');
push.writeHead(200, {
'content-type': 'text/html',
'pushed' : 'yes',
'content-length' : pushedContent.length,
'X-Connection-Http2': 'yes'
});
push.end(pushedContent);
}
res.setHeader('Content-Type', 'text/html');
if (req.httpVersionMajor != 2) {
res.setHeader('Connection', 'close');