Steamプロトコルv2

スチームロゴ



サイクルの最後の記事、最も興味深く、最もボリュームのあるもの:





この記事では、さまざまなサーバーとのSteamクライアントデータ交換プロトコルについて説明します。





繰り返しますが、問題のプロトコルは古く、現在使用されていません(互換性のためにGDSとConfigを除く)。



すべてのアルゴリズムは私のリポジトリに表示されます



プロトコル全体はソケットに基づいているため、WireSharkはほぼすべての場所で解析できました。 プロトコルの説明全体がクライアントによって検討されます(Delphiで)。 場所には、C ++のサーバーコードのセクションが与えられます。



ほとんどすべてのリクエストには共通のアルゴリズムがあります
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;
      
      







このプロトコルは、すべてのリストサーバー(GD、Config、ContentList)へのクエリに使用されます。 QUERYパラメーターでは、要求のタイプを表すバイトの配列へのポインターが渡され、QSizeではこの配列のサイズが渡されます。 Commandパラメーターでは、コマンド自体を含むバイト配列へのポインターが渡され、CSizeではこの配列のサイズが渡されます。 ReplySize変数には、要求された応答のサイズが含まれ、呼び出し後、実際に受信したデータの量と等しくなります。 上記のコードは、次の擬似コードで表すことができます。



               IP-,     Config Server'    ,            
      
      





場合によっては、次のアルゴリズムによって取得されたRSA署名が使用されます。



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); }
      
      









以前に署名に使用したキー
 // 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"
      
      







汎用ディレクトリサーバー



インフラストラクチャ全体のルートであり、他のサーバーのアドレスのみを保存します。 要求タイプは0x02000000です(ConectToServerを呼び出すときに使用されます)。 答えは、要求されたサーバーIPアドレスのリストです。





既知のクエリとそのコマンド:





どのようなCSERサーバーを理解していなかったので、他の場所では言及しません。



構成サーバー



要求タイプは0x03000000です。 既知のクエリ:





CDRとバージョンはBLOBファイルとして提供されます。



CDR


コマンドは、ファイルを受信する場合は0x02、更新を確認する場合は0x09です。 更新の場合、要求後、SHA-1ハッシュの20バイトは既存のファイル用です(そうでない場合は0x00)。 答えは、ディスクに保存され、後でクライアントによって使用されるCDRを含む「生の」ファイルです。



クライアントバージョン


チームは0x01です。



サーバー側でのBLOB'aの形成
  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;
      
      







このコードからわかるように、レコードには多くのオーバーヘッドが含まれており、その目的は明確ではありませんが、長い間一定でした。 クライアントの最新バージョンとそのUIパッケージを含む文字列は、変数から転送されます。



ネットワークキー


チームは0x04です。 これはやや非標準のデータ交換プロトコルを備えています-サーバーからの応答のサイズは、残りの応答の4バイトではなく2バイトです。 サーバー応答には以下が含まれます。





不明なリクエスト


コマンド0x07。 サーバー応答には、9バイトの定数が含まれます-\ x00 \ x01 \ x31 \ x2d \ x00 \ x00 \ x00 \ x01 \ x2c



認証サーバー



プロトコルを開くための最も面白くて難しいサーバー。 主な機能は、 PXからのナノ秒単位の非常に非標準の時間測定システムの使用です。 ソースデータ:





ユーザー名によって、Jenkins Hash関数がコンパイルされます:



関数ソースコード
 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;
      
      







一般的なサーバー相互作用アルゴリズム:





認証確認バイトは、次の状態を取ることができます。



0x00-ログインは正常に完了しました。

0x01-アカウントは存在しません。

0x02-アカウントが存在しないか、パスワードが正しくありません。

0x03-クライアントとサーバー間の時間差が大きすぎる。

0x04-アカウントはブロックされています。



ログインが行われた場合、ユーザーデータを含むパケットがサーバーから受信されます(さらに検討します)。 ユーザー名を持つパッケージは、次のフィールドで構成されます。





認証パッケージのデータの準備:



  1. 「salt」の最初の4バイト、ユーザーのパスワード、および「salt」の最後の4バイトで表されるデータブロックのハッシュを計算します。
  2. クライアントの外部およびローカルIPアドレスからデータブロックのハッシュを計算します。
  3. 現在の時間(PXでns !!!)、ローカルIPアドレス、および4バイトのデータブロック\ x04 \ x04 \ x04 \ x04を形成します。
  4. 項目3 xorまたは項目2のデータを含むパケットの最初の8バイト。
  5. キー(請求項1のデータ)と初期化ベクトル(任意のデータ)を使用するAES-CBCアルゴリズムを使用して、請求項4のデータブロックを暗号化します。


認証パッケージの構成:





これらのユーザーを使用したサーバーの応答の形式は次のとおりです。





使用される構造とそのフィールドを考慮してください。



 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;
      
      





EncrDataフィールドには、キー(認証データの準備からの項目1)と初期化ベクトルTTicket_SubHeader.outerIVを使用して、AES-CBCアルゴリズムによって復号化する必要があるデータが含まれています。 出力は、 TTicket_UserHeader型の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;
      
      





すべての分野の目的は名前から明らかですが、私と私には理解不可能です。 最終的に、これらのデータのほとんどは、それらのさらなる用途に基づいて、純粋に直感的に命名されます。 InnerKeyフィールドは後で使用されます。



 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;
      
      





前述のように、このヘッダーの後には、暗号化されたBLOBファイルを持つデータブロックがあります。 UserHeader.InnerKeyキーとTTicket_BLOBHeader.InnerIV初期化ベクトルを使用して、AES-CBCアルゴリズムで暗号化されます。



コンテンツリストサーバー



さまざまなファイルのコンテンツサーバーリストを格納します。 要求タイプは0x0200000000です。 2つのリクエストがあります。 コマンドの2番目と3番目のバイトのみが異なる:





このサーバーの一般的なコマンド形式は次のとおりです。





サーバー応答には、次の要素のリストが含まれます。



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





同じサーバーのIDフィールドが変更されたため、これがこのサーバーの負荷であると結論付けました。 次に、2組のIP:ポートがあります。これらはほとんど常に一致しています。 なぜ2ペア-わからない。



コンテンツサーバー



ゲームのコンテンツとSteam自体のファイルを直接保存し、相互作用が最も難しいサーバー。 独自のプロトコルを持ち、2つの要求を処理します。





サービスアーカイブの読み込みを検討してください。





ファイルと署名の要求は、ファイルの名前によって行われます(署名の場合は「<ファイル名> _rsa_signature」が取得されます)。





そのような各要求に対する答えは、要求されたファイルです。



説明されているアルゴリズムのソースコード
 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;
      
      







ゲームアーカイブのダウンロードははるかに複雑で、多くの段階を経ます。





ファイルの一部の受信:



  1. 4バイトのCacheIDを受け入れます。
  2. 4バイトのMessageIDを受け入れます
  3. 4バイトのパーツサイズを受け入れます。
  4. 4バイトのCacheIDを受け入れます。
  5. 4バイトのMessageIDを受け入れます
  6. 4バイトのブロックサイズを受け入れます。
  7. 指定されたサイズのブロックを受け入れます。
  8. ステップ4に進み、受信したブロックのサイズがパーツのサイズより小さくなっています。


説明されているアルゴリズムのソースコード
 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;
      
      







おわりに



そのため、Steamの時代遅れの部分に関する記事のサイクルは終わりました。 まだ積極的に使用されている唯一のものはVDFアーカイブです。



次の記事では、より関連性の高い情報-SteamAPI(steam.dll)およびSteamClienAPI(steamclient.dll)に触れます。そして、2番目が許可された範囲内でユーザーに関する情報を取得する側から考慮される場合、1番目はこのAPIの最も単純なエミュレーター側から考慮されます。それについて書くかどうかの決定はコミュニティ次第です。



All Articles