Building online multiplayers games with Meteor [codes + tutorials] - 6

This is the 6th post of a blog series. Please refer to the introductory post for the series introduction.

Source Code

Source code at the end of the previous part:

Source code at the end of the this part:

Validating game actions in server (avoid cheating)

As mentioned previously, we will address some of the security issues we intended to ignore previously. Like any other online games, or any applications in general, you should never trust client inputs. If you are new to Meteor, it’s very easy for you to miss that out, because one philosophy of Meteor is to get rid of the concept of frontend and backend. This is reflected in how we do game updates directly on client side.

In general, there are two ways to control client side updates in Meteor. The first one is to set allow/deny rules on the Collections. Ref: https://docs.meteor.com/api/collections.html#Mongo-Collection-allow

For example, if we want to ONLY allow admin users to create games, we can do

Games.allow({  
  insert: function (userId) {

    // assuming there is a role field in user document
    return Meteor.users.findOne(userId).role === 'admin';
  }
});

With the above code in effect, attempting to insert a document in Games Collection by non-admin users will throw an error. This method is elegant for numerous use cases, but with complicated validation rules, it’s not straight forward. For example, if we need to control only current player be able to make move on the game board, we might have something like:

Games.allow({  
 update: function (userId, doc, fields, modifier) {
    if (_.contains(fields, 'board')) {
      let game = new Game(doc);
      let currentPlayerIndex = game.currentPlayerIndex();
      let playerIndex = game.userIndex(userId);
      return playerIndex === currentPlayerIndex;
    } else { // update other fields
      ....
    }
 }
});

Not only it’s not straight forward, sometimes it’s not even possible to write such rules. That’s why we need to resort to another way: Meteor Methods.

Meteor Methods

Meteor Method is a useful construct to allow client side calling server declared functions. (Actually the functions can be declared in both clients and servers, but for the time being just forget this). If you are coming from traditional server-client architecture, it's similar to remote procedure calls (RPC).

Meteor methods are available in default Meteor projects. However, it doesn't come with a handy way to do input validations. Therefore, we will install two other useful packages. Execute the following commands on the project root directory.

$ meteor add aldeed:simple-schema
$ meteor add mdg:validated-method

Let's take a look at our first Meteor Method:

export const userJoinGame = new ValidatedMethod({  
  name: 'games.userJoinGame',
  validate: new SimpleSchema({
    gameId: {type: String}
  }).validator(),
  run({gameId}) {
    GamesController.userJoinGame(gameId, Meteor.user());
  }
});

There are actually a lot of magics behind, so I'd suggest you go along with it and just copy and paste the structure whenever you need to use it. Some notes though:

  1. name is the identifier of the method, which could be anything as long as it’s unique across the whole application.
  2. SimpleSchema is a simple validation package in Meteor. The validate part is checking whether the method call comes with a single parameter named gameId, which is of String type.
  3. the run function is the part where the actual work is done. It accepts a single parameter gameId (matched with validate). In this case, the actual work is calling GamesController.userJoinGame.

With this method in place, we will now execute userJoinGame.call({gameId: XXX}); instead of GamesController.userJoinGame(); in the client side.

The significance of this change is that the actual GamesController.userJoinGame routine is being executed on server side instead of client. More importantly, we have validated the client inputs (i.e. gameId) before executing the routine.

So where exactly do we put in these code? We will create a new file ./imports/api/methods/games.js with the following content:

import {GamesController} from "../controllers/gamesController.js";

export const newGame = new ValidatedMethod({  
  name: 'games.newGame',
  validate: new SimpleSchema({}).validator(),
  run({}) {
    GamesController.newGame(Meteor.user());
  }
});

export const userJoinGame = new ValidatedMethod({  
  name: 'games.userJoinGame',
  validate: new SimpleSchema({
    gameId: {type: String}
  }).validator(),
  run({gameId}) {
    GamesController.userJoinGame(gameId, Meteor.user());
  }
});

export const userLeaveGame = new ValidatedMethod({  
  name: 'games.userLeaveGame',
  validate: new SimpleSchema({
    gameId: {type: String}
  }).validator(),
  run({gameId}) {
    GamesController.userLeaveGame(gameId, Meteor.user());
  }
});

export const userMarkGame = new ValidatedMethod({  
  name: 'games.userMarkGame',
  validate: new SimpleSchema({
    gameId: {type: String},
    row: {type: Number},
    col: {type: Number}
  }).validator(),
  run({gameId, row, col}) {
    GamesController.userMarkGame(gameId, Meteor.user(), row, col);
  }
});

It contains three other methods beside userJoinGame, and all of them are pretty self-explanatory. Next, these methods need to be recognized on server. (Let me remind you again that all files under imports directory is not loaded by default). Therefore, we need to add an extra import statement on ./server/main.js which is not under imports and will be loaded automatically. i.e. on top of the file, add

import '../imports/api/methods/games.js';  
Updating UI

After that, we can update <GameList> and <GameBoard> to make use of the newly created methods.

In ./imports/ui/GameList.jsx, add an import statement:

import {newGame, userJoinGame, userLeaveGame} from '../api/methods/games.js';  

and update the content of the following three methods:

  handleNewGame() {
    newGame.call({});
  }

  handleLeaveGame(gameId) {
    userLeaveGame.call({gameId: gameId});
  }

  handleJoinGame(gameId) {
    userJoinGame.call({gameId: gameId});
  }

Similarly, in ./imports/ui/GameBoard.jsx, add an import statement:

import {userMarkGame} from '../api/methods/games.js';  

and update the following method:

  handleCellClick(row, col) {
    let game = this.props.game;
    if (game.currentPlayerIndex() !== game.userIndex(this.props.user)) return;
    userMarkGame.call({gameId: game._id, row: row, col: col});
  }

Done!

Get rid of "insecure" package

Wait… It didn’t really solve the security issue, you asked? because client can still choose NOT to call our Meteor methods, and open the browser console and execute Games.remove({_id: PICK_A_GAME_ID}) directly as we mentioned before. You are right!

The problem is that by default we are allowing clients to do any kind of updates, because default Meteor projects come with a package named insecure. The intention of including this package is to allow fast prototyping. To remove this behaviour, execute the following command in the project directory to get rid of the package:

$ meteor remove insecure

Now, open the browser console and try executing Games.remove({_id: PICK_A_GAME_ID}), and it will not longer work (like any other kind of updates). You will receive an error remove failed: Access denied.

Get rid of "autopublish" package

Beside insecure, default Meteor projects also come with another package called autopublish. This package will automatically synchronize ALL the database contents to the clients. This is actually the reason why Games.find().fetch() works on <App> even though the server has never done anything to send out the documents. Again, this is not a desired behaviour in a production ready application. Let’s remove this package by executing:

$ meteor remove autopublish

If you refresh the webpage now, you will find that all games are gone. Good! To get them back, we will need to use publish and subscribe — which is the Meteor way to control what data to send to clients.

Publish and Subscribe

As expected, it is the server’s responsibility to register publications with some kind of identifiers, allowing clients to subscribe to. Each publications should contain access controls and data filtering, only allowing permitted users to grab relevant data. To do this, let’s create a file ./imports/api/server/publications.js with the following content:

import {GameStatuses} from '../models/game.js';  
import Games from '../collections/games.js';

Meteor.publish('games', function() {  
  // access control: only for loggined-in users
  if (this.userId) { // this.userId is the id of the currently loggined in user
    // filtering: only games with WAITING and STARTED statuses
    return Games.find({status: {$in: [GameStatuses.WAITING, GameStatuses.STARTED]}});
  } else {
    return null;
  }
});

It registers a publication named games (similar to methods, publication names need to be unique across the whole application). This publication will check the current logged-in user (as in this.userId) this.userId is automatically set within all the publications calls by Meteor. In this case, we will return all the Games with status WAITING or STARTED to logged-in users.

Like before, we need to import this piece of code in ./server/main.js:

import '../imports/api/server/publications.js';  

That completes the publish part. Now switch to the client side by opening the file ./imports/ui/App.jsx, and add a line Meteor.subscribe('games'); just at the beginning of the createContainer method, i.e.:

export default createContainer(() => {  
  Meteor.subscribe('games'); // NEW

  return {
    user: Meteor.user(),
    games: Games.find().fetch()
  };  
}, App);

If you refresh the webpage again, all the games will show up.

Conclusion

Our application is now secure! To summarize, we have get rid of two development-only packages insecure and autopublish, and secured our application with Publish-and-Subscribe and Meteor Methods.

The foundation of our online Tic-Tac-Toe game is now more or less completed, although it looks a bit sketchy. In the next section, we will do a small wrap up on what we have accomplished so far and see how we are going forward.

If you are interested to use the source as a boilerplate for making your own games, I will also give some quick pointers on how to do that.

Next post:

http://blog.hiukim.com/building-online-multiplayers-games-with-meteor-codes-tutorials-7