Implementando Acesso Remoto de Desktop via HTML5

Recentemente, deparei-me com um conceito fascinante: compartilhar a área de trabalho remota usando apenas tecnologias web. Embora o artigo original fornecido fosse limitado em termos de código-fonte, decidi desenvolver uma solução funcional. Durante um período de restrições locais, investi tempo para criar uma implementação básica, ainda que instável, que estou compartilhando neste artigo.

  1. Princípio Funcional

O componente servidor, que corresponde ao computador controlaod, estabelece uma escuta em uma porta WebSocket utilizando a biblioteca Fleck. Ao receber mensagens dos clientes, o sistema interperta eventos de teclado e mouse, simulando essas entradas localmente com a ajuda do componente KeyMouseHook. Adicionalmente, um temporizador captura a tela a cada 500ms e transmite a imagem para todos os clientes conectados.

  1. Código do Servidor
using Fleck;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Windows.Forms;
using Loamen.KeyMouseHook;
using Loamen.KeyMouseHook.Native;

namespace ServidorDesktopRemoto
{
    public partial class FormPrincipal : Form
    {
        private List<IWebSocketConnection> conexoesAtivas;
        private WebSocketServer servidorWebSocket;
        private InputSimulador simulador = new InputSimulador();

        [DllImport("user32.dll")]
        public static extern bool PrintWindow(IntPtr janela, IntPtr hdcBlt, UInt32 nFlags);

        public FormPrincipal()
        {
            InitializeComponent();
        }

        private void AdicionarLogConsole(string mensagem)
        {
            if (richTextBoxLog.Text.Length > 50000)
            {
                richTextBoxLog.Clear();
            }
            richTextBoxLog.AppendText(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss fff"));
            richTextBoxLog.AppendText(" " + mensagem);
            richTextBoxLog.AppendText("\r\n");
            richTextBoxLog.ScrollToCaret();
            
            if (checkBoxHabilitarLog.Checked)
            {
                string caminhoLog = ConfigurationManager.AppSettings["caminho_log"].ToString() + "/" + DateTime.Now.ToString("yyyy-MM-dd");
                if (!Directory.Exists(caminhoLog))
                {
                    Directory.CreateDirectory(caminhoLog);
                }
                File.AppendAllText(caminhoLog + "/servidor_remoto.log", 
                    DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss fff") + " " + mensagem + "\r\n", Encoding.UTF8);
            }
        }
 
        private void btnIniciarServidor_Click(object sender, EventArgs e)
        {
            conexoesAtivas = new List<IWebSocketConnection>();
            servidorWebSocket = new WebSocketServer("ws://" + txtEnderecoIP.Text + ":" + txtPorta.Text);
            servidorWebSocket.RestartAfterListenError = true;
            
            servidorWebSocket.Start(socket =>
            {
                socket.OnOpen = () =>
                {
                    Invoke((new Action(() =>
                    {
                        AdicionarLogConsole("Conexão aberta!");
                    })));
                    conexoesAtivas.Add(socket);
                };

                socket.OnClose = () =>
                {
                    Invoke((new Action(() =>
                    {
                        AdicionarLogConsole("Conexão fechada!");
                    })));
                    conexoesAtivas.Remove(socket);
                };

                socket.OnMessage = mensagem =>
                {
                    Invoke((new Action(() =>
                    {
                        AdicionarLogConsole(mensagem);
                        if (checkBoxModoControle.Checked)
                        {
                            var comando = JsonConvert.DeserializeObject<ComandoCliente>(mensagem);
                            ProcessarComando(comando);
                        }
                    })));
                };
            });
            AdicionarLogConsole("Servidor iniciado com sucesso");
        }

        private void FormPrincipal_Shown(object sender, EventArgs e)
        {
            txtEnderecoIP.Text = ConfigurationManager.AppSettings["ip"].ToString();
            txtPorta.Text = ConfigurationManager.AppSettings["porta"].ToString();
        }

        private Image CapturarTela(IntPtr handle)
        {
            Control controle = Control.FromHandle(handle);
            Bitmap imagem = new Bitmap(controle.Width, controle.Height);
            Graphics gp = Graphics.FromImage(imagem);
            IntPtr dc = gp.GetHdc();
            PrintWindow(handle, dc, 0);
            gp.ReleaseHdc();
            gp.Dispose();
            return imagem;
        }

        private void btnCapturarTela_Click(object sender, EventArgs e)
        {
            try
            {
                Bitmap bitmapTela = new Bitmap(Screen.PrimaryScreen.Bounds.Width, Screen.PrimaryScreen.Bounds.Height);
                Graphics g = Graphics.FromImage(bitmapTela);
                g.CopyFromScreen(new Point(0, 0), new Point(0, 0), Screen.PrimaryScreen.Bounds.Size);
                imagemTemporaria = SalvarComoJpg(bitmapTela, 10);
                AdicionarLogConsole("Captura de tela realizada com sucesso");
            }
            catch (Exception ex)
            {
                AdicionarLogConsole("Erro ao capturar tela: " + ex.Message);
            }
        }

        public Image SalvarComoJpg(Image imagem, long qualidade)
        {
            ImageCodecInfo codecInfo = null;
            ImageCodecInfo[] codecs = ImageCodecInfo.GetImageEncoders();
            foreach (ImageCodecInfo codec in codecs)
            {
                if (codec.MimeType == "image/jpeg")
                {
                    codecInfo = codec;
                    break;
                }
            }
            EncoderParameters parametros = new EncoderParameters(2);
            parametros.Param[0] = new EncoderParameter(System.Drawing.Imaging.Encoder.Quality, qualidade);
            parametros.Param[1] = new EncoderParameter(System.Drawing.Imaging.Encoder.Compression, (long)EncoderValue.CompressionLZW);
            return imagem;
        }

        public byte[] ImagemParaBytes(Image imagem)
        {
            MemoryStream ms = new MemoryStream();
            imagem.Save(ms, System.Drawing.Imaging.ImageFormat.Jpeg);
            return ms.ToArray();
        }

        private void btnEnviarTela_Click(object sender, EventArgs e)
        {
            byte[] dadosImagem = ImagemParaBytes(imagemTemporaria);
            conexoesAtivas.ToList().ForEach(conexao => conexao.Send(dadosImagem));
        }

        private void checkBoxCapturaAutomatica_CheckedChanged(object sender, EventArgs e)
        {
            timerCaptura.Enabled = checkBoxCapturaAutomatica.Checked;
        }

        private void timerCaptura_Tick(object sender, EventArgs e)
        {
            btnCapturarTela_Click(sender, e);
            btnEnviarTela_Click(sender, e);
        }
        
        private void ProcessarComando(ComandoCliente comando)
        {
            switch (comando.acao)
            {
                case "mousemove":
                    var ponto = new Point(comando.x, comando.y).ToAbsolutePoint();
                    simulador.Mouse.MoverPara(ponto.X, ponto.Y);
                    break;
                case "mousedown":
                    simulador.Mouse.CliqueEsquerdoPressionado();
                    break;
                case "mouseup":
                    simulador.Mouse.CliqueEsquerdoSolto();
                    break;
                case "click":
                    simulador.Mouse.CliqueEsquerdo();
                    break;
                case "dbclick":
                    simulador.Mouse.DuploCliqueEsquerdo();
                    break;
                case "rightclick":
                    simulador.Mouse.CliqueDireito();
                    break;
                case "keyup":
                    List<VirtualKeyCode> teclasModificadoras = new List<VirtualKeyCode>();
                    if (comando.shift) teclasModificadoras.Add(VirtualKeyCode.SHIFT);
                    if (comando.ctrl) teclasModificadoras.Add(VirtualKeyCode.CONTROL);
                    if (comando.alt) teclasModificadoras.Add(VirtualKeyCode.MENU);
                    simulador.Teclado.TeclaModificada(teclasModificadoras, (VirtualKeyCode)comando.tecla);
                    break;
            }
        }
    }
}

  1. Interface do Servidor

A interface do servidor contém elementos padrão do Windows Forms, com nomes intuitivos que facilitam a identificação dos componentes. Não há necessidade de explicações detalhadas sobre a interface, pois os controles são autoexplicativos.

  1. Arquivos Adicionais do Servidor

É necessário incluir uma classe para representar os comandos do cliente, além do arquivo de configuração da aplicação.

  1. Cliente Implementado com jQuery


<html xmlns="http://www.w3.org/1999/xhtml">
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <title>Cliente Desktop Remoto</title>
    <script src="jquery-3.6.0.min.js"></script>

    <style>
      body {
        padding: 0px;
        margin: 0px;
        overflow: hidden;
      }
      #canvasDesktop {
        display: block;
        cursor: crosshair;
      }
    </style>
  </head>

  <body>
    <canvas id="canvasDesktop"></canvas>
  </body>
  <script>
    var wsImpl = window.WebSocket || window.MozWebSocket;
    var conexaoWebSocket = new wsImpl("ws://127.0.0.1:10086/");
    var canvas = document.getElementById("canvasDesktop");
    var ctx = canvas.getContext("2d");
    
    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;
    
    var imagemDesktop = new Image();
    
    conexaoWebSocket.onmessage = function (evento) {
      imagemDesktop.src = URL.createObjectURL(evento.data);
      imagemDesktop.onload = function () {
        ctx.drawImage(imagemDesktop, 0, 0, canvas.width, canvas.height);
      };
    };

    var shiftPressionado = false;
    var ctrlPressionado = false;
    var altPressionado = false;

    $(window).keydown(function (evento) {
      AtualizarTeclasModificadoras(evento, true);
      
      if (TeclaNaoEModificadora(evento)) {
        EnviarComandoTeclado("keydown", evento.keyCode);
      }
    });
    
    $(window).keyup(function (evento) {
      AtualizarTeclasModificadoras(evento, false);
      
      if (TeclaNaoEModificadora(evento)) {
        EnviarComandoTeclado("keyup", evento.keyCode);
      }
    });

    function AtualizarTeclasModificadoras(evento, pressionada) {
      ctrlPressionado = evento.ctrlKey;
      shiftPressionado = evento.shiftKey;
      altPressionado = evento.altKey;
    }
    
    function TeclaNaoEModificadora(evento) {
      return evento.key !== "Shift" && evento.key !== "Ctrl" && evento.key !== "Alt";
    }
    
    function EnviarComandoTeclado(acao, codigoTecla) {
      var comando = {
        acao: acao,
        tecla: codigoTecla,
        shift: shiftPressionado,
        ctrl: ctrlPressionado,
        alt: altPressionado
      };
      conexaoWebSocket.send(JSON.stringify(comando));
    }

    $(canvas).mousemove(function (e) {
      var x = e.offsetX;
      var y = e.offsetY;
      conexaoWebSocket.send(JSON.stringify({acao: "mousemove", x: x, y: y}));
    });

    $(canvas).contextmenu(function (e) {
      e.preventDefault();
      var x = e.offsetX;
      var y = e.offsetY;
      conexaoWebSocket.send(JSON.stringify({acao: "rightclick", x: x, y: y}));
    });

    $(canvas).mousedown(function (e) {
      var acao = e.button === 0 ? "mousedown" : "rightmousedown";
      conexaoWebSocket.send(JSON.stringify({acao: acao, x: e.offsetX, y: e.offsetY}));
    });

    $(canvas).mouseup(function (e) {
      var acao = e.button === 0 ? "mouseup" : "rightmouseup";
      conexaoWebSocket.send(JSON.stringify({acao: acao, x: e.offsetX, y: e.offsetY}));
    });

    $(canvas).click(function (e) {
      conexaoWebSocket.send(JSON.stringify({acao: "click", x: e.offsetX, y: e.offsetY}));
    });

    $(canvas).dblclick(function (e) {
      conexaoWebSocket.send(JSON.stringify({acao: "dbclick", x: e.offsetX, y: e.offsetY}));
    });

    document.oncontextmenu = function () {
      return false;
    };
    
    $(window).resize(function() {
      canvas.width = window.innerWidth;
      canvas.height = window.innerHeight;
    });
  </script>
</html>

Tags: html5 WebSocket Desktop Remoto C# javascript

Publicado em 7-15 22:53