/// <summary> /// Constructor. /// </summary> /// <param name="rule">The associated traffic manager rule.</param> /// <param name="frontend">The frontend that generated this mapping.</param> /// <param name="backendName">The backend name.</param> public HostPathMapping(TrafficHttpRule rule, TrafficHttpFrontend frontend, string backendName) { Covenant.Requires <ArgumentNullException>(rule != null); Covenant.Requires <ArgumentNullException>(frontend != null); Covenant.Requires <ArgumentNullException>(!string.IsNullOrEmpty(backendName)); this.Rule = rule; this.Frontend = frontend; this.BackendName = backendName; }
//--------------------------------------------------------------------- // Instance members. /// <summary> /// Constructor. /// </summary> /// <param name="frontend">The associated frontend.</param> public HAProxyHttpFrontend(TrafficHttpFrontend frontend) { Covenant.Requires <ArgumentNullException>(frontend != null); this.Frontend = frontend; }
/// <summary> /// Verify that we can create HTTPS traffic manager rules for a /// site on the proxy port using a specific hostname and various /// path prefixes and then verify that that the traffic manager actually /// works by spinning up a [vegomatic] based service to accept the traffic. /// </summary> /// <param name="testName">Simple name (without spaces) used to ensure that URIs cached for different tests won't conflict.</param> /// <param name="proxyPort">The inbound proxy port.</param> /// <param name="network">The proxy network.</param> /// <param name="trafficManager">The traffic manager.</param> /// <param name="useCache">Optionally enable caching and verify.</param> /// <param name="serviceName">Optionally specifies the backend service name prefix (defaults to <b>vegomatic</b>).</param> /// <returns>The tracking <see cref="Task"/>.</returns> private async Task TestHttpsPrefix(string testName, int proxyPort, string network, TrafficManager trafficManager, bool useCache = false, string serviceName = "vegomatic") { // Append a GUID to the test name to ensure that we won't // conflict with what any previous test runs may have loaded // into the cache. testName += "-" + Guid.NewGuid().ToString("D"); // Verify that we can create an HTTP traffic manager rule for a // site on the proxy port using a specific hostname and then // verify that that the traffic manager actually works by spinning // up a [vegomatic] based service to accept the traffic. var manager = hive.GetReachableManager(); var hostname = testHostname; manager.Connect(); // Allow self-signed certificates. var handler = new HttpClientHandler() { ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator }; using (var client = new TestHttpClient(disableConnectionReuse: true, handler: handler, disposeHandler: true)) { // Add the test certificate. hive.Certificate.Set("test-load-balancer", certificate); // Setup the client to query the [vegomatic] service through the // proxy without needing to configure a hive DNS entry. client.BaseAddress = new Uri($"https://{manager.PrivateAddress}:{proxyPort}/"); client.DefaultRequestHeaders.Host = hostname; // Create the traffic manager rules, one without a path prefix and // some others, some with intersecting prefixes so we can verify // that the longest prefixes are matched first. // // Each rule's backend will be routed to a service whose name // will be constructed from [testName] plus the prefix with the // slashes replaced with dashes. Each service will be configured // to return its name. var prefixes = new PrefixInfo[] { new PrefixInfo("/", $"{serviceName}"), new PrefixInfo("/foo/", $"{serviceName}-foo"), new PrefixInfo("/foo/bar/", $"{serviceName}-foo-bar"), new PrefixInfo("/foobar/", $"{serviceName}-foobar"), new PrefixInfo("/bar/", $"{serviceName}-bar") }; // Spin the services up first in parallel (for speed). Each of // these service will respond to requests with its service name. var tasks = new List <Task>(); foreach (var prefix in prefixes) { tasks.Add(Task.Run( () => { manager.SudoCommand($"docker service create --name {prefix.ServiceName} --network {network} --replicas 1 {vegomaticImage} test-server server-id={prefix.ServiceName}").EnsureSuccess(); })); } await NeonHelper.WaitAllAsync(tasks, TimeSpan.FromSeconds(30)); // Create the traffic manager rules. foreach (var prefix in prefixes) { var rule = new TrafficHttpRule() { Name = prefix.ServiceName, CheckExpect = "status 200", CheckSeconds = 1, }; if (useCache) { rule.Cache = new TrafficHttpCache() { Enabled = true }; } var frontend = new TrafficHttpFrontend() { Host = hostname, ProxyPort = proxyPort, CertName = "test-load-balancer" }; if (!string.IsNullOrEmpty(prefix.Path)) { frontend.PathPrefix = prefix.Path; } rule.Frontends.Add(frontend); rule.Backends.Add( new TrafficHttpBackend() { Server = prefix.ServiceName, Port = 80 }); trafficManager.SetRule(rule, deferUpdate: true); } trafficManager.Update(); // Wait for all of the services to report being ready. await NeonHelper.WaitForAsync( async() => { foreach (var prefix in prefixes) { try { var response = await client.GetAsync(prefix.Path); response.EnsureSuccessStatusCode(); } catch { return(false); } } return(true); }, timeout : TimeSpan.FromSeconds(60), pollTime : TimeSpan.FromSeconds(1)); // Give everything a chance to stablize. await Task.Delay(TimeSpan.FromSeconds(5)); // Now verify that prefix rules route to the correct backend service. foreach (var prefix in prefixes) { var response = await client.GetAsync($"{prefix.Path}{testName}?expires=60"); response.EnsureSuccessStatusCode(); var body = await response.Content.ReadAsStringAsync(); Assert.Equal(prefix.ServiceName, body.Trim()); if (useCache) { // Verify that the request routed through Varnish. Assert.True(ViaVarnish(response)); // This is the first request using the globally unique [testName] // so it should not be a cache hit. Assert.False(CacheHit(response)); } } // If caching is enabled, perform the requests again to ensure that // we see cache hits. if (useCache) { foreach (var prefix in prefixes) { // Request the item again and verify that it was a cache hit. var response = await client.GetAsync($"{prefix.Path}{testName}?expires=60"); response.EnsureSuccessStatusCode(); var body = await response.Content.ReadAsStringAsync(); Assert.Equal(prefix.ServiceName, body.Trim()); Assert.True(CacheHit(response)); } } } }
/// <summary> /// Generates a traffic manager rule for each <see cref="Redirection"/> passed that /// will redirect from one URI to another. /// </summary> /// <param name="trafficManager">The target traffic manager.</param> /// <param name="testName">Used to name the traffic manager rules.</param> /// <param name="singleRule"> /// Pass <c>true</c> to test a single rule with all of the redirections or /// <c>false</c> to test with one redirection per rule. /// </param> /// <param name="redirections">The redirections.</param> private async Task TestRedirect(TrafficManager trafficManager, string testName, bool singleRule, params Redirection[] redirections) { var manager = hive.GetReachableManager(); // We need local DNS mappings for each of the URI hosts to target a hive node. var hosts = new HashSet <string>(StringComparer.InvariantCultureIgnoreCase); foreach (var redirect in redirections) { if (!hosts.Contains(redirect.FromUri.Host)) { hosts.Add(redirect.FromUri.Host); } if (!hosts.Contains(redirect.ToUri.Host)) { hosts.Add(redirect.ToUri.Host); } } foreach (var host in hosts) { hiveFixture.LocalMachineHosts.AddHostAddress(host, manager.PrivateAddress.ToString(), deferCommit: true); } hiveFixture.LocalMachineHosts.Commit(); // Generate and upload a self-signed certificate for each redirect host that // uses HTTPS and upload these to the hive. Each certificate will be named // the same as the hostname. var hostToCertificate = new Dictionary <string, TlsCertificate>(StringComparer.InvariantCultureIgnoreCase); foreach (var redirect in redirections.Where(r => r.FromUri.Scheme == "https")) { var host = redirect.FromUri.Host; if (hostToCertificate.ContainsKey(host)) { continue; } hostToCertificate[host] = TlsCertificate.CreateSelfSigned(host); } foreach (var item in hostToCertificate) { hive.Certificate.Set(item.Key, item.Value); } // Create the traffic manager rule(s). if (singleRule) { var rule = new TrafficHttpRule() { Name = testName, }; foreach (var redirect in redirections) { var frontend = new TrafficHttpFrontend() { Host = redirect.FromUri.Host, ProxyPort = redirect.FromUri.Port, RedirectTo = redirect.ToUri }; if (redirect.FromUri.Scheme == "https") { frontend.CertName = redirect.FromUri.Host; } rule.Frontends.Add(frontend); } trafficManager.SetRule(rule); } else { var redirectIndex = 0; foreach (var redirect in redirections) { var rule = new TrafficHttpRule() { Name = $"{testName}-{redirectIndex}", }; var frontend = new TrafficHttpFrontend() { Host = redirect.FromUri.Host, ProxyPort = redirect.FromUri.Port, RedirectTo = redirect.ToUri }; if (redirect.FromUri.Scheme == "https") { frontend.CertName = redirect.FromUri.Host; } rule.Frontends.Add(frontend); trafficManager.SetRule(rule); redirectIndex++; } } // Give the new rules some time to deploy. await Task.Delay(TimeSpan.FromSeconds(5)); // Now all we need to do is hit all of the redirect [FromUri]s // and verify that we get redirects to the corresponding // [ToUri]s. // Allow self-signed certificates and disable client-side automatic redirect handling // so we'll be able to see the redirect responses. var handler = new HttpClientHandler() { ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator, AllowAutoRedirect = false // We need to see the redirects }; using (var client = new TestHttpClient(disableConnectionReuse: true, handler: handler, disposeHandler: true)) { foreach (var redirect in redirections) { var response = await client.GetAsync(redirect.FromUri); Assert.Equal(HttpStatusCode.Redirect, response.StatusCode); Assert.True(response.Headers.TryGetValues("Location", out var locations)); Assert.Equal(redirect.ToUri.ToString(), locations.Single()); } } }