diff --git a/build/dependencies.props b/build/dependencies.props index 4ce45e6..417ed42 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -3,6 +3,7 @@ $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + 0.9.9 2.1.0 2.1.1-rtm-15790 2.1.0 diff --git a/test/Templates.Test/CdnScriptTagTests.cs b/test/Templates.Test/CdnScriptTagTests.cs index 63e18a2..8351c97 100644 --- a/test/Templates.Test/CdnScriptTagTests.cs +++ b/test/Templates.Test/CdnScriptTagTests.cs @@ -1,107 +1,171 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using AngleSharp.Dom.Html; +using AngleSharp.Parser.Html; using System; using System.Collections.Generic; using System.IO; using System.IO.Compression; +using System.Linq; using System.Net.Http; using System.Security.Cryptography; -using System.Text.RegularExpressions; using System.Threading.Tasks; using Xunit; using Xunit.Abstractions; namespace Templates.Test { - public class CdnScriptTagTests + public class CdnScriptTagTests : IDisposable { private readonly ITestOutputHelper _output; + private readonly HttpClient _httpClient; + private static readonly string _solutionDir; + private static readonly string _artifactsDir; + private static List _scriptTags; + + static CdnScriptTagTests() + { + _solutionDir = GetSolutionDir(); + _artifactsDir = Path.Combine(_solutionDir, "artifacts", "build"); + var packages = Directory.GetFiles(_artifactsDir, "*.nupkg"); + + _scriptTags = new List(); + foreach (var packagePath in packages) + { + _scriptTags.AddRange(GetScriptTags(packagePath)); + } + } public CdnScriptTagTests(ITestOutputHelper output) { _output = output; + _httpClient = new HttpClient(); } - [Fact] - public async Task CheckSubresourceIntegrity() + public static IEnumerable SubresourceIntegrityCheckData { - var dir = GetSolutionDir(); - var artifactsDir = Path.Combine(dir, "artifacts", "build"); - var packages = Directory.GetFiles(artifactsDir, "*.nupkg"); - - var scriptTags = new List(); - foreach (var packagePath in packages) + get { - scriptTags.AddRange(GetScriptTags(packagePath)); + var scriptTags = _scriptTags + .Where(st => st.Integrity != null) + .Select(st => new object[] { st }); + Assert.NotEmpty(scriptTags); + return scriptTags; } - - Assert.NotEmpty(scriptTags); - var shasum = new Dictionary(); - - var client = new HttpClient(); - foreach (var script in scriptTags) - { - if (shasum.ContainsKey(script.Src)) - { - continue; - } - - using (var resp = await client.GetStreamAsync(script.Src)) - using (var alg = SHA384.Create()) - { - var hash = alg.ComputeHash(resp); - shasum.Add(script.Src, "sha384-" + Convert.ToBase64String(hash)); - } - } - - Assert.All(scriptTags, t => - { - Assert.True(shasum[t.Src] == t.Integrity, userMessage: $"Expected integrity on script tag to be {shasum[t.Src]} but it was {t.Integrity}. {t.FileName}:{t.Entry}"); - }); } - private struct ScriptTag + [Theory] + [MemberData(nameof(SubresourceIntegrityCheckData))] + public async Task CheckSubresourceIntegrity(ScriptTag scriptTag) + { + string expectedIntegrity; + using (var responseStream = await _httpClient.GetStreamAsync(scriptTag.Src)) + using (var alg = SHA384.Create()) + { + var hash = alg.ComputeHash(responseStream); + expectedIntegrity = "sha384-" + Convert.ToBase64String(hash); + } + + Assert.Equal(expectedIntegrity, scriptTag.Integrity); + } + + public static IEnumerable FallbackSrcCheckData + { + get + { + var scriptTags = _scriptTags + .Where(st => st.FallbackSrc != null) + .Select(st => new object[] { st }); + Assert.NotEmpty(scriptTags); + return scriptTags; + } + } + + [Theory] + [MemberData(nameof(FallbackSrcCheckData))] + public async Task FallbackSrcContent_Matches_CDNContent(ScriptTag scriptTag) + { + var fallbackSrc = scriptTag.FallbackSrc + .TrimStart('~') + .TrimStart('/'); + + var cdnContent = await _httpClient.GetStringAsync(scriptTag.Src); + var fallbackSrcContent = GetFileContentFromArchive(scriptTag, fallbackSrc); + + Assert.Equal(RemoveLineEndings(cdnContent), RemoveLineEndings(fallbackSrcContent)); + } + + public struct ScriptTag { public string Src; public string Integrity; + public string FallbackSrc; public string FileName; public string Entry; + + public override string ToString() + { + return $"{Src}, {Entry}"; + } } - private static readonly Regex _scriptRegex = new Regex(@"]*src=""(?'src'http[^""]+)""[^>]*integrity=""(?'integrity'[^""]+)""([^>]*)>", RegexOptions.Multiline); - - private IEnumerable GetScriptTags(string zipFile) + private static string GetFileContentFromArchive(ScriptTag scriptTag, string relativeFilePath) { + var file = Path.Combine(_artifactsDir, scriptTag.FileName); + using (var zip = new ZipArchive(File.OpenRead(file), ZipArchiveMode.Read, leaveOpen: false)) + { + var entry = zip.Entries + .Where(e => e.FullName.EndsWith(relativeFilePath, StringComparison.OrdinalIgnoreCase)) + .FirstOrDefault(); + + if (entry != null) + { + using (var reader = new StreamReader(entry.Open())) + { + return reader.ReadToEnd(); + } + } + } + return null; + } + + private static List GetScriptTags(string zipFile) + { + var scriptTags = new List(); using (var zip = new ZipArchive(File.OpenRead(zipFile), ZipArchiveMode.Read, leaveOpen: false)) { foreach (var entry in zip.Entries) { - if (string.Equals(".cshtml", Path.GetExtension(entry.Name), StringComparison.OrdinalIgnoreCase)) + if (!string.Equals(".cshtml", Path.GetExtension(entry.Name), StringComparison.OrdinalIgnoreCase)) { - string contents; - using (var reader = new StreamReader(entry.Open())) - { - contents = reader.ReadToEnd(); - } + continue; + } - var match = _scriptRegex.Match(contents); - while (match != null && match != Match.Empty) + IHtmlDocument htmlDocument; + var htmlParser = new HtmlParser(); + using (var reader = new StreamReader(entry.Open())) + { + htmlDocument = htmlParser.Parse(entry.Open()); + } + + foreach (var scriptElement in htmlDocument.Scripts) + { + var fallbackSrcAttribute = scriptElement.Attributes + .FirstOrDefault(attr => string.Equals("asp-fallback-src", attr.Name, StringComparison.OrdinalIgnoreCase)); + + scriptTags.Add(new ScriptTag { - var tag = new ScriptTag - { - Src = match.Groups["src"].Value, - Integrity = match.Groups["integrity"].Value, - FileName = Path.GetFileName(zipFile), - Entry = entry.FullName, - }; - yield return tag; - _output.WriteLine($"Found script tag in {tag.FileName}:{tag.Entry}, src='{tag.Src}' integrity='{tag.Integrity}'"); - match = match.NextMatch(); - } + Src = scriptElement.Source, + Integrity = scriptElement.Integrity, + FallbackSrc = fallbackSrcAttribute?.Value, + FileName = Path.GetFileName(zipFile), + Entry = entry.FullName + }); } } } + return scriptTags; } private static string GetSolutionDir() @@ -117,5 +181,15 @@ namespace Templates.Test } return dir.FullName; } + + private static string RemoveLineEndings(string originalString) + { + return originalString.Replace("\r\n", "").Replace("\n", ""); + } + + public void Dispose() + { + _httpClient.Dispose(); + } } } diff --git a/test/Templates.Test/Templates.Test.csproj b/test/Templates.Test/Templates.Test.csproj index 7af1e8e..19230e2 100644 --- a/test/Templates.Test/Templates.Test.csproj +++ b/test/Templates.Test/Templates.Test.csproj @@ -1,4 +1,4 @@ - + netcoreapp2.1 $(DefineConstants);XPLAT @@ -9,6 +9,7 @@ +