openvibers/openvibe.games Commits
Check protocol version before content digest and reject mod-pack maps
Changes
@@@ -221,6 +221,15 @@ describe('Games on platform identity, events and mods (real server, restart)', (})expect(stale.inbox.some((m) => m.t === 'welcome')).toBe(false)}++ // An older bundle sends neither the current version nor a digest; it must++ // get protocol_mismatch, the only reason the client reloads itself on.++ const oldBundle = connect(a.game)++ oldBundle.conn.identity = { guestKeyHash: hashGuestKey('old-bundle') }++ a.game.onMessage(oldBundle.conn, {++ t: 'hello', v: PROTOCOL_VERSION - 1, slot: 0, name: 'Old', appearance: defaultAppearance(),++ })++ await new Promise((r) => setTimeout(r, 0))++ expect(oldBundle.inbox).toContainEqual({ t: 'reject', reason: 'protocol_mismatch' })// The signed-in player is keyed by the canonical subject.const ana = connect(a.game)@@@ -87,6 +87,12 @@ export class SessionsSystem implements System {private async resolveHello(conn: GameConnection, msg: ClientHello): Promise<void> {const ctx = this.ctxconst serverDigest = ctx.world.content.digest++ // Protocol first: a stale bundle predates the digest and only reloads itself on protocol_mismatch.++ if (msg.v !== PROTOCOL_VERSION) {++ conn.send(encodeServerMessage({ t: 'reject', reason: 'protocol_mismatch' }))++ conn.close(4002, 'protocol_mismatch')++ return++ }if (msg.contentDigest !== serverDigest) {conn.send(encodeServerMessage({t: 'reject', reason: 'content_mismatch',@@@ -95,11 +101,6 @@ export class SessionsSystem implements System {conn.close(4013, 'content_mismatch')return}-- if (msg.v !== PROTOCOL_VERSION) {-- conn.send(encodeServerMessage({ t: 'reject', reason: 'protocol_mismatch' }))-- conn.close(4002, 'protocol_mismatch')-- return-- }if (ctx.sessions.size >= ctx.config.maxPlayers) {conn.send(encodeServerMessage({ t: 'reject', reason: 'server_full' }))conn.close(4003, 'server_full')@@@ -44,7 +44,9 @@ describe('games-content@2 definitions', () => {it('recognizes an authored map as a definition-capability section', () => {const mapPack = { map: SCRAPLANDIA_PACK.map! }expect(capabilitiesUsedBy(mapPack)).toEqual([CAP_DEFINE])-- expect(validateContentPack(mapPack, createContent()).ok).toBe(true)++ const result = validateContentPack(mapPack, createContent())++ expect(result.ok).toBe(false)++ expect(!result.ok && result.errors).toContain('map is defined by the built-in pack and cannot be replaced by a mod')expect(validateContentPack({ map: { id: 'broken' } }, createContent()).ok).toBe(false)})it('needs games.def.define for any non-empty defs section', () => {@@@ -154,6 +154,8 @@ export function validateContentPack(}const pack = value as unknown as ContentPackconst errors: string[] = []++ // The built-in pack always supplies the map, so a mod's map would be skipped at boot as a duplicate.++ if (pack.map) errors.push('map is defined by the built-in pack and cannot be replaced by a mod')if (pack.defs || pack.map) {// The content the pack would be merged onto; its own ids may not already exist.const merged = mergePackDefs(
@@ -221,6 +221,15 @@ describe('Games on platform identity, events and mods (real server, restart)', (
@@ -221,6 +221,15 @@ describe('Games on platform identity, events and mods (real server, restart)', (
})
})
expect(stale.inbox.some((m) => m.t === 'welcome')).toBe(false)
expect(stale.inbox.some((m) => m.t === 'welcome')).toBe(false)
}
}
+ // An older bundle sends neither the current version nor a digest; it must
+ // get protocol_mismatch, the only reason the client reloads itself on.
+ const oldBundle = connect(a.game)
+ oldBundle.conn.identity = { guestKeyHash: hashGuestKey('old-bundle') }
+ a.game.onMessage(oldBundle.conn, {
+ t: 'hello', v: PROTOCOL_VERSION - 1, slot: 0, name: 'Old', appearance: defaultAppearance(),
+ })
+ await new Promise((r) => setTimeout(r, 0))
+ expect(oldBundle.inbox).toContainEqual({ t: 'reject', reason: 'protocol_mismatch' })
// The signed-in player is keyed by the canonical subject.
// The signed-in player is keyed by the canonical subject.
const ana = connect(a.game)
const ana = connect(a.game)
@@ -87,6 +87,12 @@ export class SessionsSystem implements System {
@@ -87,6 +87,12 @@ export class SessionsSystem implements System {
private async resolveHello(conn: GameConnection, msg: ClientHello): Promise<void> {
private async resolveHello(conn: GameConnection, msg: ClientHello): Promise<void> {
const ctx = this.ctx
const ctx = this.ctx
const serverDigest = ctx.world.content.digest
const serverDigest = ctx.world.content.digest
+ // Protocol first: a stale bundle predates the digest and only reloads itself on protocol_mismatch.
+ if (msg.v !== PROTOCOL_VERSION) {
+ conn.send(encodeServerMessage({ t: 'reject', reason: 'protocol_mismatch' }))
+ conn.close(4002, 'protocol_mismatch')
+ return
+ }
if (msg.contentDigest !== serverDigest) {
if (msg.contentDigest !== serverDigest) {
conn.send(encodeServerMessage({
conn.send(encodeServerMessage({
t: 'reject', reason: 'content_mismatch',
t: 'reject', reason: 'content_mismatch',
@@ -95,11 +101,6 @@ export class SessionsSystem implements System {
@@ -95,11 +101,6 @@ export class SessionsSystem implements System {
conn.close(4013, 'content_mismatch')
conn.close(4013, 'content_mismatch')
return
return
}
}
- if (msg.v !== PROTOCOL_VERSION) {
- conn.send(encodeServerMessage({ t: 'reject', reason: 'protocol_mismatch' }))
- conn.close(4002, 'protocol_mismatch')
- return
- }
if (ctx.sessions.size >= ctx.config.maxPlayers) {
if (ctx.sessions.size >= ctx.config.maxPlayers) {
conn.send(encodeServerMessage({ t: 'reject', reason: 'server_full' }))
conn.send(encodeServerMessage({ t: 'reject', reason: 'server_full' }))
conn.close(4003, 'server_full')
conn.close(4003, 'server_full')
@@ -44,7 +44,9 @@ describe('games-content@2 definitions', () => {
@@ -44,7 +44,9 @@ describe('games-content@2 definitions', () => {
it('recognizes an authored map as a definition-capability section', () => {
it('recognizes an authored map as a definition-capability section', () => {
const mapPack = { map: SCRAPLANDIA_PACK.map! }
const mapPack = { map: SCRAPLANDIA_PACK.map! }
expect(capabilitiesUsedBy(mapPack)).toEqual([CAP_DEFINE])
expect(capabilitiesUsedBy(mapPack)).toEqual([CAP_DEFINE])
- expect(validateContentPack(mapPack, createContent()).ok).toBe(true)
+ const result = validateContentPack(mapPack, createContent())
+ expect(result.ok).toBe(false)
+ expect(!result.ok && result.errors).toContain('map is defined by the built-in pack and cannot be replaced by a mod')
expect(validateContentPack({ map: { id: 'broken' } }, createContent()).ok).toBe(false)
expect(validateContentPack({ map: { id: 'broken' } }, createContent()).ok).toBe(false)
})
})
it('needs games.def.define for any non-empty defs section', () => {
it('needs games.def.define for any non-empty defs section', () => {
@@ -154,6 +154,8 @@ export function validateContentPack(
@@ -154,6 +154,8 @@ export function validateContentPack(
}
}
const pack = value as unknown as ContentPack
const pack = value as unknown as ContentPack
const errors: string[] = []
const errors: string[] = []
+ // The built-in pack always supplies the map, so a mod's map would be skipped at boot as a duplicate.
+ if (pack.map) errors.push('map is defined by the built-in pack and cannot be replaced by a mod')
if (pack.defs || pack.map) {
if (pack.defs || pack.map) {
// The content the pack would be merged onto; its own ids may not already exist.
// The content the pack would be merged onto; its own ids may not already exist.
const merged = mergePackDefs(
const merged = mergePackDefs(