diff --git a/server/games/erqiwang/class.paiju.js b/server/games/erqiwang/class.paiju.js index 2454db4..53a1197 100644 --- a/server/games/erqiwang/class.paiju.js +++ b/server/games/erqiwang/class.paiju.js @@ -201,10 +201,15 @@ var cls_youle_erqiwang_paiju = cls_youle_erqiwang_paiju || { cls_youle_erqiwang_paiju.do_choiceflower(paiju, flower); } + //校验客户端提交的牌id列表本身是否合法(数组/整数/范围/无重复) + paiju.method.check_cards_valid = function(cards){ + return cls_youle_erqiwang_paiju.check_cards_valid(paiju, cards); + } + //判断牌是否在玩家手上 paiju.method.check_cards_inhand = function(cards, seat){ return cls_youle_erqiwang_paiju.check_cards_inhand(paiju, cards, seat); - } + } //埋牌 paiju.method.do_burycard = function(cards){ @@ -449,12 +454,42 @@ var cls_youle_erqiwang_paiju = cls_youle_erqiwang_paiju || { o_paiju.step = 3; }, - //判断牌是否在玩家手上 + //校验客户端提交的牌id列表本身是否合法:必须是非空数组、元素为本局牌表内的整数牌id、且互不重复 + //(客户端入参不可信:重复id 会被 can_playcard 当成对子/拖拉机,也会让埋牌少埋;越界id 会取到 + // undefined 而在后续解引用时抛异常中断 DoPack。此处显式失败,不做隐式兜底) + check_cards_valid: function(o_paiju, cards){ + if (Object.prototype.toString.call(cards) != "[object Array]"){ + return false; + } + if (cards.length <= 0){ + return false; + } + var _seen = {}; + for (var i = 0; i < cards.length; i++){ + var _id = cards[i]; + //必须是落在本局牌表范围内的整数牌id + if (typeof _id != "number" || _id != parseInt(_id) || _id < 0 || _id >= o_paiju.cards.length){ + return false; + } + //同一张牌不得重复提交 + if (_seen[_id]){ + return false; + } + _seen[_id] = 1; + } + return true; + }, + + //判断牌是否在玩家手上(入参合法性由 check_cards_valid 一并把关,本函数是唯一的持牌校验入口) check_cards_inhand: function(o_paiju, cards, seat){ + if (!cls_youle_erqiwang_paiju.check_cards_valid(o_paiju, cards)){ + return false; + } for (var i = 0; i < cards.length; i++) { var o_card = o_paiju.cards[cards[i]]; if (o_card.playround != -1){ - return; + //已埋下或已打出的牌 + return false; } if (seat == o_paiju.banker){ //是庄家 diff --git a/server/games/erqiwang/mod.js b/server/games/erqiwang/mod.js index 1046729..99e8c36 100644 --- a/server/games/erqiwang/mod.js +++ b/server/games/erqiwang/mod.js @@ -230,6 +230,10 @@ youle_erqiwang.maipai = function(pack){ if (o_paiju.banker != seat){ return; } + //检查埋牌入参本身是否合法(数组/整数牌id/范围/无重复),须先于 length 判断 + if (!o_paiju.method.check_cards_valid(cards)){ + return; + } //检查埋牌是否是8张 if (cards.length != 8){ return; @@ -355,8 +359,8 @@ youle_erqiwang.chupai = function(pack){ if (seat != o_paiju.playproc.currseat){ return; } - //检查出牌 - if (cards.length == 0){ + //检查出牌入参本身是否合法(数组/整数牌id/范围/无重复,含非空) + if (!o_paiju.method.check_cards_valid(cards)){ return; } //检查出牌是否都在玩家手上 diff --git a/server/games/erqiwang/test/test_input.js b/server/games/erqiwang/test/test_input.js new file mode 100644 index 0000000..1671652 --- /dev/null +++ b/server/games/erqiwang/test/test_input.js @@ -0,0 +1,95 @@ +// 客户端入参校验:重复牌id / 越界id / 非数组 / 非整数(check_cards_valid + check_cards_inhand) +// 回归两个真实缺陷:① 重复id 被 can_playcard 当成对子/拖拉机;② 埋牌传重复id 只埋下 1 张 +require('./_shim'); +const { mod, setup, make108, id } = require('./_rpc.js'); +const P = require('../class.paiju.js'); +const t = require('./_assert')(); + +// 造一个最小可用牌局:108 张牌全发给指定座位,主花色=1 +function newPaiju(step, banker, owner) { + const paiju = { + cards: make108(), step, banker, call: 65, flower: 1, + seatlist: [[[0, 0], [0, 0], [0, 0], [0, 0], [-1, -1]], [[0, 0], [0, 0], [0, 0], [0, 0], [-1, -1]], [[0, 0], [0, 0], [0, 0], [0, 0], [-1, -1]]], + playproc: {}, callproc: [], idx: 1 + }; + for (let i = 0; i < 108; i++) paiju.cards[i].dealowner = owner + 1; + paiju.method = { + check_cards_valid: c => P.check_cards_valid(paiju, c), + check_cards_inhand: (c, s) => P.check_cards_inhand(paiju, c, s), + do_burycard: c => P.do_burycard(paiju, c), + do_playcard: c => P.do_playcard(paiju, c), + get_seat_cards: s => P.get_seat_cards(paiju, s), + get_seat_zhucards: s => P.get_seat_zhucards(paiju, s), + have_baofu: () => (paiju.seatlist[0][4][0] == 0 || paiju.seatlist[1][4][0] == 0 || paiju.seatlist[2][4][0] == 0), + get_liangpai: () => null + }; + return paiju; +} + +// ============ L1 check_cards_valid:正 / 反 / 边界 ============ +const pj = newPaiju(5, 0, 0); +const V = c => P.check_cards_valid(pj, c); + +t.eq('valid 正 单张', V([0]), true); +t.eq('valid 正 多张不重复', V([0, 1, 2, 53]), true); +t.eq('valid 边界 最大牌id 107', V([107]), true); +t.eq('valid 边界 最小牌id 0', V([0]), true); + +t.eq('valid 反 undefined', V(undefined), false); +t.eq('valid 反 null', V(null), false); +t.eq('valid 反 字符串', V('012'), false); +t.eq('valid 反 对象', V({ 0: 1, length: 1 }), false); +t.eq('valid 反 空数组', V([]), false); +t.eq('valid 反 重复id', V([5, 5]), false); +t.eq('valid 反 多张中含重复', V([1, 2, 3, 2]), false); +t.eq('valid 反 越界上界108', V([108]), false); +t.eq('valid 反 负数id', V([-1]), false); +t.eq('valid 反 小数id', V([1.5]), false); +t.eq('valid 反 数字字符串', V(['5']), false); + +// ============ L1 check_cards_inhand ============ +t.eq('inhand 正 在手上', P.check_cards_inhand(pj, [0, 1], 0), true); +t.eq('inhand 反 不在手上(别人的)', P.check_cards_inhand(pj, [0], 1), false); +t.eq('inhand 反 重复id(经 valid 拦截)', P.check_cards_inhand(pj, [0, 0], 0), false); +t.eq('inhand 反 越界id(经 valid 拦截)', P.check_cards_inhand(pj, [999], 0), false); +pj.cards[3].playround = 1; // 已打出 +t.eq('inhand 反 已打出的牌', P.check_cards_inhand(pj, [3], 0), false); +pj.cards[4].playround = 0; // 已埋 +t.eq('inhand 反 已埋下的牌', P.check_cards_inhand(pj, [4], 0), false); + +// ============ L2 出牌:重复id 不得被当成一对 ============ +const pj2 = newPaiju(5, 0, 0); +P.new_playround(pj2, 1, 0); +const cid = id(1, 1, 13); // 方块K +t.eq('出牌 反 重复id 被 mod 层拦截(valid)', P.check_cards_valid(pj2, [cid, cid]), false); + +// ============ L3 mod.maipai:8 个重复id 必须被拒、不得进入出牌阶段 ============ +const pj3 = newPaiju(3, 0, 0); +const s3 = setup('00000', pj3); +const dup = id(1, 1, 13); +mod.maipai({ data: { seat: 0, cards: [dup, dup, dup, dup, dup, dup, dup, dup] }, conmode: 0, fromid: 0 }); +t.eq('埋牌 反 8个重复id 被拒(step仍为3)', pj3.step, 3); +t.eq('埋牌 反 8个重复id 未埋下任何牌', pj3.cards.filter(c => c.playround === 0).length, 0); + +// 对照:8 张不同的牌可以正常埋下 +const pj4 = newPaiju(3, 0, 0); +setup('00000', pj4); +mod.maipai({ data: { seat: 0, cards: [0, 1, 2, 3, 4, 5, 6, 7] }, conmode: 0, fromid: 0 }); +t.eq('埋牌 正 8张不同牌 埋牌成功(step进5)', pj4.step, 5); +t.eq('埋牌 正 实际埋下8张', pj4.cards.filter(c => c.playround === 0).length, 8); + +// ============ L3 mod.chupai:越界/非数组入参不得抛异常 ============ +const pj5 = newPaiju(5, 0, 0); +P.new_playround(pj5, 1, 0); +const s5 = setup('00000', pj5); +let threw = null; +try { + mod.chupai({ data: { seat: 0, cards: [999] }, conmode: 0, fromid: 0 }); + mod.chupai({ data: { seat: 0, cards: undefined }, conmode: 0, fromid: 0 }); + mod.chupai({ data: { seat: 0, cards: 'x' }, conmode: 0, fromid: 0 }); + mod.chupai({ data: { seat: 0, cards: [dup, dup] }, conmode: 0, fromid: 0 }); +} catch (e) { threw = String(e); } +t.eq('出牌 反 非法入参不抛异常', threw, null); +t.eq('出牌 反 非法入参不产生任何出牌', pj5.cards.filter(c => c.playround > 0).length, 0); + +process.exit(t.done('input') ? 0 : 1);