From ea5213698d9beac2512d74caed1222c1d098f243 Mon Sep 17 00:00:00 2001 From: Hinton Date: Sun, 8 Oct 2017 22:23:17 +0200 Subject: [PATCH 1/2] Add Icons application for serving website icons. --- bitwarden-core.sln | 7 +++ src/Icons/Controllers/IconController.cs | 75 ++++++++++++++++++++++++ src/Icons/Icons.csproj | 19 ++++++ src/Icons/Models/Icon.cs | 31 ++++++++++ src/Icons/Program.cs | 25 ++++++++ src/Icons/Properties/launchSettings.json | 29 +++++++++ src/Icons/Startup.cs | 40 +++++++++++++ src/Icons/appsettings.Development.json | 10 ++++ src/Icons/appsettings.json | 15 +++++ 9 files changed, 251 insertions(+) create mode 100644 src/Icons/Controllers/IconController.cs create mode 100644 src/Icons/Icons.csproj create mode 100644 src/Icons/Models/Icon.cs create mode 100644 src/Icons/Program.cs create mode 100644 src/Icons/Properties/launchSettings.json create mode 100644 src/Icons/Startup.cs create mode 100644 src/Icons/appsettings.Development.json create mode 100644 src/Icons/appsettings.json diff --git a/bitwarden-core.sln b/bitwarden-core.sln index f1d34eb4cf..970c96f2a5 100644 --- a/bitwarden-core.sln +++ b/bitwarden-core.sln @@ -41,6 +41,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jobs", "src\Jobs\Jobs.cspro EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Function", "util\Function\Function.csproj", "{A6C44A84-8E51-4C64-B9C4-7B7C23253345}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Icons", "src\Icons\Icons.csproj", "{9CF59342-3912-4B45-A2BA-0F173666586D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -89,6 +91,10 @@ Global {A6C44A84-8E51-4C64-B9C4-7B7C23253345}.Debug|Any CPU.Build.0 = Debug|Any CPU {A6C44A84-8E51-4C64-B9C4-7B7C23253345}.Release|Any CPU.ActiveCfg = Release|Any CPU {A6C44A84-8E51-4C64-B9C4-7B7C23253345}.Release|Any CPU.Build.0 = Release|Any CPU + {9CF59342-3912-4B45-A2BA-0F173666586D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9CF59342-3912-4B45-A2BA-0F173666586D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9CF59342-3912-4B45-A2BA-0F173666586D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9CF59342-3912-4B45-A2BA-0F173666586D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -104,6 +110,7 @@ Global {66B0A682-658A-4A82-B606-A077A4871448} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E} {7DCEBD8F-E5F3-4A3C-BD35-B64341590B74} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84D} {A6C44A84-8E51-4C64-B9C4-7B7C23253345} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E} + {9CF59342-3912-4B45-A2BA-0F173666586D} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84D} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {E01CBF68-2E20-425F-9EDB-E0A6510CA92F} diff --git a/src/Icons/Controllers/IconController.cs b/src/Icons/Controllers/IconController.cs new file mode 100644 index 0000000000..1ba802cd0a --- /dev/null +++ b/src/Icons/Controllers/IconController.cs @@ -0,0 +1,75 @@ +using System; +using System.IO; +using System.Net.Http; +using System.Runtime.Serialization.Formatters.Binary; +using System.Threading.Tasks; +using Icons.Models; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc; + +namespace Icons.Controllers +{ + [Route("[controller]")] + public class IconController : Controller + { + private readonly IHostingEnvironment _hostingEnvironment; + + public IconController(IHostingEnvironment hostingEnvironment) + { + _hostingEnvironment = hostingEnvironment; + } + + [HttpGet] + public async Task Get([FromQuery] string domain) + { + var uri = BuildUrl(domain); + var fileName = $"{_hostingEnvironment.ContentRootPath}/cache/{domain}.cache"; + + // Attempt to load the icon from the cache. + if (FileExists(fileName)) + { + using (Stream stream = System.IO.File.Open(fileName, FileMode.Open)) + { + var binaryFormatter = new BinaryFormatter(); + var icon = (Icon)binaryFormatter.Deserialize(stream); + + if (icon.HasNotExpired()) + { + return new FileContentResult(icon.Image, icon.Format); + } + } + } + + var httpClient = new HttpClient(); + var response = await httpClient.GetAsync(uri); + + if (!response.IsSuccessStatusCode) + { + throw new Exception("Cannot load the image"); + } + + // Serialize the icon. + using (Stream stream = System.IO.File.Open(fileName, FileMode.Create)) + { + var icon = new Icon( + await response.Content.ReadAsByteArrayAsync(), + response.Content.Headers.ContentType.MediaType + ); + + var binaryFormatter = new BinaryFormatter(); + binaryFormatter.Serialize(stream, icon); + return new FileContentResult(icon.Image, icon.Format); + } + } + + private static bool FileExists(string fileName) + { + return System.IO.File.Exists(fileName); + } + + private static string BuildUrl(string domain) + { + return $"https://icons.bitwarden.com/icon?url={domain}&size=16..24..200"; + } + } +} diff --git a/src/Icons/Icons.csproj b/src/Icons/Icons.csproj new file mode 100644 index 0000000000..89f574c340 --- /dev/null +++ b/src/Icons/Icons.csproj @@ -0,0 +1,19 @@ + + + + netcoreapp2.0 + + + + + + + + + + + + + + + diff --git a/src/Icons/Models/Icon.cs b/src/Icons/Models/Icon.cs new file mode 100644 index 0000000000..91b24c6aca --- /dev/null +++ b/src/Icons/Models/Icon.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.Serialization; +using System.Threading.Tasks; + +namespace Icons.Models +{ + [Serializable] + public class Icon + { + + public byte[] Image { get; } + + public string Format { get; } + + public DateTime CreatedAt { get; } + + public Icon(byte[] image, string format) + { + this.Image = image; + this.Format = format; + this.CreatedAt = DateTime.Now; + } + + public bool HasNotExpired() + { + return CreatedAt > DateTime.Now.AddDays(-1); + } + } +} diff --git a/src/Icons/Program.cs b/src/Icons/Program.cs new file mode 100644 index 0000000000..bfcce738a0 --- /dev/null +++ b/src/Icons/Program.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +namespace Icons +{ + public class Program + { + public static void Main(string[] args) + { + BuildWebHost(args).Run(); + } + + public static IWebHost BuildWebHost(string[] args) => + WebHost.CreateDefaultBuilder(args) + .UseStartup() + .Build(); + } +} diff --git a/src/Icons/Properties/launchSettings.json b/src/Icons/Properties/launchSettings.json new file mode 100644 index 0000000000..7f34cca0e7 --- /dev/null +++ b/src/Icons/Properties/launchSettings.json @@ -0,0 +1,29 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:50024/", + "sslPort": 0 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "icon?domain=bitwarden.com", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Icons": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "icon?domain=bitwarden.com", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "http://localhost:50025/" + } + } +} \ No newline at end of file diff --git a/src/Icons/Startup.cs b/src/Icons/Startup.cs new file mode 100644 index 0000000000..9b0a1bd191 --- /dev/null +++ b/src/Icons/Startup.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Icons +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddMvc(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IHostingEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseMvc(); + } + } +} diff --git a/src/Icons/appsettings.Development.json b/src/Icons/appsettings.Development.json new file mode 100644 index 0000000000..fa8ce71a97 --- /dev/null +++ b/src/Icons/appsettings.Development.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "IncludeScopes": false, + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + } +} diff --git a/src/Icons/appsettings.json b/src/Icons/appsettings.json new file mode 100644 index 0000000000..26bb0ac7ac --- /dev/null +++ b/src/Icons/appsettings.json @@ -0,0 +1,15 @@ +{ + "Logging": { + "IncludeScopes": false, + "Debug": { + "LogLevel": { + "Default": "Warning" + } + }, + "Console": { + "LogLevel": { + "Default": "Warning" + } + } + } +} From 753496b95dff67be3254b64f0114abec06533f97 Mon Sep 17 00:00:00 2001 From: Hinton Date: Mon, 9 Oct 2017 18:58:59 +0200 Subject: [PATCH 2/2] Use In-memory cache instead of custom file cache. --- src/Icons/Controllers/IconController.cs | 56 +++++++++---------------- src/Icons/Models/Icon.cs | 13 ------ src/Icons/Startup.cs | 1 + 3 files changed, 20 insertions(+), 50 deletions(-) diff --git a/src/Icons/Controllers/IconController.cs b/src/Icons/Controllers/IconController.cs index 1ba802cd0a..a7713ca22d 100644 --- a/src/Icons/Controllers/IconController.cs +++ b/src/Icons/Controllers/IconController.cs @@ -1,70 +1,52 @@ using System; -using System.IO; using System.Net.Http; -using System.Runtime.Serialization.Formatters.Binary; using System.Threading.Tasks; using Icons.Models; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Caching.Memory; namespace Icons.Controllers { [Route("[controller]")] public class IconController : Controller { - private readonly IHostingEnvironment _hostingEnvironment; + private readonly IMemoryCache _cache; - public IconController(IHostingEnvironment hostingEnvironment) + public IconController(IMemoryCache memoryCache) { - _hostingEnvironment = hostingEnvironment; + this._cache = memoryCache; } [HttpGet] - public async Task Get([FromQuery] string domain) + public async Task Get([FromQuery] string domain) { var uri = BuildUrl(domain); - var fileName = $"{_hostingEnvironment.ContentRootPath}/cache/{domain}.cache"; - // Attempt to load the icon from the cache. - if (FileExists(fileName)) + Icon icon = await _cache.GetOrCreateAsync(domain, async entry => { - using (Stream stream = System.IO.File.Open(fileName, FileMode.Open)) + entry.AbsoluteExpiration = DateTime.Now.AddDays(1); + + var httpClient = new HttpClient(); + var response = await httpClient.GetAsync(uri); + + if (!response.IsSuccessStatusCode) { - var binaryFormatter = new BinaryFormatter(); - var icon = (Icon)binaryFormatter.Deserialize(stream); - - if (icon.HasNotExpired()) - { - return new FileContentResult(icon.Image, icon.Format); - } + return null; } - } - var httpClient = new HttpClient(); - var response = await httpClient.GetAsync(uri); - - if (!response.IsSuccessStatusCode) - { - throw new Exception("Cannot load the image"); - } - - // Serialize the icon. - using (Stream stream = System.IO.File.Open(fileName, FileMode.Create)) - { - var icon = new Icon( + return new Icon( await response.Content.ReadAsByteArrayAsync(), response.Content.Headers.ContentType.MediaType ); + }); - var binaryFormatter = new BinaryFormatter(); - binaryFormatter.Serialize(stream, icon); - return new FileContentResult(icon.Image, icon.Format); + if (icon == null) + { + return NotFound("Cannot load the icon."); } - } - private static bool FileExists(string fileName) - { - return System.IO.File.Exists(fileName); + return new FileContentResult(icon.Image, icon.Format); } private static string BuildUrl(string domain) diff --git a/src/Icons/Models/Icon.cs b/src/Icons/Models/Icon.cs index 91b24c6aca..37c8733e0e 100644 --- a/src/Icons/Models/Icon.cs +++ b/src/Icons/Models/Icon.cs @@ -1,31 +1,18 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.Serialization; -using System.Threading.Tasks; namespace Icons.Models { [Serializable] public class Icon { - public byte[] Image { get; } public string Format { get; } - - public DateTime CreatedAt { get; } public Icon(byte[] image, string format) { this.Image = image; this.Format = format; - this.CreatedAt = DateTime.Now; - } - - public bool HasNotExpired() - { - return CreatedAt > DateTime.Now.AddDays(-1); } } } diff --git a/src/Icons/Startup.cs b/src/Icons/Startup.cs index 9b0a1bd191..6f599f9ab9 100644 --- a/src/Icons/Startup.cs +++ b/src/Icons/Startup.cs @@ -23,6 +23,7 @@ namespace Icons // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { + services.AddMemoryCache(); services.AddMvc(); }