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

Admin

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

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


Исходник серверной части:

Код:
unit Unit1;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, Forms, Controls, Graphics, Dialogs, StdCtrls, ExtCtrls,
  Windows, WinSock2, LMessages, StrUtils, ActiveX, gdiplus;

const
  EncoderQuality: TGUID = '{1D5BE4B5-FA4A-452D-9CDD-5DB35105E7EB}';
  EncoderParameterValueTypeLong = 4;

type
  TEncoderParameter = packed record
    Guid: TGUID;
    NumberOfValues: ULONG;
    Type_: ULONG;
    Value: Pointer;
  end;

  TEncoderParameters = packed record
    Count: UINT;
    Parameter: array[0..0] of TEncoderParameter;
  end;
  PEncoderParameters = ^TEncoderParameters;

const
  VNC_PORT = 5900;
  RELAY_PORT = 5500;
  MAX_CLIENTS = 5;
  WM_SOCKET = WM_USER + 1;
  ID_LENGTH = 9;

  // GDI+ статусы
  Ok = 0;
  GenericError = 1;
  InvalidParameter = 2;

// GDI+ типы и функции
type
  TGPStatus = Integer;

  GPBITMAP = Pointer;
  GPGRAPHICS = Pointer;
  GPENCODERPARAMETERS = Pointer;

  TGdiplusStartupInput = record
    GdiplusVersion: UINT32;
    DebugEventCallback: Pointer;
    SuppressBackgroundThread: BOOL;
    SuppressExternalCodecs: BOOL;
  end;

// Windows API
function GetSystemMetrics(nIndex: Integer): Integer; stdcall; external 'user32.dll';
function GetDC(hWnd: HWND): HDC; stdcall; external 'user32.dll';
function ReleaseDC(hWnd: HWND; hDC: HDC): Integer; stdcall; external 'user32.dll';
function CreateCompatibleDC(hdc: HDC): HDC; stdcall; external 'gdi32.dll';
function DeleteDC(hdc: HDC): BOOL; stdcall; external 'gdi32.dll';
function CreateDIBSection(hdc: HDC; const pbmi: BITMAPINFO; iUsage: UINT;
  var ppvBits: Pointer; hSection: THandle; dwOffset: DWORD): HBITMAP; stdcall; external 'gdi32.dll';
function SelectObject(hdc: HDC; hgdiobj: HGDIOBJ): HGDIOBJ; stdcall; external 'gdi32.dll';
function BitBlt(DestDC: HDC; X, Y, Width, Height: Integer; SrcDC: HDC;
  XSrc, YSrc: Integer; Rop: DWORD): BOOL; stdcall; external 'gdi32.dll';
function DeleteObject(hObject: HGDIOBJ): BOOL; stdcall; external 'gdi32.dll';
function SendInput(cInputs: UINT; pInputs: Pointer; cbSize: Integer): UINT; stdcall; external 'user32.dll';
function SetCursorPos(X, Y: Integer): BOOL; stdcall; external 'user32.dll';

// GDI+ функции
function GdiplusStartup(out token: ULONG; input: Pointer; output: Pointer): TGPStatus; stdcall; external 'gdiplus.dll';
procedure GdiplusShutdown(token: ULONG); stdcall; external 'gdiplus.dll';
function GdipCreateBitmapFromHBITMAP(hbm: HBITMAP; hpal: HPALETTE; out bitmap: GPBITMAP): TGPStatus; stdcall; external 'gdiplus.dll';
function GdipSaveImageToStream(image: GPBITMAP; stream: IStream; const clsidEncoder: TGUID;
  encoderParams: GPENCODERPARAMETERS): TGPStatus; stdcall; external 'gdiplus.dll';
function GdipDisposeImage(image: GPBITMAP): TGPStatus; stdcall; external 'gdiplus.dll';
function GdipGetImageEncodersSize(out numEncoders: UINT; out size: UINT): TGPStatus; stdcall; external 'gdiplus.dll';
function GdipGetImageEncoders(numEncoders: UINT; size: UINT; encoders: Pointer): TGPStatus; stdcall; external 'gdiplus.dll';

type
  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;

  TInputRec = record
    InputType: DWORD;
    case Integer of
      0: (mi: MOUSEINPUT);
      1: (ki: KEYBDINPUT);
  end;

  { TForm1 }

  TForm1 = class(TForm)
    Button1: TButton;
    Button2: TButton;
    Edit1: TEdit;
    Label1: TLabel;
    Label2: TLabel;
    Memo1: TMemo;
    Timer1: TTimer;
    CheckBox1: TCheckBox;
    Edit2: TEdit;
    Button3: TButton;
    Label3: TLabel;
    Edit3: TEdit;
    Label4: TLabel;
    Label5: TLabel;
    Button4: TButton;
    Label7: TLabel;
    ComboBox1: TComboBox;
    Label8: TLabel;

    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
    procedure Timer1Timer(Sender: TObject);
    procedure CheckBox1Click(Sender: TObject);
    procedure Button3Click(Sender: TObject);
    procedure Button4Click(Sender: TObject);
    procedure ComboBox1Change(Sender: TObject);

  private
    FServerSocket: TSocket;
    FClients: array[0..MAX_CLIENTS-1] of TSocket;
    FClientCount: Integer;
    FRunning: Boolean;
    FWSAData: TWSADATA;
    FLastFrameSentTime: DWORD;


    // Screen capture
    FScreenDC: HDC;
    FMemoryDC: HDC;
    FBitmap: HBITMAP;
    FBitmapInfo: BITMAPINFO;
    FPixelData: Pointer;
    FScreenWidth: Integer;
    FScreenHeight: Integer;
    FScreenInitialized: Boolean;

    // JPEG encoding
    FGDIPlusToken: ULONG;
    FJPEGQuality: Integer;
    FJPEGEncoderClsid: TGUID;
    FGDIPlusInitialized: Boolean;

    // Relay support
    FRelaySocket: TSocket;
    FUseRelay: Boolean;
    FMyID: string;
    FRelayBuffer: TMemoryStream;
    FRelayIP: string;
    FRelayPort: Integer;
    FIsRegistered: Boolean;
    FRelayClientConnected: Boolean;

    // Private procedures
    procedure StartServer;
    procedure StopServer;
    procedure AcceptClient;
    procedure ProcessClient(Index: Integer);
    procedure DisconnectClient(Index: Integer);
    procedure InitScreen;
    procedure CleanupScreen;
    procedure CaptureScreen;
    procedure SendScreenToAll;
    procedure SendJPEGToClient(ClientSocket: TSocket);
    procedure HandleMouse(const Msg: TMouseMsg);
    procedure HandleKey(const Msg: TKeyMsg);
    procedure AddLog(const Msg: string);
    procedure FormShow(Sender: TObject);

    // JPEG encoding
    function GetEncoderClsid(const mimeType: WideString; out clsid: TGUID): Boolean;
    function EncodeScreenToJPEG(var JPEGData: TBytes): Boolean;
    procedure InitGDIplus;

    // Relay procedures
    procedure StartRelayRegistration;
    procedure StopRelayRegistration;
    procedure ConnectToRelay;
    procedure DisconnectFromRelay;
    procedure SendToRelay(const Data: string);
    procedure SendBinaryToRelay(const Data: TBytes);
    procedure ProcessRelayData;
    procedure RegisterWithRelay;
    function GenerateID: string;

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

  public

  end;

var
  Form1: TForm1;

implementation

{$R *.lfm}

function RandomString(Length: Integer): string;
const
  Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
var
  i, CharCount: Integer;
begin
  Result := '';
  CharCount := System.Length(Chars);
  Randomize;
  for i := 1 to Length do
    Result := Result + Chars[Random(CharCount) + 1];
end;

{ TForm1 }
procedure TForm1.FormCreate(Sender: TObject);
var
  i: Integer;
begin
  try
    // Winsock - ПЕРВЫМ
    if WSAStartup(MAKEWORD(2, 2), FWSAData) <> 0 then
    begin
      Application.Terminate;
      Exit;
    end;

    // Инициализация переменных
    FServerSocket := INVALID_SOCKET;
    for i := 0 to MAX_CLIENTS-1 do
      FClients[i] := INVALID_SOCKET;
    FClientCount := 0;
    FRunning := False;
    FLastFrameSentTime := 0;

    FScreenDC := 0;
    FMemoryDC := 0;
    FBitmap := 0;
    FPixelData := nil;
    FScreenWidth := 0;
    FScreenHeight := 0;
    FScreenInitialized := False;

    FGDIPlusInitialized := False;
    FGDIPlusToken := 0;
    FJPEGQuality := 85;

    FUseRelay := False;
    FRelaySocket := INVALID_SOCKET;
    FMyID := '';
    FRelayBuffer := TMemoryStream.Create;
    FRelayIP := '10.30.28.28';    // Меняем на свой айпишник на котором висит релей
    FRelayPort := RELAY_PORT;
    FIsRegistered := False;
    FRelayClientConnected := False;

    Timer1.Interval := 100;
    Timer1.Enabled := False;

    InitGDIplus;

  except
    on E: Exception do
    begin
      Application.MessageBox(PChar('Initialization error: ' + E.Message), 'Error', MB_OK);
      Application.Terminate;
    end;
  end;
end;

procedure TForm1.FormDestroy(Sender: TObject);
begin
  Timer1.Enabled := False;

  StopServer;

  CleanupScreen;

  DisconnectFromRelay;

  if Assigned(FRelayBuffer) then
    FreeAndNil(FRelayBuffer);

  if FGDIPlusInitialized and (FGDIPlusToken <> 0) then
  begin
    GdiplusShutdown(FGDIPlusToken);
    FGDIPlusInitialized := False;
    FGDIPlusToken := 0;
  end;

  // Winsock cleanup ПОСЛЕДНИМ
  WSACleanup;
end;

procedure TForm1.FormShow(Sender: TObject);
begin
  if Assigned(Memo1) then
    AddLog('Server ready (JPEG only)');
end;

procedure TForm1.Button1Click(Sender: TObject);
begin
  StartServer;
end;

procedure TForm1.Button2Click(Sender: TObject);
begin
  StopServer;
end;

procedure TForm1.Timer1Timer(Sender: TObject);
begin
  if FRunning and (FClientCount > 0) and FScreenInitialized then
    SendScreenToAll;
end;

procedure TForm1.CheckBox1Click(Sender: TObject);
begin
  FUseRelay := CheckBox1.Checked;
  Edit2.Enabled := FUseRelay;
  Button3.Enabled := FUseRelay;
  Button4.Enabled := FUseRelay;

  if FUseRelay then
    StartRelayRegistration
  else
    StopRelayRegistration;
end;

procedure TForm1.Button3Click(Sender: TObject);
begin
  FMyID := GenerateID;
  Edit3.Text := FMyID;
  AddLog('Generated new ID: ' + FMyID);
end;

procedure TForm1.Button4Click(Sender: TObject);
begin
  if FMyID = '' then
  begin
    ShowMessage('Please generate an ID first');
    Exit;
  end;

  if FRelaySocket = INVALID_SOCKET then
    ConnectToRelay
  else
    RegisterWithRelay;
end;

procedure TForm1.ComboBox1Change(Sender: TObject);
begin
  // Защита от вызова при инициализации
  if not Assigned(Self) then Exit;
  if not Assigned(ComboBox1) then Exit;
  if csLoading in ComponentState then Exit; // КРИТИЧНО!

  case ComboBox1.ItemIndex of
    0: FJPEGQuality := 50;
    1: FJPEGQuality := 75;
    2: FJPEGQuality := 85;
    3: FJPEGQuality := 95;
  else
    FJPEGQuality := 85;
  end;

  if Assigned(Memo1) then
    AddLog('JPEG quality set to ' + IntToStr(FJPEGQuality) + '%');
end;

function TForm1.GenerateID: string;
begin
  Result := RandomString(8);
end;

procedure TForm1.StartServer;
var
  ServerAddr: TSockAddrIn;
  Port: Integer;
begin
  if FRunning then Exit;

  Port := StrToIntDef(Edit1.Text, VNC_PORT);

  try
    InitScreen;

    FServerSocket := socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if FServerSocket = INVALID_SOCKET then
      raise Exception.Create('Socket creation failed');

    FillChar(ServerAddr, SizeOf(ServerAddr), 0);
    ServerAddr.sin_family := AF_INET;
    ServerAddr.sin_addr.s_addr := INADDR_ANY;
    ServerAddr.sin_port := htons(Port);

    if bind(FServerSocket, @ServerAddr, SizeOf(ServerAddr)) = SOCKET_ERROR then
      raise Exception.Create('Bind failed');

    if listen(FServerSocket, SOMAXCONN) = SOCKET_ERROR then
      raise Exception.Create('Listen failed');

    WSAAsyncSelect(FServerSocket, Handle, WM_SOCKET, FD_ACCEPT);

    FRunning := True;
    Button1.Enabled := False;
    Button2.Enabled := True;
    Timer1.Enabled := True;

    Label1.Caption := 'Running on port ' + IntToStr(Port);
    AddLog('Server started on port ' + IntToStr(Port));

    // Если используем relay, регистрируемся
    if FUseRelay and (FMyID <> '') and (FRelaySocket <> INVALID_SOCKET) then
      RegisterWithRelay;

  except
    on E: Exception do
    begin
      AddLog('Start error: ' + E.Message);
      ShowMessage('Start error: ' + E.Message);
      CleanupScreen;
    end;
  end;
end;

procedure TForm1.StopServer;
var
  i: Integer;
begin
  if not FRunning then Exit;

  FRunning := False;
  Timer1.Enabled := False;

  for i := 0 to MAX_CLIENTS-1 do
    if FClients[i] <> INVALID_SOCKET then
      DisconnectClient(i);

  if FServerSocket <> INVALID_SOCKET then
  begin
    closesocket(FServerSocket);
    FServerSocket := INVALID_SOCKET;
  end;

  CleanupScreen;

  Button1.Enabled := True;
  Button2.Enabled := False;
  Label1.Caption := 'Stopped';
  Label2.Caption := 'Clients: 0';
  AddLog('Server stopped');
end;

procedure TForm1.AcceptClient;
var
  ClientSocket: TSocket;
  i: Integer;
  Greeting: AnsiString;
begin
  ClientSocket := accept(FServerSocket, nil, nil);
  if ClientSocket = INVALID_SOCKET then Exit;

  for i := 0 to MAX_CLIENTS-1 do
  begin
    if FClients[i] = INVALID_SOCKET then
    begin
      FClients[i] := ClientSocket;
      WSAAsyncSelect(ClientSocket, Handle, WM_SOCKET, FD_READ or FD_CLOSE);

      Greeting := 'VNC JPEG Ready'#13#10;
      send(ClientSocket, Greeting[1], Length(Greeting), 0);

      Inc(FClientCount);
      Label2.Caption := 'Clients: ' + IntToStr(FClientCount);
      AddLog('Client connected directly');

      Exit;
    end;
  end;

  closesocket(ClientSocket);
  AddLog('Client rejected - no slots');
end;

procedure TForm1.ProcessClient(Index: Integer);
var
  Buffer: array[0..255] of Byte;
  BytesReceived: Integer;
  MouseMsg: TMouseMsg;
  KeyMsg: TKeyMsg;
begin
  if FClients[Index] = INVALID_SOCKET then Exit;

  BytesReceived := recv(FClients[Index], Buffer, SizeOf(Buffer), 0);
  if BytesReceived <= 0 then
  begin
    DisconnectClient(Index);
    Exit;
  end;

  case Buffer[0] of
    1: // Update request
      begin
        AddLog(Format('Client #%d requests update', [Index]));
        SendJPEGToClient(FClients[Index]);
      end;
    2: // Mouse event
      begin
        if BytesReceived >= SizeOf(TMouseMsg) then
        begin
          Move(Buffer, MouseMsg, SizeOf(TMouseMsg));
          HandleMouse(MouseMsg);
        end;
      end;
    3: // Key event
      begin
        if BytesReceived >= SizeOf(TKeyMsg) then
        begin
          Move(Buffer, KeyMsg, SizeOf(TKeyMsg));
          HandleKey(KeyMsg);
        end;
      end;
  end;
end;

procedure TForm1.DisconnectClient(Index: Integer);
begin
  if FClients[Index] = INVALID_SOCKET then Exit;

  closesocket(FClients[Index]);
  FClients[Index] := INVALID_SOCKET;
  Dec(FClientCount);
  Label2.Caption := 'Clients: ' + IntToStr(FClientCount);
  AddLog('Client disconnected');
end;

procedure TForm1.InitScreen;
begin
  FScreenWidth := GetSystemMetrics(0); // SM_CXSCREEN
  FScreenHeight := GetSystemMetrics(1); // SM_CYSCREEN

  if (FScreenWidth <= 0) or (FScreenHeight <= 0) then
  begin
    FScreenWidth := 1024;
    FScreenHeight := 768;
  end;

  FScreenDC := GetDC(0);
  if FScreenDC = 0 then
  begin
    AddLog('Failed to get screen DC');
    Exit;
  end;

  FMemoryDC := CreateCompatibleDC(FScreenDC);
  if FMemoryDC = 0 then
  begin
    AddLog('Failed to create compatible DC');
    ReleaseDC(0, FScreenDC);
    FScreenDC := 0;
    Exit;
  end;

  FillChar(FBitmapInfo, SizeOf(FBitmapInfo), 0);
  FBitmapInfo.bmiHeader.biSize := SizeOf(BITMAPINFOHEADER);
  FBitmapInfo.bmiHeader.biWidth := FScreenWidth;
  FBitmapInfo.bmiHeader.biHeight := -FScreenHeight;
  FBitmapInfo.bmiHeader.biPlanes := 1;
  FBitmapInfo.bmiHeader.biBitCount := 24;
  FBitmapInfo.bmiHeader.biCompression := 0; // BI_RGB

  FBitmap := CreateDIBSection(FMemoryDC, FBitmapInfo, 0, FPixelData, 0, 0);
  if (FBitmap = 0) or (FPixelData = nil) then
  begin
    AddLog('Failed to create DIB section');
    DeleteDC(FMemoryDC);
    ReleaseDC(0, FScreenDC);
    FMemoryDC := 0;
    FScreenDC := 0;
    Exit;
  end;

  SelectObject(FMemoryDC, FBitmap);
  FScreenInitialized := True;

  AddLog(Format('Screen init: %dx%d', [FScreenWidth, FScreenHeight]));
end;

procedure TForm1.CleanupScreen;
begin
  if FBitmap <> 0 then
  begin
    DeleteObject(FBitmap);
    FBitmap := 0;
  end;
  if FMemoryDC <> 0 then
  begin
    DeleteDC(FMemoryDC);
    FMemoryDC := 0;
  end;
  if FScreenDC <> 0 then
  begin
    ReleaseDC(0, FScreenDC);
    FScreenDC := 0;
  end;
  FPixelData := nil;
  FScreenInitialized := False;
end;

procedure TForm1.CaptureScreen;
begin
  if not FScreenInitialized then Exit;
  BitBlt(FMemoryDC, 0, 0, FScreenWidth, FScreenHeight, FScreenDC, 0, 0, SRCCOPY);
end;

function TForm1.GetEncoderClsid(const mimeType: WideString; out clsid: TGUID): Boolean;
type
  TImageCodecInfo = record
    Clsid: TGUID;
    FormatID: TGUID;
    CodecName: PWideChar;
    DllName: PWideChar;
    FormatDescription: PWideChar;
    FilenameExtension: PWideChar;
    MimeType: PWideChar;
    Flags: DWORD;
    Version: DWORD;
    SigCount: DWORD;
    SigSize: DWORD;
    SigPattern: PByte;
    SigMask: PByte;
  end;
  PImageCodecInfo = ^TImageCodecInfo;
var
  numEncoders, size: UINT;
  encoders: PImageCodecInfo;
  i: Integer;
begin
  Result := False;

  if not FGDIPlusInitialized then
    Exit;

  if GdipGetImageEncodersSize(numEncoders, size) <> Ok then
    Exit;

  if size = 0 then
    Exit;

  GetMem(encoders, size);
  try
    if GdipGetImageEncoders(numEncoders, size, encoders) <> Ok then
      Exit;

    for i := 0 to numEncoders - 1 do
    begin
      if WideString(encoders[i].MimeType) = mimeType then
      begin
        clsid := encoders[i].Clsid;
        Result := True;
        Exit;
      end;
    end;
  finally
    FreeMem(encoders);
  end;
end;

function TForm1.EncodeScreenToJPEG(var JPEGData: TBytes): Boolean;
var
  Bitmap: GPBITMAP;
  Stream: IStream;
  Status: TGPStatus;
  hMem: HGLOBAL;
  DataSize: SIZE_T;
  pMem: Pointer;

  EncoderParameters: TEncoderParameters;
  Quality: ULONG;
const
  EncoderQuality: TGUID = '{1D5BE4B5-FA4A-452D-9CDD-5DB35105E7EB}';
begin
  Result := False;
  Stream := nil;
  Bitmap := nil;

  if not (FGDIPlusInitialized and FScreenInitialized) then Exit;

  CaptureScreen;

  // HBITMAP -> GDI+ Bitmap
  Status := GdipCreateBitmapFromHBITMAP(FBitmap, 0, Bitmap);
  if Status <> Ok then Exit;

  try
    hMem := GlobalAlloc(GMEM_MOVEABLE, 0);
    if CreateStreamOnHGlobal(hMem, True, Stream) <> S_OK then
    begin
      GlobalFree(hMem);
      Exit;
    end;

    // Настройка качества
    Quality := FJPEGQuality;
    EncoderParameters.Count := 1;
    EncoderParameters.Parameter[0].Guid := EncoderQuality;
    EncoderParameters.Parameter[0].Type_ := 4; // ValueTypeLong
    EncoderParameters.Parameter[0].NumberOfValues := 1;
    EncoderParameters.Parameter[0].Value := @Quality;

    // Кодирование
    Status := GdipSaveImageToStream(Bitmap, Stream, FJPEGEncoderClsid, @EncoderParameters);

    if Status = Ok then
    begin
      DataSize := GlobalSize(hMem);
      if DataSize > 0 then
      begin
        SetLength(JPEGData, DataSize);
        pMem := GlobalLock(hMem);
        Move(pMem^, JPEGData[0], DataSize);
        GlobalUnlock(hMem);
        Result := True;
      end;
    end;

  finally
    GdipDisposeImage(Bitmap);
  end;
end;

procedure TForm1.SendScreenToAll;
var
  JPEGData: TBytes;
  Header: TScreenHeader;
  Data: TBytes;
  i: Integer;
  CurrentTime: DWORD;
  BytesSent: Integer;
const
  MIN_FRAME_INTERVAL = 50; //минимум 50мс между кадрами (20 FPS max)
begin
  if (FClientCount = 0) and (not (FUseRelay and FIsRegistered and FRelayClientConnected)) then Exit;

  CurrentTime := GetTickCount;
  if (CurrentTime - FLastFrameSentTime) < MIN_FRAME_INTERVAL then
  begin
    Exit;
  end;
  FLastFrameSentTime := CurrentTime;

  try
    if not EncodeScreenToJPEG(JPEGData) then
    begin
      AddLog('Failed to encode JPEG');
      Exit;
    end;

    if Length(JPEGData) = 0 then
    begin
      AddLog('JPEG data is empty');
      Exit;
    end;

    Header.Width := FScreenWidth;
    Header.Height := FScreenHeight;
    Header.DataSize := Length(JPEGData);
    Header.ImageFormat := 1;

    SetLength(Data, SizeOf(Header) + Length(JPEGData));
    Move(Header, Data[0], SizeOf(Header));
    Move(JPEGData[0], Data[SizeOf(Header)], Length(JPEGData));

    if FUseRelay and FIsRegistered and (FRelaySocket <> INVALID_SOCKET) and FRelayClientConnected then
    begin
      BytesSent := send(FRelaySocket, Data[0], Length(Data), 0);
      if BytesSent = SOCKET_ERROR then
      begin
        AddLog('Relay send error: ' + IntToStr(WSAGetLastError));
      end
      else if BytesSent < Length(Data) then
      begin
        AddLog('Warning: Partial send to relay: ' + IntToStr(BytesSent) + '/' + IntToStr(Length(Data)));
      end;
    end;

    for i := 0 to MAX_CLIENTS-1 do
    begin
      if FClients[i] <> INVALID_SOCKET then
      begin
        if send(FClients[i], Data[0], Length(Data), 0) = SOCKET_ERROR then
          AddLog('Error sending to client #' + IntToStr(i));
      end;
    end;

  except
    on E: Exception do
      AddLog('Error in SendScreenToAll: ' + E.Message);
  end;
end;

procedure TForm1.SendJPEGToClient(ClientSocket: TSocket);
var
  JPEGData: TBytes;
  Header: TScreenHeader;
  Data: TBytes;
begin
  if not FScreenInitialized then
  begin
    AddLog('Screen not initialized');
    Exit;
  end;

  if not EncodeScreenToJPEG(JPEGData) then
    Exit;

  if Length(JPEGData) = 0 then
    Exit;

  Header.Width := FScreenWidth;
  Header.Height := FScreenHeight;
  Header.DataSize := Length(JPEGData);
  Header.ImageFormat := 1; // JPEG

  SetLength(Data, SizeOf(Header) + Length(JPEGData));
  Move(Header, Data[0], SizeOf(Header));
  Move(JPEGData[0], Data[SizeOf(Header)], Length(JPEGData));

  if send(ClientSocket, Data[0], Length(Data), 0) <> Length(Data) then
    AddLog('Error sending JPEG to client');
end;

procedure TForm1.HandleMouse(const Msg: TMouseMsg);
var
  Input: TInputRec;
begin
  SetCursorPos(Msg.X, Msg.Y);

  FillChar(Input, SizeOf(Input), 0);
  Input.InputType := 0;

  if (Msg.Buttons and 1) <> 0 then
    Input.mi.dwFlags := MOUSEEVENTF_LEFTDOWN
  else if (Msg.Buttons and 2) <> 0 then
    Input.mi.dwFlags := MOUSEEVENTF_RIGHTDOWN
  else if (Msg.Buttons and 4) <> 0 then
    Input.mi.dwFlags := MOUSEEVENTF_MIDDLEDOWN
  else
    Input.mi.dwFlags := MOUSEEVENTF_LEFTUP;

  SendInput(1, @Input, SizeOf(Input));
end;

procedure TForm1.HandleKey(const Msg: TKeyMsg);
var
  Input: TInputRec;
begin
  FillChar(Input, SizeOf(Input), 0);
  Input.InputType := 1;
  Input.ki.wVk := Msg.KeyCode;

  if Msg.IsDown = 0 then
    Input.ki.dwFlags := KEYEVENTF_KEYUP;

  SendInput(1, @Input, SizeOf(Input));
end;

procedure TForm1.AddLog(const Msg: string);
begin
  if not Assigned(Self) then Exit;
  if not Assigned(Memo1) then Exit;

  try
    Memo1.Lines.BeginUpdate;
    try
      Memo1.Lines.Add('[' + TimeToStr(Now) + '] ' + Msg);
      while Memo1.Lines.Count > 100 do
        Memo1.Lines.Delete(0);
    finally
      Memo1.Lines.EndUpdate;
    end;
  except
  end;
end;

procedure TForm1.StartRelayRegistration;
begin
  if FUseRelay then
  begin
    FRelayIP := Copy(Edit2.Text, 1, Pos(':', Edit2.Text) - 1);
    if FRelayIP = '' then FRelayIP := '10.30.28.28';
    FRelayPort := StrToIntDef(Copy(Edit2.Text, Pos(':', Edit2.Text) + 1, Length(Edit2.Text)), RELAY_PORT);

    ConnectToRelay;
  end;
end;

procedure TForm1.StopRelayRegistration;
begin
  DisconnectFromRelay;
end;

procedure TForm1.ConnectToRelay;
var
  RelayAddr: TSockAddrIn;
begin
  if FRelaySocket <> INVALID_SOCKET then Exit;

  try
    FRelaySocket := socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if FRelaySocket = INVALID_SOCKET then
      raise Exception.Create('Failed to create relay socket');

    FillChar(RelayAddr, SizeOf(RelayAddr), 0);
    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
      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);

    AddLog('Connected to relay server: ' + FRelayIP + ':' + IntToStr(FRelayPort));
    Label5.Caption := 'Status: Connected to relay';

  except
    on E: Exception do
    begin
      AddLog('Relay connection error: ' + E.Message);
      Label5.Caption := 'Status: Connection failed';
      FUseRelay := False;
      CheckBox1.Checked := False;
    end;
  end;
end;

procedure TForm1.DisconnectFromRelay;
begin
  if FRelaySocket <> INVALID_SOCKET then
  begin
    if FIsRegistered then
      SendToRelay('UNREGISTER ' + FMyID);

    closesocket(FRelaySocket);
    FRelaySocket := INVALID_SOCKET;
  end;

  FIsRegistered := False;
  FRelayClientConnected := False;
  FMyID := '';
  Edit3.Text := '';
  Label5.Caption := 'Status: Disconnected';
  Label7.Caption := 'Status: Not registered';
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);
    AddLog('>> Relay: ' + Data);
  end;
end;

procedure TForm1.SendBinaryToRelay(const Data: TBytes);
begin
  if FRelaySocket <> INVALID_SOCKET then
  begin
    send(FRelaySocket, Data[0], Length(Data), 0);
  end;
end;

procedure TForm1.ProcessRelayData;
var
  Buffer: array[0..16383] of Byte;
  BytesReceived: Integer;
  DataStr: string;
  Lines: TStringList;
  i: Integer;
  Line: string;
  MouseMsg: TMouseMsg;
  KeyMsg: TKeyMsg;
  RequestCount: Integer;
  MouseCount: Integer;
const
  MAX_REQUESTS_IN_BUFFER = 3;
  MAX_MOUSE_IN_BUFFER = 5;
begin
  if FRelaySocket = INVALID_SOCKET then Exit;

  BytesReceived := recv(FRelaySocket, Buffer, SizeOf(Buffer), 0);
  if BytesReceived <= 0 then
  begin
    AddLog('Relay server disconnected');
    DisconnectFromRelay;
    Exit;
  end;

  if not Assigned(FRelayBuffer) then
    FRelayBuffer := TMemoryStream.Create;

  FRelayBuffer.Write(Buffer, BytesReceived);

  if FRelayClientConnected then
  begin
    FRelayBuffer.Position := 0;
    RequestCount := 0;
    MouseCount := 0;

    while FRelayBuffer.Position < FRelayBuffer.Size do
    begin
      if FRelayBuffer.Size - FRelayBuffer.Position < 1 then Break;

      FRelayBuffer.Read(Buffer[0], 1);
      FRelayBuffer.Position := FRelayBuffer.Position - 1;

      case Buffer[0] of
        1:
          begin
            FRelayBuffer.Position := FRelayBuffer.Position + 1;
            Inc(RequestCount);

            if RequestCount <= MAX_REQUESTS_IN_BUFFER then
            begin
              SendScreenToAll;
            end
            else
            begin
              AddLog('Skipping excess update request');
            end;
          end;
        2:
          begin
            if FRelayBuffer.Size - FRelayBuffer.Position >= SizeOf(TMouseMsg) then
            begin
              FRelayBuffer.Read(MouseMsg, SizeOf(TMouseMsg));
              Inc(MouseCount);

              if MouseCount <= MAX_MOUSE_IN_BUFFER then
              begin
                HandleMouse(MouseMsg);
              end
              else
              begin
              end;
            end
            else Break;
          end;
        3:
          begin
            if FRelayBuffer.Size - FRelayBuffer.Position >= SizeOf(TKeyMsg) then
            begin
              FRelayBuffer.Read(KeyMsg, SizeOf(TKeyMsg));
              HandleKey(KeyMsg);
            end
            else Break;
          end;
        else
          FRelayBuffer.Position := FRelayBuffer.Position + 1;
      end;
    end;


    if FRelayBuffer.Position >= FRelayBuffer.Size then
      FRelayBuffer.Clear
    else if FRelayBuffer.Position > 0 then
    begin
      BytesReceived := FRelayBuffer.Size - FRelayBuffer.Position;
      Move(PByte(FRelayBuffer.Memory)[FRelayBuffer.Position],
           PByte(FRelayBuffer.Memory)[0], BytesReceived);
      FRelayBuffer.SetSize(BytesReceived);
    end;
  end
  else
  begin
    FRelayBuffer.Position := 0;
    SetLength(DataStr, FRelayBuffer.Size);
    if FRelayBuffer.Size > 0 then
      Move(FRelayBuffer.Memory^, DataStr[1], FRelayBuffer.Size);

    if Pos(#13#10, DataStr) > 0 then
    begin
      Lines := TStringList.Create;
      try
        Lines.Text := DataStr;
        for i := 0 to Lines.Count - 1 do
        begin
          Line := Trim(Lines[i]);
          if Line = '' then Continue;

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

          if Pos('REGISTERED', Line) = 1 then
          begin
            FIsRegistered := True;
            Label7.Caption := 'Status: Registered as ' + FMyID;
            AddLog('Registered with relay as: ' + FMyID);
          end
          else if Pos('CLIENT_CONNECTED', Line) = 1 then
          begin
            FRelayClientConnected := True;
            Label7.Caption := 'Status: Client connected via relay';
            AddLog('Client connected via relay');
            Timer1.Enabled := True;
          end
          else if Pos('CLIENT_DISCONNECTED', Line) = 1 then
          begin
            FRelayClientConnected := False;
            Label7.Caption := 'Status: Waiting for client';
            AddLog('Client disconnected via relay');
            Timer1.Enabled := False;
          end;
        end;
      finally
        Lines.Free;
      end;
      FRelayBuffer.Clear;
    end;
  end;
end;

procedure TForm1.RegisterWithRelay;
begin
  if FMyID = '' then
  begin
    FMyID := GenerateID;
    Edit3.Text := FMyID;
  end;

  if FRelaySocket = INVALID_SOCKET then
  begin
    ConnectToRelay;
    Exit;
  end;

  SendToRelay('REGISTER_SERVER ' + FMyID);
  Label5.Caption := 'Status: Registering...';
  AddLog('Registering with relay as: ' + FMyID);
end;

procedure TForm1.WMSocket(var Message: TLMessage);
var
  Event: Word;
  Socket: TSocket;
  i: Integer;
begin
  Event := LOWORD(Message.LParam);
  Socket := Message.WParam;

  if Socket = FRelaySocket then
  begin
    case Event of
      FD_READ:
        ProcessRelayData;
      FD_CLOSE:
        begin
          AddLog('Relay connection closed');
          DisconnectFromRelay;
        end;
    end;
    Exit;
  end;

  case Event of
    FD_ACCEPT:
      AcceptClient;
    FD_READ:
      begin
        for i := 0 to MAX_CLIENTS-1 do
          if FClients[i] = Socket then
          begin
            ProcessClient(i);
            Break;
          end;
      end;
    FD_CLOSE:
      begin
        for i := 0 to MAX_CLIENTS-1 do
          if FClients[i] = Socket then
          begin
            DisconnectClient(i);
            Break;
          end;
      end;
  end;
end;

procedure TForm1.InitGDIplus;
var
  Input: TGdiplusStartupInput;
  Status: TGPStatus;
begin
  if FGDIPlusInitialized then Exit;

  FillChar(Input, SizeOf(Input), 0);
  Input.GdiplusVersion := 1;
  Input.DebugEventCallback := nil;
  Input.SuppressBackgroundThread := False;
  Input.SuppressExternalCodecs := False;

  Status := GdiplusStartup(FGDIPlusToken, @Input, nil);
  if Status = Ok then
  begin
    FGDIPlusInitialized := True;
    if not GetEncoderClsid('image/jpeg', FJPEGEncoderClsid) then
    begin
      GdiplusShutdown(FGDIPlusToken);
      FGDIPlusInitialized := False;
      FGDIPlusToken := 0;
    end;
  end
  else
  begin
    FGDIPlusInitialized := False;
    FGDIPlusToken := 0;
  end;
end;

end.
 
Похожие темы
Admin Статья VNC через Relay (Работа за NAT) или пробуем повторить AnyDesk на Паскале.[2] Анонимность и приватность 0
Admin Статья VNC через Relay (Работа за NAT) или пробуем повторить AnyDesk на Паскале. [1] Анонимность и приватность 2
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

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