|  | 
| 2 | 2 | // The .NET Foundation licenses this file to you under the MIT license. | 
| 3 | 3 | 
 | 
| 4 | 4 | using System.Net; | 
|  | 5 | +using System.Net.Security; | 
|  | 6 | +using System.Security.Cryptography; | 
|  | 7 | +using System.Security.Cryptography.X509Certificates; | 
|  | 8 | +using Microsoft.DotNet.Cli.Utils; | 
| 5 | 9 | using Microsoft.Extensions.Logging; | 
| 6 | 10 | using Microsoft.NET.Build.Containers.Resources; | 
|  | 11 | +using System.Net.Sockets; | 
| 7 | 12 | using Moq; | 
| 8 | 13 | 
 | 
| 9 | 14 | namespace Microsoft.NET.Build.Containers.UnitTests; | 
| @@ -390,11 +395,166 @@ public async Task UploadBlobChunkedAsync_Failure() | 
| 390 | 395 |         api.Verify(api => api.Blob.Upload.UploadChunkAsync(It.IsIn(absoluteUploadUri, uploadPath), It.IsAny<HttpContent>(), It.IsAny<CancellationToken>()), Times.Exactly(1)); | 
| 391 | 396 |     } | 
| 392 | 397 | 
 | 
|  | 398 | +    [InlineData(true, true)] | 
|  | 399 | +    [InlineData(true, false)] | 
|  | 400 | +    [InlineData(false, true)] | 
|  | 401 | +    [InlineData(false, false)] | 
|  | 402 | +    [Theory] | 
|  | 403 | +    public async Task InsecureRegistry(bool serverIsHttps, bool isInsecureRegistry) | 
|  | 404 | +    { | 
|  | 405 | +        ILogger logger = _loggerFactory.CreateLogger(nameof(InsecureRegistry)); | 
|  | 406 | + | 
|  | 407 | +        // Start a dummy HTTP server that response with 200 OK. | 
|  | 408 | +        using TcpListener listener = new TcpListener(IPAddress.Loopback, 0); | 
|  | 409 | +        listener.Start(); | 
|  | 410 | +        IPEndPoint endpoint = (listener.LocalEndpoint as IPEndPoint)!; | 
|  | 411 | +        Uri registryUri = new Uri($"https://{endpoint.Address}:{endpoint.Port}"); | 
|  | 412 | +        SslServerAuthenticationOptions? sslOptions = null!; | 
|  | 413 | +        if (serverIsHttps) | 
|  | 414 | +        { | 
|  | 415 | +            var key = RSA.Create(2048); | 
|  | 416 | +            var request = new CertificateRequest("CN=localhost", key, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); | 
|  | 417 | +            X509Certificate2 serverCertificate = request.CreateSelfSigned(DateTimeOffset.Now, DateTimeOffset.Now.AddYears(1)); | 
|  | 418 | +            sslOptions = new SslServerAuthenticationOptions() | 
|  | 419 | +            { | 
|  | 420 | +                ServerCertificate = serverCertificate, | 
|  | 421 | +                ClientCertificateRequired = false | 
|  | 422 | +            }; | 
|  | 423 | +        } | 
|  | 424 | +        _ = Task.Run(async () => | 
|  | 425 | +        { | 
|  | 426 | +            while (true) | 
|  | 427 | +            { | 
|  | 428 | +                using TcpClient client = await listener.AcceptTcpClientAsync(); | 
|  | 429 | +                try | 
|  | 430 | +                { | 
|  | 431 | +                    using Stream stream = serverIsHttps ? new SslStream(client.GetStream(), leaveInnerStreamOpen: false) : client.GetStream(); | 
|  | 432 | +                    if (stream is SslStream sslStream) | 
|  | 433 | +                    { | 
|  | 434 | +                        await sslStream.AuthenticateAsServerAsync(sslOptions!, default(CancellationToken)); | 
|  | 435 | +                    } | 
|  | 436 | +                    await stream.WriteAsync("HTTP/1.0 200 OK\r\nContent-Length: 0\r\n\r\n"u8.ToArray()); | 
|  | 437 | +                } | 
|  | 438 | +                catch | 
|  | 439 | +                { } | 
|  | 440 | +            } | 
|  | 441 | +        }); | 
|  | 442 | + | 
|  | 443 | +        RegistrySettings settings = new() | 
|  | 444 | +        { | 
|  | 445 | +            IsInsecure = isInsecureRegistry | 
|  | 446 | +        }; | 
|  | 447 | +        Registry registry = new(registryUri, logger, settings: settings); | 
|  | 448 | + | 
|  | 449 | +        // Make a request. | 
|  | 450 | +        Task getManifest = registry.GetImageManifestAsync(repositoryName: "dotnet/runtime", reference: "latest", runtimeIdentifier: "linux-x64", manifestPicker: null!, cancellationToken: default!); | 
|  | 451 | + | 
|  | 452 | +        if (isInsecureRegistry) | 
|  | 453 | +        { | 
|  | 454 | +            // Falls back to http (when serverIsHttps is false) or ignores https certificate errors (when serverIsHttps is true). | 
|  | 455 | +            // Results in throwing: CONTAINER2003: The manifest for dotnet/runtime:latest from registry hwas an unknown type. | 
|  | 456 | +            await Assert.ThrowsAsync<NotImplementedException>(() => getManifest); | 
|  | 457 | +        } | 
|  | 458 | +        else | 
|  | 459 | +        { | 
|  | 460 | +            // Does not fall back and throws HttpRequestException. | 
|  | 461 | +            var requestException = await Assert.ThrowsAsync<HttpRequestException>(() => getManifest); | 
|  | 462 | +            Assert.Equal(HttpRequestError.SecureConnectionError, requestException.HttpRequestError); | 
|  | 463 | +        } | 
|  | 464 | +    } | 
|  | 465 | + | 
|  | 466 | +    [InlineData("localhost", null, true)] | 
|  | 467 | +    [InlineData("localhost:5000", null, true)] | 
|  | 468 | +    [InlineData("public.ecr.aws", null, false)] | 
|  | 469 | +    [InlineData("public.ecr.aws", "public.ecr.aws", true)] | 
|  | 470 | +    [InlineData("public.ecr.aws", "Public.ecr.aws", true)] // ignore case | 
|  | 471 | +    [InlineData("public.ecr.aws", "public.ecr.aws;docker.io", true)] // multiple registries | 
|  | 472 | +    [InlineData("public.ecr.aws", ";public.ecr.aws ;  docker.io ", true)] // ignore whitespace | 
|  | 473 | +    [InlineData("public.ecr.aws", "public.ecr.aws2;docker.io ", false)] // full name match | 
|  | 474 | +    [Theory] | 
|  | 475 | +    public void IsRegistryInsecure(string registryName, string? insecureRegistriesEnvvar, bool expectedInsecure) | 
|  | 476 | +    { | 
|  | 477 | +        var environment = new Dictionary<string, string>(); | 
|  | 478 | +        if (insecureRegistriesEnvvar is not null) | 
|  | 479 | +        { | 
|  | 480 | +            environment["SDK_CONTAINER_INSECURE_REGISTRIES"] = insecureRegistriesEnvvar; | 
|  | 481 | +        } | 
| 393 | 482 | 
 | 
|  | 483 | +        var registrySettings = new RegistrySettings(registryName, new MockEnvironmentProvider(environment)); | 
|  | 484 | + | 
|  | 485 | +        Assert.Equal(expectedInsecure, registrySettings.IsInsecure); | 
|  | 486 | +    } | 
| 394 | 487 | 
 | 
| 395 | 488 |     private static NextChunkUploadInformation ChunkUploadSuccessful(Uri requestUri, Uri uploadUrl, int? contentLength, HttpStatusCode code = HttpStatusCode.Accepted) | 
| 396 | 489 |     { | 
| 397 | 490 |         return new(uploadUrl); | 
| 398 | 491 |     } | 
| 399 | 492 | 
 | 
|  | 493 | +    private class MockEnvironmentProvider : IEnvironmentProvider | 
|  | 494 | +    { | 
|  | 495 | +        private readonly IDictionary<string, string> _environmentVariables; | 
|  | 496 | + | 
|  | 497 | +        public MockEnvironmentProvider(IDictionary<string, string> environmentVariables) | 
|  | 498 | +        { | 
|  | 499 | +            _environmentVariables = environmentVariables; | 
|  | 500 | +        } | 
|  | 501 | + | 
|  | 502 | +        public bool GetEnvironmentVariableAsBool(string name, bool defaultValue) | 
|  | 503 | +        { | 
|  | 504 | +            var str = Environment.GetEnvironmentVariable(name); | 
|  | 505 | +            if (string.IsNullOrEmpty(str)) | 
|  | 506 | +            { | 
|  | 507 | +                return defaultValue; | 
|  | 508 | +            } | 
|  | 509 | + | 
|  | 510 | +            switch (str.ToLowerInvariant()) | 
|  | 511 | +            { | 
|  | 512 | +                case "true": | 
|  | 513 | +                case "1": | 
|  | 514 | +                case "yes": | 
|  | 515 | +                    return true; | 
|  | 516 | +                case "false": | 
|  | 517 | +                case "0": | 
|  | 518 | +                case "no": | 
|  | 519 | +                    return false; | 
|  | 520 | +                default: | 
|  | 521 | +                    return defaultValue; | 
|  | 522 | +            } | 
|  | 523 | +        } | 
|  | 524 | + | 
|  | 525 | +        public string? GetEnvironmentVariable(string name) | 
|  | 526 | +        { | 
|  | 527 | +            string? value; | 
|  | 528 | +            _environmentVariables.TryGetValue(name, out value); | 
|  | 529 | +            return value; | 
|  | 530 | +        } | 
|  | 531 | + | 
|  | 532 | +        public string? GetEnvironmentVariable(string variable, EnvironmentVariableTarget target) | 
|  | 533 | +            => GetEnvironmentVariable(variable); | 
|  | 534 | + | 
|  | 535 | +        public int? GetEnvironmentVariableAsNullableInt(string variable) | 
|  | 536 | +        { | 
|  | 537 | +            if (GetEnvironmentVariable(variable) is string strValue && int.TryParse(strValue, out int intValue)) | 
|  | 538 | +            { | 
|  | 539 | +                return intValue; | 
|  | 540 | +            } | 
|  | 541 | + | 
|  | 542 | +            return null; | 
|  | 543 | +        } | 
|  | 544 | + | 
|  | 545 | +        public void SetEnvironmentVariable(string variable, string value, EnvironmentVariableTarget target) | 
|  | 546 | +            => throw new NotImplementedException(); | 
|  | 547 | + | 
|  | 548 | +        public IEnumerable<string> ExecutableExtensions | 
|  | 549 | +            => throw new NotImplementedException(); | 
|  | 550 | + | 
|  | 551 | +        public string GetCommandPath(string commandName, params string[] extensions) | 
|  | 552 | +            => throw new NotImplementedException(); | 
|  | 553 | + | 
|  | 554 | +        public string GetCommandPathFromRootPath(string rootPath, string commandName, params string[] extensions) | 
|  | 555 | +            => throw new NotImplementedException(); | 
|  | 556 | + | 
|  | 557 | +        public string GetCommandPathFromRootPath(string rootPath, string commandName, IEnumerable<string> extensions) | 
|  | 558 | +            => throw new NotImplementedException(); | 
|  | 559 | +    } | 
| 400 | 560 | } | 
0 commit comments