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(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(); } } }