Skip to content

Commit cf6ed6f

Browse files
authored
fix: check world password before spawning player (#227)
1 parent 96834bb commit cf6ed6f

10 files changed

Lines changed: 66 additions & 25 deletions

File tree

app/src/commands/player.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,9 +121,10 @@ export async function setPlayerStatus(status: PlayerStatus) {
121121
await invoke('set_player_status', { req });
122122
}
123123

124-
export async function spawnPlayer(options: PlayerOptions) {
124+
export async function spawnPlayer(options: PlayerOptions, worldPassword: Option<string>) {
125125
const req: SpawnPlayerRequest = {
126126
world: NIL.world.getIdStrict(),
127+
worldPassword: worldPassword ?? null,
127128
options,
128129
};
129130

app/src/core/game.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { exit } from '@tauri-apps/plugin-process';
99

1010
async function joinGame(options: {
1111
worldId: NonNullable<ClientOptions['worldId']>;
12+
worldPassword: ClientOptions['worldPassword'];
1213
playerId: NonNullable<ClientOptions['playerId']>;
1314
}) {
1415
await NIL.world.setId(options.worldId);
@@ -22,7 +23,8 @@ async function joinGame(options: {
2223
}
2324
}
2425
else {
25-
await commands.spawnPlayer({ id: playerId });
26+
const playerOptions: PlayerOptions = { id: playerId };
27+
await commands.spawnPlayer(playerOptions, options.worldPassword);
2628
}
2729

2830
await NIL.player.setId(playerId);
@@ -48,6 +50,7 @@ export async function joinLocalGame(options: {
4850
if (options.worldId) {
4951
return joinGame({
5052
worldId: options.worldId,
53+
worldPassword: null,
5154
playerId: options.playerId,
5255
});
5356
}
@@ -58,17 +61,23 @@ export async function joinLocalGame(options: {
5861

5962
export async function joinRemoteGame(options: {
6063
worldId: NonNullable<ClientOptions['worldId']>;
64+
worldPassword: ClientOptions['worldPassword'];
6165
authorizationToken: NonNullable<ClientOptions['authorizationToken']>;
6266
}) {
6367
const playerId = await commands.validateToken(options.authorizationToken);
6468
if (playerId) {
6569
await commands.updateClient({
6670
serverAddr: { kind: 'remote' },
6771
worldId: options.worldId,
72+
worldPassword: options.worldPassword,
6873
authorizationToken: options.authorizationToken,
6974
});
7075

71-
return joinGame({ worldId: options.worldId, playerId });
76+
return joinGame({
77+
worldId: options.worldId,
78+
worldPassword: options.worldPassword,
79+
playerId,
80+
});
7281
}
7382
else {
7483
throw new Error('Invalid token');

app/src/lib/request/player.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,5 +57,6 @@ export interface SetPlayerStatusRequest {
5757

5858
export interface SpawnPlayerRequest {
5959
readonly world: WorldId;
60+
readonly worldPassword: Option<string>;
6061
readonly options: PlayerOptions;
6162
}

app/src/scenes/online/join-remote-game/Box.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,6 @@ defineProps<{
88
<template>
99
<div class="flex h-16 flex-col items-center justify-center">
1010
<span class="text-muted-foreground text-xs md:text-sm break-all wrap-anywhere">{{ label }}</span>
11-
<span class="text-base md:text-lg font-semibold break-all wrap-anywhere">{{ content }}</span>
11+
<span class="text-sm md:text-base font-semibold break-all wrap-anywhere">{{ content }}</span>
1212
</div>
1313
</template>

app/src/scenes/online/join-remote-game/index.vue

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { computed, ref } from 'vue';
99
import { useMutex } from '@tb-dev/vue';
1010
import { useRouter } from 'vue-router';
1111
import { formatToday } from '@/lib/date';
12+
import { joinRemoteGame } from '@/core/game';
1213
import { useUserStore } from '@/stores/user';
1314
import Loading from '@/components/Loading.vue';
1415
import { isValidPassword } from '@/lib/schema';
@@ -34,19 +35,27 @@ const { authorizationToken } = storeToRefs(userStore);
3435
3536
const { remoteWorld, loading } = useRemoteWorld(worldId);
3637
37-
const password = ref<Option<string>>();
38+
const worldPassword = ref<Option<string>>();
3839
3940
const { locked, lock } = useMutex();
4041
const canJoin = computed(() => {
4142
return (
4243
Boolean(remoteWorld.value) &&
4344
Boolean(authorizationToken.value) &&
44-
(!remoteWorld.value?.hasPassword || isValidPassword(password.value))
45+
(!remoteWorld.value?.hasPassword || isValidPassword(worldPassword.value))
4546
);
4647
});
4748
4849
async function join() {
49-
await lock(async () => {});
50+
await lock(async () => {
51+
if (remoteWorld.value && authorizationToken.value) {
52+
await joinRemoteGame({
53+
worldId: remoteWorld.value.id,
54+
worldPassword: worldPassword.value,
55+
authorizationToken: authorizationToken.value,
56+
});
57+
}
58+
});
5059
}
5160
</script>
5261

@@ -69,11 +78,8 @@ async function join() {
6978
<Box :label="t('updated-at')" :content="formatToday(remoteWorld.updatedAtDate)" />
7079
</div>
7180

72-
<div class="w-full flex justify-center items-center">
73-
<span
74-
v-if="remoteWorld.description"
75-
class="md:max-w-6/8 break-all wrap-anywhere text-center text-muted-foreground"
76-
>
81+
<div v-if="remoteWorld.description" class="w-full flex justify-center items-center py-2">
82+
<span class="md:max-w-6/8 text-sm md:text-base text-center text-muted-foreground break-all wrap-anywhere">
7783
{{ remoteWorld.description }}
7884
</span>
7985
</div>
@@ -82,7 +88,7 @@ async function join() {
8288
<CardFooter class="w-full flex flex-col gap-4!">
8389
<Input
8490
v-if="remoteWorld.hasPassword"
85-
v-model="password"
91+
v-model="worldPassword"
8692
type="password"
8793
:placeholder="t('password')"
8894
:disabled="locked"

app/src/types/player.d.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,4 @@ type PlayerStatus = 'active' | 'inactive';
1616

1717
interface PlayerOptions {
1818
readonly id: PlayerId;
19-
readonly password?: Option<string>;
2019
}

crates/nil-database/src/database/game.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ use crate::sql_types::hashed_password::HashedPassword;
99
use crate::sql_types::zoned::SqlZoned;
1010
use diesel::prelude::*;
1111
use diesel::result::Error as DieselError;
12+
use nil_util::password::Password;
13+
use nil_util::result::WrapOk;
1214

1315
macro_rules! decl_get {
1416
($fn_name:ident, $model:ident) => {
@@ -81,4 +83,19 @@ impl Database {
8183
.execute(&mut *self.conn())
8284
.map_err(Into::into)
8385
}
86+
87+
pub fn verify_game_password(
88+
&self,
89+
game_id: impl Into<GameId>,
90+
password: Option<&Password>,
91+
) -> Result<bool> {
92+
if let Some(hash) = self.get_game_password(game_id)? {
93+
password
94+
.filter(|it| !it.trim().is_empty())
95+
.is_some_and(|it| hash.verify(it))
96+
.wrap_ok()
97+
} else {
98+
Ok(true)
99+
}
100+
}
84101
}

crates/nil-payload/src/player.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
use nil_core::player::{PlayerId, PlayerOptions, PlayerStatus};
55
use nil_core::world::WorldId;
6+
use nil_util::password::Password;
67
use serde::{Deserialize, Serialize};
78

89
#[derive(Clone, Debug, Deserialize, Serialize)]
@@ -87,5 +88,7 @@ pub struct SetPlayerStatusRequest {
8788
#[serde(rename_all = "camelCase")]
8889
pub struct SpawnPlayerRequest {
8990
pub world: WorldId,
91+
#[serde(default)]
92+
pub world_password: Option<Password>,
9093
pub options: PlayerOptions,
9194
}

crates/nil-server/src/router/mod.rs

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -219,18 +219,11 @@ async fn websocket(
219219
if app.server_kind().is_remote() {
220220
match app
221221
.database()
222-
.get_game_password(query.world_id)
222+
.verify_game_password(query.world_id, query.world_password.as_ref())
223223
{
224-
Ok(Some(hash))
225-
if query
226-
.world_password
227-
.filter(|it| !it.trim().is_empty())
228-
.is_none_or(|it| !hash.verify(&it)) =>
229-
{
230-
return Error::IncorrectWorldCredentials(query.world_id).into();
231-
}
224+
Ok(true) => {}
225+
Ok(false) => return Error::IncorrectWorldCredentials(query.world_id).into(),
232226
Err(err) => return from_database_err(err),
233-
_ => {}
234227
}
235228
}
236229

crates/nil-server/src/router/player.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22
// SPDX-License-Identifier: AGPL-3.0-only
33

44
use crate::app::App;
5+
use crate::error::Error;
56
use crate::middleware::authorization::CurrentPlayer;
67
use crate::res;
7-
use crate::response::EitherExt;
8+
use crate::response::{EitherExt, from_database_err};
89
use axum::extract::{Extension, Json, State};
910
use axum::response::Response;
1011
use itertools::Itertools;
@@ -146,6 +147,17 @@ pub async fn set_status(
146147
}
147148

148149
pub async fn spawn(State(app): State<App>, Json(req): Json<SpawnPlayerRequest>) -> Response {
150+
if app.server_kind().is_remote() {
151+
match app
152+
.database()
153+
.verify_game_password(req.world, req.world_password.as_ref())
154+
{
155+
Ok(true) => {}
156+
Ok(false) => return Error::IncorrectWorldCredentials(req.world).into(),
157+
Err(err) => return from_database_err(err),
158+
}
159+
}
160+
149161
app
150162
.world_mut(req.world, |world| {
151163
world.spawn_player(Player::new(req.options))

0 commit comments

Comments
 (0)