Статья VNC через Relay (Работа за NAT) или пробуем повторить AnyDesk на Паскале. [1]

Admin

Администратор

VNC через Relay (Работа за NAT) или пробуем повторить AnyDesk на Паскале. [1]​

Всем привет. Предыдущий VNC работал только в Direct режиме по прямому соединению. Так как очень много узлов работает за NAT мне захотелось реализовать протокол по схеме: Client<--->Relay Server<--->Server

Запускаем Relay Server на белом IP и можем работать с VNC за натом.

В моей реализации еще куча багов. Буду её дорабатывать по возможности, но основная идея работает, также реализовано JPEG сжатие через GDI+. Если кто-то захочет собрать проект обязательно в инспекторе Lazarus 4.0 подключаем либу "lazgdi".

Скриншот клиентской части:
1771105254241

1771105262153

1771105270774
 

Admin

Администратор
Исходник клиента:

Код:
unit Unit1;



{$mode objfpc}{$H+}



interface



uses

  Classes, SysUtils, Forms, Controls, Graphics, Dialogs, StdCtrls, ExtCtrls,

  Windows, WinSock2, LMessages, StrUtils, Math, ActiveX, gdiplus, GraphType;



// Объявление внешней функции ioctlsocket

function ioctlsocket(s: TSocket; cmd: Longint; argp: Pointer): Integer; stdcall; external 'ws2_32.dll';



const

  VNC_PORT = 5900;

  RELAY_PORT = 5500;

  WM_SOCKET = WM_USER + 1;

  FIONREAD = $4004667F;

  ID_LENGTH = 9;



  // Добавьте эти типы GDI+ здесь:

  type

    TGPStatus = (

      Ok,

      GenericError,

      InvalidParameter,

      OutOfMemory,

      ObjectBusy,

      InsufficientBuffer,

      NotImplemented,

      Win32Error,

      WrongState,

      Aborted,

      FileNotFound,

      ValueOverflow,

      AccessDenied,

      UnknownImageFormat,

      FontFamilyNotFound,

      FontStyleNotFound,

      NotTrueTypeFont,

      UnsupportedGdiplusVersion,

      GdiplusNotInitialized,

      PropertyNotFound,

      PropertyNotSupported

    );



    TStatus = TGPStatus;



    GPBITMAP = Pointer;

    GPGRAPHICS = Pointer;



    TGPRectF = record

      X: Single;

      Y: Single;

      Width: Single;

      Height: Single;

    end;



  // Объявления функций GDI+

  function GdiplusStartup(out token: ULONG; input: Pointer; output: Pointer): TGPStatus; stdcall; external 'gdiplus.dll';

  procedure GdiplusShutdown(token: ULONG); stdcall; external 'gdiplus.dll';

  function GdipCreateBitmapFromStream(stream: IStream; out bitmap: GPBITMAP): TGPStatus; stdcall; external 'gdiplus.dll';

  function GdipGetImageWidth(image: GPBITMAP; out width: UINT): TGPStatus; stdcall; external 'gdiplus.dll';

  function GdipGetImageHeight(image: GPBITMAP; out height: UINT): TGPStatus; stdcall; external 'gdiplus.dll';

  function GdipCreateFromHDC(hdc: HDC; out graphics: GPGRAPHICS): TGPStatus; stdcall; external 'gdiplus.dll';

  function GdipGraphicsClear(graphics: GPGRAPHICS; color: DWORD): TGPStatus; stdcall; external 'gdiplus.dll';

  function GdipDrawImageRect(graphics: GPGRAPHICS; image: GPBITMAP; x, y, width, height: Single): TGPStatus; stdcall; external 'gdiplus.dll';

  procedure GdipDeleteGraphics(graphics: GPGRAPHICS); stdcall; external 'gdiplus.dll';

  procedure GdipDisposeImage(image: GPBITMAP); stdcall; external 'gdiplus.dll';



  type

    TGdiplusStartupInput = record

      GdiplusVersion: UINT32;

      DebugEventCallback: Pointer;

      SuppressBackgroundThread: BOOL;

      SuppressExternalCodecs: BOOL;

    end;



type

  TConnectionMode = (cmDirect, cmRelay);



  TImageFormat = (ifRAW, ifJPEG, ifPNG);



  TScreenHeader = packed record

    Width: DWORD;

    Height: DWORD;

    DataSize: DWORD;

    ImageFormat: Byte;  // 0 = RAW, 1 = JPEG, 2 = PNG

  end;



  TMouseMsg = packed record

    MsgType: Byte;

    Buttons: Byte;

    X: WORD;

    Y: WORD;

  end;



  TKeyMsg = packed record

    MsgType: Byte;

    IsDown: Byte;

    KeyCode: DWORD;

  end;



  { TForm1 }



  TForm1 = class(TForm)

    Button1: TButton;

    Button2: TButton;

    Button3: TButton;

    Edit1: TEdit;

    Edit2: TEdit;

    Edit3: TEdit;

    Label1: TLabel;

    Label2: TLabel;

    Label3: TLabel;

    Label4: TLabel;

    Label5: TLabel;

    Memo1: TMemo;

    Panel1: TPanel;

    ScrollBox1: TScrollBox;

    PaintBox1: TPaintBox;

    Timer1: TTimer;

    RadioGroup1: TRadioGroup;

    Label6: TLabel;

    Label7: TLabel;

    Label8: TLabel;



    procedure FormCreate(Sender: TObject);

    procedure FormDestroy(Sender: TObject);

    procedure FormKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);

    procedure FormKeyUp(Sender: TObject; var Key: Word; Shift: TShiftState);

    procedure Button1Click(Sender: TObject);

    procedure Button2Click(Sender: TObject);

    procedure Button3Click(Sender: TObject);

    procedure PaintBox1Paint(Sender: TObject);

    procedure PaintBox1MouseDown(Sender: TObject; Button: TMouseButton;

      Shift: TShiftState; X, Y: Integer);

    procedure PaintBox1MouseUp(Sender: TObject; Button: TMouseButton;

      Shift: TShiftState; X, Y: Integer);

    procedure PaintBox1MouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer);

    procedure Timer1Timer(Sender: TObject);

    procedure RadioGroup1Click(Sender: TObject);





  private

    FSocket: TSocket;

    FRelaySocket: TSocket;

    FConnected: Boolean;

    FWSAData: TWSADATA;

    FScreenBitmap: Graphics.TBitmap;

    FScreenWidth: Integer;

    FScreenHeight: Integer;

    FLogFile: TextFile;

    FLogOpened: Boolean;

    FFullScreenForm: TForm;

    FConnectionMode: TConnectionMode;

    FJPEGAccumulator: TMemoryStream;

    FRemoteWidth: Integer;

    FRemoteHeight: Integer;

    FLastUpdateTime: DWORD;



    // Buffering

    FReceiveBuffer: PByte;

    FReceiveBufferSize: Integer;

    FReceiveBufferUsed: Integer;

    FExpectingHeader: Boolean;

    FCurrentHeader: TScreenHeader;

    FImageDataReceived: Integer;



    FFullScreenMode: Boolean;

    FOriginalBorderStyle: TBorderStyle;

    FOriginalWindowState: TWindowState;



    // Relay variables

    FTargetID: string;

    FRelayIP: string;

    FRelayPort: Integer;

    FRelayBuffer: TMemoryStream;

    FRelayState: (rsDisconnected, rsConnected, rsWaiting, rsReady);

    FThroughRelay: Boolean;



    // GDI+ variables

    FGDIPlusToken: ULONG;

    FIsMouseDown: Boolean; // Предполагается, что эта переменная уже есть для отслеживания нажатия

    // Переменные для Mouse Throttling:

    FMouseTimer: TTimer;

    FNewMouseX: Integer;

    FNewMouseY: Integer;

    FMouseCoordsReady: Boolean;



    // Private procedures

    procedure SafeLog(const Msg: string);

    procedure ConnectToServer;

    procedure ConnectViaRelay;

    procedure DisconnectFromServer;

    procedure ProcessServerData;

    procedure ProcessRelayData;

    procedure RequestUpdate;

    procedure MouseTimerTick(Sender: TObject);

    procedure SendMouseCoords(X, Y: Integer; ButtonMask: Byte = 0);

    procedure SendMouseEvent(X, Y: Integer; Buttons: Byte);

    procedure SendKeyEvent(KeyCode: DWORD; IsDown: Boolean);

    procedure UpdateDisplay;

    procedure SetStatus(const Msg: string);

    procedure UpdateBitmapRegion(DataOffset: Integer; DataSize: Integer);

    procedure UpdateConnectionUI;

    procedure UpdateBitmapFromRelay(const Data: TBytes; StartIndex, DataSize: Integer);





    // JPEG decoding procedures

    procedure DecodeJPEGData(const JPEGData: PByte; DataSize: Integer);

    procedure ProcessJPEGFrame(const JPEGData: PByte; DataSize: Integer);



    // Relay procedures

    procedure ConnectToRelayServer;

    procedure DisconnectFromRelay;

    procedure SendToRelay(const Data: string);

    procedure SendBinaryToRelay(const Data: TBytes);

    procedure HandleRelayResponse(const Response: string);

    procedure ProcessRelayBinaryData;

    procedure ParseRelayAddress;



  protected

    procedure WMSocket(var Message: TLMessage); message WM_SOCKET;



  public



  end;



var

  Form1: TForm1;



implementation



{$R *.lfm}



function MinDouble(a, b: Double): Double;

begin

  if a < b then Result := a else Result := b;

end;



{ TForm1 }



procedure TForm1.FormCreate(Sender: TObject);

var

  LogFileName: string;

  Input: TGdiplusStartupInput;

  Status: TStatus;

begin

  // Инициализирую GDI+

  Input.DebugEventCallback := nil;

  Input.SuppressBackgroundThread := False;

  Input.SuppressExternalCodecs := False;

  Input.GdiplusVersion := 1;

  if GdiplusStartup(FGDIPlusToken, @Input, nil) <> Ok then

    SafeLog('Warning: GDI+ initialization failed');



  FNewMouseX := -1;

  FNewMouseY := -1;

  FMouseCoordsReady := False;

  //FMouseTimer := TTimer.Create(Self);

  //FMouseTimer.Interval := 50;

  //FMouseTimer.OnTimer := @MouseTimerTick;

  //FMouseTimer.Enabled := True;

  FRemoteWidth := 640;

  FRemoteHeight := 480;

  FLastUpdateTime := 0;



  // Инициализирую лог файл

  FLogOpened := False;

  try

    LogFileName := ExtractFilePath(Application.ExeName) + 'vnc_client_relay.log';

    AssignFile(FLogFile, LogFileName);

    Rewrite(FLogFile);

    FLogOpened := True;

    SafeLog('=== VNC Client with JPEG Support Started ===');

  except

    FLogOpened := False;

    SafeLog('Ошибка открытия лог-файла.');

  end;

  FJPEGAccumulator := TMemoryStream.Create;



  try

    SafeLog('Начинаем инициализацию FormCreate');



    Caption := 'VNC Client (JPEG Support)';

    Button1.Caption := 'Connect';

    Button2.Caption := 'Disconnect';

    Button3.Caption := 'Fullscreen';

    Button2.Enabled := False;

    Button3.Enabled := False;

    Timer1.Enabled := False;



    Label1.Caption := 'Server IP:';

    Label2.Caption := 'Port:';

    Label4.Caption := 'Server ID:';

    Label5.Caption := 'Mode:';

    Label6.Caption := 'Relay Status:';

    Label7.Caption := 'Disconnected';

    Label7.Font.Color := clRed;

    Label8.Caption := 'Connection: Direct';



    Edit1.Text := '127.0.0.1';

    Edit2.Text := IntToStr(VNC_PORT);

    Edit3.Text := '';



    RadioGroup1.ItemIndex := 0;

    RadioGroup1.OnClick := @RadioGroup1Click;



    FSocket := INVALID_SOCKET;

    FRelaySocket := INVALID_SOCKET;

    FConnected := False;

    FScreenWidth := 640;

    FScreenHeight := 480;



    FFullScreenMode := False;

    FOriginalBorderStyle := Self.BorderStyle;

    FOriginalWindowState := Self.WindowState;



    // Инициализация буферизации

    FReceiveBufferSize := 65536;

    GetMem(FReceiveBuffer, FReceiveBufferSize);

    FReceiveBufferUsed := 0;

    FExpectingHeader := True;

    FImageDataReceived := 0;

    FRelayBuffer := TMemoryStream.Create;





    // Relay initialization

    FConnectionMode := cmDirect;

    FTargetID := '';

    FRelayIP := '127.0.0.1';

    FRelayPort := RELAY_PORT;

    FRelayBuffer := TMemoryStream.Create;

    FRelayState := rsDisconnected;

    FThroughRelay := False;



    SafeLog('Создаем bitmap');



    // Инициализация Bitmap

    FScreenBitmap := nil;

    try

      FScreenBitmap := Graphics.TBitmap.Create;

      FScreenBitmap.PixelFormat := pf24bit;

      FScreenBitmap.Width := FScreenWidth;

      FScreenBitmap.Height := FScreenHeight;



      FScreenBitmap.Canvas.Brush.Color := clBlack;

      FScreenBitmap.Canvas.FillRect(Classes.Rect(0, 0, FScreenWidth, FScreenHeight));



      SafeLog('Bitmap создан успешно');

    except

      on E: Exception do

      begin

        SafeLog('Ошибка создания bitmap: ' + E.Message);

        if Assigned(FScreenBitmap) then

        begin

          FScreenBitmap.Free;

          FScreenBitmap := nil;

        end;

      end;

    end;



    PaintBox1.Width := FScreenWidth;

    PaintBox1.Height := FScreenHeight;



    KeyPreview := True;



    SafeLog('Инициализируем Winsock');



    // Инициализация Winsock

    if WSAStartup(MAKEWORD(2, 2), FWSAData) <> 0 then

    begin

      SafeLog('Ошибка инициализации Winsock: ' + IntToStr(WSAGetLastError));

      ShowMessage('Winsock init failed');

      Exit;

    end;



    SafeLog('Winsock инициализирован успешно');



    Timer1.Interval := 200;

    Timer1.Enabled := False;



    SetStatus('Ready to connect');

    UpdateConnectionUI;

    SafeLog('FormCreate завершен успешно');



  except

    on E: Exception do

    begin

      SafeLog('КРИТИЧЕСКАЯ ОШИБКА в FormCreate: ' + E.Message);

      ShowMessage('Critical error in FormCreate: ' + E.Message);

    end;

  end;

end;



procedure TForm1.DecodeJPEGData(const JPEGData: PByte; DataSize: Integer);

var

  Stream: IStream;

  Bitmap: GPBITMAP;

  Graphics: GPGRAPHICS;

  Rect: TGPRectF;

  Status: TStatus;

  hMem: HGLOBAL;

  pMem: Pointer;

  ImgWidth, ImgHeight: UINT;

  HexStr: string;

  i: Integer;

begin

  if not Assigned(FScreenBitmap) or (DataSize = 0) then Exit;



  try

    HexStr := '';

    for i := 0 to Min(15, DataSize - 1) do

      HexStr := HexStr + IntToHex(PByte(JPEGData + i)^, 2) + ' ';

    SafeLog(Format('DecodeJPEGData: DataSize=%d, First bytes: %s', [DataSize, HexStr]));



    hMem := GlobalAlloc(GMEM_MOVEABLE, DataSize);

    if hMem = 0 then Exit;



    try

      pMem := GlobalLock(hMem);

      if pMem = nil then Exit;



      Move(JPEGData^, pMem^, DataSize);

      GlobalUnlock(hMem);



      if CreateStreamOnHGlobal(hMem, False, Stream) <> S_OK then

      begin

        SafeLog('CreateStreamOnHGlobal failed');

        Exit;

      end;



      Status := GdipCreateBitmapFromStream(Stream, Bitmap);

      if Status <> Ok then

      begin

        SafeLog('GdipCreateBitmapFromStream failed with status: ' + IntToStr(Ord(Status)));



        if DataSize < 100 then

          SafeLog('JPEG data is too small: ' + IntToStr(DataSize) + ' bytes');



        if (DataSize >= 2) and (PByte(JPEGData)^ = $FF) and (PByte(JPEGData + 1)^ = $D8) then

          SafeLog('JPEG starts with correct marker FF D8')

        else

          SafeLog('JPEG does not start with FF D8');



        if (DataSize >= 4) then

        begin

          SafeLog(Format('First 4 bytes: %2.2x %2.2x %2.2x %2.2x',

            [PByte(JPEGData)^, PByte(JPEGData + 1)^,

             PByte(JPEGData + 2)^, PByte(JPEGData + 3)^]));

        end;



        Exit;

      end;



      try

        GdipGetImageWidth(Bitmap, ImgWidth);

        GdipGetImageHeight(Bitmap, ImgHeight);





        FRemoteWidth := ImgWidth;

        FRemoteHeight := ImgHeight;



        SafeLog(Format('JPEG dimensions: %dx%d', [ImgWidth, ImgHeight]));



        if (FScreenWidth <> Integer(ImgWidth)) or (FScreenHeight <> Integer(ImgHeight)) then

        begin

          FScreenWidth := ImgWidth;

          FScreenHeight := ImgHeight;

          FScreenBitmap.Width := ImgWidth;

          FScreenBitmap.Height := ImgHeight;

          PaintBox1.Width := ImgWidth;

          PaintBox1.Height := ImgHeight;

        end;



        Status := GdipCreateFromHDC(FScreenBitmap.Canvas.Handle, Graphics);

        if Status <> Ok then

        begin

          SafeLog('Failed to create graphics: ' + IntToStr(Ord(Status)));

          Exit;

        end;



        try

          GdipGraphicsClear(Graphics, $FF000000);



          Rect.X := 0;

          Rect.Y := 0;

          Rect.Width := ImgWidth;

          Rect.Height := ImgHeight;



          Status := GdipDrawImageRect(Graphics, Bitmap,

            Rect.X, Rect.Y, Rect.Width, Rect.Height);



          if Status <> Ok then

            SafeLog('Failed to draw image: ' + IntToStr(Ord(Status)))

          else

            SafeLog('Image drawn successfully');



        finally

          GdipDeleteGraphics(Graphics);

        end;



        PaintBox1.Invalidate;



      finally

        GdipDisposeImage(Bitmap);

      end;



    finally

      GlobalFree(hMem);

    end;



  except

    on E: Exception do

      SafeLog('Error in DecodeJPEGData: ' + E.Message);

  end;

end;



procedure TForm1.MouseTimerTick(Sender: TObject);

var

  ButtonMask: Byte;

begin

  if FMouseCoordsReady and (FRelayState = rsReady) then

  begin

    ButtonMask := 0;

    if FIsMouseDown then

      ButtonMask := 1;



    SendMouseCoords(FNewMouseX, FNewMouseY, ButtonMask);



    FMouseCoordsReady := False; // Сброс флага до следующего движения мыши

  end;

end;



procedure TForm1.UpdateBitmapFromRelay(const Data: TBytes; StartIndex, DataSize: Integer);

var

  BytesPerPixel, BytesPerLine: Integer;

  CurrentOffset, Row, ColOffset, ChunkSize: Integer;

  DestPtr: PByte;

  SrcIndex: Integer;

begin

  if (FScreenBitmap = nil) then Exit;



  BytesPerPixel := 3;

  BytesPerLine := FScreenBitmap.Width * BytesPerPixel;



  SrcIndex := StartIndex;

  CurrentOffset := 0;



  FScreenBitmap.BeginUpdate;

  try

    while (DataSize > 0) and (CurrentOffset < FScreenBitmap.Height * BytesPerLine) do

    begin

      Row := CurrentOffset div BytesPerLine;

      ColOffset := CurrentOffset mod BytesPerLine;



      if Row >= FScreenBitmap.Height then Break;



      DestPtr := FScreenBitmap.ScanLine[Row];

      Inc(DestPtr, ColOffset);



      ChunkSize := Min(DataSize, BytesPerLine - ColOffset);



      Move(Data[SrcIndex], DestPtr^, ChunkSize);



      Inc(SrcIndex, ChunkSize);

      Inc(CurrentOffset, ChunkSize);

      Dec(DataSize, ChunkSize);

    end;

  finally

    FScreenBitmap.EndUpdate;

  end;

end;



procedure TForm1.ProcessJPEGFrame(const JPEGData: PByte; DataSize: Integer);

var

  i: Integer;

begin

  if DataSize < 100 then

  begin

    SafeLog('JPEG data too small: ' + IntToStr(DataSize) + ' bytes');

    Exit;

  end;



  // Проверяем JPEG маркеры

  if (JPEGData^ = $FF) and ((JPEGData + 1)^ = $D8) then

  begin

    SafeLog('Valid JPEG start marker found');

    DecodeJPEGData(JPEGData, DataSize);

  end

  else

  begin

    SafeLog('Invalid JPEG data - no start marker');



    // Ищем JPEG маркер в данных

    for i := 0 to DataSize - 2 do

    begin

      if (PByte(JPEGData + i)^ = $FF) and (PByte(JPEGData + i + 1)^ = $D8) then

      begin

        SafeLog(Format('Found JPEG marker at offset %d, attempting decode', [i]));

        DecodeJPEGData(JPEGData + i, DataSize - i);

        Exit;

      end;

    end;



    SafeLog('No JPEG markers found in data');

  end;

end;



procedure TForm1.FormDestroy(Sender: TObject);

begin

  try

    SafeLog('Начинаем FormDestroy');



    if Assigned(FRelayBuffer) then

    FreeAndNil(FRelayBuffer);



    // Закрываю полноэкранную форму если она открыта

    if Assigned(FFullScreenForm) then

    begin

      FFullScreenForm.Close;

      FFullScreenForm := nil;

    end;



    FreeAndNil(FJPEGAccumulator);



    DisconnectFromServer;



    if Assigned(FScreenBitmap) then

    begin

      FScreenBitmap.Free;

      FScreenBitmap := nil;

      SafeLog('Bitmap освобожден');

    end;



    if FReceiveBuffer <> nil then

    begin

      FreeMem(FReceiveBuffer);

      FReceiveBuffer := nil;

      FReceiveBufferSize := 0;

      FReceiveBufferUsed := 0;

      SafeLog('Буфер приема освобожден');

    end;



    if Assigned(FRelayBuffer) then

      FRelayBuffer.Free;



    // Shutdown GDI+

    GdiplusShutdown(FGDIPlusToken);



    WSACleanup;

    SafeLog('Winsock очищен');



    if FLogOpened then

    begin

      SafeLog('=== VNC Client Stopped ===');

      CloseFile(FLogFile);

      FLogOpened := False;

    end;

  except

    on E: Exception do

    begin

      SafeLog('Ошибка в FormDestroy: ' + E.Message);

    end;

  end;

end;



procedure TForm1.FormKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);

begin

  try

    // Обработка горячих клавиш

    if (ssCtrl in Shift) and (Key = VK_F) then

    begin

      SafeLog('Нажата горячая клавиша Ctrl+F');

      if FFullScreenMode then

      begin

        Button3Click(nil);

        Key := 0;

        Exit;

      end;

    end;



    // Обработка клавиши Escape для выхода из полноэкранного режима

    if (Key = VK_ESCAPE) and FFullScreenMode then

    begin

      SafeLog('Нажата клавиша Escape в полноэкранном режиме');

      Button3Click(nil);

      Key := 0;

      Exit;

    end;



    // Отправляем событие клавиши

    if FConnected and (Key <> 0) then

    begin

      SafeLog(Format('Отправляем KeyDown: Code=%d', [Key]));

      SendKeyEvent(Key, True);

    end;

  except

    on E: Exception do

      SafeLog('Ошибка в FormKeyDown: ' + E.Message);

  end;

end;



procedure TForm1.FormKeyUp(Sender: TObject; var Key: Word; Shift: TShiftState);

begin

  try

    if FConnected and (Key <> 0) then

    begin

      SafeLog(Format('Отправляем KeyUp: Code=%d', [Key]));

      SendKeyEvent(Key, False);

    end;

  except

    on E: Exception do

      SafeLog('Ошибка в FormKeyUp: ' + E.Message);

  end;

end;



procedure TForm1.Button1Click(Sender: TObject);

begin

  try

    SafeLog('Нажата кнопка Connect');



    if FConnectionMode = cmDirect then

      ConnectToServer

    else

      ConnectViaRelay;

  except

    on E: Exception do

    begin

      SafeLog('Ошибка в Button1Click: ' + E.Message);

    end;

  end;

end;



procedure TForm1.Button2Click(Sender: TObject);

begin

  try

    SafeLog('Нажата кнопка Disconnect');

    DisconnectFromServer;

  except

    on E: Exception do

      SafeLog('Ошибка в Button2Click: ' + E.Message);

  end;

end;



procedure TForm1.Button3Click(Sender: TObject);

begin

  try

    SafeLog('Нажата кнопка Fullscreen');



    if not FFullScreenMode then

    begin

      // Создаем полноэкранную форму

      FFullScreenForm := TForm.Create(Self);

      FFullScreenForm.BorderStyle := bsNone;

      FFullScreenForm.WindowState := wsFullScreen;

      FFullScreenForm.KeyPreview := True;

      FFullScreenForm.Color := clBlack;



      // Создаю PaintBox для полноэкранной формы

      with TPaintBox.Create(FFullScreenForm) do

      begin

        Parent := FFullScreenForm;

        Align := alClient;

        OnPaint := @PaintBox1Paint;

        OnMouseDown := @PaintBox1MouseDown;

        OnMouseUp := @PaintBox1MouseUp;

        OnMouseMove := @PaintBox1MouseMove;

      end;
 

Admin

Администратор
Код:
      // Обработчики клавиатуры для полноэкранной формы



      FFullScreenForm.OnKeyDown := @FormKeyDown;



      FFullScreenForm.OnKeyUp := @FormKeyUp;







      FFullScreenForm.Show;



      FFullScreenMode := True;



      Button3.Caption := 'Exit Fullscreen';







      SafeLog('Полноэкранный режим включен. Используйте Ctrl+F или Escape для выхода.');



    end



    else



    begin



      // Закрываю полноэкранную форму



      if Assigned(FFullScreenForm) then



      begin



        FFullScreenForm.Close;



        FFullScreenForm := nil;



      end;







      FFullScreenMode := False;



      Button3.Caption := 'Fullscreen';







      SafeLog('Полноэкранный режим выключен');



    end;







  except



    on E: Exception do



      SafeLog('Ошибка в Button3Click: ' + E.Message);



  end;



end;











procedure TForm1.PaintBox1MouseDown(Sender: TObject; Button: TMouseButton;



  Shift: TShiftState; X, Y: Integer);



var



  ButtonMask: Byte;



  ActualX, ActualY: Integer;



begin



  try



    ButtonMask := 0;



    if Button = mbLeft then ButtonMask := 1



    else if Button = mbRight then ButtonMask := 2



    else if Button = mbMiddle then ButtonMask := 4;







    // Преобразую координаты с учетом масштабирования в полноэкранном режиме



    if FFullScreenMode then



    begin



      ActualX := Round(X * FScreenWidth / PaintBox1.Width);



      ActualY := Round(Y * FScreenHeight / PaintBox1.Height);



    end



    else



    begin



      ActualX := X;



      ActualY := Y;



    end;







    SendMouseEvent(ActualX, ActualY, ButtonMask);



  except



    on E: Exception do



      SafeLog('Ошибка в PaintBox1MouseDown: ' + E.Message);



  end;



end;







procedure TForm1.PaintBox1MouseUp(Sender: TObject; Button: TMouseButton;



  Shift: TShiftState; X, Y: Integer);



var



  ActualX, ActualY: Integer;



begin



  try



    // Преобразую координаты с учетом маштабирования в полноэкранном режиме



    if FFullScreenMode then



    begin



      ActualX := Round(X * FScreenWidth / PaintBox1.Width);



      ActualY := Round(Y * FScreenHeight / PaintBox1.Height);



    end



    else



    begin



      ActualX := X;



      ActualY := Y;



    end;







    SendMouseEvent(ActualX, ActualY, 0);



  except



    on E: Exception do



      SafeLog('Ошибка в PaintBox1MouseUp: ' + E.Message);



  end;



end;







procedure TForm1.PaintBox1MouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer);



begin



  // ОТПРАВЛЯЕМ СРАЗУ, но с проверкой частоты внутри SendMouseCoords



  if FConnected then



    SendMouseCoords(X, Y, 0);



end;







procedure TForm1.SendMouseCoords(X, Y: Integer; ButtonMask: Byte);



var



  Buffer: array[0..5] of Byte;



  CurrentTime: DWORD;



  TimeDiff: DWORD;



const



  MIN_MOUSE_DELAY = 100;



begin



  CurrentTime := GetTickCount;



  if CurrentTime >= FLastUpdateTime then



    TimeDiff := CurrentTime - FLastUpdateTime



  else



    TimeDiff := MAXDWORD - FLastUpdateTime + CurrentTime;







  if TimeDiff < MIN_MOUSE_DELAY then



    Exit;







  FLastUpdateTime := CurrentTime;







  if (X < 0) or (Y < 0) or (X >= FRemoteWidth) or (Y >= FRemoteHeight) then



    Exit;







  Buffer[0] := $02;



  Buffer[1] := ButtonMask;



  PWord(@Buffer[2])^ := Word(X);



  PWord(@Buffer[4])^ := Word(Y);







  send(FRelaySocket, Buffer, SizeOf(Buffer), 0);



end;







procedure TForm1.Timer1Timer(Sender: TObject);



begin



  try



    if FConnected then



    begin



      SafeLog('Timer: запрашиваем обновление');



      RequestUpdate;



    end;



  except



    on E: Exception do



    begin



      SafeLog('Ошибка в Timer1Timer: ' + E.Message);



      Timer1.Enabled := False;



    end;



  end;



end;







procedure TForm1.RadioGroup1Click(Sender: TObject);



begin



  FConnectionMode := TConnectionMode(RadioGroup1.ItemIndex);







  if FConnectionMode = cmDirect then



  begin



    Edit1.Enabled := True;



    Edit2.Enabled := True;



    Edit3.Enabled := False;



    Label8.Caption := 'Connection: Direct';



  end



  else



  begin



    Edit1.Enabled := False;



    Edit2.Enabled := False;



    Edit3.Enabled := True;



    Label8.Caption := 'Connection: Via Relay';



  end;







  SafeLog('Changed connection mode to: ' + IntToStr(Ord(FConnectionMode)));



end;







procedure TForm1.SafeLog(const Msg: string);



var



  TimeStr: string;



  FullMsg: string;



begin



  try



    TimeStr := FormatDateTime('hh:nn:ss', Now);



    FullMsg := '[' + TimeStr + '] ' + Msg;







    if Assigned(Memo1) then



    begin



      Memo1.Lines.Add(FullMsg);



      while Memo1.Lines.Count > 50 do



        Memo1.Lines.Delete(0);



      Application.ProcessMessages;



    end;







    if FLogOpened then



    begin



      WriteLn(FLogFile, FullMsg);



      Flush(FLogFile);



    end;



  except



  end;



end;







procedure TForm1.ConnectToServer;



var



  ServerAddr: TSockAddrIn;



  ServerIP: string;



  ServerPort: Integer;



  SocketBufferSize: Integer;



  OptLen: Integer;



begin



  if FConnected then Exit;







  try



    ServerIP := Edit1.Text;



    ServerPort := StrToIntDef(Edit2.Text, VNC_PORT);







    SafeLog('Подключаемся напрямую к ' + ServerIP + ':' + IntToStr(ServerPort));







    if ServerIP = '' then



    begin



      ShowMessage('Enter server IP address');



      Exit;



    end;







    SetStatus('Connecting to server...');







    FSocket := socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);



    if FSocket = INVALID_SOCKET then



    begin



      SafeLog('Ошибка создания сокета: ' + IntToStr(WSAGetLastError));



      raise Exception.Create('Failed to create socket');



    end;







    SocketBufferSize := 1024 * 1024;



    OptLen := SizeOf(SocketBufferSize);



    if setsockopt(FSocket, SOL_SOCKET, SO_RCVBUF, @SocketBufferSize, OptLen) = SOCKET_ERROR then



    begin



      SafeLog('Предупреждение: не удалось установить размер буфера приема: ' + IntToStr(WSAGetLastError));



    end



    else



    begin



      SafeLog('Буфер приема сокета установлен в ' + IntToStr(SocketBufferSize) + ' байт');



    end;







    SafeLog('Сокет создан: ' + IntToStr(FSocket));







    ZeroMemory(@ServerAddr, SizeOf(ServerAddr));



    ServerAddr.sin_family := AF_INET;



    ServerAddr.sin_addr.s_addr := inet_addr(PAnsiChar(AnsiString(ServerIP)));



    ServerAddr.sin_port := htons(ServerPort);







    SafeLog('Вызываем connect()');







    if connect(FSocket, @ServerAddr, SizeOf(ServerAddr)) = SOCKET_ERROR then



    begin



      SafeLog('Ошибка connect: ' + IntToStr(WSAGetLastError));



      closesocket(FSocket);



      FSocket := INVALID_SOCKET;



      raise Exception.Create('Failed to connect to server');



    end;







    SafeLog('TCP соединение установлено');



    SetStatus('Connection established');







    SafeLog('Настраиваем асинхронные уведомления');



    if WSAAsyncSelect(FSocket, Handle, WM_SOCKET, FD_READ or FD_CLOSE) = SOCKET_ERROR then



    begin



      SafeLog('Ошибка WSAAsyncSelect: ' + IntToStr(WSAGetLastError));



      closesocket(FSocket);



      FSocket := INVALID_SOCKET;



      raise Exception.Create('Failed to set up async notifications');



    end



    else



    begin



      SafeLog('Асинхронные уведомления настроены успешно');



    end;







    FConnected := True;



    FThroughRelay := False;



    UpdateConnectionUI;







    // Сбрасываю состояние буфера



    FReceiveBufferUsed := 0;



    FExpectingHeader := True;



    FImageDataReceived := 0;







    Timer1.Interval := 200;



    Timer1.Enabled := True;







    SetStatus('Connected to ' + ServerIP + ':' + IntToStr(ServerPort));



    SafeLog('Прямое подключение завершено, ждем данных от сервера');







  except



    on E: Exception do



    begin



      SafeLog('ОШИБКА подключения: ' + E.Message);



      SetStatus('Connection error: ' + E.Message);



      ShowMessage('Connection error: ' + E.Message);







      if FSocket <> INVALID_SOCKET then



      begin



        closesocket(FSocket);



        FSocket := INVALID_SOCKET;



        SafeLog('Сокет закрыт после ошибки');



      end;



      UpdateConnectionUI;



    end;



  end;



end;







procedure TForm1.ConnectViaRelay;



begin



  if FConnected then Exit;







  FTargetID := Edit3.Text;







  if FTargetID = '' then



  begin



    ShowMessage('Enter server ID');



    Exit;



  end;







  SetStatus('Connecting via relay to ID: ' + FTargetID);







  // Сначала подключаемся к relay серверу



  ConnectToRelayServer;



end;







procedure TForm1.ConnectToRelayServer;



var



  RelayAddr: TSockAddrIn;



begin



  if FRelayState <> rsDisconnected then Exit;







  try



    ParseRelayAddress;







    SafeLog('Connecting to relay server: ' + FRelayIP + ':' + IntToStr(FRelayPort));







    FRelaySocket := socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);



    if FRelaySocket = INVALID_SOCKET then



    begin



      SafeLog('Ошибка создания relay сокета: ' + IntToStr(WSAGetLastError));



      raise Exception.Create('Failed to create relay socket');



    end;







    ZeroMemory(@RelayAddr, SizeOf(RelayAddr));



    RelayAddr.sin_family := AF_INET;



    RelayAddr.sin_addr.s_addr := inet_addr(PAnsiChar(AnsiString(FRelayIP)));



    RelayAddr.sin_port := htons(FRelayPort);







    if connect(FRelaySocket, @RelayAddr, SizeOf(RelayAddr)) = SOCKET_ERROR then



    begin



      SafeLog('Ошибка подключения к relay: ' + IntToStr(WSAGetLastError));



      closesocket(FRelaySocket);



      FRelaySocket := INVALID_SOCKET;



      raise Exception.Create('Failed to connect to relay server');



    end;







    WSAAsyncSelect(FRelaySocket, Handle, WM_SOCKET, FD_READ or FD_CLOSE);







    FRelayState := rsConnected;



    Label7.Caption := 'Connected to relay';



    Label7.Font.Color := clGreen;







    SafeLog('Connected to relay server');







    // Отправляем запрос на подключение к целевому серверу



    SendToRelay('CONNECT_CLIENT ' + FTargetID);



    FRelayState := rsWaiting;



    Label7.Caption := 'Waiting for server...';



    Label7.Font.Color := clBlue;







    SetStatus('Waiting for server connection via relay...');







  except



    on E: Exception do



    begin



      SafeLog('Relay connection error: ' + E.Message);



      SetStatus('Relay connection error: ' + E.Message);



      FRelayState := rsDisconnected;



      Label7.Caption := 'Connection failed';



      Label7.Font.Color := clRed;



    end;



  end;



end;







procedure TForm1.DisconnectFromServer;



begin



  try



    if not FConnected and (FRelayState = rsDisconnected) then Exit;







    SafeLog('Начинаем отключение');







    FConnected := False;



    Timer1.Enabled := False;







    if FSocket <> INVALID_SOCKET then



    begin



      WSAAsyncSelect(FSocket, Handle, 0, 0);



      closesocket(FSocket);



      FSocket := INVALID_SOCKET;



      SafeLog('Основной сокет закрыт');



    end;







    DisconnectFromRelay;







    // Сбрасываю состояние буфера



    FReceiveBufferUsed := 0;



    FExpectingHeader := True;



    FImageDataReceived := 0;







    if Assigned(FScreenBitmap) then



    begin



      FScreenBitmap.Canvas.Brush.Color := clBlack;



      FScreenBitmap.Canvas.FillRect(Classes.Rect(0, 0, FScreenBitmap.Width, FScreenBitmap.Height));



      PaintBox1.Invalidate;



    end;







    SetStatus('Disconnected');



    UpdateConnectionUI;



    SafeLog('Отключение завершено');







  except



    on E: Exception do



      SafeLog('Ошибка в DisconnectFromServer: ' + E.Message);



  end;



end;







procedure TForm1.DisconnectFromRelay;



begin



  if FRelaySocket <> INVALID_SOCKET then



  begin



    WSAAsyncSelect(FRelaySocket, Handle, 0, 0);



    closesocket(FRelaySocket);



    FRelaySocket := INVALID_SOCKET;



    SafeLog('Relay сокет закрыт');



  end;







  FRelayState := rsDisconnected;



  Label7.Caption := 'Disconnected';



  Label7.Font.Color := clRed;



  FRelayBuffer.Clear;



end;







procedure TForm1.ProcessServerData;



var



  BytesReceived: Integer;



  BytesProcessed: Integer;



  RemainingDataInSocket: LongWord;



  ExpectedTotalData: Cardinal;



  MaxBufferSize: Cardinal;



  FramesProcessed: Integer;



  RemainingImageData: Integer;



  AvailableData: Integer;



  RemainingBytes: Integer;



begin



  try



    if FSocket = INVALID_SOCKET then



    begin



      SafeLog('ProcessServerData: FSocket недействителен, выход.');



      Exit;



    end;







    MaxBufferSize := 10 * 1024 * 1024;







    if ioctlsocket(FSocket, FIONREAD, @RemainingDataInSocket) = SOCKET_ERROR then



    begin



      SafeLog('Ошибка ioctlsocket (FIONREAD): ' + IntToStr(WSAGetLastError));



      DisconnectFromServer;



      Exit;



    end;







    if RemainingDataInSocket = 0 then



    begin



      SafeLog('ProcessServerData: Нет данных для чтения из сокета.');



      Exit;



    end;







    if FReceiveBufferUsed + RemainingDataInSocket > MaxBufferSize then



    begin



      SafeLog(Format('ПРЕДУПРЕЖДЕНИЕ: Буфер переполнен (%d + %d > %d), сбрасываем старые данные',



        [FReceiveBufferUsed, RemainingDataInSocket, MaxBufferSize]));







      FReceiveBufferUsed := 0;



      FExpectingHeader := True;



      FImageDataReceived := 0;







      RemainingDataInSocket := Min(RemainingDataInSocket, MaxBufferSize div 2);



    end;







    SafeLog(Format('ProcessServerData: в сокете доступно %d байт, в буфере %d байт',



      [RemainingDataInSocket, FReceiveBufferUsed]));







    ExpectedTotalData := FReceiveBufferUsed + RemainingDataInSocket;



    if FReceiveBufferSize < ExpectedTotalData then



    begin



      FReceiveBufferSize := ExpectedTotalData + (64 * 1024);



      ReallocMem(FReceiveBuffer, FReceiveBufferSize);



      SafeLog(Format('Буфер приема увеличен до %d байт', [FReceiveBufferSize]));



    end;







    BytesReceived := recv(FSocket, (FReceiveBuffer + FReceiveBufferUsed)^, RemainingDataInSocket, 0);







    if BytesReceived = SOCKET_ERROR then



    begin



      SafeLog('Ошибка recv: ' + IntToStr(WSAGetLastError));



      DisconnectFromServer;



      Exit;



    end;







    if BytesReceived = 0 then



    begin



      SafeLog('Соединение закрыто сервером (recv вернул 0)');



      DisconnectFromServer;



      Exit;



    end;







    Inc(FReceiveBufferUsed, BytesReceived);



    SafeLog(Format('Получено %d байт, в буфере теперь %d байт', [BytesReceived, FReceiveBufferUsed]));







    BytesProcessed := 0;



    FramesProcessed := 0;







    while (FReceiveBufferUsed - BytesProcessed >= SizeOf(TScreenHeader)) and (FramesProcessed < 3) do



    begin



      if FExpectingHeader then



      begin



        Move((FReceiveBuffer + BytesProcessed)^, FCurrentHeader, SizeOf(TScreenHeader));



        Inc(BytesProcessed, SizeOf(TScreenHeader));







        SafeLog(Format('Заголовок: %dx%d, данных: %d байт, формат: %d',



          [FCurrentHeader.Width, FCurrentHeader.Height, FCurrentHeader.DataSize, FCurrentHeader.ImageFormat]));







        if (FCurrentHeader.Width < 1) or (FCurrentHeader.Width > 4096) or



           (FCurrentHeader.Height < 1) or (FCurrentHeader.Height > 4096) or



           (FCurrentHeader.DataSize < 1) or (FCurrentHeader.DataSize > 50 * 1024 * 1024) then



        begin



          SafeLog(Format('Некорректный заголовок (%dx%d, DataSize=%d), поиск следующего',



            [FCurrentHeader.Width, FCurrentHeader.Height, FCurrentHeader.DataSize]));







          while (BytesProcessed < FReceiveBufferUsed - SizeOf(TScreenHeader)) do



          begin



            Inc(BytesProcessed);



            Move((FReceiveBuffer + BytesProcessed)^, FCurrentHeader, SizeOf(TScreenHeader));



            if (FCurrentHeader.Width >= 1) and (FCurrentHeader.Width <= 4096) and



               (FCurrentHeader.Height >= 1) and (FCurrentHeader.Height <= 4096) and



               (FCurrentHeader.DataSize >= 1) and (FCurrentHeader.DataSize <= 50 * 1024 * 1024) then



            begin



              SafeLog(Format('Найден корректный заголовок на позиции %d', [BytesProcessed]));



              Inc(BytesProcessed, SizeOf(TScreenHeader));



              Break;



            end;



          end;







          if BytesProcessed >= FReceiveBufferUsed - SizeOf(TScreenHeader) then



          begin



            SafeLog('Корректный заголовок не найден, очищаем буфер');



            FReceiveBufferUsed := 0;



            FExpectingHeader := True;



            Exit;



          end;



        end;







        if (FScreenWidth <> FCurrentHeader.Width) or (FScreenHeight <> FCurrentHeader.Height) then



        begin



          SafeLog(Format('Изменяем размер bitmap: %dx%d -> %dx%d',



            [FScreenWidth, FScreenHeight, FCurrentHeader.Width, FCurrentHeader.Height]));







          FScreenWidth := FCurrentHeader.Width;



          FScreenHeight := FCurrentHeader.Height;







          if not Assigned(FScreenBitmap) then



          begin



            FScreenBitmap := Graphics.TBitmap.Create;



            FScreenBitmap.PixelFormat := pf24bit;



          end;







          FScreenBitmap.Width := FScreenWidth;



          FScreenBitmap.Height := FScreenHeight;



          FScreenBitmap.Canvas.Brush.Color := clBlack;



          FScreenBitmap.Canvas.FillRect(Classes.Rect(0, 0, FScreenWidth, FScreenHeight));







          PaintBox1.Width := FScreenWidth;



          PaintBox1.Height := FScreenHeight;



        end;







        FExpectingHeader := False;



        FImageDataReceived := 0;



      end;







      if not FExpectingHeader then



      begin



        RemainingImageData := Integer(FCurrentHeader.DataSize) - FImageDataReceived;



        AvailableData := FReceiveBufferUsed - BytesProcessed;







        if AvailableData >= RemainingImageData then



        begin



          if FCurrentHeader.ImageFormat = 1 then // JPEG



          begin



            // Обработка JPEG через GDI+



            ProcessJPEGFrame(FReceiveBuffer + BytesProcessed, RemainingImageData);



          end



          else // RAW



          begin



            UpdateBitmapRegion(BytesProcessed, RemainingImageData);



          end;







          Inc(BytesProcessed, RemainingImageData);



          Inc(FImageDataReceived, RemainingImageData);







          SafeLog(Format('Кадр %d завершен (%d байт, Формат: %s)',



            [FramesProcessed + 1, FCurrentHeader.DataSize,



             IfThen(FCurrentHeader.ImageFormat = 1, 'JPEG', 'RAW')]));







          FExpectingHeader := True;



          FImageDataReceived := 0;



          Inc(FramesProcessed);







          if FramesProcessed = 1 then



            UpdateDisplay;



        end



        else



        begin



          if AvailableData > 0 then



          begin



            UpdateBitmapRegion(BytesProcessed, AvailableData);



            Inc(FImageDataReceived, AvailableData);



            Inc(BytesProcessed, AvailableData);







            SafeLog(Format('Частичные данные: %d/%d байт получено',



              [FImageDataReceived, FCurrentHeader.DataSize]));



          end;



          Break;



        end;



      end;



    end;







    if FramesProcessed > 1 then



    begin



      SafeLog(Format('Обработано %d кадров за один вызов', [FramesProcessed]));



      UpdateDisplay;



    end;







    if BytesProcessed > 0 then



    begin



      RemainingBytes := FReceiveBufferUsed - BytesProcessed;



      if RemainingBytes > 0 then



      begin



        Move((FReceiveBuffer + BytesProcessed)^, FReceiveBuffer^, RemainingBytes);



        FReceiveBufferUsed := RemainingBytes;



        SafeLog(Format('Буфер сдвинут, осталось %d байт', [FReceiveBufferUsed]));



      end



      else



      begin



        FReceiveBufferUsed := 0;



        SafeLog('Буфер полностью обработан');



      end;



    end;







    if not Timer1.Enabled and FConnected then



    begin



      Timer1.Enabled := True;



      SafeLog('Таймер обновлений запущен');



    end;







  except



    on E: Exception do



    begin



      SafeLog('ОШИБКА в ProcessServerData: ' + E.Message);



      FReceiveBufferUsed := 0;



      FExpectingHeader := True;



      FImageDataReceived := 0;



      DisconnectFromServer;



    end;



  end;



end;







procedure TForm1.ProcessRelayData;



var



  Buffer: array[0..65535] of Byte;



  BytesReceived: Integer;



  StrData: string;



  NewlinePos: Integer;



  RemainingData: string;



begin



  if FRelaySocket = INVALID_SOCKET then Exit;







  // 1. Читаем из сокета



  BytesReceived := recv(FRelaySocket, Buffer, SizeOf(Buffer), 0);







  if BytesReceived <= 0 then



  begin



    SafeLog('Relay server disconnected (recv 0)');



    DisconnectFromServer;



    Exit;



  end;







  // 2. ВАЖНО: Ставим позицию в конец для ЗАПИСИ



  FRelayBuffer.Position := FRelayBuffer.Size;



  FRelayBuffer.Write(Buffer, BytesReceived);







  // 3. Обработка



  if FRelayState = rsWaiting then



  begin



    // --- ТЕКСТОВЫЙ РЕЖИМ ---



    FRelayBuffer.Position := 0; // Читаем с начала



    SetLength(StrData, FRelayBuffer.Size);



    if Length(StrData) > 0 then



      FRelayBuffer.Read(StrData[1], Length(StrData));







    NewlinePos := Pos(#13#10, StrData);



    if NewlinePos > 0 then



    begin



      HandleRelayResponse(Copy(StrData, 1, NewlinePos - 1));







      // Сохраняем "хвост" если он есть (начало JPEG)



      if NewlinePos + 1 < Length(StrData) then



      begin



        RemainingData := Copy(StrData, NewlinePos + 2, Length(StrData));



        FRelayBuffer.Clear;



        if Length(RemainingData) > 0 then



          FRelayBuffer.Write(RemainingData[1], Length(RemainingData));



      end



      else



        FRelayBuffer.Clear;



    end;



  end



  else if FRelayState = rsReady then



  begin



    // --- БИНАРНЫЙ РЕЖИМ ---



    // ВАЖНО: Сбрасываем позицию в 0, чтобы функция обработки видела данные



    FRelayBuffer.Position := 0;



    ProcessRelayBinaryData;



  end;



end;







procedure TForm1.HandleRelayResponse(const Response: string);



var



  Lines: TStringList;



  i: Integer;



  Line: string;



begin



  Lines := TStringList.Create;



  try



    Lines.Text := Response;







    for i := 0 to Lines.Count - 1 do



    begin



      Line := Trim(Lines[i]);







      if Line = '' then Continue;







      SafeLog('<< Relay: ' + Line);







      if Pos('CONNECTED', Line) = 1 then



      begin



        FConnected := True;



        FThroughRelay := True;







        FRelayState := rsReady;



        Label7.Caption := 'Connected to server';



        Label7.Font.Color := clGreen;







        SetStatus('Connected via relay to server: ' + FTargetID);







        UpdateConnectionUI;







        FExpectingHeader := True;



        FImageDataReceived := 0;







        // Сбрасываем буфер релея



        FRelayBuffer.Clear;







        Timer1.Enabled := True;







        SafeLog('Successfully connected via relay to server');







        // Запросить первый кадр



        RequestUpdate();







      end



      else if Pos('ERROR', Line) = 1 then



      begin



        SetStatus('Relay error: ' + Copy(Line, 7, MaxInt));



        SafeLog('Relay error: ' + Line);



        DisconnectFromServer;



      end



      else if Pos('SERVER_DISCONNECTED', Line) = 1 then



      begin



        SetStatus('Server disconnected via relay');



        SafeLog('Server disconnected');



        DisconnectFromServer;



      end;



    end;



  finally



    Lines.Free;



  end;



end;







procedure TForm1.ProcessRelayBinaryData;



var



  Header: TScreenHeader;



  TotalFrameSize: Integer;



  TempData: TBytes;



  BytesRemained: Integer;



  FrameProcessed: Boolean;



  begin



    while True do



    begin



      FrameProcessed := False;







      if FRelayBuffer.Size < SizeOf(TScreenHeader) then



        Break;







      FRelayBuffer.Position := 0;



      FRelayBuffer.Read(Header, SizeOf(TScreenHeader));







      if (Header.DataSize = 0) or (Header.DataSize > 50 * 1024 * 1024) then



      begin



        SafeLog('Critical: Bad header size (' + IntToStr(Header.DataSize) + '). Resetting buffer.');



        FRelayBuffer.Clear;



        Break;



      end;







      TotalFrameSize := SizeOf(TScreenHeader) + Header.DataSize;







      if FRelayBuffer.Size < TotalFrameSize then



        Break;







      try



        if Header.DataSize > 0 then



        begin



          SetLength(TempData, Header.DataSize);



          FRelayBuffer.Position := SizeOf(TScreenHeader);



          FRelayBuffer.Read(TempData[0], Header.DataSize);







          if Header.ImageFormat = 1 then



            ProcessJPEGFrame(@TempData[0], Header.DataSize);







          FrameProcessed := True;



        end;



      except



        on E: Exception do



          SafeLog('Error processing frame: ' + E.Message);



      end;







      BytesRemained := FRelayBuffer.Size - TotalFrameSize;







      if BytesRemained > 0 then



      begin



        Move(PByte(FRelayBuffer.Memory)[TotalFrameSize],



             PByte(FRelayBuffer.Memory)[0],



             BytesRemained);



        FRelayBuffer.SetSize(BytesRemained);



      end



      else



      begin



        FRelayBuffer.Clear;



      end;







      FRelayBuffer.Position := 0;







    end;







    FRelayBuffer.Position := FRelayBuffer.Size;



  end;







procedure TForm1.UpdateBitmapRegion(DataOffset: Integer; DataSize: Integer);



var



  i: Integer;



  CurrentLineBytes: PByte;



  SrcPtr: PByte;



  BytesPerPixel: Integer;



  PixelsToCopy: Integer;



begin



  if not Assigned(FScreenBitmap) then Exit;







  BytesPerPixel := 3;



  if FCurrentHeader.DataSize > 0 then



    BytesPerPixel := FCurrentHeader.DataSize div (FCurrentHeader.Width * FCurrentHeader.Height);







  FScreenBitmap.BeginUpdate;



  try



    SrcPtr := FReceiveBuffer + DataOffset;







    for i := 0 to FScreenHeight - 1 do



    begin



      CurrentLineBytes := FScreenBitmap.ScanLine[i];



      PixelsToCopy := FScreenWidth * BytesPerPixel;







      if DataSize >= PixelsToCopy then



      begin



        Move(SrcPtr^, CurrentLineBytes^, PixelsToCopy);



        Inc(SrcPtr, PixelsToCopy);



        Dec(DataSize, PixelsToCopy);



      end



      else if DataSize > 0 then



      begin



        Move(SrcPtr^, CurrentLineBytes^, DataSize);



        FillChar((CurrentLineBytes + DataSize)^, PixelsToCopy - DataSize, 0);



        SrcPtr := SrcPtr + DataSize;



        DataSize := 0;



      end



      else



      begin



        Break;



      end;



    end;







  finally



    FScreenBitmap.EndUpdate;



  end;



end;







procedure TForm1.RequestUpdate;



var



  Msg: Byte;



  CurrentTime: DWORD;



  TimeDiff: DWORD;



const



  MIN_FRAME_DELAY = 50; // Если к примеру изменить с 30 до 100 мс (клиент будет отображать 10 FPS вместо 33)



begin



  try



    if not FConnected then Exit;







    CurrentTime := GetTickCount;



    if CurrentTime >= FLastUpdateTime then



      TimeDiff := CurrentTime - FLastUpdateTime



    else



      TimeDiff := MAXDWORD - FLastUpdateTime + CurrentTime;







    if TimeDiff < MIN_FRAME_DELAY then



    begin



      // SafeLog('Frame request throttled'); // для отладки



      Exit;



    end;







    FLastUpdateTime := CurrentTime;







    Msg := 1;



    if FThroughRelay then



    begin



      SendBinaryToRelay([Msg]);



    end



    else



    begin



      if send(FSocket, Msg, SizeOf(Msg), 0) = SOCKET_ERROR then



        DisconnectFromServer;



    end;



  except



    on E: Exception do



      SafeLog('Error in RequestUpdate: ' + E.Message);



  end;



end;







procedure TForm1.SendMouseEvent(X, Y: Integer; Buttons: Byte);



var



  Msg: TMouseMsg;



  Data: TBytes;



begin



  try



    if not FConnected then Exit;







    if (X < 0) or (X >= FScreenWidth) or (Y < 0) or (Y >= FScreenHeight) then



    begin



      SafeLog(Format('Попытка отправить недействительные координаты мыши: X=%d, Y=%d', [X, Y]));



      Exit;



    end;







    Msg.MsgType := 2;



    Msg.X := X;



    Msg.Y := Y;



    Msg.Buttons := Buttons;







    SetLength(Data, SizeOf(Msg));



    Move(Msg, Data[0], SizeOf(Msg));







    if FThroughRelay then



    begin



      SendBinaryToRelay(Data);



    end



    else



    begin



      if send(FSocket, Msg, SizeOf(Msg), 0) = SOCKET_ERROR then



      begin



        SafeLog('Ошибка отправки mouse event: ' + IntToStr(WSAGetLastError));



      end



      else



      begin



        SafeLog(Format('Mouse: X=%d, Y=%d, Buttons=%d', [X, Y, Buttons]));



      end;



    end;



  except



    on E: Exception do



      SafeLog('Ошибка в SendMouseEvent: ' + E.Message);



  end;



end;







procedure TForm1.SendKeyEvent(KeyCode: DWORD; IsDown: Boolean);



var



  Msg: TKeyMsg;



  Data: TBytes;



begin



  try



    if not FConnected then Exit;







    Msg.MsgType := 3;



    if IsDown then



      Msg.IsDown := 1



    else



      Msg.IsDown := 0;



    Msg.KeyCode := KeyCode;







    SetLength(Data, SizeOf(Msg));



    Move(Msg, Data[0], SizeOf(Msg));







    if FThroughRelay then



    begin



      SendBinaryToRelay(Data);



    end



    else



    begin



      if send(FSocket, Msg, SizeOf(Msg), 0) = SOCKET_ERROR then



      begin



        SafeLog('Ошибка отправки key event: ' + IntToStr(WSAGetLastError));



      end



      else



      begin



        SafeLog(Format('Key: Code=%d, Down=%s', [KeyCode, BoolToStr(IsDown, True)]));



      end;



    end;



  except



    on E: Exception do



      SafeLog('Ошибка в SendKeyEvent: ' + E.Message);



  end;



end;







procedure TForm1.UpdateDisplay;



var



  LastUpdateTime: Cardinal;



  CurrentTime: Cardinal;



const



  MIN_UPDATE_INTERVAL = 33;



begin



  try



    CurrentTime := GetTickCount;



    LastUpdateTime := 0;







    if CurrentTime - LastUpdateTime >= MIN_UPDATE_INTERVAL then



    begin



      PaintBox1.Invalidate;



      LastUpdateTime := CurrentTime;







      if FFullScreenMode and Assigned(FFullScreenForm) then



      begin



        FFullScreenForm.Invalidate;



      end;



    end;



  except



    on E: Exception do



      SafeLog('Ошибка в UpdateDisplay: ' + E.Message);



  end;



end;







procedure TForm1.SetStatus(const Msg: string);



begin



  try



    if Assigned(Label3) then



      Label3.Caption := Msg;



    Application.ProcessMessages;



  except



    on E: Exception do



      SafeLog('Ошибка в SetStatus: ' + E.Message);



  end;



end;







procedure TForm1.UpdateConnectionUI;



begin



  Button1.Enabled := not FConnected;



  Button2.Enabled := FConnected;



  Button3.Enabled := FConnected;







  if FConnectionMode = cmDirect then



  begin



    Edit1.Enabled := not FConnected;



    Edit2.Enabled := not FConnected;



    Edit3.Enabled := False;



  end



  else



  begin



    Edit1.Enabled := False;



    Edit2.Enabled := False;



    Edit3.Enabled := not FConnected;



  end;







  RadioGroup1.Enabled := not FConnected;







  case FRelayState of



    rsDisconnected:



    begin



      Label7.Caption := 'Disconnected';



      Label7.Font.Color := clRed;



    end;



    rsConnected:



    begin



      Label7.Caption := 'Connected to relay';



      Label7.Font.Color := clGreen;



    end;



    rsWaiting:



    begin



      Label7.Caption := 'Waiting for server...';



      Label7.Font.Color := clBlue;



    end;



    rsReady:



    begin



      Label7.Caption := 'Connected to server';



      Label7.Font.Color := clGreen;



    end;



  end;



end;







procedure TForm1.SendToRelay(const Data: string);



var



  FullData: AnsiString;



begin



  if FRelaySocket <> INVALID_SOCKET then



  begin



    FullData := AnsiString(Data) + #13#10;



    send(FRelaySocket, FullData[1], Length(FullData), 0);



    SafeLog('>> Relay: ' + Data);



  end;



end;







procedure TForm1.SendBinaryToRelay(const Data: TBytes);



begin



  if FRelaySocket <> INVALID_SOCKET then



  begin



    send(FRelaySocket, Data[0], Length(Data), 0);



    SafeLog('Binary data sent to relay: ' + IntToStr(Length(Data)) + ' bytes');



  end;



end;







procedure TForm1.ParseRelayAddress;



begin



  // Дефолтный адрес релей сервера для подключения



  FRelayIP := '10.30.28.28';



  FRelayPort := RELAY_PORT;



end;







procedure TForm1.PaintBox1Paint(Sender: TObject);



var



  PaintBox: TPaintBox;



  DestRect: TRect;



  ScaleX, ScaleY, Scale: Double;



begin



  try



    PaintBox := Sender as TPaintBox;







    if Assigned(FScreenBitmap) and (FScreenWidth > 0) and (FScreenHeight > 0) then



    begin



      if FFullScreenMode then



      begin



        ScaleX := PaintBox.Width / FScreenWidth;



        ScaleY := PaintBox.Height / FScreenHeight;



        Scale := MinDouble(ScaleX, ScaleY);







        DestRect.Left := (PaintBox.Width - Round(FScreenWidth * Scale)) div 2;



        DestRect.Top := (PaintBox.Height - Round(FScreenHeight * Scale)) div 2;



        DestRect.Right := DestRect.Left + Round(FScreenWidth * Scale);



        DestRect.Bottom := DestRect.Top + Round(FScreenHeight * Scale);







        PaintBox.Canvas.StretchDraw(DestRect, FScreenBitmap);



      end



      else



      begin



        PaintBox.Canvas.Draw(0, 0, FScreenBitmap);



      end;



    end



    else



    begin



      // Рисуем черный фон, если битмап не готов или имеет нулевые размеры



      PaintBox.Canvas.Brush.Color := clBlack;



      PaintBox.Canvas.FillRect(PaintBox.ClientRect);



    end;



  except



    on E: Exception do



      SafeLog('Ошибка в PaintBox1Paint: ' + E.Message);



  end;



end;







procedure TForm1.WMSocket(var Message: TLMessage);



var



  Event: Word;



  Error: Integer;



  Socket: TSocket;



begin



  Event := LOWORD(Message.LParam);



  Error := HIWORD(Message.LParam);



  Socket := Message.WParam;







  try



    SafeLog(Format('WMSocket вызван. Событие: %d, Ошибка: %d, Socket: %d',



                   [Event, Error, Socket]));







    if Socket = FRelaySocket then



    begin



      case Event of



        FD_READ:



          if Error = 0 then



            ProcessRelayData



          else



            DisconnectFromServer;







        FD_CLOSE:



          begin



            SafeLog('Relay connection closed');



            DisconnectFromServer;



          end;



      end;



    end



    else if Socket = FSocket then



    begin



      case Event of



        FD_READ:



          if Error = 0 then



            ProcessServerData



          else



            DisconnectFromServer;







        FD_CLOSE:



          begin



            SafeLog('Direct connection closed');



            DisconnectFromServer;



          end;



      end;



    end;







  except



    on E: Exception do



    begin



      SafeLog('КРИТИЧЕСКАЯ ОШИБКА в WMSocket: ' + E.Message);



      DisconnectFromServer;



    end;



  end;



end;







end.
 
Похожие темы
Admin Статья VNC через Relay (Работа за NAT) или пробуем повторить AnyDesk на Паскале.[3] Анонимность и приватность 0
Admin Статья VNC через Relay (Работа за NAT) или пробуем повторить AnyDesk на Паскале.[2] Анонимность и приватность 0
V VNC продам/Sell VNC Доступы: RDP, VPS, SQL inj, базы, сайты, shell's 1
H Free VNC Раздачи и сливы 0
R RDP4YOU: Все о дедиках для тебя | RDP, SSH, VNC Доступы: RDP, VPS, SQL inj, базы, сайты, shell's 0
M VNC USA Доступы: RDP, VPS, SQL inj, базы, сайты, shell's 0
A Дедики под любые нужды RDP VNC туннели ssh Доступы: RDP, VPS, SQL inj, базы, сайты, shell's 1
S Взламываем RDP,SSH,VNC при помощи Ncrack Уязвимости и взлом 0
Admin Захват VNC сессии на удаленном компьютере. Уязвимости и взлом 1
Admin Интересно Reverse shell через ICMP. Опубликован руткит под лицензией MIT, который обходит все защиты Linux. Новости в сети 0
Admin Статья Управляй компьютером через сотовый! Полезные статьи 0
Admin Статья Поднимаем личный VPN-сервер за 30 мин на своем VPS через Amnezia VPN Анонимность и приватность 0
Admin Статья Поднимаем свой XMPP сервер через i2p в 2025. Полезные статьи 0
Admin Статья Генерируем варианты утерянного пароля через HashCat Полезные статьи 0
Admin Статья Находим бекенд айпи сайта за CDN через дедупликацию OSINT 0
Admin Статья Разбираем инциденты, анализируем honeypots через дашборды для поимки хищников, атакующих нашу инфраструктуру. [Part 2] Анонимность и приватность 0
Admin Статья Деанонимизация через «забытые» утечки OSINT 0
Admin Интересно ИИ стал предателем: LangChain взламывают через ответы самой модели — она сама крадёт ваши секреты. Новости в сети 0
Admin Статья SQL-инъекции: Анатомия атаки и искусство защиты через Prepared Statements Уязвимости и взлом 0
Admin Статья Безопасное использование джаббера через .onion Анонимность и приватность 0
Admin Интересно Через кряки и YouTube распространяют новые загрузчики вредоносов. Новости в сети 0
Admin Интересно Киберпанк в Средиземном море. Французы ищут, какая держава хотела угнать паром через интернет. Новости в сети 0
Admin Интересно А что, так можно было? В системных утилитах FreeBSD нашли уязвимость, позволяющую исполнять чужой код через обычный роутер. Новости в сети 0
Support81 45 тыс фунтов за одно покушение — беглый топ-менеджер Wirecard финансировал шпионов и киллеров через крипту Новости в сети 0
Support81 «Менеджер» с архивом и черным ходом через Yandex. Как группировка APT31 годами шпионила за российскими IT-компаниями Новости в сети 1
Support81 Хакеры захватили 8,7 млн WordPress-сайтов за два дня — атакуют через критические уязвимости в популярных плагинах Новости в сети 0
Support81 Анонимности — конец: депутат Госдумы пообещал, что через три года все действия в российском интернете будут деанонимизированы Новости в сети 3
turbion0 Мошенники похищают аккаунты на «Госуслугах» через объявления Новости в сети 0
Support81 RCE через Game Pass: тысячи ПК взломаны через Call of Duty Новости в сети 0
Support81 Microsoft снова сыграла на руку хакерам — корпоративные сети ломают через официальный софт Новости в сети 0
Support81 SMS для Google и Meta? Перешлём через Намибию, Чечню и швейцарский гараж Новости в сети 0
Support81 JSFireTruck и HelloTDS: новая инфраструктура веб-атак через легитимные домены Новости в сети 0
Support81 Забудьте о вирусах в письмах — теперь ими заражаются через вертикальные видео в TikTok Новости в сети 0
Support81 Миллиарды наркодолларов идут через WeChat — прокуратура вскрыла схему картелей и китайских банков Новости в сети 0
Support81 Seed → POST-запрос → пустой баланс: как устроена быстрая и чистая кража крипты через FreeDrain Новости в сети 0
turbion0 Мошенники выдают фальшивые акции банков в Telegram и воруют деньги через фишинг Новости в сети 0
Support81 Одно касание — и прощайте, деньги: китайская SuperCard X ворует данные карт через NFC-модуль Новости в сети 0
turbion0 Мошенники получают доступ к банковским реквизитам через фейковые сайты. Новости в сети 0
Support81 Хакеры наносят удар через Google Ads: юристы — главная цель Новости в сети 0
Support81 StilachiRAT: хищная киберкрыса похищает биткоины и подглядывает за вами через веб-камеру Новости в сети 0
Support81 Яд в коде: злоумышленники заставляют ИИ внедрять бэкдоры через Unicode-символы Новости в сети 0
Support81 «Мир не готов»: эксперты предсказывают появление сверхразума через год Новости в сети 0
Support81 Вирус в заявке: как Zhong Stealer «ломает» компании через службу поддержки Новости в сети 0
Support81 Фейковая «Безопасность»: Telegram-аккаунты снова крадут через сообщения Новости в сети 0
Support81 WantToCry: новое поколение вымогателей атакует через незащищённый SMB Новости в сети 0
wrangler65 Интересно Как сэкономить до 70% на переводах USDT TRC-20 через энергию Полезные статьи 0
Support81 Слежка через смартфоны: новый скандал с участием спецслужб Новости в сети 0
Support81 Обновление Windows спровоцировало волну атак через архивы Новости в сети 0
Support81 Аналитики IBM: хакеры записывают домашние видео через взломанные маршрутизаторы Новости в сети 0
Support81 Разоблачена тайная программа слежки через мобильные приложения Новости в сети 0

Название темы