Very first version
This commit is contained in:
		
							
								
								
									
										6
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | |||||||
|  | .vscode/ | ||||||
|  | *.crt | ||||||
|  | docs/ | ||||||
|  | arstomp/obj/ | ||||||
|  | arstomp/bin/ | ||||||
|  | arstomp/src/internal | ||||||
							
								
								
									
										60
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,60 @@ | |||||||
|  | # Simple STOMP over WS client | ||||||
|  |  | ||||||
|  | Implemented in C# (.NET Core and Framework) | ||||||
|  |  | ||||||
|  | Works with RabbitMQ. | ||||||
|  |  | ||||||
|  | Uses (and probably requires) binary frames. | ||||||
|  |  | ||||||
|  | Supports 'custom' Root CA certficates. | ||||||
|  |  | ||||||
|  | Has simple RPC helper. | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ## Usage | ||||||
|  |  | ||||||
|  | ```csharp | ||||||
|  | class Program | ||||||
|  | { | ||||||
|  | 	static async Task Main(string[] args) | ||||||
|  | 	{ | ||||||
|  | 		var utf8 = Encoding.UTF8; | ||||||
|  |  | ||||||
|  | 		byte[] bytes = File.ReadAllBytes("path/to/ca.crt"); | ||||||
|  | 		X509Certificate2 myca = new X509Certificate2(bytes); | ||||||
|  | 		X509Certificate2Collection mycerts = new X509Certificate2Collection { myca }; | ||||||
|  |  | ||||||
|  | 		StompClient client = new StompClient(mycerts); | ||||||
|  | 		// can get messages in handler | ||||||
|  | 		//client.OnMessage += OnMessage; | ||||||
|  | 		// or use something to make request/response simpler | ||||||
|  | 		using var rpc = new RPC(client); | ||||||
|  |  | ||||||
|  | 		// connect | ||||||
|  | 		var uri = new Uri("wss://stomp.server:15673/ws"); | ||||||
|  | 		await client.Connect(uri, "login", "pass"); | ||||||
|  |  | ||||||
|  | 		// subscriptions | ||||||
|  | 		var sub1 = await client.Subscribe("/exchange/ex1/test.#"); | ||||||
|  | 		sub1.OnMessage += OnBroadcast; | ||||||
|  | 		var sub2 = await client.Subscribe("/exchange/ex2/test.#"); | ||||||
|  | 		sub2.OnMessage += OnBroadcast; | ||||||
|  |  | ||||||
|  | 		// simple publish (no response) | ||||||
|  | 		await client.Send("/exchange/something/test", "1", utf8.GetBytes("Test 1")); | ||||||
|  |  | ||||||
|  | 		// simple call (sending request and expecting response) | ||||||
|  | 		var result = await rpc.Call("/exchange/rpc/test", utf8.GetBytes("Test 2"), | ||||||
|  | 			TimeSpan.FromSeconds(3)); | ||||||
|  | 		Console.WriteLine("RCP: {0}", result); | ||||||
|  |  | ||||||
|  | 		await client.Close(); | ||||||
|  | 	} | ||||||
|  | 	static void OnBroadcast(object sender, SubscriptionEventArgs ea) | ||||||
|  | 	{ | ||||||
|  | 		Console.WriteLine("Broadcast {0}", ea.Frame); | ||||||
|  | 		var body = Encoding.UTF8.GetString(ea.Frame.Body); | ||||||
|  | 		Console.WriteLine("Body: {0}", body); | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | ``` | ||||||
							
								
								
									
										8
									
								
								arstomp/arstomp.csproj
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								arstomp/arstomp.csproj
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | |||||||
|  | <Project Sdk="Microsoft.NET.Sdk"> | ||||||
|  |  | ||||||
|  |   <PropertyGroup> | ||||||
|  |     <OutputType>library</OutputType> | ||||||
|  |     <TargetFramework>netcoreapp3.1</TargetFramework> | ||||||
|  |   </PropertyGroup> | ||||||
|  |  | ||||||
|  | </Project> | ||||||
							
								
								
									
										183
									
								
								arstomp/src/Helpers.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										183
									
								
								arstomp/src/Helpers.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,183 @@ | |||||||
|  | using System; | ||||||
|  | using System.Collections.Generic; | ||||||
|  | using System.Net.WebSockets; | ||||||
|  | using System.Threading; | ||||||
|  | using System.Threading.Tasks; | ||||||
|  | using System.IO; | ||||||
|  | using System.Text; | ||||||
|  |  | ||||||
|  |  | ||||||
|  | namespace ArStomp | ||||||
|  | { | ||||||
|  | internal static class Helpers | ||||||
|  | 	{ | ||||||
|  | 		/// <summary> | ||||||
|  | 		/// Static instance of heartbeat frame | ||||||
|  | 		/// </summary> | ||||||
|  | 		private readonly static Frame HeartbeatFrame = new Frame() { Type = FrameType.Heartbeat }; | ||||||
|  |  | ||||||
|  | 		internal static readonly Dictionary<string, FrameType> CmdMap = new Dictionary<string, FrameType>() | ||||||
|  | 		{ | ||||||
|  | 			{"CONNECTED", FrameType.Connected}, | ||||||
|  | 			{"ERROR", FrameType.Error}, | ||||||
|  | 			{"RECEIPT", FrameType.Receipt}, | ||||||
|  | 			{"MESSAGE", FrameType.Message}, | ||||||
|  | 			{"", FrameType.Heartbeat} // fake command | ||||||
|  | 		}; | ||||||
|  |  | ||||||
|  | 		internal static string GetCmdString(FrameType type) | ||||||
|  | 		{ | ||||||
|  | 			return type switch | ||||||
|  | 			{ | ||||||
|  | 				FrameType.Unknown => "UNKNOWN", | ||||||
|  | 				FrameType.Connected => "CONNECTED", | ||||||
|  | 				FrameType.Message => "MESSAGE", | ||||||
|  | 				FrameType.Receipt => "RECEIPT", | ||||||
|  | 				FrameType.Error => "ERROR", | ||||||
|  | 				FrameType.Stomp => "STOMP", | ||||||
|  | 				FrameType.Send => "SEND", | ||||||
|  | 				FrameType.Subscribe => "SUBSCRIBE", | ||||||
|  | 				FrameType.Unsubscribe => "UNSUBSCRIBE", | ||||||
|  | 				FrameType.Ack => "ACK", | ||||||
|  | 				FrameType.Nack => "NACK", | ||||||
|  | 				FrameType.Begin => "BEGIN", | ||||||
|  | 				FrameType.Commit => "COMMIT", | ||||||
|  | 				FrameType.Abort => "ABORT", | ||||||
|  | 				FrameType.Disconnect => "DISCONNECT", | ||||||
|  | 				FrameType.Heartbeat => "", | ||||||
|  | 				_ => "UNKNOWN" | ||||||
|  | 			}; | ||||||
|  | 		} | ||||||
|  | 		private static async Task GetMessage(MemoryStream output, ClientWebSocket ws, CancellationToken cancellationToken) | ||||||
|  | 		{ | ||||||
|  | 			var barray = new byte[128 * 1024]; | ||||||
|  | 			// read first frame | ||||||
|  | 			ArraySegment<byte> buffer = new ArraySegment<byte>(barray); | ||||||
|  | 			var result = await ws.ReceiveAsync(buffer, cancellationToken); | ||||||
|  |  | ||||||
|  | 			if (result.CloseStatus != null) | ||||||
|  | 			{ | ||||||
|  | 				throw new Exception($"Unexpected close: {result.CloseStatus}: {result.CloseStatusDescription}"); | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			output.Write(barray, 0, result.Count); | ||||||
|  |  | ||||||
|  | 			while (result.EndOfMessage != true) | ||||||
|  | 			{ | ||||||
|  | 				buffer = new ArraySegment<byte>(barray); | ||||||
|  | 				result = await ws.ReceiveAsync(buffer, cancellationToken); | ||||||
|  | 				output.Write(barray, 0, result.Count); | ||||||
|  | 			} | ||||||
|  | 			output.Seek(0, SeekOrigin.Begin); | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		private static StreamReader findBody(Stream input) | ||||||
|  | 		{ | ||||||
|  | 			var output = new MemoryStream(); | ||||||
|  | 			int count; | ||||||
|  | 			// read headers | ||||||
|  | 			do | ||||||
|  | 			{ | ||||||
|  | 				// read one line | ||||||
|  | 				count = 0; | ||||||
|  | 				while (true) | ||||||
|  | 				{ | ||||||
|  | 					var ch = input.ReadByte(); | ||||||
|  | 					if (ch == -1) throw new Exception("Unexpected end of data"); | ||||||
|  | 					byte b = (byte)(0xff & ch); // convert to byte | ||||||
|  | 					if (b == 13) continue; //skip CR | ||||||
|  | 					output.WriteByte(b); | ||||||
|  | 					if (b != 10) // LF - end of line | ||||||
|  | 					{ | ||||||
|  | 						count++; // chars in line | ||||||
|  | 					} | ||||||
|  | 					else | ||||||
|  | 					{ | ||||||
|  | 						break; | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 			} while (count > 0); // finish when got empty line | ||||||
|  | 			output.Seek(0, SeekOrigin.Begin); // start read from begining | ||||||
|  | 			return new StreamReader(output, Encoding.UTF8); // return UTF8 reader | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		internal static async Task<Frame> GetFrame(ClientWebSocket ws, CancellationToken cancellationToken) | ||||||
|  | 		{ | ||||||
|  | 			var utf8 = Encoding.UTF8; | ||||||
|  |  | ||||||
|  | 			var inputstream = new MemoryStream(); | ||||||
|  | 			var bodyoutput = new MemoryStream(); | ||||||
|  | 			try | ||||||
|  | 			{ | ||||||
|  | 				await GetMessage(inputstream, ws, cancellationToken); | ||||||
|  | 			} | ||||||
|  | 			catch (TaskCanceledException) | ||||||
|  | 			{ | ||||||
|  | 				return HeartbeatFrame; // just return empty frame | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			if (inputstream.ReadByte() == 10) | ||||||
|  | 			{ | ||||||
|  | 				return HeartbeatFrame; | ||||||
|  | 			} | ||||||
|  | 			else | ||||||
|  | 			{ | ||||||
|  | 				inputstream.Seek(0, SeekOrigin.Begin); | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			StreamReader reader = findBody(inputstream); | ||||||
|  |  | ||||||
|  | 			var cmd = reader.ReadLine(); | ||||||
|  |  | ||||||
|  | 			if (!CmdMap.ContainsKey(cmd)) | ||||||
|  | 			{ | ||||||
|  | 				throw new Exception($"Bad STOMP Frame, unknown command {cmd}"); | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			Frame frame = new Frame | ||||||
|  | 			{ | ||||||
|  | 				Type = CmdMap[cmd] | ||||||
|  | 			}; | ||||||
|  | 			// parse headers | ||||||
|  | 			var line = reader.ReadLine().TrimEnd(); | ||||||
|  | 			while (line != "") | ||||||
|  | 			{ | ||||||
|  | 				var colon = line.IndexOf(":"); | ||||||
|  | 				if (colon < 1) // must exist and cannot by first character in the line | ||||||
|  | 				{ | ||||||
|  | 					throw new Exception("Cannot parse header"); | ||||||
|  | 				} | ||||||
|  | 				var key = line.Substring(0, colon); | ||||||
|  | 				var value = line[(colon + 1)..]; | ||||||
|  | 				frame.Headers[key.ToLower()] = value; | ||||||
|  |  | ||||||
|  | 				line = reader.ReadLine().TrimEnd(); // next header | ||||||
|  | 			} | ||||||
|  | 			int length = -1; | ||||||
|  | 			if (frame.Headers.ContainsKey("content-length")) | ||||||
|  | 			{ | ||||||
|  | 				if (!int.TryParse(frame.Headers["content-length"], out length)) | ||||||
|  | 				{ | ||||||
|  | 					throw new Exception("Error: not valid value of header content-length"); | ||||||
|  | 				} | ||||||
|  | 				byte[] body = new byte[length]; | ||||||
|  | 				inputstream.Read(body, 0, body.Length); | ||||||
|  | 				frame.Body = new ArraySegment<byte>(body); | ||||||
|  | 			} | ||||||
|  | 			else | ||||||
|  | 			{ | ||||||
|  | 				var bodyStream = new MemoryStream(); | ||||||
|  | 				int b; | ||||||
|  | 				while ((b = inputstream.ReadByte()) > 0) // not -1 and not 0 | ||||||
|  | 				{ | ||||||
|  | 					bodyStream.WriteByte((byte)b); | ||||||
|  | 				} | ||||||
|  | 				var bl = (int)bodyStream.Length; | ||||||
|  | 				byte[] data = bodyStream.GetBuffer(); | ||||||
|  | 				frame.Body = new ArraySegment<byte>(data, 0, bl); | ||||||
|  | 			} | ||||||
|  | 			if (StompClient.Debug) Console.WriteLine("<<<\n{0}\n<<<\n", frame); | ||||||
|  | 			return frame; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										101
									
								
								arstomp/src/RPC.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										101
									
								
								arstomp/src/RPC.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,101 @@ | |||||||
|  | using System; | ||||||
|  | using System.Threading.Tasks; | ||||||
|  | using System.Timers; | ||||||
|  | using System.Collections.Concurrent; | ||||||
|  |  | ||||||
|  | /// <summary> | ||||||
|  | /// Minimal implementation of stomp client | ||||||
|  | /// </summary> | ||||||
|  | namespace ArStomp | ||||||
|  | { | ||||||
|  | 	/// <summary> | ||||||
|  | 	/// Simple helper implementing Remote Procedure Call on stomp. | ||||||
|  | 	/// </summary> | ||||||
|  | 	public class RPC : IDisposable | ||||||
|  | 	{ | ||||||
|  | 		private readonly ConcurrentDictionary<string, Request> requests = new ConcurrentDictionary<string, Request>(5, 10); | ||||||
|  | 		private Timer timer; | ||||||
|  |  | ||||||
|  | 		private readonly StompClient client; | ||||||
|  | 		/// <summary> | ||||||
|  | 		/// Creates new RPC layer fror given stomp client | ||||||
|  | 		/// </summary> | ||||||
|  | 		/// <param name="client">initialized and connected stomp client</param> | ||||||
|  | 		public RPC(StompClient client) | ||||||
|  | 		{ | ||||||
|  | 			this.client = client; | ||||||
|  | 			client.OnMessage += this.OnMessage; | ||||||
|  | 			timer = new Timer(3000); | ||||||
|  | 			timer.Elapsed += OnTimer; | ||||||
|  | 			timer.Start(); | ||||||
|  | 		} | ||||||
|  | 		/// <summary> | ||||||
|  | 		/// Calles remote method | ||||||
|  | 		/// </summary> | ||||||
|  | 		/// <param name="destination">destination, where request is sent</param> | ||||||
|  | 		/// <param name="body">content of the message</param> | ||||||
|  | 		/// <param name="timeout">how long we're going to wait for response (default is 15s)</param> | ||||||
|  | 		/// <returns>response for our call (<see cref="Frame"/> of returned message)</returns> | ||||||
|  | 		/// <exception cref="TimeoutException">if there is no reponse after timeout</exception> | ||||||
|  | 		public async Task<Frame> Call(string destination, byte[] body, TimeSpan timeout = default) | ||||||
|  | 		{ | ||||||
|  | 			string correlationId = Guid.NewGuid().ToString(); | ||||||
|  | 			TimeSpan to = (timeout != default) ? timeout : TimeSpan.FromSeconds(15); | ||||||
|  | 			var req = new Request(correlationId, to); | ||||||
|  | 			if (!requests.TryAdd(correlationId, req)) | ||||||
|  | 				throw new Exception("Request with this cid is already processed"); | ||||||
|  | 			await client.Send(destination, correlationId, body); | ||||||
|  | 			return await req.Source.Task; | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		public void Dispose() | ||||||
|  | 		{ | ||||||
|  | 			if (timer != null) | ||||||
|  | 			{ | ||||||
|  | 				var t = timer; | ||||||
|  | 				timer = null; | ||||||
|  | 				client.OnMessage -= this.OnMessage; | ||||||
|  | 				t.Stop(); | ||||||
|  | 				t.Dispose(); | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		private void OnTimer(object sender, ElapsedEventArgs ea) | ||||||
|  | 		{ | ||||||
|  | 			var ts = DateTime.UtcNow; | ||||||
|  | 			foreach (var i in requests) | ||||||
|  | 			{ | ||||||
|  | 				if (i.Value.Timeout < ts) | ||||||
|  | 				{ | ||||||
|  | 					if (requests.TryRemove(i.Key, out Request req)) | ||||||
|  | 					{ | ||||||
|  | 						req.Source.SetException(new TimeoutException()); | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		private void OnMessage(object sender, SubscriptionEventArgs ea) | ||||||
|  | 		{ | ||||||
|  | 			var cid = ea.Frame.Headers["correlation-id"]; | ||||||
|  | 			if (requests.TryRemove(cid, out Request req)) | ||||||
|  | 			{ | ||||||
|  | 				req.Source.SetResult(ea.Frame); | ||||||
|  | 			} | ||||||
|  | 			// else skip unknown message | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	internal sealed class Request | ||||||
|  | 	{ | ||||||
|  | 		public readonly DateTime Timeout; | ||||||
|  | 		public readonly string Cid; | ||||||
|  | 		public TaskCompletionSource<Frame> Source { get; } = new TaskCompletionSource<Frame>(); | ||||||
|  | 		public Request(string cid, TimeSpan waitingTime) | ||||||
|  | 		{ | ||||||
|  | 			this.Cid = cid; | ||||||
|  | 			this.Timeout = DateTime.UtcNow + waitingTime; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										291
									
								
								arstomp/src/StompClient.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										291
									
								
								arstomp/src/StompClient.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,291 @@ | |||||||
|  | using System.Security.Cryptography.X509Certificates; | ||||||
|  | using System.Net.Security; | ||||||
|  | using System.Net.WebSockets; | ||||||
|  | using System.Threading; | ||||||
|  | using System.Threading.Tasks; | ||||||
|  | using System.Text; | ||||||
|  | using System; | ||||||
|  | using System.Collections.Generic; | ||||||
|  | using System.Linq; | ||||||
|  |  | ||||||
|  | namespace ArStomp | ||||||
|  | { | ||||||
|  | 	/// <summary> | ||||||
|  | 	/// Stomp client | ||||||
|  | 	/// </summary> | ||||||
|  | 	public class StompClient | ||||||
|  | 	{ | ||||||
|  | 		private Task runner = null; | ||||||
|  | 		public static bool Debug { get; set; } = false; | ||||||
|  | 		private ClientWebSocket ws = new ClientWebSocket(); | ||||||
|  | 		public CancellationTokenSource Token { get; } = new CancellationTokenSource(); | ||||||
|  | 		private readonly X509Certificate2Collection certCollection; | ||||||
|  | 		private readonly Dictionary<string, Subscription> subs = new Dictionary<string, Subscription>(); | ||||||
|  | 		/// <summary> | ||||||
|  | 		/// Handler of incoming messages. | ||||||
|  | 		/// Used only for RPC. <see cref="Subscription">Subsscriptions</see> have own handlers. | ||||||
|  | 		/// </summary> | ||||||
|  | 		public event EventHandler<SubscriptionEventArgs> OnMessage; | ||||||
|  | 		/// <summary> | ||||||
|  | 		/// Creates new object. | ||||||
|  | 		/// Supports RabbitMQ. | ||||||
|  | 		/// Works with binary frames. | ||||||
|  | 		/// </summary> | ||||||
|  | 		/// <param name="certCollection">collection of root ca certificates (if TLS is used)</param> | ||||||
|  | 		public StompClient(X509Certificate2Collection certCollection = null) | ||||||
|  | 		{ | ||||||
|  |  | ||||||
|  | 			ws = new ClientWebSocket(); | ||||||
|  | 			ws.Options.AddSubProtocol("v12.stomp"); | ||||||
|  | 			if (certCollection != null && certCollection.Count > 0) | ||||||
|  | 			{ | ||||||
|  | 				this.certCollection = certCollection; | ||||||
|  | 				ws.Options.RemoteCertificateValidationCallback = RemoteCertificateValidationCallback; | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		internal void InvokeOnMessage(SubscriptionEventArgs args) | ||||||
|  | 		{ | ||||||
|  | 			try | ||||||
|  | 			{ | ||||||
|  | 				OnMessage?.Invoke(this, args); | ||||||
|  | 			} | ||||||
|  | 			catch | ||||||
|  | 			{ | ||||||
|  | 				// skip all exceptions | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		/// <summary> | ||||||
|  | 		/// Verify cert chain in case of using own (custom) ca certificates for TLS | ||||||
|  | 		/// </summary> | ||||||
|  | 		/// <param name="sender">tls stream</param> | ||||||
|  | 		/// <param name="certificate">server's certificate</param> | ||||||
|  | 		/// <param name="chain">constructed chain of trust</param> | ||||||
|  | 		/// <param name="sslPolicyErrors">detected errors</param> | ||||||
|  | 		/// <returns>true if server certificate is valid, false otherwise</returns> | ||||||
|  | 		private bool RemoteCertificateValidationCallback(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) | ||||||
|  | 		{ | ||||||
|  | 			// if there is no detected problems we can say OK | ||||||
|  | 			if ((sslPolicyErrors & (SslPolicyErrors.None)) > 0) return true; | ||||||
|  | 			// sins that cannot be forgiven | ||||||
|  | 			if ( | ||||||
|  | 				(sslPolicyErrors & (SslPolicyErrors.RemoteCertificateNameMismatch)) > 0 || | ||||||
|  | 				(sslPolicyErrors & (SslPolicyErrors.RemoteCertificateNotAvailable)) > 0 | ||||||
|  | 			) return false; | ||||||
|  | 			// last certificate in chain should be one of our trust anchors | ||||||
|  | 			X509Certificate2 projectedRootCert = chain.ChainElements[^1].Certificate; | ||||||
|  | 			// check if server's root ca is one of our trusted | ||||||
|  | 			bool anytrusted = false; | ||||||
|  | 			foreach (var cert in certCollection) | ||||||
|  | 			{ | ||||||
|  | 				anytrusted = anytrusted || (projectedRootCert.Thumbprint == cert.Thumbprint); | ||||||
|  | 			} | ||||||
|  | 			if (!anytrusted) return false; | ||||||
|  | 			// any other problems than unknown CA? | ||||||
|  | 			if (chain.ChainStatus.Any(statusFlags => statusFlags.Status != X509ChainStatusFlags.UntrustedRoot)) return false; | ||||||
|  | 			// everything OK | ||||||
|  | 			return true; | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		private void ExpectFrame(Frame frame, FrameType expected) | ||||||
|  | 		{ | ||||||
|  | 			if (frame.Type != expected) | ||||||
|  | 			{ | ||||||
|  | 				var reason = "Unknown reason"; | ||||||
|  | 				if (frame.Type == FrameType.Error) | ||||||
|  | 				{ | ||||||
|  | 					if (frame.Body != null) | ||||||
|  | 					{ | ||||||
|  | 						reason = Encoding.UTF8.GetString(frame.Body); | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 				throw new Exception($"Unexpected frame '{frame.Type}'. Message from server: {reason}"); | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		/// <summary> | ||||||
|  | 		/// Connects to stomp server. | ||||||
|  | 		/// Uses <see cref="ClientWebSocket"/>. | ||||||
|  | 		/// </summary> | ||||||
|  | 		/// <param name="uri">uri in format ws://host[:port][/path] or wss://host[:port][/path]</param> | ||||||
|  | 		/// <param name="login">login name</param> | ||||||
|  | 		/// <param name="password">password</param> | ||||||
|  | 		public async Task Connect(Uri uri, string login, string password) | ||||||
|  | 		{ | ||||||
|  | 			if (runner != null) throw new Exception("Cannot connect in this state. Should close before"); | ||||||
|  | 			var ct = Token.Token; | ||||||
|  | 			await ws.ConnectAsync(uri, ct); | ||||||
|  |  | ||||||
|  | 			StompFrm connect = new StompFrm(login, password); | ||||||
|  | 			await connect.Serialize(ws, ct); | ||||||
|  |  | ||||||
|  | 			Frame fr = await Helpers.GetFrame(ws, ct); | ||||||
|  |  | ||||||
|  | 			ExpectFrame(fr, FrameType.Connected); | ||||||
|  | 			runner = Run(); // Run is async | ||||||
|  | 		} | ||||||
|  | 		/// <summary> | ||||||
|  | 		/// Reports state of conection | ||||||
|  | 		/// </summary> | ||||||
|  | 		/// <returns>true if it looks like we have proper connection to server</returns> | ||||||
|  | 		public bool IsConnected() | ||||||
|  | 		{ | ||||||
|  | 			return ws.CloseStatus == null; | ||||||
|  | 		} | ||||||
|  | 		/// <summary> | ||||||
|  | 		/// Send message | ||||||
|  | 		/// </summary> | ||||||
|  | 		/// <param name="destination">queue o exchange (eg /exchange/name/routing-key in case of RabbitMQ)</param> | ||||||
|  | 		/// <param name="correlationId">property correlationId for the message</param> | ||||||
|  | 		/// <param name="body">content of the message</param> | ||||||
|  | 		public ValueTask Send(string destination, string correlationId, byte[] body) | ||||||
|  | 		{ | ||||||
|  | 			var ct = Token.Token; | ||||||
|  |  | ||||||
|  | 			SendFrm send = new SendFrm(destination, correlationId, body); | ||||||
|  | 			return send.Serialize(ws, ct); | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		private int SubId = 0; | ||||||
|  | 		/// <summary> | ||||||
|  | 		/// Create news subscription | ||||||
|  | 		/// </summary> | ||||||
|  | 		/// <param name="destination">eg /exchange/name/rouring-key</param> | ||||||
|  | 		/// <returns><see cref="Subscription"/> object</returns> | ||||||
|  | 		public async Task<Subscription> Subscribe(string destination) | ||||||
|  | 		{ | ||||||
|  | 			var ct = Token.Token; | ||||||
|  | 			var id = $"sub-{++SubId}"; | ||||||
|  | 			var sub = new Subscription() | ||||||
|  | 			{ | ||||||
|  | 				Destination = destination, | ||||||
|  | 				Id = id | ||||||
|  | 			}; | ||||||
|  | 			var sframe = new SubscribeFrame(id, destination); | ||||||
|  | 			await sframe.Serialize(ws, ct); | ||||||
|  | 			subs.Add(id, sub); | ||||||
|  | 			return sub; | ||||||
|  | 		} | ||||||
|  | 		/// <summary> | ||||||
|  | 		/// Cancel subscrption | ||||||
|  | 		/// </summary> | ||||||
|  | 		/// <param name="sub">object of subscription</param> | ||||||
|  | 		public async Task Unsubscribe(Subscription sub) | ||||||
|  | 		{ | ||||||
|  | 			if (subs.ContainsKey(sub.Id)) | ||||||
|  | 			{ | ||||||
|  | 				subs.Remove(sub.Id); | ||||||
|  |  | ||||||
|  | 				var ct = Token.Token; | ||||||
|  | 				var sframe = new UnsubscribeFrame(sub.Id); | ||||||
|  | 				await sframe.Serialize(ws, ct); | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		private async Task Run() | ||||||
|  | 		{ | ||||||
|  | 			var ct = Token.Token; | ||||||
|  | 			try | ||||||
|  | 			{ | ||||||
|  | 				while (!ct.IsCancellationRequested) | ||||||
|  | 				{ | ||||||
|  | 					Frame fr = null; | ||||||
|  | 					try | ||||||
|  | 					{ | ||||||
|  | 						fr = await Helpers.GetFrame(ws, ct); | ||||||
|  | 					} | ||||||
|  | 					catch (ThreadInterruptedException) | ||||||
|  | 					{ | ||||||
|  | 						break; | ||||||
|  | 					} | ||||||
|  | 					if (fr.Type == FrameType.Error) ExpectFrame(fr, FrameType.Message); | ||||||
|  | 					if (fr.Type == FrameType.Message) | ||||||
|  | 					{ | ||||||
|  | 						if (fr.Headers.ContainsKey("subscription")) | ||||||
|  | 						{ | ||||||
|  | 							if (fr.Headers["subscription"] == "/temp-queue/rpc-replies") | ||||||
|  | 							{ | ||||||
|  | 								InvokeOnMessage(new SubscriptionEventArgs(fr)); | ||||||
|  | 							} | ||||||
|  | 							else | ||||||
|  | 							{ | ||||||
|  | 								var sub = subs[fr.Headers["subscription"]]; | ||||||
|  | 								if (sub != null) sub.InvokeOnMessage(new SubscriptionEventArgs(fr)); | ||||||
|  | 								else Console.WriteLine("Nieoczekiwany komunikat {0}", fr); | ||||||
|  |  | ||||||
|  | 							} | ||||||
|  | 						} | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 			finally | ||||||
|  | 			{ | ||||||
|  | 				await Close(); | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		/// <summary> | ||||||
|  | 		/// Cancel current operaton and close connection | ||||||
|  | 		/// </summary> | ||||||
|  | 		/// <returns></returns> | ||||||
|  | 		public async Task Close() | ||||||
|  | 		{ | ||||||
|  | 			try | ||||||
|  | 			{ | ||||||
|  | 				Token.Cancel(); | ||||||
|  | 				var ct = new CancellationTokenSource().Token; | ||||||
|  | 				await ws.CloseAsync(WebSocketCloseStatus.Empty, null, ct); | ||||||
|  | 			} | ||||||
|  | 			catch | ||||||
|  | 			{ | ||||||
|  | 				// skip | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	/// <summary> | ||||||
|  | 	/// Represents the subscpriotn | ||||||
|  | 	/// </summary> | ||||||
|  | 	public class Subscription | ||||||
|  | 	{ | ||||||
|  | 		/// <summary> | ||||||
|  | 		/// Id of this particular subscription | ||||||
|  | 		/// </summary> | ||||||
|  | 		public string Id { get; internal set; } | ||||||
|  | 		/// <summary> | ||||||
|  | 		/// Destination used with this syubscription | ||||||
|  | 		/// </summary> | ||||||
|  | 		/// <value></value> | ||||||
|  | 		public string Destination { get; internal set; } | ||||||
|  | 		/// <summary> | ||||||
|  | 		/// Handler fro incoming messages | ||||||
|  | 		/// </summary> | ||||||
|  | 		public event EventHandler<SubscriptionEventArgs> OnMessage; | ||||||
|  |  | ||||||
|  | 		internal void InvokeOnMessage(SubscriptionEventArgs args) | ||||||
|  | 		{ | ||||||
|  | 			try | ||||||
|  | 			{ | ||||||
|  | 				OnMessage?.Invoke(this, args); | ||||||
|  | 			} | ||||||
|  | 			catch | ||||||
|  | 			{ | ||||||
|  | 				// skip all exceptions | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	/// <summary> | ||||||
|  | 	/// Arguments for OnMessage handlers | ||||||
|  | 	/// </summary> | ||||||
|  | 	public class SubscriptionEventArgs : EventArgs | ||||||
|  | 	{ | ||||||
|  | 		/// <summary> | ||||||
|  | 		/// The frame (MESSAGE) got from server | ||||||
|  | 		/// </summary> | ||||||
|  | 		/// <value></value> | ||||||
|  | 		public Frame Frame { get; } | ||||||
|  |  | ||||||
|  | 		internal SubscriptionEventArgs(Frame f) | ||||||
|  | 		{ | ||||||
|  | 			Frame = f; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										150
									
								
								arstomp/src/StompFrame.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										150
									
								
								arstomp/src/StompFrame.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,150 @@ | |||||||
|  | using System; | ||||||
|  | using System.Collections.Generic; | ||||||
|  | using System.Net.WebSockets; | ||||||
|  | using System.Threading; | ||||||
|  | using System.Threading.Tasks; | ||||||
|  | using System.IO; | ||||||
|  | using System.Text; | ||||||
|  |  | ||||||
|  |  | ||||||
|  | namespace ArStomp | ||||||
|  | { | ||||||
|  | 	/// <summary> | ||||||
|  | 	/// Possible STOMP frame types. Not everything is used in this library | ||||||
|  | 	/// </summary> | ||||||
|  | 	public enum FrameType | ||||||
|  | 	{ | ||||||
|  | 		// no type | ||||||
|  | 		Unknown = 0, | ||||||
|  | 		// server frames | ||||||
|  | 		Connected, | ||||||
|  | 		Message, | ||||||
|  | 		Receipt, | ||||||
|  | 		Error, | ||||||
|  |  | ||||||
|  | 		// client frames | ||||||
|  | 		Stomp, | ||||||
|  | 		Send, | ||||||
|  | 		Subscribe, | ||||||
|  | 		Unsubscribe, | ||||||
|  | 		Ack, | ||||||
|  | 		Nack, | ||||||
|  | 		Begin, | ||||||
|  | 		Commit, | ||||||
|  | 		Abort, | ||||||
|  | 		Disconnect, | ||||||
|  | 		Heartbeat // fake, but gives common view of server messages | ||||||
|  | 	} | ||||||
|  | 	/// <summary> | ||||||
|  | 	/// Represents the frame of STOMP protocol: message, command or error | ||||||
|  | 	/// </summary> | ||||||
|  | 	public class Frame | ||||||
|  | 	{ | ||||||
|  | 		/// <summary> | ||||||
|  | 		/// Type of frame | ||||||
|  | 		/// </summary> | ||||||
|  | 		public FrameType Type { get; internal set; } | ||||||
|  | 		/// <summary> | ||||||
|  | 		/// Headers from STOMP frame | ||||||
|  | 		/// </summary> | ||||||
|  | 		/// <typeparam name="string">header's name</typeparam> | ||||||
|  | 		/// <typeparam name="string">header's value</typeparam> | ||||||
|  | 		public Dictionary<string, string> Headers { get; } = new Dictionary<string, string>(); | ||||||
|  | 		/// <summary> | ||||||
|  | 		/// Content (body) of the message. | ||||||
|  | 		/// Not parsed and not processed in any way. | ||||||
|  | 		/// </summary>s | ||||||
|  | 		public ArraySegment<byte> Body { get; internal set; } | ||||||
|  |  | ||||||
|  | 		public override string ToString() | ||||||
|  | 		{ | ||||||
|  | 			StringBuilder sb = new StringBuilder(); | ||||||
|  | 			sb.Append(Helpers.GetCmdString(Type)).Append("\n"); | ||||||
|  | 			foreach (var i in Headers) | ||||||
|  | 			{ | ||||||
|  | 				sb.AppendFormat("{0}:{1}\n", i.Key, i.Value); | ||||||
|  | 			} | ||||||
|  | 			sb.AppendFormat("Body size: {0}", Body.Count); | ||||||
|  | 			return sb.ToString(); | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		internal ValueTask Serialize(ClientWebSocket ws, CancellationToken cancellationToken) | ||||||
|  | 		{ | ||||||
|  | 			var utf8 = Encoding.UTF8; | ||||||
|  | 			var EOL = utf8.GetBytes("\n"); | ||||||
|  | 			var COLON = utf8.GetBytes(":"); | ||||||
|  | 			var NUL = new byte[] { 0 }; | ||||||
|  | 			var stream = new MemoryStream(); | ||||||
|  |  | ||||||
|  | 			// write command | ||||||
|  | 			var cmd = Helpers.GetCmdString(Type); | ||||||
|  | 			stream.Write(utf8.GetBytes(cmd)); | ||||||
|  | 			stream.Write(EOL); | ||||||
|  | 			// write headers | ||||||
|  | 			foreach (var i in Headers) | ||||||
|  | 			{ | ||||||
|  | 				stream.Write(utf8.GetBytes(i.Key)); | ||||||
|  | 				stream.Write(COLON); | ||||||
|  | 				stream.Write(utf8.GetBytes(i.Value)); | ||||||
|  | 				stream.Write(EOL); | ||||||
|  | 			} | ||||||
|  | 			// write empty line | ||||||
|  | 			stream.Write(EOL); | ||||||
|  | 			// write body | ||||||
|  | 			if (Body != null && Body.Count > 0) | ||||||
|  | 			{ | ||||||
|  | 				stream.Write(Body); | ||||||
|  | 			} | ||||||
|  | 			// write NUL character | ||||||
|  | 			stream.Write(NUL); | ||||||
|  | 			stream.Flush(); | ||||||
|  | 			var array = stream.GetBuffer(); | ||||||
|  | 			if (StompClient.Debug) Console.WriteLine(">>>\n{0}\n>>>\n", this); | ||||||
|  | 			return ws.SendAsync(array.AsMemory(0, (int)stream.Position), WebSocketMessageType.Binary, true, cancellationToken); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	internal class StompFrm : Frame | ||||||
|  | 	{ | ||||||
|  | 		public StompFrm(string login, string passwd) | ||||||
|  | 		{ | ||||||
|  | 			Type = FrameType.Stomp; | ||||||
|  | 			Headers["login"] = login; | ||||||
|  | 			Headers["passcode"] = passwd; | ||||||
|  | 			Headers["accept-version"] = "1.2"; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	internal class SendFrm : Frame | ||||||
|  | 	{ | ||||||
|  | 		public SendFrm(string destination, string correlationId, byte[] body) | ||||||
|  | 		{ | ||||||
|  | 			Type = FrameType.Send; | ||||||
|  | 			Headers["destination"] = destination; | ||||||
|  | 			Headers["reply-to"] = "/temp-queue/rpc-replies"; | ||||||
|  | 			if (correlationId != null) Headers["correlation-id"] = correlationId; | ||||||
|  | 			Headers["content-length"] = body.Length.ToString(); | ||||||
|  | 			Body = body; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	internal class SubscribeFrame : Frame | ||||||
|  | 	{ | ||||||
|  | 		public SubscribeFrame(string id, string destination) | ||||||
|  | 		{ | ||||||
|  | 			Type = FrameType.Subscribe; | ||||||
|  | 			Headers["destination"] = destination; | ||||||
|  | 			Headers["id"] = id; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	internal class UnsubscribeFrame : Frame | ||||||
|  | 	{ | ||||||
|  | 		public UnsubscribeFrame(string id) | ||||||
|  | 		{ | ||||||
|  | 			Type = FrameType.Unsubscribe; | ||||||
|  | 			Headers["id"] = id; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user