Подключаемся к внутреннему протоколу iiko

Отредактировано

Как подключиться к внутреннему протоколу iiko, если возможностей публичного API не хватает для выполнения задач.

2К открытий2К показов

Относительно давно мне пришлось подразобраться во внутреннем протоколе работы сервера айко, т.к данных, даваемых публичным API мне не хватало. Плюс к этому, во времена когда я это делал, «биз» работал не стабильно и периодически падал.

Как я парсил данные?

С помощью burp, если коротко. Я сделал образ в virtualbox с чистой windows, установил туда Админку (iikoOffice), настроил прокси на компьютер, где установлен burp.

И так предположим, вы решили посмотреть, ну например, как получить товары: запустили анализатор; открыли Админку, в левом меню, в разделе «Товары и склады» щелкнули на пункт товары, открыв анализатор, вы увидите примерно следующую картину:

Подключаемся к внутреннему протоколу iiko 1

Непонятно! Я, например, ожидал увидеть путь похожий на «/goods/product/list». Более того, пройдясь по всем запросам, скорей всего в их ответах вы не найдёте вообще ничего похожего на список товаров. Но как так? В программе мы их же видим! Откуда они там берутся?

Давайте разбираться!

На самом деле, ответ прост: “админка” берёт данные из собственной БД! Да,да, каждый клиент имеет собственную БД, в которой хранит практически все данные.

Насколько я понял, только заказы (они же доставки в программе) не хранятся. По крайней мере, их и список терминалов можно получить отдельным запросом.

Но как данные попадают в локальную БД?

Синхронизация

Если посмотреть на список запросов, то можно увидеть:

  1. Все данные передаются и принимаются в xml.
  2. Все запросы методом POST.

Также, если присмотреться,то увидите часто встречающийся post-запрос на url: «/services/update?methodName=waitEntitiesUpdate». Если посмотреть, что он возвращает, то опять же, вы вряд ли найдёте что-то интересное для себя.

Подключаемся к внутреннему протоколу iiko 2

Но вот если попробовать создать, например, товар, только не на отслеживаемом клиенте, а на каком-нибудь другом (чуть позже поймёте, почему на другом), и снова посмотреть, что он вернёт, то увидите, примерно следующий ответ:

			<?xml version="1.0" encoding="UTF-8"?>
<result>
    <returnValue></returnValue>
    <success>true</success>
    <errorString null="1"></errorString>
    <resultStatus>SUCCESS</resultStatus>
    <stackTrace null="1"></stackTrace>
    <entitiesUpdate>
        <serverInstanceId>6308f3a9-298d-6b7d-0186-38a0563f0001</serverInstanceId>
        <revision>237070</revision>
        <items>
            <i>
                <id>97b00c04-beb1-4ddc-85cc-a2e43ae95e9a</id>
                <type>Product</type>
                <deleted>false</deleted>
                <xml null="1"></xml>
                <r cls="Product" eid="97b00c04-beb1-4ddc-85cc-a2e43ae95e9a">
                    <revision>237070</revision>
                    <lastModifyNode null="1"></lastModifyNode>
                    <deleted>false</deleted>
                    <modified>2023-02-26T10:38:32.410+04:00</modified>
                    <created>2023-02-26T10:38:32.398+04:00</created>
                    <deletedDate>1900-01-01T00:00:00.000+04:00</deletedDate>
                    <userCreatedId null="1"></userCreatedId>
                    <userModifiedId null="1"></userModifiedId>
                    <localId>4386</localId>
                    <name>
                        <defaultResourceId null="1"></defaultResourceId>
                        <currentResourceId null="1"></currentResourceId>
                        <customValue>TestGoods</customValue>
                    </name>
                    <system>false</system>
                    <description></description>
                    <num>21000090000213124</num>
                    <parent null="1"></parent>
                    <code>6095</code>
                    <userCreated>008c6c5a-e7bf-4475-8a20-57b363dd94f1</userCreated>
                    <userModified>008c6c5a-e7bf-4475-8a20-57b363dd94f1</userModified>
                    <modifiers></modifiers>
                    <modifierSchema null="1"></modifierSchema>
                    .........
                    
                </r>
            </i>
        </items>
        <fullUpdate>false</fullUpdate>
        <version null="1"></version>
    </entitiesUpdate>
    <licenseInfo>
        <licenseHash>-724401794</licenseHash>
        <licenseData null="1"></licenseData>
        <stateHash>3442</stateHash>
        <stateData null="1"></stateData>
        <validTill>2023-05-29T10:38:32.413+04:00</validTill>
    </licenseInfo>
    <watchDogCheckResults>
        <i cls="LicenseCheckResult">
            <severity>ERROR</severity>
            <message>messge</message>
            <checkerId>LicenseChecker</checkerId>
            <expireSeconds>480096</expireSeconds>
        </i>
    </watchDogCheckResults>
</result>
		

Заметили? Да, в entitiesUpdate->items лежат данные, которые необходимо обновить.

Так, а давайте создадим товар на текущей машине и посмотрим, как, в этом случае, нам ответит сервер. При создании товара “админка” отправляет post запрос на адрес:»/services/products?methodName=createProduct». В ответ мы получим примерно тот же самый ответ,что и получили выше при запросе на «/services/update?methodName=waitEntitiesUpdate». То есть, мы также получили в ответе (entitiesUpdate->items) данные, которые необходимо обновить. Вообще, какой бы вы запрос ни отправляли на сервер, вы будете получать одну и ту же структуру, в которой всегда будет entitiesUpdate.

Сервер при любом запросе всегда возвращает следующею структуру:

			<result>
 <returnValue></returnValue>
 <success></success>
 <errorString></errorString>
 <resultStatus></resultStatus>
 <stackTrace></stackTrace>
 <entitiesUpdate></entitiesUpdate>
 <licenseInfo></licenseInfo>
 <watchDogCheckResults><watchDogCheckResults>
</result>
		
Подключаемся к внутреннему протоколу iiko 3

Но как сервер понимает, что надо возвращать? Посмотрим, что мы отправляли на «/services/update?methodName=waitEntitiesUpdate»:

			<?xml version="1.0" encoding="utf-8"?>
<args>
    <entities-version>237069</entities-version>
    <client-type>BACK</client-type>
    <enable-warnings>false</enable-warnings>
    <client-call-id>dc193feb-8bd6-4357-b945-2795769583d0</client-call-id>
    <license-hash>123133</license-hash>
    <restrictions-state-hash>3072</restrictions-state-hash>
    <obtained-license-connections-ids>4d7daff1-a070-45bf-928a-24d0a1c47f73</obtained-license-connections-ids>
    <request-watchdog-check-results>true</request-watchdog-check-results>
    <use-raw-entities>true</use-raw-entities>
    <fromRevision>237069</fromRevision>
    <timeoutMillis>30000</timeoutMillis>
</args>
		

Обратите внимание, на число «entities-version», а теперь посмотрите в ответ entitiesUpdate->revision. В моем случае, в ответе revision,больше на 1. Более того, у продукта, который мы создали, также есть свой revision, в данном случае, он равен entitiesUpdate->revision.

Насколько я понял:

  1. На сервере в БД у каждой таблицы есть колонка revision.
  2. Есть глобальная перемененная revision на сервере.
  3. В момент обновления или вставки мы сначала увеличиваем общий «revision», потом вставляем его в текущею таблицу.

Когда клиент запрашивает данные, то в «entities-version» указывает свой текущий revision.

Сервер в свою очередь проходится по всем таблицам и берет данные, где «revision» больше «entities-version», и возвращает их в ответе (items), где также возвращает свой revision.

Подключаемся к внутреннему протоколу iiko 4

Фух, почти всё! Остался последний пункт.

Аутентификация

Так пришло время разобраться, как сервер понимает, что мы это мы. Давайте взглянем на заголовок, который передаётся в каждом запросе:

			POST /resto/services/update?methodName=waitEntitiesUpdate HTTP/1.1
Content-Type: text/xml
X-Resto-CorrelationId: d0d3901c-d314-42cd-b363-81139698a229
X-Resto-LoginName: Login
X-Resto-PasswordHash: {{sha1hash(pass)}}
X-Resto-BackVersion: 8.2.7014.0
X-Resto-AuthType: BACK
X-Resto-ServerEdition: IIKO_RMS
Accept-Language: ru
Host: {{you_host}}
Content-Length: 654
Expect: 100-continue
Accept-Encoding: gzip, deflate
Connection: close
		

Кроме стандартных, мы видим заголовки, начинающиеся с «X-Resto-».

Давайте по порядку:

  1. X-Resto-CorrelationId: генерируете любой uuidv4 при каждом запросе и вставляете сюда, а также в тело запроса в поле
  2. X-Resto-LoginName: тут думаю понятно. Логин, который вы прописываете в форме авторизации в iikoOffice.
  3. X-Resto-PasswordHash: sha1 хеш от вашего пароля.
  4. X-Resto-BackVersion: текущая версия, актуальную можно найти по адресу: /get_server_info.jsp.
  5. X-Resto-AuthType: всегда «BACK»
  6. X-Resto-ServerEdition: IIKO_RMS

Дальше обязательные поля в теле запроса:

  1. entities-version: C этим мы разобрались ранее.
  2. client-type: BACK
  3. client-call-id: тоже самое, что и в X-Resto-CorrelationId
  4. license-hash: для /services/authorization?methodName=getCurrentFingerPrints можно ставить «0»,в остальных случаях берем актуальное значение (смотри ниже).
  5. restrictions-state-hash: для /services/authorization?methodName=getCurrentFingerPrints можно ставить «0», в остальных случаях берем актуальное значение (смотри ниже).
  6. obtained-license-connections-ids: Не понял зачем это нужно, но запросы работают без него, поэтому оставляем пустым.
  7. use-raw-entities: ставим «true». Не заметил, чтобы на что-то влияло.

По поводу «license-hash» и «restrictions-state-hash» можно получить, отправив запрос по адресу: «/services/authorization?methodName=getCurrentFingerPrints». Все! Как видите, все не так сложно:).

Итог

Возможно вам не хватает стандартных инструментов или вы хотите большей стабильности. Предугадать причины, по которым вы решите пойти по моему пути, я не могу.  Я могу лишь пожелать вам удачи:).  И  да, также я подготовил демопроект на php. 

Следите за новыми постами
Следите за новыми постами по любимым темам
2К открытий2К показов