Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3405057e5b |
@@ -0,0 +1,49 @@
|
||||
-- DB update 2026_07_22_00 -> 2026_08_16_00
|
||||
-- MoonWell launcher-only authentication.
|
||||
|
||||
ALTER TABLE `account`
|
||||
ADD COLUMN `session_auth` enum('none','launcher','dev') NOT NULL DEFAULT 'none' AFTER `session_key`;
|
||||
|
||||
UPDATE `account`
|
||||
SET `session_key` = NULL,
|
||||
`session_auth` = 'none';
|
||||
|
||||
CREATE TABLE `launcher_ticket` (
|
||||
`account_id` int unsigned NOT NULL,
|
||||
`generation_id` binary(16) NOT NULL,
|
||||
`srp_salt` binary(32) NOT NULL,
|
||||
`srp_verifier` binary(32) NOT NULL,
|
||||
`client_build` smallint unsigned NOT NULL,
|
||||
`launcher_session_hash` binary(32) DEFAULT NULL,
|
||||
`issued_at` datetime(6) NOT NULL,
|
||||
`expires_at` datetime(6) NOT NULL,
|
||||
PRIMARY KEY (`account_id`),
|
||||
UNIQUE KEY `uq_launcher_ticket_generation` (`generation_id`),
|
||||
KEY `idx_launcher_ticket_expiry` (`expires_at`),
|
||||
CONSTRAINT `fk_launcher_ticket_account`
|
||||
FOREIGN KEY (`account_id`) REFERENCES `account` (`id`) ON DELETE CASCADE,
|
||||
CONSTRAINT `chk_launcher_ticket_expiry`
|
||||
CHECK (`expires_at` > `issued_at` AND `expires_at` <= `issued_at` + INTERVAL 60 SECOND)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE `launcher_ticket_consumption` (
|
||||
`generation_id` binary(16) NOT NULL,
|
||||
`account_id` int unsigned NOT NULL,
|
||||
`claim_nonce` binary(16) NOT NULL,
|
||||
`consumed_at` datetime(6) NOT NULL,
|
||||
PRIMARY KEY (`generation_id`),
|
||||
UNIQUE KEY `uq_launcher_ticket_claim_nonce` (`claim_nonce`),
|
||||
KEY `idx_launcher_ticket_consumption_account` (`account_id`, `consumed_at`),
|
||||
CONSTRAINT `fk_launcher_ticket_consumption_account`
|
||||
FOREIGN KEY (`account_id`) REFERENCES `account` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE `launcher_dev_account` (
|
||||
`account_id` int unsigned NOT NULL,
|
||||
`allowed_ip` varchar(45) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
||||
`comment` varchar(255) NOT NULL DEFAULT '',
|
||||
`enabled` tinyint unsigned NOT NULL DEFAULT 1,
|
||||
PRIMARY KEY (`account_id`, `allowed_ip`),
|
||||
CONSTRAINT `fk_launcher_dev_account_account`
|
||||
FOREIGN KEY (`account_id`) REFERENCES `account` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
@@ -0,0 +1,371 @@
|
||||
# Контракт авторизации MoonWell через лаунчер
|
||||
|
||||
Статус документа: обязательный контракт для Release-сборок.
|
||||
|
||||
Ключевые слова **ДОЛЖЕН**, **НЕЛЬЗЯ**, **СЛЕДУЕТ** и **МОЖЕТ** описывают
|
||||
обязательность требований.
|
||||
|
||||
## 1. Цель и границы доверия
|
||||
|
||||
Игровой клиент World of Warcraft 3.3.5a остаётся стандартным SRP6-клиентом,
|
||||
но вместо постоянного пароля получает короткоживущий одноразовый ticket от
|
||||
MoonWell Launcher. Наличие или отсутствие полей логина в GlueXML не является
|
||||
границей безопасности. Решение о допустимости входа всегда принимает
|
||||
`authserver`.
|
||||
|
||||
В поток входят четыре компонента:
|
||||
|
||||
1. backend аутентифицирует пользователя и выпускает game ticket;
|
||||
2. launcher передаёт game account name и ticket только через environment block
|
||||
нового `Wow.exe`;
|
||||
3. MoonWell.dll забирает значения до загрузки GlueXML и инициирует обычный SRP;
|
||||
4. `authserver` использует временные salt/verifier и атомарно погашает ticket
|
||||
после успешного `CMD_AUTH_LOGON_PROOF`.
|
||||
|
||||
## 2. Термины
|
||||
|
||||
- **backend account** — учётная запись пользователя в сервисах MoonWell;
|
||||
- **game account** — имя учётной записи AzerothCore, передаваемое в поле `I`
|
||||
`CMD_AUTH_LOGON_CHALLENGE`;
|
||||
- **launcher session** — аутентифицированная сессия launcher с backend;
|
||||
- **ticket** — одноразовый временный пароль для SRP;
|
||||
- **ticket generation** — уникальный внутренний идентификатор выпуска ticket,
|
||||
используемый для отзыва и атомарного погашения;
|
||||
- **dev account** — явно разрешённая сервером учётная запись с постоянным SRP
|
||||
verifier для локальной разработки.
|
||||
|
||||
## 3. Формат launcher credentials
|
||||
|
||||
Launcher передаёт ровно две обязательные переменные:
|
||||
|
||||
```text
|
||||
MOONWELL_LAUNCH_ACCOUNT=<game account name>
|
||||
MOONWELL_LAUNCH_TICKET=<single-use SRP password>
|
||||
```
|
||||
|
||||
### 3.1 `MOONWELL_LAUNCH_ACCOUNT`
|
||||
|
||||
- непустая строка печатного ASCII (`0x21..0x7E`), без пробельных символов;
|
||||
- не содержит `NUL`, `=` и управляющих символов;
|
||||
- верхний предел безопасного чтения из environment — 320 байт;
|
||||
- перед SRP приводится к той же канонической форме, что и имя в AzerothCore,
|
||||
то есть к ASCII uppercase;
|
||||
- backend и `authserver` сравнивают каноническое имя, а не display name.
|
||||
|
||||
Ограничение 320 байт является только пределом входного буфера MoonWell.dll.
|
||||
Текущий протокол и этот репозиторий не поддерживают game account такой длины:
|
||||
`I_len` имеет размер `uint8`, `AuthSession.cpp` принимает challenge лишь с
|
||||
добавкой до 16 байт, а `AccountMgr` использует `MAX_ACCOUNT_STR = 17`.
|
||||
Поэтому backend **ДОЛЖЕН** отображать длинную backend account на отдельное
|
||||
короткое game account name, совместимое с текущим сервером (не более 17 байт).
|
||||
Расширение этого лимита является отдельным изменением протокола/сервера и не
|
||||
входит в настоящий контракт.
|
||||
|
||||
### 3.2 `MOONWELL_LAUNCH_TICKET`
|
||||
|
||||
- ровно 16 ASCII-символов;
|
||||
- допустимый алфавит: `A-Z` и `0-9`;
|
||||
- генерируется CSPRNG с равномерным rejection sampling;
|
||||
- энтропия полного пространства `36^16` — около 82,7 бит;
|
||||
- срок действия: не более 60 секунд от момента выпуска;
|
||||
- одноразовый;
|
||||
- новый выпуск атомарно отзывает предыдущий активный ticket game account.
|
||||
|
||||
Ни один компонент **НЕ ДОЛЖЕН** нормализовать, менять регистр или обрезать
|
||||
ticket. Для SRP ticket используется в точности как получен.
|
||||
|
||||
## 4. Контракт backend
|
||||
|
||||
Конкретный HTTP route может определяться backend, но операция выпуска имеет
|
||||
следующую семантику:
|
||||
|
||||
```json
|
||||
{
|
||||
"launcher_session_id": "opaque launcher session identifier",
|
||||
"client_build": 12340
|
||||
}
|
||||
```
|
||||
|
||||
Успешный ответ:
|
||||
|
||||
```json
|
||||
{
|
||||
"account": "GAMEACCOUNT",
|
||||
"ticket": "7Q9AV4R2M8ZK3W1P",
|
||||
"expires_at": "2026-08-16T12:34:56Z"
|
||||
}
|
||||
```
|
||||
|
||||
Требования к операции:
|
||||
|
||||
- запрос разрешён только из действующей аутентифицированной launcher session;
|
||||
- требуемая пользователю MFA завершается до выпуска ticket;
|
||||
- backend проверяет право launcher session входить в указанный game account;
|
||||
- `client_build` должен входить в разрешённый список;
|
||||
- выпуск и отзыв предыдущей generation выполняются одной транзакцией;
|
||||
- ответ передаётся только по TLS и содержит `Cache-Control: no-store`;
|
||||
- ticket не возвращается повторно и не попадает в URL, telemetry, analytics,
|
||||
tracing, audit payload, exception text или access log;
|
||||
- rate limit применяется как минимум к пользователю, game account, launcher
|
||||
session и IP;
|
||||
- время жизни вычисляется по серверному времени. Часы клиента не являются
|
||||
источником истины.
|
||||
|
||||
Рекомендуемые машинные ошибки: `UNAUTHENTICATED`, `ACCOUNT_NOT_ALLOWED`,
|
||||
`CLIENT_BUILD_NOT_ALLOWED`, `RATE_LIMITED`, `GAME_ACCOUNT_NOT_COMPATIBLE` и
|
||||
`INTERNAL_ERROR`. Ошибка не должна содержать ticket или verifier.
|
||||
|
||||
## 5. Хранение ticket
|
||||
|
||||
Предпочтительный вариант — не хранить открытый ticket. Backend вычисляет
|
||||
временные SRP salt и verifier тем же кодом/алгоритмом, что
|
||||
`Acore::Crypto::SRP6::MakeRegistrationData(canonicalAccount, ticket)`, сохраняет
|
||||
их вместе с generation и уничтожает открытый ticket после формирования ответа.
|
||||
|
||||
Минимальная логическая запись:
|
||||
|
||||
```text
|
||||
account_id UNIQUE
|
||||
generation_id UNIQUE, unpredictable
|
||||
srp_salt BINARY(32)
|
||||
srp_verifier BINARY(32)
|
||||
launcher_session_hash nullable/optional binding
|
||||
client_build required binding
|
||||
issued_at server timestamp
|
||||
expires_at server timestamp, <= issued_at + 60 seconds
|
||||
consumed_at nullable
|
||||
revoked_at nullable
|
||||
```
|
||||
|
||||
Активна только запись, для которой одновременно выполнено:
|
||||
|
||||
```text
|
||||
consumed_at IS NULL
|
||||
AND revoked_at IS NULL
|
||||
AND expires_at > server_now
|
||||
```
|
||||
|
||||
Если архитектура временно требует хранения открытого ticket, он должен быть
|
||||
зашифрован отдельным ротируемым ключом, недоступным read-only потребителям БД,
|
||||
и удалён при погашении/истечении. Хеш ticket сам по себе недостаточен для
|
||||
построения SRP verifier после выпуска.
|
||||
|
||||
## 6. Запуск `Wow.exe`
|
||||
|
||||
Launcher **ДОЛЖЕН**:
|
||||
|
||||
1. получить ticket непосредственно перед запуском;
|
||||
2. построить отдельный Unicode environment block для дочернего процесса,
|
||||
сохранив необходимые штатные переменные и добавив две MoonWell-переменные;
|
||||
3. вызвать `CreateProcessW` с `CREATE_UNICODE_ENVIRONMENT`;
|
||||
4. не добавлять credentials в command line;
|
||||
5. сразу после возврата `CreateProcessW` затереть все собственные изменяемые
|
||||
буферы с ticket через гарантированно не оптимизируемую операцию, например
|
||||
`SecureZeroMemory`;
|
||||
6. уничтожить созданный environment block независимо от результата запуска.
|
||||
|
||||
Launcher **НЕ ДОЛЖЕН** менять собственное глобальное окружение через
|
||||
`SetEnvironmentVariable`, поскольку это создаёт окно утечки и гонки при
|
||||
параллельных запусках. Переменные должны существовать только в явно собранном
|
||||
environment block конкретного дочернего процесса.
|
||||
|
||||
Ticket нельзя помещать в аргументы, файлы, реестр, crash metadata или логи.
|
||||
Дамп памяти процесса остаётся привилегированной атакой; MoonWell.dll должна
|
||||
минимизировать время жизни plaintext и очищать временные буферы.
|
||||
|
||||
## 7. Контракт MoonWell.dll и GlueXML
|
||||
|
||||
MoonWell.dll до загрузки GlueXML:
|
||||
|
||||
1. читает обе launcher-переменные в ограниченные изменяемые буферы;
|
||||
2. немедленно удаляет их из окружения `Wow.exe`;
|
||||
3. валидирует обе переменные целиком;
|
||||
4. передаёт валидную пару GlueXML только в памяти процесса;
|
||||
5. очищает исходные и промежуточные буферы после начала SRP-входа.
|
||||
|
||||
Пара считается валидной только если присутствуют и корректны обе переменные.
|
||||
Частичная, пустая, слишком длинная или синтаксически неверная пара полностью
|
||||
отбрасывается. Нельзя переходить к ручному логину из-за ошибки credentials в
|
||||
Release.
|
||||
|
||||
Состояния интерфейса:
|
||||
|
||||
| Сборка/вход | `AccountLoginUI` | Основное действие |
|
||||
| --- | --- | --- |
|
||||
| Release, валидная пара | скрыт | один вызов `DefaultServerLogin(account, ticket)` |
|
||||
| Release, пары нет/она невалидна | поля, password и save options скрыты | открыть launcher |
|
||||
| Debug и `MOONWELL_DEV_LOGIN=1` | прежняя форма видима | штатный ручной SRP-вход |
|
||||
| Debug без dev-флага | как Release | открыть launcher |
|
||||
|
||||
При каждом не-dev запуске клиент удаляет ранее сохранённые account/password и
|
||||
отключает параметры их сохранения до отображения UI. Повторный автоматический
|
||||
вызов `DefaultServerLogin` в рамках одного процесса запрещён.
|
||||
|
||||
`MOONWELL_DEV_LOGIN` читается и удаляется тем же ранним нативным кодом. Он
|
||||
учитывается только Debug-сборкой MoonWell.dll; Release-сборка всегда его
|
||||
игнорирует.
|
||||
|
||||
## 8. Открытие launcher
|
||||
|
||||
Кнопка «Авторизоваться»:
|
||||
|
||||
1. ищет `MoonWellLauncher.exe` в каталоге фактически запущенного `Wow.exe`;
|
||||
2. если файл найден, запускает его без credentials;
|
||||
3. иначе открывает `moonwell://authorize?source=client` системным обработчиком.
|
||||
|
||||
Путь нельзя брать из current working directory. Перед запуском следует
|
||||
проверять издателя Authenticode/ожидаемую подпись launcher.
|
||||
|
||||
Установщик регистрирует URI scheme только для текущего пользователя в
|
||||
`HKCU\Software\Classes\moonwell`. Команда обработчика должна корректно заключать
|
||||
путь executable и аргумент URI в кавычки. Launcher рассматривает URI как
|
||||
недоверенный ввод, разрешает только известные host/action/query keys и никогда
|
||||
не принимает credentials через URI.
|
||||
|
||||
## 9. Обязательный контракт `authserver`
|
||||
|
||||
### 9.1 `CMD_AUTH_LOGON_CHALLENGE`
|
||||
|
||||
После обычного разбора и канонизации game account сервер:
|
||||
|
||||
1. загружает account, блокировки и режим авторизации;
|
||||
2. для обычного account ищет активную launcher ticket generation;
|
||||
3. проверяет expiry, `client_build` и дополнительные привязки;
|
||||
4. если активной generation нет, возвращает общий отказ до обращения к
|
||||
постоянным account salt/verifier;
|
||||
5. сохраняет `generation_id` в объекте конкретной `AuthSession`;
|
||||
6. строит SRP challenge только из временных salt/verifier этой generation.
|
||||
|
||||
Для launcher-only account SRP challenge не должен запрашивать интерактивный
|
||||
TOTP/security token: MFA является свойством launcher/backend-сессии и должна
|
||||
быть завершена до выпуска ticket. Иначе скрытый GlueXML не сможет закончить
|
||||
автоматический вход. Штатный AzerothCore TOTP может оставаться только в явно
|
||||
описанном dev-потоке с видимой формой.
|
||||
|
||||
Для отсутствующего, истёкшего, отозванного или уже использованного ticket
|
||||
следует возвращать одинаковый протокольный результат, например
|
||||
`WOW_FAIL_UNKNOWN_ACCOUNT`, чтобы не создавать oracle состояния ticket.
|
||||
|
||||
### 9.2 `CMD_AUTH_LOGON_PROOF`
|
||||
|
||||
После успешной криптографической проверки proof, TOTP (если применяется) и
|
||||
версии клиента сервер **ДОЛЖЕН** в одной транзакции:
|
||||
|
||||
1. выполнить условное погашение именно generation из текущей `AuthSession`:
|
||||
|
||||
```sql
|
||||
UPDATE launcher_ticket
|
||||
SET consumed_at = CURRENT_TIMESTAMP(6)
|
||||
WHERE account_id = ?
|
||||
AND generation_id = ?
|
||||
AND consumed_at IS NULL
|
||||
AND revoked_at IS NULL
|
||||
AND expires_at > CURRENT_TIMESTAMP(6);
|
||||
```
|
||||
|
||||
2. убедиться, что изменена ровно одна строка;
|
||||
3. сохранить новую game session key и штатные login metadata;
|
||||
4. зафиксировать транзакцию;
|
||||
5. только после commit отправить клиенту успешный `AUTH_LOGON_PROOF`.
|
||||
|
||||
Если условное погашение изменило ноль строк или commit не удался, сервер
|
||||
возвращает общий отказ и не создаёт авторизованную game session. Это обеспечивает
|
||||
отказ повторному proof даже если два challenge успели получить одну generation.
|
||||
|
||||
Неуспешный proof не погашает ticket, но существующие механизмы rate limit и
|
||||
защиты от перебора продолжают действовать. Ticket всё равно истекает не позднее
|
||||
60 секунд после выпуска.
|
||||
|
||||
### 9.3 Постоянный verifier и dev-доступ
|
||||
|
||||
Для обычных accounts `authserver` никогда не выбирает и не проверяет постоянные
|
||||
`account.salt/account.verifier`. При миграции их следует сделать nullable и
|
||||
удалить у launcher-only accounts либо заменить отдельным режимом хранения,
|
||||
который невозможно случайно использовать в launcher-ветке.
|
||||
|
||||
Ручной SRP разрешён только если серверная политика независимо подтверждает все
|
||||
условия:
|
||||
|
||||
- account присутствует в явном dev allowlist;
|
||||
- account имеет режим `DEV_SRP`;
|
||||
- источник соответствует VPN/IP allowlist;
|
||||
- production-конфигурация не разрешает dev-режим по умолчанию.
|
||||
|
||||
Клиентский Debug-флаг не передаётся authserver и не участвует в решении.
|
||||
|
||||
### 9.4 Reconnect
|
||||
|
||||
`CMD_AUTH_RECONNECT_*` может использовать session key только от ранее успешно
|
||||
погашенного ticket или разрешённого dev-входа. При включённом reconnect сервер
|
||||
должен хранить происхождение/время session key и отклонить legacy session keys,
|
||||
созданные до внедрения этой политики. Reconnect не оживляет ticket и не позволяет
|
||||
новому процессу выполнить новый logon без нового ticket.
|
||||
|
||||
## 10. Состояния ticket и гонки
|
||||
|
||||
Допустимые переходы:
|
||||
|
||||
```text
|
||||
ISSUED -> CONSUMED
|
||||
ISSUED -> REVOKED (выпущена новая generation)
|
||||
ISSUED -> EXPIRED (server_now >= expires_at)
|
||||
```
|
||||
|
||||
`CONSUMED`, `REVOKED` и `EXPIRED` — терминальные состояния. Cleanup может
|
||||
физически удалить терминальные записи позже, но не должен превращать их обратно
|
||||
в активные.
|
||||
|
||||
Выдача новой generation не обязана завершать уже созданную игровую сессию, но
|
||||
обязана сделать proof старой незавершённой generation невозможным. Это
|
||||
обеспечивается сравнением `generation_id` при погашении.
|
||||
|
||||
## 11. Логи и наблюдаемость
|
||||
|
||||
Разрешено логировать только:
|
||||
|
||||
- внутренний account id либо необратимо псевдонимизированный идентификатор;
|
||||
- generation id в хешированном/усечённом виде;
|
||||
- тип события: issued, rejected, consumed, revoked, expired;
|
||||
- код результата, build и тайминги без секретов.
|
||||
|
||||
Запрещено логировать ticket, SRP verifier, environment block, полный launcher
|
||||
session token и HTTP response body. Фильтрация секретов должна применяться также
|
||||
к debug-логам, SQL tracing, crash reports и APM.
|
||||
|
||||
## 12. Критерии приёмки
|
||||
|
||||
Реализация считается соответствующей контракту, если автоматические или
|
||||
интеграционные тесты подтверждают:
|
||||
|
||||
1. валидный свежий ticket даёт ровно один успешный вход;
|
||||
2. повторный proof и второй процесс с тем же ticket получают отказ;
|
||||
3. новый ticket отзывает предыдущий, включая уже полученный старый challenge;
|
||||
4. ticket старше 60 секунд отклоняется по серверному времени;
|
||||
5. прямой Release-запуск не показывает и не использует ручные credentials;
|
||||
6. одна отсутствующая/невалидная environment-переменная не запускает SRP;
|
||||
7. переменные удалены из окружения до GlueXML и отсутствуют в дочерних
|
||||
процессах, созданных позже;
|
||||
8. Release игнорирует `MOONWELL_DEV_LOGIN=1`;
|
||||
9. Debug dev-login работает только для server-side allowlisted account и
|
||||
разрешённого источника;
|
||||
10. обычный account с корректным старым постоянным паролем получает отказ;
|
||||
11. ticket, URL и command line отсутствуют во всех проверяемых логах;
|
||||
12. параллельные proof одной generation дают ровно один success;
|
||||
13. запрещённый client build и несоответствующая launcher-session binding дают
|
||||
общий отказ без fallback на постоянный verifier;
|
||||
14. legacy session keys не позволяют обойти launcher-only политику reconnect.
|
||||
15. launcher-only challenge не требует интерактивного TOTP после запуска
|
||||
`DefaultServerLogin`.
|
||||
|
||||
## 13. Локальный запуск разработчика
|
||||
|
||||
Поддерживаемая команда:
|
||||
|
||||
```powershell
|
||||
.\run.ps1 -Env local -DeveloperLogin
|
||||
```
|
||||
|
||||
Скрипт собирает Debug runtime, формирует отдельный environment block и добавляет
|
||||
`MOONWELL_DEV_LOGIN=1` только запускаемому процессу. Он не изменяет глобальное
|
||||
окружение пользователя и не делает server-side account dev-разрешённым.
|
||||
Debug DLL не включается в пользовательские Release-дистрибутивы.
|
||||
@@ -0,0 +1,84 @@
|
||||
# Эксплуатация launcher-авторизации
|
||||
|
||||
Нормативный протокол описан в
|
||||
[`LauncherAuthorizationContract.md`](LauncherAuthorizationContract.md). Этот
|
||||
документ относится к реализованной в данном репозитории серверной части.
|
||||
|
||||
## Что входит в репозиторий
|
||||
|
||||
- `authserver` использует временные SRP salt/verifier для всех обычных
|
||||
аккаунтов и не делает fallback на постоянный пароль;
|
||||
- SQL update `2026_08_16_00.sql` добавляет активные ticket, журнал погашений,
|
||||
точный IP allowlist для dev-аккаунтов и provenance session key;
|
||||
- `issue-launcher-ticket.sh` является reference adapter для backend и локальной
|
||||
интеграции. Production backend должен реализовать ту же операцию внутри
|
||||
своего доверенного процесса, включая аутентификацию launcher session и rate
|
||||
limiting;
|
||||
- reconnect и вход в worldserver разрешены только session key с provenance
|
||||
`launcher` или `dev`.
|
||||
|
||||
Исходников MoonWell Launcher, MoonWell.dll, GlueXML и backend в этом
|
||||
репозитории нет. Их реализация должна следовать отдельным разделам контракта.
|
||||
|
||||
## Выпуск ticket
|
||||
|
||||
База должна быть запущена и обновлена штатным `ac-db-import`:
|
||||
|
||||
```bash
|
||||
./issue-launcher-ticket.sh PLAYER --build 12340 --ttl 60
|
||||
```
|
||||
|
||||
Успешный результат — одна JSON-строка в stdout. Она содержит plaintext ticket
|
||||
и должна быть сразу передана launcher по TLS с `Cache-Control: no-store`.
|
||||
Диагностика выводится в stderr. Новый вызов для того же account атомарно
|
||||
заменяет предыдущую активную generation.
|
||||
|
||||
Скрипт не является публичным HTTP endpoint. Нельзя предоставлять игрокам
|
||||
доступ к его запуску или к MySQL credentials.
|
||||
|
||||
## Dev allowlist
|
||||
|
||||
Постоянный verifier используется только при совпадении account и точного
|
||||
remote IP с активной строкой allowlist:
|
||||
|
||||
```sql
|
||||
INSERT INTO launcher_dev_account (account_id, allowed_ip, comment)
|
||||
SELECT id, '10.20.30.40', 'local developer over VPN'
|
||||
FROM account
|
||||
WHERE username = 'DEVACCOUNT'
|
||||
ON DUPLICATE KEY UPDATE enabled = 1, comment = VALUES(comment);
|
||||
```
|
||||
|
||||
Отключение:
|
||||
|
||||
```sql
|
||||
UPDATE launcher_dev_account
|
||||
SET enabled = 0
|
||||
WHERE account_id = (SELECT id FROM account WHERE username = 'DEVACCOUNT')
|
||||
AND allowed_ip = '10.20.30.40';
|
||||
```
|
||||
|
||||
CIDR и wildcard намеренно не поддерживаются: allowlist требует точного IP.
|
||||
|
||||
## Миграция и откат
|
||||
|
||||
SQL update очищает существующие `account.session_key`, чтобы legacy reconnect
|
||||
не обходил launcher-only политику. После применения старые клиенты должны
|
||||
получить новый launcher ticket.
|
||||
|
||||
Миграция безопасности намеренно не имеет автоматического fallback. Перед
|
||||
развёртыванием должны быть готовы backend issuer и Release-клиент. Для
|
||||
аварийного dev-доступа заранее добавьте только выделенный account и VPN IP в
|
||||
`launcher_dev_account`.
|
||||
|
||||
## Проверка
|
||||
|
||||
После сборки и применения update:
|
||||
|
||||
1. выпустить ticket для тестового account;
|
||||
2. убедиться, что прямой вход со старым паролем отвергнут;
|
||||
3. войти с ticket;
|
||||
4. убедиться, что строка удалена из `launcher_ticket` и появилась в
|
||||
`launcher_ticket_consumption`;
|
||||
5. повторить вход с тем же ticket и получить отказ;
|
||||
6. выпустить два ticket подряд и убедиться, что работает только второй.
|
||||
@@ -0,0 +1,167 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ENV_FILE="${ENV_FILE:-$ROOT_DIR/.env}"
|
||||
CLIENT_BUILD=""
|
||||
TTL_SECONDS=60
|
||||
|
||||
if [[ -f "$ENV_FILE" ]]; then
|
||||
set -a
|
||||
# shellcheck disable=SC1090
|
||||
source "$ENV_FILE"
|
||||
set +a
|
||||
fi
|
||||
|
||||
CLIENT_BUILD="${ACORE_REALMLIST_GAMEBUILD:-12340}"
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $(basename "$0") <game-account> [--build <client-build>] [--ttl <1-60>]
|
||||
|
||||
Issues one single-use MoonWell launcher ticket and revokes the account's
|
||||
previous active ticket. The JSON response containing the ticket is written
|
||||
only to stdout; diagnostics go to stderr.
|
||||
EOF
|
||||
}
|
||||
|
||||
if (($# == 0)) || [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
|
||||
usage
|
||||
exit 0
|
||||
fi
|
||||
|
||||
ACCOUNT="$1"
|
||||
shift
|
||||
|
||||
while (($#)); do
|
||||
case "$1" in
|
||||
--build)
|
||||
[[ $# -ge 2 ]] || { printf '%s\n' '--build requires a value' >&2; exit 1; }
|
||||
CLIENT_BUILD="$2"
|
||||
shift 2
|
||||
;;
|
||||
--ttl)
|
||||
[[ $# -ge 2 ]] || { printf '%s\n' '--ttl requires a value' >&2; exit 1; }
|
||||
TTL_SECONDS="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
printf 'Unknown option: %s\n' "$1" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if ! [[ "$CLIENT_BUILD" =~ ^[0-9]+$ ]] || ((CLIENT_BUILD < 1 || CLIENT_BUILD > 65535)); then
|
||||
printf '%s\n' 'client build must be an integer from 1 to 65535' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! [[ "$TTL_SECONDS" =~ ^[0-9]+$ ]] || ((TTL_SECONDS < 1 || TTL_SECONDS > 60)); then
|
||||
printf '%s\n' 'ttl must be an integer from 1 to 60 seconds' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
export MOONWELL_ISSUER_ACCOUNT="$ACCOUNT"
|
||||
mapfile -t ACCOUNT_DATA < <(python3 <<'PY'
|
||||
import json
|
||||
import os
|
||||
|
||||
account = os.environ["MOONWELL_ISSUER_ACCOUNT"]
|
||||
account = "".join(chr(ord(ch) - 32) if "a" <= ch <= "z" else ch for ch in account)
|
||||
encoded = account.encode("ascii", "strict")
|
||||
|
||||
if not encoded or len(encoded) > 17:
|
||||
raise SystemExit("game account must contain 1 to 17 ASCII bytes")
|
||||
if any(byte < 0x21 or byte > 0x7E for byte in encoded) or b"=" in encoded:
|
||||
raise SystemExit("game account must be printable ASCII without whitespace or '='")
|
||||
|
||||
print(account)
|
||||
print(encoded.hex().upper())
|
||||
print(json.dumps(account, ensure_ascii=True))
|
||||
PY
|
||||
)
|
||||
unset MOONWELL_ISSUER_ACCOUNT ACCOUNT
|
||||
|
||||
CANONICAL_ACCOUNT="${ACCOUNT_DATA[0]}"
|
||||
ACCOUNT_HEX="${ACCOUNT_DATA[1]}"
|
||||
ACCOUNT_JSON="${ACCOUNT_DATA[2]}"
|
||||
unset ACCOUNT_DATA
|
||||
|
||||
cd "$ROOT_DIR"
|
||||
|
||||
if ! docker compose ps --status running --services | grep -qx 'ac-database'; then
|
||||
printf '%s\n' 'ac-database is not running; start it with ./start-server.sh' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ACCOUNT_ID="$(docker compose exec -T ac-database bash -lc \
|
||||
'mysql -N -B -uroot -p"$MYSQL_ROOT_PASSWORD" acore_auth' \
|
||||
<<<"SELECT id FROM account WHERE username = CONVERT(0x$ACCOUNT_HEX USING ascii) LIMIT 1;")"
|
||||
|
||||
if ! [[ "$ACCOUNT_ID" =~ ^[0-9]+$ ]]; then
|
||||
printf 'game account does not exist: %s\n' "$CANONICAL_ACCOUNT" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
export MOONWELL_ISSUER_CANONICAL_ACCOUNT="$CANONICAL_ACCOUNT"
|
||||
mapfile -t TICKET_DATA < <(python3 <<'PY'
|
||||
import hashlib
|
||||
import os
|
||||
import secrets
|
||||
|
||||
alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
account = os.environ["MOONWELL_ISSUER_CANONICAL_ACCOUNT"]
|
||||
ticket = "".join(secrets.choice(alphabet) for _ in range(16))
|
||||
generation = secrets.token_bytes(16)
|
||||
salt = secrets.token_bytes(32)
|
||||
|
||||
n = int("894B645E89E1535BBDAD5B8B290650530801B18EBFBF5E8FAB3C82872A3E9BB7", 16)
|
||||
inner = hashlib.sha1(f"{account}:{ticket}".encode("ascii")).digest()
|
||||
x = hashlib.sha1(salt + inner).digest()
|
||||
verifier = pow(7, int.from_bytes(x, "little"), n).to_bytes(32, "little")
|
||||
|
||||
print(generation.hex().upper())
|
||||
print(salt.hex().upper())
|
||||
print(verifier.hex().upper())
|
||||
print(ticket)
|
||||
PY
|
||||
)
|
||||
unset MOONWELL_ISSUER_CANONICAL_ACCOUNT
|
||||
|
||||
GENERATION_HEX="${TICKET_DATA[0]}"
|
||||
SALT_HEX="${TICKET_DATA[1]}"
|
||||
VERIFIER_HEX="${TICKET_DATA[2]}"
|
||||
TICKET="${TICKET_DATA[3]}"
|
||||
unset TICKET_DATA
|
||||
|
||||
SQL="INSERT INTO launcher_ticket
|
||||
(account_id, generation_id, srp_salt, srp_verifier, client_build, issued_at, expires_at)
|
||||
VALUES
|
||||
($ACCOUNT_ID, UNHEX('$GENERATION_HEX'), UNHEX('$SALT_HEX'), UNHEX('$VERIFIER_HEX'),
|
||||
$CLIENT_BUILD, UTC_TIMESTAMP(6), UTC_TIMESTAMP(6) + INTERVAL $TTL_SECONDS SECOND)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
generation_id = VALUES(generation_id),
|
||||
srp_salt = VALUES(srp_salt),
|
||||
srp_verifier = VALUES(srp_verifier),
|
||||
client_build = VALUES(client_build),
|
||||
launcher_session_hash = NULL,
|
||||
issued_at = VALUES(issued_at),
|
||||
expires_at = VALUES(expires_at);
|
||||
SELECT DATE_FORMAT(expires_at, '%Y-%m-%dT%H:%i:%sZ')
|
||||
FROM launcher_ticket
|
||||
WHERE account_id = $ACCOUNT_ID;"
|
||||
|
||||
EXPIRES_AT="$(docker compose exec -T ac-database bash -lc \
|
||||
'mysql -N -B -uroot -p"$MYSQL_ROOT_PASSWORD" acore_auth' <<<"$SQL")"
|
||||
|
||||
printf '{"account":%s,"ticket":"%s","expires_at":"%s","client_build":%s}\n' \
|
||||
"$ACCOUNT_JSON" "$TICKET" "$EXPIRES_AT" "$CLIENT_BUILD"
|
||||
|
||||
TICKET="$(printf '%*s' 16 '')"
|
||||
unset TICKET SQL GENERATION_HEX SALT_HEX VERIFIER_HEX ACCOUNT_HEX CANONICAL_ACCOUNT ACCOUNT_JSON
|
||||
@@ -183,6 +183,7 @@ bool AuthSession::Update()
|
||||
return false;
|
||||
|
||||
_queryProcessor.ProcessReadyCallbacks();
|
||||
_transactionProcessor.ProcessReadyCallbacks();
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -309,7 +310,9 @@ bool AuthSession::HandleLogonChallenge()
|
||||
// Get the account details from the account table
|
||||
LoginDatabasePreparedStatement* stmt = LoginDatabase.GetPreparedStatement(LOGIN_SEL_LOGONCHALLENGE);
|
||||
stmt->SetData(0, GetRemoteIpAddress().to_string());
|
||||
stmt->SetData(1, login);
|
||||
stmt->SetData(1, GetRemoteIpAddress().to_string());
|
||||
stmt->SetData(2, _build);
|
||||
stmt->SetData(3, login);
|
||||
|
||||
_queryProcessor.AddCallback(LoginDatabase.AsyncQuery(stmt).WithPreparedCallback(std::bind(&AuthSession::LogonChallengeCallback, this, std::placeholders::_1)));
|
||||
return true;
|
||||
@@ -387,8 +390,21 @@ void AuthSession::LogonChallengeCallback(PreparedQueryResult result)
|
||||
|
||||
uint8 securityFlags = 0;
|
||||
|
||||
// Check if a TOTP token is needed
|
||||
if (!fields[12].IsNull())
|
||||
_isDeveloperLogin = fields[15].Get<bool>();
|
||||
_launcherTicketGeneration.reset();
|
||||
|
||||
if (!_isDeveloperLogin && (fields[16].IsNull() || fields[17].IsNull() || fields[18].IsNull()))
|
||||
{
|
||||
pkt << uint8(WOW_FAIL_UNKNOWN_ACCOUNT);
|
||||
SendPacket(pkt);
|
||||
LOG_INFO("server.authserver", "'{}:{}' [AuthChallenge] launcher ticket required for account id {}",
|
||||
ipAddress, port, _accountInfo.Id);
|
||||
return;
|
||||
}
|
||||
|
||||
// Launcher authentication completes MFA before ticket issuance. Only the
|
||||
// explicitly allowlisted developer flow may request the legacy TOTP UI.
|
||||
if (_isDeveloperLogin && !fields[12].IsNull())
|
||||
{
|
||||
securityFlags = 4;
|
||||
_totpSecret = fields[12].Get<Binary>();
|
||||
@@ -406,9 +422,19 @@ void AuthSession::LogonChallengeCallback(PreparedQueryResult result)
|
||||
}
|
||||
}
|
||||
|
||||
_srp6.emplace(_accountInfo.Login,
|
||||
fields[13].Get<Binary, Acore::Crypto::SRP6::SALT_LENGTH>(),
|
||||
fields[14].Get<Binary, Acore::Crypto::SRP6::VERIFIER_LENGTH>());
|
||||
if (_isDeveloperLogin)
|
||||
{
|
||||
_srp6.emplace(_accountInfo.Login,
|
||||
fields[13].Get<Binary, Acore::Crypto::SRP6::SALT_LENGTH>(),
|
||||
fields[14].Get<Binary, Acore::Crypto::SRP6::VERIFIER_LENGTH>());
|
||||
}
|
||||
else
|
||||
{
|
||||
_launcherTicketGeneration = fields[16].Get<Binary, 16>();
|
||||
_srp6.emplace(_accountInfo.Login,
|
||||
fields[17].Get<Binary, Acore::Crypto::SRP6::SALT_LENGTH>(),
|
||||
fields[18].Get<Binary, Acore::Crypto::SRP6::VERIFIER_LENGTH>());
|
||||
}
|
||||
|
||||
// Fill the response packet with the result
|
||||
if (AuthHelper::IsAcceptedClientBuild(_build))
|
||||
@@ -515,45 +541,77 @@ bool AuthSession::HandleLogonProof()
|
||||
// No SQL injection (escaped user name) and IP address as received by socket
|
||||
|
||||
std::string address = sConfigMgr->GetOption<bool>("AllowLoggingIPAddressesInDatabase", true, true) ? GetRemoteIpAddress().to_string() : "0.0.0.0";
|
||||
LoginDatabasePreparedStatement* stmt = LoginDatabase.GetPreparedStatement(LOGIN_UPD_LOGONPROOF);
|
||||
stmt->SetData(0, _sessionKey);
|
||||
stmt->SetData(1, address);
|
||||
stmt->SetData(2, GetLocaleByName(_localizationName));
|
||||
stmt->SetData(3, _os);
|
||||
stmt->SetData(4, _accountInfo.Login);
|
||||
_queryProcessor.AddCallback(LoginDatabase.AsyncQuery(stmt)
|
||||
.WithPreparedCallback([this, M2 = Acore::Crypto::SRP6::GetSessionVerifier(logonProof->A, logonProof->clientM, _sessionKey)](PreparedQueryResult const&)
|
||||
Acore::Crypto::SHA1::Digest serverProof = Acore::Crypto::SRP6::GetSessionVerifier(logonProof->A, logonProof->clientM, _sessionKey);
|
||||
|
||||
if (_isDeveloperLogin)
|
||||
{
|
||||
// Finish SRP6 and send the final result to the client
|
||||
ByteBuffer packet;
|
||||
if (_expversion & POST_BC_EXP_FLAG) // 2.x and 3.x clients
|
||||
LoginDatabasePreparedStatement* stmt = LoginDatabase.GetPreparedStatement(LOGIN_UPD_LOGONPROOF);
|
||||
stmt->SetData(0, _sessionKey);
|
||||
stmt->SetData(1, address);
|
||||
stmt->SetData(2, GetLocaleByName(_localizationName));
|
||||
stmt->SetData(3, _os);
|
||||
stmt->SetData(4, _accountInfo.Login);
|
||||
_queryProcessor.AddCallback(LoginDatabase.AsyncQuery(stmt)
|
||||
.WithPreparedCallback([this, serverProof](PreparedQueryResult const&)
|
||||
{
|
||||
sAuthLogonProof_S proof;
|
||||
proof.M2 = M2;
|
||||
proof.cmd = AUTH_LOGON_PROOF;
|
||||
proof.error = 0;
|
||||
proof.AccountFlags = _accountInfo.Flags;
|
||||
proof.SurveyId = 0;
|
||||
proof.LoginFlags = 0; // 0x1 = has account message
|
||||
SendLogonProofSuccess(serverProof);
|
||||
}));
|
||||
}
|
||||
else
|
||||
{
|
||||
ASSERT(_launcherTicketGeneration);
|
||||
std::array<uint8, 16> claimNonce = Acore::Crypto::GetRandomBytes<16>();
|
||||
|
||||
packet.resize(sizeof(proof));
|
||||
std::memcpy(packet.contents(), &proof, sizeof(proof));
|
||||
}
|
||||
else
|
||||
LoginDatabaseTransaction transaction = LoginDatabase.BeginTransaction();
|
||||
|
||||
LoginDatabasePreparedStatement* consume = LoginDatabase.GetPreparedStatement(LOGIN_INS_LAUNCHER_TICKET_CONSUMPTION);
|
||||
consume->SetData(0, claimNonce);
|
||||
consume->SetData(1, _accountInfo.Id);
|
||||
consume->SetData(2, *_launcherTicketGeneration);
|
||||
transaction->Append(consume);
|
||||
|
||||
LoginDatabasePreparedStatement* remove = LoginDatabase.GetPreparedStatement(LOGIN_DEL_LAUNCHER_TICKET);
|
||||
remove->SetData(0, _accountInfo.Id);
|
||||
remove->SetData(1, *_launcherTicketGeneration);
|
||||
transaction->Append(remove);
|
||||
|
||||
LoginDatabasePreparedStatement* update = LoginDatabase.GetPreparedStatement(LOGIN_UPD_LAUNCHER_LOGONPROOF);
|
||||
update->SetData(0, _sessionKey);
|
||||
update->SetData(1, address);
|
||||
update->SetData(2, GetLocaleByName(_localizationName));
|
||||
update->SetData(3, _os);
|
||||
update->SetData(4, _accountInfo.Id);
|
||||
update->SetData(5, *_launcherTicketGeneration);
|
||||
update->SetData(6, claimNonce);
|
||||
transaction->Append(update);
|
||||
|
||||
std::array<uint8, 16> generation = *_launcherTicketGeneration;
|
||||
_transactionProcessor.AddCallback(LoginDatabase.AsyncCommitTransaction(transaction)).AfterComplete(
|
||||
[this, serverProof, generation, claimNonce](bool success)
|
||||
{
|
||||
sAuthLogonProof_S_Old proof;
|
||||
proof.M2 = M2;
|
||||
proof.cmd = AUTH_LOGON_PROOF;
|
||||
proof.error = 0;
|
||||
proof.unk2 = 0x00;
|
||||
if (!success)
|
||||
{
|
||||
SendLogonProofFailure();
|
||||
return;
|
||||
}
|
||||
|
||||
packet.resize(sizeof(proof));
|
||||
std::memcpy(packet.contents(), &proof, sizeof(proof));
|
||||
}
|
||||
LoginDatabasePreparedStatement* claim = LoginDatabase.GetPreparedStatement(LOGIN_SEL_LAUNCHER_TICKET_CLAIM);
|
||||
claim->SetData(0, _accountInfo.Id);
|
||||
claim->SetData(1, generation);
|
||||
claim->SetData(2, claimNonce);
|
||||
_queryProcessor.AddCallback(LoginDatabase.AsyncQuery(claim)
|
||||
.WithPreparedCallback([this, serverProof](PreparedQueryResult result)
|
||||
{
|
||||
if (!result)
|
||||
{
|
||||
SendLogonProofFailure();
|
||||
return;
|
||||
}
|
||||
|
||||
SendPacket(packet);
|
||||
_status = STATUS_AUTHED;
|
||||
}));
|
||||
SendLogonProofSuccess(serverProof);
|
||||
}));
|
||||
});
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -618,6 +676,47 @@ bool AuthSession::HandleLogonProof()
|
||||
return true;
|
||||
}
|
||||
|
||||
void AuthSession::SendLogonProofFailure()
|
||||
{
|
||||
ByteBuffer packet;
|
||||
packet << uint8(AUTH_LOGON_PROOF);
|
||||
packet << uint8(WOW_FAIL_UNKNOWN_ACCOUNT);
|
||||
packet << uint16(0);
|
||||
SendPacket(packet);
|
||||
}
|
||||
|
||||
void AuthSession::SendLogonProofSuccess(Acore::Crypto::SHA1::Digest const& serverProof)
|
||||
{
|
||||
ByteBuffer packet;
|
||||
if (_expversion & POST_BC_EXP_FLAG)
|
||||
{
|
||||
sAuthLogonProof_S proof;
|
||||
proof.M2 = serverProof;
|
||||
proof.cmd = AUTH_LOGON_PROOF;
|
||||
proof.error = 0;
|
||||
proof.AccountFlags = _accountInfo.Flags;
|
||||
proof.SurveyId = 0;
|
||||
proof.LoginFlags = 0;
|
||||
|
||||
packet.resize(sizeof(proof));
|
||||
std::memcpy(packet.contents(), &proof, sizeof(proof));
|
||||
}
|
||||
else
|
||||
{
|
||||
sAuthLogonProof_S_Old proof;
|
||||
proof.M2 = serverProof;
|
||||
proof.cmd = AUTH_LOGON_PROOF;
|
||||
proof.error = 0;
|
||||
proof.unk2 = 0x00;
|
||||
|
||||
packet.resize(sizeof(proof));
|
||||
std::memcpy(packet.contents(), &proof, sizeof(proof));
|
||||
}
|
||||
|
||||
SendPacket(packet);
|
||||
_status = STATUS_AUTHED;
|
||||
}
|
||||
|
||||
bool AuthSession::HandleReconnectChallenge()
|
||||
{
|
||||
_status = STATUS_CLOSED;
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
#include "QueryResult.h"
|
||||
#include "SRP6.h"
|
||||
#include "Socket.h"
|
||||
#include "Transaction.h"
|
||||
#include <boost/asio/ip/tcp.hpp>
|
||||
|
||||
using boost::asio::ip::tcp;
|
||||
@@ -89,10 +90,14 @@ private:
|
||||
void LogonChallengeCallback(PreparedQueryResult result);
|
||||
void ReconnectChallengeCallback(PreparedQueryResult result);
|
||||
void RealmListCallback(PreparedQueryResult result);
|
||||
void SendLogonProofFailure();
|
||||
void SendLogonProofSuccess(Acore::Crypto::SHA1::Digest const& serverProof);
|
||||
|
||||
bool VerifyVersion(uint8 const* a, int32 aLength, Acore::Crypto::SHA1::Digest const& versionProof, bool isReconnect);
|
||||
|
||||
Optional<Acore::Crypto::SRP6> _srp6;
|
||||
Optional<std::array<uint8, 16>> _launcherTicketGeneration;
|
||||
bool _isDeveloperLogin = false;
|
||||
SessionKey _sessionKey = {};
|
||||
std::array<uint8, 16> _reconnectProof = {};
|
||||
|
||||
@@ -106,6 +111,7 @@ private:
|
||||
uint8 _expversion;
|
||||
|
||||
QueryCallbackProcessor _queryProcessor;
|
||||
AsyncCallbackProcessor<TransactionCallback> _transactionProcessor;
|
||||
};
|
||||
|
||||
#pragma pack(push, 1)
|
||||
|
||||
@@ -27,11 +27,14 @@ void LoginDatabaseConnection::DoPrepareStatements()
|
||||
"SELECT a.id, a.username, a.locked, a.lock_country, a.last_ip, a.Flags, a.failed_logins, "
|
||||
"ab.unbandate > UNIX_TIMESTAMP() OR ab.unbandate = ab.bandate, ab.unbandate = ab.bandate, "
|
||||
"ipb.unbandate > UNIX_TIMESTAMP() OR ipb.unbandate = ipb.bandate, ipb.unbandate = ipb.bandate, "
|
||||
"aa.gmlevel, a.totp_secret, a.salt, a.verifier "
|
||||
"aa.gmlevel, a.totp_secret, a.salt, a.verifier, "
|
||||
"lda.account_id IS NOT NULL, lt.generation_id, lt.srp_salt, lt.srp_verifier "
|
||||
"FROM account a "
|
||||
"LEFT JOIN account_access aa ON a.id = aa.id "
|
||||
"LEFT JOIN account_banned ab ON ab.id = a.id AND ab.active = 1 "
|
||||
"LEFT JOIN ip_banned ipb ON ipb.ip = ? "
|
||||
"LEFT JOIN launcher_dev_account lda ON lda.account_id = a.id AND lda.allowed_ip = ? AND lda.enabled = 1 "
|
||||
"LEFT JOIN launcher_ticket lt ON lt.account_id = a.id AND lt.expires_at > UTC_TIMESTAMP(6) AND lt.client_build = ? "
|
||||
"WHERE a.username = ?", CONNECTION_ASYNC);
|
||||
PrepareStatement(LOGIN_SEL_RECONNECTCHALLENGE,
|
||||
"SELECT a.id, a.username, a.locked, a.lock_country, a.last_ip, a.Flags, a.failed_logins, "
|
||||
@@ -42,11 +45,11 @@ void LoginDatabaseConnection::DoPrepareStatements()
|
||||
"LEFT JOIN account_access aa ON a.id = aa.id "
|
||||
"LEFT JOIN account_banned ab ON ab.id = a.id AND ab.active = 1 "
|
||||
"LEFT JOIN ip_banned ipb ON ipb.ip = ? "
|
||||
"WHERE a.username = ? AND a.session_key IS NOT NULL", CONNECTION_ASYNC);
|
||||
"WHERE a.username = ? AND a.session_key IS NOT NULL AND a.session_auth IN ('launcher', 'dev')", CONNECTION_ASYNC);
|
||||
PrepareStatement(LOGIN_SEL_ACCOUNT_INFO_BY_NAME, "SELECT a.id, a.session_key, a.last_ip, a.locked, a.lock_country, a.expansion, a.Flags, a.mutetime, a.locale, a.recruiter, a.os, a.totaltime, "
|
||||
"aa.gmlevel, ab.unbandate > UNIX_TIMESTAMP() OR ab.unbandate = ab.bandate, r.id FROM account a LEFT JOIN account_access aa ON a.id = aa.id AND aa.RealmID IN (-1, ?) "
|
||||
"LEFT JOIN account_banned ab ON a.id = ab.id AND ab.active = 1 LEFT JOIN account r ON a.id = r.recruiter WHERE a.username = ? "
|
||||
"AND a.session_key IS NOT NULL ORDER BY aa.RealmID DESC LIMIT 1", CONNECTION_ASYNC);
|
||||
"AND a.session_key IS NOT NULL AND a.session_auth IN ('launcher', 'dev') ORDER BY aa.RealmID DESC LIMIT 1", CONNECTION_ASYNC);
|
||||
PrepareStatement(LOGIN_SEL_IP_INFO, "SELECT unbandate > UNIX_TIMESTAMP() OR unbandate = bandate AS banned, NULL as country FROM ip_banned WHERE ip = ?", CONNECTION_ASYNC);
|
||||
PrepareStatement(LOGIN_SEL_REALMLIST, "SELECT id, name, address, localAddress, localSubnetMask, port, icon, flag, timezone, allowedSecurityLevel, population, gamebuild FROM realmlist WHERE flag <> 3 ORDER BY name", CONNECTION_SYNCH);
|
||||
PrepareStatement(LOGIN_DEL_EXPIRED_IP_BANS, "DELETE FROM ip_banned WHERE unbandate<>bandate AND unbandate<=UNIX_TIMESTAMP()", CONNECTION_ASYNC);
|
||||
@@ -61,7 +64,20 @@ void LoginDatabaseConnection::DoPrepareStatements()
|
||||
PrepareStatement(LOGIN_INS_ACCOUNT_AUTO_BANNED, "INSERT INTO account_banned VALUES (?, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()+?, 'realmd', 'Failed login autoban', 1)", CONNECTION_ASYNC);
|
||||
PrepareStatement(LOGIN_DEL_ACCOUNT_BANNED, "DELETE FROM account_banned WHERE id = ?", CONNECTION_ASYNC);
|
||||
PrepareStatement(LOGIN_UPD_LOGON, "UPDATE account SET salt = ?, verifier = ? WHERE id = ?", CONNECTION_ASYNC);
|
||||
PrepareStatement(LOGIN_UPD_LOGONPROOF, "UPDATE account SET session_key = ?, last_ip = ?, last_login = NOW(), locale = ?, failed_logins = 0, os = ? WHERE username = ?", CONNECTION_ASYNC);
|
||||
PrepareStatement(LOGIN_UPD_LOGONPROOF, "UPDATE account SET session_key = ?, session_auth = 'dev', last_ip = ?, last_login = NOW(), locale = ?, failed_logins = 0, os = ? WHERE username = ?", CONNECTION_ASYNC);
|
||||
PrepareStatement(LOGIN_INS_LAUNCHER_TICKET_CONSUMPTION,
|
||||
"INSERT INTO launcher_ticket_consumption (generation_id, account_id, claim_nonce, consumed_at) "
|
||||
"SELECT generation_id, account_id, ?, UTC_TIMESTAMP(6) FROM launcher_ticket "
|
||||
"WHERE account_id = ? AND generation_id = ? AND expires_at > UTC_TIMESTAMP(6)", CONNECTION_ASYNC);
|
||||
PrepareStatement(LOGIN_DEL_LAUNCHER_TICKET,
|
||||
"DELETE FROM launcher_ticket WHERE account_id = ? AND generation_id = ?", CONNECTION_ASYNC);
|
||||
PrepareStatement(LOGIN_UPD_LAUNCHER_LOGONPROOF,
|
||||
"UPDATE account a JOIN launcher_ticket_consumption c ON c.account_id = a.id "
|
||||
"SET a.session_key = ?, a.session_auth = 'launcher', a.last_ip = ?, a.last_login = NOW(), "
|
||||
"a.locale = ?, a.failed_logins = 0, a.os = ? "
|
||||
"WHERE a.id = ? AND c.generation_id = ? AND c.claim_nonce = ?", CONNECTION_ASYNC);
|
||||
PrepareStatement(LOGIN_SEL_LAUNCHER_TICKET_CLAIM,
|
||||
"SELECT 1 FROM launcher_ticket_consumption WHERE account_id = ? AND generation_id = ? AND claim_nonce = ?", CONNECTION_ASYNC);
|
||||
PrepareStatement(LOGIN_UPD_FAILEDLOGINS, "UPDATE account SET failed_logins = failed_logins + 1 WHERE username = ?", CONNECTION_ASYNC);
|
||||
PrepareStatement(LOGIN_SEL_FAILEDLOGINS, "SELECT id, failed_logins FROM account WHERE username = ?", CONNECTION_SYNCH);
|
||||
PrepareStatement(LOGIN_SEL_ACCOUNT_ID_BY_NAME, "SELECT id FROM account WHERE username = ?", CONNECTION_SYNCH);
|
||||
|
||||
@@ -41,6 +41,10 @@ enum LoginDatabaseStatements : uint32
|
||||
LOGIN_DEL_ACCOUNT_BANNED,
|
||||
LOGIN_UPD_LOGON,
|
||||
LOGIN_UPD_LOGONPROOF,
|
||||
LOGIN_INS_LAUNCHER_TICKET_CONSUMPTION,
|
||||
LOGIN_DEL_LAUNCHER_TICKET,
|
||||
LOGIN_UPD_LAUNCHER_LOGONPROOF,
|
||||
LOGIN_SEL_LAUNCHER_TICKET_CLAIM,
|
||||
LOGIN_SEL_LOGONCHALLENGE,
|
||||
LOGIN_SEL_RECONNECTCHALLENGE,
|
||||
LOGIN_UPD_FAILEDLOGINS,
|
||||
|
||||
Reference in New Issue
Block a user