mirror of
				https://github.com/zoriya/Kyoo.git
				synced 2025-10-31 02:27:11 -04:00 
			
		
		
		
	Add support for RabbitMQ connection string to Backend service
Signed-off-by: Fred Heinecke <fred.heinecke@yahoo.com>
This commit is contained in:
		
							parent
							
								
									73e2eaaf70
								
							
						
					
					
						commit
						79ee70a1a2
					
				| @ -4,6 +4,7 @@ | |||||||
| # http route prefix (will listen to $KYOO_PREFIX/movie for example) | # http route prefix (will listen to $KYOO_PREFIX/movie for example) | ||||||
| KYOO_PREFIX="" | KYOO_PREFIX="" | ||||||
| 
 | 
 | ||||||
|  | # Postgres settings | ||||||
| # POSTGRES_URL=postgres://user:password@hostname:port/dbname?sslmode=verify-full&sslrootcert=/path/to/server.crt&sslcert=/path/to/client.crt&sslkey=/path/to/client.key | # POSTGRES_URL=postgres://user:password@hostname:port/dbname?sslmode=verify-full&sslrootcert=/path/to/server.crt&sslcert=/path/to/client.crt&sslkey=/path/to/client.key | ||||||
| # The behavior of the below variables match what is documented here: | # The behavior of the below variables match what is documented here: | ||||||
| # https://www.postgresql.org/docs/current/libpq-envars.html | # https://www.postgresql.org/docs/current/libpq-envars.html | ||||||
| @ -18,3 +19,12 @@ PGPORT=5432 | |||||||
| # PGSSLROOTCERT=/my/serving.crt | # PGSSLROOTCERT=/my/serving.crt | ||||||
| # PGSSLCERT=/my/client.crt | # PGSSLCERT=/my/client.crt | ||||||
| # PGSSLKEY=/my/client.key | # PGSSLKEY=/my/client.key | ||||||
|  | 
 | ||||||
|  | # RabbitMQ settings | ||||||
|  | # Full list of options: https://www.rabbitmq.com/uri-spec.html, https://www.rabbitmq.com/docs/uri-query-parameters | ||||||
|  | # RABBITMQ_URL=amqps://user:password@rabbitmq-server:1234/vhost?cacertfile=/path/to/cacert.pem&certfile=/path/to/cert.pem&keyfile=/path/to/key.pem&verify=verify_peer&auth_mechanism=EXTERNAL | ||||||
|  | # These values override what is provided the the URL variable | ||||||
|  | RABBITMQ_DEFAULT_USER=guest | ||||||
|  | RABBITMQ_DEFAULT_PASS=guest | ||||||
|  | RABBITMQ_HOST=rabbitmq | ||||||
|  | RABBITMQ_PORT=5672 | ||||||
|  | |||||||
| @ -16,8 +16,11 @@ | |||||||
| // You should have received a copy of the GNU General Public License | // You should have received a copy of the GNU General Public License | ||||||
| // along with Kyoo. If not, see <https://www.gnu.org/licenses/>. | // along with Kyoo. If not, see <https://www.gnu.org/licenses/>. | ||||||
| 
 | 
 | ||||||
|  | using System.Net.Security; | ||||||
|  | using System.Security.Cryptography.X509Certificates; | ||||||
| using Kyoo.Abstractions.Controllers; | using Kyoo.Abstractions.Controllers; | ||||||
| using Microsoft.AspNetCore.Builder; | using Microsoft.AspNetCore.Builder; | ||||||
|  | using Microsoft.AspNetCore.WebUtilities; | ||||||
| using Microsoft.Extensions.Configuration; | using Microsoft.Extensions.Configuration; | ||||||
| using Microsoft.Extensions.DependencyInjection; | using Microsoft.Extensions.DependencyInjection; | ||||||
| using RabbitMQ.Client; | using RabbitMQ.Client; | ||||||
| @ -30,18 +33,215 @@ public static class RabbitMqModule | |||||||
| 	{ | 	{ | ||||||
| 		builder.Services.AddSingleton(_ => | 		builder.Services.AddSingleton(_ => | ||||||
| 		{ | 		{ | ||||||
| 			ConnectionFactory factory = | 			ConnectionFactory factory = new(); | ||||||
| 				new() | 
 | ||||||
| 				{ | 			// See https://www.rabbitmq.com/docs/uri-spec | ||||||
| 					UserName = builder.Configuration.GetValue("RABBITMQ_DEFAULT_USER", "guest"), | 			string? connectionString = builder.Configuration.GetValue<string>("RABBITMQ_URL"); | ||||||
| 					Password = builder.Configuration.GetValue("RABBITMQ_DEFAULT_PASS", "guest"), | 			if (!string.IsNullOrEmpty(connectionString)) | ||||||
| 					HostName = builder.Configuration.GetValue("RABBITMQ_HOST", "rabbitmq"), | 				factory._ConfigureFactoryWithConnectionString(connectionString); | ||||||
| 					Port = builder.Configuration.GetValue("RABBITMQ_PORT", 5672), | 			else | ||||||
| 				}; | 				factory._ConfigureFactoryWithEnvironmentVars(builder.Configuration); | ||||||
| 
 | 
 | ||||||
| 			return factory.CreateConnection(); | 			return factory.CreateConnection(); | ||||||
| 		}); | 		}); | ||||||
| 		builder.Services.AddSingleton<RabbitProducer>(); | 		builder.Services.AddSingleton<RabbitProducer>(); | ||||||
| 		builder.Services.AddSingleton<IScanner, ScannerProducer>(); | 		builder.Services.AddSingleton<IScanner, ScannerProducer>(); | ||||||
| 	} | 	} | ||||||
|  | 
 | ||||||
|  | 	private static void _ConfigureFactoryWithConnectionString( | ||||||
|  | 		this ConnectionFactory factory, | ||||||
|  | 		string? connectionString | ||||||
|  | 	) | ||||||
|  | 	{ | ||||||
|  | 		if (string.IsNullOrEmpty(connectionString)) | ||||||
|  | 			return; | ||||||
|  | 
 | ||||||
|  | 		// Important: setting this property will not use any query parameters, so they must be parsed here instead.. | ||||||
|  | 		factory.Uri = new Uri(connectionString); | ||||||
|  | 
 | ||||||
|  | 		// Support query parameters defined here: | ||||||
|  | 		// https://www.rabbitmq.com/docs/uri-query-parameters | ||||||
|  | 		Dictionary<string, Microsoft.Extensions.Primitives.StringValues> queryParameters = | ||||||
|  | 			QueryHelpers.ParseQuery(factory.Uri.Query); | ||||||
|  | 
 | ||||||
|  | 		queryParameters.TryGetValue( | ||||||
|  | 			"heartbeat", | ||||||
|  | 			out Microsoft.Extensions.Primitives.StringValues heartbeats | ||||||
|  | 		); | ||||||
|  | 		if (int.TryParse(heartbeats.LastOrDefault(), out int heartbeatValue)) | ||||||
|  | 			factory.RequestedHeartbeat = TimeSpan.FromSeconds(heartbeatValue); | ||||||
|  | 
 | ||||||
|  | 		queryParameters.TryGetValue( | ||||||
|  | 			"connection_timeout", | ||||||
|  | 			out Microsoft.Extensions.Primitives.StringValues connectionTimeouts | ||||||
|  | 		); | ||||||
|  | 		if (int.TryParse(connectionTimeouts.LastOrDefault(), out int connectionTimeoutValue)) | ||||||
|  | 			factory.RequestedConnectionTimeout = TimeSpan.FromSeconds(connectionTimeoutValue); | ||||||
|  | 
 | ||||||
|  | 		queryParameters.TryGetValue( | ||||||
|  | 			"channel_max", | ||||||
|  | 			out Microsoft.Extensions.Primitives.StringValues channelMaxValues | ||||||
|  | 		); | ||||||
|  | 		if (ushort.TryParse(channelMaxValues.LastOrDefault(), out ushort channelMaxValue)) | ||||||
|  | 			factory.RequestedChannelMax = channelMaxValue; | ||||||
|  | 
 | ||||||
|  | 		if (!factory.Ssl.Enabled) | ||||||
|  | 			return; | ||||||
|  | 
 | ||||||
|  | 		queryParameters.TryGetValue( | ||||||
|  | 			"cacertfile", | ||||||
|  | 			out Microsoft.Extensions.Primitives.StringValues caCertFiles | ||||||
|  | 		); | ||||||
|  | 		var caCertFile = caCertFiles.LastOrDefault(); | ||||||
|  | 		if (!string.IsNullOrEmpty(caCertFile)) | ||||||
|  | 		{ | ||||||
|  | 			// Load the cert once at startup instead of on every connection. | ||||||
|  | 			X509Certificate2 rootCA = new(caCertFile); | ||||||
|  | 
 | ||||||
|  | 			// This is a custom validator that obeys the set SslPolicyErrors, while also using the CA cert specified in the query string. | ||||||
|  | 			factory.Ssl.CertificateValidationCallback = ( | ||||||
|  | 				sender, | ||||||
|  | 				certificate, | ||||||
|  | 				chain, | ||||||
|  | 				sslPolicyErrors | ||||||
|  | 			) => | ||||||
|  | 			{ | ||||||
|  | 				// If no cert was provided | ||||||
|  | 				if (sslPolicyErrors.HasFlag(SslPolicyErrors.RemoteCertificateNotAvailable)) | ||||||
|  | 				{ | ||||||
|  | 					// Accept the cert anyway if the client was explicitly configured to ignore this. | ||||||
|  | 					if ( | ||||||
|  | 						factory.Ssl.AcceptablePolicyErrors.HasFlag( | ||||||
|  | 							SslPolicyErrors.RemoteCertificateNotAvailable | ||||||
|  | 						) | ||||||
|  | 					) | ||||||
|  | 						return true; | ||||||
|  | 					// Otherwise, reject it. | ||||||
|  | 					return false; | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				// If the cert hostname does not match | ||||||
|  | 				if (sslPolicyErrors.HasFlag(SslPolicyErrors.RemoteCertificateNameMismatch)) | ||||||
|  | 				{ | ||||||
|  | 					// Accept the cert anyway if the client was explicitly configured to ignore this. | ||||||
|  | 					if ( | ||||||
|  | 						factory.Ssl.AcceptablePolicyErrors.HasFlag( | ||||||
|  | 							SslPolicyErrors.RemoteCertificateNameMismatch | ||||||
|  | 						) | ||||||
|  | 					) | ||||||
|  | 						return true; | ||||||
|  | 					// Otherwise, reject it. | ||||||
|  | 					return false; | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				// This shouldn't ever happen, and is mostly just here to satisfy the linter | ||||||
|  | 				if (chain == null || certificate == null) | ||||||
|  | 					return false; | ||||||
|  | 
 | ||||||
|  | 				// Verify that the certificate came from the specified CA. | ||||||
|  | 				chain.ChainPolicy.ExtraStore.AddRange( | ||||||
|  | 					chain.ChainElements.Select(x => x.Certificate).ToArray() | ||||||
|  | 				); | ||||||
|  | 				chain.ChainPolicy.CustomTrustStore.Clear(); | ||||||
|  | 				chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust; | ||||||
|  | 				chain.ChainPolicy.CustomTrustStore.Add(rootCA); | ||||||
|  | 
 | ||||||
|  | 				return chain.Build(new X509Certificate2(certificate)); | ||||||
|  | 			}; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		queryParameters.TryGetValue("certfile", out var certfiles); | ||||||
|  | 		var certfile = certfiles.LastOrDefault(); | ||||||
|  | 		queryParameters.TryGetValue("keyfile", out var keyfiles); | ||||||
|  | 		var keyfile = keyfiles.LastOrDefault(); | ||||||
|  | 		if (!string.IsNullOrEmpty(certfile) && !string.IsNullOrEmpty(keyfile)) | ||||||
|  | 			factory.Ssl.Certs = [X509Certificate2.CreateFromPemFile(certfile, keyfile)]; | ||||||
|  | 
 | ||||||
|  | 		queryParameters.TryGetValue("verify", out var verifyValues); | ||||||
|  | 		switch (verifyValues.LastOrDefault()) | ||||||
|  | 		{ | ||||||
|  | 			case "verify_none": | ||||||
|  | 				factory.Ssl.AcceptablePolicyErrors = ~SslPolicyErrors.None; | ||||||
|  | 				break; | ||||||
|  | 			case "verify_peer": | ||||||
|  | 				factory.Ssl.AcceptablePolicyErrors = SslPolicyErrors.None; | ||||||
|  | 				break; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		queryParameters.TryGetValue( | ||||||
|  | 			"server_name_indication", | ||||||
|  | 			out Microsoft.Extensions.Primitives.StringValues sniValues | ||||||
|  | 		); | ||||||
|  | 		var sni = sniValues.LastOrDefault(); | ||||||
|  | 		if (!string.IsNullOrEmpty(sni)) | ||||||
|  | 		{ | ||||||
|  | 			if (sni == "disabled") // Special value, see https://www.rabbitmq.com/docs/ssl#erlang-ssl | ||||||
|  | 			{ | ||||||
|  | 				factory.Ssl.ServerName = null; | ||||||
|  | 				factory.Ssl.AcceptablePolicyErrors |= SslPolicyErrors.RemoteCertificateNameMismatch; | ||||||
|  | 			} | ||||||
|  | 			else | ||||||
|  | 				factory.Ssl.ServerName = sni; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		queryParameters.TryGetValue( | ||||||
|  | 			"auth_mechanism", | ||||||
|  | 			out Microsoft.Extensions.Primitives.StringValues authMechanisms | ||||||
|  | 		); | ||||||
|  | 		if (authMechanisms.Count > 0) | ||||||
|  | 		{ | ||||||
|  | 			factory.AuthMechanisms.Clear(); | ||||||
|  | 			foreach (var authMechanism in authMechanisms) | ||||||
|  | 			{ | ||||||
|  | 				switch (authMechanism) | ||||||
|  | 				{ | ||||||
|  | 					case "external": | ||||||
|  | 						factory.AuthMechanisms.Add(new ExternalMechanismFactory()); | ||||||
|  | 						break; | ||||||
|  | 					case "plain": | ||||||
|  | 						factory.AuthMechanisms.Add(new PlainMechanismFactory()); | ||||||
|  | 						break; | ||||||
|  | 					default: | ||||||
|  | 						throw new NotSupportedException( | ||||||
|  | 							$"Unsupported authentication mechanism: {authMechanism}" | ||||||
|  | 						); | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	private static void _ConfigureFactoryWithEnvironmentVars( | ||||||
|  | 		this ConnectionFactory factory, | ||||||
|  | 		IConfigurationManager configuration | ||||||
|  | 	) | ||||||
|  | 	{ | ||||||
|  | 		factory.UserName = _GetNonEmptyString( | ||||||
|  | 			configuration.GetValue<string>("RABBITMQ_DEFAULT_USER"), | ||||||
|  | 			factory.UserName, | ||||||
|  | 			"guest" | ||||||
|  | 		); | ||||||
|  | 		factory.Password = _GetNonEmptyString( | ||||||
|  | 			configuration.GetValue<string>("RABBITMQ_DEFAULT_PASS"), | ||||||
|  | 			factory.Password, | ||||||
|  | 			"guest" | ||||||
|  | 		); | ||||||
|  | 		factory.HostName = _GetNonEmptyString( | ||||||
|  | 			configuration.GetValue<string>("RABBITMQ_HOST"), | ||||||
|  | 			factory.HostName, | ||||||
|  | 			"rabbitmq" | ||||||
|  | 		); | ||||||
|  | 		var port = configuration.GetValue<int?>("RABBITMQ_PORT"); | ||||||
|  | 		if (port != null) | ||||||
|  | 			factory.Port = port.Value; | ||||||
|  | 		else if (factory.Port == 0) | ||||||
|  | 			factory.Port = 5672; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	private static string _GetNonEmptyString(params string?[] values) | ||||||
|  | 	{ | ||||||
|  | 		foreach (var value in values) | ||||||
|  | 			if (!string.IsNullOrEmpty(value)) | ||||||
|  | 				return value; | ||||||
|  | 		return string.Empty; | ||||||
|  | 	} | ||||||
| } | } | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user