Files
ewsdr/WebUtils.pas
T
2026-03-08 22:34:18 +03:00

317 lines
12 KiB
ObjectPascal

unit WebUtils;
{
WebUtils.pas — Вспомогательные функции для WebServer:
- Кросс-платформенные обёртки сокетов (Windows/Linux)
- SHA-1 (минимальная реализация для WebSocket handshake)
- Base64 (кодирование)
- JSON helpers (JsonGetStr, JsonGetFloat, JsonGetInt, JsonGetBool)
ИСПРАВЛЕНИЯ:
- (Windows build fix) Порядок uses: стандартные RTL-юниты первыми,
платформенные (WinSock2) последними — исключает конфликт
идентификатора Create в режиме {$MODE Delphi} под Windows.
- (Linux shutdown fix) Добавлена SockShutdown вызов shutdown(SHUT_RDWR)
перед close, что немедленно прерывает заблокированные fpAccept/fpRecv
в других потоках и предотвращает зависание при закрытии программы.
}
{$IFDEF FPC}
{$MODE Delphi}
{$LONGSTRINGS ON}
{$ENDIF}
interface
uses
SysUtils, Math
{$IFDEF WINDOWS}, Windows, WinSock2{$ELSE}, BaseUnix, Sockets{$ENDIF};
{ ── Кросс-платформенные константы сокетов ─────────────────────────────────── }
{$IFDEF WINDOWS}
const
SOCK_INVALID = INVALID_SOCKET;
SOCK_ERR = SOCKET_ERROR;
{$ELSE}
const
SOCK_INVALID = TSocket(-1);
SOCK_ERR = -1;
INVALID_SOCKET = TSocket(-1);
{$ENDIF}
{ ── Обёртки системных вызовов сокетов ─────────────────────────────────────── }
function SockClose(S: TSocket): Integer; inline;
{ SockShutdown — прерывает все блокирующие recv/accept на сокете в других
потоках. На Linux необходимо вызывать ДО SockClose, иначе потоки не
разблокируются и программа зависнет при завершении.
На Windows работает через SD_BOTH. }
procedure SockShutdown(S: TSocket);
function SockRecv(S: TSocket; Buf: Pointer; Len, Flags: Integer): Integer; inline;
function SockSend(S: TSocket; Buf: Pointer; Len, Flags: Integer): Integer; inline;
procedure SockSetNonBlock(S: TSocket; NB: Boolean);
{ ── SHA-1 ─────────────────────────────────────────────────────────────────── }
type
TSHA1Digest = array[0..19] of Byte;
TSHA1State = array[0..4] of LongWord;
procedure SHA1Transform(var S: TSHA1State; const Block: array of Byte);
function SHA1(const Data: string): TSHA1Digest;
{ ── Base64 ───────────────────────────────────────────────────────────────── }
function Base64EncodeBytes(const Data: array of Byte; Len: Integer): string;
function Base64EncodeStr(const S: string): string;
{ ── JSON helpers ─────────────────────────────────────────────────────────── }
function JsonGetStr(const Json, Key: string): string;
function JsonGetFloat(const Json, Key: string; Def: Double): Double;
function JsonGetInt(const Json, Key: string; Def: Integer): Integer;
function JsonGetBool(const Json, Key: string; Def: Boolean): Boolean;
implementation
{ ═══════════════════════════════════════════════════════════════════════════
Кросс-платформенные обёртки сокетов
═══════════════════════════════════════════════════════════════════════════ }
{$IFDEF WINDOWS}
function SockClose(S: TSocket): Integer;
begin
Result := closesocket(S);
end;
procedure SockShutdown(S: TSocket);
begin
// SD_BOTH = 2 — прерывает и recv и send, разблокирует accept/recv в других потоках
shutdown(S, SD_BOTH);
end;
function SockRecv(S: TSocket; Buf: Pointer; Len, Flags: Integer): Integer;
begin
Result := recv(S, Buf^, Len, Flags);
end;
function SockSend(S: TSocket; Buf: Pointer; Len, Flags: Integer): Integer;
begin
Result := send(S, Buf^, Len, Flags);
end;
procedure SockSetNonBlock(S: TSocket; NB: Boolean);
var Mode: LongWord;
begin
Mode := Ord(NB);
ioctlsocket(S, FIONBIO, @Mode);
end;
{$ELSE}
function SockClose(S: TSocket): Integer;
begin
Result := fpClose(S);
end;
procedure SockShutdown(S: TSocket);
begin
// SHUT_RDWR = 2 — прерывает все блокирующие fpAccept/fpRecv в других потоках.
// На Linux одного fpClose недостаточно — он не прерывает системный вызов
// в чужом потоке. После shutdown поток получит 0 или ECONNRESET и выйдет.
fpShutdown(S, 2 {SHUT_RDWR});
end;
function SockRecv(S: TSocket; Buf: Pointer; Len, Flags: Integer): Integer;
begin
Result := fpRecv(S, Buf, Len, Flags);
end;
function SockSend(S: TSocket; Buf: Pointer; Len, Flags: Integer): Integer;
begin
Result := fpSend(S, Buf, Len, Flags);
end;
procedure SockSetNonBlock(S: TSocket; NB: Boolean);
var Flags: Integer;
begin
Flags := fpFcntl(S, F_GETFL, 0);
if NB then Flags := Flags or O_NONBLOCK
else Flags := Flags and (not O_NONBLOCK);
fpFcntl(S, F_SETFL, Flags);
end;
{$ENDIF}
{ ═══════════════════════════════════════════════════════════════════════════
SHA-1 (минимальная реализация для WebSocket handshake)
═══════════════════════════════════════════════════════════════════════════ }
procedure SHA1Transform(var S: TSHA1State; const Block: array of Byte);
var
W: array[0..79] of LongWord;
i: Integer;
a, b, c, d, e, t, f, k: LongWord;
begin
for i := 0 to 15 do
W[i] := (Block[i*4] shl 24) or (Block[i*4+1] shl 16) or
(Block[i*4+2] shl 8) or Block[i*4+3];
for i := 16 to 79 do
begin
t := W[i-3] xor W[i-8] xor W[i-14] xor W[i-16];
W[i] := (t shl 1) or (t shr 31);
end;
a := S[0]; b := S[1]; c := S[2]; d := S[3]; e := S[4];
for i := 0 to 79 do
begin
if i < 20 then begin f := (b and c) or ((not b) and d); k := $5A827999; end
else if i < 40 then begin f := b xor c xor d; k := $6ED9EBA1; end
else if i < 60 then begin f := (b and c) or (b and d) or (c and d); k := $8F1BBCDC; end
else begin f := b xor c xor d; k := $CA62C1D6; end;
t := ((a shl 5) or (a shr 27)) + f + e + k + W[i];
e := d; d := c; c := (b shl 30) or (b shr 2); b := a; a := t;
end;
Inc(S[0], a); Inc(S[1], b); Inc(S[2], c); Inc(S[3], d); Inc(S[4], e);
end;
function SHA1(const Data: string): TSHA1Digest;
var
S: TSHA1State;
Buf: array[0..63] of Byte;
Len, BitLen, i, Pad: Integer;
begin
S[0] := $67452301; S[1] := $EFCDAB89;
S[2] := $98BADCFE; S[3] := $10325476; S[4] := $C3D2E1F0;
Len := Length(Data);
BitLen := Len * 8;
i := 0;
while i + 64 <= Len do
begin
Move(Data[i+1], Buf[0], 64);
SHA1Transform(S, Buf);
Inc(i, 64);
end;
Pad := Len - i;
FillChar(Buf[0], 64, 0);
if Pad > 0 then Move(Data[i+1], Buf[0], Pad);
Buf[Pad] := $80;
if Pad >= 55 then
begin
SHA1Transform(S, Buf);
FillChar(Buf[0], 64, 0);
end;
Buf[63] := Byte(BitLen); Buf[62] := Byte(BitLen shr 8);
Buf[61] := Byte(BitLen shr 16); Buf[60] := Byte(BitLen shr 24);
SHA1Transform(S, Buf);
for i := 0 to 4 do
begin
Result[i*4] := Byte(S[i] shr 24); Result[i*4+1] := Byte(S[i] shr 16);
Result[i*4+2] := Byte(S[i] shr 8); Result[i*4+3] := Byte(S[i]);
end;
end;
{ ═══════════════════════════════════════════════════════════════════════════
Base64
═══════════════════════════════════════════════════════════════════════════ }
const
B64Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
function Base64EncodeBytes(const Data: array of Byte; Len: Integer): string;
var
i, j, n: Integer;
begin
Result := '';
i := 0;
while i < Len do
begin
n := Data[i] shl 16;
if i+1 < Len then n := n or (Data[i+1] shl 8);
if i+2 < Len then n := n or Data[i+2];
Result := Result
+ B64Chars[(n shr 18) and 63 + 1]
+ B64Chars[(n shr 12) and 63 + 1]
+ B64Chars[(n shr 6) and 63 + 1]
+ B64Chars[ n and 63 + 1];
Inc(i, 3);
end;
j := Len mod 3;
if j = 1 then begin Result[Length(Result)-1] := '='; Result[Length(Result)] := '='; end
else if j = 2 then Result[Length(Result)] := '=';
end;
function Base64EncodeStr(const S: string): string;
var
B: array of Byte;
i: Integer;
begin
SetLength(B, Length(S));
for i := 1 to Length(S) do B[i-1] := Ord(S[i]);
Result := Base64EncodeBytes(B, Length(S));
end;
{ ═══════════════════════════════════════════════════════════════════════════
JSON helpers (минимальный парсер без зависимостей)
═══════════════════════════════════════════════════════════════════════════ }
function JsonGetStr(const Json, Key: string): string;
var
P, P2: Integer;
K: string;
begin
Result := '';
// Ищем "key": — с двоеточием, чтобы не совпасть с "key" внутри значения
// Например в {"cmd":"mode","mode":1} поиск "mode": не найдёт "mode" как значение cmd
K := '"' + Key + '":';
P := System.Pos(K, Json);
if P = 0 then Exit;
Inc(P, Length(K));
// Пропускаем пробелы после двоеточия
while (P <= Length(Json)) and (Json[P] = ' ') do Inc(P);
if P > Length(Json) then Exit;
if Json[P] = '"' then
begin
Inc(P); P2 := P;
while (P2 <= Length(Json)) and (Json[P2] <> '"') do Inc(P2);
Result := Copy(Json, P, P2 - P);
end
else
begin
P2 := P;
while (P2 <= Length(Json)) and not (Json[P2] in [',', '}']) do Inc(P2);
Result := Trim(Copy(Json, P, P2 - P));
end;
end;
function JsonGetFloat(const Json, Key: string; Def: Double): Double;
var S: string;
begin
S := JsonGetStr(Json, Key);
if S = '' then
Result := Def
else
begin
val(S, Result);
if IsNaN(Result) then Result := Def;
end;
end;
function JsonGetInt(const Json, Key: string; Def: Integer): Integer;
begin
Result := Round(JsonGetFloat(Json, Key, Def));
end;
function JsonGetBool(const Json, Key: string; Def: Boolean): Boolean;
var S: string;
begin
S := JsonGetStr(Json, Key);
if S = '' then Result := Def
else Result := (S = 'true') or (S = '1');
end;
end.