5 Commits

Author SHA1 Message Date
sindoring 907e577797 воскрешение в начале подземелья, 2026-09-19 22:35:30 +04:00
sindoring d9e793cac1 fix 2026-08-30 17:45:23 +04:00
sindoring b6770199c3 fix 2026-08-30 16:35:41 +04:00
sindoring f06300031e Merge branch 'launcher-authoritive-auth' 2026-08-30 02:14:13 +04:00
sindoring 3405057e5b wip 2026-08-17 20:24:17 +04:00
26 changed files with 16654 additions and 13113 deletions
+16
View File
@@ -300,6 +300,22 @@ ACORE_AUTOBALANCE_STAT_MODIFIER_RAID_HEROIC_BOSS_HEALTH=0.60
# Множитель урона heroic-рейдовых боссов.
ACORE_AUTOBALANCE_STAT_MODIFIER_RAID_HEROIC_BOSS_DAMAGE=0.45
# ------------------------------------------------------------------------------
# mod-auto-resurrect
# ------------------------------------------------------------------------------
# 1 = включает автоматическое воскрешение после выхода духом из инстанса.
ACORE_AUTO_RESURRECT_ENABLE=1
# 1 = сообщает игроку о включенном модуле при входе.
ACORE_AUTO_RESURRECT_NOTIFICATION=0
# Типы инстансов, в которых разрешено автоматическое воскрешение.
ACORE_AUTO_RESURRECT_DUNGEON=1
ACORE_AUTO_RESURRECT_HEROIC_DUNGEON=1
ACORE_AUTO_RESURRECT_RAID=1
ACORE_AUTO_RESURRECT_HEROIC_RAID=1
# ------------------------------------------------------------------------------
# mod-individual-progression
# ------------------------------------------------------------------------------
@@ -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;
+371
View File
@@ -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-дистрибутивы.
+84
View File
@@ -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 подряд и убедиться, что работает только второй.
+1507 -1147
View File
File diff suppressed because it is too large Load Diff
+13793 -11919
View File
File diff suppressed because it is too large Load Diff
+167
View File
@@ -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
+8
View File
@@ -0,0 +1,8 @@
[*]
charset = utf-8
indent_style = space
indent_size = 4
tab_width = 4
insert_final_newline = true
trim_trailing_whitespace = true
max_line_length = 80
@@ -0,0 +1,45 @@
### TITLE
## Type(Scope/Subscope): Commit ultra short explanation
## |---- Write below the examples with a maximum of 50 characters ----|
## Example 1: fix(DB/SAI): Missing spell to NPC Hogger
## Example 2: fix(CORE/Raid): Phase 2 of Ragnaros
## Example 3: feat(CORE/Commands): New GM command to do something
### DESCRIPTION
## Explain why this change is being made, what does it fix etc...
## |---- Write below the examples with a maximum of 72 characters per lines ----|
## Example: Hogger (id: 492) was not charging player when being engaged.
## Provide links to any issue, commit, pull request or other resource
## Example 1: Closes issue #23
## Example 2: Ported from other project's commit (link)
## Example 3: References taken from wowpedia / wowhead / wowwiki / https://wowgaming.altervista.org/aowow/
## =======================================================
## EXTRA INFOS
## =======================================================
## "Type" can be:
## feat (new feature)
## fix (bug fix)
## refactor (refactoring production code)
## style (formatting, missing semi colons, etc; no code change)
## docs (changes to documentation)
## test (adding or refactoring tests; no production code change)
## chore (updating bash scripts, git files etc; no production code change)
## --------------------
## Remember to
## Capitalize the subject line
## Use the imperative mood in the subject line
## Do not end the subject line with a period
## Separate subject from body with a blank line
## Use the body to explain what and why rather than how
## Can use multiple lines with "-" for bullet points in body
## --------------------
## More info here https://www.conventionalcommits.org/en/v1.0.0-beta.2/
## =======================================================
## "Scope" can be:
## CORE (core related, c++)
## DB (database related, sql)
## =======================================================
## "Subscope" is optional and depends on the nature of the commit.
## =======================================================
+105
View File
@@ -0,0 +1,105 @@
## AUTO-DETECT
## Handle line endings automatically for files detected as
## text and leave all files detected as binary untouched.
## This will handle all files NOT defined below.
* text=auto eol=lf
# Text
*.conf text
*.conf.dist text
*.cmake text
## Scripts
*.sh text
*.fish text
*.lua text
## SQL
*.sql text
## C++
*.c text
*.cc text
*.cxx text
*.cpp text
*.c++ text
*.hpp text
*.h text
*.h++ text
*.hh text
## For documentation
# Documents
*.doc diff=astextplain
*.DOC diff=astextplain
*.docx diff=astextplain
*.DOCX diff=astextplain
*.dot diff=astextplain
*.DOT diff=astextplain
*.pdf diff=astextplain
*.PDF diff=astextplain
*.rtf diff=astextplain
*.RTF diff=astextplain
## DOCUMENTATION
*.markdown text
*.md text
*.mdwn text
*.mdown text
*.mkd text
*.mkdn text
*.mdtxt text
*.mdtext text
*.txt text
AUTHORS text
CHANGELOG text
CHANGES text
CONTRIBUTING text
COPYING text
copyright text
*COPYRIGHT* text
INSTALL text
license text
LICENSE text
NEWS text
readme text
*README* text
TODO text
## GRAPHICS
*.ai binary
*.bmp binary
*.eps binary
*.gif binary
*.ico binary
*.jng binary
*.jp2 binary
*.jpg binary
*.jpeg binary
*.jpx binary
*.jxr binary
*.pdf binary
*.png binary
*.psb binary
*.psd binary
*.svg text
*.svgz binary
*.tif binary
*.tiff binary
*.wbmp binary
*.webp binary
## ARCHIVES
*.7z binary
*.gz binary
*.jar binary
*.rar binary
*.tar binary
*.zip binary
## EXECUTABLES
*.exe binary
*.pyc binary
@@ -0,0 +1,49 @@
name: core-build
on:
push:
pull_request:
jobs:
build:
strategy:
fail-fast: false
matrix:
compiler: [clang]
runs-on: ubuntu-latest
name: ${{ matrix.compiler }}
env:
COMPILER: ${{ matrix.compiler }}
steps:
- uses: actions/checkout@v2
with:
repository: 'azerothcore/azerothcore-wotlk'
ref: 'master'
submodules: 'recursive'
- uses: actions/checkout@v2
with:
submodules: 'recursive'
path: 'modules/skeleton-module'
- name: Cache
uses: actions/cache@v2
with:
path: /home/runner/.ccache
key: ccache:${{ matrix.compiler }}:${{ github.ref }}:${{ github.sha }}
restore-keys: |
ccache:${{ matrix.compiler }}:${{ github.ref }}
ccache:${{ matrix.compiler }}
- name: Configure OS
run: source ./acore.sh install-deps
env:
CONTINUOUS_INTEGRATION: true
- name: Create conf/config.sh
run: source ./apps/ci/ci-conf.sh
- name: Import db
run: source ./apps/ci/ci-import-db.sh
- name: Build
run: source ./apps/ci/ci-compile.sh
- name: Dry run
run: source ./apps/ci/ci-worldserver-dry-run.sh
- name: Check startup errors
run: source ./apps/ci/ci-error-check.sh
- name: Run unit tests
run: source ./apps/ci/ci-run-unit-tests.sh
+48
View File
@@ -0,0 +1,48 @@
!.gitignore
#
#Generic
#
.directory
.mailmap
*.orig
*.rej
*.*~
.hg/
*.kdev*
.DS_Store
CMakeLists.txt.user
*.bak
*.patch
*.diff
*.REMOTE.*
*.BACKUP.*
*.BASE.*
*.LOCAL.*
#
# IDE & other softwares
#
/.settings/
/.externalToolBuilders/*
# exclude in all levels
nbproject/
.sync.ffs_db
*.kate-swp
#
# Eclipse
#
*.pydevproject
.metadata
.gradle
tmp/
*.tmp
*.swp
*~.nib
local.properties
.settings/
.loadpath
.project
.cproject
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 AzerothCore
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+22
View File
@@ -0,0 +1,22 @@
# Auto Resurrect
## Description
This mod allows players to automatically resurrect at the start of an instance or raid when releasing their spirit.
## Requirements
- AzerothCore v4.0.0+
## Installation
```
1) Simply `git clone` the module under the `modules` directory of your AzerothCore source or copy paste it manually.
2) Re-run cmake and launch a clean build of AzerothCore.
3) Go to your server configuration directory (where your `worldserver` or `worldserver.exe` is), copy `mod-auto-resurrect.conf.dist` to `mod-auto-resurrect.conf` and edit that new file
```
## Credits
- [Elmegaard](https://github.com/Elmegaard) (author of the module)
- AzerothCore: [repository](https://github.com/azerothcore) - [website](http://azerothcore.org/) - [discord chat community](https://discord.gg/PaqQRkd)
@@ -0,0 +1,4 @@
#!/usr/bin/env bash
## CUSTOM SQL - Important file used by the db_assembler.sh
## Keep only the required variables (base sql files or updates, depending on the DB)
@@ -0,0 +1,57 @@
#
# Copyright (C) 2016+ AzerothCore <www.azerothcore.org>, released under GNU AGPL v3 license: https://github.com/azerothcore/azerothcore-wotlk/blob/master/LICENSE-AGPL3
#
[worldserver]
########################################
# Auto Resurrect configuration
########################################
#
# AutoResurrect.Enable
# Description: Enable or Disable the Auto Resurrect mod
# Default: 0 - Disabled
# 1 - Enabled
#
AutoResurrect.Enable = 0
#
# AutoResurrect.Notification
# Description: Notify players about this module on login
# Default: 0 - Disabled
# 1 - Enabled
#
AutoResurrect.Notification = 0
#
# AutoResurrect.Dungeon
# Description: Auto resurrect in normal dungeons
# Default: 0 - Disabled
# 1 - Enabled
#
AutoResurrect.Dungeon = 0
#
# AutoResurrect.HeroicDungeon
# Description: Auto resurrect in heroic dungeons
# Default: 0 - Disabled
# 1 - Enabled
#
AutoResurrect.HeroicDungeon = 0
#
# AutoResurrect.Raid
# Description: Auto resurrect in raids
# Default: 0 - Disabled
# 1 - Enabled
#
AutoResurrect.Raid = 0
#
# AutoResurrect.HeroicRaid
# Description: Auto resurrect in heroic raids
# Default: 0 - Disabled
# 1 - Enabled
#
AutoResurrect.HeroicRaid = 0
+9
View File
@@ -0,0 +1,9 @@
#!/usr/bin/env bash
MOD_AUTO_RESURRECT_ROOT="$( cd "$( dirname "${BASH_SOURCE[0]}" )/" && pwd )"
source $MOD_AUTO_RESURRECT_ROOT"/conf/conf.sh.dist"
if [ -f $MOD_AUTO_RESURRECT_ROOT"/conf/conf.sh" ]; then
source $MOD_AUTO_RESURRECT_ROOT"/conf/conf.sh"
fi
@@ -0,0 +1,4 @@
#!/usr/bin/env bash
## Set a local git commit template
git config --local commit.template ".git_commit_template.txt" ;
@@ -0,0 +1,15 @@
/*
* Copyright (C) 2016+ AzerothCore <www.azerothcore.org>, released under GNU AGPL v3 license: https://github.com/azerothcore/azerothcore-wotlk/blob/master/LICENSE-AGPL3
*/
// From SC
void AddAutoResurrectScripts();
// Add all
// cf. the naming convention https://github.com/azerothcore/azerothcore-wotlk/blob/master/doc/changelog/master.md#how-to-upgrade-4
// additionally replace all '-' in the module folder name with '_' here
void Addmod_auto_resurrectScripts()
{
AddAutoResurrectScripts();
}
@@ -0,0 +1,83 @@
/*
* Copyright (C) 2016+ AzerothCore <www.azerothcore.org>, released under GNU AGPL v3 license: https://github.com/azerothcore/azerothcore-wotlk/blob/master/LICENSE-AGPL3
*/
#include "ScriptMgr.h"
#include "Player.h"
#include "Config.h"
#include "Chat.h"
// Add player scripts
class AutoResurrect : public PlayerScript
{
public:
AutoResurrect()
: PlayerScript("AutoResurrect", {
PLAYERHOOK_ON_LOGIN,
PLAYERHOOK_ON_PLAYER_RELEASED_GHOST,
PLAYERHOOK_ON_MAP_CHANGED,
}) { }
std::map<uint32, const AreaTriggerTeleport*> playerLocation = {};
void DebugLog(Player* player, std::string log) {
if (sConfigMgr->GetOption<bool>("AutoResurrect.Enable", false) && sConfigMgr->GetOption<bool>("AutoResurrect.Notification", false)) {
ChatHandler(player->GetSession()).SendSysMessage(log);
}
}
void OnPlayerLogin(Player* player) override
{
if (sConfigMgr->GetOption<bool>("AutoResurrect.Enable", false) && sConfigMgr->GetOption<bool>("AutoResurrect.Notification", false))
{
DebugLog(player, "Auto Resurrect module enabled");
}
}
void OnPlayerReleasedGhost(Player* player) override {
if (!sConfigMgr->GetOption<bool>("AutoResurrect.Enable", false)) {
return;
}
Map* map = player->GetMap();
bool dungeons = sConfigMgr->GetOption<bool>("AutoResurrect.Dungeon", false);
bool heroics = sConfigMgr->GetOption<bool>("AutoResurrect.HeroicDungeon", false);
bool raids = sConfigMgr->GetOption<bool>("AutoResurrect.Raid", false);
bool heroicRaids = sConfigMgr->GetOption<bool>("AutoResurrect.HeroicRaid", false);
if ((dungeons && !map->IsHeroic() && map->IsDungeon()) ||
(heroics && map->IsHeroic() && map->IsDungeon()) ||
(raids && !map->IsHeroic() && map->IsRaid()) ||
(heroicRaids && map->IsHeroic() && map->IsRaid())) {
AreaTriggerTeleport const* at = sObjectMgr->GetMapEntranceTrigger(player->GetMapId());
playerLocation[player->GetGUID().GetCounter()] = at;
}
}
virtual void OnPlayerMapChanged(Player* player) override {
if (!sConfigMgr->GetOption<bool>("AutoResurrect.Enable", false)) {
return;
}
if (!playerLocation.count(player->GetGUID().GetCounter())) {
return;
}
AreaTriggerTeleport const* at = playerLocation[player->GetGUID().GetCounter()];
if (at == NULL) {
return;
}
player->ResurrectPlayer(1.0f);
player->TeleportTo(at->target_mapId, at->target_X, at->target_Y, at->target_Z, at->target_Orientation);
player->SaveToDB(false, false);
playerLocation[player->GetGUID().GetCounter()] = NULL;
}
};
// Add all scripts in one
void AddAutoResurrectScripts()
{
new AutoResurrect();
}
+25
View File
@@ -63,6 +63,13 @@ FORCE_PLAYERBOTS="${FORCE_PLAYERBOTS:-${ACORE_PLAYERBOTS_FORCE:-0}}"
: "${ACORE_AUTOBALANCE_STAT_MODIFIER_RAID_HEROIC_BOSS_HEALTH:=0.60}"
: "${ACORE_AUTOBALANCE_STAT_MODIFIER_RAID_HEROIC_BOSS_DAMAGE:=0.45}"
: "${ACORE_AUTO_RESURRECT_ENABLE:=1}"
: "${ACORE_AUTO_RESURRECT_NOTIFICATION:=0}"
: "${ACORE_AUTO_RESURRECT_DUNGEON:=1}"
: "${ACORE_AUTO_RESURRECT_HEROIC_DUNGEON:=1}"
: "${ACORE_AUTO_RESURRECT_RAID:=1}"
: "${ACORE_AUTO_RESURRECT_HEROIC_RAID:=1}"
: "${ACORE_PROGRESSION_ENABLE:=1}"
: "${ACORE_PROGRESSION_SIMPLE_CONFIG_OVERRIDE:=1}"
: "${ACORE_PROGRESSION_STARTING_PROGRESSION:=0}"
@@ -375,6 +382,8 @@ sync_module_dists() {
"$MODULES_ETC_DIR/mod_aoe_loot.conf.dist"
sync_file "$ROOT_DIR/modules/mod-autobalance/conf/AutoBalance.conf.dist" \
"$MODULES_ETC_DIR/AutoBalance.conf.dist"
sync_file "$ROOT_DIR/modules/mod-auto-resurrect/conf/mod-auto-resurrect.conf.dist" \
"$MODULES_ETC_DIR/mod-auto-resurrect.conf.dist"
sync_file "$ROOT_DIR/modules/mod-eluna/conf/mod_eluna.conf.dist" \
"$MODULES_ETC_DIR/mod_eluna.conf.dist"
sync_file "$ROOT_DIR/modules/mod-individual-progression/conf/individualProgression.conf.dist" \
@@ -440,6 +449,20 @@ AutoBalance.StatModifierRaidHeroic.Boss.Damage = ${ACORE_AUTOBALANCE_STAT_MODIFI
EOF
}
write_auto_resurrect_conf() {
write_file "$MODULES_ETC_DIR/mod-auto-resurrect.conf.dist" <<EOF
[worldserver]
# Managed by ./setup-modules.sh
AutoResurrect.Enable = ${ACORE_AUTO_RESURRECT_ENABLE}
AutoResurrect.Notification = ${ACORE_AUTO_RESURRECT_NOTIFICATION}
AutoResurrect.Dungeon = ${ACORE_AUTO_RESURRECT_DUNGEON}
AutoResurrect.HeroicDungeon = ${ACORE_AUTO_RESURRECT_HEROIC_DUNGEON}
AutoResurrect.Raid = ${ACORE_AUTO_RESURRECT_RAID}
AutoResurrect.HeroicRaid = ${ACORE_AUTO_RESURRECT_HEROIC_RAID}
EOF
}
write_individual_progression_conf() {
write_file "$MODULES_ETC_DIR/individualProgression.conf.dist" <<EOF
[worldserver]
@@ -748,6 +771,7 @@ cleanup_legacy_module_overrides() {
local legacy_files=(
"$MODULES_ETC_DIR/mod_progressive_quest_xp.conf"
"$MODULES_ETC_DIR/AutoBalance.conf"
"$MODULES_ETC_DIR/mod-auto-resurrect.conf"
"$MODULES_ETC_DIR/individualProgression.conf"
"$MODULES_ETC_DIR/mod_aoe_loot.conf"
"$MODULES_ETC_DIR/mod_eluna.conf"
@@ -807,6 +831,7 @@ main() {
cleanup_legacy_module_overrides
configure_worldserver
write_autobalance_conf
write_auto_resurrect_conf
write_individual_progression_conf
write_progressive_quest_xp_conf
write_aoe_loot_conf
+4 -4
View File
@@ -70,7 +70,7 @@ VALUES
81,
2,
21,
69,341,185,
69341185,
1.0,
1.42857,
1.0,
@@ -109,7 +109,9 @@ VALUES
1,
0,
'',
12340) ON DUPLICATE KEY UPDATE entry = entry;
12340) ON DUPLICATE KEY UPDATE
npcflag = VALUES(npcflag),
AIName = VALUES(AIName);
-- Add Squire localization
@@ -372,5 +374,3 @@ VALUES
'',
'',
'') ON DUPLICATE KEY UPDATE ID = ID;
+138 -39
View File
@@ -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,