diff --git a/Examples/AspNetPageEmitter/Program.cs b/Examples/AspNetPageEmitter/Program.cs index d2bf5e3..896ac69 100644 --- a/Examples/AspNetPageEmitter/Program.cs +++ b/Examples/AspNetPageEmitter/Program.cs @@ -7,8 +7,17 @@ var app = builder.Build(); -app.MapFallback((HttpContext context) => - context.WriteDocumentAsync(new TestDocument())); +app.Use((context, next) => +{ + context.Response.GetTypedHeaders().CacheControl = new Microsoft.Net.Http.Headers.CacheControlHeaderValue() + { + Private = true, + NoCache = true, + }; + + return next(); +}); +app.MapFallback(new TestDocument().WriteToAsync); await app.RunAsync().ConfigureAwait(false); @@ -21,20 +30,7 @@ class TestDocument : IHtmlDocument Task IHtmlDocument.WriteBodyContentsAsync(HtmlWriter writer, CancellationToken cancellationToken) { writer.WriteElement("p", null, children => children.WriteText("Test bytes.")); + writer.WriteScript(ValidatedScript.ForInlineSource("console.log('test')")); return Task.CompletedTask; } } - -static class Extensions -{ - public static Task WriteDocumentAsync(this HttpContext context, IHtmlDocument document) - { - var request = context.Request; - var response = context.Response; - response.ContentType = "text/html; charset=utf-8"; - var baseUri = $"{request.Scheme}://{request.Host}/"; - response.Headers.ContentSecurityPolicy = $"default-src {baseUri}; base-uri {baseUri}"; - - return document.WriteAsync(response.BodyWriter, context.RequestAborted); - } -} \ No newline at end of file diff --git a/HtmlUtilities/HtmlDocumentExtensions.cs b/HtmlUtilities/HtmlDocumentExtensions.cs index 5f18ad4..c6ea3b6 100644 --- a/HtmlUtilities/HtmlDocumentExtensions.cs +++ b/HtmlUtilities/HtmlDocumentExtensions.cs @@ -1,4 +1,5 @@ -using System.Buffers; +using Microsoft.AspNetCore.Http; +using System.Buffers; namespace HtmlUtilities; @@ -8,19 +9,25 @@ namespace HtmlUtilities; public static class HtmlDocumentExtensions { /// - /// Writes the document to . + /// Writes the document to . /// /// The document to write. - /// Receives the written document. - /// Cancels emission of document data. - /// - public static async Task WriteAsync( - this IHtmlDocument document, - IBufferWriter writer, - CancellationToken cancellationToken = default) + /// Receives the written document. + /// A task that shows completion when the document is written. + public static Task WriteToAsync(this IHtmlDocument document, HttpContext context) { ArgumentNullException.ThrowIfNull(document, nameof(document)); - cancellationToken.ThrowIfCancellationRequested(); + ArgumentNullException.ThrowIfNull(context, nameof(context)); + + var request = context.Request; + var response = context.Response; + response.ContentType = "text/html; charset=utf-8"; + Span cspNonceUtf16 = stackalloc char[32]; + System.Security.Cryptography.RandomNumberGenerator.GetHexString(cspNonceUtf16, true); + response.Headers.ContentSecurityPolicy = $"base-uri {request.Scheme}://{request.Host}/;default-src 'none';script-src 'unsafe-inline' 'nonce-{cspNonceUtf16}'"; + // unsafe-inline only applies to browsers that don't support nonce. Can be removed someday. + + var writer = context.Response.BodyWriter; writer.Write(""u8); - await document.WriteBodyContentsAsync(new HtmlWriter(writer), cancellationToken).ConfigureAwait(false); + return document.WriteBodyContentsAsync(new HtmlWriter(writer, new Validated.ValidatedAttribute("nonce", cspNonceUtf16)), context.RequestAborted); - writer.Write(""u8); + // HTML5 spec doesn't require , so that simplifies things a bit here. } } diff --git a/HtmlUtilities/HtmlUtilities.csproj b/HtmlUtilities/HtmlUtilities.csproj index d8b4ff4..133dfad 100644 --- a/HtmlUtilities/HtmlUtilities.csproj +++ b/HtmlUtilities/HtmlUtilities.csproj @@ -8,4 +8,7 @@ True Recommended + + + diff --git a/HtmlUtilities/HtmlWriter.cs b/HtmlUtilities/HtmlWriter.cs index 505e0e2..2a4bd9d 100644 --- a/HtmlUtilities/HtmlWriter.cs +++ b/HtmlUtilities/HtmlWriter.cs @@ -9,17 +9,24 @@ namespace HtmlUtilities; /// A high-performance writer for HTML content. /// /// UTF-8 is always used. -public readonly struct HtmlWriter +public sealed class HtmlWriter { private static readonly ValidatedElement html = new( ""u8.ToArray(), ""u8.ToArray()); private readonly IBufferWriter writer; + private readonly ValidatedAttribute cspNonce; internal HtmlWriter(IBufferWriter writer) + : this(writer, new ValidatedAttribute()) + { + } + + internal HtmlWriter(IBufferWriter writer, ValidatedAttribute cspNonce) { ArgumentNullException.ThrowIfNull(this.writer = writer, nameof(writer)); + ArgumentNullException.ThrowIfNull(this.cspNonce = cspNonce, nameof(cspNonce)); } /// @@ -317,6 +324,7 @@ public void WriteScript(ValidatedScript script) throw new ArgumentException("script was never initialized.", nameof(script)); writer.Write(""u8); } diff --git a/HtmlUtilities/IHtmlDocument.cs b/HtmlUtilities/IHtmlDocument.cs index 032ad78..d6af62c 100644 --- a/HtmlUtilities/IHtmlDocument.cs +++ b/HtmlUtilities/IHtmlDocument.cs @@ -1,5 +1,4 @@ using HtmlUtilities.Validated; -using System.Buffers; namespace HtmlUtilities; @@ -35,6 +34,8 @@ public interface IHtmlDocument /// By default, directs the viewer to https://github.com/RyanLamansky/html-utilities to learn how to use this function. Task WriteBodyContentsAsync(HtmlWriter writer, CancellationToken cancellationToken = default) { + ArgumentNullException.ThrowIfNull(writer, nameof(writer)); + writer.WriteElement("p", null, children => { writer.WriteText("Visit ");