1 Commits

Author SHA1 Message Date
sindoring 3405057e5b wip 2026-08-17 20:24:17 +04:00
19 changed files with 1278 additions and 1057 deletions
@@ -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 подряд и убедиться, что работает только второй.
-9
View File
@@ -341,14 +341,6 @@ import_encounter_journal_sql() {
fi
}
import_premium_sql() {
log "Started importing Premium SQL files"
local world_premium_sql="$ROOT_DIR/sql/Premium/world.sql"
import_sql_file "acore_world" "$world_premium_sql"
}
if (($# > 0)); then
case "$1" in
-h|--help)
@@ -381,7 +373,6 @@ import_playerbots_ru_sql
import_encounter_journal_sql
import_store_sql
import_transmog_sql
import_premium_sql
configure_server_motd
log "custom SQL bootstrap completed"
+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
-1
View File
@@ -1 +0,0 @@
return {}
-204
View File
@@ -1,204 +0,0 @@
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 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
---@return table
local function SplitString(inputstr, separator)
if separator == nil then
separator = "%s"
end
local t = {}
for str in string.gmatch(inputstr, "([^" .. separator .. "]+)") do
table.insert(t, str)
end
return t
end
---@return boolean
local function IsPremium(player)
return premiums_table[player:GetAccountId()] == true
end
local function OnPremiumPlayer(player)
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)
local query = AuthDBQuery("SELECT flags FROM account WHERE id = " .. player:GetAccountId())
if query then
local function HasFlag(mask, flag)
return bit_and(mask, flag) == flag
end
local flags = query:GetUInt32(0)
if HasFlag(flags, ACCOUNT_FLAG_RECURRING_BILLING) then
premiums_table[player:GetAccountId()] = true
OnPremiumPlayer(player)
return true
end
end
return false
end
local function ApplyPremium(player)
AuthDBExecute(
string.format(
"UPDATE account SET flags = flags | %d WHERE id = %d",
ACCOUNT_FLAG_RECURRING_BILLING,
player:GetAccountId()
)
)
RefreshPremiumStatus(player)
end
local function OnLogin(event, player)
RefreshPremiumStatus(player)
end
--- @param event any
--- @param player any
--- @param command string
--- @param chatHandler any
local function OnCommand(event, player, command, chatHandler)
---Console is not supported yet
if not player then return end
local parts = SplitString(command:lower(), ' ')
if parts[1] ~= 'premium' then return end
if parts[2] == 'add' then
ApplyPremium(player:GetSelection())
end
return false
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())
end
end
local function OnSpellCast(event, player, spell, skipCheck)
local spellId = spell:GetEntry()
if spellId == SUMMON_SQUIRE_SPELL_ID then
OnSummonSquireCast(player)
end
end
local function OnGossipHello(event, player, creature)
player:GossipClearMenu()
player:GossipMenuAddItem(4, "Открыть почтовый ящик", 900001, 1)
player:GossipMenuAddItem(1, "Открыть банк", 900001, 2)
player:GossipMenuAddItem(6, "Открыть аукцион", 900001, 3)
player:GossipMenuAddItem(4, "Отремонтировать снаряжение", 900001, 4)
player:GossipSendMenu(NPC_TEXT_ID, creature)
end
local function OnGossipSelect(event, player, creature, sender, intid, code)
if sender == 900001 then
if intid == 1 then
player:SendShowMailBox(creature:GetGUID())
player:GossipComplete()
end
if intid == 2 then
player:SendShowBank(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)
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)
RegisterCreatureGossipEvent(SQUIRE_CREATURE_TEMPLATE_ID, 2, OnGossipSelect)
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)
+2 -2
View File
@@ -61,9 +61,9 @@
# false - (disabled)
Eluna.Enabled = true
Eluna.TraceBack = true
Eluna.TraceBack = false
Eluna.ScriptPath = "lua_scripts"
Eluna.PlayerAnnounceReload = true
Eluna.PlayerAnnounceReload = false
Eluna.RequirePaths = ""
Eluna.RequireCPaths = ""
Eluna.AutoReload = false
File diff suppressed because it is too large Load Diff
-1
View File
@@ -233,7 +233,6 @@ 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,7 +497,6 @@ 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,20 +825,4 @@ 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,7 +785,6 @@ 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>
*
-376
View File
@@ -1,376 +0,0 @@
-- Create Squire NPC
INSERT
INTO
acore_world.creature_template (entry,
difficulty_entry_1,
difficulty_entry_2,
difficulty_entry_3,
KillCredit1,
KillCredit2,
name,
subname,
IconName,
gossip_menu_id,
minlevel,
maxlevel,
`exp`,
faction,
npcflag,
speed_walk,
speed_run,
speed_swim,
speed_flight,
detection_range,
`rank`,
dmgschool,
DamageModifier,
BaseAttackTime,
RangeAttackTime,
BaseVariance,
RangeVariance,
unit_class,
unit_flags,
unit_flags2,
dynamicflags,
family,
`type`,
type_flags,
lootid,
pickpocketloot,
skinloot,
PetSpellDataId,
VehicleId,
mingold,
maxgold,
AIName,
MovementType,
HoverHeight,
HealthModifier,
ManaModifier,
ArmorModifier,
ExperienceModifier,
RacialLeader,
movementId,
RegenHealth,
flags_extra,
ScriptName,
VerifiedBuild)
VALUES
(9000001,
29831,
0,
0,
0,
0,
'Eluna Squire',
'',
'',
0,
81,
81,
2,
21,
69,341,185,
1.0,
1.42857,
1.0,
1.0,
20.0,
1,
0,
7.5,
2000,
2000,
1.0,
1.0,
1,
64,
2048,
8,
0,
6,
72,
100003,
0,
0,
0,
0,
1163,
1524,
'PassiveAI',
0,
1.0,
4.0,
1.0,
1.0,
2.5,
0,
144,
1,
0,
'',
12340) ON DUPLICATE KEY UPDATE entry = entry;
-- Add Squire localization
INSERT INTO
acore_world.creature_template_locale (entry,
locale,
Name,
Title,
VerifiedBuild)
VALUES
(9000001,
'ruRU',
'Оруженосец',
'',
12340) ON DUPLICATE KEY UPDATE entry = entry;
-- Add Squire model
INSERT
INTO
acore_world.creature_template_model (CreatureID,
Idx,
CreatureDisplayID,
DisplayScale,
Probability,
VerifiedBuild)
VALUES
(9000001,
0,
16927,
1.0,
1.0,
12340) ON
DUPLICATE KEY
UPDATE
CreatureID = CreatureID;
-- Add Squire dialogue text
INSERT
INTO
acore_world.npc_text (ID,
text0_0,
text0_1,
BroadcastTextID0,
lang0,
Probability0,
em0_0,
em0_1,
em0_2,
em0_3,
em0_4,
em0_5,
text1_0,
text1_1,
BroadcastTextID1,
lang1,
Probability1,
em1_0,
em1_1,
em1_2,
em1_3,
em1_4,
em1_5,
text2_0,
text2_1,
BroadcastTextID2,
lang2,
Probability2,
em2_0,
em2_1,
em2_2,
em2_3,
em2_4,
em2_5,
text3_0,
text3_1,
BroadcastTextID3,
lang3,
Probability3,
em3_0,
em3_1,
em3_2,
em3_3,
em3_4,
em3_5,
text4_0,
text4_1,
BroadcastTextID4,
lang4,
Probability4,
em4_0,
em4_1,
em4_2,
em4_3,
em4_4,
em4_5,
text5_0,
text5_1,
BroadcastTextID5,
lang5,
Probability5,
em5_0,
em5_1,
em5_2,
em5_3,
em5_4,
em5_5,
text6_0,
text6_1,
BroadcastTextID6,
lang6,
Probability6,
em6_0,
em6_1,
em6_2,
em6_3,
em6_4,
em6_5,
text7_0,
text7_1,
BroadcastTextID7,
lang7,
Probability7,
em7_0,
em7_1,
em7_2,
em7_3,
em7_4,
em7_5,
VerifiedBuild)
VALUES
(10000000,
'',
'By the grace of the stars, I have been bound to your side, $n. This ancient pile of bones is ready to serve you.',
0,
0,
0.0,
0,
0,
0,
0,
0,
0,
'',
'',
0,
0,
0.0,
0,
0,
0,
0,
0,
0,
'',
'',
0,
0,
0.0,
0,
0,
0,
0,
0,
0,
'',
'',
0,
0,
0.0,
0,
0,
0,
0,
0,
0,
'',
'',
0,
0,
0.0,
0,
0,
0,
0,
0,
0,
'',
'',
0,
0,
0.0,
0,
0,
0,
0,
0,
0,
'',
'',
0,
0,
0.0,
0,
0,
0,
0,
0,
0,
'',
'',
0,
0,
0.0,
0,
0,
0,
0,
0,
0,
0) ON DUPLICATE KEY UPDATE ID = ID;
-- Add squire dialogue text locale
INSERT
INTO
acore_world.npc_text_locale (ID,
Locale,
Text0_0,
Text0_1,
Text1_0,
Text1_1,
Text2_0,
Text2_1,
Text3_0,
Text3_1,
Text4_0,
Text4_1,
Text5_0,
Text5_1,
Text6_0,
Text6_1,
Text7_0,
Text7_1)
VALUES
(10000000,
'ruRU',
'',
'Милостью высших сил я был приставлен к тебе, $n. Эти древние кости готовы служить. Чего желаешь?',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'',
'') 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,
+123 -115
View File
@@ -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,14 +271,22 @@ 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
{
CreatureTemplate const *auctioneerInfo = creature->GetCreatureTemplate();
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);
if (!auctioneerInfo)
{
LOG_ERROR("network.opcode", "Non existing auctioneer ({})", auctioneer.ToString());
@@ -286,7 +294,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);
}
@@ -306,7 +314,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);
@@ -324,21 +332,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());
@@ -359,13 +367,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])
@@ -398,23 +406,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;
@@ -423,7 +431,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())
{
@@ -431,7 +439,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());
@@ -442,10 +450,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))
{
@@ -462,16 +470,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;
}
@@ -482,16 +490,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;
}
@@ -520,7 +528,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);
@@ -531,23 +539,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();
@@ -565,13 +573,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);
@@ -582,8 +590,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;
@@ -596,7 +604,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());
@@ -607,47 +615,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
@@ -659,15 +667,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))
{
@@ -675,7 +683,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());
@@ -691,7 +699,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;
@@ -711,16 +719,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;
@@ -728,7 +736,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;
@@ -737,8 +745,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;
@@ -746,7 +754,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;
@@ -783,7 +791,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;
@@ -791,7 +799,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;
@@ -822,12 +830,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);
@@ -838,14 +846,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