This commit is contained in:
2026-08-17 20:25:40 +04:00
parent 9e36265f81
commit 6a73d760c4
7 changed files with 366 additions and 0 deletions
@@ -0,0 +1,89 @@
<?php
namespace App\Services;
use App\Models\GameAccount;
use Carbon\CarbonImmutable;
use Illuminate\Support\Facades\DB;
use RuntimeException;
class LauncherGameTicketService
{
private const string ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
private const int TICKET_LENGTH = 16;
public function __construct(private readonly AzerothCoreSrpService $srp) {}
/**
* @return array{account: string, ticket: string, expires_at: string}
*/
public function issue(GameAccount $account, string $launcherSessionToken, int $clientBuild): array
{
$username = $this->srp->normalizeUsername((string) $account->username);
$this->assertCompatibleAccountName($username);
$ticket = $this->randomTicket();
[$saltHex, $verifierHex] = $this->srp->makeRegistrationData($username, $ticket);
$generationId = random_bytes(16);
$sessionHash = hash('sha256', $launcherSessionToken, true);
$issuedAt = CarbonImmutable::now('UTC');
$ttl = max(5, min(60, (int) config('moonwell.launcher.game_ticket_ttl', 60)));
$expiresAt = $issuedAt->addSeconds($ttl);
$connection = DB::connection((string) config('moonwell.auth_connection'));
$connection->transaction(function () use (
$connection,
$account,
$generationId,
$saltHex,
$verifierHex,
$clientBuild,
$sessionHash,
$issuedAt,
$expiresAt,
): void {
$connection->table('launcher_ticket')->updateOrInsert(
['account_id' => (int) $account->id],
[
'generation_id' => $generationId,
'srp_salt' => hex2bin($saltHex),
'srp_verifier' => hex2bin($verifierHex),
'client_build' => $clientBuild,
'launcher_session_hash' => $sessionHash,
'issued_at' => $issuedAt,
'expires_at' => $expiresAt,
],
);
});
return [
'account' => $username,
'ticket' => $ticket,
'expires_at' => $expiresAt->format('Y-m-d\\TH:i:s\\Z'),
];
}
private function randomTicket(): string
{
$ticket = '';
$lastIndex = strlen(self::ALPHABET) - 1;
for ($i = 0; $i < self::TICKET_LENGTH; $i++) {
$ticket .= self::ALPHABET[random_int(0, $lastIndex)];
}
return $ticket;
}
private function assertCompatibleAccountName(string $username): void
{
if (
$username === ''
|| strlen($username) > 17
|| preg_match('/^[!-<>-~]+$/D', $username) !== 1
) {
throw new RuntimeException('Game account is not compatible with the 3.3.5a authentication protocol.');
}
}
}