├── 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 | }
--------------------------------------------------------------------------------