From ad51b11d4b524e00bd7fa79217798277a9aff133 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Tue, 20 Oct 2020 11:03:43 +0200 Subject: [PATCH] Cleanup of https handling and reverse proxy handling. --- Dockerfile | 3 - .../DefaultCertificateAccountStore.cs | 59 ------------- .../DefaultCertificateStore.cs | 73 ---------------- .../Squidex.Domain.Users.csproj | 1 - .../Pipeline/CleanupHostMiddleware.cs | 4 + .../Pipeline/EnforceHttpsMiddleware.cs | 49 ----------- backend/src/Squidex.Web/UrlsOptions.cs | 8 -- .../src/Squidex/Config/Web/WebExtensions.cs | 33 ------- backend/src/Squidex/Config/Web/WebServices.cs | 36 -------- backend/src/Squidex/Squidex.csproj | 1 - backend/src/Squidex/Startup.cs | 1 - backend/src/Squidex/appsettings.json | 22 ----- .../DefaultCertificateAccountStoreTests.cs | 51 ----------- .../DefaultCertificateStoreTests.cs | 72 ---------------- .../Pipeline/CleanupHostMiddlewareTests.cs | 1 + .../Pipeline/EnforceHttpsMiddlewareTests.cs | 85 ------------------- 16 files changed, 5 insertions(+), 494 deletions(-) delete mode 100644 backend/src/Squidex.Domain.Users/DefaultCertificateAccountStore.cs delete mode 100644 backend/src/Squidex.Domain.Users/DefaultCertificateStore.cs delete mode 100644 backend/src/Squidex.Web/Pipeline/EnforceHttpsMiddleware.cs delete mode 100644 backend/tests/Squidex.Domain.Users.Tests/DefaultCertificateAccountStoreTests.cs delete mode 100644 backend/tests/Squidex.Domain.Users.Tests/DefaultCertificateStoreTests.cs delete mode 100644 backend/tests/Squidex.Web.Tests/Pipeline/EnforceHttpsMiddlewareTests.cs diff --git a/Dockerfile b/Dockerfile index 8c7732eae..cbcaac61c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -68,10 +68,7 @@ WORKDIR /app COPY --from=backend /build/ . COPY --from=frontend /build/ wwwroot/build/ -ENV ASPNETCORE_URLS=https://+;http://+ - EXPOSE 80 -EXPOSE 443 EXPOSE 11111 ENTRYPOINT ["dotnet", "Squidex.dll"] diff --git a/backend/src/Squidex.Domain.Users/DefaultCertificateAccountStore.cs b/backend/src/Squidex.Domain.Users/DefaultCertificateAccountStore.cs deleted file mode 100644 index 8bab5b35f..000000000 --- a/backend/src/Squidex.Domain.Users/DefaultCertificateAccountStore.cs +++ /dev/null @@ -1,59 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading; -using System.Threading.Tasks; -using LettuceEncrypt.Accounts; -using Squidex.Infrastructure; -using Squidex.Infrastructure.States; - -namespace Squidex.Domain.Users -{ - public sealed class DefaultCertificateAccountStore : IAccountStore - { - private readonly ISnapshotStore store; - - [CollectionName("Identity_CertificateAccount")] - public sealed class State - { - public AccountModel Account { get; set; } - - public State() - { - } - - public State(AccountModel account) - { - Account = account; - } - } - - public DefaultCertificateAccountStore(ISnapshotStore store) - { - Guard.NotNull(store, nameof(store)); - - this.store = store; - } - - public async Task GetAccountAsync(CancellationToken cancellationToken) - { - var (value, _) = await store.ReadAsync(default); - - return value?.Account; - } - - public Task SaveAccountAsync(AccountModel account, CancellationToken cancellationToken) - { - Guard.NotNull(account, nameof(account)); - - var state = new State(account); - - return store.WriteAsync(default, state, EtagVersion.Any, 0); - } - } -} diff --git a/backend/src/Squidex.Domain.Users/DefaultCertificateStore.cs b/backend/src/Squidex.Domain.Users/DefaultCertificateStore.cs deleted file mode 100644 index c56abdc9e..000000000 --- a/backend/src/Squidex.Domain.Users/DefaultCertificateStore.cs +++ /dev/null @@ -1,73 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Security.Cryptography.X509Certificates; -using System.Threading; -using System.Threading.Tasks; -using LettuceEncrypt; -using Squidex.Infrastructure; -using Squidex.Infrastructure.States; - -namespace Squidex.Domain.Users -{ - public sealed class DefaultCertificateStore : ICertificateRepository, ICertificateSource - { - private readonly ISnapshotStore store; - - [CollectionName("Identity_Certificates")] - public sealed class State - { - public byte[] Certificate { get; set; } - - public State() - { - } - - public State(X509Certificate2 certificate) - { - Certificate = certificate.Export(X509ContentType.Pfx); - } - - public X509Certificate2 ToCertificate() - { - return new X509Certificate2(Certificate); - } - } - - public DefaultCertificateStore(ISnapshotStore store) - { - Guard.NotNull(store, nameof(store)); - - this.store = store; - } - - public async Task> GetCertificatesAsync(CancellationToken cancellationToken = default) - { - var result = new List(); - - await store.ReadAllAsync((state, _) => - { - result.Add(state.ToCertificate()); - - return Task.CompletedTask; - }, cancellationToken); - - return result; - } - - public Task SaveAsync(X509Certificate2 certificate, CancellationToken cancellationToken = default) - { - Guard.NotNull(certificate, nameof(certificate)); - - var state = new State(certificate); - - return store.WriteAsync(Guid.NewGuid(), state, EtagVersion.Any, 0); - } - } -} diff --git a/backend/src/Squidex.Domain.Users/Squidex.Domain.Users.csproj b/backend/src/Squidex.Domain.Users/Squidex.Domain.Users.csproj index f9ca6cdd6..c9ff573a8 100644 --- a/backend/src/Squidex.Domain.Users/Squidex.Domain.Users.csproj +++ b/backend/src/Squidex.Domain.Users/Squidex.Domain.Users.csproj @@ -17,7 +17,6 @@ - diff --git a/backend/src/Squidex.Web/Pipeline/CleanupHostMiddleware.cs b/backend/src/Squidex.Web/Pipeline/CleanupHostMiddleware.cs index 8457f52a5..038504c38 100644 --- a/backend/src/Squidex.Web/Pipeline/CleanupHostMiddleware.cs +++ b/backend/src/Squidex.Web/Pipeline/CleanupHostMiddleware.cs @@ -16,6 +16,7 @@ namespace Squidex.Web.Pipeline { private readonly RequestDelegate next; private readonly HostString host; + private readonly string schema; public CleanupHostMiddleware(RequestDelegate next, IOptions options) { @@ -31,11 +32,14 @@ namespace Squidex.Web.Pipeline { host = new HostString(uri.Host, uri.Port); } + + schema = uri.Scheme.ToLowerInvariant(); } public Task InvokeAsync(HttpContext context) { context.Request.Host = host; + context.Request.Scheme = schema; return next(context); } diff --git a/backend/src/Squidex.Web/Pipeline/EnforceHttpsMiddleware.cs b/backend/src/Squidex.Web/Pipeline/EnforceHttpsMiddleware.cs deleted file mode 100644 index 1262769f0..000000000 --- a/backend/src/Squidex.Web/Pipeline/EnforceHttpsMiddleware.cs +++ /dev/null @@ -1,49 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Options; -using Squidex.Infrastructure; - -namespace Squidex.Web.Pipeline -{ - public sealed class EnforceHttpsMiddleware : IMiddleware - { - private readonly UrlsOptions urlsOptions; - - public EnforceHttpsMiddleware(IOptions urlsOptions) - { - Guard.NotNull(urlsOptions, nameof(urlsOptions)); - - this.urlsOptions = urlsOptions.Value; - } - - public async Task InvokeAsync(HttpContext context, RequestDelegate next) - { - if (!urlsOptions.EnforceHTTPS) - { - await next(context); - } - else - { - var hostName = context.Request.Host.ToString().ToLowerInvariant(); - - if (!context.Request.IsHttps) - { - var newUrl = string.Concat("https://", hostName, context.Request.Path, context.Request.QueryString); - - context.Response.Redirect(newUrl, true); - } - else - { - await next(context); - } - } - } - } -} diff --git a/backend/src/Squidex.Web/UrlsOptions.cs b/backend/src/Squidex.Web/UrlsOptions.cs index f00d16d73..53503fa16 100644 --- a/backend/src/Squidex.Web/UrlsOptions.cs +++ b/backend/src/Squidex.Web/UrlsOptions.cs @@ -18,14 +18,6 @@ namespace Squidex.Web private string baseUrl; private string[] trustedHosts; - public bool EnableXForwardedHost { get; set; } - - public bool EnableLetsEncrypt { get; set; } - - public bool EnforceHTTPS { get; set; } - - public string Email { get; set; } = "admin@squidex.io"; - public string BaseUrl { get diff --git a/backend/src/Squidex/Config/Web/WebExtensions.cs b/backend/src/Squidex/Config/Web/WebExtensions.cs index 2671186ad..ba5ae8e1e 100644 --- a/backend/src/Squidex/Config/Web/WebExtensions.cs +++ b/backend/src/Squidex/Config/Web/WebExtensions.cs @@ -12,7 +12,6 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Diagnostics.HealthChecks; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.HttpOverrides; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.HealthChecks; @@ -131,39 +130,7 @@ namespace Squidex.Config.Web public static void UseSquidexForwardingRules(this IApplicationBuilder app, IConfiguration config) { - app.UseForwardedHeaders(GetForwardingOptions(config)); - - app.UseMiddleware(); - app.UseMiddleware(); } - - private static ForwardedHeadersOptions GetForwardingOptions(IConfiguration config) - { - var urlsOptions = config.GetSection("urls").Get(); - - if (!string.IsNullOrWhiteSpace(urlsOptions.BaseUrl) && urlsOptions.EnableXForwardedHost) - { - return new ForwardedHeadersOptions - { - AllowedHosts = new List - { - new Uri(urlsOptions.BaseUrl).Host - }, - ForwardedHeaders = ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost, - ForwardLimit = null, - RequireHeaderSymmetry = false - }; - } - else - { - return new ForwardedHeadersOptions - { - ForwardedHeaders = ForwardedHeaders.XForwardedProto, - ForwardLimit = null, - RequireHeaderSymmetry = false - }; - } - } } } diff --git a/backend/src/Squidex/Config/Web/WebServices.cs b/backend/src/Squidex/Config/Web/WebServices.cs index 92dc41d6d..809113739 100644 --- a/backend/src/Squidex/Config/Web/WebServices.cs +++ b/backend/src/Squidex/Config/Web/WebServices.cs @@ -5,9 +5,6 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System; -using LettuceEncrypt; -using LettuceEncrypt.Accounts; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Infrastructure; @@ -17,7 +14,6 @@ using Microsoft.Extensions.Localization; using Microsoft.Extensions.Options; using Squidex.Config.Domain; using Squidex.Domain.Apps.Entities; -using Squidex.Domain.Users; using Squidex.Infrastructure.Caching; using Squidex.Infrastructure.Translations; using Squidex.Pipeline.Plugins; @@ -55,9 +51,6 @@ namespace Squidex.Config.Web services.AddSingletonAs() .AsSelf(); - services.AddSingletonAs() - .AsSelf(); - services.AddSingletonAs() .AsSelf(); @@ -108,34 +101,5 @@ namespace Squidex.Config.Web .AddSquidexPlugins(config) .AddSquidexSerializers(); } - - public static void AddSquidexLetsEncrypt(this IServiceCollection services, IConfiguration config) - { - var urlsOptions = config.GetSection("urls").Get(); - - if (!urlsOptions.EnableLetsEncrypt) - { - return; - } - - services.AddLettuceEncrypt(options => - { - options.AcceptTermsOfService = true; - - options.DomainNames = new[] - { - new Uri(urlsOptions.BaseUrl).Host - }; - - options.EmailAddress = urlsOptions.Email; - }); - - services.AddSingletonAs() - .As() - .As(); - - services.AddSingletonAs() - .As(); - } } } diff --git a/backend/src/Squidex/Squidex.csproj b/backend/src/Squidex/Squidex.csproj index b15271ad2..2fb0153d2 100644 --- a/backend/src/Squidex/Squidex.csproj +++ b/backend/src/Squidex/Squidex.csproj @@ -39,7 +39,6 @@ - diff --git a/backend/src/Squidex/Startup.cs b/backend/src/Squidex/Startup.cs index fb227e754..22f3d276e 100644 --- a/backend/src/Squidex/Startup.cs +++ b/backend/src/Squidex/Startup.cs @@ -41,7 +41,6 @@ namespace Squidex services.AddNonBreakingSameSiteCookies(); services.AddSquidexMvcWithPlugins(config); - services.AddSquidexLetsEncrypt(config); services.AddSquidexApps(); services.AddSquidexAssetInfrastructure(config); diff --git a/backend/src/Squidex/appsettings.json b/backend/src/Squidex/appsettings.json index 657a4e144..a1c3f2fa6 100644 --- a/backend/src/Squidex/appsettings.json +++ b/backend/src/Squidex/appsettings.json @@ -11,28 +11,6 @@ * Set the base url of your application, to generate correct urls in background process. */ "baseUrl": "https://localhost:5001", - - /* - * Set it to true to redirect the user from http to https permanently. - */ - "enforceHttps": false, - - /* - * Set it to true to use the X-Forwarded-Host header as internal Hostname. - */ - "enableXForwardedHost": false, - - /* - * Enable lets encrypt certificate handling. - * - * Attention: Can only be used in production environment with custom domain names and a single server. - */ - "enableLetsEncrypt": true, - - /* - * The email address for the certificate. - */ - "email": "admin@squidex.io" }, "fullText": { diff --git a/backend/tests/Squidex.Domain.Users.Tests/DefaultCertificateAccountStoreTests.cs b/backend/tests/Squidex.Domain.Users.Tests/DefaultCertificateAccountStoreTests.cs deleted file mode 100644 index a470f3cf0..000000000 --- a/backend/tests/Squidex.Domain.Users.Tests/DefaultCertificateAccountStoreTests.cs +++ /dev/null @@ -1,51 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using FakeItEasy; -using LettuceEncrypt.Accounts; -using Squidex.Infrastructure.States; -using Xunit; - -namespace Squidex.Domain.Users -{ - public class DefaultCertificateAccountStoreTests - { - private readonly ISnapshotStore store = A.Fake>(); - private readonly DefaultCertificateAccountStore sut; - - public DefaultCertificateAccountStoreTests() - { - sut = new DefaultCertificateAccountStore(store); - } - - [Fact] - public async Task Should_read_from_store() - { - var model = new AccountModel(); - - A.CallTo(() => store.ReadAsync(default)) - .Returns((new DefaultCertificateAccountStore.State { Account = model }, 0)); - - var result = await sut.GetAccountAsync(default); - - Assert.Same(model, result); - } - - [Fact] - public async Task Should_write_to_store() - { - var model = new AccountModel(); - - await sut.SaveAccountAsync(model, default); - - A.CallTo(() => store.WriteAsync(A._, A._, A._, 0)) - .MustHaveHappened(); - } - } -} diff --git a/backend/tests/Squidex.Domain.Users.Tests/DefaultCertificateStoreTests.cs b/backend/tests/Squidex.Domain.Users.Tests/DefaultCertificateStoreTests.cs deleted file mode 100644 index 4ae8260eb..000000000 --- a/backend/tests/Squidex.Domain.Users.Tests/DefaultCertificateStoreTests.cs +++ /dev/null @@ -1,72 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Linq; -using System.Security.Cryptography; -using System.Security.Cryptography.X509Certificates; -using System.Threading; -using System.Threading.Tasks; -using FakeItEasy; -using Squidex.Infrastructure.States; -using Xunit; - -namespace Squidex.Domain.Users -{ - public sealed class DefaultCertificateStoreTests - { - private readonly ISnapshotStore store = A.Fake>(); - private readonly DefaultCertificateStore sut; - - public DefaultCertificateStoreTests() - { - sut = new DefaultCertificateStore(store); - } - - [Fact] - public async Task Should_read_from_store() - { - A.CallTo(() => store.ReadAllAsync(A>._, A._)) - .Invokes((Func callback, CancellationToken _) => - { - callback(new DefaultCertificateStore.State - { - Certificate = MakeCert().Export(X509ContentType.Pfx) - }, 0); - - callback(new DefaultCertificateStore.State - { - Certificate = MakeCert().Export(X509ContentType.Pfx) - }, 0); - }); - - var xml = await sut.GetCertificatesAsync(); - - Assert.Equal(2, xml.Count()); - } - - [Fact] - public async Task Should_write_to_store() - { - var certificate = MakeCert(); - - await sut.SaveAsync(certificate, default); - - A.CallTo(() => store.WriteAsync(A._, A._, A._, 0)) - .MustHaveHappened(); - } - - private static X509Certificate2 MakeCert() - { - var ecdsa = ECDsa.Create(); - - var certificateRequest = new CertificateRequest("cn=foobar", ecdsa, HashAlgorithmName.SHA256); - - return certificateRequest.CreateSelfSigned(DateTimeOffset.Now, DateTimeOffset.Now.AddYears(5)); - } - } -} diff --git a/backend/tests/Squidex.Web.Tests/Pipeline/CleanupHostMiddlewareTests.cs b/backend/tests/Squidex.Web.Tests/Pipeline/CleanupHostMiddlewareTests.cs index ffb55236b..706d49a3b 100644 --- a/backend/tests/Squidex.Web.Tests/Pipeline/CleanupHostMiddlewareTests.cs +++ b/backend/tests/Squidex.Web.Tests/Pipeline/CleanupHostMiddlewareTests.cs @@ -53,6 +53,7 @@ namespace Squidex.Web.Pipeline await sut.InvokeAsync(httpContext); Assert.Equal(expectedHost, httpContext.Request.Host.Value); + Assert.Equal(uri.Scheme, httpContext.Request.Scheme); Assert.True(isNextCalled); } } diff --git a/backend/tests/Squidex.Web.Tests/Pipeline/EnforceHttpsMiddlewareTests.cs b/backend/tests/Squidex.Web.Tests/Pipeline/EnforceHttpsMiddlewareTests.cs deleted file mode 100644 index 7eca49342..000000000 --- a/backend/tests/Squidex.Web.Tests/Pipeline/EnforceHttpsMiddlewareTests.cs +++ /dev/null @@ -1,85 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; -using Xunit; -using Options = Microsoft.Extensions.Options.Options; - -namespace Squidex.Web.Pipeline -{ - public class EnforceHttpsMiddlewareTests - { - private readonly RequestDelegate next; - private readonly UrlsOptions options = new UrlsOptions(); - private readonly EnforceHttpsMiddleware sut; - private bool isNextCalled; - - public EnforceHttpsMiddlewareTests() - { - next = context => - { - isNextCalled = true; - - return Task.CompletedTask; - }; - - sut = new EnforceHttpsMiddleware(Options.Create(options)); - } - - [Fact] - public async Task Should_make_permanent_redirect_if_redirect_is_required() - { - var httpContext = CreateHttpContext(); - - options.EnforceHTTPS = true; - - await sut.InvokeAsync(httpContext, next); - - Assert.False(isNextCalled); - Assert.Equal("https://squidex.local/path?query=1", httpContext.Response.Headers["Location"]); - } - - [Fact] - public async Task Should_not_redirect_if_already_on_https() - { - var httpContext = CreateHttpContext("https"); - - options.EnforceHTTPS = true; - - await sut.InvokeAsync(httpContext, next); - - Assert.True(isNextCalled); - Assert.Null((string)httpContext.Response.Headers["Location"]); - } - - [Fact] - public async Task Should_not_redirect_if_not_required() - { - var httpContext = CreateHttpContext(); - - options.EnforceHTTPS = false; - - await sut.InvokeAsync(httpContext, next); - - Assert.True(isNextCalled); - Assert.Null((string)httpContext.Response.Headers["Location"]); - } - - private static DefaultHttpContext CreateHttpContext(string scheme = "http") - { - var httpContext = new DefaultHttpContext(); - - httpContext.Request.QueryString = new QueryString("?query=1"); - httpContext.Request.Host = new HostString("squidex.local"); - httpContext.Request.Path = new PathString("/path"); - httpContext.Request.Scheme = scheme; - - return httpContext; - } - } -}