Завершающая статья цикла, самая интересная и самая объемная:
- Steam Protocol 2 и Steam Files — Введение
- Steam Files. Часть 1 — GCF/NCF
- Steam Files. Часть 2 — BLOB, CDR, VDF, PAK, VPK
В статье будут рассмотрены протоколы обмена данными клиента Steam с различными серверами:
- General Direcrory Server;
- Config Server;
- Authentication Server;
- Content Lists Server;
- Content Server.
В очередной раз напомню, что рассматриваемые протоколы устарели и в настоящее время не используются (за исключением GDS и Config — для совместимости).
Все алгоритмы представлены в моем репозитории.
Весь протокол базируется на Socket'ах, поэтому для его разбора практически везде хватало 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"
General Direcrory Server
Является корневым во всей инфраструктуре и только хранит адреса прочих серверов. Тип запроса — 0x02000000 (используется при вызове ConectToServer). Ответом является список IP-адресов серверов в соответствии с запросом:
- 4 байта длина списка;
- N элементов списка вида IP:port (4+2 байта).
Известные запросы и их команды:
- Список конфигурационных серверов — \x00;
- Список аутентификационных серверов — \x00\xC4\x1D\x1A\x00;
- Список корневых контент-серверов — \x06;
- Список CSER-серверов — \x14.
Что за CSER-сервера — я так и не понял, поэтому больше нигде не упоминается о них.
Config Server
Тип запроса — 0x03000000. Известные запросы:
- CDR;
- Версия клиента;
- Сетевые ключи;
- Неизвестный запрос.
CDR и версии отдаются в виде BLOB-файлов.
CDR
Команда — 0x02 для получения файла или 0x09 для проверки его обновления. В случае обновления после запроса следует 20 байт хэша SHA-1 для имеющегося файла (или 0x00, если его нет). Ответом является «сырой» файл с CDR, который просто сохраняется на диск и в дальнейшем используется клиентом.
Версия клиента
Команда — 0x01.
Формирование BLOB'а на стороне сервера
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. Имеет несколько нестандартный протокол обмена данными — размер ответа от сервера имеет размер 2 байта вместо 4-х для остальных ответов. Ответ сервера содержит:
- Заголовок — \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;
- Данные \x02\x01\x11 (последний байт из которого, видимо, является экспоненциальной частью NetworkKey;
- 256 байт RSA-подписи с использованием MainKeySign. Размер подписи не включается а размер пакета!
Неизвестный запрос
Команда 0x07. Ответ сервера содержит константные 9 байт — \x00\x01\x31\x2d\x00\x00\x00\x01\x2c.
Authentication Server
Самый интересный и сложный для вскрытия протокола сервер. Основной особенностью является использование очень нестандартной системы измерения времени — в наносекундах от РХ. Исходные данные:
- Имя пользователя;
- Пароль пользователя.
По имени пользователя составляется Хеш-функция Дженкинса:
Исходный код функции
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-адрес;
- Отправляем хэш имени пользователя;
- Принимаем флаг подтверждения соединения. Будет ложь, если пользователя с таким хэшем имени нет в БД сервера;
- Принимаем внешний IP-адрес клиента;
- Отправляем пакет с именем пользователя (состав будет рассмотрен далее);
- Принимаем «соль» для шифрования (8 байт);
- Формируем пакет аутентификации (рассмотрим далее);
- Принимаем байт подтверждения аутентификации;
- Принимаем 8 байт времени сервера (вспоминаем наносекунды с РХ!);
- Принимаем 8 байт срока действия пакета.
Байт подтверждения аутентификации может принимать следующие состояния:
0x00 — вход успешно выполнен;
0x01 — аккаунт не существует;
0x02 — аккаунт не существует или неверный пароль;
0x03 — слишком большая разница во времени клиента и сервера;
0x04 — аккаунт заблокирован.
Если вход был выполнен, то от сервера принимается пакет с данными пользователя (рассмотрим далее). Пакет с именем пользователя состоит из следующих полей:
- Размер пакета (4 байта);
- 1 байт 0x02;
- Длина имени (2 байта);
- Имя пользователя;
- Длина имени (2 байта);
- Имя пользователя.
Подготовка данных для пакета аутентификации:
- Рассчитываем хэш для блока данных, представленного первыми 4-мя байтами «соли», паролем пользователя и последними 4-мя байтами «соли»;
- Рассчитываем хэш для блока данных из внешнего и локального IP-адресов клиента;
- Формируем блок данных из текущего времени (нс с РХ!!!), локального IP-адреса и 4-х байт \x04\x04\x04\x04;
- Первые 8 байт пакета с п.3 xor'им с данными из п.2;
- Шифруем блок данных из п.4 алгоритмом AES-CBC с использованием ключа (данные из п.1) и вектора инициализации (любые данные).
Состав пакета аутентификации:
- Размер пакета — константа, 0x00000036;
- Вектор инициализации (при формировании данных);
- 4 байта константы — \x00\x0C\x00\x10. Судя по значениям — это 16-битные размеры данных (IV и зашифрованной части);
- Зашифрованные данные.
Ответ сервера с данными пользователями имеет следующий формат:
- Заголовок TTicket_SubHeader;
- Заголовок TTicketHeader;
- Вектор инициализации firstIV;
- Блок данных размером TTicketHeader.SZ2;
- Второй заголовок TTicketHeader(2);
- Второй блок данных размером TTicketHeader(2).SZ2;
- Заголовок 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 содержит данные, которые необходимо расшифровать алгоритмом AES-CBC с использованием ключа (п.1 из подготовки данных аутентификации) и вектора инициализации TTicket_SubHeader.outerIV. На выходе получим заголовок UserHeader типа 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;
Назначение всех полей понятно из названия, а непонятные мне и самому непонятны. В конце-концов, большинство этих данных названо чисто интуитивно, исходя из их дальнейшего применения. Поле 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-файлом. Зашифрован он алгоритмом AES-CBC с использованием ключа UserHeader.InnerKey и вектора инициализации TTicket_BLOBHeader.InnerIV.
Content Lists Server
Хранит списки контент-серверов для различных файлов. Тип запроса — 0x0200000000. Имеет 2 запроса. Отличающихся только вторым и третьим байтами команды:
- Список серверов для архива — 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 менялось для одних и тех же серверов, из чего я сделал вывод, что это нагрузка на данный сервер. Дальше следует две пары IP:port, которые почти всегда совпадали. Почему 2 пары — без понятия.
Content Server
Самый сложный для взаимодействия сервер, хранящий непосредственно контент игр и файлы самого 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;
- Принимаем флаг подтверждения;
- Закрываем соединение.
Прием 1 части файла:
- Принимаем 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). И если вторая будет рассматриваться со стороны получения информации о пользователе в рамках разрешенного, то первая будет рассматриваться со стороны простейшего эмулятора этого API. Решение о том, писать об этом или нет — принимать сообществу.
Only registered users can participate in poll. Log in, please.
Писать ли статьи о SteamAPI и SteamClientAPI?
84.05% Да, пиши обе216
7% Да, пиши о SteamAPI18
3.11% Да, пиши о SteamClientAPI8
5.84% Нет, не надо15
257 users voted. 54 users abstained.