90 lines
2.8 KiB
PHP
90 lines
2.8 KiB
PHP
<?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.');
|
|
}
|
|
}
|
|
}
|