├── package_icon.png ├── .nuke └── parameters.json ├── global.json ├── .gitignore ├── .idea └── .idea.CamoDotNet │ └── .idea │ ├── encodings.xml │ ├── vcs.xml │ ├── indexLayout.xml │ └── .gitignore ├── .config └── dotnet-tools.json ├── src ├── CamoDotNet.Sample │ ├── Properties │ │ └── launchSettings.json │ ├── Program.cs │ ├── CamoDotNet.Sample.csproj │ └── Startup.cs ├── CamoDotNet │ ├── Extensions │ │ └── PathStringExtensions.cs │ ├── CamoDotNet.csproj │ ├── CamoServerSettings.cs │ ├── CamoServerAppBuilderExtensions.cs │ └── CamoServer.cs └── CamoDotNet.Core │ ├── CamoDotNet.Core.csproj │ ├── CamoUrlHelper.cs │ ├── CamoSignature.cs │ └── Extensions │ └── StringExtensions.cs ├── .gitattributes ├── LICENSE.md ├── tests └── CamoDotNet.Tests │ ├── PathStringExtensionsFacts.cs │ ├── CamoSignatureFacts.cs │ ├── CamoDotNet.Tests.csproj │ ├── CamoServerFactsBase.cs │ ├── StringExtensionsFacts.cs │ ├── CamoServerProxyFacts.cs │ └── CamoServerDefaultDocumentFacts.cs ├── Directory.Build.targets ├── README.md └── CamoDotNet.sln /package_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maartenba/CamoDotNet/HEAD/package_icon.png -------------------------------------------------------------------------------- /.nuke/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./build.schema.json", 3 | "Solution": "CamoDotNet.sln" 4 | } -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "5.0.202", 4 | "rollForward": "latestMajor", 5 | "allowPrerelease": false 6 | } 7 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | msbuild.log 2 | _ReSharper.* 3 | packages/ 4 | artifacts/ 5 | obj/ 6 | bin/ 7 | .vs/ 8 | *.suo 9 | *.user 10 | project.lock.json 11 | 12 | riderModule.iml 13 | workspace.xml -------------------------------------------------------------------------------- /.idea/.idea.CamoDotNet/.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.config/dotnet-tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "isRoot": true, 4 | "tools": { 5 | "nuke.globaltool": { 6 | "version": "5.1.1", 7 | "commands": [ 8 | "nuke" 9 | ] 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /.idea/.idea.CamoDotNet/.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/.idea.CamoDotNet/.idea/indexLayout.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/CamoDotNet.Sample/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "CamoDotNet.Sample": { 4 | "commandName": "Project", 5 | "launchBrowser": true, 6 | "launchUrl": "http://localhost:5000", 7 | "environmentVariables": { 8 | "ASPNETCORE_ENVIRONMENT": "Development" 9 | } 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /.idea/.idea.CamoDotNet/.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Rider ignored files 5 | /contentModel.xml 6 | /modules.xml 7 | /projectSettingsUpdater.xml 8 | /.idea.CamoDotNet.iml 9 | # Datasource local storage ignored files 10 | /dataSources/ 11 | /dataSources.local.xml 12 | # Editor-based HTTP Client requests 13 | /httpRequests/ 14 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | *.sln merge=union 7 | *.csproj merge=union 8 | *.vbproj merge=union 9 | *.fsproj merge=union 10 | *.dbproj merge=union 11 | 12 | # Standard to msysgit 13 | *.doc diff=astextplain 14 | *.DOC diff=astextplain 15 | *.docx diff=astextplain 16 | *.DOCX diff=astextplain 17 | *.dot diff=astextplain 18 | *.DOT diff=astextplain 19 | *.pdf diff=astextplain 20 | *.PDF diff=astextplain 21 | *.rtf diff=astextplain 22 | *.RTF diff=astextplain -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) Maarten Balliauw. All rights reserved. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use 4 | these files except in compliance with the License. You may obtain a copy of the 5 | License at 6 | 7 | [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) 8 | 9 | Unless required by applicable law or agreed to in writing, software distributed 10 | under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 11 | CONDITIONS OF ANY KIND, either express or implied. See the License for the 12 | specific language governing permissions and limitations under the License. 13 | -------------------------------------------------------------------------------- /src/CamoDotNet/Extensions/PathStringExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Maarten Balliauw. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using Microsoft.AspNetCore.Http; 5 | 6 | namespace CamoDotNet.Extensions 7 | { 8 | public static class PathStringExtensions 9 | { 10 | public static PathString RemovePrefix(this PathString current, PathString prefix) 11 | { 12 | if (prefix.HasValue) 13 | { 14 | return new PathString(current.Value.Substring(prefix.Value.Length)); 15 | } 16 | return current; 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /src/CamoDotNet.Sample/Program.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Maarten Balliauw. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using Microsoft.AspNetCore.Hosting; 5 | using Microsoft.Extensions.Hosting; 6 | 7 | namespace CamoDotNet.Sample 8 | { 9 | public class Program 10 | { 11 | public static void Main(string[] args) 12 | { 13 | CreateHostBuilder(args).Build().Run(); 14 | } 15 | 16 | public static IHostBuilder CreateHostBuilder(string[] args) => 17 | Host.CreateDefaultBuilder(args) 18 | .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); }); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tests/CamoDotNet.Tests/PathStringExtensionsFacts.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Maarten Balliauw. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using Microsoft.AspNetCore.Http; 5 | using CamoDotNet.Extensions; 6 | using Xunit; 7 | 8 | namespace CamoDotNet.Tests 9 | { 10 | public class PathStringExtensionsFacts 11 | { 12 | [Theory] 13 | [InlineData("/foo/bar/baz", "/foo", "/bar/baz")] 14 | [InlineData("/foo/bar/baz", "/foo/bar", "/baz")] 15 | public void RemovesPathPrefixFromPathString(string current, string prefix, string expected) 16 | { 17 | var result = new PathString(current).RemovePrefix(new PathString(prefix)); 18 | 19 | Assert.Equal(expected, result.Value); 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /src/CamoDotNet.Core/CamoDotNet.Core.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | True 6 | CamoDotNet is all about making insecure assets look secure. This is an SSL image proxy to prevent mixed content warnings on secure pages. Package contains client-side code. 7 | CamoDotNet;https;ssl;image proxy 8 | true 9 | true 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /tests/CamoDotNet.Tests/CamoSignatureFacts.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Maarten Balliauw. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System.Security.Cryptography; 5 | using System.Text; 6 | using CamoDotNet.Core; 7 | using Xunit; 8 | 9 | namespace CamoDotNet.Tests 10 | { 11 | public class CamoSignatureFacts 12 | { 13 | [Theory] 14 | [InlineData("https://raw.githubusercontent.com/NuGet/Home/dev/resources/nuget.png", "10453F3F3F3F3F6D413F3F3F3F75493F3F3F3F73126E3F472F3F3F3F3F023F5F")] 15 | public void MatchesValidSignature(string url, string signature) 16 | { 17 | var target = new CamoSignature(new HMACSHA256(Encoding.ASCII.GetBytes("TEST1234"))); 18 | var result = target.IsValidSignature(url, signature); 19 | 20 | Assert.True(result); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/CamoDotNet.Core/CamoUrlHelper.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Maarten Balliauw. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using CamoDotNet.Core.Extensions; 5 | 6 | namespace CamoDotNet.Core 7 | { 8 | public class CamoUrlHelper 9 | { 10 | private readonly CamoSignature _signature; 11 | private readonly string _serverUrl; 12 | 13 | public CamoUrlHelper(CamoSignature signature, string serverUrl) 14 | { 15 | _signature = signature; 16 | _serverUrl = serverUrl; 17 | } 18 | 19 | public string GenerateUrl(string originalUrl) 20 | { 21 | return string.Format("{0}/{1}/{2}", 22 | _serverUrl.TrimEnd('/'), 23 | _signature.GenerateSignature(originalUrl), 24 | originalUrl.ToHex()); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/CamoDotNet.Sample/CamoDotNet.Sample.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | true 6 | Exe 7 | 8 | 9 | 10 | 11 | PreserveNewest 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | <_ContentIncludedByDefault Remove="Properties\launchSettings.json" /> 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/CamoDotNet.Core/CamoSignature.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Maarten Balliauw. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System.Security.Cryptography; 5 | using System.Text; 6 | using CamoDotNet.Core.Extensions; 7 | 8 | namespace CamoDotNet.Core 9 | { 10 | public class CamoSignature 11 | { 12 | private readonly HMAC _hmac; 13 | 14 | public CamoSignature(HMAC hmac) 15 | { 16 | _hmac = hmac; 17 | _hmac.Initialize(); 18 | } 19 | 20 | public bool IsValidSignature(string url, string signature) 21 | { 22 | return signature == GenerateSignature(url); 23 | } 24 | 25 | public string GenerateSignature(string stringToSign) 26 | { 27 | return Encoding.ASCII.GetString( 28 | _hmac.ComputeHash(Encoding.ASCII.GetBytes(stringToSign))) 29 | .ToHex(); 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /tests/CamoDotNet.Tests/CamoDotNet.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | all 17 | runtime; build; native; contentfiles; analyzers; buildtransitive 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/CamoDotNet.Core/Extensions/StringExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Maarten Balliauw. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | 6 | namespace CamoDotNet.Core.Extensions 7 | { 8 | public static class StringExtensions 9 | { 10 | public static string FromHex(this string from) 11 | { 12 | var result = ""; 13 | while (from.Length > 0) 14 | { 15 | result += Convert.ToChar(Convert.ToUInt32(from.Substring(0, 2), 16)).ToString(); 16 | from = from.Substring(2, from.Length - 2); 17 | } 18 | return result; 19 | } 20 | 21 | public static string ToHex(this string from) 22 | { 23 | var result = ""; 24 | foreach (var c in from) 25 | { 26 | int tmp = c; 27 | result += string.Format("{0:X2}", Convert.ToUInt32(tmp.ToString())); 28 | } 29 | return result; 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /src/CamoDotNet/CamoDotNet.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | True 6 | CamoDotNet is all about making insecure assets look secure. This is an SSL image proxy to prevent mixed content warnings on secure pages. Package contains server-side code. 7 | CamoDotNet;https;ssl;image proxy 8 | true 9 | true 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/CamoDotNet/CamoServerSettings.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Maarten Balliauw. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System.Security.Cryptography; 5 | using System.Text; 6 | 7 | namespace CamoDotNet 8 | { 9 | public class CamoServerSettings 10 | { 11 | private const string DefaultUserAgent = "CamoDotNet Asset Proxy/4.0.0"; 12 | 13 | public HMAC SharedKey { get; } 14 | public string UserAgent { get; set; } 15 | public int ContentLengthLimit { get; } 16 | 17 | public CamoServerSettings(HMACSHA256 sharedKey, string userAgent, int contentLengthLimit) 18 | { 19 | SharedKey = sharedKey; 20 | UserAgent = userAgent; 21 | ContentLengthLimit = contentLengthLimit; 22 | } 23 | 24 | public static CamoServerSettings GetDefault(string sharedKey) 25 | { 26 | return new CamoServerSettings( 27 | new HMACSHA256(Encoding.ASCII.GetBytes(sharedKey)), 28 | DefaultUserAgent, 29 | 5242880); 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /src/CamoDotNet/CamoServerAppBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Maarten Balliauw. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System.Net.Http; 5 | using Microsoft.AspNetCore.Builder; 6 | using Microsoft.AspNetCore.Http; 7 | 8 | namespace CamoDotNet 9 | { 10 | public static class CamoServerApplicationBuilderExtensions 11 | { 12 | public static IApplicationBuilder UseCamoServer(this IApplicationBuilder builder, CamoServerSettings settings, HttpClient httpClient) 13 | { 14 | return UseCamoServer(builder, new PathString(), settings, httpClient); 15 | } 16 | 17 | public static IApplicationBuilder UseCamoServer(this IApplicationBuilder builder, string pathMatch, CamoServerSettings settings, HttpClient httpClient) 18 | { 19 | return UseCamoServer(builder, new PathString(pathMatch), settings, httpClient); 20 | } 21 | 22 | public static IApplicationBuilder UseCamoServer(this IApplicationBuilder builder, PathString pathMatch, CamoServerSettings settings, HttpClient httpClient) 23 | { 24 | var server = new CamoServer(pathMatch, settings, httpClient); 25 | 26 | builder.Use(async (context, next) => 27 | { 28 | await next(); 29 | await server.Invoke(context); 30 | }); 31 | 32 | return builder; 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /tests/CamoDotNet.Tests/CamoServerFactsBase.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Maarten Balliauw. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Net.Http; 6 | using CamoDotNet.Core; 7 | using Microsoft.AspNetCore.Builder; 8 | using Microsoft.AspNetCore.Hosting; 9 | using Microsoft.AspNetCore.TestHost; 10 | 11 | namespace CamoDotNet.Tests 12 | { 13 | public abstract class CamoServerFactsBase 14 | { 15 | private const string SharedKeyForTests = "TEST1234"; 16 | 17 | protected virtual TestServer CreateServer() 18 | { 19 | return new TestServer(new WebHostBuilder() 20 | .UseStartup()); 21 | } 22 | 23 | protected string GenerateSignedUrl(string url) 24 | { 25 | var helper = new CamoUrlHelper(new CamoSignature( 26 | CamoServerSettings.GetDefault(SharedKeyForTests).SharedKey), ""); 27 | 28 | return helper.GenerateUrl(url); 29 | } 30 | 31 | // ReSharper disable once ClassNeverInstantiated.Local 32 | private class TestStartup 33 | { 34 | // ReSharper disable once UnusedMember.Local 35 | public void Configure(IApplicationBuilder app) 36 | { 37 | app.UseCamoServer( 38 | CamoServerSettings.GetDefault(SharedKeyForTests), 39 | new HttpClient { Timeout = TimeSpan.FromSeconds(10) }); 40 | } 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /Directory.Build.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 4 | disable 5 | 6 | 7 | package_icon.png 8 | True 9 | True 10 | true 11 | true 12 | true 13 | 14 | Maarten Balliauw 15 | Maarten Balliauw 16 | 4.1.0 17 | 4.1.0 18 | Maarten Balliauw 19 | portable 20 | https://github.com/maartenba/CamoDotNet 21 | https://github.com/maartenba/CamoDotNet 22 | Apache-2.0 23 | true 24 | $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb 25 | CamoDotNet 26 | 27 | 28 | 29 | 30 | true 31 | / 32 | 33 | 34 | -------------------------------------------------------------------------------- /tests/CamoDotNet.Tests/StringExtensionsFacts.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Maarten Balliauw. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using CamoDotNet.Core.Extensions; 5 | using Xunit; 6 | 7 | namespace CamoDotNet.Tests 8 | { 9 | public class StringExtensionsFacts 10 | { 11 | [Theory] 12 | [InlineData("test", "74657374")] 13 | [InlineData("test1234", "7465737431323334")] 14 | [InlineData("TEST", "54455354")] 15 | [InlineData("TEST1234", "5445535431323334")] 16 | [InlineData("This is a test", "5468697320697320612074657374")] 17 | [InlineData("https://raw.githubusercontent.com/NuGet/Home/dev/resources/nuget.png", "68747470733A2F2F7261772E67697468756275736572636F6E74656E742E636F6D2F4E754765742F486F6D652F6465762F7265736F75726365732F6E756765742E706E67")] 18 | public void ToHexReturnsProperHex(string input, string expected) 19 | { 20 | var result = input.ToHex(); 21 | Assert.Equal(expected, result); 22 | } 23 | 24 | [Theory] 25 | [InlineData("74657374", "test")] 26 | [InlineData("7465737431323334", "test1234")] 27 | [InlineData("54455354", "TEST")] 28 | [InlineData("5445535431323334", "TEST1234")] 29 | [InlineData("5468697320697320612074657374", "This is a test")] 30 | [InlineData("68747470733A2F2F7261772E67697468756275736572636F6E74656E742E636F6D2F4E754765742F486F6D652F6465762F7265736F75726365732F6E756765742E706E67", "https://raw.githubusercontent.com/NuGet/Home/dev/resources/nuget.png")] 31 | public void FromHexReturnsProperString(string input, string expected) 32 | { 33 | var result = input.FromHex(); 34 | Assert.Equal(expected, result); 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /tests/CamoDotNet.Tests/CamoServerProxyFacts.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Maarten Balliauw. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System.Net; 5 | using System.Net.Http; 6 | using System.Threading.Tasks; 7 | using Xunit; 8 | 9 | namespace CamoDotNet.Tests 10 | { 11 | public class CamoServerProxyFacts 12 | : CamoServerFactsBase 13 | { 14 | [Fact] 15 | public async Task ProxiesValidUrl() 16 | { 17 | using (var server = CreateServer()) 18 | { 19 | HttpResponseMessage response = await server.CreateClient().GetAsync(GenerateSignedUrl("https://raw.githubusercontent.com/NuGet/Home/dev/meta/resources/nuget.png")); 20 | 21 | Assert.Equal(HttpStatusCode.OK, response.StatusCode); 22 | Assert.IsType(response.Content); 23 | } 24 | } 25 | 26 | [Fact] 27 | public async Task DoesNotProxyInvalidContentType() 28 | { 29 | using (var server = CreateServer()) 30 | { 31 | HttpResponseMessage response = await server.CreateClient().GetAsync(GenerateSignedUrl("https://www.github.com")); 32 | 33 | Assert.Contains("Non-Image content-type returned", await response.Content.ReadAsStringAsync()); 34 | Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); 35 | } 36 | } 37 | 38 | [Fact] 39 | public async Task DoesNotProxyInvalidUrl() 40 | { 41 | using (var server = CreateServer()) 42 | { 43 | HttpResponseMessage response = await server.CreateClient().GetAsync(GenerateSignedUrl("https://www.github.com.thisdoesnotexist")); 44 | 45 | Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); 46 | } 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /src/CamoDotNet.Sample/Startup.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Maarten Balliauw. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Net.Http; 6 | using CamoDotNet.Core; 7 | using JetBrains.Annotations; 8 | using Microsoft.AspNetCore.Builder; 9 | using Microsoft.AspNetCore.Hosting; 10 | using Microsoft.AspNetCore.Http; 11 | using Microsoft.Extensions.DependencyInjection; 12 | using Microsoft.Extensions.Hosting; 13 | using Microsoft.Extensions.Logging; 14 | 15 | namespace CamoDotNet.Sample 16 | { 17 | public class Startup 18 | { 19 | [UsedImplicitly] 20 | public void ConfigureServices(IServiceCollection services) 21 | { 22 | } 23 | 24 | [UsedImplicitly] 25 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory) 26 | { 27 | if (env.IsDevelopment()) 28 | { 29 | app.UseDeveloperExceptionPage(); 30 | } 31 | 32 | var camoServerSettings = CamoServerSettings.GetDefault("TEST1234"); 33 | var camoUrlHelper = new CamoUrlHelper( 34 | new CamoSignature(camoServerSettings.SharedKey), "/camo"); 35 | 36 | app.UseCamoServer( 37 | "/camo", 38 | camoServerSettings, 39 | new HttpClient { Timeout = TimeSpan.FromSeconds(10) }); 40 | 41 | app.Use(async (context, next) => 42 | { 43 | if (context.Request.Path.Value == "/") 44 | { 45 | await context.Response.WriteAsync(@" 46 | 47 | CamoDotNet example 48 | 49 | 50 | 51 | "); 52 | } 53 | }); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /tests/CamoDotNet.Tests/CamoServerDefaultDocumentFacts.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Maarten Balliauw. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System.Net; 5 | using System.Net.Http; 6 | using System.Threading.Tasks; 7 | using Xunit; 8 | 9 | namespace CamoDotNet.Tests 10 | { 11 | public class CamoServerSignatureFacts 12 | : CamoServerFactsBase 13 | { 14 | [Fact] 15 | public async Task Returns404NotFoundForChecksumMismatchWithPathFormat() 16 | { 17 | using (var server = CreateServer()) 18 | { 19 | var response = await server.CreateClient().GetAsync("/74657374/68747470733a2f2f7261772e67697468756275736572636f6e74656e742e636f6d2f4e754765742f486f6d652f6465762f7265736f75726365732f6e756765742e706e67"); 20 | 21 | Assert.Contains("checksum mismatch", await response.Content.ReadAsStringAsync()); 22 | Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); 23 | } 24 | } 25 | 26 | [Fact] 27 | public async Task Returns404NotFoundForChecksumMismatchWithQueryFormat() 28 | { 29 | using (var server = CreateServer()) 30 | { 31 | HttpResponseMessage response = await server.CreateClient().GetAsync("/74657374?url=http%3A%2F%2Fwww.nuget.org%2Ftest"); 32 | 33 | Assert.Contains("checksum mismatch", await response.Content.ReadAsStringAsync()); 34 | Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); 35 | } 36 | } 37 | } 38 | public class CamoServerDefaultDocumentFacts 39 | : CamoServerFactsBase 40 | { 41 | [Fact] 42 | public async Task Returns405MethodNotAllowedForPostToRootUrl() 43 | { 44 | using (var server = CreateServer()) 45 | { 46 | HttpResponseMessage response = await server.CreateClient().PostAsync("/", new StringContent("data")); 47 | 48 | Assert.Equal(HttpStatusCode.MethodNotAllowed, response.StatusCode); 49 | } 50 | } 51 | 52 | [Fact] 53 | public async Task Returns200OkForRootUrl() 54 | { 55 | using (var server = CreateServer()) 56 | { 57 | HttpResponseMessage response = await server.CreateClient().GetAsync("/"); 58 | 59 | Assert.Equal(HttpStatusCode.OK, response.StatusCode); 60 | } 61 | } 62 | 63 | [Fact] 64 | public async Task Returns200OkForFavIconUrl() 65 | { 66 | using (var server = CreateServer()) 67 | { 68 | HttpResponseMessage response = await server.CreateClient().GetAsync("/favicon.ico"); 69 | 70 | Assert.Equal(HttpStatusCode.OK, response.StatusCode); 71 | } 72 | } 73 | } 74 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CamoDotNet 2 | 3 | CamoDotNet is a .NET port of [camo](https://github.com/atmos/camo). It is all about making insecure assets look secure. This is an SSL image proxy to prevent mixed content warnings on secure pages. 4 | 5 | [Check the GitHub blog](https://github.com/blog/743-sidejack-prevention-phase-3-ssl-proxied-assets) for background on why camo exists. 6 | 7 | Using a shared key, proxy URLs are encrypted with [hmac](http://en.wikipedia.org/wiki/HMAC) so we can bust caches/ban/rate limit if needed. 8 | 9 | CamoDotNet currently runs on: 10 | 11 | * CamoDotNet 1.x - OWIN 3.0 12 | * CamoDotNet 2.x - .NET Core 13 | * CamoDotNet 3.x - .NET Standard 2.0 14 | * CamoDotNet 4.x - .NET Core 3.1 15 | 16 | ## Features 17 | 18 | * Max size for proxied images 19 | * Restricts proxied images content-types to a whitelist 20 | * Forward images regardless of HTTP status code 21 | 22 | ## URL Formats 23 | 24 | CamoDotNet supports two distinct URL formats: 25 | 26 | http://example.org/?url= 27 | http://example.org// 28 | 29 | The `` is a 40 character hex encoded HMAC digest generated with a shared secret key and the unescaped `` value. 30 | 31 | The `` is the absolute URL locating an image. In the first format, the `` should be 32 | URL escaped aggressively to ensure the original value isn't mangled in transit. 33 | 34 | In the second format, each byte of the `` should be hex encoded such that the resulting value includes only characters `[0-9a-f]`. 35 | 36 | ## Usage 37 | 38 | ### Server 39 | 40 | The CamoDotNet server is implemented as an OWIN middleware and can be added to any OWIN application, either as a middleware (using `IAppBuilder.Use`) or as the main server (`using IAppBuilder.Run`). The following example bootstraps a CamoDotNetServer under the `/camo` path. 41 | 42 | public class Startup 43 | { 44 | public void Configuration(IAppBuilder app) // or IApplicationBuilder in .NET Core 45 | { 46 | var camoServerSettings = CamoServerSettings.GetDefault("shared_key_goes_here"); 47 | var camoUrlHelper = new CamoUrlHelper( 48 | new CamoSignature(camoServerSettings.SharedKey), "/camo"); 49 | 50 | app.UseCamoServer( 51 | "/camo", 52 | camoServerSettings, 53 | new HttpClient { Timeout = TimeSpan.FromSeconds(10) }); 54 | } 55 | } 56 | 57 | The `CamoDotNet.Sample` project contains a minimal sample of embedding CamoDotNet in an application. 58 | 59 | ### Client 60 | 61 | All the client has to to is render an `` tag that references a proxied image. URLs can be generated manually, using the URL format described above. Another option is by using the `CamoDotNet.Core.CamoUrlHelper` class: 62 | 63 | var helper = new CamoUrlHelper(new CamoSignature( 64 | CamoServerSettings.GetDefault("shared_key_goes_here").SharedKey), "https://camo-url/"); 65 | 66 | return helper.GenerateUrl(url); 67 | 68 | The `CamoDotNet.Sample` project contains a minimal sample that renders an image proxied through CamoDotNet. 69 | 70 | ## Configuration 71 | 72 | CamoDotNet comes with several configuration options which can be specified as a parameter to the CamoDotNet server. 73 | 74 | * `SharedKey`: The shared key used to generate the HMAC digest. 75 | * `UserAgent`: The string for Camo to include in the `Via` and `User-Agent` headers it sends in requests to origin servers. (default: `CamoDotNet Asset Proxy/1.0`) 76 | * `ContentLengthLimit`: The maximum `Content-Length` Camo will proxy. (default: 5242880) 77 | 78 | 79 | -------------------------------------------------------------------------------- /CamoDotNet.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26228.4 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CamoDotNet.Core", "src\CamoDotNet.Core\CamoDotNet.Core.csproj", "{A21C940A-B9FD-4429-A006-CEFC1A3AFB55}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CamoDotNet", "src\CamoDotNet\CamoDotNet.csproj", "{B838049F-4D0D-4229-AAD6-8667805D5CE8}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CamoDotNet.Sample", "src\CamoDotNet.Sample\CamoDotNet.Sample.csproj", "{DC140795-C99E-4ADD-88BB-1702453DAF7E}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CamoDotNet.Tests", "tests\CamoDotNet.Tests\CamoDotNet.Tests.csproj", "{3EF5B812-9C88-436C-8DFD-BC33B5F5DAD9}" 13 | EndProject 14 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_", "_", "{8228B746-A658-471D-A7AD-B3DA16F13863}" 15 | ProjectSection(SolutionItems) = preProject 16 | Directory.Build.targets = Directory.Build.targets 17 | global.json = global.json 18 | package_icon.png = package_icon.png 19 | EndProjectSection 20 | EndProject 21 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{7A1F3EAD-8EB8-446A-85AF-1958ACF40C76}" 22 | EndProject 23 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{5E2C75FD-F1E9-4CE4-AED4-21957691F09F}" 24 | EndProject 25 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".config", ".config", "{C4E252F2-5FA6-45BB-B9E6-E9956B6B307F}" 26 | ProjectSection(SolutionItems) = preProject 27 | .config\dotnet-tools.json = .config\dotnet-tools.json 28 | EndProjectSection 29 | EndProject 30 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "_build", "build\_build.csproj", "{526A9D9A-11E4-421F-8EE3-F3489AF2CBFF}" 31 | EndProject 32 | Global 33 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 34 | Debug|Any CPU = Debug|Any CPU 35 | Release|Any CPU = Release|Any CPU 36 | EndGlobalSection 37 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 38 | {526A9D9A-11E4-421F-8EE3-F3489AF2CBFF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 39 | {526A9D9A-11E4-421F-8EE3-F3489AF2CBFF}.Release|Any CPU.ActiveCfg = Release|Any CPU 40 | {A21C940A-B9FD-4429-A006-CEFC1A3AFB55}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 41 | {A21C940A-B9FD-4429-A006-CEFC1A3AFB55}.Debug|Any CPU.Build.0 = Debug|Any CPU 42 | {A21C940A-B9FD-4429-A006-CEFC1A3AFB55}.Release|Any CPU.ActiveCfg = Release|Any CPU 43 | {A21C940A-B9FD-4429-A006-CEFC1A3AFB55}.Release|Any CPU.Build.0 = Release|Any CPU 44 | {B838049F-4D0D-4229-AAD6-8667805D5CE8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 45 | {B838049F-4D0D-4229-AAD6-8667805D5CE8}.Debug|Any CPU.Build.0 = Debug|Any CPU 46 | {B838049F-4D0D-4229-AAD6-8667805D5CE8}.Release|Any CPU.ActiveCfg = Release|Any CPU 47 | {B838049F-4D0D-4229-AAD6-8667805D5CE8}.Release|Any CPU.Build.0 = Release|Any CPU 48 | {DC140795-C99E-4ADD-88BB-1702453DAF7E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 49 | {DC140795-C99E-4ADD-88BB-1702453DAF7E}.Debug|Any CPU.Build.0 = Debug|Any CPU 50 | {DC140795-C99E-4ADD-88BB-1702453DAF7E}.Release|Any CPU.ActiveCfg = Release|Any CPU 51 | {DC140795-C99E-4ADD-88BB-1702453DAF7E}.Release|Any CPU.Build.0 = Release|Any CPU 52 | {3EF5B812-9C88-436C-8DFD-BC33B5F5DAD9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 53 | {3EF5B812-9C88-436C-8DFD-BC33B5F5DAD9}.Debug|Any CPU.Build.0 = Debug|Any CPU 54 | {3EF5B812-9C88-436C-8DFD-BC33B5F5DAD9}.Release|Any CPU.ActiveCfg = Release|Any CPU 55 | {3EF5B812-9C88-436C-8DFD-BC33B5F5DAD9}.Release|Any CPU.Build.0 = Release|Any CPU 56 | EndGlobalSection 57 | GlobalSection(SolutionProperties) = preSolution 58 | HideSolutionNode = FALSE 59 | EndGlobalSection 60 | GlobalSection(NestedProjects) = preSolution 61 | {B838049F-4D0D-4229-AAD6-8667805D5CE8} = {7A1F3EAD-8EB8-446A-85AF-1958ACF40C76} 62 | {A21C940A-B9FD-4429-A006-CEFC1A3AFB55} = {7A1F3EAD-8EB8-446A-85AF-1958ACF40C76} 63 | {DC140795-C99E-4ADD-88BB-1702453DAF7E} = {7A1F3EAD-8EB8-446A-85AF-1958ACF40C76} 64 | {3EF5B812-9C88-436C-8DFD-BC33B5F5DAD9} = {5E2C75FD-F1E9-4CE4-AED4-21957691F09F} 65 | {C4E252F2-5FA6-45BB-B9E6-E9956B6B307F} = {8228B746-A658-471D-A7AD-B3DA16F13863} 66 | {526A9D9A-11E4-421F-8EE3-F3489AF2CBFF} = {8228B746-A658-471D-A7AD-B3DA16F13863} 67 | EndGlobalSection 68 | EndGlobal 69 | -------------------------------------------------------------------------------- /src/CamoDotNet/CamoServer.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Maarten Balliauw. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Net; 8 | using System.Net.Http; 9 | using System.Net.Http.Headers; 10 | using System.Threading.Tasks; 11 | using CamoDotNet.Core; 12 | using CamoDotNet.Core.Extensions; 13 | using CamoDotNet.Extensions; 14 | using Microsoft.AspNetCore.Http; 15 | 16 | namespace CamoDotNet 17 | { 18 | public class CamoServer 19 | { 20 | private readonly PathString _pathPrefix; 21 | private readonly CamoServerSettings _settings; 22 | private readonly HttpClient _httpClient; 23 | private readonly CamoSignature _signature; 24 | 25 | private readonly string[] _supportedMediaTypes = 26 | { 27 | "image/bmp", 28 | "image/cgm", 29 | "image/g3fax", 30 | "image/gif", 31 | "image/ief", 32 | "image/jp2", 33 | "image/jpeg", 34 | "image/jpg", 35 | "image/pict", 36 | "image/png", 37 | "image/prs.btif", 38 | "image/svg+xml", 39 | "image/tiff", 40 | "image/vnd.adobe.photoshop", 41 | "image/vnd.djvu", 42 | "image/vnd.dwg", 43 | "image/vnd.dxf", 44 | "image/vnd.fastbidsheet", 45 | "image/vnd.fpx", 46 | "image/vnd.fst", 47 | "image/vnd.fujixerox.edmics-mmr", 48 | "image/vnd.fujixerox.edmics-rlc", 49 | "image/vnd.microsoft.icon", 50 | "image/vnd.ms-modi", 51 | "image/vnd.net-fpx", 52 | "image/vnd.wap.wbmp", 53 | "image/vnd.xiff", 54 | "image/webp", 55 | "image/x-cmu-raster", 56 | "image/x-cmx", 57 | "image/x-icon", 58 | "image/x-macpaint", 59 | "image/x-pcx", 60 | "image/x-pict", 61 | "image/x-portable-anymap", 62 | "image/x-portable-bitmap", 63 | "image/x-portable-graymap", 64 | "image/x-portable-pixmap", 65 | "image/x-quicktime", 66 | "image/x-rgb", 67 | "image/x-xbitmap", 68 | "image/x-xpixmap", 69 | "image/x-xwindowdump" 70 | }; 71 | 72 | private readonly Dictionary _defaultHeaders = new Dictionary 73 | { 74 | {"X-Frame-Options", "deny"}, 75 | {"X-XSS-Protection", "1; mode=block"}, 76 | {"X-Content-Type-Options", "nosniff"}, 77 | {"Content-Security-Policy", "default-src 'none'; img-src data:; style-src 'unsafe-inline'"}, 78 | //{ "Strict-Transport-Security", "max-age=31536000; includeSubDomains" } 79 | }; 80 | 81 | public CamoServer(CamoServerSettings settings, HttpClient httpClient) 82 | : this(new PathString(), settings, httpClient) 83 | { 84 | } 85 | 86 | public CamoServer(PathString pathPrefix, CamoServerSettings settings, HttpClient httpClient) 87 | { 88 | _pathPrefix = pathPrefix; 89 | _settings = settings; 90 | _httpClient = httpClient; 91 | _signature = new CamoSignature(_settings.SharedKey); 92 | } 93 | 94 | public async Task Invoke(HttpContext context) 95 | { 96 | // does our path match? 97 | if (!context.Request.Path.StartsWithSegments(_pathPrefix)) 98 | { 99 | return; 100 | } 101 | 102 | // check request method 103 | if (context.Request.Method != "GET") 104 | { 105 | await WriteHead(context.Response, HttpStatusCode.MethodNotAllowed, _defaultHeaders); 106 | return; 107 | } 108 | 109 | // check incoming URL 110 | var requestPath = context.Request.Path.RemovePrefix(_pathPrefix); 111 | switch (requestPath.Value) 112 | { 113 | case "/": 114 | case "/favicon.ico": 115 | await WriteHead(context.Response, HttpStatusCode.OK, _defaultHeaders); 116 | return; 117 | } 118 | 119 | // parse parameters 120 | var parameters = requestPath.Value.TrimStart('/').Split(new [] { '/' }, 2); 121 | 122 | string url; 123 | string signature = parameters[0]; 124 | if (parameters.Length == 2) 125 | { 126 | url = parameters[1].FromHex(); 127 | } 128 | else 129 | { 130 | url = context.Request.Query["url"]; 131 | } 132 | 133 | // validate signature 134 | if (!_signature.IsValidSignature(url, signature)) 135 | { 136 | await WriteInvalidSignature(context.Response, url, signature); 137 | return; 138 | } 139 | 140 | // is it a loop? 141 | if ((context.Request.Headers.ContainsKey("User-Agent") && context.Request.Headers["User-Agent"] == _settings.UserAgent) 142 | || (context.Request.Headers.ContainsKey("Via") && context.Request.Headers["Via"].Contains(_settings.UserAgent))) 143 | { 144 | await WriteHead(context.Response, HttpStatusCode.BadRequest, _defaultHeaders); 145 | return; 146 | } 147 | 148 | // proxy the request 149 | var upstreamRequest = new HttpRequestMessage(HttpMethod.Get, url); 150 | upstreamRequest.Headers.UserAgent.ParseAdd(_settings.UserAgent); 151 | upstreamRequest.Headers.Via.ParseAdd("1.1 camo (" + _settings.UserAgent + ")"); 152 | await TransferHeaders(context.Request.Headers, upstreamRequest.Headers); 153 | HttpResponseMessage upstreamResponse; 154 | try 155 | { 156 | upstreamResponse = await _httpClient.SendAsync(upstreamRequest); 157 | } 158 | catch (HttpRequestException) 159 | { 160 | await WriteHead(context.Response, HttpStatusCode.BadRequest, _defaultHeaders); 161 | return; 162 | } 163 | 164 | using (var upstreamResponseStream = await upstreamResponse.Content.ReadAsStreamAsync()) 165 | { 166 | // validate response 167 | if (upstreamResponseStream.Length > _settings.ContentLengthLimit) 168 | { 169 | await WriteContentLengthExceeded(context.Response, _defaultHeaders); 170 | return; 171 | } 172 | 173 | var contentTypes = upstreamResponse.Content.Headers.ContentType != null 174 | ? upstreamResponse.Content.Headers.ContentType.MediaType.Split(new [] { ";" }, StringSplitOptions.RemoveEmptyEntries) 175 | : new string[0]; 176 | if (contentTypes.Length == 0 || !_supportedMediaTypes.Any( 177 | mt => contentTypes.Any(ct => ct.Equals(mt, StringComparison.OrdinalIgnoreCase)))) 178 | { 179 | await WriteContentTypeUnsupported(context.Response, upstreamResponse.Content.Headers.ContentType?.MediaType, _defaultHeaders); 180 | return; 181 | } 182 | 183 | // stream response 184 | var headers = new Dictionary(_defaultHeaders) 185 | { 186 | { "Via", "1.1 camo (" + _settings.UserAgent + ")" } 187 | }; 188 | if (upstreamResponse.Headers.ETag != null) 189 | { 190 | headers.Add("Etag", upstreamResponse.Headers.ETag.ToString()); 191 | } 192 | if (upstreamResponse.Content.Headers.LastModified.HasValue) 193 | { 194 | headers.Add("last-modified", upstreamResponse.Content.Headers.LastModified.ToString()); 195 | } 196 | 197 | context.Response.StatusCode = (int) upstreamResponse.StatusCode; 198 | await WriteHeaders(context.Response, headers); 199 | context.Response.ContentType = upstreamResponse.Content.Headers.ContentType.ToString(); 200 | 201 | await upstreamResponseStream.CopyToAsync(context.Response.Body); 202 | } 203 | } 204 | 205 | private Task TransferHeaders(IHeaderDictionary sourceHeaders, HttpRequestHeaders destinationHeaders) 206 | { 207 | destinationHeaders.Add("Accept", sourceHeaders.ContainsKey("Accept") 208 | ? sourceHeaders["Accept"].ToString() 209 | : "image/*"); 210 | 211 | destinationHeaders.Add("Accept-Encoding", sourceHeaders.ContainsKey("Accept-Encoding") 212 | ? sourceHeaders["Accept-Encoding"].ToString() 213 | : string.Empty); 214 | 215 | destinationHeaders.Add("X-Frame-Options", _defaultHeaders["X-Frame-Options"]); 216 | 217 | destinationHeaders.Add("X-XSS-Protection", _defaultHeaders["X-XSS-Protection"]); 218 | 219 | destinationHeaders.Add("X-Content-Type-Options", _defaultHeaders["X-Content-Type-Options"]); 220 | 221 | //destinationHeaders.Add("Content-Security-Policy", _defaultHeaders["Content-Security-Policy"]); 222 | 223 | return Task.CompletedTask; 224 | } 225 | 226 | private Task WriteHeaders(HttpResponse response, Dictionary headers) 227 | { 228 | foreach (var headerPair in headers) 229 | { 230 | response.Headers.Append(headerPair.Key, headerPair.Value); 231 | } 232 | 233 | return Task.CompletedTask; 234 | } 235 | 236 | private async Task WriteInvalidSignature(HttpResponse response, string url, string signature) 237 | { 238 | response.StatusCode = (int)HttpStatusCode.NotFound; 239 | await response.WriteAsync(string.Format("checksum mismatch {0}:{1}", url, signature)); 240 | } 241 | 242 | private async Task WriteContentLengthExceeded(HttpResponse response, Dictionary headers) 243 | { 244 | response.StatusCode = (int)HttpStatusCode.NotFound; 245 | await WriteHeaders(response, headers); 246 | await response.WriteAsync("Content-Length exceeded"); 247 | } 248 | 249 | private async Task WriteContentTypeUnsupported(HttpResponse response, string contentTypeReturned, Dictionary headers) 250 | { 251 | response.StatusCode = (int)HttpStatusCode.NotFound; 252 | await WriteHeaders(response, headers); 253 | await response.WriteAsync(string.Format("Non-Image content-type returned '{0}'", contentTypeReturned ?? "unspecified")); 254 | } 255 | 256 | private async Task WriteHead(HttpResponse response, HttpStatusCode statusCode, Dictionary headers) 257 | { 258 | response.StatusCode = (int)statusCode; 259 | await WriteHeaders(response, headers); 260 | } 261 | } 262 | } --------------------------------------------------------------------------------