The last couple of days I played around with the script core socket functions and the TCP admin port.
Actually I wanted to create a socket accepting incoming connections to a Soldat server, but eventually, after talking to EnEsCe and doing some test, I realized it is impossible to allow incoming connections via sockets. Hence, I had to think about a new idea, which resulted into using the admin port as TCP socket server, but unfortunately they are hard to deal with: Obviously, you are not able to identify sockets connected to the admin port by an id or sth. unique and before some nerds now start to complain that there are IPs to identify a socket, I ask you what happens, when there are multiple sockets connected to the admin port from the same IP??? - Correct, there's no way to determine which socket sent a message, or to which socket I want to send data. Nevertheless, you can write your own socket identification system by assigning an unique id to each connected socket and every package sent via them. Responses from the TCP socket server have to be "labeled" with these unique ids, so sockets can determine if a package is addressed to them. But what happens if a server disconnects and you exactly need to know which socket it was? - OnAdminDisconnect will be called with the IP of the socket which disconnected. Hmmm IPs are not unique and in order to properly disconnect a server and to know which socket disconnected, we need to ping each server with the IP which just disconnected.
Basically, that's how you can use the admin port as TCP socket server. I provided a little demonstration below. It is about a "semi-global account system" which makes use of this trick to provide a "global account database" for different servers. Unfortunately, I haven't got around to write the client script, hence you can only test this demo with putty etc. Please note that this script is only for demonstration purposes and that it is coded in a very bad way. If you want to test the script, send me a pm and I'll try to explain how you can fake a client connecting to TCP server.
The database script used by the server:
const
MySSQL_PATH = './tables/';
type
TMySSQL_Column = record
Offset: word;
Value: string;
end;
TMySSQL_Row = record
Columns: array of TMySSQL_Column;
Data: string;
Cached: boolean;
end;
TMySSQL_Table = record
Rows: array of TMySSQL_Row;
Name, Exception: string;
Modified, Linked: boolean;
end;
function GetTypeOF(Value: variant): string;
begin
case VarType(Value) of
3 : Result:= IntToStr(Value);
5 : Result:= FloatToStr(Value);
11 : Result:= iif(Value, 'true', 'false');
256: Result:= Value;
end;
end;
function AryVntToAryStr(const AryVnt: array of variant): array of string;
var
i: integer;
begin
Result:= [];
SetArrayLength(Result, GetArrayLength(AryVnt));
for i:= 0 to GetArrayLength(Result) - 1 do
begin
Result[i]:= AryVnt[i];
end;
end;
function Implode(const Source: array of string; const Delimiter: string): string;
var
i: integer;
begin
Result:= '';
for i:= 0 to GetArrayLength(Source) - 1 do
begin
Result:= Result + Source[i] + Delimiter;
end;
end;
function ExplodeRow(Source: string; const Delimiter: string): array of TMySSQL_Row;
var
x, y, l: integer;
b: word;
begin
l:= length(Delimiter);
b:= length(Source);
Source:= Source + Delimiter;
Result:= [];
repeat
x:= Pos(Delimiter, Source);
SetArrayLength(Result, y + 1);
Result[y].Data:= Copy(Source, 1, x - 1);
y:= y + 1;
Delete(Source, 1, x + l - 1);
until (x = 0);
SetArrayLength(Result, iif(b = 0, y - 2, y - 1));
end;
function ReadFromFile(const Path, Filename: string): string;
begin
Result:= ReadFile(Path + Filename);
Result:= Copy(Result, 0, length(Result) - 2);
end;
function RowExists(const Table: TMySSQL_Table; const Row: word): boolean;
begin
Result:= GetArrayLength(Table.Rows) - 1 >= Row;
end;
function ColumnCached(var Table: TMySSQL_Table; const Row, Column: word): boolean;
begin
if (RowExists(Table, Row)) then
begin
Result:= GetArrayLength(Table.Rows[Row].Columns) - 1 >= Column;
end;
end;
procedure OnErrorOccur(var Table: TMySSQL_Table; const Method, Message: string);
begin
Table.Exception:= ' [*] [Error] MySSQL -> (' + Method + '): ' + Message;
end;
function DumpCacheRow(var Table: TMySSQL_Table; const Row: word): boolean;
var
Cache: string;
i: integer;
begin
if (RowExists(Table, Row)) then
begin
if (Table.Rows[Row].Cached) then
begin
Cache:= '';
for i:= 0 to GetArrayLength(Table.Rows[Row].Columns) - 1 do
begin
Cache:= Cache + Table.Rows[Row].Columns[i].Value + #9 ;
end;
Table.Rows[Row].Data:= Cache;
Result:= true;
end;
end else
begin
OnErrorOccur(Table, 'DumpCacheRow', 'Row (' + IntToStr(Row) + ') does not exist');
end;
end;
procedure DumpCacheTable(var Table: TMySSQL_Table);
var
i: integer;
begin
for i:= 0 to GetArrayLength(Table.Rows) - 1 do
begin
DumpCacheRow(Table, i);
end;
end;
procedure UncacheRow(var Table: TMySSQL_Table; const Row: word);
begin
if (DumpCacheRow(Table, Row)) then
begin
Table.Rows[Row].Columns:= [];
Table.Rows[Row].Cached:= false;
end;
end;
function SaveTable(var Table: TMySSQL_Table): boolean;
var
Rows: string;
i: integer;
begin
if (Table.Linked) then
begin
Rows:= '';
DumpCacheTable(Table);
for i:= 0 to GetArrayLength(Table.Rows) - 1 do
begin
Rows:= Rows + #13#10 + Table.Rows[i].Data;
end;
Delete(Rows, 1, 2);
WriteFile(MySSQL_PATH + Table.Name + '.myssql', Rows);
Table.Modified:= false;
Result:= true;
end else
begin
OnErrorOccur(Table, 'SaveTable', 'Table is not linked');
end;
end;
function LoadTable(var Table: TMySSQL_Table; const Tablename: string): boolean;
begin
if (FileExists(MySSQL_PATH + Tablename + '.myssql')) then
begin
Table.Rows:= ExplodeRow(ReadFromFile(MySSQL_PATH, Tablename + '.myssql'), #13#10);
Table.Name:= Tablename;
Table.Linked:= true;
Table.Modified:= false;
Table.Exception:= '';
Result:= true;
end else
begin
OnErrorOccur(Table, 'LoadTable', 'Unable to load table "' + Tablename + '"');
end;
end;
function UnloadTable(var Table: TMySSQL_Table): boolean;
begin
if (Table.Linked) then
begin
if (Table.Modified) then
begin
SaveTable(Table);
end;
Table.Rows:= [];
Table.Linked:= false;
Table.Name:= '';
Table.Exception:= '';
Result:= true;
end else
begin
OnErrorOccur(Table, 'UnloadTable', 'Table is not linked');
end;
end;
function CreateTable(var Table: TMySSQL_Table; const Tablename: string): boolean;
begin
WriteFile(MySSQL_PATH + Tablename + '.myssql', '');
if (LoadTable(Table, Tablename)) then
begin
Result:= true;
end else
begin
OnErrorOccur(Table, 'CreateTable', 'Unable to create table "' + Tablename + '"');
end;
end;
procedure DestroyTable(var Table: TMySSQL_Table);
begin
Table.Rows:= [];
end;
function CreateRow(var Table: TMySSQL_Table; const Columns: array of variant): word;
begin
Result:= GetArrayLength(Table.Rows);
SetArrayLength(Table.Rows, Result + 1);
Table.Rows[Result].Data:= Implode(AryVnttoAryStr(Columns), #9);
Table.Modified:= true;
end;
function DestroyRow(var Table: TMySSQL_Table; const Row: word): boolean;
var
HIndex: integer;
begin
if (RowExists(Table, Row)) then
begin
HIndex:= GetArrayLength(Table.Rows) - 1;
if (HIndex <> Row) then
begin
Table.Rows[Row]:= Table.Rows[HIndex];
end;
SetArrayLength(Table.Rows, iif(HIndex > 0, HIndex, 0));
Table.Modified:= true;
Result:= true;
end else
begin
OnErrorOccur(Table, 'DestroyRow', 'Row (' + IntToStr(Row) + ') does not exist');
end;
end;
function FetchColumn(var Table: TMySSQL_Table; const Row, Column: word; var DummyC: TMySSQL_Column): boolean;
var
Source: string;
TPos: integer;
x, c: word;
begin
if (not(RowExists(Table, Row))) then
begin
exit;
end;
Source:= Table.Rows[Row].Data;
c:= 1;
for x:= 0 to Column do
begin
Delete(Source, 1, TPos);
c:= c + TPos;
TPos:= Pos(#9, Source);
end;
if (TPos > 0) then
begin
DummyC.Value:= Copy(Source, 1, Pos(#9, Source) - 1);
DummyC.Offset:= c;
Result:= true;
end;
end;
function FetchRowByColumn(var Table: TMySSQL_Table; const Column: word; const Value: variant): integer;
var
C: TMySSQL_Column;
i: integer;
begin
Result:= -1;
for i:= 0 to GetArrayLength(Table.Rows) - 1 do
begin
if ((FetchColumn(Table, i, Column, C) = true) and (C.Value = Value)) then
begin
Result:= i;
break;
end;
end;
end;
function CacheRow(var Table: TMySSQL_Table; const Row: word): boolean;
var
C: TMySSQL_Column;
i, x: word;
begin
if (RowExists(Table, Row)) then
begin
if (not(Table.Rows[Row].Cached)) then
begin
while (FetchColumn(Table, Row, i, C) = true) do
begin
SetArrayLength(Table.Rows[Row].Columns, x + 1);
Table.Rows[Row].Columns[x]:= C;
x:= x + 1;
i:= i + 1;
end;
Table.Rows[Row].Cached:= true;
Result:= true;
end;
end else
begin
OnErrorOccur(Table, 'CacheRow', 'Row (' + IntToStr(Row) + ') does not exist');
end;
end;
function SetColumn(var Table: TMySSQL_Table; const Row, Column: word; const Value: variant): boolean;
var
C: TMySSQL_Column;
Cached: boolean;
begin
if (RowExists(Table, Row)) then
begin
Cached:= Table.Rows[Row].Cached;
UnCacheRow(Table, Row);
if (FetchColumn(Table, Row, Column, C)) then
begin
Delete(Table.Rows[Row].Data, C.Offset, Length(C.Value));
Insert(GetTypeOF(Value), Table.Rows[Row].Data, C.Offset);
Table.Modified:= true;
Result:= true;
end else
begin
OnErrorOccur(Table, 'SetColumn', 'Out of row range (' + IntToStr(Row) + '/' + IntToStr(Column) + ')');
end;
if (Cached) then
begin
CacheRow(Table, Row);
end;
end else
begin
OnErrorOccur(Table, 'SetColumn', 'Row (' + IntToStr(Row) + ') does not exist');
end;
end;
function CFetchColumn(var Table: TMySSQL_Table; const Row, Column: word): string;
begin
if (ColumnCached(Table, Row, Column)) then
begin
Result:= Table.Rows[Row].Columns[Column].Value;
end else
begin
OnErrorOccur(Table, 'CFetchColumn', 'Column (' + IntToStr(Column) + ') is not cached');
end;
end;
function CSetColumn(var Table: TMySSQL_Table; const Row, Column: word; const Value: variant): boolean;
begin
if (ColumnCached(Table, Row, Column)) then
begin
Table.Rows[Row].Columns[Column].Value:= GetTypeOF(Value);
Table.Modified:= true;
Result:= true;
end else
begin
OnErrorOccur(Table, 'CSetColumn', 'Column (' + IntToStr(Column) + ') is not cached');
end;
end;
function CIncColumn(var Table: TMySSQL_Table; const Row, Column: word; const Increase: extended): boolean;
var
Value: string;
begin
if (ColumnCached(Table, Row, Column)) then
begin
Value:= CFetchColumn(Table, Row, Column);
if (RegExpMatch('^-?(\d+|\d+.?\d+)$', Value)) then
begin
Table.Rows[Row].Columns[Column].Value:= FloatToStr(StrToFloat(Value) + Increase);
Table.Modified:= true;
Result:= true;
end else
begin
OnErrorOccur(Table, 'CIncColumn', 'Column (' + IntToStr(Column) + ') represents no numeric value');
end;
end else
begin
OnErrorOccur(Table, 'CIncColumn', 'Column (' + IntToStr(Column) + ') is not cached');
end;
end;
function AppendColumn(var Table: TMySSQL_Table; const Row: word; const Value: variant): boolean;
var
Cached: boolean;
begin
if (RowExists(Table, Row)) then
begin
Cached:= Table.Rows[Row].Cached;
UncacheRow(Table, Row);
Table.Rows[Row].Data:= Table.Rows[Row].Data + GetTypeOF(Value) + #9;
if (Cached) then
begin
CacheRow(Table, Row);
end;
Table.Modified:= true;
Result:= true;
end else
begin
OnErrorOccur(Table, 'AppendColumn', 'Row (' + IntToStr(Row) + ') does not exist');
end;
end;
The socket server script....
const
CLIST = 'b2bG9NLYF77heDtF|create|pong|delete|login|loginip|logout|changepass|ping';
type
TRef = record
Row: word;
Name: string;
end;
TServer = record
IP: string;
ID: integer;
Timeout: cardinal;
Refs: array of TRef;
end;
var
Accounts, ReqQueue: TMySSQL_Table;
Servers : array of TServer;
Counter: integer;
function Explode(Source: string; const Delimiter: string): array of string;
var
x, y, l: integer;
b: word;
begin
l:= length(Delimiter);
b:= length(Source);
Source:= Source + Delimiter;
Result:= [];
repeat
x:= Pos(Delimiter, Source);
SetArrayLength(Result, y + 1);
Result[y]:= Copy(Source, 1, x - 1);
y:= y + 1;
Delete(Source, 1, x + l - 1);
until (x = 0);
SetArrayLength(Result, iif(b = 0, y - 2, y - 1));
end;
procedure ActivateServer();
begin
if (not(LoadTable(Accounts, 'accounts'))) then
begin
WriteLn('Unable to load accounts database');
Sleep(3000);
Shutdown;
end;
end;
function GetNewID(): integer;
begin
Inc(Counter, 1);
Result:= Counter;
end;
function ServerRegistered(const SID: word): integer;
var
i: integer;
begin
Result:= -1;
for i:= 0 to GetArrayLength(Servers) - 1 do
begin
if (Servers[i].ID = SID) then
begin
Result:= i;
break;
end;
end;
end;
function RegisterServer(const IP: string): word;
var
Index, i: integer;
begin
Index:= GetArrayLength(Servers);
for i:= 0 to Index - 1 do
begin
if (Servers[i].ID = 0) then
begin
Index:= i;
break;
end;
end;
if (Index = GetArrayLength(Servers)) then
begin
SetArrayLength(Servers, Index + 1);
end;
Servers[Index].IP:= IP;
Servers[Index].ID:= GetNewID();
Result:= Servers[Index].ID;
end;
procedure OnServerDisconnect(const Index: integer);
var
i, HIndex: integer;
begin
for i:= 0 to GetArrayLength(Servers[Index].Refs) - 1 do
begin
CSetColumn(Accounts, Servers[Index].Refs[i].Row, 3, 0);
CSetColumn(Accounts, Servers[Index].Refs[i].Row, 4, false);
UnCacheRow(Accounts, Servers[Index].Refs[i].Row);
end;
HIndex:= GetArrayLength(Servers) - 1;
if (HIndex <> Index) then
begin
Servers[Index]:= Servers[HIndex];
end;
SetArrayLength(Servers, iif(HIndex > 0, HIndex, 0));
SaveTable(Accounts);
WriteLn('Server disconnected');
end;
function OnServerSignIn(const IP: string): string;
begin
Result:= IntToStr(RegisterServer(IP));
end;
procedure AddReference(const Index: integer; const Row: word; const Name: string);
var
HIndex: integer;
begin
HIndex:= GetArrayLength(Servers[Index].Refs);
SetArrayLength(Servers[Index].Refs, HIndex + 1);
Servers[Index].Refs[HIndex].Row:= Row;
Servers[Index].Refs[HIndex].Name:= Name;
CacheRow(Accounts, Row);
end;
function GetReference(const Index: integer; const Name: string): integer;
var
i: integer;
begin
Result:= -1;
for i:= 0 to GetArrayLength(Servers[Index].Refs) - 1 do
begin
if (Servers[Index].Refs[i].Name = Name) then
begin
Result:= i;
break;
end;
end;
end;
function GetReferenceRow(const Index, Row: integer): integer;
var
i: integer;
begin
Result:= -1;
for i:= 0 to GetArrayLength(Servers[Index].Refs) - 1 do
begin
if (Servers[Index].Refs[i].Row = Row) then
begin
Result:= i;
break;
end;
end;
end;
function DeleteReference(const Index: integer; const Name: string): integer;
var
FIndex, HIndex: integer;
begin
FIndex:= GetReference(Index, Name);
Result:= -1;
if (FIndex >= 0) then
begin
HIndex:= GetArrayLength(Servers[Index].Refs) - 1;
Result:= Servers[Index].Refs[FIndex].Row;
if (HIndex <> FIndex) then
begin
Servers[Index].Refs[FIndex]:= Servers[Index].Refs[HIndex];
end;
SetArrayLength(Servers[Index].Refs, iif(HIndex > 0, HIndex, 0));
end;
end;
function OnAccountCreate(const Index: integer; const Name, Pass, IP: string): string;
begin
if (FetchRowByColumn(Accounts, 0, Name) < 0) then
begin
AddReference(Index, CreateRow(Accounts, [Name, Pass, IP, Servers[Index].ID, true]), Name);
Result:= 'Account ' + Name + ' successfully created.';
end else
begin
Result:= 'An account with the name ' + Name + ' already exists.';
end;
end;
function OnAccountDelete(const Index: integer; const Name: string): string;
var
Row, SID, SignedIn: integer;
begin
Row:= DeleteReference(Index, Name);
if (Row >= 0) then
begin
DestroyRow(Accounts, Row);
// Update row reference if row deleted is referenced; intricate :/
if (Row < GetArrayLength(Accounts.Rows)) then
begin
if (Accounts.Rows[Row].Cached) then
begin
SID:= StrToInt(CFetchColumn(Accounts, Row, 3));
if (SID > 0) then
begin
SignedIn:= ServerRegistered(SID);
if (SignedIn > -1) then
begin
Servers[SignedIn].Refs[GetReferenceRow(SignedIn, GetArrayLength(Accounts.Rows))].Row:= Row;
WriteLn('Successfully updated row index after delete');
end;
end;
end;
end;
Result:= 'Account successfully deleted.';
end else
begin
Result:= 'You either have no account, or you are not logged in.';
end;
end;
function OnPlayerLogin(const Index: integer; const Name, Password, IP: string): string;
var
Row: integer;
MCached: boolean;
begin
Row:= FetchRowByColumn(Accounts, 0, Name);
if (Row > -1) then
begin
if (CacheRow(Accounts, Row)) then
begin
MCached:= true;
end;
if (CFetchColumn(Accounts, Row, 1) = Password) then
begin
if (CFetchColumn(Accounts, Row, 4) = 'false') then
begin
CSetColumn(Accounts, Row, 2, IP);
CSetColumn(Accounts, Row, 3, Servers[Index].ID);
CSetColumn(Accounts, Row, 4, true);
AddReference(Index, Row, Name);
MCached:= false;
Result:= 'Successfully logged in.';
end else
begin
Result:= 'Account already in use.';
end;
end else
begin
Result:= 'Invalid password.';
end;
end else
begin
Result:= 'Account does not exist.';
end;
if (MCached) then
begin
UnCacheRow(Accounts, Row);
end;
end;
function OnPlayerChangePass(const Index: integer; const Name, NewPass: string): string;
var
Row: integer;
begin
Row:= GetReference(Index, Name);
if (Row >= 0) then
begin
if (CSetColumn(Accounts, Row, 1, NewPass)) then
begin
Result:= 'Password changed';
end else
begin
Result:= 'An unexpected database error occured. Try again later.';
end;
end else
begin
Result:= 'You either do not have an account, or you are not logged in.';
end;
end;
function OnPlayerLogout(const Index: integer; const Name: string): string;
var
Row: integer;
begin
Row:= DeleteReference(Index, Name);
if (Row >= 0) then
begin
CSetColumn(Accounts, Row, 3, 0);
CSetColumn(Accounts, Row, 4, false);
UncacheRow(Accounts, Row);
Result:= 'Successfully logged out.';
end else
begin
Result:= 'You are not logged in, or you do not have an account.';
end;
end;
function OnPlayerLoginIP(const Index: integer; const Name, IP: string): string;
var
Row: integer;
MCached: boolean;
begin
Row:= FetchRowByColumn(Accounts, 0, Name);
if (Row > -1) then
begin
if (CacheRow(Accounts, Row)) then
begin
MCached:= true;
end;
if (CFetchColumn(Accounts, Row, 2) = IP) then
begin
if (CFetchColumn(Accounts, Row, 4) = 'false') then
begin
CSetColumn(Accounts, Row, 3, Servers[Index].ID);
CSetColumn(Accounts, Row, 4, true);
AddReference(Index, Row, Name);
MCached:= false;
Result:= 'Successfully logged in';
end else
begin
Result:= 'Account already in use.';
end;
end else
begin
Result:= 'IP does not match.';
end;
end else
begin
Result:= 'Account does not exist.';
end;
if (MCached) then
begin
UnCacheRow(Accounts, Row);
end;
end;
procedure SendResponse(const RID: word; const Reply: string);
begin
TCPAdminPM(CFetchColumn(ReqQueue, RID, 0), CFetchColumn(ReqQueue, RID, 1) + #9 + CFetchColumn(ReqQueue, RID, 2) + #9 + Reply);
end;
procedure Pong(const Index: integer);
begin
Servers[Index].Timeout:= 0;
end;
procedure Ping(const IP: string; const SID: integer);
begin
TCPAdminPM(IP, IntToStr(SID) + #9 + '-1' + #9 + 'ping');
end;
procedure OnAdminDisconnect(IP: string);
var
i: integer;
begin
for i:= 0 to GetArrayLength(Servers) - 1 do
begin
if ((Servers[i].IP = IP) and (Servers[i].ID > 0)) then
begin
Ping(IP, Servers[i].ID);
Servers[i].Timeout:= GetTickCount() + 180;
end;
end;
end;
procedure OnAdminMessage(const IP, Package: string);
begin
if (RegExpMatch('^(-?\d+)\t(-?\d+)\t(' + CLIST + ')(\t[\w !-~€@]+)*$', Package)) then
begin
CreateRow(ReqQueue, [IP, Package]);
end;
end;
procedure AppOnIdle(Ticks: integer);
var
i, Index: integer;
Reply: string;
begin
if (Accounts.Exception <> '') then
begin
WriteLn(Accounts.Exception);
end;
for i:= 0 to GetArrayLength(Servers) - 1 do
begin
if (Servers[i].Timeout > 0) then
begin
if (GetTickCount() >= Servers[i].Timeout) then
begin
OnServerDisconnect(i);
end;
end;
end;
for i:= 0 to GetArrayLength(ReqQueue.Rows) - 1 do
begin
CacheRow(ReqQueue, i);
Reply:= '';
Index:= ServerRegistered(StrToInt(CFetchColumn(ReqQueue, i, 1)));
if (Index > -1) then
begin
case CFetchColumn(ReqQueue, i, 3) of
'create' : Reply:= OnAccountCreate(Index, CFetchColumn(ReqQueue, i, 4), CFetchColumn(ReqQueue, i, 5), CFetchColumn(ReqQueue, i, 6));
'delete' : Reply:= OnAccountDelete(Index, CFetchColumn(ReqQueue, i, 4));
'login' : Reply:= OnPlayerLogin(Index, CFetchColumn(ReqQueue, i, 4), CFetchColumn(ReqQueue, i, 5), CFetchColumn(ReqQueue, i, 6));
'loginip' : Reply:= OnPlayerLoginIP(Index, CFetchColumn(ReqQueue, i, 4), CFetchColumn(ReqQueue, i, 5));
'logout' : Reply:= OnPlayerLogout(Index, CFetchColumn(ReqQueue, i, 4));
'changepass': Reply:= OnPlayerChangePass(Index, CFetchColumn(ReqQueue, i, 4), CFetchColumn(ReqQueue, i, 5));
'pong' : Pong(Index);
'ping' : SendData(Servers[Index].ID, 'pong' + #13#10);
else Reply:= 'Unknown command';
end;
end else
begin
case CFetchColumn(ReqQueue, i, 3) of
'b2bG9NLYF77heDtF': Reply:= OnServerSignIn(CFetchColumn(ReqQueue, i, 0));
end;
end;
if (Reply <> '') then
begin
SendResponse(i, Reply);
end;
end;
DestroyTable(ReqQueue);
end;