1<?php
2
3// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0.
4// See the LICENCE file in the repository root for full licence text.
5
6declare(strict_types=1);
7
8namespace App\Libraries;
9
10use App\Exceptions\ClientCheckParseTokenException;
11use App\Models\Build;
12use Illuminate\Http\Request;
13
14class ClientCheck
15{
16 public static function parseToken(Request $request): array
17 {
18 $token = $request->header('x-token');
19 $assertValid = $GLOBALS['cfg']['osu']['client']['check_version'];
20 $ret = [
21 'buildId' => $GLOBALS['cfg']['osu']['client']['default_build_id'],
22 'token' => null,
23 ];
24
25 try {
26 if ($token === null) {
27 throw new ClientCheckParseTokenException('missing token header');
28 }
29
30 $input = static::splitToken($token);
31
32 $build = Build::firstWhere([
33 'hash' => $input['clientHash'],
34 'allow_ranking' => true,
35 ]);
36
37 if ($build === null) {
38 throw new ClientCheckParseTokenException('invalid client hash');
39 }
40
41 $ret['buildId'] = $build->getKey();
42
43 $computed = hash_hmac(
44 'sha1',
45 $input['clientData'],
46 static::getKey($build),
47 true,
48 );
49
50 if (!hash_equals($computed, $input['expected'])) {
51 throw new ClientCheckParseTokenException('invalid verification hash');
52 }
53
54 $now = time();
55 if (abs($now - $input['clientTime']) > $GLOBALS['cfg']['osu']['client']['token_lifetime']) {
56 throw new ClientCheckParseTokenException('expired token');
57 }
58
59 $ret['token'] = $token;
60 // to be included in queue
61 $ret['body'] = base64_encode($request->getContent());
62 $ret['url'] = $request->getRequestUri();
63 } catch (ClientCheckParseTokenException $e) {
64 abort_if($assertValid, 422, $e->getMessage());
65 }
66
67 return $ret;
68 }
69
70 public static function queueToken(?array $tokenData, int $scoreId): void
71 {
72 if ($tokenData['token'] === null) {
73 return;
74 }
75
76 \LaravelRedis::lpush($GLOBALS['cfg']['osu']['client']['token_queue'], json_encode([
77 'body' => $tokenData['body'],
78 'id' => $scoreId,
79 'token' => $tokenData['token'],
80 'url' => $tokenData['url'],
81 ]));
82 }
83
84 private static function getKey(Build $build): string
85 {
86 return $GLOBALS['cfg']['osu']['client']['token_keys'][$build->platform()]
87 ?? $GLOBALS['cfg']['osu']['client']['token_keys']['default']
88 ?? '';
89 }
90
91 private static function splitToken(string $token): array
92 {
93 $data = substr($token, -82);
94 if (strlen($data) !== 82 || !ctype_xdigit(substr($data, 0, 80))) {
95 $data = str_repeat('0', 82);
96 }
97 $clientTime = unpack('V', hex2bin(substr($data, 32, 8)))[1];
98
99 return [
100 'clientData' => substr($data, 0, 40),
101 'clientHash' => hex2bin(substr($data, 0, 32)),
102 'clientTime' => $clientTime,
103 'expected' => hex2bin(substr($data, 40, 40)),
104 'version' => substr($data, 80, 2),
105 ];
106 }
107}