📜 ⬆️ ⬇️

Steam Protocol v2

Steam Logo

The final article of the cycle, the most interesting and most voluminous:


The article will discuss the protocols for exchanging Steam client data with various servers:
')

Once again, let me remind you that the protocols in question are outdated and are not currently used (with the exception of GDS and Config - for compatibility).

All algorithms are presented in my repository .

The entire protocol is based on Sockets, so WireShark was enough to parse it almost everywhere. The entire protocol description will be viewed by the client (in Delphi). Mostly there will be sections of server code in C ++.

Almost all requests have a common algorithm.
function TSteamNetwork.ConectToServer(Addr: TSockAddr; const QUERY; QSize: uint32; Command: pByte; CSize: uint32; var ReplySize: uint32; IsConfigServer: boolean = false): pByte; var Accept: boolean; DestIP: uint32; Sock: CSocket; begin result:=nil; Sock:=CSocket.Create(SOCKET_IP); if not Sock.Connect(Addr) then Exit; {if (Sock=nil) or (not Sock.Connect(Addr)) then Exit; } Sock.SetTimeOut(3000); if not Sock.Send(QUERY, QSize) then Exit; if not Sock.recv(Accept, 1) then Exit; if IsConfigServer then if not Sock.recv(DestIP, 4) then Exit; if not Accept then Exit; CSize:=htonl(CSize); if not Sock.send(CSize, 4) then Exit; CSize:=htonl(CSize); if not Sock.send(Command^, CSize) then Exit; Sock.OnLoadingProc:=OnLoadingProc; result:=Sock.RecvFromLen(ReplySize); Sock.Free; end; 


This protocol is used when querying all list servers (GD, Config, ContentList). In the QUERY parameter, a pointer is passed to an array of bytes representing the type of request, and in QSize, the size of this array. In the Command parameter, a pointer is passed to the byte array with the command itself, and to CSize, the size of this array. The variable ReplySize contains the size of the requested response and after the call will be equal to the actually received amount of data. The above code can be represented by the following pseudocode:

               IP-,     Config Server'    ,             

In some cases, RSA-signature is used, obtained by the following algorithm:

Signature of the data block for Steam
 char *RSASign(RSA *key, char *Mess, UINT32 size, UINT32 sign_size) { char *sign = new char[sign_size]; memset(sign, 0, sign_size); sign[0] = '\x00'; sign[1] = '\x01'; memset(&sign[2], 0xff, sign_size-38); memcpy(&sign[sign_size-36], "\x00\x30\x21\x30\x09\x06\x05\x2b\x0e\x03\x02\x1a\x05\x00\x04\x14", 0x10); void *hash = HashSHA1(Mess, size); memcpy((void*)&sign[sign_size-20], hash, 20); delete hash; RSA_public_encrypt(sign_size, (UCHAR*)sign, (UCHAR*)sign, key, RSA_NO_PADDING); return sign; } char *RSASignMessage(RSA *key, char *Mess, UINT32 size) { return RSASign(key, Mess, size, 128); } char *RSASignMessage1024(RSA *key, char *Mess, UINT32 size) { return RSASign(key, Mess, size, 256); } 



Previously used keys for signing
 // MainKeySign #define MainKeySign_n "86724794f8a0fcb0c129b979e7af2e1e309303a7042503d835708873b1df8a9e307c228b9c0862f8f5dbe6f81579233db8a4fe6ba14551679ad72c01973b5ee4ecf8ca2c21524b125bb06cfa0047e2d202c2a70b7f71ad7d1c3665e557a7387bbc43fe52244e58d91a14c660a84b6ae6fdc857b3f595376a8e484cb6b90cc992f5c57cccb1a1197ee90814186b046968f872b84297dad46ed4119ae0f402803108ad95777615c827de8372487a22902cb288bcbad7bc4a842e03a33bd26e052386cbc088c3932bdd1ec4fee1f734fe5eeec55d51c91e1d9e5eae46cf7aac15b2654af8e6c9443b41e92568cce79c08ab6fa61601e4eed791f0436fdc296bb373" #define MainKeySign_e "07e89acc87188755b1027452770a4e01c69f3c733c7aa5df8aac44430a768faef3cb11174569e7b44ab2951da6e90212b0822d1563d6e6abbdd06c0017f46efe684adeb74d4113798cec42a54b4f85d01e47af79259d4670c56c9c950527f443838b876e3e5ef62ae36aa241ebc83376ffde9bbf4aae6cabea407cfbb08848179e466bcb046b0a857d821c5888fcd95b2aae1b92aa64f3a6037295144aa45d0dbebce075023523bce4243ae194258026fc879656560c109ea9547a002db38b89caac90d75758e74c5616ed9816f3ed130ff6926a1597380b6fc98b5eeefc5104502d9bee9da296ca26b32d9094452ab1eb9cf970acabeecde6b1ffae57b56401" #define MainKeySign_d "11" // NetworkKey #define NetworkKey_n "bf973e24beb372c12bea4494450afaee290987fedae8580057e4f15b93b46185b8daf2d952e24d6f9a23805819578693a846e0b8fcc43c23e1f2bf49e843aff4b8e9af6c5e2e7b9df44e29e3c1c93f166e25e42b8f9109be8ad03438845a3c1925504ecc090aabd49a0fc6783746ff4e9e090aa96f1c8009baf9162b66716059" #define NetworkKey_e "11" #define NetworkKey_d "4ee3ec697bb34d5e999cb2d3a3f5766210e5ce961de7334b6f7c6361f18682825b2cfa95b8b7894c124ada7ea105ec1eaeb3c5f1d17dfaa55d099a0f5fa366913b171af767fe67fb89f5393efdb69634f74cb41cb7b3501025c4e8fef1ff434307c7200f197b74044e93dbcf50dcc407cbf347b4b817383471cd1de7b5964a9d" 


General Direcrory Server


It is the root of the entire infrastructure and only stores the addresses of other servers. Request type is 0x02000000 (used when calling ConectToServer). The answer is a list of server IP addresses as requested:


Known queries and their commands:


What a CSER server - I did not understand, so nowhere else is it mentioned about them.

Config server


Type of request - 0x03000000. Known queries:


CDRs and versions are given as BLOB files.

Cdr

The command is 0x02 to get the file or 0x09 to check for its update. In the case of an update, the request is followed by 20 bytes of the SHA-1 hash for the existing file (or 0x00 if it is not). The answer is a “raw” CDR file, which is simply saved to disk and later used by the client.

Client version

Team - 0x01.

Formation BLOB'a server side
  case ACTION_GET_VERSIONS_BLOB: #ifdef LOG Log(Client->ServerName, "Client %s - Sending Versions Blob", ClientAddr); #endif blob = new CBLOBFile(); rootNode = blob->RootNode(); rootNode->AddString("\x00\x00\x00\x00", 4, "\x00\x00\x00\x00", 4); rootNode->AddData("\x01\x00\x00\x00", 4, (char*)&SteamVersion, 4); rootNode->AddData("\x02\x00\x00\x00", 4, (char*)&SteamUIVersion, 4); rootNode->AddString("\x03\x00\x00\x00", 4, "\x00\x00\x00\x00", 4); rootNode->AddString("\x04\x00\x00\x00", 4, "\x14\x00\x00\x00", 4); rootNode->AddString("\x05\x00\x00\x00", 4, "\x17\x00\x00\x00", 4); rootNode->AddString("\x06\x00\x00\x00", 4, "\x0e\x00\x00\x00", 4); rootNode->AddString("\x07\x00\x00\x00", 4, "boo\x00", 4); //rootNode->AddString("\x08\x00\x00\x00", 4, "\x5c\x01\x00\x00", 4); rootNode->AddString("\x09\x00\x00\x00", 4, "foo\x00", 4); rootNode->AddString("\x0a\x00\x00\x00", 4, "\x11\x00\x00\x00", 4); rootNode->AddString("\x0b\x00\x00\x00", 4, "bar\x00", 4); rootNode->AddString("\x0c\x00\x00\x00", 4, "\x12\x00\x00\x00", 4); rootNode->AddString("\x0d\x00\x00\x00", 4, "foo\x00", 4); rootNode->AddString("\x0e\x00\x00\x00", 4, "", 0); rootNode->AddString("\x0f\x00\x00\x00", 4, "\x50\x01\x00\x00", 4); ReplySize = blob->SaveToMem(&reply, false); delete blob; Socket->SendInt32(ReplySize, true); Socket->Send(reply, ReplySize); break; 


As can be seen from this code, the record contains a lot of service data, the purpose of which is not clear, but they were constant over a long period of time. Strings with the latest version of the client itself and the UI package for it are transferred from the variables.

Network keys

Team - 0x04. It has several non-standard data exchange protocol - the size of the response from the server is 2 bytes in size instead of 4 for the remaining answers. Server response contains:


Unknown request

Team 0x07. The server response contains the constant 9 bytes - \ x00 \ x01 \ x31 \ x2d \ x00 \ x00 \ x00 \ x01 \ x2c.

Authentication Server


The most interesting and difficult to open the protocol server. The main feature is the use of a very non-standard time measurement system - in nanoseconds from PX . Initial data:


Jenkins’s hash function is compiled by user name:

Function source code
 procedure mix(var a, b, c: uint32); inline; begin dec(a, b); dec(a, c); a:=a xor (c shr 13); dec(b, c); dec(b, a); b:=b xor (a shl 8); dec(c, a); dec(c, b); c:=c xor (b shr 13); dec(a, b); dec(a, c); a:=a xor (c shr 12); dec(b, c); dec(b, a); b:=b xor (a shl 16); dec(c, a); dec(c, b); c:=c xor (b shr 5); dec(a, b); dec(a, c); a:=a xor (c shr 3); dec(b, c); dec(b, a); b:=b xor (a shl 10); dec(c, a); dec(c, b); c:=c xor (b shr 15); end; function jenkinsLookupHash2(Data: pByte; Length: integer; InitVal: uint32): uint32; var a, b, c, len: uint32; begin len:=Length; a:=$9e3779b9; b:=a; c:=InitVal; while (len>=12) do begin inc(a, Data[0] + (Data[1] shl 8) + (Data[2] shl 16) + (Data[3] shl 24)); inc(b, Data[4] + (Data[5] shl 8) + (Data[6] shl 16) + (Data[7] shl 24)); inc(c, Data[8] + (Data[9] shl 8) + (Data[10] shl 16) + (Data[11] shl 24)); mix(a, b, c); Data:=pByte(@Data[12]); dec(len, 12); end; inc(c, length); if len>=11 then inc(c, Data[10] shl 24); if len>=10 then inc(c, Data[9] shl 16); if len>=9 then inc(c, Data[8] shl 8); if len>=8 then inc(b, Data[7] shl 24); if len>=7 then inc(b, Data[6] shl 16); if len>=6 then inc(b, Data[5] shl 8); if len>=5 then inc(b, Data[4]); if len>=4 then inc(a, Data[3] shl 24); if len>=3 then inc(a, Data[2] shl 16); if len>=2 then inc(a, Data[1] shl 8); if len>=1 then inc(a, Data[0]); mix(a, b, c); result:=c; end; 


General algorithm of interaction with the server:


The authentication confirmation byte can take the following states:

0x00 - input successfully completed;
0x01 - account does not exist;
0x02 - the account does not exist or the password is incorrect;
0x03 - too big difference in client and server time;
0x04 - account is blocked.

If the login was made , then a packet with user data is received from the server (we will consider further). The package with the user name consists of the following fields:


Data preparation for authentication package:

  1. Calculate the hash for the data block represented by the first 4 bytes of the salt, the user password, and the last 4 bytes of the salt;
  2. Calculate the hash for a block of data from the external and local IP addresses of the client;
  3. We form a data block from the current time (ns with PX !!!), local IP-address and 4 bytes \ x04 \ x04 \ x04 \ x04;
  4. The first 8 bytes of the packet with p.3 xor'im with the data from p.2;
  5. We encrypt the data block from step 4 with the AES-CBC algorithm using the key (data from item 1) and the initialization vector (any data).

Authentication Package:


The server response with these users has the following format:


Consider the structures used and their fields.

 TTicket_SubHeader = packed record nullData1: uint16; outerIV: array[0..15] of byte; nullData2: uint16; nullData3: uint16; EncrData: array[0..63] of byte; TicketLen: uint16; end; 

The EncrData field contains the data that needs to be decrypted using the AES-CBC algorithm using the key (claim 1 of the preparation of authentication data) and the TTicket_SubHeader.outerIV initialization vector. At the output, we get a header UserHeader type TTicket_UserHeader.

 TTicket_UserHeader = packed record InnerKey: array[0..15] of byte; Dummy1: uint16; SteamID: uint64; Servers: packed record IP1: uint32; Port1: uint16; IP2: uint32; Port2: uint16; end; CurrentTime: uint64; ExpiredTime: uint64; Dummy2: array[0..9] of byte; end; 

The purpose of all the fields is clear from the title, but incomprehensible to me and most incomprehensible. In the end, most of these data is purely intuitively named, based on their further use. The InnerKey field will be used later.

 TTicketHeader = record SZ1, SZ2: uint16; end; 

 TTicket_TestData = packed record len: uint16; //always $1000 SteamID: uint64; ExternalIP: uint32; end; 

 TTicket_BLOBHeader = packed record NodeHeader: uint16; Len2: uint32; ZerosSize: uint32; BLOBLen: uint32; InnerIV: array[0..15] of byte; end; 

As mentioned earlier, after this header is a block of data with an encrypted BLOB file. It is encrypted with the AES-CBC algorithm using the UserHeader.InnerKey key and the TTicket_BLOBHeader.InnerIV initialization vector .

Content Lists Server


Stores lists of content servers for various files. Type of request - 0x0200000000. Has 2 requests. Differing only in the second and third bytes of the command:


The general command format for this server is:


The server response contains a list of the following items:

 TContentListEntry = packed record ID: uint32; //   ??? ClientUpdateIP: uint32; ClientUpdatePort: uint16; ContentServerIP: uint32; ContentServerPort: uint16; end; 

The ID field changed for the same servers, from which I concluded that this is the load on this server. This is followed by two pairs of IP: port, which are almost always the same. Why 2 pairs - no idea.

Content Server


The most difficult to interact server that stores directly the content of the games and the files of Steam itself. It has its own protocol and handles 2 requests:


Consider the download of the service archive:


The file request and the signature are made by the file name (for the signature it is obtained "<file name> _rsa_signature"):


The answer to each such request is the requested file.

The source code of the described algorithm
 function TSteamNetwork.Content_DownloadPackage(Name: AnsiString; FileName: string): ENetWorkResult; var Accepted: boolean; Sock: CSocket; PacketSize, Request, MessSize: uint32; Data, Mess: pByte; str: TStream; Addr: TSockAddr; procedure ProcPackage(N, FN: AnsiString); begin PacketSize:=htonl(4+8+Length(N)+4); if not Sock.Send(PacketSize, 4) then Exit; if not Sock.Send(CS_PACKAGE_GET_FILE, 4) then Exit; Request:=0; if not Sock.Send(Request, 4) then Exit; Request:=htonl(Length(N)); if not Sock.Send(Request, 4) then Exit; if not Sock.Send(N[1], Length(N)) then Exit; Request:=0; if not Sock.Send(Request, 4) then Exit; if not Sock.Recv(PacketSize, 4) then Exit; Data:=Sock.RecvFromLen(PacketSize); end; begin result:=eConnectionError; Addr:=ContentList_GetContentServer(); if Addr.sin_addr.S_addr=0 then Exit; Sock:=CSocket.Create(SOCKET_IP); Sock.SetTimeOut(3000); if (Sock=nil) or (not Sock.Connect(Addr)) then Exit; if not Sock.Send(CS_PACKAGE_QUERY, 4) then Exit; if not Sock.Recv(Accepted, 1) then Exit; if not Accepted then begin result:=eServerReset; Exit; end; ProcPackage(Name, Wide2Ansi(FileName)); MessSize:=PacketSize; Mess:=Data; ProcPackage(Name+'_rsa_signature', ''); Sock.Free; if RSACheckSign(NetWorkKeySign, Data, Mess, MessSize, 128) then begin str:=TStream.CreateWriteFileStream(FileName); str.Write(Mess^, MessSize); str.Free; result:=eOK; end else result:=eSignError; FreeMem(Mess, MessSize); FreeMem(Data, 128); end; 


Downloading the game archive is much more complicated and goes through many stages:


Receive 1 part file:

  1. Accept 4 bytes CacheID ;
  2. Accept 4 bytes MessageID ;
  3. Accept 4 bytes of the size of the part;
  4. Accept 4 bytes CacheID ;
  5. Accept 4 bytes MessageID ;
  6. Accept 4 bytes of block size;
  7. Accept a block of the specified size;
  8. We turn to step 4, while the size of the received blocks is less than the size of the part.

The source code of the described algorithm
 function TSteamNetwork.Content_DownloadGCF(AppID, Version: uint32): ENetWorkResult; var Accepted: boolean; Sock: CSocket; i: integer; ConnID, MessageID, MsgID, BlockSize, CacheID, ManifestCheck: uint32; ManifestSize, ChecksumSize, PS: uint32; Manifest, Checksum: pByte; UpdateList: puint32; str: TStream; GCF: TGCFFile; q: array[0..HL_GCF_CHECKSUM_LENGTH*2] of byte; Addr: TSockAddr; function RecvPacket(var Size: uint32): pByte; var Pos, recived: uint32; begin result:=nil; if not Sock.Recv(CacheID, 4) then Exit; if not Sock.Recv(MsgID, 4) then Exit; if not Sock.Recv(Accepted, 1) then Exit; if Accepted then Exit; if not Sock.Recv(Size, 4) then Exit; if not Sock.Recv(CacheID, 4) then Exit; if not Sock.Recv( MsgID, 4) then Exit; if not Sock.Recv(BlockSize, 4) then Exit; Pos:=0; Size:=htonl(Size); BlockSize:=htonl(BlockSize); GetMem(result, Size); repeat recived:=Sock.Recvi(pByte(result+Pos)^, BlockSize); inc(Pos, recived); until (Pos>=Size) or (recived=0); end; function GetBannerURL(): boolean; var URL: pAnsiChar; Len: uint16; begin result:=false; FillChar(Q[0], 9, 0); Q[0]:=CS_STORAGE_BANNER_URL; Sock.SendFromLen(5, @Q[0]); if not Sock.Recv(Accepted, 1) then Exit; URL:=pAnsiChar(Sock.RecvFromLenShort(Len)); //URL:=pAnsiChar(URL+#0); Writeln('Banner URL: "'+URL+'"'); FreeMem(URL, Len); result:=true; end; function Open(): boolean; begin result:=false; AppID:=htonl(AppID); Version:=htonl(Version); FillChar(Q[0], 17, 0); Q[0]:=CS_STORAGE_OPEN; Move(ConnID, Q[1], 4); Move(MessageID, Q[5], 4); Move(AppID, Q[9], 4); Move(Version, Q[13], 4); if not Sock.SendFromLen(17, @Q[0]) then Exit; if not Sock.Recv(ConnID, 4) then Exit; if not Sock.Recv(MsgID, 4) then Exit; if not Sock.Recv(Accepted, 1) then Exit; if Accepted then Exit; if not Sock.Recv(CacheID, 4) then Exit; if not Sock.Recv(ManifestCheck, 4) then Exit; AppID:=htonl(AppID); Version:=htonl(Version); result:=true; end; function OpenEx(): boolean; begin //result:=false; result:=true; end; function GetManifest(): boolean; begin result:=false; FillChar(Q[0], 9, 0); Q[0]:=CS_STORAGE_GET_MANIFEST; Move(CacheID, Q[1], 4); Move(MessageID, Q[5], 4); if not Sock.SendFromLen(9, @Q[0]) then Exit; Manifest:=RecvPacket(ManifestSize); result:=(Manifest<>nil); inc(MessageID); end; function GetChecksum(): boolean; begin result:=false; FillChar(Q[0], 9, 0); Q[0]:=CS_STORAGE_GET_CHECKSUM; Move(CacheID, Q[1], 4); Move(MessageID, Q[5], 4); if not Sock.SendFromLen(9, @Q[0]) then Exit; Checksum:=RecvPacket(ChecksumSize); result:=(Checksum<>nil); inc(MessageID); end; function GetListUpdateFiles(): boolean; var r: byte; Count: uint32; begin result:=false; FillChar(Q[0], 13, 0); Q[0]:=CS_STORAGE_GET_LIST_UPDATE_FILES; Move(CacheID, Q[1], 4); Move(MessageID, Q[5], 4); Move(#0#0#0#0, Q[9], 4); Sock.SendFromLen(13, @Q[0]); if not Sock.Recv(CacheID, 4) then Exit; if not Sock.Recv(MsgID, 4) then Exit; if not Sock.Recv(r, 1) then Exit; if not Sock.Recv(Count, 4) then Exit; if Count=0 then Exit; if not Sock.Recv(CacheID, 4) then Exit; if not Sock.Recv(MsgID, 4) then Exit; UpdateList:=puint32(Sock.RecvFromLen(PS)); str:=TStream.CreateWriteFileStream('.\package\7.diff'); str.Write(UpdateList^, PS); str.Free; result:=true; inc(MessageID); end; function RecvChunk(var Size: uint32): pByte; //inline; var len, recvd: uint32; begin result:=nil; Size:=0; if not Sock.Recv(CacheID, 4) then Exit; if not Sock.Recv(MsgID, 4) then Exit; if not Sock.Recv(Size, 4) then Exit; Size:=htonl(Size); len:=0; GetMem(result, Size); repeat if not Sock.Recv( CacheID, 4) then Exit; if not Sock.Recv(MsgID, 4) then Exit; if not Sock.Recv(BlockSize, 4) then Exit; BlockSize:=htonl(BlockSize); write(BlockSize); recvd:=0; repeat inc(recvd, Sock.Recvi(pByte(result+len)^, BlockSize)); until recvd=BlockSize; if recvd=uint32(SOCKET_ERROR) then break; inc(len, recvd); until len>=Size; inc(MessageID); end; function GetFile(Idx: uint32): ENetWorkResult; var Start, Count, i: integer; FileIdx, IsCompressed, ChunkSize, UncSize: uint32; Chunk: pByte; begin result:=eConnectionError; str:=GCF.OpenFile(Idx, ACCES_WRITE); Start:=0; Count:=GCF.ItemSize[Idx].Size div HL_GCF_CHECKSUM_LENGTH; //    = HL_GCF_CHECKSUM_LENGTH if GCF.ItemSize[Idx].Size mod HL_GCF_CHECKSUM_LENGTH>0 then inc(Count); FileIdx:=htonl(GCF.CheckIdx(Idx)); Start:=htonl(Start); Count:=htonl(Count); FillChar(Q[0], 22, 0); Q[0]:=CS_STORAGE_GET_FILE; Move(CacheID, Q[1], 4); Move(MessageID, Q[5], 4); Move(FileIdx, Q[9], 4); Move(Start, Q[13], 4); Move(Count, Q[17], 4); Q[21]:=$00; if not Sock.SendFromLen(22, @Q[0]) then Exit; if not Sock.Recv(CacheID, 4) then Exit; if not Sock.Recv(MsgID, 4) then Exit; if not Sock.Recv(Accepted, 1) then Exit; if Accepted then Exit; if not Sock.Recv(Count, 4) then Exit; if not Sock.Recv(IsCompressed, 4) then Exit; Count:=htonl(Count); IsCompressed:=htonl(IsCompressed); result:=eOK; for i:=0 to Count-1 do begin Chunk:=RecvChunk(ChunkSize); UncSize:=HL_GCF_CHECKSUM_LENGTH; if (IsCompressed=1) then begin // zipped if uncompress(@q[0], UncSize, Chunk, ChunkSize)<>0 then begin result:=eZLibError; break; end; str.Write(q[0], UncSize); end else if (IsCompressed=2) then begin writeln(HL_GCF_CHECKSUM_LENGTH-ChunkSize); str.Write(Chunk^, ChunkSize); FillChar(q[0], HL_GCF_CHECKSUM_LENGTH, 0); str.Write(q[0], HL_GCF_CHECKSUM_LENGTH-ChunkSize); end else str.Write(Chunk^, ChunkSize); FreeMem(Chunk, ChunkSize); {$IFDEF DEBUG_CS_SLEEP} sleep(300); {$ENDIF} end; if (IsCompressed=2) and (CDR<>nil) then begin // encrypted (and zipped?) //GCF.DecryptItem(Idx, CDR.AppRecord[AppID].DecryptKey(Version)); end; str.Free; end; function Close(): boolean; begin result:=false; FillChar(Q[0], 9, 0); Q[0]:=CS_STORAGE_CLOSE; Move(CacheID, Q[1], 4); Move(MessageID, Q[5], 4); Sock.SendFromLen(9, @Q[0]); if not Sock.Recv(CacheID, 4) then Exit; if not Sock.Recv(MsgID, 4) then Exit; if not Sock.Recv(Accepted, 1) then Exit; result:=true; end; begin result:=eConnectionError; Addr:=ContentList_GetContentServer(AppID, Version, REGION_Rest_World); if Addr.sin_addr.S_addr=0 then Exit; Sock:=CSocket.Create(SOCKET_IP); Sock.SetTimeOut(3000); if (Sock=nil) or (not Sock.Connect(Addr)) then Exit; if not Sock.Send(CS_STORAGE_QUERY, 4) then Exit; if not Sock.Recv(Accepted, 1) then Exit; if not Accepted then begin result:=eServerReset; Exit; end; ConnID:=0; MessageID:=0; writeln('Get banner URL'); if not GetBannerURL() then begin Sock.Free; Exit; end; writeln('Open'); if not Open() then begin Sock.Free; Exit; end; writeln('Get manifest'); if not GetManifest() then begin Sock.Free; Exit; end; writeln('Get checksums'); if not GetChecksum() then begin Sock.Free; Exit; end; //RSASignMessage(NetWorkKeySign, Checksum, ChecksumSize-128); {if not GetListUpdateFiles() then begin closesocket(Sock); Exit; end;} GCF:=TGCFFile.Create('.\storage\common\'+Int2Str(AppID)); GCF.LoadFromMem(Manifest, Checksum, ManifestSize, ChecksumSize, false); GCF.SaveToFile('.\storage\'+Int2Str(AppID)+'.ncf'); for i:=0 to GCF.ItemsCount-1 do if (GCF.IsFile(i)) and (GCF.GetCompletion(i)<1) then begin Writeln(GCF.ItemPath[i]); {$IFDEF DEBUG_CS_SLEEP} sleep(100); {$ENDIF} if GetFile(i)<>eOK then break; end; GCF.Free; Close(); Sock.Free; str:=TStream.CreateWriteFileStream('.\storage\'+Int2Str(AppID)+'.manifest'); str.Write(Manifest^, ManifestSize); str.Free; FreeMem(Manifest, ManifestSize); str:=TStream.CreateWriteFileStream('.\storage\'+Int2Str(AppID)+'.checksum'); str.Write(Checksum^, ChecksumSize); str.Free; FreeMem(Checksum, ChecksumSize); result:=eOK; end; 


Conclusion


That came to an end the cycle of articles on the outdated part of Steam The only thing that is still actively used is VDF-archives.

In the next article I can touch on more relevant information - SteamAPI (steam.dll) and SteamClienAPI (steamclient.dll). And if the second is considered from the side of receiving information about the user within the limits of the permitted, then the first will be considered from the side of the simplest emulator of this API. The decision whether or not to write about it is to the community.

Source: https://habr.com/ru/post/269061/


All Articles