Вступление
Здесь я постараюсь объяснить суть работы официального патчера ПВ. Если вы пробовали бету моего апдейтера, заснифирить, углядеть или что-то еще, что вам не помогло, вам будет как минимум полезно.
Если смотреть официальную документацию, то у каждого оф-сервера есть 4 сервера для патчей. patch1— тот куда заливают непереведенные патчи китайцы, patch2 — туда наши заливают перевод, чтобы китаёзы сделали патч, patch4 — сюда китаёзы заливают непосредственно патч для тестов. Если все с этим патчем хорошо, то он перемещается с patch4 на patch3, откуда все пользователи его благополучно и скачивают.
Лично нам интересен момент когда китайцы делают патч для тестов. Т.к. тестить-то нам, пряморуким ходячим хомосапиенсам не надо, мы после него сразу на 4 пункт переходим
))
Структура
Для того, чтобы сделать патчер/патч нам нужны несколько вещей: умение делать цифровую подпись файлов с помощью связки открытого и закрытого ключа, умение подсчитывать проверочные суммы файлов с помощью алгоритма md5, умение пользоваться base64 с пользовательским словарем и знакомство с алгоритмом/либой для упаковки zlib.
Рассмотрим файлы, необходимые для обновления с обоих сторон.
Те кто пытались достучаться до сервера обновлений ру(май/ки и других)-офа, могли заметить следующию структуру:
— patch3.pwonline.ru/CPW/
—— pid
—— patcher
—— element
—— launcher
Эти папки — основа для любого клиента любой игры созданной Perfect World Ltd. Каждая такая папка, кроме pid, состоит из одной подпапки с файлами с аналогичным названием (т.е. есть папка patch3.pwonline.ru/CPW/patcher/patcher/), в которой содержаться упакованные файлы для загрузки, и файлами для проверки:
——— version — файл, в котором содержится номер текущей версии на сервере;
——— files.md5 — файл со списком всех-всех файлов клиента, с md5-суммами и цифровой подписью внизу;
——— v-X.inc, где X — разница между версией на сервере и версией клиента — файлы, в которых содержаться только пути к изменившимся/добавленным/удаленным файлам игры, их md5-сумма и цифровая подпись.
Папка pid содержит единственный файл info.
В клиенте ПВ два уровня патчера:
1) Perfect World/launcher/launcher.exe — та мини-программа, которая лезет на сервер обновлений только для того, чтобы узнать не нужно ли обновлять себя и программу из пункта 2;
2) Perfect World//patcher/patcher.exe — основной патчер. да-да, это тот, что качает кусочки упакованного содержимого pck-файлов, карты, списки серверов, словом все что в папке element.
Конфиги с номером текущей версии клиента и ошибками, произошедшими во время обновления, хранятся в корне клиента в папке Perfect World/config/.
Вариант первый, простой запуск через launcher.exe.
1) первым делом launcher.exe сверяет /pid/info с Perfect World/patcher/server/pid.ini:
— если содержимое одинаковое, то ланчер идет на пункт 2;
— если нет, то выдаст какую-то ошибку. Попробуйте кто-нибудь, я уже не помню.
2) далее, ланчер качает /patcher/version и сверяет с Perfect World/config/patcher/version.sw:
— если версия клиента отстает от серверной, то происходит процесс обновления, о котором будет сказано отдельным абзацем, а потом запуск патчера;
— если версии совпадают, то происходит запуск патчера;
— если версия на сервере меньше клиентской вылетит ошибка, смысл которой будет, что сервер временно не доступен.
3) запускается патчер. Проверяет pid, а затем он качает /element/version и сверяет с Perfect World/config/element/version.sw:
— если версия клиента отстает от серверной, то происходит процесс обновления, о котором будет сказано отдельным абзацем, а потом запуск патчера;
— если версии совпадают, то происходит запуск патчера;
— если версия на сервере меньше клиентской вылетит ошибка, смысл которой будет, что сервер временно не доступен.
Да, Ctrl-C, Ctrl-V.
Вариант второй, полная проверка и запуск через Perfect World/launcher/FixIt.bat.
1) launcher.exe запускается с ключом полной проверки FullCheck. Он проверяет все файлы с серверными (об этом так же чуть позже) и заменяет отличающиеся файлы из папки патчера и своей на серверные. Тем не менее, проверка /pid/info с Perfect World/patcher/server/pid.ini так же осуществляется!
2) запускается патчер, который поступит совершенно так же как и ланчер, только для папки elements, если нажать кнопку «Проверка».
1. Обычное обновление
Для примера будем считать, что ланчер этот этап прошел.
Как мы уже выяснили, патчер уже знает версию сервера и клиента. Посчитав разность и отставание версии клиента, он стучится к файлам v-X.inc .
Допустим, у меня свежий клиент с обновлением 73. Но сегодня нивал выпустил патч 74, поэтому патчер качает файлик
http://patch3.pwonline.ru/CPW/element/v-1.inc
Его содержимое:
Код HTML:
# 73 74 118
!3007c149c0ffc0785fc21dce583ede62 /aW50ZXJmYWNlcw==/bG9hZGluZy5zdGY=
-----BEGIN ELEMENT SIGNATURE-----
iWp9dTEEyZuQFVp/DiDNdCVWJbCb6dn3rbz0CImfMfSxnp9XXawQygcS9JkBIPAw
K05VnT+rCXXKGcNrlmxZwWx5tkY2iYlCidOltJsD1YBqbt+Ck7 1v1r07rvJp9lVW
yjS0zizU0/JW3rJfofBAkIin2wQjGhSSiA+IE2PIbNc=
Переведем на понятный язык:
Код HTML:
# ClientVersion ServerVersion BytesToDownload
FLAGmd5sum PathToFile
-----BEGIN ELEMENT SIGNATURE-----
SIGNKEY
ClientVersion — версия клиент;
ServerVersion — версия сервера;
BytesToDownload — придется скачать ровно столько байт;
FLAG — состояние файла. Нам известны два типа флагов — добавлен (+), изменен (!), но, вероятно, есть и удаление;
md5sum — md5-сумма файла, перед закачкой клиент сверится, т.к. возможно вы уже пытались обновится и у вас произошел обрыв;
PathToFile — путь к файлу относительно корневой директории. Если путь начинается со слеша (/), то это путь относительно корневой директории, если нет, то относительно текущей. У первого файла слеш точно должен быть! Сам путь получается в очень забавном виде: делается base64_encode (base64.ru для опытов) для каждой папки до файла и самого файла, в массиве этих строк для каждой строки делается замена слеша (/) на минус (-), потом объединяется с помощью тех же слешей в путь. Для распакованных паков опускается pck на конце. Приведу пример: файл из пака interfaces.pck/loading.stf, который как раз в этом обновлении, патчер будет качать по адресу patch3.pwonline.ru/CPW/element/element/aW50ZXJmYWNlcw==/bG9hZGluZy5zdGY=. Файл по этому адресу упакован смешанным алгоритмом с помощью zlib.
SIGNKEY — подпись файла (именно файла v-1.inc в нашем случае) с помощью закрытого ключа с серверной стороны. Эта подпись проверяется клиентом у которого свой открытый ключ (он прописан в launcher.exe и patcher.exe) до загрузки файлов, и если результат будет отрицательным — загрузки файлов не будет! Подпись осуществляется для текста с самого начала файла до конца списка файлов. После расчета подписи, патчер на сервере дописывает в конец следующее:
Код HTML:
\n-----BEGIN ELEMENT SIGNATURE-----\n
ПОДПИСЬ_РАЗБИТАЯ_ПО_64_СИМВО ЛА_НА_СТРОКУ
Каждый следующий файл (v-2.inc, например), содержит в себе все меньшие. Т.е. в файле v-10.inc содержаться все новые/модифицированные файлы из v-1, v-2, v-3, v-4, v-5 .. v-9. Что заставляет нас подписывать все файлы каждый раз, после обновления
2. Полная проверка
А тут для примера будем думать, что мы запустили ланчер с помощью FixIt (полная проверка патчера, в общем).
От автора: вы не подумайте что не хочу для element рассмотреть, просто размещать здесь 5 мегабайт текста не хочется
В этом случае, ланчер качается полный список файлов (files.md5) и проверяет отличающиеся файлы.
Сначала он скачает для себя
http://patch3.pwonline.ru/CPW/launcher/files.md5
Его содержимое:
Код HTML:
# 2
0445fb1b766e098bd767285a880cd553 /Zml4aXQuYmF0
6011e90ea67d338fbb399560b5ea7f1a bGF1bmNoZXIuYm1w
f4a55cd348d4ca26ec588be3a98c5734 bGF1bmNoZXIuZXhl
90f6235a3bb787828ba01efd67ad841d c3RyaW5ndGFiLnR4dA==
e40ef2ebd497d0a15421658643bd05c6 cGFja2RsbC5kbGw=
344a159d62deb21acb18313c96b24d1c dW5pY293cy5kbGw=
-----BEGIN ELEMENT SIGNATURE-----
Z5aRDLdAR6XSCtwMYu6y3FQtINfxn2kxB2YsiaElirAJOFkbUa f7t5cB6K50yUZv
o4mIt6LgOHW4wvKrzX6zY3dVrbJu+z+fDLuRmckJbnPu8HzGLn vzgP3hnIvSHEGP
+hnpl58SkkMYaHCJXd+2bpFwOR8WFsDXlCYDx6dHI+k=
и проверит суммы, потом для патчера
http://patch3.pwonline.ru/CPW/patcher/files.md5
Код HTML:
# 5
80ccae5334d030fa9da7aafa60125189 /c2VydmVy/cGlkLmluaQ==
0b9a282d2fd208716162e1292268ffbb dXBkYXRlc2VydmVyLnR4dA==
44e9d53e2a1b869d49a074a054b8c866 /c2tpbg==/aW1hZ2U=/Y29tYm8tYmcuYm1w
797bf5ee4384b5ef246dca080e1051f3 Y29tYm8tZXhwYW5kYmcuYm1w
7d12b5ae21a001a043d6a6ef482ef309 Y29tYm8tZm9jdXNlZC5ibXA=
6ee841d62f57e4ac5a533df9a86375e9 Y29tYm8taG92ZXIuYm1w
b50d2b0b6d3ab004c4ed397b332c8e5e Y29tYm8tbWFwLmJtcA==
1c491d0bd38d197c314d2d47a75117f6 Y29tYm8tc2VsZWN0ZWQuYm1w
b6eac91f513b61c4735302134f936230 Y29tYm9zbWFsbC1iZy5ibXA=
7a2fcffd7d9c39362009cc15a09f8d58 Y29tYm9zbWFsbC1leHBhbmRiZy5ibXA=
[...здесь пропущено много строк...]
9f4ec0abc9b7217ee7b26d19714eb38b OTcwMC5pbmk=
11a4017214ce0ade915d32b4c4bc561b Z2Vmb3JjZTJfNDAwLmluaQ==
0c73592438b247d9bf352e1bcfd4a09c Z2Vmb3JjZTYyMDAuaW5p
e0cf04af20b21f2d55f73b71170c47fe dG50Mi5pbmk=
-----BEGIN ELEMENT SIGNATURE-----
jTJK8aW9bZBcV9DK8syw8AUwcVMAiJKl2Gy0fzi4AyEpzux0fp qm4vFuzoJlBAIz
rfs8k/o0mBaNOJOcQTDyZULOsR+H2sSzJPISHUXj19Og+RJNdY34B3OM 5ZwEBF74
QXT4pP3CkGa/eJ4ATVj004dq5OWy+8bN1G0H2oDKt2U=
проверит суммы и обновит файлы патчера, если необходимо.
отличие от файлов v-X.inc:
1) присутствует только версия на сервере;
2) нет флагов;
3) присутствуют все файлы.
Последнее — особенно важно. Чисто теоретически, можно написать программу, которая будет выкачивать ЦЕЛИКОМ клинт с сервера обновлений! Т.е. скачали три полных списка (launcher, patcher + element —
http://patch3.pwonline.ru/CPW/element/files.md5), и распаковали в нужном порядке
О .cup файлах
Это мини-патч, в каком-то виде. Фактически, внутри этого пака таже самая структура, что и на сервере, только там есть содержимое исключительно папки element. Опытов с ними я не проводил, но знаю, что обычно они содержат ограниченное количество изменений (т.е. внутри нет files.md5 — оно и понятно
)
Собственно, не знаю что еще можно рассказать о структуре, так что перейдем к программистской части.
Программируем?
Вроде все просто. А ведь так и есть. Итак, приведу сложные моменты в реализации всего:
1) понять смешанный алгоритм упаковки;
2) понять какой алгоритм подписи и как подписывать;
3) найти в клиенте открытый ключ и заменить на свой (если не знаете почему, погуглите rsa private public key).
Приведу примеры решений описанных выше проблем.
Важно! Все примеры написаны на java и являются кусками нескольких классов! Возможно я скопирую не все, что нужно, но смысл работы понять можно.
1. упаковка отличается только тем, что в начало упакованного архива дописывается размер файла до упаковки
Код HTML:
/**
* Интерфейс упаковщика для строковых входных переменных.
* @param input откуда
* @param output куда
* @throws Exception
*/
public void doPakage(String input, String output) throws Exception {
doPakage(new File(input),new File(output));
}
/**
* Упаковщик файлов.
* @param fileInput откуда
* @param fileOutput куда
* @throws FileNotFoundException
* @throws IOException
*/
public void doPakage(File fileInput, File fileOutput) throws FileNotFoundException, IOException {
// создадим новый файл, должно просто стереть старый.
fileOutput.createNewFile();
InputStream fileInputStream = new FileInputStream(fileInput);
OutputStream fileOutputStream = new FileOutputStream(fileOutput);
byte[] buffer = new byte[(int)fileInput.length()];
byte[] buffer2 = new byte[(int)fileInput.length()];
ByteBuffer temp = ByteBuffer.allocate(4);
temp.order(ByteOrder.LITTLE_ENDIAN);
temp.putInt((int)fileInput.length());
int compressedDataLength = (int)fileInput.length();
fileInputStream.read(buffer);
// упаковка файла
Deflater compresser = new Deflater(Deflater.BEST_SPEED,false);
compresser.setInput(buffer);
compresser.finish();
compressedDataLength = compresser.deflate(buffer2);
if(fileInput.length() <= compressedDataLength)
buffer2 = buffer;
fileOutputStream.write(temp.array());
fileOutputStream.write(buffer2, 0, compressedDataLength);
fileOutputStream.flush();
fileOutputStream.close();
fileInputStream.close();
2. мы выяснили, что после каждого нового патча, необходимо заново переподписать все листы (именно все, v-X.inc и files.md5). Кусочек класса подписи:
Код HTML:
private PrivateKey privateKey; // приватный ключ
private PublicKey publicKey; // паблик ключ
private BigInteger modus; // сюда мы подгрузим ключи, чтобы не приходилось каждый раз генерировать
private BigInteger privateX;
private BigInteger publicX;
/**
* Надпись перед подписью.
*/
public String signatereText = "-----BEGIN ELEMENT SIGNATURE-----\n";
/**
* Генерация ключей.
* @throws NoSuchAlgorithmException
* @throws InvalidKeySpecException
* @throws NoSuchProviderException
*/
private void doGenerate() throws NoSuchAlgorithmException, InvalidKeySpecException, NoSuchProviderException {
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
SecureRandom random = SecureRandom.getInstance("SHA1PRNG", "SUN");
keyGen.initialize(1024, random);
KeyPair pair = keyGen.generateKeyPair();
privateKey = pair.getPrivate();
publicKey = pair.getPublic();
KeyFactory kf = KeyFactory.getInstance("RSA");
RSAPrivateKeySpec prks = kf.getKeySpec(privateKey, RSAPrivateKeySpec.class);
modus = prks.getModulus();
privateX = prks.getPrivateExponent();
RSAPublicKeySpec pubks = kf.getKeySpec(publicKey, RSAPublicKeySpec.class);
publicX = pubks.getPublicExponent();
}
/**
* Загрузка ключей из входных переменных.
* @param privateXL
* @param publicXL
* @param modusL
* @throws NoSuchAlgorithmException
* @throws InvalidKeySpecException
*/
private void doLoad(BigInteger privateXL, BigInteger publicXL, BigInteger modusL) throws NoSuchAlgorithmException, InvalidKeySpecException {
modus = modusL;
privateX = privateXL;
publicX = publicXL;
KeyFactory kf = KeyFactory.getInstance("RSA");
RSAPrivateKeySpec new_prks = new RSAPrivateKeySpec(modus, privateX);
RSAPublicKeySpec new_pubks = new RSAPublicKeySpec(modus, publicX);
privateKey = kf.generatePrivate(new_prks);
publicKey = kf.generatePublic(new_pubks);
}
/**
* Подпись для строки what
* @param what
* @return
* @throws IOException
* @throws NoSuchAlgorithmException
* @throws InvalidKeyException
* @throws SignatureException
*/
public String doSignature(String what) throws IOException, NoSuchAlgorithmException, InvalidKeyException, SignatureException {
String encryptedText;
byte[] textBytes = what.getBytes();
Signature dsa = Signature.getInstance("MD5withRSA");
dsa.initSign(privateKey);
dsa.update(textBytes);
byte[] encryptedBytes = dsa.sign();
encryptedText = CPWBase64.encodeBytes(encryptedBytes).toString();
return signatereText + encryptedText;
}
/**
* Рассчет подписи файла what и запись этой подписи в конец файла.
* @param what
* @param fos
* @return
* @throws IOException
* @throws NoSuchAlgorithmException
* @throws InvalidKeyException
* @throws SignatureException
*/
public boolean doSignature(File what, OutputStream fos) throws IOException, NoSuchAlgorithmException, InvalidKeyException, SignatureException {
Signature dsa = Signature.getInstance("MD5withRSA");
dsa.initSign(privateKey);
FileInputStream fis = new FileInputStream(what);
BufferedInputStream bufin = new BufferedInputStream(fis);
byte[] buffer = new byte[1024];
int len;
while(bufin.available() != 0) {
len = bufin.read(buffer);
dsa.update(buffer, 0, len);
}
bufin.close();
fis.close();
byte[] realSig = dsa.sign();
fos.write(signatereText.getBytes());
String signature = CPWBase64.encodeBytes(realSig);
for(int i=0; i<signature.length(); i=i+64) {
int b=i+64;
if(b > signature.length()) b = signature.length();
fos.write(new String(signature.substring(i,b)+"\n").getBytes());
}
return true;
}
Кстати, проблема хранения md5 сумм файлов, изменений версии от версии — достаточно трудоемкая, если не использовать базы данных! Поэтому я, к примеру, использую MySQL
3. на эмудеве я посоветовал просто найти соответствующее вхождение хекс-редактором и заменить на свое. Но мы же джедаи программирования, надо чтобы можно было тока кнопочку нажать
ops:
ПомогЭ тык спасибо!)
Гайд пренодлежит мне!)
Просьба соблюдайте копирайты!)))