サイクルの最後の記事、最も興味深く、最もボリュームのあるもの:
この記事では、さまざまなサーバーとの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アドレスのリストです。
- 4バイトのリストの長さ。
- IP:port(4 + 2バイト)の形式のN個のリスト項目。
既知のクエリとそのコマンド:
- 構成サーバーのリスト-\ x00;
- 認証サーバーのリスト-\ x00 \ xC4 \ x1D \ x1A \ x00;
- ルートコンテンツサーバーのリスト-\ x06;
- CSERサーバーのリストは\ x14です。
どのようなCSERサーバーを理解していなかったので、他の場所では言及しません。
構成サーバー
要求タイプは0x03000000です。 既知のクエリ:
- CDR ;
- クライアントバージョン。
- ネットワークキー;
- 不明なリクエスト。
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バイトです。 サーバー応答には以下が含まれます。
- タイトル-\ x30 \ x81 \ x9d \ x30 \ x0d \ x06 \ x09 \ x2a \ x86 \ x48 \ x86 \ xf7 \ x0d \ x01 \ x01 \ x01 \ x05 \ x00 \ x03 \ x81 \ x8b \ x00 \ x30 \ x81 \ x87 \ x02 \ x81 \ x81 \ x00;
- NetworkKeyの通常の部分。
- Data \ x02 \ x01 \ x11(最後のバイトは、明らかにNetworkKeyの指数部です。
- MainKeySignを使用した256バイトのRSA署名。 署名のサイズは含まれていませんが、パッケージのサイズです!
不明なリクエスト
コマンド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;
一般的なサーバー相互作用アルゴリズム:
- 認証リクエストを送信します-5バイト\ x00 \ x00 \ x00 \ x00 \ x04;
- ローカルIPアドレスを送信します。
- ユーザー名のハッシュを送信します。
- 接続確認フラグを受け入れます。 このような名前のハッシュを持つユーザーがサーバーデータベースに存在しない場合はfalseです。
- クライアントの外部IPアドレスを受け入れます。
- ユーザー名を含むパッケージを送信します(構成については後で説明します)。
- 暗号化には「塩」を使用できます(8バイト)。
- 認証パッケージを作成します(さらに検討します)。
- 認証確認バイトを受け入れます。
- 8バイトのサーバー時間を受け入れます(PXではナノ秒を忘れないでください!)。
- 8バイトのパケット有効期間を受け入れます。
認証確認バイトは、次の状態を取ることができます。
0x00-ログインは正常に完了しました。
0x01-アカウントは存在しません。
0x02-アカウントが存在しないか、パスワードが正しくありません。
0x03-クライアントとサーバー間の時間差が大きすぎる。
0x04-アカウントはブロックされています。
ログインが行われた場合、ユーザーデータを含むパケットがサーバーから受信されます(さらに検討します)。 ユーザー名を持つパッケージは、次のフィールドで構成されます。
- パケットサイズ(4バイト);
- 1バイト0x02;
- 名前の長さ(2バイト);
- ユーザー名
- 名前の長さ(2バイト);
- ユーザー名
認証パッケージのデータの準備:
- 「salt」の最初の4バイト、ユーザーのパスワード、および「salt」の最後の4バイトで表されるデータブロックのハッシュを計算します。
- クライアントの外部およびローカルIPアドレスからデータブロックのハッシュを計算します。
- 現在の時間(PXでns !!!)、ローカルIPアドレス、および4バイトのデータブロック\ x04 \ x04 \ x04 \ x04を形成します。
- 項目3 xorまたは項目2のデータを含むパケットの最初の8バイト。
- キー(請求項1のデータ)と初期化ベクトル(任意のデータ)を使用するAES-CBCアルゴリズムを使用して、請求項4のデータブロックを暗号化します。
認証パッケージの構成:
- パッケージサイズ-定数、0x00000036;
- 初期化ベクトル(データ生成中);
- 4バイトの定数-\ x00 \ x0C \ x00 \ x10。 値から判断すると、これらは16ビットのデータサイズ(IVおよび暗号化された部分)です。
- 暗号化されたデータ。
これらのユーザーを使用したサーバーの応答の形式は次のとおりです。
- タイトルTTicket_SubHeader;
- タイトルTTicketHeader;
- 初期化ベクトルfirstIV ;
- サイズTTicketHeader.SZ2のデータブロック。
- 2番目のヘッダーTTicketHeader(2);
- サイズTTicketHeader(2).SZ2の2番目のデータブロック。
- タイトルTTicket_TestData;
- TicketSignデータ。
- タイトルTTicket_BLOBHeader;
- blob自体はTTicket_BLOBHeader.Len2-20-sizeof(TTicket_BLOBHeader)です。
- パッケージの署名。
使用される構造とそのフィールドを考慮してください。
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番目のバイトのみが異なる:
- アーカイブのサーバーのリスト-0x0000;
- サービスアーカイブのサーバーのリストは0x0100です。
このサーバーの一般的なコマンド形式は次のとおりです。
- 1バイト-0x00;
- 2バイトのコマンドの改良(\ x00 \ x00または\ x0100);
- 4バイト-要求されたアーカイブのID。
- 4バイト-要求されたアーカイブのバージョン。
- 2バイト-応答内のサーバーの最大数。
- 4バイト-リージョン。
- 4バイト-0xFF。
サーバー応答には、次の要素のリストが含まれます。
TContentListEntry = packed record ID: uint32; // ??? ClientUpdateIP: uint32; ClientUpdatePort: uint16; ContentServerIP: uint32; ContentServerPort: uint16; end;
同じサーバーのIDフィールドが変更されたため、これがこのサーバーの負荷であると結論付けました。 次に、2組のIP:ポートがあります。これらはほとんど常に一致しています。 なぜ2ペア-わからない。
コンテンツサーバー
ゲームのコンテンツとSteam自体のファイルを直接保存し、相互作用が最も難しいサーバー。 独自のプロトコルを持ち、2つの要求を処理します。
- サービスアーカイブ(クライアントファイル)をダウンロードします。
- ゲームアーカイブをダウンロードします。
サービスアーカイブの読み込みを検討してください。
- コマンドを送信します-\ x03 \ x00 \ x00 \ x00;
- 接続フラグを受け入れます。
- 要求されたファイルを受け入れます。
- ファイルのRSA署名を受け入れます。
ファイルと署名の要求は、ファイルの名前によって行われます(署名の場合は「<ファイル名> _rsa_signature」が取得されます)。
- 4バイト-パケットサイズ(ファイル名の長さ+ 16);
- 4バイト-コマンド\ x00 \ x00 \ x00 \ x00;
- 4バイト-\ x00 \ x00 \ x00 \ x00;
- 4バイト-ファイル名の長さ。
- ファイル名自体。
そのような各要求に対する答えは、要求されたファイルです。
説明されているアルゴリズムのソースコード
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;
ゲームアーカイブのダウンロードははるかに複雑で、多くの段階を経ます。
- コマンド\ x07 \ x00 \ x00 \ x00を送信します。
- 接続フラグを受け入れます。
- バナーを受信するためにコマンドの5バイトを送信します-\ x00 \ x00 \ x00 \ x00 \ x00;
- 確認フラグを取得します。
- 文字列の長さを受け入れます。
- リンクのある行を受け入れます。
- アーカイブを開くコマンドを送信します-\ x09;
- 8バイト\ x00を送信します。
- 要求されたアーカイブの4バイトのIDを送信します。
- 要求されたアーカイブのバージョンの4バイトを送信します。
- 4バイトの接続IDを受け入れます 。
- 4バイトのMessageIDを受け入れます 。
- 確認フラグを受け入れます。
- 4バイトのCacheIDを受け入れます。
- 4バイトのManifestCheckを受け入れます。
- マニフェストを受信するコマンドを送信します-\ x04;
- 4バイトのCacheIDを送信します。
- 4バイトのMessageIDを送信します 。
- マニフェスト( GCF / NCFアーカイブのヘッダーの一部)を持つデータブロックを受け入れます。
- チェックサムを受け取るコマンドを送信します-\ x06;
- 4バイトのCacheIDを送信します。
- 4バイトのMessageIDを送信します 。
- チェックサム( GCF / NCFアーカイブのヘッダーの一部)を含むデータブロックを受け入れます。
- ファイルを受信するコマンドを送信します-\ x07;
- 4バイトのCacheIDを送信します。
- 4バイトのMessageIDを送信します 。
- 4バイトを送信します-アーカイブ内のファイルのインデックス(0から始まります);
- 4バイトを送信します-最初に必要な部分の数(1つの部分のサイズは0x00002000です- 前述のセクターサイズに従って);
- 4バイト-要求されたパーツの数を送信します。
- 4バイトのCacheIDを受け入れます。
- 4バイトのMessageIDを受け入れます 。
- 確認フラグを受け入れます。
- ユーザーに送られた部品の数を受け入れます。
- 指定された数の部品を受け入れます(さらに検討します)。
- ファイルを閉じるコマンドを送信します-\ x03;
- 4バイトのCacheIDを受け入れます。
- 4バイトのMessageIDを受け入れます 。
- 確認フラグを受け入れます。
- 接続を閉じます。
ファイルの一部の受信:
- 4バイトのCacheIDを受け入れます。
- 4バイトのMessageIDを受け入れます 。
- 4バイトのパーツサイズを受け入れます。
- 4バイトのCacheIDを受け入れます。
- 4バイトのMessageIDを受け入れます 。
- 4バイトのブロックサイズを受け入れます。
- 指定されたサイズのブロックを受け入れます。
- ステップ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の最も単純なエミュレーター側から考慮されます。それについて書くかどうかの決定はコミュニティ次第です。