Browse Source

Cleanup of https handling and reverse proxy handling.

pull/593/head
Sebastian 5 years ago
parent
commit
ad51b11d4b
  1. 3
      Dockerfile
  2. 59
      backend/src/Squidex.Domain.Users/DefaultCertificateAccountStore.cs
  3. 73
      backend/src/Squidex.Domain.Users/DefaultCertificateStore.cs
  4. 1
      backend/src/Squidex.Domain.Users/Squidex.Domain.Users.csproj
  5. 4
      backend/src/Squidex.Web/Pipeline/CleanupHostMiddleware.cs
  6. 49
      backend/src/Squidex.Web/Pipeline/EnforceHttpsMiddleware.cs
  7. 8
      backend/src/Squidex.Web/UrlsOptions.cs
  8. 33
      backend/src/Squidex/Config/Web/WebExtensions.cs
  9. 36
      backend/src/Squidex/Config/Web/WebServices.cs
  10. 1
      backend/src/Squidex/Squidex.csproj
  11. 1
      backend/src/Squidex/Startup.cs
  12. 22
      backend/src/Squidex/appsettings.json
  13. 51
      backend/tests/Squidex.Domain.Users.Tests/DefaultCertificateAccountStoreTests.cs
  14. 72
      backend/tests/Squidex.Domain.Users.Tests/DefaultCertificateStoreTests.cs
  15. 1
      backend/tests/Squidex.Web.Tests/Pipeline/CleanupHostMiddlewareTests.cs
  16. 85
      backend/tests/Squidex.Web.Tests/Pipeline/EnforceHttpsMiddlewareTests.cs

3
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"]

59
backend/src/Squidex.Domain.Users/DefaultCertificateAccountStore.cs

@ -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<State, Guid> store;
[CollectionName("Identity_CertificateAccount")]
public sealed class State
{
public AccountModel Account { get; set; }
public State()
{
}
public State(AccountModel account)
{
Account = account;
}
}
public DefaultCertificateAccountStore(ISnapshotStore<State, Guid> store)
{
Guard.NotNull(store, nameof(store));
this.store = store;
}
public async Task<AccountModel?> 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);
}
}
}

73
backend/src/Squidex.Domain.Users/DefaultCertificateStore.cs

@ -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<State, Guid> 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<State, Guid> store)
{
Guard.NotNull(store, nameof(store));
this.store = store;
}
public async Task<IEnumerable<X509Certificate2>> GetCertificatesAsync(CancellationToken cancellationToken = default)
{
var result = new List<X509Certificate2>();
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);
}
}
}

1
backend/src/Squidex.Domain.Users/Squidex.Domain.Users.csproj

@ -17,7 +17,6 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="IdentityServer4" Version="4.1.1" />
<PackageReference Include="LettuceEncrypt" Version="1.0.0" />
<PackageReference Include="Microsoft.Extensions.Identity.Stores" Version="3.1.8" />
<PackageReference Include="Microsoft.Win32.Registry" Version="4.7.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />

4
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<UrlsOptions> 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);
}

49
backend/src/Squidex.Web/Pipeline/EnforceHttpsMiddleware.cs

@ -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> 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);
}
}
}
}
}

8
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

33
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<EnforceHttpsMiddleware>();
app.UseMiddleware<CleanupHostMiddleware>();
}
private static ForwardedHeadersOptions GetForwardingOptions(IConfiguration config)
{
var urlsOptions = config.GetSection("urls").Get<UrlsOptions>();
if (!string.IsNullOrWhiteSpace(urlsOptions.BaseUrl) && urlsOptions.EnableXForwardedHost)
{
return new ForwardedHeadersOptions
{
AllowedHosts = new List<string>
{
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
};
}
}
}
}

36
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<RobotsTxtMiddleware>()
.AsSelf();
services.AddSingletonAs<EnforceHttpsMiddleware>()
.AsSelf();
services.AddSingletonAs<LocalCacheMiddleware>()
.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<UrlsOptions>();
if (!urlsOptions.EnableLetsEncrypt)
{
return;
}
services.AddLettuceEncrypt(options =>
{
options.AcceptTermsOfService = true;
options.DomainNames = new[]
{
new Uri(urlsOptions.BaseUrl).Host
};
options.EmailAddress = urlsOptions.Email;
});
services.AddSingletonAs<DefaultCertificateStore>()
.As<ICertificateRepository>()
.As<ICertificateSource>();
services.AddSingletonAs<DefaultCertificateAccountStore>()
.As<IAccountStore>();
}
}
}

1
backend/src/Squidex/Squidex.csproj

@ -39,7 +39,6 @@
<PackageReference Include="IdentityServer4" Version="4.1.1" />
<PackageReference Include="IdentityServer4.AccessTokenValidation" Version="3.0.1" />
<PackageReference Include="IdentityServer4.AspNetIdentity" Version="4.1.1" />
<PackageReference Include="LettuceEncrypt" Version="1.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.Google" Version="3.1.8" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.MicrosoftAccount" Version="3.1.8" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.1.8" />

1
backend/src/Squidex/Startup.cs

@ -41,7 +41,6 @@ namespace Squidex
services.AddNonBreakingSameSiteCookies();
services.AddSquidexMvcWithPlugins(config);
services.AddSquidexLetsEncrypt(config);
services.AddSquidexApps();
services.AddSquidexAssetInfrastructure(config);

22
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": {

51
backend/tests/Squidex.Domain.Users.Tests/DefaultCertificateAccountStoreTests.cs

@ -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<DefaultCertificateAccountStore.State, Guid> store = A.Fake<ISnapshotStore<DefaultCertificateAccountStore.State, Guid>>();
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<Guid>._, A<DefaultCertificateAccountStore.State>._, A<long>._, 0))
.MustHaveHappened();
}
}
}

72
backend/tests/Squidex.Domain.Users.Tests/DefaultCertificateStoreTests.cs

@ -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<DefaultCertificateStore.State, Guid> store = A.Fake<ISnapshotStore<DefaultCertificateStore.State, Guid>>();
private readonly DefaultCertificateStore sut;
public DefaultCertificateStoreTests()
{
sut = new DefaultCertificateStore(store);
}
[Fact]
public async Task Should_read_from_store()
{
A.CallTo(() => store.ReadAllAsync(A<Func<DefaultCertificateStore.State, long, Task>>._, A<CancellationToken>._))
.Invokes((Func<DefaultCertificateStore.State, long, Task> 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<Guid>._, A<DefaultCertificateStore.State>._, A<long>._, 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));
}
}
}

1
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);
}
}

85
backend/tests/Squidex.Web.Tests/Pipeline/EnforceHttpsMiddlewareTests.cs

@ -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;
}
}
}
Loading…
Cancel
Save