diff --git a/RemoteFrameBuffer/Client/RemoteFrameBufferClientProtocol.cs b/RemoteFrameBuffer/Client/RemoteFrameBufferClientProtocol.cs index d0f3b13..1f5c9e5 100644 --- a/RemoteFrameBuffer/Client/RemoteFrameBufferClientProtocol.cs +++ b/RemoteFrameBuffer/Client/RemoteFrameBufferClientProtocol.cs @@ -1,18 +1,234 @@ using RemoteFrameBuffer.Common; +using System.Data; +using System.Text; namespace RemoteFrameBuffer.Client { public abstract class RemoteFrameBufferClientProtocol { + public enum RfbClientProtocolPhase + { + None, + Constructed, + Handshake, + HandshakeDone, + Initialization, + InitializationDone, + Listening, + Stopped + } + + private Task? backgroundWorker; + private CancellationTokenSource? cancellationTokenSource; + private Timer? _timer; + public RfbClientProtocolPhase Phase { get; protected set; } = RfbClientProtocolPhase.None; + protected readonly Stream vncStream; public abstract RfbProtoVersion Version { get; } + public RemoteFramebufferSecurityType SecurityType { get; protected set; } = RemoteFramebufferSecurityType.Invalid; + + private RgbFrameBuffer? _frameBuffer; public RemoteFrameBufferClientProtocol(Stream s) { this.vncStream = s; } - public abstract void Handshake(); - public abstract void Initialization(); + public abstract void Handshake(CancellationToken token); + public void Initialization(CancellationToken token) + { + if (this.Phase != RfbClientProtocolPhase.HandshakeDone) + { + throw new InvalidOperationException("In wrong phase when calling Initialization"); + } + this.Phase = RfbClientProtocolPhase.Initialization; + Byte[] buf; + // ClientInit: Ask for a shared session; should probably make it configurable later + this.vncStream.WriteByte((Byte)0); + // ServerInit + buf = new Byte[24]; + CancellationTokenSource readTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + CancellationTokenSource readTokenSource = CancellationTokenSource.CreateLinkedTokenSource(token, readTimeout.Token); + this.vncStream.ReadExactlyAsync(buf, 0, buf.Length, readTokenSource.Token).AsTask().Wait(); + if (BitConverter.IsLittleEndian) + { + Array.Reverse(buf, 0, 2);// fb width + Array.Reverse(buf, 2, 2);// fb height + Array.Reverse(buf, 8, 2);// red-max + Array.Reverse(buf, 10, 2);// green-max + Array.Reverse(buf, 12, 2);// blue-max + Array.Reverse(buf, 20, 4);// name mength + } + UInt16 width = BitConverter.ToUInt16(buf, 0); + UInt16 height = BitConverter.ToUInt16(buf, 2); + Console.Error.WriteLine($"FB: {width}x{height} {buf[5]}({buf[4]}) {(buf[6] == 0 ? "LE" : "BE")} {(buf[7] == 0 ? "pallette" : "TrueColor")}"); + this._frameBuffer = new(width, height); + UInt32 nameLen = BitConverter.ToUInt32(buf, 20); + String name; + if (nameLen > 0) + { + buf = new Byte[nameLen]; + readTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + readTokenSource = CancellationTokenSource.CreateLinkedTokenSource(token, readTimeout.Token); + this.vncStream.ReadExactlyAsync(buf, 0, buf.Length, readTokenSource.Token).AsTask().Wait(); + name = Encoding.UTF8.GetString(buf); + Console.Error.WriteLine($"Name: {name}"); + } + + this.Phase = RfbClientProtocolPhase.InitializationDone; + } + + public Task StartListener(CancellationToken token) + { + if (this.Phase == RfbClientProtocolPhase.InitializationDone) + { + this.cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(token); + this.Phase = RfbClientProtocolPhase.Listening; + Console.Error.WriteLine("VNC client starting"); + this.backgroundWorker = new Task(this.Listener, this.cancellationTokenSource.Token); + this.backgroundWorker.ContinueWith(this.ListenerStopped); + this.backgroundWorker.Start(); + this._timer = new(this.FrameUpdateRequest, null, TimeSpan.FromMilliseconds(100), TimeSpan.FromMilliseconds(2000)); + return this.backgroundWorker; + }else + { + throw new InvalidOperationException("In wrong phase when calling StartListener"); + } + } + + protected void FrameUpdateRequest(Object? si) + { + Console.Error.WriteLine("Requesting update"); + Byte[] w = BitConverter.GetBytes(this._frameBuffer!.Width); + Byte[] h = BitConverter.GetBytes(this._frameBuffer!.Height); + if (BitConverter.IsLittleEndian) + { + Array.Reverse(w); + Array.Reverse(h); + } + Byte[] buf = new Byte[]{ 3, 1, 0, 0, 0, 0, w[0], w[1], h[0], h[1] }; + this.vncStream.Write(buf); + } + + private void ListenerStopped(Task t) + { + this.Phase = RfbClientProtocolPhase.Stopped; + Console.Error.WriteLine("VNC client exited"); + this.backgroundWorker = null; + } + + public void StopListener() + { + if (this.Phase != RfbClientProtocolPhase.Listening) + { + throw new InvalidOperationException("In wrong phase when calling StopListener"); + } + if (this.backgroundWorker != null) + { + this.cancellationTokenSource!.Cancel(); + this.backgroundWorker.Wait(); + } + } + + private void Listener() + { + Byte[] buf = new Byte[1]; + while (!this.cancellationTokenSource!.Token.IsCancellationRequested) + { + this.vncStream.ReadExactlyAsync(buf, 0, buf.Length, this.cancellationTokenSource.Token).AsTask().Wait(); + //Console.Error.WriteLine($"<== ServerMessage {buf[0]}"); + this.HandleServerMessage(buf[0]); + } + } + + protected void HandleServerMessage(Byte code) + { + switch (code) + { + case 0: + this.ServerMsg_FramebufferUpdate(); + break; + case 1: + this.ServerMsg_SetColourMapEntries(); + break; + case 2: + this.ServerMsg_Bell(); + break; + case 3: + this.ServerMsg_ServerCutText(); + break; + default: + throw new NotSupportedException($"Unknown server message ID: {code}"); + } + } + + protected void ServerMsg_FramebufferUpdate() + { + Byte[] buf = new Byte[3]; + this.vncStream.ReadExactlyAsync(buf, 0, buf.Length, this.cancellationTokenSource!.Token).AsTask().Wait(); + if (BitConverter.IsLittleEndian) + { + Array.Reverse(buf, 1, 2); + } + UInt16 rectangles = BitConverter.ToUInt16(buf, 1); + Console.Error.WriteLine($"<<= FramebufferUpdate ({rectangles})"); + for (Int32 i = 0; i < rectangles; i++) + { + buf = new Byte[12]; + this.vncStream.ReadExactlyAsync(buf, 0, buf.Length, this.cancellationTokenSource!.Token).AsTask().Wait(); + if (BitConverter.IsLittleEndian) + { + Array.Reverse(buf, 0, 2); + Array.Reverse(buf, 2, 2); + Array.Reverse(buf, 4, 2); + Array.Reverse(buf, 6, 2); + Array.Reverse(buf, 8, 4); + } + UInt16 x = BitConverter.ToUInt16(buf,0); + UInt16 y = BitConverter.ToUInt16(buf,2); + UInt16 w = BitConverter.ToUInt16(buf,4); + UInt16 h = BitConverter.ToUInt16(buf,6); + UInt32 encoding = BitConverter.ToUInt32(buf,8); + Console.Error.WriteLine($"RECT {w}x{h} @ {x}:{y}, as {encoding}"); + switch (encoding) + { + case 0: +#warning I'm straight up assuming 24-bit little endian RGB sent as padded 32bit pixels, ignoring what format the server specified + Int32 bytes = w * h * 4; + buf = new Byte[bytes]; + this.vncStream.ReadExactlyAsync(buf, 0, buf.Length, this.cancellationTokenSource!.Token).AsTask().Wait(); + RgbPixel[][] px = new RgbPixel[h][]; + for (Int32 row = 0; row < h; row++) + { + px[row] = new RgbPixel[w]; + for (Int32 col = 0; col < w; col++) + { + Int32 off = (row*w*4)+(col*4); + px[row][col] = new RgbPixel() { Red = buf[off+2], Green = buf[off+1], Blue = buf[off] }; + } + } + this._frameBuffer!.SetRegion(px, 0, 0, w, h, x, y); + break; + default: + throw new NotImplementedException(); + } + } + this._frameBuffer!.SaveBitmap($"{DateTime.Now:yyyyMMdd-HHmmss}.bmp"); + } + + protected void ServerMsg_SetColourMapEntries() + { + throw new NotImplementedException(); + } + + protected void ServerMsg_Bell() + { + throw new NotImplementedException(); + } + + protected void ServerMsg_ServerCutText() + { + throw new NotImplementedException(); + } } } diff --git a/RemoteFrameBuffer/Client/RemoteFrameBufferClientProtocol_3_3.cs b/RemoteFrameBuffer/Client/RemoteFrameBufferClientProtocol_3_3.cs index eba03ce..1ab02b8 100644 --- a/RemoteFrameBuffer/Client/RemoteFrameBufferClientProtocol_3_3.cs +++ b/RemoteFrameBuffer/Client/RemoteFrameBufferClientProtocol_3_3.cs @@ -1,4 +1,5 @@ using RemoteFrameBuffer.Common; +using System.Text; namespace RemoteFrameBuffer.Client { @@ -8,15 +9,42 @@ namespace RemoteFrameBuffer.Client public RemoteFrameBufferClientProtocol_3_3(Stream s) : base(s) { + this.Phase = RfbClientProtocolPhase.Constructed; } - public override void Handshake() + public override void Handshake(CancellationToken token) { - } - - public override void Initialization() - { - + if (this.Phase != RfbClientProtocolPhase.Constructed) + { + throw new InvalidOperationException("In wrong phase when calling Handshake"); + } + this.Phase = RfbClientProtocolPhase.Handshake; + Byte[] buf; + // Request protocol version + this.vncStream.Write(Encoding.ASCII.GetBytes("RFB 003.003\n")); + // Read security type + buf = new Byte[4]; + CancellationTokenSource readTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + CancellationTokenSource readTokenSource = CancellationTokenSource.CreateLinkedTokenSource(token, readTimeout.Token); + this.vncStream.ReadExactlyAsync(buf, 0, buf.Length, readTokenSource.Token).AsTask().Wait(); + if (BitConverter.IsLittleEndian) + { + Array.Reverse(buf); + } + this.SecurityType = (RemoteFramebufferSecurityType)BitConverter.ToUInt32(buf, 0); + Console.Error.WriteLine($"Server uses security type {this.SecurityType}"); + switch (this.SecurityType) + { + case RemoteFramebufferSecurityType.Invalid: + throw new Exception("Connection failed"); + case RemoteFramebufferSecurityType.None: + break;// RFB 3.3 straight up jumps to initialization here + case RemoteFramebufferSecurityType.VncAuthentication: + throw new NotImplementedException("VNC Authentication is not implemented"); + default: + throw new NotSupportedException("RFB version 3.3 doesn't support the security type returned by the server"); + } + this.Phase = RfbClientProtocolPhase.HandshakeDone; } } } diff --git a/RemoteFrameBuffer/Client/RemoteFramebufferClient.cs b/RemoteFrameBuffer/Client/RemoteFramebufferClient.cs index 0e108e9..736a60d 100644 --- a/RemoteFrameBuffer/Client/RemoteFramebufferClient.cs +++ b/RemoteFrameBuffer/Client/RemoteFramebufferClient.cs @@ -84,13 +84,21 @@ namespace RemoteFrameBuffer throw new Exception("Cannot parse protocol version"); } RfbProtoVersion serverVersion = new(Int16.Parse(m.Groups["major"].Value), Int16.Parse(m.Groups["minor"].Value)); - RfbProtoVersion maxProto = _protocolHandlers.Keys.Where((ph) => ph <= serverVersion).Max(); + RfbProtoVersion[] supportedProto = _protocolHandlers.Keys.Where((ph) => ph <= serverVersion).ToArray(); + if (supportedProto.Length == 0) + { + throw new NotSupportedException("No common protocol between client and server"); + } + RfbProtoVersion maxProto = supportedProto.Max(); RemoteFrameBufferClientProtocol proto = _protocolHandlers[maxProto].Construct(s); Console.Error.WriteLine($"Using client protocol {proto.Version.Major}.{proto.Version.Minor}"); - proto.Handshake(); - proto.Initialization(); + // TODO: register auth callback and the like + proto.Handshake(this.cancellationTokenSource.Token); + proto.Initialization(this.cancellationTokenSource.Token); + proto.StartListener(this.cancellationTokenSource.Token).Wait(); } - +#warning REMOVE THIS + return; } } diff --git a/RemoteFrameBuffer/Common/RemoteFramebufferSecurityType.cs b/RemoteFrameBuffer/Common/RemoteFramebufferSecurityType.cs new file mode 100644 index 0000000..e2e956d --- /dev/null +++ b/RemoteFrameBuffer/Common/RemoteFramebufferSecurityType.cs @@ -0,0 +1,9 @@ +namespace RemoteFrameBuffer.Common +{ + public enum RemoteFramebufferSecurityType + { + Invalid = 0, + None = 1, + VncAuthentication = 2, + } +} diff --git a/RemoteFrameBuffer/Common/RfbProtoVersion.cs b/RemoteFrameBuffer/Common/RfbProtoVersion.cs index 92f6585..8296cb3 100644 --- a/RemoteFrameBuffer/Common/RfbProtoVersion.cs +++ b/RemoteFrameBuffer/Common/RfbProtoVersion.cs @@ -5,6 +5,8 @@ public Int16 Major = major; public Int16 Minor = minor; + public override Boolean Equals(Object? obj) => obj is RfbProtoVersion version && this.Major == version.Major && this.Minor == version.Minor; + public override Int32 GetHashCode() => HashCode.Combine(this.Major, this.Minor); public static Boolean operator ==(RfbProtoVersion a, RfbProtoVersion b) { @@ -30,11 +32,11 @@ } public static Boolean operator <=(RfbProtoVersion a, RfbProtoVersion b) { - return b < a; + return !(b < a); } public static Boolean operator >=(RfbProtoVersion a, RfbProtoVersion b) { - return a < b; + return !(a < b); } } } diff --git a/RemoteFrameBuffer/Common/RgbFrameBuffer.cs b/RemoteFrameBuffer/Common/RgbFrameBuffer.cs new file mode 100644 index 0000000..20f285f --- /dev/null +++ b/RemoteFrameBuffer/Common/RgbFrameBuffer.cs @@ -0,0 +1,78 @@ +namespace RemoteFrameBuffer.Common +{ + public class RgbFrameBuffer + { + private RgbPixel[][] _frame; + + public UInt16 Width + { + get + { + return (UInt16)this._frame[0].Length; + } + } + public UInt16 Height + { + get + { + return (UInt16)this._frame.Length; + } + } + + private static RgbPixel[][] MakeFrameBuffer(UInt16 width, UInt16 height) + { + RgbPixel[][] buf = new RgbPixel[height][]; + for (Int32 i = 0; i < height; i++) + { + buf[i] = new RgbPixel[width]; + } + return buf; + } + + public RgbFrameBuffer(UInt16 width, UInt16 height) + { + this._frame = MakeFrameBuffer(width, height); + } + + public void SetRegion(RgbPixel[][] source, UInt16 source_x, UInt16 source_y, UInt16 width, UInt16 height, UInt16 target_x, UInt16 target_y) + { + for (Int32 y = 0; y < height; y++) + { + Array.Copy(source[y + source_y], source_x, this._frame[y + target_y], target_x, width); + } + } + + public void CopyRegion(UInt16 source_x, UInt16 source_y, UInt16 width, UInt16 height, UInt16 target_x, UInt16 target_y) => this.SetRegion(this._frame, source_x, source_y, width, height, target_x, target_y); + + public void SaveBitmap(String filename) + { +#warning Endianness is straight up ignored + Int32 rowBytes = (this.Width * 3) + 3; + rowBytes -= rowBytes % 4; + Int32 bytes = 54 + (this.Height * rowBytes); + Byte[] buf = new Byte[bytes]; + buf[0] = 0x42; + buf[1] = 0x4D; + Array.Copy(BitConverter.GetBytes(bytes), 0, buf, 2, 4); + buf[10] = 54; + buf[14] = 40; + Array.Copy(BitConverter.GetBytes((UInt32)this.Width), 0, buf, 18, 4); + Array.Copy(BitConverter.GetBytes((UInt32)this.Height), 0, buf, 22, 4); + buf[26] = 1; + buf[28] = 24; + Array.Copy(BitConverter.GetBytes(this.Height * rowBytes), 0, buf, 34, 4); + for (Int32 j = 0; j < this.Height; j++) + { + Int32 off = 54 + (j * rowBytes); + for (Int32 i = 0; i < this.Width; i++) + { + Int32 pos = off + (i * 3); + buf[pos] = this._frame[this.Height - j - 1][i].Blue; + buf[pos + 1] = this._frame[this.Height - j - 1][i].Green; + buf[pos + 2] = this._frame[this.Height - j - 1][i].Red; + } + } + File.WriteAllBytes(filename, buf); + } + } +} diff --git a/RemoteFrameBuffer/Common/RgbPixel.cs b/RemoteFrameBuffer/Common/RgbPixel.cs new file mode 100644 index 0000000..7214103 --- /dev/null +++ b/RemoteFrameBuffer/Common/RgbPixel.cs @@ -0,0 +1,9 @@ +namespace RemoteFrameBuffer.Common +{ + public struct RgbPixel + { + public Byte Red { get; set; } + public Byte Green { get; set; } + public Byte Blue { get; set; } + } +}