Skip to content

Commit

Permalink
Rework score token validation
Browse files Browse the repository at this point in the history
Mainly in relation to multiplayer play.
  • Loading branch information
nanaya committed Jan 8, 2025
1 parent 52ea0cf commit 1eabf9c
Show file tree
Hide file tree
Showing 10 changed files with 75 additions and 52 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

namespace App\Http\Controllers\Multiplayer\Rooms\Playlist;

use App\Exceptions\InvariantException;
use App\Http\Controllers\Controller as BaseController;
use App\Libraries\ClientCheck;
use App\Models\Multiplayer\PlaylistItem;
Expand Down Expand Up @@ -182,15 +181,10 @@ public function store($roomId, $playlistId)
$playlistItem = $room->playlist()->findOrFail($playlistId);
$user = \Auth::user();
$request = \Request::instance();
$params = $request->all();

if (get_string($params['beatmap_hash'] ?? null) !== $playlistItem->beatmap->checksum) {
throw new InvariantException(osu_trans('score_tokens.create.beatmap_hash_invalid'));
}

$buildId = ClientCheck::parseToken($request)['buildId'];

$scoreToken = $room->startPlay($user, $playlistItem, $buildId);
$scoreToken = $room->startPlay($user, $playlistItem, $buildId, $request->all());

return json_item($scoreToken, new ScoreTokenTransformer());
}
Expand Down
15 changes: 1 addition & 14 deletions app/Http/Controllers/ScoreTokensController.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,27 +34,14 @@ public function store($beatmapId)
'ruleset_id:int',
]);

$checks = [
'beatmap_hash' => fn (string $value): bool => $value === $beatmap->checksum,
'ruleset_id' => fn (int $value): bool => Beatmap::modeStr($value) !== null && $beatmap->canBeConvertedTo($value),
];
foreach ($checks as $key => $testFn) {
if (!isset($params[$key])) {
throw new InvariantException("missing {$key}");
}
if (!$testFn($params[$key])) {
throw new InvariantException("invalid {$key}");
}
}

$buildId = ClientCheck::parseToken($request)['buildId'];

try {
$scoreToken = ScoreToken::create([
'beatmap_id' => $beatmap->getKey(),
'build_id' => $buildId,
'ruleset_id' => $params['ruleset_id'],
'user_id' => $user->getKey(),
...$params,
]);
} catch (PDOException $e) {
// TODO: move this to be a validation inside Score model
Expand Down
13 changes: 9 additions & 4 deletions app/Models/Multiplayer/Room.php
Original file line number Diff line number Diff line change
Expand Up @@ -658,13 +658,19 @@ public function endGame(User $requestingUser)
$this->save();
}

public function startPlay(User $user, PlaylistItem $playlistItem, int $buildId)
public function startPlay(User $user, PlaylistItem $playlistItem, int $buildId, array $rawParams): ScoreToken
{
priv_check_user($user, 'MultiplayerScoreSubmit', $this)->ensureCan();

$params = [
'beatmap_hash' => get_string($rawParams['beatmap_hash'] ?? null),
'beatmap_id' => $playlistItem->beatmap_id,
'ruleset_id' => $playlistItem->ruleset_id,
];

$this->assertValidStartPlay($user, $playlistItem);

return $this->getConnection()->transaction(function () use ($buildId, $user, $playlistItem) {
return $this->getConnection()->transaction(function () use ($buildId, $user, $params, $playlistItem) {
$agg = UserScoreAggregate::new($user, $this);
if ($agg->wasRecentlyCreated) {
$this->incrementInstance('participant_count');
Expand All @@ -676,11 +682,10 @@ public function startPlay(User $user, PlaylistItem $playlistItem, int $buildId)
$playlistItemAgg->updateUserAttempts();

return ScoreToken::create([
'beatmap_id' => $playlistItem->beatmap_id,
'build_id' => $buildId,
'playlist_item_id' => $playlistItem->getKey(),
'ruleset_id' => $playlistItem->ruleset_id,
'user_id' => $user->getKey(),
...$params,
]);
});
}
Expand Down
33 changes: 33 additions & 0 deletions app/Models/ScoreToken.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

namespace App\Models;

use App\Exceptions\InvariantException;
use App\Models\Multiplayer\PlaylistItem;
use App\Models\Solo\Score;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
Expand All @@ -25,6 +26,8 @@
*/
class ScoreToken extends Model
{
public ?string $beatmapHash = null;

public function beatmap()
{
return $this->belongsTo(Beatmap::class, 'beatmap_id');
Expand Down Expand Up @@ -74,4 +77,34 @@ public function getAttribute($key)
'user' => $this->getRelationValue($key),
};
}

public function setBeatmapHashAttribute(?string $value): void
{
$this->beatmapHash = $value;
}

public function assertValid(): void
{
$beatmap = $this->beatmap;
if ($this->beatmapHash !== $beatmap->checksum) {
throw new InvariantException(osu_trans('score_tokens.create.beatmap_hash_invalid'));
}

$rulesetId = $this->ruleset_id;
if ($rulesetId === null) {
throw new InvariantException('missing ruleset_id');
}
if (Beatmap::modeStr($rulesetId) === null || !$beatmap->canBeConvertedTo($rulesetId)) {
throw new InvariantException('invalid ruleset_id');
}
}

public function save(array $options = []): bool
{
if (!$this->exists) {
$this->assertValid();
}

return parent::save($options);
}
}
3 changes: 2 additions & 1 deletion database/factories/ScoreTokenFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ public function definition(): array
'build_id' => Build::factory(),
'user_id' => User::factory(),

// depends on beatmap_id
// depend on beatmap_id
'beatmap_hash' => fn (array $attr) => Beatmap::find($attr['beatmap_id'])->checksum,
'ruleset_id' => fn (array $attr) => Beatmap::find($attr['beatmap_id'])->playmode,
];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ public function testUpdate($bodyParams, $status)
$playlistItem = PlaylistItem::factory()->create();
$room = $playlistItem->room;
$build = Build::factory()->create(['allow_ranking' => true]);
$scoreToken = $room->startPlay($user, $playlistItem, 0);
$scoreToken = static::roomStartPlay($user, $playlistItem);

$this->withHeaders(['x-token' => static::createClientToken($build)]);

Expand Down
22 changes: 7 additions & 15 deletions tests/Controllers/ScoreTokensControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public function testStore(string $beatmapState, int $status): void
/**
* @dataProvider dataProviderForTestStoreInvalidParameter
*/
public function testStoreInvalidParameter(string $paramKey, ?string $paramValue, int $status): void
public function testStoreInvalidParameter(string $paramKey, ?string $paramValue, int $status, string $errorMessage): void
{
$origClientCheckVersion = $GLOBALS['cfg']['osu']['client']['check_version'];
config_set('osu.client.check_version', true);
Expand Down Expand Up @@ -78,14 +78,6 @@ public function testStoreInvalidParameter(string $paramKey, ?string $paramValue,

$this->expectCountChange(fn () => ScoreToken::count(), 0);

$errorMessage = $paramValue === null ? 'missing' : 'invalid';
$errorMessage .= ' ';
$errorMessage .= $paramKey === 'client_token'
? ($paramValue === null
? 'token header'
: 'client hash'
) : $paramKey;

$this->json(
'POST',
route('api.beatmaps.solo.score-tokens.store', $routeParams),
Expand Down Expand Up @@ -161,14 +153,14 @@ public static function dataProviderForTestStore(): array
public static function dataProviderForTestStoreInvalidParameter(): array
{
return [
'invalid client token' => ['client_token', md5('invalid_'), 422],
'missing client token' => ['client_token', null, 422],
'invalid client token' => ['client_token', md5('invalid_'), 422, 'invalid client hash'],
'missing client token' => ['client_token', null, 422, 'missing token header'],

'invalid ruleset id' => ['ruleset_id', '5', 422],
'missing ruleset id' => ['ruleset_id', null, 422],
'invalid ruleset id' => ['ruleset_id', '5', 422, 'invalid ruleset_id'],
'missing ruleset id' => ['ruleset_id', null, 422, 'missing ruleset_id'],

'invalid beatmap hash' => ['beatmap_hash', 'xxx', 422],
'missing beatmap hash' => ['beatmap_hash', null, 422],
'invalid beatmap hash' => ['beatmap_hash', 'xxx', 422, 'invalid or missing beatmap_hash'],
'missing beatmap hash' => ['beatmap_hash', null, 422, 'invalid or missing beatmap_hash'],
];
}

Expand Down
16 changes: 8 additions & 8 deletions tests/Models/Multiplayer/RoomTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ public function testRoomHasEnded()
]);

$this->expectException(InvariantException::class);
$room->startPlay($user, $playlistItem, 0);
static::roomStartPlay($user, $playlistItem);
}

public function testStartPlay(): void
Expand All @@ -137,7 +137,7 @@ public function testStartPlay(): void
$this->expectCountChange(fn () => $room->userHighScores()->count(), 1);
$this->expectCountChange(fn () => $playlistItem->scoreTokens()->count(), 1);

$room->startPlay($user, $playlistItem, 0);
static::roomStartPlay($user, $playlistItem);
$room->refresh();

$this->assertSame($user->getKey(), $playlistItem->scoreTokens()->last()->user_id);
Expand All @@ -150,14 +150,14 @@ public function testMaxAttemptsReached()
$playlistItem1 = PlaylistItem::factory()->create(['room_id' => $room]);
$playlistItem2 = PlaylistItem::factory()->create(['room_id' => $room]);

$room->startPlay($user, $playlistItem1, 0);
static::roomStartPlay($user, $playlistItem1);
$this->assertTrue(true);

$room->startPlay($user, $playlistItem2, 0);
static::roomStartPlay($user, $playlistItem2);
$this->assertTrue(true);

$this->expectException(InvariantException::class);
$room->startPlay($user, $playlistItem1, 0);
static::roomStartPlay($user, $playlistItem1);
}

public function testMaxAttemptsForItemReached()
Expand All @@ -174,19 +174,19 @@ public function testMaxAttemptsForItemReached()
]);

$initialCount = $playlistItem1->scoreTokens()->count();
$room->startPlay($user, $playlistItem1, 0);
static::roomStartPlay($user, $playlistItem1);
$this->assertSame($initialCount + 1, $playlistItem1->scoreTokens()->count());

$initialCount = $playlistItem1->scoreTokens()->count();
try {
$room->startPlay($user, $playlistItem1, 0);
static::roomStartPlay($user, $playlistItem1);
} catch (Exception $ex) {
$this->assertTrue($ex instanceof InvariantException);
}
$this->assertSame($initialCount, $playlistItem1->scoreTokens()->count());

$initialCount = $playlistItem2->scoreTokens()->count();
$room->startPlay($user, $playlistItem2, 0);
static::roomStartPlay($user, $playlistItem2);
$this->assertSame($initialCount + 1, $playlistItem2->scoreTokens()->count());
}

Expand Down
2 changes: 1 addition & 1 deletion tests/Models/Multiplayer/UserScoreAggregateTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ public function testStartingPlayIncreasesAttempts(): void
$user = User::factory()->create();
$playlistItem = $this->createPlaylistItem();

$this->room->startPlay($user, $playlistItem, 0);
static::roomStartPlay($user, $playlistItem);
$agg = UserScoreAggregate::new($user, $this->room);

$this->assertSame(1, $agg->attempts);
Expand Down
13 changes: 12 additions & 1 deletion tests/TestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use App\Models\Multiplayer\PlaylistItem;
use App\Models\Multiplayer\ScoreLink;
use App\Models\OAuth\Client;
use App\Models\ScoreToken;
use App\Models\User;
use Artisan;
use Carbon\CarbonInterface;
Expand Down Expand Up @@ -111,7 +112,7 @@ protected static function resetAppDb(DatabaseManager $database): void
protected static function roomAddPlay(User $user, PlaylistItem $playlistItem, array $scoreParams): ScoreLink
{
return $playlistItem->room->completePlay(
$playlistItem->room->startPlay($user, $playlistItem, 0),
static::roomStartPlay($user, $playlistItem),
[
'accuracy' => 0.5,
'beatmap_id' => $playlistItem->beatmap_id,
Expand All @@ -126,6 +127,16 @@ protected static function roomAddPlay(User $user, PlaylistItem $playlistItem, ar
);
}

protected static function roomStartPlay(User $user, PlaylistItem $playlistItem): ScoreToken
{
return $playlistItem->room->startPlay(
$user,
$playlistItem,
0,
['beatmap_hash' => $playlistItem->beatmap->checksum]
);
}

protected function setUp(): void
{
$this->beforeApplicationDestroyed(fn () => $this->runExpectedCountsCallbacks());
Expand Down

0 comments on commit 1eabf9c

Please sign in to comment.