Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Drop ETag and use Last-Modified header #890

Merged
merged 2 commits into from Feb 13, 2019
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
100 changes: 23 additions & 77 deletions Emby.Server.Implementations/HttpServer/HttpResultFactory.cs
Expand Up @@ -422,27 +422,27 @@ private static string SerializeToXmlString(object from)
/// <summary>
/// Pres the process optimized result.
/// </summary>
private object GetCachedResult(IRequest requestContext, IDictionary<string, string> responseHeaders, Guid cacheKey, string cacheKeyString, DateTime? lastDateModified, TimeSpan? cacheDuration, string contentType)
private object GetCachedResult(IRequest requestContext, IDictionary<string, string> responseHeaders, StaticResultOptions options)
{
bool noCache = (requestContext.Headers.Get("Cache-Control") ?? string.Empty).IndexOf("no-cache", StringComparison.OrdinalIgnoreCase) != -1;
AddCachingHeaders(responseHeaders, options.CacheDuration, noCache, options.DateLastModified);

if (!noCache)
{
if (IsNotModified(requestContext, cacheKey))
DateTime.TryParse(requestContext.Headers.Get("If-Modified-Since"), out var ifModifiedSinceHeader);

if (IsNotModified(ifModifiedSinceHeader, options.CacheDuration, options.DateLastModified))
{
AddAgeHeader(responseHeaders, lastDateModified);
AddExpiresHeader(responseHeaders, cacheKeyString, cacheDuration);
AddAgeHeader(responseHeaders, options.DateLastModified);

var result = new HttpResult(Array.Empty<byte>(), contentType ?? "text/html", HttpStatusCode.NotModified);
var result = new HttpResult(Array.Empty<byte>(), options.ContentType ?? "text/html", HttpStatusCode.NotModified);

AddResponseHeaders(result, responseHeaders);

return result;
}
}

AddCachingHeaders(responseHeaders, cacheKeyString, cacheDuration);

return null;
}

Expand Down Expand Up @@ -487,9 +487,6 @@ public Task<object> GetStaticFileResult(IRequest requestContext, StaticFileResul
options.DateLastModified = _fileSystem.GetLastWriteTimeUtc(path);
}

var cacheKey = path + options.DateLastModified.Value.Ticks;

options.CacheKey = cacheKey.GetMD5();
options.ContentFactory = () => Task.FromResult(GetFileStream(path, fileShare));

options.ResponseHeaders = options.ResponseHeaders ?? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
Expand Down Expand Up @@ -520,7 +517,6 @@ private Stream GetFileStream(string path, FileShareMode fileShare)
return GetStaticResult(requestContext, new StaticResultOptions
{
CacheDuration = cacheDuration,
CacheKey = cacheKey,
ContentFactory = factoryFn,
ContentType = contentType,
DateLastModified = lastDateModified,
Expand All @@ -534,14 +530,10 @@ public async Task<object> GetStaticResult(IRequest requestContext, StaticResultO
options.ResponseHeaders = options.ResponseHeaders ?? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);

var contentType = options.ContentType;
var etag = requestContext.Headers.Get("If-None-Match");
var cacheKey = etag != null ? new Guid(etag.Trim('\"')) : Guid.Empty;
if (!cacheKey.Equals(Guid.Empty))
if (!string.IsNullOrEmpty(requestContext.Headers.Get("If-Modified-Since")))
{
var key = cacheKey.ToString("N");

// See if the result is already cached in the browser
var result = GetCachedResult(requestContext, options.ResponseHeaders, cacheKey, key, options.DateLastModified, options.CacheDuration, contentType);
var result = GetCachedResult(requestContext, options.ResponseHeaders, options);

if (result != null)
{
Expand All @@ -553,6 +545,8 @@ public async Task<object> GetStaticResult(IRequest requestContext, StaticResultO
var isHeadRequest = options.IsHeadRequest || string.Equals(requestContext.Verb, "HEAD", StringComparison.OrdinalIgnoreCase);
var factoryFn = options.ContentFactory;
var responseHeaders = options.ResponseHeaders;
AddCachingHeaders(responseHeaders, options.CacheDuration, false, options.DateLastModified);
AddAgeHeader(responseHeaders, options.DateLastModified);

var rangeHeader = requestContext.Headers.Get("Range");

Expand All @@ -566,21 +560,10 @@ public async Task<object> GetStaticResult(IRequest requestContext, StaticResultO
};

AddResponseHeaders(hasHeaders, options.ResponseHeaders);
// Generate an ETag based on identifying information - TODO read contents from filesystem instead?
var responseId = $"{hasHeaders.ContentType}{options.Path}{hasHeaders.TotalContentLength}";
var hashedId = MD5.Create().ComputeHash(Encoding.Default.GetBytes(responseId));
hasHeaders.Headers["ETag"] = new Guid(hashedId).ToString("N");

return hasHeaders;
}

var stream = await factoryFn().ConfigureAwait(false);
// Generate an etag based on stream content
var streamHash = MD5.Create().ComputeHash(stream);
var newEtag = new Guid(streamHash).ToString("N");

// reset position so the response can re-use it -- TODO is this ok?
stream.Position = 0;

var totalContentLength = options.ContentLength;
if (!totalContentLength.HasValue)
Expand All @@ -603,7 +586,6 @@ public async Task<object> GetStaticResult(IRequest requestContext, StaticResultO
};

AddResponseHeaders(hasHeaders, options.ResponseHeaders);
hasHeaders.Headers["ETag"] = newEtag;
return hasHeaders;
}
else
Expand All @@ -628,7 +610,6 @@ public async Task<object> GetStaticResult(IRequest requestContext, StaticResultO
};

AddResponseHeaders(hasHeaders, options.ResponseHeaders);
hasHeaders.Headers["ETag"] = newEtag;
return hasHeaders;
}
}
Expand All @@ -641,37 +622,28 @@ public async Task<object> GetStaticResult(IRequest requestContext, StaticResultO
/// <summary>
/// Adds the caching responseHeaders.
/// </summary>
private void AddCachingHeaders(IDictionary<string, string> responseHeaders, string cacheKey, TimeSpan? cacheDuration)
private void AddCachingHeaders(IDictionary<string, string> responseHeaders, TimeSpan? cacheDuration,
bool noCache, DateTime? lastModifiedDate)
{
if (cacheDuration.HasValue)
{
responseHeaders["Cache-Control"] = "public, max-age=" + Convert.ToInt32(cacheDuration.Value.TotalSeconds);
}
else if (!string.IsNullOrEmpty(cacheKey))
{
responseHeaders["Cache-Control"] = "public";
}
else
if (noCache)
{
responseHeaders["Cache-Control"] = "no-cache, no-store, must-revalidate";
responseHeaders["pragma"] = "no-cache, no-store, must-revalidate";
return;
}

AddExpiresHeader(responseHeaders, cacheKey, cacheDuration);
}

/// <summary>
/// Adds the expires header.
/// </summary>
private static void AddExpiresHeader(IDictionary<string, string> responseHeaders, string cacheKey, TimeSpan? cacheDuration)
{
if (cacheDuration.HasValue)
{
responseHeaders["Expires"] = DateTime.UtcNow.Add(cacheDuration.Value).ToString("r");
responseHeaders["Cache-Control"] = "public, max-age=" + Convert.ToInt32(cacheDuration.Value.TotalSeconds);
cvium marked this conversation as resolved.
Show resolved Hide resolved
}
else if (string.IsNullOrEmpty(cacheKey))
else
{
responseHeaders["Expires"] = "-1";
responseHeaders["Cache-Control"] = "public";
}

if (lastModifiedDate.HasValue)
{
responseHeaders["Last-Modified"] = lastModifiedDate.ToString();
}
}

Expand All @@ -687,32 +659,6 @@ private static void AddAgeHeader(IDictionary<string, string> responseHeaders, Da
responseHeaders["Age"] = Convert.ToInt64((DateTime.UtcNow - lastDateModified.Value).TotalSeconds).ToString(CultureInfo.InvariantCulture);
}
}
/// <summary>
/// Determines whether [is not modified] [the specified cache key].
/// </summary>
/// <param name="requestContext">The request context.</param>
/// <param name="cacheKey">The cache key.</param>
/// <param name="lastDateModified">The last date modified.</param>
/// <param name="cacheDuration">Duration of the cache.</param>
/// <returns><c>true</c> if [is not modified] [the specified cache key]; otherwise, <c>false</c>.</returns>
private bool IsNotModified(IRequest requestContext, Guid cacheKey)
{
var ifNoneMatchHeader = requestContext.Headers.Get("If-None-Match");

bool hasCacheKey = !cacheKey.Equals(Guid.Empty);

// Validate If-None-Match
if (hasCacheKey && !string.IsNullOrEmpty(ifNoneMatchHeader))
{
if (Guid.TryParse(ifNoneMatchHeader, out var ifNoneMatch)
&& cacheKey.Equals(ifNoneMatch))
{
return true;
}
}

return false;
}

/// <summary>
/// Determines whether [is not modified] [the specified if modified since].
Expand Down
2 changes: 0 additions & 2 deletions MediaBrowser.Controller/Net/StaticResultOptions.cs
Expand Up @@ -12,8 +12,6 @@ public class StaticResultOptions
public string ContentType { get; set; }
public TimeSpan? CacheDuration { get; set; }
public DateTime? DateLastModified { get; set; }
public Guid CacheKey { get; set; }

public Func<Task<Stream>> ContentFactory { get; set; }

public bool IsHeadRequest { get; set; }
Expand Down