Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@dotMorten
Created November 17, 2019 07:41
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dotMorten/e86178f289c7f13bc759dc3cc5aa5583 to your computer and use it in GitHub Desktop.
Save dotMorten/e86178f289c7f13bc759dc3cc5aa5583 to your computer and use it in GitHub Desktop.
Quick and dirty implementation of etag and cache control: Note: threading issues! Don't use as is
using System;
using System.IO;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace HttpCacheClient
{
public class CacheHttpHandler : DelegatingHandler
{
private readonly string foldername;
public CacheHttpHandler(string foldername = "cache") : base(new SocketsHttpHandler())
{
this.foldername = foldername;
if (!Directory.Exists(foldername))
Directory.CreateDirectory(foldername);
}
protected override void Dispose(bool disposing)
{
base.InnerHandler.Dispose();
base.Dispose(disposing);
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
bool isCacheable = (request.Method == HttpMethod.Get || request.Method == HttpMethod.Head) &&
request.Headers.Authorization == null && request.Content == null;
string etag = null;
string cachefile = null;
if (isCacheable)
{
var hash = MD5Hash(request.RequestUri.OriginalString);
cachefile = Path.Combine(foldername, hash);
if (File.Exists(cachefile))
{
long expires;
using (var fs = File.OpenRead(cachefile +".dat"))
{
using(var br = new BinaryReader(fs))
{
expires = br.ReadInt64();
etag = br.ReadString();
}
}
if (new DateTime(expires) > DateTime.UtcNow)
{
var resp = new HttpResponseMessage(System.Net.HttpStatusCode.OK);
resp.Content = new StreamContent(File.OpenRead(cachefile));
return resp;
}
if(string.IsNullOrEmpty(etag))
{
File.Delete(cachefile + ".dat");
File.Delete(cachefile);
etag = null;
}
else
{
request.Headers.IfNoneMatch.Add(new System.Net.Http.Headers.EntityTagHeaderValue(etag));
}
}
}
var response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
if (!string.IsNullOrEmpty(etag))
{
if (response.StatusCode == System.Net.HttpStatusCode.NotModified)
{
WriteMetadata(response, cachefile); //update metadata
response.StatusCode = System.Net.HttpStatusCode.OK;
response.Content = new StreamContent(File.OpenRead(cachefile));
return response;
}
else
{
File.Delete(cachefile);
File.Delete(cachefile + ".dat");
}
}
if (isCacheable && response.StatusCode == System.Net.HttpStatusCode.OK)
{
bool canCache = response.Headers.ETag?.Tag != null || response.Headers.CacheControl != null &&
!response.Headers.CacheControl.NoCache &&
!response.Headers.CacheControl.NoStore &&
response.Headers.CacheControl.Public;
if(canCache)
{
using (var fs = File.Create(cachefile))
{
await response.Content.CopyToAsync(fs).ConfigureAwait(false);
response.Content.Dispose();
}
WriteMetadata(response, cachefile);
response.Content = new StreamContent(File.OpenRead(cachefile));
}
}
return response;
}
private static void WriteMetadata(HttpResponseMessage response, string file)
{
using (var fs = File.Create(file +".dat"))
{
using (BinaryWriter bw = new BinaryWriter(fs))
{
bw.Write(response.Headers.CacheControl.MaxAge.HasValue ? DateTime.UtcNow.Add(response.Headers.CacheControl.MaxAge.Value).Ticks : 0L);
bw.Write(response.Headers.ETag?.Tag ?? string.Empty);
}
}
}
private static MD5CryptoServiceProvider md5provider = new MD5CryptoServiceProvider();
private static string MD5Hash(string input)
{
StringBuilder hash = new StringBuilder();
byte[] bytes = md5provider.ComputeHash(new UTF8Encoding().GetBytes(input));
for (int i = 0; i < bytes.Length; i++)
{
hash.Append(bytes[i].ToString("x2"));
}
return hash.ToString();
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment