Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f06300031e | |||
| 442c708e85 | |||
| 451d783994 | |||
| 1df95daa5a | |||
| f9cb8e6e44 | |||
| a0ec2976ef | |||
| 991afe708a | |||
| 72cede7878 | |||
| deff67efeb | |||
| 7c4b2c77bb | |||
| f04afd14d8 | |||
| 73c4349fa9 | |||
| 86cb8811b1 | |||
| 0c72d04d38 | |||
| 2f1278cb7c | |||
| f8177f5ac4 | |||
| 98858d29b5 | |||
| 334d127e6a | |||
| bca073ef4a | |||
| 423e140f46 | |||
| 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
|
||||
@@ -2,17 +2,24 @@ local PLAYER_EVENT_ON_LOGIN = 3
|
||||
local PLAYER_EVENT_ON_SPELL_CAST = 5
|
||||
local PLAYER_EVENT_ON_LOGOUT = 4
|
||||
local PLAYER_EVENT_ON_COMMAND = 42
|
||||
local PLAYER_EVENT_ON_BEFORE_TELEPORT = 67
|
||||
|
||||
local PREMIUM_FLAG_INDEX = 100
|
||||
|
||||
local ACCOUNT_FLAG_RECURRING_BILLING = 8192
|
||||
|
||||
local SUMMON_SQUIRE_SPELL_ID = 80000
|
||||
local HEARTHSTONE_SPELL_ID = 8690
|
||||
|
||||
local SQUIRE_CREATURE_TEMPLATE_ID = 9000001
|
||||
|
||||
local ALLIANCE_AUCTIONEER_CREATURE_TEMPLATE_ID = 15659
|
||||
local HORDE_AUCTIONEER_CREATURE_TEMPLATE_ID = 8673
|
||||
|
||||
local NPC_TEXT_ID = 10000000
|
||||
|
||||
local premiums_table = {}
|
||||
local squires_table = {}
|
||||
|
||||
---todo extract to utils
|
||||
---@param inputstr string
|
||||
---@param separator string
|
||||
@@ -30,20 +37,18 @@ end
|
||||
|
||||
---@return boolean
|
||||
local function IsPremium(player)
|
||||
return player:HasFlag(PREMIUM_FLAG_INDEX, 1)
|
||||
return premiums_table[player:GetAccountId()] == true
|
||||
end
|
||||
|
||||
local function OnPremiumPlayer(player)
|
||||
if not IsPremium(player) then
|
||||
return
|
||||
end
|
||||
|
||||
player:SendAreaTriggerMessage(
|
||||
'На вашем аккаунте активна подписка Premium! Благодарим за поддержку проекта MoonWell.')
|
||||
|
||||
player:LearnSpell(SUMMON_SQUIRE_SPELL_ID)
|
||||
player:SendBroadcastMessage("Милостью Элуны вам была выдана способность |cff71d5ff|Hspell:" ..
|
||||
SUMMON_SQUIRE_SPELL_ID .. "|h[" .. "Призыв Оруженосца]h|")
|
||||
|
||||
player:ResetSpellCooldown(HEARTHSTONE_SPELL_ID)
|
||||
end
|
||||
|
||||
local function RefreshPremiumStatus(player)
|
||||
@@ -58,7 +63,7 @@ local function RefreshPremiumStatus(player)
|
||||
local flags = query:GetUInt32(0)
|
||||
|
||||
if HasFlag(flags, ACCOUNT_FLAG_RECURRING_BILLING) then
|
||||
player:SetFlag(PREMIUM_FLAG_INDEX, 1)
|
||||
premiums_table[player:GetAccountId()] = true
|
||||
|
||||
OnPremiumPlayer(player)
|
||||
|
||||
@@ -108,7 +113,15 @@ end
|
||||
local function OnSummonSquireCast(player)
|
||||
local x, y, z, o = player:GetLocation()
|
||||
|
||||
if (squires_table[player:GetGUIDLow()] ~= nil) then
|
||||
local existing_squire = player:GetMap():GetWorldObject(squires_table[player:GetGUIDLow()])
|
||||
if (existing_squire ~= nil) then
|
||||
existing_squire:DespawnOrUnsummon()
|
||||
end
|
||||
end
|
||||
|
||||
local squire = player:SpawnCreature(SQUIRE_CREATURE_TEMPLATE_ID, x + 1, y + 1, z, o, 1, 30000)
|
||||
squires_table[player:GetGUIDLow()] = squire:GetGUID()
|
||||
|
||||
if squire then
|
||||
squire:SetFaction(player:GetFaction())
|
||||
@@ -141,26 +154,45 @@ local function OnGossipSelect(event, player, creature, sender, intid, code)
|
||||
player:GossipComplete()
|
||||
end
|
||||
|
||||
if intid == 2 then
|
||||
player:SendShowBank(creature)
|
||||
player:GossipComplete()
|
||||
end
|
||||
if intid == 2 then
|
||||
player:SendShowBank(creature)
|
||||
player:GossipComplete()
|
||||
end
|
||||
|
||||
if intid == 3 then
|
||||
player:SendAuctionMenu(creature)
|
||||
player:GossipComplete()
|
||||
end
|
||||
if intid == 3 then
|
||||
local x, y, z, o = player:GetLocation()
|
||||
local auctioneer_template = player:GetTeam() == 0 and ALLIANCE_AUCTIONEER_CREATURE_TEMPLATE_ID or
|
||||
HORDE_AUCTIONEER_CREATURE_TEMPLATE_ID
|
||||
local auctioneer = player:SpawnCreature(auctioneer_template, x, y, z, o, 1, 30000)
|
||||
auctioneer:SetDisplayId(11686)
|
||||
|
||||
if intid == 4 then
|
||||
player:DurabilityRepairAll()
|
||||
player:PlayDirectSound(1116)
|
||||
player:GossipComplete()
|
||||
end
|
||||
player:SendAuctionMenu(auctioneer)
|
||||
|
||||
player:GossipComplete()
|
||||
end
|
||||
|
||||
if intid == 4 then
|
||||
player:DurabilityRepairAll()
|
||||
player:PlayDirectSound(1116)
|
||||
player:GossipComplete()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function OnLogout(event, player)
|
||||
player:RemoveSpell(SUMMON_SQUIRE_SPELL_ID)
|
||||
squires_table[player:GetGUID()] = nil
|
||||
end
|
||||
|
||||
local function OnBeforeTeleport(event, player, mapid, x, y, z, orientation, options, target)
|
||||
if not IsPremium(player) then return end
|
||||
|
||||
-- Get current generic spell (SpellType 1)
|
||||
local current_spell = player:GetCurrentSpell(1)
|
||||
|
||||
if current_spell ~= nil and current_spell:GetEntry() == HEARTHSTONE_SPELL_ID then
|
||||
player:ResetSpellCooldown(HEARTHSTONE_SPELL_ID)
|
||||
end
|
||||
end
|
||||
|
||||
RegisterCreatureGossipEvent(SQUIRE_CREATURE_TEMPLATE_ID, 1, OnGossipHello)
|
||||
@@ -169,4 +201,4 @@ RegisterPlayerEvent(PLAYER_EVENT_ON_LOGIN, OnLogin)
|
||||
RegisterPlayerEvent(PLAYER_EVENT_ON_LOGOUT, OnLogout)
|
||||
RegisterPlayerEvent(PLAYER_EVENT_ON_COMMAND, OnCommand)
|
||||
RegisterPlayerEvent(PLAYER_EVENT_ON_SPELL_CAST, OnSpellCast)
|
||||
|
||||
RegisterPlayerEvent(PLAYER_EVENT_ON_BEFORE_TELEPORT, OnBeforeTeleport)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -233,6 +233,7 @@ namespace Hooks
|
||||
PLAYER_EVENT_ON_AURA_APPLY = 64, // (event, player, aura)
|
||||
PLAYER_EVENT_ON_HEAL = 65, // (event, player, target, gain) - Can return new heal amount
|
||||
PLAYER_EVENT_ON_DAMAGE = 66, // (event, player, target, damage) - Can return new damage amount
|
||||
PLAYER_EVENT_ON_BEFORE_TELEPORT = 67, // (event, player, mapid, x, y, z, orientation, options, target)
|
||||
|
||||
PLAYER_EVENT_COUNT
|
||||
};
|
||||
|
||||
@@ -497,6 +497,7 @@ public:
|
||||
void OnPlayerAuraApply(Player* player, Aura* aura);
|
||||
void OnPlayerHeal(Player* player, Unit* target, uint32& gain);
|
||||
void OnPlayerDamage(Player* player, Unit* target, uint32& gain);
|
||||
bool OnPlayerBeforeTeleport(Player* player, uint32 mapid, float x, float y, float z, float orientation, uint32 options, Unit* target);
|
||||
|
||||
/* Vehicle */
|
||||
void OnInstall(Vehicle* vehicle);
|
||||
|
||||
@@ -825,4 +825,20 @@ void Eluna::OnPlayerDamage(Player* player, Unit* target, uint32& damage)
|
||||
}
|
||||
|
||||
CleanUpStack(3);
|
||||
}
|
||||
}
|
||||
|
||||
bool Eluna::OnPlayerBeforeTeleport(Player* player, uint32 mapid, float x, float y, float z, float orientation, uint32 options, Unit* target)
|
||||
{
|
||||
START_HOOK_WITH_RETVAL(PLAYER_EVENT_ON_BEFORE_TELEPORT, true);
|
||||
|
||||
Push(player);
|
||||
Push(mapid);
|
||||
Push(x);
|
||||
Push(y);
|
||||
Push(z);
|
||||
Push(orientation);
|
||||
Push(options);
|
||||
Push(target);
|
||||
|
||||
return CallAllFunctionsBool(PlayerEventBindings, key, true);
|
||||
}
|
||||
|
||||
@@ -785,6 +785,7 @@ namespace LuaGlobalFunctions
|
||||
* PLAYER_EVENT_ON_AURA_APPLY = 64, // (event, player, aura)
|
||||
* PLAYER_EVENT_ON_HEAL = 65, // (event, player, target, heal) - Can return new heal amount
|
||||
* PLAYER_EVENT_ON_DAMAGE = 66, // (event, player, target, damage) - Can return new damage amount
|
||||
* PLAYER_EVENT_ON_BEFORE_TELEPORT = 67, // (event, player, target, damage) - Can return new damage amount
|
||||
* };
|
||||
* </pre>
|
||||
*
|
||||
|
||||
@@ -52,8 +52,6 @@ INSERT
|
||||
RacialLeader,
|
||||
movementId,
|
||||
RegenHealth,
|
||||
mechanic_immune_mask,
|
||||
spell_school_immune_mask,
|
||||
flags_extra,
|
||||
ScriptName,
|
||||
VerifiedBuild)
|
||||
@@ -72,7 +70,7 @@ VALUES
|
||||
81,
|
||||
2,
|
||||
21,
|
||||
1,
|
||||
69,341,185,
|
||||
1.0,
|
||||
1.42857,
|
||||
1.0,
|
||||
@@ -99,7 +97,7 @@ VALUES
|
||||
0,
|
||||
1163,
|
||||
1524,
|
||||
'SmartAI',
|
||||
'PassiveAI',
|
||||
0,
|
||||
1.0,
|
||||
4.0,
|
||||
@@ -109,8 +107,6 @@ VALUES
|
||||
0,
|
||||
144,
|
||||
1,
|
||||
1007366674,
|
||||
0,
|
||||
0,
|
||||
'',
|
||||
12340) ON DUPLICATE KEY UPDATE entry = entry;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -30,13 +30,13 @@
|
||||
#include "WorldPacket.h"
|
||||
#include "WorldSession.h"
|
||||
|
||||
//void called when player click on auctioneer npc
|
||||
void WorldSession::HandleAuctionHelloOpcode(WorldPacket& recvData)
|
||||
// void called when player click on auctioneer npc
|
||||
void WorldSession::HandleAuctionHelloOpcode(WorldPacket &recvData)
|
||||
{
|
||||
ObjectGuid guid; //NPC guid
|
||||
ObjectGuid guid; // NPC guid
|
||||
recvData >> guid;
|
||||
|
||||
Creature* unit = GetPlayer()->GetNPCIfCanInteractWith(guid, UNIT_NPC_FLAG_AUCTIONEER);
|
||||
Creature *unit = GetPlayer()->GetNPCIfCanInteractWith(guid, UNIT_NPC_FLAG_AUCTIONEER);
|
||||
if (!unit)
|
||||
{
|
||||
LOG_DEBUG("network", "WORLD: HandleAuctionHelloOpcode - Unit ({}) not found or you can't interact with him.", guid.ToString());
|
||||
@@ -50,8 +50,8 @@ void WorldSession::HandleAuctionHelloOpcode(WorldPacket& recvData)
|
||||
SendAuctionHello(guid, unit);
|
||||
}
|
||||
|
||||
//this void causes that auction window is opened
|
||||
void WorldSession::SendAuctionHello(ObjectGuid guid, Creature* unit)
|
||||
// this void causes that auction window is opened
|
||||
void WorldSession::SendAuctionHello(ObjectGuid guid, Creature *unit)
|
||||
{
|
||||
if (GetPlayer()->GetLevel() < sWorld->getIntConfig(CONFIG_AUCTION_LEVEL_REQ))
|
||||
{
|
||||
@@ -62,18 +62,18 @@ void WorldSession::SendAuctionHello(ObjectGuid guid, Creature* unit)
|
||||
if (!sScriptMgr->CanSendAuctionHello(this, guid, unit))
|
||||
return;
|
||||
|
||||
AuctionHouseEntry const* ahEntry = AuctionHouseMgr::GetAuctionHouseEntryFromFactionTemplate(unit->GetFaction());
|
||||
AuctionHouseEntry const *ahEntry = AuctionHouseMgr::GetAuctionHouseEntryFromFactionTemplate(unit->GetFaction());
|
||||
if (!ahEntry)
|
||||
return;
|
||||
|
||||
WorldPacket data(MSG_AUCTION_HELLO, 12);
|
||||
data << guid;
|
||||
data << uint32(ahEntry->houseId);
|
||||
data << uint8(1); // 3.3.3: 1 - AH enabled, 0 - AH disabled
|
||||
data << uint8(1); // 3.3.3: 1 - AH enabled, 0 - AH disabled
|
||||
SendPacket(&data);
|
||||
}
|
||||
|
||||
//call this method when player bids, creates, or deletes auction
|
||||
// call this method when player bids, creates, or deletes auction
|
||||
void WorldSession::SendAuctionCommandResult(uint32 auctionId, uint32 Action, uint32 ErrorCode, uint32 bidError)
|
||||
{
|
||||
WorldPacket data(SMSG_AUCTION_COMMAND_RESULT, 16);
|
||||
@@ -81,11 +81,11 @@ void WorldSession::SendAuctionCommandResult(uint32 auctionId, uint32 Action, uin
|
||||
data << Action;
|
||||
data << ErrorCode;
|
||||
if (!ErrorCode && Action)
|
||||
data << bidError; //when bid, then send 0, once...
|
||||
data << bidError; // when bid, then send 0, once...
|
||||
SendPacket(&data);
|
||||
}
|
||||
|
||||
//this function sends notification, if bidder is online
|
||||
// this function sends notification, if bidder is online
|
||||
void WorldSession::SendAuctionBidderNotification(uint32 location, uint32 auctionId, ObjectGuid bidder, uint32 bidSum, uint32 diff, uint32 item_template)
|
||||
{
|
||||
WorldPacket data(SMSG_AUCTION_BIDDER_NOTIFICATION, (8 * 4));
|
||||
@@ -99,22 +99,22 @@ void WorldSession::SendAuctionBidderNotification(uint32 location, uint32 auction
|
||||
SendPacket(&data);
|
||||
}
|
||||
|
||||
//this void causes on client to display: "Your auction sold"
|
||||
void WorldSession::SendAuctionOwnerNotification(AuctionEntry* auction)
|
||||
// this void causes on client to display: "Your auction sold"
|
||||
void WorldSession::SendAuctionOwnerNotification(AuctionEntry *auction)
|
||||
{
|
||||
WorldPacket data(SMSG_AUCTION_OWNER_NOTIFICATION, (8 * 4));
|
||||
data << uint32(auction->Id);
|
||||
data << uint32(auction->bid);
|
||||
data << uint32(0); //unk
|
||||
data << uint64(0); //unk (bidder guid?)
|
||||
data << uint32(0); // unk
|
||||
data << uint64(0); // unk (bidder guid?)
|
||||
data << uint32(auction->item_template);
|
||||
data << uint32(0); //unk
|
||||
data << float(0); //unk (time?)
|
||||
data << uint32(0); // unk
|
||||
data << float(0); // unk (time?)
|
||||
SendPacket(&data);
|
||||
}
|
||||
|
||||
//this void creates new auction and adds auction to some auctionhouse
|
||||
void WorldSession::HandleAuctionSellItem(WorldPacket& recvData)
|
||||
// this void creates new auction and adds auction to some auctionhouse
|
||||
void WorldSession::HandleAuctionSellItem(WorldPacket &recvData)
|
||||
{
|
||||
ObjectGuid auctioneer;
|
||||
uint32 itemsCount, etime, bid, buyout;
|
||||
@@ -161,19 +161,19 @@ void WorldSession::HandleAuctionSellItem(WorldPacket& recvData)
|
||||
if (bid > MAX_MONEY_AMOUNT || buyout > MAX_MONEY_AMOUNT)
|
||||
{
|
||||
LOG_DEBUG("network", "WORLD: HandleAuctionSellItem - Player {} ({}) attempted to sell item with higher price than max gold amount.",
|
||||
_player->GetName(), _player->GetGUID().ToString());
|
||||
_player->GetName(), _player->GetGUID().ToString());
|
||||
SendAuctionCommandResult(0, AUCTION_SELL_ITEM, ERR_AUCTION_DATABASE_ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
Creature* creature = GetPlayer()->GetNPCIfCanInteractWith(auctioneer, UNIT_NPC_FLAG_AUCTIONEER);
|
||||
Creature *creature = GetPlayer()->GetNPCIfCanInteractWith(auctioneer, UNIT_NPC_FLAG_AUCTIONEER);
|
||||
if (!creature)
|
||||
{
|
||||
LOG_DEBUG("network", "WORLD: HandleAuctionSellItem - Unit ({}) not found or you can't interact with him.", auctioneer.ToString());
|
||||
return;
|
||||
}
|
||||
|
||||
AuctionHouseEntry const* auctionHouseEntry = AuctionHouseMgr::GetAuctionHouseEntryFromFactionTemplate(creature->GetFaction());
|
||||
AuctionHouseEntry const *auctionHouseEntry = AuctionHouseMgr::GetAuctionHouseEntryFromFactionTemplate(creature->GetFaction());
|
||||
if (!auctionHouseEntry)
|
||||
{
|
||||
LOG_DEBUG("network", "WORLD: HandleAuctionSellItem - Unit ({}) has wrong faction.", auctioneer.ToString());
|
||||
@@ -184,25 +184,25 @@ void WorldSession::HandleAuctionSellItem(WorldPacket& recvData)
|
||||
|
||||
switch (etime)
|
||||
{
|
||||
case 1*MIN_AUCTION_TIME:
|
||||
case 2*MIN_AUCTION_TIME:
|
||||
case 4*MIN_AUCTION_TIME:
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
case 1 * MIN_AUCTION_TIME:
|
||||
case 2 * MIN_AUCTION_TIME:
|
||||
case 4 * MIN_AUCTION_TIME:
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
if (GetPlayer()->HasUnitState(UNIT_STATE_DIED))
|
||||
GetPlayer()->RemoveAurasByType(SPELL_AURA_FEIGN_DEATH);
|
||||
|
||||
Item* items[MAX_AUCTION_ITEMS];
|
||||
Item *items[MAX_AUCTION_ITEMS];
|
||||
|
||||
uint32 finalCount = 0;
|
||||
uint32 itemEntry = 0;
|
||||
|
||||
for (uint32 i = 0; i < itemsCount; ++i)
|
||||
{
|
||||
Item* item = _player->GetItemByGuid(itemGUIDs[i]);
|
||||
Item *item = _player->GetItemByGuid(itemGUIDs[i]);
|
||||
|
||||
if (!item)
|
||||
{
|
||||
@@ -214,8 +214,8 @@ void WorldSession::HandleAuctionSellItem(WorldPacket& recvData)
|
||||
itemEntry = item->GetTemplate()->ItemId;
|
||||
|
||||
if (sAuctionMgr->GetAItem(item->GetGUID()) || !item->CanBeTraded() || item->IsNotEmptyBag() ||
|
||||
item->GetTemplate()->HasFlag(ITEM_FLAG_CONJURED) || item->GetUInt32Value(ITEM_FIELD_DURATION) ||
|
||||
item->GetCount() < count[i] || itemEntry != item->GetTemplate()->ItemId)
|
||||
item->GetTemplate()->HasFlag(ITEM_FLAG_CONJURED) || item->GetUInt32Value(ITEM_FIELD_DURATION) ||
|
||||
item->GetCount() < count[i] || itemEntry != item->GetTemplate()->ItemId)
|
||||
{
|
||||
SendAuctionCommandResult(0, AUCTION_SELL_ITEM, ERR_AUCTION_DATABASE_ERROR);
|
||||
return;
|
||||
@@ -246,7 +246,7 @@ void WorldSession::HandleAuctionSellItem(WorldPacket& recvData)
|
||||
|
||||
for (uint32 i = 0; i < itemsCount; ++i)
|
||||
{
|
||||
Item* item = items[i];
|
||||
Item *item = items[i];
|
||||
|
||||
if (item->GetMaxStackCount() < finalCount)
|
||||
{
|
||||
@@ -257,10 +257,10 @@ void WorldSession::HandleAuctionSellItem(WorldPacket& recvData)
|
||||
|
||||
for (uint32 i = 0; i < itemsCount; ++i)
|
||||
{
|
||||
Item* item = items[i];
|
||||
Item *item = items[i];
|
||||
|
||||
uint32 auctionTime = uint32(etime * sWorld->getRate(RATE_AUCTION_TIME));
|
||||
AuctionHouseObject* auctionHouse = sAuctionMgr->GetAuctionsMap(creature->GetFaction());
|
||||
AuctionHouseObject *auctionHouse = sAuctionMgr->GetAuctionsMap(creature->GetFaction());
|
||||
|
||||
uint32 deposit = sAuctionMgr->GetAuctionDeposit(auctionHouseEntry, etime, item, finalCount);
|
||||
if (!_player->HasEnoughMoney(deposit))
|
||||
@@ -271,22 +271,14 @@ void WorldSession::HandleAuctionSellItem(WorldPacket& recvData)
|
||||
|
||||
_player->ModifyMoney(-int32(deposit));
|
||||
|
||||
AuctionEntry* AH = new AuctionEntry;
|
||||
AuctionEntry *AH = new AuctionEntry;
|
||||
AH->Id = sObjectMgr->GenerateAuctionID();
|
||||
|
||||
if (sWorld->getBoolConfig(CONFIG_ALLOW_TWO_SIDE_INTERACTION_AUCTION))
|
||||
AH->houseId = AuctionHouseId::Neutral;
|
||||
else
|
||||
{
|
||||
CreatureData const* auctioneerData = sObjectMgr->GetCreatureData(creature->GetSpawnId());
|
||||
if (!auctioneerData)
|
||||
{
|
||||
LOG_ERROR("network.opcode", "Data for auctioneer not found ({})", auctioneer.ToString());
|
||||
delete AH;
|
||||
return;
|
||||
}
|
||||
|
||||
CreatureTemplate const* auctioneerInfo = sObjectMgr->GetCreatureTemplate(auctioneerData->id);
|
||||
CreatureTemplate const *auctioneerInfo = creature->GetCreatureTemplate();
|
||||
if (!auctioneerInfo)
|
||||
{
|
||||
LOG_ERROR("network.opcode", "Non existing auctioneer ({})", auctioneer.ToString());
|
||||
@@ -294,7 +286,7 @@ void WorldSession::HandleAuctionSellItem(WorldPacket& recvData)
|
||||
return;
|
||||
}
|
||||
|
||||
AuctionHouseEntry const* AHEntry = sAuctionMgr->GetAuctionHouseEntryFromFactionTemplate(auctioneerInfo->faction);
|
||||
AuctionHouseEntry const *AHEntry = sAuctionMgr->GetAuctionHouseEntryFromFactionTemplate(auctioneerInfo->faction);
|
||||
AH->houseId = AuctionHouseId(AHEntry->houseId);
|
||||
}
|
||||
|
||||
@@ -314,7 +306,7 @@ void WorldSession::HandleAuctionSellItem(WorldPacket& recvData)
|
||||
AH->auctionHouseEntry = auctionHouseEntry;
|
||||
|
||||
LOG_DEBUG("network.opcode", "CMSG_AUCTION_SELL_ITEM: Player {} ({}) is selling item {} entry {} ({}) with count {} with initial bid {} with buyout {} and with time {} (in sec) in auctionhouse {}",
|
||||
_player->GetName(), _player->GetGUID().ToString(), item->GetTemplate()->Name1, item->GetEntry(), item->GetGUID().ToString(), item->GetCount(), bid, buyout, auctionTime, AH->GetHouseId());
|
||||
_player->GetName(), _player->GetGUID().ToString(), item->GetTemplate()->Name1, item->GetEntry(), item->GetGUID().ToString(), item->GetCount(), bid, buyout, auctionTime, AH->GetHouseId());
|
||||
sAuctionMgr->AddAItem(item);
|
||||
auctionHouse->AddAuction(AH);
|
||||
|
||||
@@ -332,21 +324,21 @@ void WorldSession::HandleAuctionSellItem(WorldPacket& recvData)
|
||||
if (HasPermission(rbac::RBAC_PERM_LOG_GM_TRADE))
|
||||
{
|
||||
LOG_GM(GetAccountId(), "GM {} (Account: {}) created auction: {} (Item: {} Count: {}) Bid: {} Buyout: {}",
|
||||
_player->GetName(), GetAccountId(), AH->Id,
|
||||
item->GetTemplate()->Name1, item->GetCount(), bid, buyout);
|
||||
_player->GetName(), GetAccountId(), AH->Id,
|
||||
item->GetTemplate()->Name1, item->GetCount(), bid, buyout);
|
||||
}
|
||||
|
||||
GetPlayer()->UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_CREATE_AUCTION, 1);
|
||||
|
||||
LOG_INFO("entities.player.auctionhouse", "AuctionHouse: Account: {} (IP: {}), Player [{}] (GUID: {}) created auction #{}: Item '{}' (Entry: {}) x{}, StartBid: {} copper, Buyout: {} copper, Deposit: {} copper",
|
||||
GetAccountId(), GetRemoteAddress(), _player->GetName(), _player->GetGUID().GetCounter(), AH->Id,
|
||||
item->GetTemplate()->Name1, item->GetEntry(), item->GetCount(), bid, buyout, deposit);
|
||||
GetAccountId(), GetRemoteAddress(), _player->GetName(), _player->GetGUID().GetCounter(), AH->Id,
|
||||
item->GetTemplate()->Name1, item->GetEntry(), item->GetCount(), bid, buyout, deposit);
|
||||
|
||||
return;
|
||||
}
|
||||
else // Required stack size of auction does not match to current item stack size, clone item and set correct stack size
|
||||
{
|
||||
Item* newItem = item->CloneItem(finalCount, _player);
|
||||
Item *newItem = item->CloneItem(finalCount, _player);
|
||||
if (!newItem)
|
||||
{
|
||||
LOG_ERROR("network.opcode", "CMSG_AUCTION_SELL_ITEM: Could not create clone of item {}", item->GetEntry());
|
||||
@@ -367,13 +359,13 @@ void WorldSession::HandleAuctionSellItem(WorldPacket& recvData)
|
||||
AH->auctionHouseEntry = auctionHouseEntry;
|
||||
|
||||
LOG_DEBUG("network.opcode", "CMSG_AUCTION_SELL_ITEM: Player {} ({}) is selling item {} entry {} ({}) with count {} with initial bid {} with buyout {} and with time {} (in sec) in auctionhouse {}",
|
||||
_player->GetName(), _player->GetGUID().ToString(), newItem->GetTemplate()->Name1, newItem->GetEntry(), newItem->GetGUID().ToString(), newItem->GetCount(), bid, buyout, auctionTime, AH->GetHouseId());
|
||||
_player->GetName(), _player->GetGUID().ToString(), newItem->GetTemplate()->Name1, newItem->GetEntry(), newItem->GetGUID().ToString(), newItem->GetCount(), bid, buyout, auctionTime, AH->GetHouseId());
|
||||
sAuctionMgr->AddAItem(newItem);
|
||||
auctionHouse->AddAuction(AH);
|
||||
|
||||
for (uint32 j = 0; j < itemsCount; ++j)
|
||||
{
|
||||
Item* item2 = items[j];
|
||||
Item *item2 = items[j];
|
||||
|
||||
// Item stack count equals required count, ready to delete item - cloned item will be used for auction
|
||||
if (item2->GetCount() == count[j])
|
||||
@@ -406,23 +398,23 @@ void WorldSession::HandleAuctionSellItem(WorldPacket& recvData)
|
||||
if (HasPermission(rbac::RBAC_PERM_LOG_GM_TRADE))
|
||||
{
|
||||
LOG_GM(GetAccountId(), "GM {} (Account: {}) created auction: {} (Item: {} Count: {}) Bid: {} Buyout: {}",
|
||||
_player->GetName(), GetAccountId(), AH->Id,
|
||||
newItem->GetTemplate()->Name1, newItem->GetCount(), bid, buyout);
|
||||
_player->GetName(), GetAccountId(), AH->Id,
|
||||
newItem->GetTemplate()->Name1, newItem->GetCount(), bid, buyout);
|
||||
}
|
||||
|
||||
GetPlayer()->UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_CREATE_AUCTION, 1);
|
||||
|
||||
LOG_INFO("entities.player.auctionhouse", "AuctionHouse: Account: {} (IP: {}), Player [{}] (GUID: {}) created auction #{}: Item '{}' (Entry: {}) x{}, StartBid: {} copper, Buyout: {} copper, Deposit: {} copper",
|
||||
GetAccountId(), GetRemoteAddress(), _player->GetName(), _player->GetGUID().GetCounter(), AH->Id,
|
||||
newItem->GetTemplate()->Name1, newItem->GetEntry(), newItem->GetCount(), bid, buyout, deposit);
|
||||
GetAccountId(), GetRemoteAddress(), _player->GetName(), _player->GetGUID().GetCounter(), AH->Id,
|
||||
newItem->GetTemplate()->Name1, newItem->GetEntry(), newItem->GetCount(), bid, buyout, deposit);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//this function is called when client bids or buys out auction
|
||||
void WorldSession::HandleAuctionPlaceBid(WorldPacket& recvData)
|
||||
// this function is called when client bids or buys out auction
|
||||
void WorldSession::HandleAuctionPlaceBid(WorldPacket &recvData)
|
||||
{
|
||||
ObjectGuid auctioneer;
|
||||
uint32 auctionId;
|
||||
@@ -431,7 +423,7 @@ void WorldSession::HandleAuctionPlaceBid(WorldPacket& recvData)
|
||||
recvData >> auctionId >> price;
|
||||
|
||||
if (!auctionId || !price)
|
||||
return; //check for cheaters
|
||||
return; // check for cheaters
|
||||
|
||||
if (sWorld->getBoolConfig(CONFIG_TRIAL_RESTRICTION_AUCTION) && IsTrialAccount())
|
||||
{
|
||||
@@ -439,7 +431,7 @@ void WorldSession::HandleAuctionPlaceBid(WorldPacket& recvData)
|
||||
return;
|
||||
}
|
||||
|
||||
Creature* creature = GetPlayer()->GetNPCIfCanInteractWith(auctioneer, UNIT_NPC_FLAG_AUCTIONEER);
|
||||
Creature *creature = GetPlayer()->GetNPCIfCanInteractWith(auctioneer, UNIT_NPC_FLAG_AUCTIONEER);
|
||||
if (!creature)
|
||||
{
|
||||
LOG_DEBUG("network", "WORLD: HandleAuctionPlaceBid - Unit ({}) not found or you can't interact with him.", auctioneer.ToString());
|
||||
@@ -450,10 +442,10 @@ void WorldSession::HandleAuctionPlaceBid(WorldPacket& recvData)
|
||||
if (GetPlayer()->HasUnitState(UNIT_STATE_DIED))
|
||||
GetPlayer()->RemoveAurasByType(SPELL_AURA_FEIGN_DEATH);
|
||||
|
||||
AuctionHouseObject* auctionHouse = sAuctionMgr->GetAuctionsMap(creature->GetFaction());
|
||||
AuctionHouseObject *auctionHouse = sAuctionMgr->GetAuctionsMap(creature->GetFaction());
|
||||
|
||||
AuctionEntry* auction = auctionHouse->GetAuction(auctionId);
|
||||
Player* player = GetPlayer();
|
||||
AuctionEntry *auction = auctionHouse->GetAuction(auctionId);
|
||||
Player *player = GetPlayer();
|
||||
|
||||
if (!sScriptMgr->OnPlayerCanPlaceAuctionBid(player, auction))
|
||||
{
|
||||
@@ -470,16 +462,16 @@ void WorldSession::HandleAuctionPlaceBid(WorldPacket& recvData)
|
||||
|
||||
if (auction->owner == player->GetGUID())
|
||||
{
|
||||
//you cannot bid your own auction:
|
||||
// you cannot bid your own auction:
|
||||
SendAuctionCommandResult(0, AUCTION_PLACE_BID, ERR_AUCTION_BID_OWN);
|
||||
return;
|
||||
}
|
||||
|
||||
// impossible have online own another character (use this for speedup check in case online owner)
|
||||
Player* auction_owner = ObjectAccessor::FindConnectedPlayer(auction->owner);
|
||||
Player *auction_owner = ObjectAccessor::FindConnectedPlayer(auction->owner);
|
||||
if (!auction_owner && sCharacterCache->GetCharacterAccountIdByGuid(auction->owner) == GetAccountId())
|
||||
{
|
||||
//you cannot bid your another character auction:
|
||||
// you cannot bid your another character auction:
|
||||
SendAuctionCommandResult(0, AUCTION_PLACE_BID, ERR_AUCTION_BID_OWN);
|
||||
return;
|
||||
}
|
||||
@@ -490,16 +482,16 @@ void WorldSession::HandleAuctionPlaceBid(WorldPacket& recvData)
|
||||
|
||||
// price too low for next bid if not buyout
|
||||
if ((price < auction->buyout || auction->buyout == 0) &&
|
||||
price < auction->bid + AuctionEntry::CalculateAuctionOutBid(auction->bid))
|
||||
price < auction->bid + AuctionEntry::CalculateAuctionOutBid(auction->bid))
|
||||
{
|
||||
//auction has already higher bid, client tests it!
|
||||
// auction has already higher bid, client tests it!
|
||||
return;
|
||||
}
|
||||
|
||||
if (!player->HasEnoughMoney(price))
|
||||
{
|
||||
//you don't have enought money!, client tests!
|
||||
//SendAuctionCommandResult(auction->auctionId, AUCTION_PLACE_BID, ???);
|
||||
// you don't have enought money!, client tests!
|
||||
// SendAuctionCommandResult(auction->auctionId, AUCTION_PLACE_BID, ???);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -528,7 +520,7 @@ void WorldSession::HandleAuctionPlaceBid(WorldPacket& recvData)
|
||||
|
||||
GetPlayer()->UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_HIGHEST_AUCTION_BID, price);
|
||||
|
||||
CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_UPD_AUCTION_BID);
|
||||
CharacterDatabasePreparedStatement *stmt = CharacterDatabase.GetPreparedStatement(CHAR_UPD_AUCTION_BID);
|
||||
stmt->SetData(0, auction->bidder.GetCounter());
|
||||
stmt->SetData(1, auction->bid);
|
||||
stmt->SetData(2, auction->Id);
|
||||
@@ -539,23 +531,23 @@ void WorldSession::HandleAuctionPlaceBid(WorldPacket& recvData)
|
||||
if (HasPermission(rbac::RBAC_PERM_LOG_GM_TRADE))
|
||||
{
|
||||
LOG_GM(GetAccountId(), "GM {} (Account: {}) bid on auction: {} (Item: {} Count: {}) Bid: {}",
|
||||
player->GetName(), GetAccountId(), auction->Id,
|
||||
auction->item_template, auction->itemCount, price);
|
||||
player->GetName(), GetAccountId(), auction->Id,
|
||||
auction->item_template, auction->itemCount, price);
|
||||
}
|
||||
|
||||
LOG_INFO("entities.player.auctionhouse", "AuctionHouse: Account: {} (IP: {}), Player [{}] (GUID: {}) placed bid on auction #{}: Item (Entry: {}) x{}, Bid: {} copper, Owner: {} (GUID: {})",
|
||||
GetAccountId(), GetRemoteAddress(), player->GetName(), player->GetGUID().GetCounter(),
|
||||
auction->Id, auction->item_template, auction->itemCount, price, auction->owner.GetCounter(), auction->owner.GetCounter());
|
||||
GetAccountId(), GetRemoteAddress(), player->GetName(), player->GetGUID().GetCounter(),
|
||||
auction->Id, auction->item_template, auction->itemCount, price, auction->owner.GetCounter(), auction->owner.GetCounter());
|
||||
}
|
||||
else
|
||||
{
|
||||
//buyout:
|
||||
// buyout:
|
||||
if (player->GetGUID() == auction->bidder)
|
||||
player->ModifyMoney(-int32(auction->buyout - auction->bid));
|
||||
else
|
||||
{
|
||||
player->ModifyMoney(-int32(auction->buyout));
|
||||
if (auction->bidder) //buyout for bidded auction ..
|
||||
if (auction->bidder) // buyout for bidded auction ..
|
||||
sAuctionMgr->SendAuctionOutbiddedMail(auction, auction->buyout, GetPlayer(), trans);
|
||||
}
|
||||
auction->bidder = player->GetGUID();
|
||||
@@ -573,13 +565,13 @@ void WorldSession::HandleAuctionPlaceBid(WorldPacket& recvData)
|
||||
if (HasPermission(rbac::RBAC_PERM_LOG_GM_TRADE))
|
||||
{
|
||||
LOG_GM(GetAccountId(), "GM {} (Account: {}) bought out auction: {} (Item: {} Count: {}) Buyout: {}",
|
||||
player->GetName(), GetAccountId(), auction->Id,
|
||||
auction->item_template, auction->itemCount, auction->buyout);
|
||||
player->GetName(), GetAccountId(), auction->Id,
|
||||
auction->item_template, auction->itemCount, auction->buyout);
|
||||
}
|
||||
|
||||
LOG_INFO("entities.player.auctionhouse", "AuctionHouse: Account: {} (IP: {}), Player [{}] (GUID: {}) bought out auction #{}: Item (Entry: {}) x{}, Buyout: {} copper, Owner: {} (GUID: {})",
|
||||
GetAccountId(), GetRemoteAddress(), player->GetName(), player->GetGUID().GetCounter(),
|
||||
auction->Id, auction->item_template, auction->itemCount, auction->buyout, auction->owner.GetCounter(), auction->owner.GetCounter());
|
||||
GetAccountId(), GetRemoteAddress(), player->GetName(), player->GetGUID().GetCounter(),
|
||||
auction->Id, auction->item_template, auction->itemCount, auction->buyout, auction->owner.GetCounter(), auction->owner.GetCounter());
|
||||
|
||||
auction->DeleteFromDB(trans);
|
||||
|
||||
@@ -590,8 +582,8 @@ void WorldSession::HandleAuctionPlaceBid(WorldPacket& recvData)
|
||||
CharacterDatabase.CommitTransaction(trans);
|
||||
}
|
||||
|
||||
//this void is called when auction_owner cancels his auction
|
||||
void WorldSession::HandleAuctionRemoveItem(WorldPacket& recvData)
|
||||
// this void is called when auction_owner cancels his auction
|
||||
void WorldSession::HandleAuctionRemoveItem(WorldPacket &recvData)
|
||||
{
|
||||
ObjectGuid auctioneer;
|
||||
uint32 auctionId;
|
||||
@@ -604,7 +596,7 @@ void WorldSession::HandleAuctionRemoveItem(WorldPacket& recvData)
|
||||
return;
|
||||
}
|
||||
|
||||
Creature* creature = GetPlayer()->GetNPCIfCanInteractWith(auctioneer, UNIT_NPC_FLAG_AUCTIONEER);
|
||||
Creature *creature = GetPlayer()->GetNPCIfCanInteractWith(auctioneer, UNIT_NPC_FLAG_AUCTIONEER);
|
||||
if (!creature)
|
||||
{
|
||||
LOG_DEBUG("network", "WORLD: HandleAuctionRemoveItem - Unit ({}) not found or you can't interact with him.", auctioneer.ToString());
|
||||
@@ -615,47 +607,47 @@ void WorldSession::HandleAuctionRemoveItem(WorldPacket& recvData)
|
||||
if (GetPlayer()->HasUnitState(UNIT_STATE_DIED))
|
||||
GetPlayer()->RemoveAurasByType(SPELL_AURA_FEIGN_DEATH);
|
||||
|
||||
AuctionHouseObject* auctionHouse = sAuctionMgr->GetAuctionsMap(creature->GetFaction());
|
||||
AuctionHouseObject *auctionHouse = sAuctionMgr->GetAuctionsMap(creature->GetFaction());
|
||||
|
||||
AuctionEntry* auction = auctionHouse->GetAuction(auctionId);
|
||||
Player* player = GetPlayer();
|
||||
AuctionEntry *auction = auctionHouse->GetAuction(auctionId);
|
||||
Player *player = GetPlayer();
|
||||
|
||||
CharacterDatabaseTransaction trans = CharacterDatabase.BeginTransaction();
|
||||
if (auction && auction->owner == player->GetGUID())
|
||||
{
|
||||
Item* pItem = sAuctionMgr->GetAItem(auction->item_guid);
|
||||
Item *pItem = sAuctionMgr->GetAItem(auction->item_guid);
|
||||
if (!pItem)
|
||||
return;
|
||||
|
||||
if (auction->bidder) // If we have a bidder, we have to send him the money he paid
|
||||
if (auction->bidder) // If we have a bidder, we have to send him the money he paid
|
||||
{
|
||||
uint32 auctionCut = auction->GetAuctionCut();
|
||||
if (!player->HasEnoughMoney(auctionCut)) //player doesn't have enough money, maybe message needed
|
||||
if (!player->HasEnoughMoney(auctionCut)) // player doesn't have enough money, maybe message needed
|
||||
return;
|
||||
//some auctionBidderNotification would be needed, but don't know that parts..
|
||||
// some auctionBidderNotification would be needed, but don't know that parts..
|
||||
sAuctionMgr->SendAuctionCancelledToBidderMail(auction, trans);
|
||||
player->ModifyMoney(-int32(auctionCut));
|
||||
}
|
||||
|
||||
// item will deleted or added to received mail list
|
||||
MailDraft(auction->BuildAuctionMailSubject(AUCTION_CANCELED), AuctionEntry::BuildAuctionMailBody(ObjectGuid::Empty, 0, auction->buyout, auction->deposit))
|
||||
.AddItem(pItem)
|
||||
.SendMailTo(trans, player, auction, MAIL_CHECK_MASK_COPIED);
|
||||
.AddItem(pItem)
|
||||
.SendMailTo(trans, player, auction, MAIL_CHECK_MASK_COPIED);
|
||||
}
|
||||
else
|
||||
{
|
||||
SendAuctionCommandResult(0, AUCTION_CANCEL, ERR_AUCTION_DATABASE_ERROR);
|
||||
//this code isn't possible ... maybe there should be assert
|
||||
// this code isn't possible ... maybe there should be assert
|
||||
LOG_ERROR("network.opcode", "CHEATER : {}, he tried to cancel auction (id: {}) of another player, or auction is nullptr", player->GetGUID().ToString(), auctionId);
|
||||
return;
|
||||
}
|
||||
|
||||
//inform player, that auction is removed
|
||||
// inform player, that auction is removed
|
||||
SendAuctionCommandResult(auction->Id, AUCTION_CANCEL, ERR_AUCTION_OK);
|
||||
|
||||
LOG_INFO("entities.player.auctionhouse", "AuctionHouse: Account: {} (IP: {}), Player [{}] (GUID: {}) cancelled auction #{}: Item (Entry: {}) x{}, Buyout: {} copper",
|
||||
GetAccountId(), GetRemoteAddress(), player->GetName(), player->GetGUID().GetCounter(),
|
||||
auction->Id, auction->item_template, auction->itemCount, auction->buyout);
|
||||
GetAccountId(), GetRemoteAddress(), player->GetName(), player->GetGUID().GetCounter(),
|
||||
auction->Id, auction->item_template, auction->itemCount, auction->buyout);
|
||||
|
||||
// Now remove the auction
|
||||
|
||||
@@ -667,15 +659,15 @@ void WorldSession::HandleAuctionRemoveItem(WorldPacket& recvData)
|
||||
auctionHouse->RemoveAuction(auction);
|
||||
}
|
||||
|
||||
//called when player lists his bids
|
||||
void WorldSession::HandleAuctionListBidderItems(WorldPacket& recvData)
|
||||
// called when player lists his bids
|
||||
void WorldSession::HandleAuctionListBidderItems(WorldPacket &recvData)
|
||||
{
|
||||
ObjectGuid guid; //NPC guid
|
||||
uint32 listfrom; //page of auctions
|
||||
uint32 outbiddedCount; //count of outbidded auctions
|
||||
ObjectGuid guid; // NPC guid
|
||||
uint32 listfrom; // page of auctions
|
||||
uint32 outbiddedCount; // count of outbidded auctions
|
||||
|
||||
recvData >> guid;
|
||||
recvData >> listfrom; // not used in fact (this list not have page control in client)
|
||||
recvData >> listfrom; // not used in fact (this list not have page control in client)
|
||||
recvData >> outbiddedCount;
|
||||
if (recvData.size() != (16 + outbiddedCount * 4))
|
||||
{
|
||||
@@ -683,7 +675,7 @@ void WorldSession::HandleAuctionListBidderItems(WorldPacket& recvData)
|
||||
outbiddedCount = 0;
|
||||
}
|
||||
|
||||
Creature* creature = GetPlayer()->GetNPCIfCanInteractWith(guid, UNIT_NPC_FLAG_AUCTIONEER);
|
||||
Creature *creature = GetPlayer()->GetNPCIfCanInteractWith(guid, UNIT_NPC_FLAG_AUCTIONEER);
|
||||
if (!creature)
|
||||
{
|
||||
LOG_DEBUG("network", "WORLD: HandleAuctionListBidderItems - Unit ({}) not found or you can't interact with him.", guid.ToString());
|
||||
@@ -699,7 +691,7 @@ void WorldSession::HandleAuctionListBidderItems(WorldPacket& recvData)
|
||||
if (GetPlayer()->HasUnitState(UNIT_STATE_DIED))
|
||||
GetPlayer()->RemoveAurasByType(SPELL_AURA_FEIGN_DEATH);
|
||||
|
||||
AuctionHouseEntry const* ahEntry = AuctionHouseMgr::GetAuctionHouseEntryFromFactionTemplate(creature->GetFaction());
|
||||
AuctionHouseEntry const *ahEntry = AuctionHouseMgr::GetAuctionHouseEntryFromFactionTemplate(creature->GetFaction());
|
||||
if (!ahEntry)
|
||||
return;
|
||||
|
||||
@@ -719,16 +711,16 @@ void WorldSession::HandleAuctionListBidderItems(WorldPacket& recvData)
|
||||
sAuctionMgr->GetAuctionHouseSearcher()->QueueSearchRequest(new AuctionSearchBidderListRequest(auctionHouseFaction, std::move(auctionIds), GetPlayer()->GetGUID()));
|
||||
}
|
||||
|
||||
//this void sends player info about his auctions
|
||||
void WorldSession::HandleAuctionListOwnerItems(WorldPacket& recvData)
|
||||
// this void sends player info about his auctions
|
||||
void WorldSession::HandleAuctionListOwnerItems(WorldPacket &recvData)
|
||||
{
|
||||
ObjectGuid guid;
|
||||
uint32 listfrom;
|
||||
|
||||
recvData >> guid;
|
||||
recvData >> listfrom; // not used in fact (this list does not have page control in client)
|
||||
recvData >> listfrom; // not used in fact (this list does not have page control in client)
|
||||
|
||||
Creature* creature = GetPlayer()->GetNPCIfCanInteractWith(guid, UNIT_NPC_FLAG_AUCTIONEER);
|
||||
Creature *creature = GetPlayer()->GetNPCIfCanInteractWith(guid, UNIT_NPC_FLAG_AUCTIONEER);
|
||||
if (!creature)
|
||||
return;
|
||||
|
||||
@@ -736,7 +728,7 @@ void WorldSession::HandleAuctionListOwnerItems(WorldPacket& recvData)
|
||||
if (GetPlayer()->HasUnitState(UNIT_STATE_DIED))
|
||||
GetPlayer()->RemoveAurasByType(SPELL_AURA_FEIGN_DEATH);
|
||||
|
||||
AuctionHouseEntry const* ahEntry = AuctionHouseMgr::GetAuctionHouseEntryFromFactionTemplate(creature->GetFaction());
|
||||
AuctionHouseEntry const *ahEntry = AuctionHouseMgr::GetAuctionHouseEntryFromFactionTemplate(creature->GetFaction());
|
||||
if (!ahEntry)
|
||||
return;
|
||||
|
||||
@@ -745,8 +737,8 @@ void WorldSession::HandleAuctionListOwnerItems(WorldPacket& recvData)
|
||||
sAuctionMgr->GetAuctionHouseSearcher()->QueueSearchRequest(new AuctionSearchOwnerListRequest(auctionHouseFaction, GetPlayer()->GetGUID()));
|
||||
}
|
||||
|
||||
//this void is called when player clicks on search button
|
||||
void WorldSession::HandleAuctionListItems(WorldPacket& recvData)
|
||||
// this void is called when player clicks on search button
|
||||
void WorldSession::HandleAuctionListItems(WorldPacket &recvData)
|
||||
{
|
||||
std::string searchedname;
|
||||
uint8 levelmin, levelmax, usable;
|
||||
@@ -754,7 +746,7 @@ void WorldSession::HandleAuctionListItems(WorldPacket& recvData)
|
||||
ObjectGuid guid;
|
||||
|
||||
recvData >> guid;
|
||||
recvData >> listfrom; // start, used for page control listing by 50 elements
|
||||
recvData >> listfrom; // start, used for page control listing by 50 elements
|
||||
recvData >> searchedname;
|
||||
|
||||
recvData >> levelmin >> levelmax;
|
||||
@@ -791,7 +783,7 @@ void WorldSession::HandleAuctionListItems(WorldPacket& recvData)
|
||||
|
||||
wstrToLower(wsearchedname);
|
||||
|
||||
Creature* creature = GetPlayer()->GetNPCIfCanInteractWith(guid, UNIT_NPC_FLAG_AUCTIONEER);
|
||||
Creature *creature = GetPlayer()->GetNPCIfCanInteractWith(guid, UNIT_NPC_FLAG_AUCTIONEER);
|
||||
if (!creature)
|
||||
return;
|
||||
|
||||
@@ -799,7 +791,7 @@ void WorldSession::HandleAuctionListItems(WorldPacket& recvData)
|
||||
if (_player->HasUnitState(UNIT_STATE_DIED))
|
||||
_player->RemoveAurasByType(SPELL_AURA_FEIGN_DEATH);
|
||||
|
||||
AuctionHouseEntry const* ahEntry = AuctionHouseMgr::GetAuctionHouseEntryFromFactionTemplate(creature->GetFaction());
|
||||
AuctionHouseEntry const *ahEntry = AuctionHouseMgr::GetAuctionHouseEntryFromFactionTemplate(creature->GetFaction());
|
||||
if (!ahEntry)
|
||||
return;
|
||||
|
||||
@@ -830,12 +822,12 @@ void WorldSession::HandleAuctionListItems(WorldPacket& recvData)
|
||||
usablePlayerInfo.raceMask = GetPlayer()->getRaceMask();
|
||||
usablePlayerInfo.level = GetPlayer()->GetLevel();
|
||||
|
||||
SkillStatusMap const& skillMap = GetPlayer()->GetSkillStatusMap();
|
||||
for (auto const& pair : skillMap)
|
||||
SkillStatusMap const &skillMap = GetPlayer()->GetSkillStatusMap();
|
||||
for (auto const &pair : skillMap)
|
||||
usablePlayerInfo.skills.insert(std::make_pair(pair.first, GetPlayer()->GetSkillValue(pair.first)));
|
||||
|
||||
PlayerSpellMap const& spellMap = GetPlayer()->GetSpellMap();
|
||||
for (auto const& pair : spellMap)
|
||||
PlayerSpellMap const &spellMap = GetPlayer()->GetSpellMap();
|
||||
for (auto const &pair : spellMap)
|
||||
{
|
||||
if (pair.second->State != PLAYERSPELL_REMOVED && pair.second->IsInSpec(GetPlayer()->GetActiveSpec()))
|
||||
usablePlayerInfo.spells.insert(pair.first);
|
||||
@@ -846,14 +838,14 @@ void WorldSession::HandleAuctionListItems(WorldPacket& recvData)
|
||||
sAuctionMgr->GetAuctionHouseSearcher()->QueueSearchRequest(new AuctionSearchListRequest(auctionHouseFaction, std::move(ahSearchInfo), std::move(ahPlayerInfo)));
|
||||
}
|
||||
|
||||
void WorldSession::HandleAuctionListPendingSales(WorldPacket& recvData)
|
||||
void WorldSession::HandleAuctionListPendingSales(WorldPacket &recvData)
|
||||
{
|
||||
recvData.read_skip<uint64>();
|
||||
|
||||
uint32 count = 0;
|
||||
|
||||
WorldPacket data(SMSG_AUCTION_LIST_PENDING_SALES, 4);
|
||||
data << uint32(count); // count
|
||||
data << uint32(count); // count
|
||||
/*for (uint32 i = 0; i < count; ++i)
|
||||
{
|
||||
data << ""; // string
|
||||
|
||||
Reference in New Issue
Block a user