├── .gitignore ├── COPYING.txt ├── Gruntfile.js ├── LICENSE.txt ├── README.md ├── doc ├── 2PC.svg ├── 2PC.xml ├── hsm.grammar ├── send_item.html ├── send_item.png ├── send_item_interaction.svg ├── send_item_interaction.xml ├── send_item_lhs.png ├── send_item_rhs.png └── shop.png ├── lib └── statechart.js ├── models ├── .shop ├── auction.hsm ├── auction.rules ├── exhaustive.hsm ├── injection.data ├── injection.rules ├── send_item.hsm ├── send_item.rules ├── send_item.svg ├── send_item.xml ├── shop.hsm └── shop.rules ├── package.json ├── properties.js ├── src ├── execute.js ├── execute.template ├── execute_externs.js ├── firebase_io.js ├── firesafe-cli.js ├── firesafe_main.js ├── hsm_to_client.js ├── hsm_to_rules.js └── hsm_to_rules_parser.js └── test ├── assets └── deadcode.js ├── auction_test.js ├── authentication_test.js ├── closure_test.js ├── exhaustive_hsm_test.js ├── firebase_test.js ├── hsm_to_rules_test.js ├── injection_test.js ├── send_item_test.js └── test_utils.js /.gitignore: -------------------------------------------------------------------------------- 1 | properties.js 2 | README.html 3 | .idea* 4 | node_modules/* 5 | *.sublime-* 6 | -------------------------------------------------------------------------------- /COPYING.txt: -------------------------------------------------------------------------------- 1 | Copyright (C) 2013-2014 Tom Larkworthy. All rights reserved. 2 | 3 | This program may be distributed and modified under the terms of the 4 | GNU General Public License as published by the Free Software Foundation, 5 | either version 3 of the License, or (at your option) any later version. 6 | 7 | Alternatively, this program may be distributed and modified under the 8 | terms of Quantum Leaps commercial licenses, which expressly supersede 9 | the GNU General Public License and are specifically designed for 10 | licensees interested in retaining the proprietary status of their code. 11 | 12 | This program is distributed in the hope that it will be useful, but 13 | WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 14 | or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 15 | for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | 20 | 21 | Contact information: 22 | -------------------- 23 | Firesafe hosting: https://github.com/tomlarkworthy/firesafe 24 | e-mail: tom.larkworthy@gmail.com 25 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | // Project configuration. 3 | grunt.initConfig({ 4 | pkg: grunt.file.readJSON('package.json'), 5 | 6 | //see https://github.com/gruntjs/grunt-contrib-nodeunit 7 | //only files ending in _test.js in directory and subdirectories of test are run 8 | nodeunit: { 9 | all: ['test/**/*_test.js'], 10 | send_item: ['test/send_item_test.js'], 11 | auction: ['test/auction_test.js'], 12 | firebase: ['test/firebase_test.js'], 13 | authentication: ['test/authentication_test.js'], 14 | hsm_to_rules: ['test/hsm_to_rules_test.js'], 15 | exhaustive: ['test/exhaustive_hsm_test.js'], 16 | injection: ['test/injection_test.js'], 17 | closure: ['test/closure_test.js'] 18 | },pandoc: { 19 | toHtml: { 20 | configs: { 21 | "publish" : 'HTML' 22 | }, 23 | files: { 24 | "from": [ 25 | "README.md" 26 | ] 27 | } 28 | } 29 | },peg: { 30 | hsm_to_rules: { 31 | src: "doc/hsm.grammar", 32 | dest: "src/hsm_to_rules_parser.js", 33 | options: { exportVar: "exports.parser" } 34 | } 35 | } 36 | 37 | }); 38 | 39 | // Load the plugins for this project 40 | grunt.loadNpmTasks('grunt-contrib-nodeunit'); 41 | grunt.loadNpmTasks('grunt-pandoc'); 42 | grunt.loadNpmTasks('grunt-peg'); 43 | 44 | // Default task(s). 45 | grunt.registerTask('default', ['nodeunit']); 46 | 47 | // generate documentation 48 | grunt.registerTask('doc', ['pandoc']); 49 | 50 | // generate parsers 51 | grunt.registerTask('parser', ['peg']); 52 | 53 | grunt.registerTask('test_parser', ['peg', "nodeunit:hsm_to_rules"]); 54 | grunt.registerTask('test_exhaustive', ['peg', "nodeunit:exhaustive"]); 55 | grunt.registerTask('test', ['peg', "nodeunit:all"]); 56 | }; 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Firesafe 2 | by Tom Larkworthy 3 | 4 | Firesafe is a technology to enforce data integrity, and enable complex transactions, on Firebase. 5 | 6 | Firesafe compiles hierarchical state machines (HSM) definitions into Firebase security rules. The Firesafe HSM language is expressive, and a super set of the Firebase security language. Adding consistent, concurrent and fail safe protocols to Firebase is now a whole lot simpler (e.g. cross-tree transactions). 7 | 8 | Firebase is already the future of databases, offering scalable low latency database-as-a-service. Firesafe compliments this amazing technology with an expressive syntax to get the most out of its security model. (Firesafe is not endorsed by Firebase). Development of secure protocols in Firesafe is supported by a static analyser (see https://github.com/tomlarkworthy/firesafe/wiki/Send-Item) 9 | 10 | ## Motivation 11 | 12 | In multiplayer apps/games, people cheat. It's a loss of direct sales, AND the free loaders also diminish the fun for everyone else. Multiplayer games are web-scale, which means the old solutions to data integrity and transactions don't work (e.g. SQL). 13 | 14 | Firebase solved one problem of the cost of providing low latency persistent data storage to mobile and desktop games. It's the first scalable NoSQL hosted solution that didn't suck. It also provided an unorthodox security and transactions abstraction. Turns out that abstraction has enough purchase to do some really cool things not possible in many NoSQL environments. Unfortunately, properly configuring the security layer is extremely verbose and error prone. 15 | 16 | Proper data integrity is very important, especially when users have *something to gain*. A classic game exploit is the "item clone". In this exploit, a player gives an item to a friend, and yanks their connection to the game. Now, the player and the friend have the item. Various variations exist, some not even requiring a friend. 17 | 18 | * Zynga: http://gamersunite.coolchaser.com/topics/31432-farmville-how-to-duplicate-items-no-scam-or-hack 19 | * Kixeye: http://bpoutpost.com/battle-pirates-glitches-cheats-ghosting-cloning-fleets.html 20 | * World of Warcraft: http://eu.battle.net/wow/en/forum/topic/3313135576 21 | 22 | The reason why many games are affected by the same type of bug is that a difficult technical issue lies behind the exploit. It is very had to enforce the semantics of a **cross user transaction**. 23 | 24 | Firebase *can* solve this with a **two phase commit protocol**. Classic literature on 2 phase commits state it *can* become deadlocked. However, normal deadlock issues don't arise in this case, because Firebase is a central authority, and client's states are persisted on disconnect. Unfortunately, actually writing a two phase commit protocol in Firebase security rules is actually really hard and error prone, and the 2-phase commit protocol was one motivation for creating the Firesafe abstraction of Hierarchical State Machines (HSM) in Firebase. 25 | 26 | However, while HSM will prevent silly mistakes, it still can't prevent high level protocol errors. We have connected Firesafe to a theorem prover which allows **proveing non-blocking properties and protocol invariants for arbitrary HSMs**. Our long term goal is to make **"provably secure, deadlock free, multi-user interactions"** simple. 27 | 28 | 29 | #The HSM Language 30 | 31 | In Firesafe we provide the Hierarchical State Machines (HSM) language. A similar language (UML statecharts) is often used in protocol design. A machine is a set of states and a set of transitions between them. Each machine gets compiled down to a single, massive ".write" rule. 32 | 33 | The language has a very natural graphical representation. To explain the language let's start with simple examples and build up to a two phase commit protocol for secure exchange of items between players. 34 | 35 | ## Simple Example: Shop 36 | 37 | One common source of complaints and support headaches is buying items in-game. If it goes wrong players could lose currency, or, if it's exploitable, cheaters profit and devalue the currency. This is an acute problem if the currency is bought with real world money. 38 | 39 | ![send_item_lhs_picture](/doc/shop.png) 40 | 41 | In a normal Firebase setting user accounts are represented as wild card ($user) children of "users". By placing our state machine as a child of "$user", Firesafe will generate an state (and signal) variable as a child of any "$user" nodes. 42 | 43 | We create secure Firesafe variables: gold, swords and water, at the same hierarchical level as the state machine. This indicates those variables values are protected by Firesafe. They can only ever be tampered with by a connected client *if* explicitly mentioned in a "guard" of "effect" clause of the machine. 44 | 45 | In the shopping scenario a user has just a single state, "playing". New players won't have Firebase data initially, so the black circle represents the initialization. New clients cannot initialize their new user accounts to anything though (a source of cheating). **Effect clauses denote post conditions of Firesafe variables, which must evaluate to true**. **Any Firesafe variable not mentioned in an effect is locked**. Guard and effect clauses are expressed as normal Firebase security expressions. In the shop case, the diagram forces clients to initialise their accounts with: 46 | 47 | ``` 48 | { 49 | state:"playing", 50 | gold:100, 51 | swords:0, 52 | water:0 53 | } 54 | ``` 55 | 56 | Once a player is playing, they have two options in the shop, buying a sword or water. Both these transition are labelled, which indicated the client must state which signal they are using. In this example we use a guard to ensure the player has enough gold to make a purchase and they do not exceed a maximum for the items (max(swords) 2, max(water) 20). 57 | 58 | **Guard clauses (diamond) prevent transitions unless the guard evaluates to true**. The guard in the shop ensure the player has the money and space in their inventory. So to buy a sword after initialization when the user has 100 gold and 0 swords, the new player can update 59 | ``` 60 | { 61 | signal:"BUY_SWORD", 62 | gold:90, 63 | swords:1 64 | } 65 | ``` 66 | on their "/user/" Firebase reference. The player *cannot* sneak in an update to "water" at the same time, as Firebase enforces no variable mentioned outside the effect clause can change. 67 | 68 | The final diagrammatic feature worth mentioning is the unattached red arrow. Unattached arrows on a digram denote a *transition role*. **Transitions only occur if their role clause evaluates to true, which commonly is used to express permissions**. In the shop we only allow the owner of the user record to invoke transitions. The transition role in particular is an excellent building block for complex multi-user interactions, because you can easily express that some transitions can only be performed by certain user roles. 69 | 70 | * complete hsm source file - https://github.com/tomlarkworthy/firesafe/blob/master/models/shop.hsm 71 | * generated validation rules - https://github.com/tomlarkworthy/firesafe/blob/master/models/shop.rules 72 | 73 | # Example: Item Trade 74 | 75 | Sending items from one player to another is problematic in many games and applications. Firebase natively does not offer much help, as Firebase transactions occur only on a subtree. To exchange items across users, you need to transfer data between different data trees (a **cross-tree transaction**). Firebase's atomic transactions cannot express this case, so the trade must be broken down into several smaller transactions that Firebase does support. 76 | 77 | One background issue is that a user might disconnect at any time (possible deliberately). One player disconnecting should not trap the other player in an unescapable state (a.k.a. **deadlock**). 78 | 79 | A cross tree transaction should either 1. occur, or 2. error and rollback. For a trade, an important invariant is the total number of objects in the world does not change. Drawing inspiration from existing literature, we adapt a **2-phase commit protocol** using in the banking sector for distributed balance trades, to implement trades in Firebase. 80 | 81 | Each user is either IDLE, sending an item (TX) or receiving an item (RX). Furthermore, the one partner in the trade must acknowledge the other's actions in a 2-phase commit, so some additional acknowledgement states are required (ACK_RX, ACK_TX). 82 | 83 | ![send_item_lhs_picture](http://tomlarkworthy.github.io/firesafe/doc/send_item_interaction.svg) 84 | 85 | The complete protocol is described step-by-step in the wiki (https://github.com/tomlarkworthy/firesafe/wiki/Send-Item). The final version in diagram form is as follows: 86 | 87 | ![complete protocol](http://tomlarkworthy.github.io/firesafe/models/send_item.svg) 88 | 89 | This protocol has been through a static analyser and shown to be correct. Sign up to our announcements if formal verification is important to you https://groups.google.com/forum/?hl=en-GB#!forum/firesafe-announce 90 | 91 | * complete hsm source file - https://github.com/tomlarkworthy/firesafe/blob/master/models/send_item.hsm 92 | * generated validation rules - https://github.com/tomlarkworthy/firesafe/blob/master/models/send_item.rules 93 | 94 | #Example: Hierarchical State Machines 95 | 96 | So far the machines discussed are flat. If you have ever tried to use a finite state machine in a real world setting you might have found yourself replicating arrows until your diagram is an unmanageable mess. Finite state machines suffer from *state space explosion*. Nesting states addresses this issue by reusing transitions via an inheritance-like mechanism. 97 | 98 | Miro Samek in the book *"Practical UML Statecharts for C/C++"* explains how to use state nesting, and explains common "patterns" for scalable state machine application. Slide 18 onwards of this online presentation explains in brief why HSMs are so much better than FSMs: http://www.slideshare.net/quantum-leaps/qp-qm-overviewnotes 99 | 100 | Note our HSMs do not map directly onto UML statecharts. In particular, UML statecharts are deterministic, whereas Firesafe's are not. In our HSM implementation, you do not need to specify signals at all, and can therefore leave the behaviour of the state chart ambiguous. While the behaviour of firesafe is ambiguous server side, the client still has to explicitly decide which states to move into. The client logic is often not modelable by state machines. Hence, non-deterministic hierarchical state machines are a better formalism for modeling server side data integrity than the deterministic formalism. That said, much of Miro's material is applicable to Firesafe's formalism, and the *"Practical UML Statecharts for C/C++"* book is a thoroughly recommended resource (Note: we are not in any way endorsed by either Miro Samek or Quantum Leaps, LLC, we just love their software!). 101 | 102 | #Firesafe Status 103 | 104 | ### current 105 | * .hsm -> Firebase rules command line compiler complete 106 | 107 | ### under development 108 | * Transitions with TIMESTAMP examples 109 | * Formal protocol verification 110 | 111 | ### on roadmap 112 | * .hsm -> client state machine generator 113 | 114 | Updates for major developments or calls for help! https://groups.google.com/forum/?hl=en-GB#!forum/firesafe-announce 115 | 116 | Requesting features, or just chat, at https://groups.google.com/forum/?hl=en-GB#!forum/firesafe-dev 117 | 118 | # Using Firesafe 119 | 120 | ### Install with NPM 121 | 122 | ``` 123 | npm install -g firesafe 124 | ``` 125 | 126 | ### Run 127 | ``` 128 | firesafe 129 | ``` 130 | 131 | upload the generated file to your Firebase security rules via the web API. We can automate this if it is a highly requested feature (we have a test driven framework). 132 | 133 | 134 | 135 | 136 | 137 | -------------------------------------------------------------------------------- /doc/2PC.svg: -------------------------------------------------------------------------------- 1 | Bank ABank BCentral AuthorityBank ACentralAuthorityBank Bprepare operationprepare operationagree/disagreeagree/disagreecommit/rollbackcommit/rollbackackack -------------------------------------------------------------------------------- /doc/2PC.xml: -------------------------------------------------------------------------------- 1 | 7ZlNk6IwEIZ/jcfZgoQPOarzsYfZqqnaw+4eo0RMTTRUiDP67zdIQKFxRxkEtmouVuiE2P3mSXeiIzxb754kiVc/REj5CFnhboTvRwiNLUt/poZ9xRBJFmYm2xi2LKRJyaSE4IrFZeNCbDZ0oUq2peDlyWISUWD4uSAcWn+xUK2Mc8g72r9TFq3yr7G9IOtJ1D6fI6RLsuXq7mBCaTd+GOGZFEJlrfVuRnmqRh5pFvrjmd7CJ0k36pIXUPbCG+Fb49KUbF61ZQJ8TVYkTpvbNZ8slJAjPH2jUjGtyDOZU/4iEqaY2Oghc6GUWJ8MmHAWpR1KxNBD43Q6lu5OTMbjJyrWVMm9HmJ6Xe+bm71jiMAGiPfjOjjGtDpZgrGxEUNDVMx8VEg3jEj1guFzgk0HLBge96iYAxSb6UgkScdMtmolJFP7AYuHbL8/8caN9uczW1LONvppGlPJ9LdSLeA9N+aXo23ahkBBRR8bQYFsq0YhbLUgUXCeL52K8Y7oKC/irFvV7MDuVTbbapTJuhUJe1a/ItlApDY2DAZLH8CoUE1QfhsxubeIya8uFMJudyF5IKRY0jQebRSaU3IoFVWsaagPdOaR8rl4fzgapgeD7siLSk19KQoP3YQTKQ/D51wsXg8mHcNvbbDzhz/6wfqG3LNiJ2IrF7QMniIyoqq8cKnblyzJHa4m5mJN6sQ287wIpv06pikffTRJ5rd5r7JohVeXrSMsdy2g6Vi9sgnr02DYtNpjc3wlm+DQgJyr2URONY8it6D1Azoh6JASOFkWchuoI1h+b1JZCoY/QL2Nw2oewklMJJJUg/4YssQ0u+R8x1SRgnU7o9y/kHIXUp4LdzHltjOurMfdv3LKwDGHk3nl2OxKbGK5TOindwq8cg+CKqsRVeM2qMKAKu9qqmpACPymVNUgCicbFlXOLfKvV61quOa6cquTRl6+T0JaiPWaaakepeB8TjS//9EZONeptFecr73S/V6B16uBgNXsAFsLln8tWAiA5bZQ2rHTGKwAJB8417C48m+Rg+F1r9MkDK+wne+Ozxx78wR7ujeCr63R+daAvxr0hVGjc26+tT+FkQ0wcq7GCK580PjuhFyAJJysT4wO9Tj/6zgbfvxHHT/8BQ== -------------------------------------------------------------------------------- /doc/hsm.grammar: -------------------------------------------------------------------------------- 1 | /* PEG.js grammar 2 | * Representation of hierarchical state machines for generating validation rules 3 | * 4 | * special syntax is ".states", ".transitions", ".roles", ".variables" 5 | * we do not process ".write" if a machine is defined 6 | * you are free to add read and validate where you like 7 | */ 8 | 9 | block 10 | = _ "{" _ bk1:block_item? _ bk2:("," _ block_item _?)* "}" _ 11 | { 12 | var val = {}; 13 | if (bk1.key != null){ 14 | val[bk1.key] = bk1.val; 15 | } 16 | if (bk2 != null){ 17 | for (var i =0;i 2 | 3 | 4 | 5 | 22 | 23 | 24 | This is a prototype to find a better visualisation for JSON data using D3. Its clearly needs some work, e.g. new line breaks in SVG 25 |
26 | 27 | 28 | 182 | 183 | 188 | 189 | -------------------------------------------------------------------------------- /doc/send_item.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomlarkworthy/firesafe/9dddcce9c8423837e9f7b6d7d9a76af9dc7a682a/doc/send_item.png -------------------------------------------------------------------------------- /doc/send_item_interaction.svg: -------------------------------------------------------------------------------- 1 | Player AClient APlayer BClient BServerFirebaseprepare to sendprepare to receiveacknowledge receiveacknowledge transmitcommit sendcommit receiveTXRXIDLEIDLEACK TXACK RXIDLEIDLE -------------------------------------------------------------------------------- /doc/send_item_interaction.xml: -------------------------------------------------------------------------------- 1 | 1Vpbk6o4EP41PjpFSPDyqM6cs1s7WzW1s1V79hEhamoisUKc0f31GySgJIFBBVQfLOiENvn660sae3C23v3k/mb1Jwsx7blOuOvB557rjhxHfieCvSZYchKmIqAEWxLiuCASjFFBNkVhwKIIB6IgWzBaVLbxl9gQvAc+NaX/kFCs1OLcwVH+GybLVfYzYDBOR2Kxz3SEeOFvqegfRG4yDF96cMYZE+nVejfDNEEj22m69R8lo/maOI5EnQfc9IFPn27Vkt6ov8dcyibGauOVv0kut2s6CQTjPTj9xFwQicmrP8f0jcVEEBbJKXMmBFufTJhQskwGBNuYa1TLTubi3YlIrfknZmss+F5OUaMDxQDFCKhuv452QEq0OjHBSMl8xYZlrveIkLxQINkBQwZgM0qSjXwD2CtZYEoieTfdYE7k70qQ4TNV4rejbNoAPrCID/BMgIBjQQg2AZFXzqnpHXMKuTcj1aCcVJWIdUyq8S1ZNTQwesf8M2HVveDjurfEZ2Tg84NwPPdjfMc+53o387mxgVcT+9ESUx4MTzbkWjaEvOs3BBxjRxuOk+0cyqCEAjgKDTLgUFY16hbTOft6OQqmB4EcyKxvIULOEKl8wvlh+pyy4KPKqYTPl1gUDJGsow7EQIfYqSCD0vLGyCGY7or0yTRADf2YbXmA1UOaAfIF1bOJ6ZVtsGxkuo2VZQ24DTD9RmMZxwEmn2bUaZNoEku+/yUFILv5V944T5n7WeBOjVzIv6eszCxXl5Z69eAOKlzaTktDxVCzV3O8zDnXKi+hJZy3xUvLScYPPiL2RQ8WvA0ta8W/THZpAITO2UzTAiDUDdAg0czDQBtE6zAAWor1ItEE96N4TcQdMs27LqbBqjKlJKYhTcV4+NRatu2mpkMdRjVo1nQBWx/IdafV3LXlHLo6mqH2ohnsJG2iDqMZNNNmTrC7ypiXxiwEjYBzftRC43qUMhQZ3NQVpa7TCDehYcm/fxm242wbhThUlbLPg3fyXzIkNwynC0LpjNGkISDnwoX8BIk8Fpx94MKIIz/NdE30s4ylaWJrAbhNkN8sTv56BMjMTmWHmJnlz+/Pry8PiJrbJWpmH+BBUCvrtnQBmlnOTWZ/SMFDxDXPK0Z+C3BeS8AhZABnJvFj7mUbHKXZWIEG5LamoR+vckDxjoikveKo67S7MvQqWi/1Oi/KxKcVZJbI6mb8PtCPs6ACQ3ue/l5H3Yzf12Ozrqi5jI/KztZXmxlUmNm5xMxZN61g5zNPo30w1KCt8pWSekxT4Q5GyfYusjQAmq6h9wTHaAgHI+QCx0OwNcOb78LSwPgY1QsoRkZoqfhai4xl/fj7cxnb4TozfH2XKevInuEy42+8rq6/5H2TMkUNOkhZI+j+EmDWkytExjN7KP38TXNZQ/cCM9d8+fC9mXVFzZnZcw0zP2ZVjTo89Hpmn+BufcNSHWY2r+8begfau7pqQJeGQKClPkNRg76BHtU39HO61+GR03uckhpa6oPM6BfXB2g4Pr9bqjc5PcdQUjt5aA5iUXWxi8jb4/9q0+nHvxvDl/8B -------------------------------------------------------------------------------- /doc/send_item_lhs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomlarkworthy/firesafe/9dddcce9c8423837e9f7b6d7d9a76af9dc7a682a/doc/send_item_lhs.png -------------------------------------------------------------------------------- /doc/send_item_rhs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomlarkworthy/firesafe/9dddcce9c8423837e9f7b6d7d9a76af9dc7a682a/doc/send_item_rhs.png -------------------------------------------------------------------------------- /doc/shop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomlarkworthy/firesafe/9dddcce9c8423837e9f7b6d7d9a76af9dc7a682a/doc/shop.png -------------------------------------------------------------------------------- /lib/statechart.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2010 David Durman 2 | // 3 | // The contents of this file are subject to the MIT License (the "License"); 4 | // you may not use this file except in compliance with the License. You may obtain a copy of the License at 5 | // http://opensource.org/licenses/MIT. 6 | // 7 | // This hierarchical state machine implementation has been inspired 8 | // by the QP active object framework, see http://www.state-machine.com/ 9 | 10 | 11 | (function(root, factory) { 12 | 13 | if (typeof exports === 'object') { 14 | 15 | // Node. Does not work with strict CommonJS, but 16 | // only CommonJS-like enviroments that support module.exports, 17 | // like Node. 18 | module.exports = factory(); 19 | 20 | } else if (typeof define === 'function' && define.amd) { 21 | 22 | // AMD. Register as an anonymous module. 23 | define(factory); 24 | 25 | } else { 26 | 27 | // Browser globals (root is window) 28 | root.Statechart = factory(); 29 | } 30 | 31 | }(this, function() { 32 | 33 | "use strict"; 34 | 35 | var assert = function(assertion){ 36 | if (!assertion) 37 | throw new Error("Assertion failed."); 38 | }; 39 | 40 | 41 | // Statechart. 42 | // ----------- 43 | 44 | // `myState` - the current state 45 | // `mySource` - the source of the current transition 46 | 47 | var Statechart = { 48 | 49 | run: function(opt){ 50 | 51 | opt = opt || {}; 52 | 53 | this.debug = opt.debug ? opt.debug : function() {}; 54 | 55 | this.construct(this.initialState); 56 | this.init(null); 57 | }, 58 | 59 | construct: function(initialState){ 60 | this.myState = this.top(); 61 | this.mySource = this.state("Initial"); 62 | 63 | // Initial pseudo-state 64 | this.states.Initial = { 65 | empty: function(){ 66 | this.newInitialState(initialState); 67 | } 68 | }; 69 | var handled = function(){ return null; }; 70 | // TOP state 71 | this.states.TOP = { 72 | entry: handled, 73 | exit: handled, 74 | init: handled, 75 | empty: handled 76 | }; 77 | this.flatten(); 78 | }, 79 | 80 | // Trigger the initial transition and recursively enter the submachine of the top state. 81 | // Must be called only once for a given Statechart before dispatching any events to it. 82 | init: function(anEventOrNull){ 83 | assert(this.myState === this.top() && this.mySource != null); 84 | var s = this.myState; // save top in temp 85 | this.mySource.trigger(anEventOrNull); // topmost initial transition 86 | assert(s.equals(this.myState.superstate())); // verify that we only went one level deep 87 | s = this.myState; 88 | s.enter(); 89 | while (s.init() === null){ // while init is handled (i.e. till we reach a leaf node) 90 | assert(s.equals(this.myState.superstate())); // verify that we only went one level deep 91 | s = this.myState; 92 | s.enter(); 93 | } 94 | }, 95 | 96 | state: function(stateOrName){ 97 | return (stateOrName && stateOrName instanceof QState) 98 | ? stateOrName 99 | : new QState(this, stateOrName); 100 | }, 101 | top: function(stateOrName){ 102 | // create the top state only once and store it to an auxiliary property 103 | return (this._topState || (this._topState = new QState(this, "TOP"))); 104 | }, 105 | currentState: function(){ 106 | return this.myState; 107 | }, 108 | flatten: function(){ 109 | this.statesTable = this.statesTable || {}; 110 | this._flatten(this.states, this.top().name); 111 | }, 112 | _flatten: function(states, parent){ 113 | if (!states) return; 114 | for (var state in states){ 115 | if (!states.hasOwnProperty(state)) continue; 116 | this.statesTable[state] = states[state]; 117 | this.statesTable[state].parent = parent; 118 | this._flatten(states[state].states, state); 119 | } 120 | }, 121 | selectState: function(stateName){ 122 | return this.statesTable[stateName]; 123 | }, 124 | dispatchEvent: function(anEvent, state, act){ 125 | act = act || state[anEvent.type]; 126 | 127 | // Action might also be an array in which case it is assumed that evaluating guards decides 128 | // which target to enter. 129 | if (act instanceof Array) { 130 | for (var i = 0; i < act.length; i++) { 131 | this.dispatchEvent(anEvent, state, act[i]); 132 | } 133 | } 134 | 135 | // @todo This is terrible edge case used just for more fancy Statechart representation 136 | // It allows using "MyState": { init: "MySubState", ... } intead of 137 | // "MyState": { init: function(){ this.newInitialState("MySubState"); }, ... } 138 | // In some cases the latter form can be useful for better control of the Statechart 139 | if (anEvent.type == "init" && typeof act == "string"){ 140 | this.newInitialState(act); 141 | return null; // handled 142 | } 143 | 144 | if (act instanceof Function){ 145 | act.call(this, anEvent.args); 146 | return null; // handled 147 | } else if (act) { 148 | // no guard at all or the guard condition is met 149 | if (!act.guard || (act.guard && act.guard.call(this, anEvent.args))){ 150 | if (act.action) act.action.call(this, anEvent.args); 151 | if (act.target) this.newState(act.target); 152 | return null; // handled 153 | } 154 | } else { // act is undefined (no handler in state for anEvent) 155 | if (state === this.selectState("TOP")){ 156 | this.handleUnhandledEvent(anEvent); // not-handled 157 | return null; // handled (TOP state handles all events) 158 | } 159 | } 160 | return this.state(state.parent); // not-handled 161 | }, 162 | 163 | // Override this when needed. 164 | handleUnhandledEvent: function(anEvent){ 165 | this.debug("Unhandled event: " + anEvent.type); 166 | return null; 167 | }, 168 | 169 | // Traverse the state hierarchy starting from the currently active state myState. 170 | // Advance up the state hierarchy (i.e., from substates to superstates), invoking all 171 | // the state handlers in succession. At each level of state nesting, it intercepts the value 172 | // returned from a state handler to obtain the superstate needed to advance to the next level. 173 | dispatch: function(anEvent, args){ 174 | if (!anEvent || !(anEvent instanceof QEvent)) 175 | anEvent = new QEvent(anEvent, args); 176 | this.mySource = this.myState; 177 | while (this.mySource) 178 | this.mySource = this.mySource.trigger(anEvent); 179 | }, 180 | 181 | // Performs dynamic transition. (macro Q_TRAN_DYN()) 182 | newState: function(aStateName){ 183 | this.transition(this.state(aStateName)); 184 | }, 185 | 186 | // Used by handlers only in response to the #init event. (macro Q_INIT()) 187 | // USAGE: return this.newInitialState("whatever"); 188 | // @return null for convenience 189 | 190 | newInitialState: function(aStateOrName){ 191 | this.myState = this.state(aStateOrName); 192 | return null; 193 | }, 194 | 195 | // Dynamic transition. (Q_TRAN_DYN()) 196 | transition: function(target){ 197 | 198 | assert(!target.equals(this.top())); 199 | 200 | var entry = []; 201 | var mySource = this.mySource; 202 | var s = this.myState; 203 | 204 | // exit all the nested states between myState and mySource 205 | while (!s.equals(mySource)){ 206 | assert(s != null); 207 | s = s.exit() || s.superstate(); 208 | } 209 | 210 | // check all seven possible source/target state combinations 211 | entry[entry.length] = target; 212 | 213 | // (a) mySource == target (self transition) 214 | if (mySource.equals(target)){ 215 | mySource.exit(); 216 | return this.enterVia(target, entry); 217 | } 218 | 219 | // (b) mySource == target.superstate (one level deep) 220 | var p = target.superstate(); 221 | if (mySource.equals(p)) 222 | return this.enterVia(target, entry); 223 | 224 | assert(mySource != null); 225 | 226 | // (c) mySource.superstate == target.superstate (most common - fsa) 227 | var q = mySource.superstate(); 228 | if (q.equals(p)){ 229 | mySource.exit(); 230 | return this.enterVia(target, entry); 231 | } 232 | 233 | // (d) mySource.superstate == target (one level up) 234 | if (q.equals(target)){ 235 | mySource.exit(); 236 | entry.pop(); // do not enter the LCA 237 | return this.enterVia(target, entry); 238 | } 239 | 240 | // (e) mySource == target.superstate.superstate... hierarchy (many levels deep) 241 | entry[entry.length] = p; 242 | s = p.superstate(); 243 | while (s !== null){ 244 | if (mySource.equals(s)) 245 | return this.enterVia(target, entry); 246 | entry[entry.length] = s; 247 | s = s.superstate(); 248 | } 249 | 250 | // otherwise we're definitely exiting mySource 251 | mySource.exit(); 252 | 253 | // entry array is complete, save its length to avoid computing it repeatedly 254 | var entryLength = entry.length; 255 | 256 | // (f) mySource.superstate == target.superstate.superstate... hierarchy 257 | var lca; 258 | for (lca = entryLength - 1; lca > 0; lca--){ 259 | if (q.equals(entry[lca])){ 260 | return this.enterVia(target, entry.slice(0, lca)); // do not enter lca 261 | } 262 | } 263 | 264 | // (g) each mySource.superstate.superstate... for each target.superstate.superstate... 265 | s = q; 266 | while (s !== null){ 267 | for (lca = entryLength - 1; lca > 0; lca--){ 268 | if (s.equals(entry[lca])){ 269 | return this.enterVia(target, entry.slice(0, lca - 1)); // do not enter lca 270 | } 271 | } 272 | s.exit(); 273 | s = s.superstate(); 274 | } 275 | }, 276 | 277 | // tail of transition() 278 | // We are in the LCA of mySource and target. 279 | enterVia: function(target, entry){ 280 | // retrace the entry path in reverse order 281 | var idx = entry.length; 282 | while (idx--) entry[idx].enter(); 283 | 284 | this.myState = target; 285 | while (target.init() == null){ 286 | // initial transition must go one level deep 287 | assert(target.equals(this.myState.superstate())); 288 | target = this.myState; 289 | target.enter(); 290 | } 291 | } 292 | }; 293 | 294 | // QState. 295 | // ------- 296 | 297 | function QState(fsm, name){ 298 | this.fsm = fsm; 299 | this.name = name; 300 | } 301 | 302 | QState.prototype = { 303 | equals: function(state){ 304 | return (this.name === state.name && this.fsm === state.fsm); 305 | }, 306 | dispatchEvent: function(anEvent, state){ 307 | return this.fsm.dispatchEvent(anEvent, state); 308 | }, 309 | trigger: function(anEvent){ 310 | var evt = anEvent || QEventEmpty; 311 | var state = this.fsm.selectState(this.name); 312 | return this.dispatchEvent(evt, state); 313 | }, 314 | enter: function(){ 315 | this.fsm.debug("[" + this.name + "] enter"); 316 | return this.trigger(QEventEntry); 317 | }, 318 | exit: function(){ 319 | this.fsm.debug("[" + this.name + "] exit"); 320 | return this.trigger(QEventExit); 321 | }, 322 | init: function(){ 323 | this.fsm.debug("[" + this.name + "] init"); 324 | return this.trigger(QEventInit); 325 | }, 326 | 327 | // Answer my superstate. Default is to return fsm top state. 328 | superstate: function(){ 329 | var superstate = this.trigger(QEventEmpty); 330 | if (superstate && superstate instanceof QState) 331 | return superstate; 332 | superstate = this.fsm.top(); 333 | if (this.name === superstate.name) 334 | return null; 335 | return superstate; 336 | } 337 | }; 338 | 339 | // QEvent 340 | // ------ 341 | 342 | function QEvent(type, args){ 343 | this.type = type; 344 | this.args = args; 345 | } 346 | 347 | // these events are static, they do not carry any arguments 348 | // -> create them only once 349 | // moreover, they don't have to be exposed to the outer world 350 | var QEventEntry = new QEvent("entry"); 351 | var QEventExit = new QEvent("exit"); 352 | var QEventInit = new QEvent("init"); 353 | var QEventEmpty = new QEvent("empty"); 354 | 355 | 356 | return Statechart; 357 | 358 | })); 359 | -------------------------------------------------------------------------------- /models/.shop: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 1 7 | 0 8 | 3 9 | 10 | 11 | 0,0,1549,408,* 12 | 13 | 14 | 4129280 15 | 0 16 | 17 | 18 | 19 | 20 | 0 21 | 22 | 23 | 24 | 25 | 0 26 | 27 | 28 | 0 29 | 30 | 31 | 32 | 33 | 0 34 | 35 | 36 | 0 37 | 38 | 39 | 40 | 41 | 0 42 | 43 | 44 | 0 45 | 46 | 47 | 48 | 49 | 0 50 | 51 | 52 | 0 53 | 54 | 55 | 56 | 57 | 0 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /models/auction.hsm: -------------------------------------------------------------------------------- 1 | //implementation of an auction house, anybody can sell, each bid increased the time till the action is over 2 | //highest bidder gets the item, or the item is left unsold 3 | { 4 | "rules": { 5 | ".read": true, 6 | "auctions": { 7 | "$item_id":{ 8 | ".variables":{ 9 | "name":{}, 10 | "seller":{}, 11 | "bid" :{}, 12 | "bidder":{}, 13 | "modified":{} 14 | },".states":{ 15 | "SELLING":{}, 16 | "SOLD":{}, 17 | "UNSOLD":{} 18 | },".roles":{ 19 | "bidder":"$user == auth.username", 20 | "seller":"$item.child('seller') == auth.username" 21 | },".transitions":{ 22 | "0":{ 23 | "from":"null", 24 | "to":"SELLING", 25 | "signal":"SELL", 26 | "effect":" 27 | newData.child('name').val() != null && 28 | newData.child('seller').val() == auth.username && 29 | newData.child('modified').val() == now" 30 | }, 31 | "1":{ 32 | "from":"null", 33 | "to":"SELLING", 34 | "signal":"SELL(item_name)", 35 | "execute":' 36 | return { 37 | "name":item_name, 38 | "modified":now, 39 | "seller":auth.username, 40 | };' 41 | } 42 | } 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /models/auction.rules: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "rules":{ 4 | ".read":true, 5 | "auctions":{ 6 | "$item_id":{ 7 | ".write":" 8 | ( //0: null -> SELLING, null 9 | /*role */(true) 10 | /*from */ && data.child('state').val() == null 11 | /*to */ && newData.child('state').val() == 'SELLING' 12 | /*signal*/ && newData.child('signal').val() == 'SELL' 13 | /*effect*/ && ( 14 | newData.child('name').val() != null && 15 | newData.child('seller').val() == auth.username && 16 | newData.child('modified').val() == now 17 | ) 18 | && newData.child('bid').val() == data.child('bid').val() //lock for bid 19 | && newData.child('bidder').val() == data.child('bidder').val() //lock for bidder 20 | )|| 21 | ( //1: null -> SELLING, null 22 | /*role */(true) 23 | /*from */ && data.child('state').val() == null 24 | /*to */ && newData.child('state').val() == 'SELLING' 25 | /*signal*/ && newData.child('signal').val() == 'SELL(item_name)' 26 | /*execut*/ && ( 27 | newData.child('name').val() == newData.child('item_name').val() && 28 | newData.child('modified').val() == now && 29 | newData.child('seller').val() == auth.username 30 | ) 31 | && newData.child('bid').val() == data.child('bid').val() //lock for bid 32 | && newData.child('bidder').val() == data.child('bidder').val() //lock for bidder 33 | ) 34 | " 35 | } 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /models/exhaustive.hsm: -------------------------------------------------------------------------------- 1 | //an enumeration of all possible state transitions in a hierarchy, inspired by Miro Samek, direct translation of 2 | //David Durman's statechart repository (which is slightly different to Miro's) 3 | { 4 | "rules": { 5 | ".variables":{ 6 | "foo":{} 7 | },".states":{ 8 | "S0":{ 9 | ".init":"S1", 10 | "S1":{ 11 | ".init":"S11", 12 | "S11":{} 13 | }, 14 | "S2":{ 15 | ".init":"S21", 16 | "S21":{ 17 | ".init":"S211", 18 | "S211":{} 19 | } 20 | } 21 | } 22 | },".roles":{ 23 | "all":"true" 24 | },".transitions":{ 25 | "INIT": { "to":"S0", "role":"all", 26 | "effect":"newData.child('foo').val() == false"}, 27 | "0": {"from":"S0", "to":"S211", "role":"all", "signal":"E"}, 28 | "1": {"from":"S1", "to":"S1", "role":"all", "signal":"A"}, 29 | "2": {"from":"S1", "to":"S11", "role":"all", "signal":"B"}, 30 | "3": {"from":"S1", "to":"S2", "role":"all", "signal":"C"}, 31 | "4": {"from":"S1", "to":"S0", "role":"all", "signal":"D"}, 32 | "5": {"from":"S1", "to":"S211", "role":"all", "signal":"F"}, 33 | "6": {"from":"S11","to":"S211", "role":"all", "signal":"G"}, 34 | "7": {"from":"S11","to":"S11", "role":"all", "signal":"H", 35 | "guard" :"data.child('foo').val() == true", 36 | "effect":"newData.child('foo').val() == false" 37 | }, 38 | "8": {"from":"S2", "to":"S1", "role":"all", "signal":"C"}, 39 | "9": {"from":"S2", "to":"S11", "role":"all", "signal":"F"}, 40 | "10":{"from":"S21", "to":"S211","role":"all", "signal":"B"}, 41 | "11":{"from":"S21", "to":"S21", "role":"all", "signal":"H", 42 | "guard" :"data.child('foo').val() == false", 43 | "effect":"newData.child('foo').val() == true" 44 | }, 45 | "12":{"from":"S211", "to":"S21","role":"all", "signal":"D"}, 46 | "13":{"from":"S211", "to":"S0", "role":"all", "signal":"G"} 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /models/injection.data: -------------------------------------------------------------------------------- 1 | { 2 | "users":{ 3 | "fred":{ 4 | "data":"fred_secret" 5 | }, 6 | "fred/data":{ 7 | "data":"fred/data_secret" 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /models/injection.rules: -------------------------------------------------------------------------------- 1 | { 2 | "rules":{ 3 | ".read":true, 4 | ".write":false, 5 | "users":{ 6 | "$username":{ 7 | ".write":"root.child($username).exists()" 8 | } 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /models/send_item.hsm: -------------------------------------------------------------------------------- 1 | //sending an item securely using a 2 phase commit, writing in candidate diagram language about nested heirachical state machines 2 | { 3 | "rules": { 4 | ".read": true, //grant read access to all 5 | "users": { 6 | "$user":{ 7 | ".variables":{ 8 | "rx_ptr":{".type":"$user"}, 9 | "tx_ptr":{".type":"$user"}, 10 | "item": {".type":"$item"}, 11 | "tx_itm":{".type":"$item"}, 12 | "rx_itm":{".type":"$item"} 13 | },".states":{ 14 | "IDLE":{}, 15 | "TX":{}, 16 | "RX":{}, 17 | "ACK_TX":{}, 18 | "ACK_RX":{} 19 | },".roles":{ 20 | "self":"$user == auth.username", 21 | "other":"auth.username == data.child('rx_ptr').val() || auth.username == data.child('tx_ptr').val()", 22 | "either":"$user == auth.username || auth.username == data.child('rx_ptr').val() || auth.username == data.child('tx_ptr').val()" 23 | },".transitions":{ 24 | "INITIALIZE":{"from":"null", "to":"IDLE","role":"self", 25 | "effect":" 26 | newData.child('item').val() == null && 27 | newData.child('tx_itm').val() == null && 28 | newData.child('tx_ptr').val() == null && 29 | newData.child('rx_itm').val() == null && 30 | newData.child('rx_ptr').val() == null" 31 | },"SEND":{"from":"IDLE", "to":"TX","role":"self", 32 | "guard":" 33 | data.child('item').val() != null && 34 | root.child('users').child(newData.child('tx_ptr').val()).child('state').val() != null && 35 | newData.child('tx_ptr').val() != $user", //bug discovered during FM, can't send to self 36 | "effect":" 37 | newData.child('tx_itm').val() == data.child('item').val() && 38 | newData.child('tx_ptr').val() != null && 39 | newData.child('item').val() == null" 40 | },"RECEIVE":{"from":"IDLE", "to":"RX","role":"self", 41 | "guard":" 42 | data.child('item').val() == null && 43 | $user == root.child('users').child(newData.child('rx_ptr').val()).child('tx_ptr').val()", 44 | "effect":" 45 | newData.child('rx_itm').val() == root.child('users').child(newData.child('rx_ptr').val()).child('tx_itm').val() && 46 | newData.child('rx_ptr').val() != null && 47 | newData.child('tx_itm').val() == null" //locking bug fix 48 | },"ACK_RX":{"from":"RX", "to":"ACK_RX","role":"other", 49 | "guard":" 50 | root.child('users').child(data.child('rx_ptr').val()).child('state').val() == 'TX' && //bug discovered during FM, check other hasn't cancelled 51 | root.child('users').child(data.child('rx_ptr').val()).child('tx_ptr').val() == $user" //and they are sending to us, otherwise sender can swap recipient 52 | },"ACK_TX":{"from":"TX", "to":"ACK_TX","role":"other", 53 | "guard":" 54 | root.child('users').child(newData.child('tx_ptr').val()).child('state').val() == 'ACK_RX'" 55 | },"COMMIT_TX":{"from":"ACK_TX", "to":"IDLE","role":"either", 56 | "guard":" 57 | root.child('users').child(data.child('tx_ptr').val()).child('state').val() == 'ACK_RX'", 58 | "effect":" 59 | newData.child('item').val() == null && 60 | newData.child('tx_ptr').val() == null && 61 | newData.child('tx_itm').val() == null" 62 | },"COMMIT_RX":{"from":"ACK_RX", "to":"IDLE","role":"either", 63 | "guard":" 64 | root.child('users').child(data.child('rx_ptr').val()).child('state').val() != 'TX' && 65 | root.child('users').child(data.child('rx_ptr').val()).child('state').val() != 'ACK_TX'", 66 | "effect":" 67 | newData.child('item').val() == data.child('rx_itm').val()&& 68 | newData.child('rx_ptr').val() == null && 69 | newData.child('rx_itm').val() == null" 70 | },"CANCEL_TX":{"from":"TX", "to":"IDLE","role":"self", 71 | "guard":" 72 | (root.child('users').child(data.child('tx_ptr').val()).child('state').val() != 'RX' && 73 | root.child('users').child(data.child('tx_ptr').val()).child('state').val() != 'ACK_RX') ||( 74 | root.child('users').child(data.child('tx_ptr').val()).child('rx_ptr').val() != $user)", 75 | "effect":" 76 | newData.child('tx_itm').val() == null && 77 | newData.child('tx_ptr').val() == null && 78 | newData.child('item').val() == data.child('tx_itm').val()" 79 | },"CANCEL_RX":{"from":"RX", "to":"IDLE","role":"either", 80 | "guard":" 81 | root.child('users').child(data.child('rx_ptr').val()).child('state').val() == 'TX'", 82 | "effect":" 83 | newData.child('rx_itm').val() == null && 84 | newData.child('rx_ptr').val() == null && 85 | newData.child('item').val() == null" 86 | },"CANCEL_ACK_RX":{"from":"ACK_RX", "to":"RX","role":"either", 87 | "guard":" 88 | root.child('users').child(data.child('rx_ptr').val()).child('state').val() == 'TX' && 89 | root.child('users').child(data.child('rx_ptr').val()).child('tx_ptr').val() == $user" 90 | },"CANCEL_ACK_TX":{"from":"ACK_TX", "to":"TX","role":"either", //rare event that ACK_RX->RX cancelled simultaneously as ACK_TX 91 | "guard":" 92 | root.child('users').child(data.child('tx_ptr').val()).child('state').val() == 'RX' && 93 | root.child('users').child(data.child('tx_ptr').val()).child('rx_ptr').val() == $user" 94 | } 95 | } 96 | } 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /models/send_item.rules: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "rules":{ 4 | ".read":true, 5 | "users":{ 6 | "$user":{ 7 | ".write":" 8 | ( //0: null -> IDLE, self 9 | /*role */($user == auth.username) 10 | /*from */ && data.child('state').val() == null 11 | /*to */ && newData.child('state').val() == 'IDLE' 12 | /*signal*/ && newData.child('signal').val() == null 13 | /*effect*/ && ( 14 | newData.child('item').val() == null && 15 | newData.child('tx_itm').val() == null && 16 | newData.child('tx_ptr').val() == null && 17 | newData.child('rx_itm').val() == null && 18 | newData.child('rx_ptr').val() == null 19 | ) 20 | )|| 21 | ( //1: IDLE -> TX, self 22 | /*role */($user == auth.username) 23 | /*from */ && data.child('state').val() == 'IDLE' 24 | /*to */ && newData.child('state').val() == 'TX' 25 | /*signal*/ && newData.child('signal').val() == null 26 | /*guards*/ && ( 27 | data.child('item').val() != null && 28 | root.child('users').child(newData.child('tx_ptr').val()).child('state').val() != null && 29 | newData.child('tx_ptr').val() != $user 30 | ) 31 | /*effect*/ && ( 32 | newData.child('tx_itm').val() == data.child('item').val() && 33 | newData.child('tx_ptr').val() != null && 34 | newData.child('item').val() == null 35 | ) 36 | && newData.child('rx_ptr').val() == data.child('rx_ptr').val() //lock for rx_ptr 37 | && newData.child('rx_itm').val() == data.child('rx_itm').val() //lock for rx_itm 38 | )|| 39 | ( //2: IDLE -> RX, self 40 | /*role */($user == auth.username) 41 | /*from */ && data.child('state').val() == 'IDLE' 42 | /*to */ && newData.child('state').val() == 'RX' 43 | /*signal*/ && newData.child('signal').val() == null 44 | /*guards*/ && ( 45 | data.child('item').val() == null && 46 | $user == root.child('users').child(newData.child('rx_ptr').val()).child('tx_ptr').val() 47 | ) 48 | /*effect*/ && ( 49 | newData.child('rx_itm').val() == root.child('users').child(newData.child('rx_ptr').val()).child('tx_itm').val() && 50 | newData.child('rx_ptr').val() != null && 51 | newData.child('tx_itm').val() == null 52 | ) 53 | && newData.child('tx_ptr').val() == data.child('tx_ptr').val() //lock for tx_ptr 54 | && newData.child('item').val() == data.child('item').val() //lock for item 55 | )|| 56 | ( //3: TX -> ACK_TX, other 57 | /*role */(auth.username == data.child('rx_ptr').val() || auth.username == data.child('tx_ptr').val()) 58 | /*from */ && data.child('state').val() == 'TX' 59 | /*to */ && newData.child('state').val() == 'ACK_TX' 60 | /*signal*/ && newData.child('signal').val() == null 61 | /*guards*/ && ( 62 | root.child('users').child(newData.child('tx_ptr').val()).child('state').val() == 'ACK_RX' 63 | ) 64 | && newData.child('rx_ptr').val() == data.child('rx_ptr').val() //lock for rx_ptr 65 | && newData.child('tx_ptr').val() == data.child('tx_ptr').val() //lock for tx_ptr 66 | && newData.child('item').val() == data.child('item').val() //lock for item 67 | && newData.child('tx_itm').val() == data.child('tx_itm').val() //lock for tx_itm 68 | && newData.child('rx_itm').val() == data.child('rx_itm').val() //lock for rx_itm 69 | )|| 70 | ( //4: TX -> IDLE, self 71 | /*role */($user == auth.username) 72 | /*from */ && data.child('state').val() == 'TX' 73 | /*to */ && newData.child('state').val() == 'IDLE' 74 | /*signal*/ && newData.child('signal').val() == null 75 | /*guards*/ && ( 76 | (root.child('users').child(data.child('tx_ptr').val()).child('state').val() != 'RX' && 77 | root.child('users').child(data.child('tx_ptr').val()).child('state').val() != 'ACK_RX') ||( 78 | root.child('users').child(data.child('tx_ptr').val()).child('rx_ptr').val() != $user) 79 | ) 80 | /*effect*/ && ( 81 | newData.child('tx_itm').val() == null && 82 | newData.child('tx_ptr').val() == null && 83 | newData.child('item').val() == data.child('tx_itm').val() 84 | ) 85 | && newData.child('rx_ptr').val() == data.child('rx_ptr').val() //lock for rx_ptr 86 | && newData.child('rx_itm').val() == data.child('rx_itm').val() //lock for rx_itm 87 | )|| 88 | ( //5: RX -> ACK_RX, other 89 | /*role */(auth.username == data.child('rx_ptr').val() || auth.username == data.child('tx_ptr').val()) 90 | /*from */ && data.child('state').val() == 'RX' 91 | /*to */ && newData.child('state').val() == 'ACK_RX' 92 | /*signal*/ && newData.child('signal').val() == null 93 | /*guards*/ && ( 94 | root.child('users').child(data.child('rx_ptr').val()).child('state').val() == 'TX' && 95 | root.child('users').child(data.child('rx_ptr').val()).child('tx_ptr').val() == $user 96 | ) 97 | && newData.child('rx_ptr').val() == data.child('rx_ptr').val() //lock for rx_ptr 98 | && newData.child('tx_ptr').val() == data.child('tx_ptr').val() //lock for tx_ptr 99 | && newData.child('item').val() == data.child('item').val() //lock for item 100 | && newData.child('tx_itm').val() == data.child('tx_itm').val() //lock for tx_itm 101 | && newData.child('rx_itm').val() == data.child('rx_itm').val() //lock for rx_itm 102 | )|| 103 | ( //6: RX -> IDLE, either 104 | /*role */($user == auth.username || auth.username == data.child('rx_ptr').val() || auth.username == data.child('tx_ptr').val()) 105 | /*from */ && data.child('state').val() == 'RX' 106 | /*to */ && newData.child('state').val() == 'IDLE' 107 | /*signal*/ && newData.child('signal').val() == null 108 | /*guards*/ && ( 109 | root.child('users').child(data.child('rx_ptr').val()).child('state').val() == 'TX' 110 | ) 111 | /*effect*/ && ( 112 | newData.child('rx_itm').val() == null && 113 | newData.child('rx_ptr').val() == null && 114 | newData.child('item').val() == null 115 | ) 116 | && newData.child('tx_ptr').val() == data.child('tx_ptr').val() //lock for tx_ptr 117 | && newData.child('tx_itm').val() == data.child('tx_itm').val() //lock for tx_itm 118 | )|| 119 | ( //7: ACK_TX -> IDLE, either 120 | /*role */($user == auth.username || auth.username == data.child('rx_ptr').val() || auth.username == data.child('tx_ptr').val()) 121 | /*from */ && data.child('state').val() == 'ACK_TX' 122 | /*to */ && newData.child('state').val() == 'IDLE' 123 | /*signal*/ && newData.child('signal').val() == null 124 | /*guards*/ && ( 125 | root.child('users').child(data.child('tx_ptr').val()).child('state').val() == 'ACK_RX' 126 | ) 127 | /*effect*/ && ( 128 | newData.child('item').val() == null && 129 | newData.child('tx_ptr').val() == null && 130 | newData.child('tx_itm').val() == null 131 | ) 132 | && newData.child('rx_ptr').val() == data.child('rx_ptr').val() //lock for rx_ptr 133 | && newData.child('rx_itm').val() == data.child('rx_itm').val() //lock for rx_itm 134 | )|| 135 | ( //8: ACK_TX -> TX, either 136 | /*role */($user == auth.username || auth.username == data.child('rx_ptr').val() || auth.username == data.child('tx_ptr').val()) 137 | /*from */ && data.child('state').val() == 'ACK_TX' 138 | /*to */ && newData.child('state').val() == 'TX' 139 | /*signal*/ && newData.child('signal').val() == null 140 | /*guards*/ && ( 141 | root.child('users').child(data.child('tx_ptr').val()).child('state').val() == 'RX' && 142 | root.child('users').child(data.child('tx_ptr').val()).child('rx_ptr').val() == $user 143 | ) 144 | && newData.child('rx_ptr').val() == data.child('rx_ptr').val() //lock for rx_ptr 145 | && newData.child('tx_ptr').val() == data.child('tx_ptr').val() //lock for tx_ptr 146 | && newData.child('item').val() == data.child('item').val() //lock for item 147 | && newData.child('tx_itm').val() == data.child('tx_itm').val() //lock for tx_itm 148 | && newData.child('rx_itm').val() == data.child('rx_itm').val() //lock for rx_itm 149 | )|| 150 | ( //9: ACK_RX -> IDLE, either 151 | /*role */($user == auth.username || auth.username == data.child('rx_ptr').val() || auth.username == data.child('tx_ptr').val()) 152 | /*from */ && data.child('state').val() == 'ACK_RX' 153 | /*to */ && newData.child('state').val() == 'IDLE' 154 | /*signal*/ && newData.child('signal').val() == null 155 | /*guards*/ && ( 156 | root.child('users').child(data.child('rx_ptr').val()).child('state').val() != 'TX' && 157 | root.child('users').child(data.child('rx_ptr').val()).child('state').val() != 'ACK_TX' 158 | ) 159 | /*effect*/ && ( 160 | newData.child('item').val() == data.child('rx_itm').val()&& 161 | newData.child('rx_ptr').val() == null && 162 | newData.child('rx_itm').val() == null 163 | ) 164 | && newData.child('tx_ptr').val() == data.child('tx_ptr').val() //lock for tx_ptr 165 | && newData.child('tx_itm').val() == data.child('tx_itm').val() //lock for tx_itm 166 | )|| 167 | ( //10: ACK_RX -> RX, either 168 | /*role */($user == auth.username || auth.username == data.child('rx_ptr').val() || auth.username == data.child('tx_ptr').val()) 169 | /*from */ && data.child('state').val() == 'ACK_RX' 170 | /*to */ && newData.child('state').val() == 'RX' 171 | /*signal*/ && newData.child('signal').val() == null 172 | /*guards*/ && ( 173 | root.child('users').child(data.child('rx_ptr').val()).child('state').val() == 'TX' && 174 | root.child('users').child(data.child('rx_ptr').val()).child('tx_ptr').val() == $user 175 | ) 176 | && newData.child('rx_ptr').val() == data.child('rx_ptr').val() //lock for rx_ptr 177 | && newData.child('tx_ptr').val() == data.child('tx_ptr').val() //lock for tx_ptr 178 | && newData.child('item').val() == data.child('item').val() //lock for item 179 | && newData.child('tx_itm').val() == data.child('tx_itm').val() //lock for tx_itm 180 | && newData.child('rx_itm').val() == data.child('rx_itm').val() //lock for rx_itm 181 | ) 182 | " 183 | } 184 | } 185 | } 186 | } -------------------------------------------------------------------------------- /models/send_item.svg: -------------------------------------------------------------------------------- 1 |

/


[Object]

users


[Object]

$user


item, rx, rx_loc, tx, tx_loc 

[Object]
IDLE

[Object]
TX

[Object]
ACK_TX

[Object]
RX

[Object]
ACK_RX

[Object]
newData.child('tx_itm').val() == data.child('item').val() &&newData.child('tx_ptr').val() != null &&newData.child('item').val() == nullnewData.child('tx_itm').val() == null &&newData.child('tx_ptr').val() == null &&newData.child('item').val() == data.child('tx_itm').val()newData.child('item').val() == null &&newData.child('tx_ptr').val() == null &&newData.child('tx_itm').val() == nullnewData.child('rx_itm').val() == null &&newData.child('rx_ptr').val() == null &&newData.child('item').val() == nullnewData.child('rx_itm').val() == root.child('users').child(newData.child('rx_ptr').val()).child('tx_itm').val() &&newData.child('rx_ptr').val() != null &&newData.child('tx_itm').val() == nulldata.child('item').val() == null &&$user == root.child('users').child(newData.child('rx_ptr').val()).child('tx_ptr').val()newData.child('item').val() == data.child('rx_itm').val()&&newData.child('rx_ptr').val() == null &&newData.child('rx_itm').val() == nullroot.child('users').child(data.child('rx_ptr').val()).child('state').val() != 'TX' &&root.child('users').child(data.child('rx_ptr').val()).child('state').val() != 'ACK_TX'root.child('users').child(newData.child('tx_ptr').val()).child('state').val() == 'ACK_RX'data.child('item').val() != null &&root.child('users').child(newData.child('tx_ptr').val()).child('state').val() != null &&newData.child('tx_ptr').val() != $usernewData.child('item').val() == null && newData.child('tx_itm').val() == null && newData.child('tx_ptr').val() == null && newData.child('rx_itm').val() == null && newData.child('rx_ptr').val() == nullroot.child('users').child(data.child('rx_ptr').val()).child('state').val() == 'TX' && root.child('users').child(data.child('rx_ptr').val()).child('tx_ptr').val() == $userroot.child('users').child(data.child('tx_ptr').val()).child('state').val() == 'ACK_RX'(root.child('users').child(data.child('tx_ptr').val()).child('state').val() != 'RX' &&root.child('users').child(data.child('tx_ptr').val()).child('state').val() != 'ACK_RX') ||(root.child('users').child(data.child('tx_ptr').val()).child('rx_ptr').val() != $user)root.child('users').child(data.child('rx_ptr').val()).child('state').val() == 'TX'root.child('users').child(data.child('rx_ptr').val()).child('state').val() == 'TX' &&root.child('users').child(data.child('rx_ptr').val()).child('tx_ptr').val() == $userroot.child('users').child(data.child('tx_ptr').val()).child('state').val() == 'RX' &&root.child('users').child(data.child('tx_ptr').val()).child('rx_ptr').val() == $userother/auth.username == data.child('rx_ptr').val() || auth.username == data.child('tx_ptr').val()self/$user == auth.usernameeither/$user == auth.username || auth.username == data.child('rx_ptr').val() || eitherauth.username == data.child('tx_ptr').val()Authentication Legend
-------------------------------------------------------------------------------- /models/send_item.xml: -------------------------------------------------------------------------------- 1 | 7V1bj6M4Fv4t+xBV90NKYBsDj3XpmlntrLSabml6nkZUQhK0JGQIqa6aX78mYBIfm2vsFJmtdEsVjDEGf+c7Fx87E/ywfv0pDbarfyfzMJ4ga/46wY8ThGyKKPuTl7wVJZ5lFQXLNJqXlcqCfTQPd0JRliRxFm3Fwlmy2YSzTChbJLHY2DZYhlLB11kQy6W/RfNsVXaO9zYv/zmMlit+G5v6xZld9sbbmIeLYB9n00MRyk/jLxP8kCZJVnxbvz6Ecf46+JMWj/5Uc7bqUxpusk4XoLJTL0G8Lzs1QTRmF99vhb7SP/d5p+7XQbqMNhN8x85aWzZI9+xLUTjNkm1xgvATWfiaTYM4WpZXzFi/wvTk3DycJWmQRUlZYb+Zh2kcbcJDHX5T9m1Z/j10bZelyWZ5LH3iJ9hXeK46sYVlq3SSv56nYzkYnpcwzSI24nfFAzwenu++fJzHOFzkVyWs1iJOfrCSRcReLr5fJJvsa/RX3oiNyuOnYB3FOYB/DuOXMG8170C2jvNK5RCwu4WvtQNpV/BgkhIm6zBL31iV8oKpV460VUqJXR7+OGLTxrzO6hSYhJSlQSkly6r1I3LYlxI8NUDC1vUDab8L090HlCqCLaHkOjKUGAsroIQdRweU7OuH0gSRHE2DwSSW9H/sAlH5Ga88A/sdZeF6gthoWOkr//tHnMyK79kr/1uU0WCdA3bzvNvW9v6KIe9yjHP2pAr6RNhRYB45WugT1WJ+Hr00DP8R4HUDrQboPx9/+dKKzmeJ1urormro0NsaRKRJLkrzw6AxJKSzcpyZAlIhoRpgfK/GUl7vIYmT9NA8XrDPzCqf9L+hcMZiHwlcGoCDbZErPUdBllTBlVQLavCFUfPt+wdmtGPGtu2LgoZcGDR3D//64wM4JoCD0UWB41wYOL+2gwaCQG93PqBXBz1qXRR59B0o6wN9o0UfwQrz3Bz6XAl9m/DHY5AFt7NVFM8/3TAvKcrWN59vWZVPn/NRY7Xyitb8pFbucZ3W4U7V6Rf2Gl9z50a+wTZLTy7+R9H8Zn/oap+mYC94T4umAJbC+TL8Wh6G8XPy48ux4P5QwE6skjT6iw15ECsw9JxkWbLOa2/md2l6uCDZhpuipESJ1wgnhpL07TsrtG4dfvh7fpgfvEbZySl29HsJZuhHcsezdPcPT5o/noDJXbJPZ7zIKa3rjMltyIHKTe4+ziUV0YtxAyrLZv6TRKz1Ewmgtqj6LQ+EWopeltcd0d2lKRs0VbwFqSk2fMHbSbVtXmHX0GmELXWna/tGHA+4UzYQ1aITNZfnKFAFQXmPk8ViF2YTKPzVmHXkA284HwwQWEn2z2msTvrnDb0fNSfYAiPcuk4tKdRopmYOoK6KA9C7cIAjNuH5oInODOBAsSRmCAAR8Ua+azV3jLhixzDpI/62L0o/MiP98oxZLw37frLfxkojknP3ybKengTdb1jOyXjknPhAPm3YSGdJJ0ACbQQn+3TpeodCUsEtwo4RkHarnDvvqOxtJIr7FKFbwGN6JJ6/MsH7HJGw+D7GorAApYjqhUW2lDsIi8uTKwRhcXoLi41c4NdBo7aDtABhIdCY7SwroCEK53UMKUXaIifc4Xir0NhHJ1IgI2YERJ6wBaon1WkQpyYN4uL/aBXjxWWdZy2NQTFSyBZD1SJoiLqXsX890iLqoFfnGL+2a0gZouGynue18VqHXBtWqThslPDP9QbsOawxPITW05BOV8n6eb8bRygVATXncjUnZDooQqlQ2AaFUok8Yd0UIe3D61XGjVa8CZA5Ww+AGGRdxL23Vnh6wuzTECodRP1c1M8086xb4oj/ShCkYRxk0YvYgz5+kgegDMMYrfpAMzUSCdyjsh06oUSjT41V+OGg6kFZlCcEctMBBr66YMUS23BhaLi7Sw1Q55jyqGH0vM16mBJfDJ5T3MefJj4IniMz9oOcRDAgRA1tjHdyHtrcmhEJf2tETbfjQKhKe7yP40BAG2hw6NwDJj2BPGIqnuZ6LdLvYRBO88+KpsGZc03SLydy1Btp83YLbZcFWSjb9Dffvt+0ibKR+xapbjeS5I/KBaDSUKvWiihcgCa9290FkLMpxk6UDWkHpzbSoVp/K0nFk1xO+gRTLeDZTSUN3slOAtYNbKN7jAXGU7GhLAPIlI7VkykJ7hVoARpgSi1DkRY5zWBUguK7j5brdhUUDQaFMhJJeguKA1V406KlOtscrrZDg9NxpKaqSJ7xdBzUlo6DLWBToF4T8keXQrNkyFPwnUM9WTeFXhnyN0Uq6MhVOozq2Ui1pMBYWI/HED+oqmQlVeYgR+0HVQ2gKn4nU1SFYPTDzEyp0yv+3WOGQjf/nTU5opxnqeLz18SjPr0ojZJx06jWjOwuNIoUNMpFqI9rRACdkAGekUxJkIy78yhoyr9UArZP2lgU5GT5qFf+NYEJ2NDn08Sisv13XeaG5okWqpITPCCEgBxxlmRqQ2R2EhSxEQdmV/SQE7ElYyEEKCe0LYRQrQHgz+ifJydGxIR2scrjONrucpnYrYJtXsgsgTT7mpsDskq2rFIBXFIlg7HBCs9GFaxsmiborJH5Uhkzyd6TKt/svP9aV5+Y69QZ826GOnVeWqK5ToE3NeZ5RYVNKM4rOhIp1IfPu+g6S6XrUG9a0TKt6Iks7sNUgO5r8uyW3ujSdBRkXFNb0HRtigtGAKbG8gy5TWNqprCSqIa5QkvXdGE9+V2HVwxA43AUnKhg1X5gWjaWo6o9bv6PjX1uAwsEyKXlHYx9AI3BOdmSDf5h7PfiSzKQL/+eEyJUClWrNiwxFsnjYcPxJzl0TGRoorcupEVUpEX6kxaM5KEBnCUtVZR8xcF2m7wDprH1lTxDy9z6SkukLWpoeSWVE4I+aaUunpz1q66ksJ73LQnzwKLuA/v/Sdcd69a0lFalvGRhTBxtg0Rqhk/ZrDRI0d64KfroWJcUbQsUjXRwtDRYXeaxC3k16lnDeW/bGuxZV7kJlSFgyLWGi/2qGxna7QKBBX9TqAk1sXOfXJvz/fAr4itXtcW6MbriLvj1bELQMDdst1ibA9lK6Rn7zWwlWZQYZvR3sCghyXiuLrqi1Iw9Ke1C4DeT1TlLk8EeBIbMSL4pxXsGDD+ihTJNOhe16lwkoWCkNKmkQrePVWcN5UnVigO3NVx4/l4tDlxcMHgRJqRJYigREdKkY7fQ5Pg3a3HHHk9v2+FEw9ZfbRJClWsXcYuEwJUhto28W3zy6W1XwCAm8nFDg12lB84B2uyBb+nJ5zKyZNstSxqrqR9uOFiEyI8/ItGiQ1c39o7Bmw5l1a+SvkYTxFb+GpY5G4RcG8M2zFi2Rp0G2iBEwbCF/FyUYeXwEFz4PDzShA3tDCERKf9dt78NkbodYrPLNNlva8mi/InH4JlXt3qTCHjLSOHHqPIOkQ4KceT5wyRb5bT7dELxwT5b3eZsvAnW4SlJ1zuZEz4HMRGyWzq1pH07JBNJGPrswgKEDVPMDgzAWg1DXyOZYCn+8J1eW9rRNg2pvE1drzwxcQJ7fSYgwds1tMrakW22XRgvREFT7C0GJGZEstBkVulGfP+dmNr2GRkIeNiMJrw7yrvUz+jwPKwxA16ebg8jWbe0Q364IumkkopOXbtikhNt3kExNZkk3aRU+iWNgWIqtWNGL6GWMB3QS4SOUExlE/iOyQJDAQPm4bdpkfVLuGTwk/Ce/4htjsVtMIs2y2/JlhVPaxy3Nj+7Dmv6jWB2ePzd8+KNHX8PHn/5Hw== -------------------------------------------------------------------------------- /models/shop.hsm: -------------------------------------------------------------------------------- 1 | //buying items 2 | { 3 | "rules": { 4 | ".read": true, //grant read access to all 5 | "users": { 6 | "$user":{ 7 | ".variables":{ 8 | "gold":{}, 9 | "swords":{}, 10 | "water":{} 11 | },".states":{ 12 | "playing":{} 13 | },".roles":{ 14 | "self":"$user == auth.username" 15 | },".transitions":{ 16 | "INITIALIZE":{"from":"null", "to":"playing", "role":"self", 17 | "effect":" 18 | newData.child('gold').val()==20 && 19 | newData.child('swords').val()==0 && 20 | newData.child('water').val()==1" 21 | },"BUY_SWORD":{ 22 | "from":"playing", "to":"playing", 23 | "role":"self", 24 | "signal":"BUY_SWORD", 25 | "guard":" 26 | newData.child('gold').val()>=10 && 27 | newData.child('swords').val() <=1", 28 | "effect":" 29 | newData.child('gold').val() == data.child('gold').val() - 10 && 30 | newData.child('swords').val() == data.child('swords').val() + 1" 31 | },"BUY_WATER":{ 32 | "from":"playing", "to":"playing", 33 | "role":"self", 34 | "signal":"BUY_SWORD", 35 | "guard":" 36 | newData.child('gold').val()>=4 && 37 | newData.child('water').val() <=14", 38 | "effect":" 39 | newData.child('gold').val() == data.child('gold').val() - 4 && 40 | newData.child('swords').val() == data.child('swords').val() +6" 41 | } 42 | } 43 | } 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /models/shop.rules: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "rules":{ 4 | ".read":true, 5 | "users":{ 6 | "$user":{ 7 | ".write":" 8 | ( //0: null -> playing, self 9 | /*type */($user == auth.username) 10 | /*from */ && data.child('state').val() == null 11 | /*to */ && newData.child('state').val() == 'playing' 12 | /*signal*/ && newData.child('signal').val() == null 13 | /*effect*/ && ( 14 | newData.child('gold').val()==20 && 15 | newData.child('swords').val()==0 && 16 | newData.child('water').val()==1 17 | ) 18 | )|| 19 | ( //1: playing -> playing, self 20 | /*type */($user == auth.username) 21 | /*from */ && data.child('state').val() == 'playing' 22 | /*to */ && newData.child('state').val() == 'playing' 23 | /*signal*/ && newData.child('signal').val() == 'BUY_SWORD' 24 | /*guards*/ && ( 25 | newData.child('gold').val()>=10 && 26 | newData.child('swords').val() <=1 27 | ) 28 | /*effect*/ && ( 29 | newData.child('gold').val() == data.child('gold').val() - 10 && 30 | newData.child('swords').val() == data.child('swords').val() + 1 31 | ) 32 | && newData.child('water').val() == data.child('water').val() //lock for water 33 | )|| 34 | ( //2: playing -> playing, self 35 | /*type */($user == auth.username) 36 | /*from */ && data.child('state').val() == 'playing' 37 | /*to */ && newData.child('state').val() == 'playing' 38 | /*signal*/ && newData.child('signal').val() == 'BUY_SWORD' 39 | /*guards*/ && ( 40 | newData.child('gold').val()>=4 && 41 | newData.child('water').val() <=14 42 | ) 43 | /*effect*/ && ( 44 | newData.child('gold').val() == data.child('gold').val() - 4 && 45 | newData.child('swords').val() == data.child('swords').val() +6 46 | ) 47 | && newData.child('water').val() == data.child('water').val() //lock for water 48 | ) 49 | " 50 | } 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "firesafe", 3 | "description": "Compile hierarchical state machine into Firebase security rules", 4 | "version": "0.1.1", 5 | "private": false, 6 | "homepage": "https://github.com/tomlarkworthy/firesafe", 7 | "author": { 8 | "name": "Tom Larkworthy", 9 | "email": "tom.larkworthy@gmail.com" 10 | }, 11 | "repository": { 12 | "role": "git", 13 | "url": "git://github.com/tomlarkworthy/firesafe.git" 14 | }, 15 | "bugs": { 16 | "url": "https://github.com/tomlarkworthy/firesafe/issues" 17 | }, 18 | "licenses": [ 19 | { 20 | "role": "GPL-3.0+", 21 | "url": "https://github.com/tomlarkworthy/firesafe/blob/master/LICENSE.txt" 22 | },{ 23 | "role": "Firesafe", 24 | "url": "https://github.com/tomlarkworthy/firesafe/blob/master/COPYING.txt" 25 | } 26 | ], 27 | "engines": { 28 | "node": ">= 0.8" 29 | }, 30 | "scripts": { 31 | "test": "grunt test" 32 | }, 33 | "main": "./src/firesafe_main.js", 34 | "devDependencies": { 35 | "firebase": "1.0", 36 | "firebase-token-generator": "0.1.4", 37 | "grunt": "~0.4.2", 38 | "grunt-contrib-jshint": "~0.6.3", 39 | "grunt-contrib-nodeunit": "~0.2.0", 40 | "grunt-contrib-uglify": "~0.2.2", 41 | "grunt-pandoc":"0.2.3", 42 | "grunt-peg":"1.0.0", 43 | "underscore": "~1.4.2", 44 | "sprintf": "~0.1.3", 45 | "jquery-deferred": "0.3.0", 46 | "expect.js": "~0.2.0", 47 | "mocha": "~1.8.1", 48 | "closure-compiler":"0.2.3", 49 | "esprima": "1.0.4", 50 | "estraverse": "1.5.0", 51 | "escodegen": "1.2.0", 52 | "falafel": "0.3.1", 53 | "handlebars": "1.3.0" 54 | }, 55 | "preferGlobal": "true", 56 | "bin": { 57 | "firesafe" : "src/firesafe-cli.js" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /properties.js: -------------------------------------------------------------------------------- 1 | exports.FIREBASE_LOCATION = 'firesafe-sandbox.firebaseio.com'; 2 | exports.FIREBASE_SECRET = "9MPlKqcjUPZtbvbUuqD8omoK7f4kRU7FDxBIz2fX"; 3 | -------------------------------------------------------------------------------- /src/execute.js: -------------------------------------------------------------------------------- 1 | var closure = require("closure-compiler"); 2 | var fs = require("fs"); 3 | var handlebars = require("handlebars"); 4 | var template = handlebars.compile(fs.readFileSync('./src/execute.template').toString()); 5 | var $ = require('jquery-deferred'); 6 | var esprima = require('esprima'); 7 | var estraverse = require('estraverse'); 8 | var escodegen = require('escodegen'); 9 | var falafel = require('falafel'); 10 | 11 | 12 | 13 | 14 | function assert(condition, msg) { 15 | if(!condition){ 16 | console.log("error ", msg); 17 | } 18 | } 19 | 20 | /** 21 | * extracts parameters from a signal name like go(x,y) 22 | * returns ["x","y"] 23 | * @param signal_name 24 | */ 25 | var extract_parameters = function(signal_name){ 26 | console.log("signal_name", signal_name) 27 | var args = /\(([^)]+)/.exec(signal_name); 28 | if (args[1]) { 29 | args = args[1].split(/\s*,\s*/); 30 | } 31 | 32 | return args; 33 | }; 34 | 35 | var extern_statement = handlebars.compile("/** @type {string} */\nvar {{{param}}};\n"); 36 | 37 | 38 | /** 39 | * returns an deferred that expands to an object that represents a generate clause from a snippet of src code 40 | */ 41 | exports.new_execute = function(signal, src){ 42 | var def = $.Deferred(); 43 | 44 | /* 45 | We run the src through the closue compiler to remove redundant code and rearrange the function so 46 | that all the arithmatic occurs in the funal ruturn statement 47 | */ 48 | 49 | try{ 50 | 51 | //the paramaters for the signal are encoded in the name 52 | var params = extract_parameters(signal); 53 | 54 | console.log("params ", params); 55 | 56 | //we build a function out of our information, and deed this to closure 57 | var function_src = template({ 58 | src:src, 59 | params:params 60 | } 61 | ); 62 | 63 | //console.log("closure input: ", function_src) 64 | 65 | var options = 66 | { 67 | compilation_level : 'ADVANCED_OPTIMIZATIONS', 68 | language_in : 'ECMASCRIPT5_STRICT', 69 | jscomp_error: "es5Strict", 70 | externs: "./src/execute_externs.js" 71 | }; 72 | 73 | 74 | //the callback for processing the results of the closure compiler 75 | function post_compile (err, code, stderr) { 76 | console.log(stderr); 77 | if (err){ 78 | test.ok(false); 79 | } 80 | 81 | //code contains our closure compiled snippit, now we have to extract the 82 | //object built after the return 83 | console.log(code); 84 | //convert to AST 85 | var ast = esprima.parse(code); 86 | 87 | var return_expr = null; 88 | 89 | //find the return {value} 90 | estraverse.traverse(ast, { 91 | enter: function (node) { 92 | if (node.type == 'ReturnStatement'){ 93 | return_expr = node; 94 | this.break(); 95 | } 96 | } 97 | }); 98 | assert(return_expr != null, "there was no return in the execute clause"); 99 | var object_exp = return_expr.argument; 100 | assert(object_exp.type == "ObjectExpression", "return was not followed by an object definition"); 101 | 102 | var assignments = {}; 103 | 104 | for(var p_id in object_exp.properties){ 105 | var property = object_exp.properties[p_id]; 106 | assert(property.type == "Property", "Object was not filled with properties"); 107 | assert(property.key.type == "Identifier", "Kay in object was not an Identifier"); 108 | var key = property.key.name; 109 | var value_ast = property.value; 110 | var value = escodegen.generate(value_ast); //convert expressions or whatever back into src code 111 | 112 | assignments[key] = value; 113 | } 114 | 115 | //this is the passback of the enclosing function, delivered via a Deferred 116 | //it contains the intermediate representations and a function (rules()) for generating 117 | //serverside logic 118 | var execute = { 119 | src:src, 120 | assignments: assignments, 121 | params:params, 122 | //this should generate the security clause for enforcing the execute logic server side (as best possible) 123 | rules: function(){ 124 | var clauses = []; 125 | 126 | for (var variable in this.assignments){ 127 | var clause = "newData.child('" + variable + "').val() == " + this.assignments[variable]; 128 | 129 | //final tidy up is to replace all the symbols with the correct ones 130 | var replace_symbols = function(node){ 131 | 132 | if(node.type === "Identifier"){ 133 | //console.log("\nidentifier", node.name); 134 | //check to see if its a parameter, and replace it with a lookup 135 | for(var param_id in params){ 136 | var param = params[param_id]; 137 | if(param === node.name){ 138 | console.log("param", param); 139 | node.update("newData.child('" + param + "').val()"); 140 | break; 141 | } 142 | } 143 | /* 144 | if(node.name == "now"){ 145 | node.update("Firebase.server.TIMESTAMP"); 146 | }*/ 147 | } 148 | }; 149 | 150 | //program_state gets filled in, indirectly, with a C_VAR 151 | clause = falafel(clause, {}, replace_symbols); 152 | clauses.push(clause); 153 | } 154 | console.log("rules: ", clauses.join(" &&\n")) 155 | return clauses.join(" &&\n"); 156 | } 157 | }; 158 | 159 | def.resolve(execute); 160 | } 161 | 162 | closure.compile(function_src, options, post_compile); 163 | 164 | }catch(e){ 165 | console.log(e); 166 | console.log(e.stack); 167 | def.reject(data.status); 168 | } 169 | 170 | return def; 171 | }; -------------------------------------------------------------------------------- /src/execute.template: -------------------------------------------------------------------------------- 1 | $["export"] = function({{#each params}} x{{@index}}, {{/each}} DUMMY){ 2 | 3 | {{#each params}} 4 | {{this}} = x{{@index}}; 5 | {{/each}} 6 | 7 | {{{src}}} 8 | } 9 | -------------------------------------------------------------------------------- /src/execute_externs.js: -------------------------------------------------------------------------------- 1 | /** @type {number} */ 2 | var now; 3 | 4 | 5 | //fields that should not be optimized 6 | /** @dict */ 7 | var auth; 8 | /** @type {string} */ 9 | auth.username; 10 | /** @type {string} */ 11 | auth.provider; 12 | /** @type {string} */ 13 | auth.id; 14 | /** @type {string} */ 15 | auth.uid; 16 | /** @type {string} */ 17 | auth.access_token; 18 | /** @type {string} */ 19 | auth.rememberMe; 20 | /** @type {string} */ 21 | auth.scope; 22 | /** @type {string} */ 23 | auth.oauth_token; 24 | /** @type {string} */ 25 | auth.accessToken; 26 | /** @type {string} */ 27 | auth.accessTokenSecret; 28 | /** @type {string} */ 29 | auth.displayName; 30 | /** @type {string} */ 31 | auth.firebaseAuthToken; 32 | /** @type {string} */ 33 | auth.thirdPartyUserData; 34 | /** @type {string} */ 35 | auth.displayName; 36 | /** @type {string} */ 37 | auth.email; 38 | /** @type {string} */ 39 | auth.md5_hash; 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/firebase_io.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module is for utility functions for Firebase 3 | * For standard operations you can jsut use require("firebase") directly 4 | * 5 | * This is meant for more complex functions like uploading/downloading a validation/rule set 6 | */ 7 | 8 | 9 | exports.Firebase = require('firebase'); 10 | exports.sandbox = new exports.Firebase(require('../properties.js').FIREBASE_LOCATION); 11 | exports.FIREBASE_SECRET = require('../properties.js').FIREBASE_SECRET; 12 | exports.FIREBASE_LOCATION = require('../properties.js').FIREBASE_LOCATION; 13 | 14 | /** 15 | * uploads the validation rules (representated as a string) 16 | * returns a deferred object, the error handler is called if the upload is rejected (e.g. invalid rules) 17 | */ 18 | exports.setValidationRules = function(rules_str){ 19 | //console.log("\n setValidationRules: ", rules_str); 20 | //http://stackoverflow.com/questions/18840080/updating-firebase-security-rules-through-firebaseref-set 21 | var $ = require('jquery-deferred'); 22 | var def = $.Deferred(); 23 | var https = require('https'); 24 | 25 | //equivelent of curl -X PUT -d '{ "rules": { ".read": true } }' https://SampleChat.firebaseio-demo.com/.settings/rules.json?auth=FIREBASE_SECRET 26 | var options = { 27 | hostname: exports.FIREBASE_LOCATION, 28 | port: 443, 29 | path: '/.settings/rules.json?auth='+exports.FIREBASE_SECRET, 30 | method: 'PUT' 31 | }; 32 | 33 | var req = https.request(options, function(res) { 34 | //console.log("statusCode: ", res.statusCode); 35 | //console.log("headers: ", res.headers); 36 | 37 | res.on('data', function(d) { 38 | process.stdout.write(d); 39 | var data = JSON.parse(d); //check the return json's status that Firebase writes 40 | if (data.status == "ok"){ 41 | def.resolve(data.status); //so Firebase says it uploaded ok 42 | }else{ 43 | def.reject(data.status); // the rules could be rejected for a variety of reasons 44 | } 45 | }); 46 | }); 47 | 48 | req.write( rules_str , 'utf8' );//write the actual rules in the request payload 49 | req.end(); 50 | 51 | req.on('error', function(e) { 52 | console.error(e);//the whole request went bad which is not a good 53 | def.reject(e) 54 | }); 55 | 56 | return def; 57 | }; 58 | 59 | /** 60 | * retreives the validation rules (representated as a string) 61 | * returns a deferred object, with the rules being the data payload 62 | */ 63 | exports.getValidationRules = function(rules_str){ 64 | console.log("\n getValidationRules"); 65 | //http://stackoverflow.com/questions/18840080/updating-firebase-security-rules-through-firebaseref-set 66 | var $ = require('jquery-deferred'); 67 | var def = $.Deferred(); 68 | var https = require('https'); 69 | 70 | //curl https://SampleChat.firebaseio-demo.com/.settings/rules.json?auth=FIREBASE_SECRET 71 | var options = { 72 | hostname: exports.FIREBASE_LOCATION, 73 | port: 443, 74 | path: '/.settings/rules.json?auth='+exports.FIREBASE_SECRET, 75 | method: 'GET' 76 | }; 77 | 78 | var req = https.request(options, function(res) { 79 | console.log("statusCode: ", res.statusCode); 80 | console.log("headers: ", res.headers); 81 | 82 | res.on('data', function(d) { 83 | var data = d.toString(); 84 | console.log("\n", data); 85 | def.resolve(data); //so Firebase returned the rules ok 86 | }); 87 | }); 88 | 89 | req.end(); 90 | 91 | req.on('error', function(e) { 92 | console.error(e);//the whole request went bad which is not a good 93 | def.reject(e) 94 | }); 95 | 96 | return def; 97 | }; 98 | 99 | exports.getAuthToken = function(username, admin){ 100 | var FBTokenGenMod = require('firebase-token-generator'); 101 | var FBTokenGenerator = new FBTokenGenMod(exports.FIREBASE_SECRET); 102 | return FBTokenGenerator.createToken({ username: username }, {admin: admin}); 103 | }; 104 | 105 | exports.login = function(AUTH_TOKEN){ 106 | var $ = require('jquery-deferred'); 107 | var def = $.Deferred(); 108 | 109 | exports.sandbox.auth(AUTH_TOKEN, function(error) { 110 | if(error) { 111 | console.log("Login FAILED!"); 112 | def.reject() 113 | } else { 114 | console.log("Login Succeeded!"); 115 | def.resolve(); 116 | } 117 | }); 118 | return def; 119 | }; 120 | 121 | /** 122 | * login to sandbox with the given username, and boolean option to be an admin 123 | * @param username 124 | * @param admin boolean, should the login be granted blanket read and write access?? 125 | * @return {*} 126 | */ 127 | exports.loginAs = function(username, admin){ 128 | return exports.login(exports.getAuthToken(username, admin)); 129 | }; 130 | -------------------------------------------------------------------------------- /src/firesafe-cli.js: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | 3 | var userArgs = process.argv.slice(2); 4 | 5 | var argv = process.argv.slice(2); 6 | var help = 7 | "\nusage: \n firesafe \n\n" + 8 | "Generates Firebase validation rules based on a Hierarchical State Machine definition file (.hsm),\n\n" + 9 | "srcfile : the input hsm description file (.hsm) \n"+ 10 | "dstfile : the file to output firebase validation rules, copy and paste into security section of Firebase webpage \n"+ 11 | " alternatively setup automatic upload of rules (see firebase_io.js) \n\n"+ 12 | "-h, [--help] # Show this help message and quit\n\n"+ 13 | "Commercial licenses, graphical tools and formal model checking under development\n" + 14 | "Keep up to date with new features at firesafe-announce@googlegroups.com\n"; 15 | 16 | var tasks = {}; 17 | tasks.help = function(){ 18 | console.log(help);}; 19 | 20 | if(argv[0] === "--help" || argv[0] === "-h" || argv.length != 2) { 21 | tasks.help(); 22 | }else{ 23 | var src_file = userArgs[0]; 24 | var dst_file = userArgs[1]; 25 | 26 | var fs = require('fs'); 27 | var firesafe = require('./firesafe_main.js'); 28 | 29 | //load hsm rules from file 30 | try{ 31 | var hsm_def = fs.readFileSync(src_file, "utf8"); 32 | 33 | //transform hsm into rules 34 | var rules = firesafe.hsm_to_rules(hsm_def); 35 | 36 | //write the rules to the rule file 37 | fs.writeFileSync(dst_file, rules); 38 | }catch(e){ 39 | console.log(e); 40 | } 41 | 42 | } 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/firesafe_main.js: -------------------------------------------------------------------------------- 1 | exports.hsm_to_rules = require("./hsm_to_rules.js").convert; -------------------------------------------------------------------------------- /src/hsm_to_client.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tomlarkworthy/firesafe/9dddcce9c8423837e9f7b6d7d9a76af9dc7a682a/src/hsm_to_client.js -------------------------------------------------------------------------------- /src/hsm_to_rules.js: -------------------------------------------------------------------------------- 1 | /** 2 | *generator that takes the hsm json language, and outputs a set of validation rules enforcing the semantics 3 | */ 4 | 5 | var $ = require('jquery-deferred'); 6 | 7 | /** 8 | * main method, reads at hsm block and returns a rule file (synchronous, cannot do closure compiler) 9 | * @param hsm 10 | * @depricated 11 | */ 12 | exports.convert = function(hsm){ 13 | var parser = require('./hsm_to_rules_parser.js'); 14 | 15 | //strip comments 16 | hsm = hsm.replace(/(\/\*([\s\S]*?)\*\/)|(\/\/(.*)$)/gm, ''); 17 | 18 | try{ 19 | //convert into nested json structures, using grammar 20 | var top_block = parser.parser.parse(hsm, "block"); 21 | }catch(e){ 22 | console.log(e); 23 | throw e; 24 | } 25 | 26 | //generate code 27 | var code = exports.top_block(top_block, "\n", []); 28 | 29 | //console.log(code); 30 | 31 | return code 32 | }; 33 | 34 | /** 35 | * main method, reads at hsm block and returns a rule file (asynchronous) 36 | * preceeded by a asyncrounous preprocessing step 37 | * @param hsm 38 | */ 39 | exports.convert_async = function(hsm){ 40 | var parser = require('./hsm_to_rules_parser.js'); 41 | 42 | //strip comments 43 | hsm = hsm.replace(/(\/\*([\s\S]*?)\*\/)|(\/\/(.*)$)/gm, ''); 44 | 45 | try{ 46 | //convert into nested json structures, using grammar 47 | var top_block = parser.parser.parse(hsm, "block"); 48 | }catch(e){ 49 | console.log(e); 50 | throw e; 51 | } 52 | 53 | var result = $.Deferred(); 54 | 55 | var preprocessing = { 56 | executes:{} // the compiled execute clauses 57 | }; 58 | 59 | $.when.apply($, preprocess_top_block(top_block, preprocessing)).then(function(){ 60 | console.log("\npreprocessing", preprocessing); 61 | //generate code 62 | var code = exports.top_block(top_block, "\n", [], preprocessing); 63 | result.resolve(code); 64 | }); 65 | 66 | return result; 67 | 68 | }; 69 | 70 | //helpers 71 | exports.is_string = function(x){ 72 | return x.substring != undefined; 73 | }; 74 | 75 | exports.replace_prefix = function(x, new_prefix){ 76 | return x.replace(/\n\s*/gm, new_prefix); 77 | }; 78 | 79 | exports.sortObject = function(o) { 80 | var sorted = {}, 81 | key, a = []; 82 | 83 | for (key in o) { 84 | if (o.hasOwnProperty(key)) { 85 | a.push(key); 86 | } 87 | } 88 | 89 | a.sort(); 90 | 91 | for (key = 0; key < a.length; key++) { 92 | sorted[a[key]] = o[a[key]]; 93 | } 94 | return sorted; 95 | }; 96 | 97 | 98 | 99 | 100 | /** 101 | * top down generator root 102 | * @param hsm 103 | */ 104 | exports.top_block = function(top_block, prefix, types, preprocessing){ 105 | //console.log("\n", "top_block"); 106 | //console.log("\n", top_block); 107 | 108 | var result = 109 | prefix + '{' + 110 | prefix + '\t"rules":'+ 111 | exports.block(top_block['val']['rules'], prefix + "\t", types, preprocessing) + 112 | prefix + '}'; 113 | 114 | return result; 115 | 116 | }; 117 | 118 | exports.block = function(block, prefix, types, preprocessing){ 119 | //console.log("\n", "block"); 120 | //console.log("\n", block); 121 | //console.log("\n", block["!type"]); 122 | 123 | if(block["!type"] === "OBJ"){ 124 | var lines = []; 125 | 126 | var machine = null; 127 | 128 | if(block['val'][".states"]){ 129 | machine = exports.new_machine(); 130 | machine.process_states(block['val'][".states"], null); 131 | } 132 | 133 | if(block['val'][".variables"]){ 134 | if(machine == null) machine = exports.new_machine(); 135 | machine.process_variables(block['val'][".variables"]) 136 | } 137 | 138 | if(block['val'][".transitions"]){ 139 | if(machine == null) machine = exports.new_machine(); 140 | machine.process_transitions(block['val'][".transitions"]); 141 | } 142 | 143 | if(block['val'][".roles"]){ 144 | if(machine == null) machine = exports.new_machine(); 145 | machine.process_roles(block['val'][".roles"]) 146 | } 147 | 148 | for (var key in block['val']) { 149 | if(key === ".write" && machine){ 150 | //ignore writes when in machine mode 151 | console.log("\n WARNING: .write ignored when declared in same layer as a state machine") 152 | }else if(key === ".states"){ 153 | }else if(key === ".variables"){ //ignore all the machine special syntax 154 | }else if(key === ".transitions"){ 155 | }else if(key === ".roles"){ 156 | }else if(key === ".types"){ 157 | }else{ 158 | lines.push('"'+key +'"'+':' + exports.block(block['val'][key], prefix + "\t", types, preprocessing)); 159 | } 160 | } 161 | 162 | if(machine){ 163 | machine.flatten_transactions(); 164 | lines.push('".write"'+':' + machine.gen_write(prefix + "\t", preprocessing)); 165 | } 166 | 167 | return "{"+prefix + "\t" + lines.join("," + prefix + "\t") + prefix + "}"; 168 | 169 | }else if(block["!type"] === "STR"){ 170 | return '"'+block.val+'"'; 171 | }else if(block["!type"] === "BOOL"){ 172 | return block.val; 173 | }else{ 174 | console.log("\n **** UNRECOGNISED TOKEN ***", block["!type"]) 175 | } 176 | 177 | }; 178 | 179 | exports.new_machine = function(){ 180 | var machine = { 181 | states:{}, //map from name -> {parent, init} 182 | transitions:{}, 183 | variables:{}, 184 | roles:{}, 185 | signals:{},//all possible signals encountered 186 | initial:null 187 | }; 188 | 189 | /** 190 | * sets up the state list recursively for the machine, and sets the initial state 191 | */ 192 | machine.process_states = function(states_parse_obj, parent){ 193 | //console.log("\nstates:", states_parse_obj); 194 | 195 | if(states_parse_obj.val[".init"]){ 196 | var init = states_parse_obj.val[".init"].val; 197 | 198 | if(parent != null){ 199 | machine.states[parent].init = init; 200 | machine.states[parent].leaf = false; 201 | } 202 | }else{ 203 | var init = null; 204 | } 205 | 206 | for(var name in states_parse_obj.val){ 207 | 208 | if(name == ".init"){ 209 | }else{ 210 | machine.states[name] = {parent:parent, leaf:true}; 211 | machine.process_states(states_parse_obj.val[name], name) 212 | } 213 | } 214 | 215 | if(parent == null){ 216 | //console.log("\nmachine.states:", machine.states); 217 | } 218 | }; 219 | 220 | machine.process_variables = function(variables_parse_obj){ 221 | //console.log("\nprocess_variables:", variables_parse_obj); 222 | for(var name in variables_parse_obj.val){ 223 | machine.process_variable(name, variables_parse_obj.val[name].val); 224 | } 225 | //console.log("\nmachine.variables:", machine.variables); 226 | }; 227 | 228 | machine.process_variable = function(name, properties){ 229 | //console.log("\nprocess_variable:", name, properties); 230 | machine.variables[name] = {} 231 | }; 232 | 233 | machine.process_transitions = function(transitions_parse_obj){ 234 | //console.log("\nprocess_transitions:", transitions_parse_obj); 235 | for(var name in transitions_parse_obj.val){ 236 | machine.process_transition(name, transitions_parse_obj.val[name].val); 237 | } 238 | //console.log("\ntransitions:", machine.transitions); 239 | machine.signals = exports.sortObject(machine.signals); 240 | //console.log("\signals:", machine.signals); 241 | }; 242 | 243 | machine.process_transition = function(name, properties){ 244 | //console.log("\nprocess_transition:", name, properties); 245 | var from, to, guard="", effect=""; 246 | var transition = {}; 247 | if(properties.from){ 248 | transition.from = properties.from.val; 249 | }else{ 250 | transition.from = null; 251 | } 252 | 253 | transition.to = properties.to.val; 254 | 255 | if(properties.role){ 256 | transition.role = properties.role.val; 257 | }else{ 258 | transition.role = null; 259 | } 260 | 261 | if(properties.signal){ 262 | transition.signal = properties.signal.val; 263 | }else{ 264 | transition.signal = null; 265 | } 266 | 267 | if(properties.guard){ 268 | transition.guard = properties.guard.val 269 | }else{ 270 | transition.guard = null 271 | } 272 | if(properties.effect){ 273 | transition.effect = properties.effect.val 274 | }else{ 275 | transition.effect = null 276 | } 277 | 278 | if(properties.execute){ 279 | transition.execute = properties.execute.val 280 | }else{ 281 | transition.execute = null 282 | } 283 | 284 | machine.signals[transition.signal] = {}; 285 | machine.transitions[name] = transition; 286 | 287 | //console.log("\ntransition:", transition); 288 | }; 289 | 290 | machine.process_roles = function(types_parse_obj){ 291 | //console.log("\ntypes_parse_obj:", types_parse_obj); 292 | for(var name in types_parse_obj.val){ 293 | machine.roles[name] = types_parse_obj.val[name].val; 294 | } 295 | //console.log("\nmachine.roles:", machine.roles); 296 | }; 297 | 298 | /** 299 | * This replaces the hierarchical transitions with many explicit flat transformation 300 | * 301 | * For example is S is the parent of S1 302 | * and S has a transition "G" to S2, by the UML spec S1 also should also respond to "G" 303 | * The process of flattening makes explicit all the inheritance. 304 | * by creating all the transitions possible from leaf states 305 | */ 306 | machine.flatten_transactions = function(){ 307 | var flat_transitions = {}; 308 | var uid = 0; 309 | 310 | //find init transitions 311 | for(var t_id in machine.transitions){ 312 | var transition = machine.transitions[t_id]; 313 | if(transition.from == null){ //its an init transition 314 | var target = machine.resolve_target(transition.to); 315 | flat_transitions[uid] = { 316 | from:null, 317 | to:target, 318 | role:transition.role, 319 | signal:transition.signal, 320 | guard:transition.guard, 321 | effect:transition.effect, 322 | execute:transition.execute 323 | }; 324 | uid += 1; 325 | } 326 | } 327 | 328 | for(var from in machine.states){ 329 | if(machine.states[from].leaf){ 330 | //we have ancestor transitions that are inherited by this leaf node 331 | for(var signal in machine.signals){ 332 | var transitions = machine.resolve_signals(signal, from); 333 | for(t_id in transitions){ 334 | var transition = transitions[t_id]; 335 | var target = machine.resolve_target(transition.to); 336 | 337 | flat_transitions[uid] = { 338 | from:from, 339 | to:target, 340 | role:transition.role, 341 | signal:signal, 342 | guard:transition.guard, 343 | effect:transition.effect, 344 | execute:transition.execute 345 | }; 346 | uid += 1; 347 | } 348 | } 349 | //check for null signal too 350 | var transitions = machine.resolve_signals(null, from); 351 | for(t_id in transitions){ 352 | var transition = transitions[t_id]; 353 | var target = machine.resolve_target(transition.to); 354 | 355 | flat_transitions[uid] = { 356 | from:from, 357 | to:target, 358 | role:transition.role, 359 | signal:null, 360 | guard:transition.guard, 361 | effect:transition.effect, 362 | execute:transition.execute 363 | }; 364 | uid += 1; 365 | } 366 | 367 | } 368 | } 369 | 370 | //console.log("\nflat_transitions", flat_transitions) 371 | machine.transitions = flat_transitions; 372 | }; 373 | 374 | /** 375 | * calculates where a transition would end up, if starting and pointing to the specific location 376 | */ 377 | machine.resolve_target = function(to){ 378 | //exit's would fire at current state - from first in normal embedded 379 | //then the transition takes place to the high level destination 380 | var current = to; 381 | //we then recurse until reaching a leaf state 382 | while(machine.states[current].init){ 383 | current = machine.states[current].init 384 | } 385 | 386 | return current; 387 | }; 388 | 389 | /** 390 | * returns a set of transitions. 391 | * Finds the super transitions with the provided signal, leaving the provided state, (or one of it's ancestors) 392 | */ 393 | machine.resolve_signals = function(signal, state){ 394 | var matches = []; 395 | //console.log("\n resolve signal:", signal, "on:", state); 396 | var current = state; 397 | 398 | while(current != null){ 399 | //look for transition leaving current with right signal 400 | for(var t_id in machine.transitions){ 401 | if(machine.transitions[t_id].signal === signal && machine.transitions[t_id].from === current){ 402 | matches.push(machine.transitions[t_id]); 403 | } 404 | } 405 | 406 | if(matches.length > 0){ 407 | return matches; 408 | } 409 | 410 | //no matches, so go up hierarchy 411 | current = machine.states[current].parent; 412 | } 413 | 414 | return []; 415 | }; 416 | 417 | /** 418 | * a specific machine is encoded in a single ".write" clause in the validation rules 419 | * this encodes all the different transitions, and the initial condition 420 | * @param prefix 421 | */ 422 | machine.gen_write = function(prefix, preprocessing){ 423 | var clauses = []; 424 | 425 | for(var name in machine.transitions){ 426 | var transition = machine.transitions[name]; 427 | 428 | //first add a comment 429 | var clause = prefix + "\t( //" + name + ": " + transition.from + " -> " + transition.to + ", " + transition.role 430 | 431 | //add the authentication clause 432 | if(transition.role!= null){ 433 | clause += prefix + "\t\t/*role */("+machine.roles[transition.role] +")"; 434 | }else{ 435 | clause += prefix + "\t\t/*role */(true)"; 436 | } 437 | 438 | //then add the from state requirement (if any, could be initial state) 439 | if(transition.from!= null && transition.from != 'null'){ 440 | clause += prefix + "\t\t/*from */ && data.child('state').val() == '" + transition.from +"'"; 441 | }else{ 442 | clause += prefix + "\t\t/*from */ && data.child('state').val() == null"; 443 | } 444 | 445 | //then add the to state requirement 446 | if(transition.to!= null && transition.to != 'null'){ 447 | clause += prefix + "\t\t/*to */ && newData.child('state').val() == '" + transition.to+"'"; 448 | }else{ 449 | clause += prefix + "\t\t/*to */ && newData.child('state').val() == null"; 450 | } 451 | 452 | //then add the signal logic (if any) 453 | if(transition.signal != null){ 454 | clause += prefix + "\t\t/*signal*/ && newData.child('signal').val() == '"+transition.signal+"'"; 455 | }else{ 456 | clause += prefix + "\t\t/*signal*/ && newData.child('signal').val() == null"; 457 | } 458 | 459 | //then add the guard logic (if any) 460 | if(transition.guard != null){ 461 | clause += prefix + "\t\t/*guards*/ && (" + exports.replace_prefix(transition.guard, prefix + "\t\t\t"); 462 | clause += prefix + "\t\t)"; 463 | } 464 | 465 | //then add the effect logic (if any) 466 | if(transition.effect != null){ 467 | clause += prefix + "\t\t/*effect*/ && (" + exports.replace_prefix(transition.effect, prefix + "\t\t\t"); 468 | clause += prefix + "\t\t)"; 469 | } 470 | 471 | var unlocked_variables = []; //denotes which variables do not need fixing as they are mentioned in an execute of effect clause 472 | 473 | if(transition.execute != null){ 474 | console.log("adding exclude"); 475 | var execute = preprocessing.executes[name]; 476 | 477 | for(var variable in execute.assignments){ 478 | unlocked_variables.push(variable) 479 | } 480 | 481 | clause += prefix + "\t\t/*execut*/ && (" + prefix + "\t\t\t" + exports.replace_prefix(execute.rules(), prefix + "\t\t\t"); 482 | clause += prefix + "\t\t)"; 483 | } 484 | 485 | 486 | //then add the fixings for variables 487 | //from the effect clause 488 | for(var variable in machine.variables){ 489 | //look to see whether this variable was already mentioned in the effects 490 | //todo: should check whether its mentioned as a POST CONDITION not just string matching 491 | //we have a security leak here 492 | if(transition.effect!= null && transition.effect.indexOf(variable) !== -1){ 493 | //its mentioned in the effects, no need to lock 494 | unlocked_variables.push(variable); 495 | } 496 | } 497 | 498 | //from the execute clause 499 | 500 | //apply locks to variables not mentioned in execute or effects 501 | for(var variable in machine.variables){ 502 | var unlocked = false; 503 | for(var unlocked_id in unlocked_variables) { 504 | if(unlocked_variables[unlocked_id] === variable){unlocked = true;} 505 | } 506 | if(!unlocked){ 507 | clause += prefix + "\t\t&& newData.child('" + variable + "').val() == data.child('"+variable +"').val() //lock for " + variable 508 | } 509 | } 510 | 511 | clause += prefix + "\t)"; 512 | 513 | clauses.push(clause); 514 | } 515 | 516 | //or together all the transitions 517 | return '"' + clauses.join("||") + prefix + '"' 518 | }; 519 | 520 | return machine; 521 | 522 | }; 523 | 524 | 525 | /******************************************* 526 | * PRE PROCESSING 527 | * 528 | ******************************************/ 529 | 530 | /** 531 | * first pass finds things that may take a long time (like closure compiler), so we can start the heavy lifting in parrallel 532 | */ 533 | var preprocess_top_block = function(top_block, preprocessing){ 534 | //console.log("\npreprocess_top_block"); 535 | return preprocess_block(top_block['val']['rules'], preprocessing); 536 | }; 537 | 538 | var preprocess_block = function(block, preprocessing){ 539 | //console.log("\npreprocess_block"); 540 | var defs = []; 541 | if(block["!type"] === "OBJ"){ 542 | if(block['val'][".transitions"]){ 543 | defs = defs.concat(preprocess_transitions(block['val'][".transitions"], preprocessing)); 544 | } 545 | 546 | for (var key in block['val']) { 547 | if(key === ".write" && machine){ 548 | //ignore writes when in machine mode 549 | console.log("\n WARNING: .write ignored when declared in same layer as a state machine") 550 | }else if(key === ".states"){ 551 | }else if(key === ".variables"){ //ignore all the machine special syntax 552 | }else if(key === ".transitions"){ 553 | }else if(key === ".roles"){ 554 | }else if(key === ".types"){ 555 | }else{ 556 | defs = defs.concat(preprocess_block(block['val'][key], preprocessing)); 557 | } 558 | } 559 | } 560 | return defs; 561 | }; 562 | 563 | var preprocess_transitions = function(transitions_parse_obj, preprocessing){ 564 | //console.log("\npreprocess_transitions"); 565 | var defs = []; 566 | //console.log("\nprocess_transitions:", transitions_parse_obj); 567 | for(var name in transitions_parse_obj.val){ 568 | defs = defs.concat(preprocess_transition(name, transitions_parse_obj.val[name].val, preprocessing)); 569 | } 570 | 571 | //console.log("\npreprocess_transitions defs", defs); 572 | return defs; 573 | }; 574 | 575 | var preprocess_transition = function(name, properties, preprocessing){ 576 | //console.log("\npreprocess_transition"); 577 | var defs = []; 578 | if(properties.execute){ 579 | var execute_src = properties.execute.val; 580 | //so we start compiling an execute clause 581 | var execute_lib = require("./execute.js"); 582 | var execute_def = execute_lib.new_execute(properties.signal.val, execute_src); 583 | //we both register the built object in a lookup when it is ready, 584 | //and also add the deferred object to the big list of things we need to wait for after preprocessing 585 | $.when(execute_def).then(function(execute_obj){ 586 | preprocessing.executes[name] = execute_obj; 587 | }); 588 | defs.push(execute_def); 589 | } 590 | 591 | //console.log("\npreprocess_transition defs", defs); 592 | return defs; 593 | }; -------------------------------------------------------------------------------- /test/assets/deadcode.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * @param {number} input value in 5 | */ 6 | keep["test"] = function(input){ 7 | "use strict"; 8 | x = 5; 9 | x = input; 10 | var y = x*2; 11 | return {"out":y}; 12 | }; 13 | 14 | 15 | -------------------------------------------------------------------------------- /test/auction_test.js: -------------------------------------------------------------------------------- 1 | var firebase_io = require('../src/firebase_io.js'); 2 | var firebase = require('firebase'); 3 | var $ = require('jquery-deferred'); 4 | var fs = require('fs'); 5 | var converter = require('../src/hsm_to_rules.js'); 6 | var test_utils = require("../test/test_utils.js"); 7 | 8 | /********************************************************************************************************************** 9 | * INITIAL RULES 10 | ********************************************************************************************************************* 11 | * Send the hand crafted rules to Firebase, important this occurs first to setup test suite with the rules we want to test 12 | */ 13 | 14 | exports.testWriteSendXRulesValid = function(test){ 15 | //load rules as hsm file 16 | var hsm_def = fs.readFileSync("./models/auction.hsm", "utf8"); 17 | 18 | try{ 19 | //convert into normal rules 20 | $.when(converter.convert_async(hsm_def)).then(function(rules){ 21 | fs.writeFileSync("./models/auction.rules", rules); 22 | 23 | $.when(firebase_io.setValidationRules(rules)) 24 | .then(function(){ 25 | test.ok(true, "these rules should have been accepted"); 26 | test.done(); 27 | },function(error){ //deferred error handler should not be called 28 | test.ok(false, "these rules should not have been rejected"); 29 | test.done(); 30 | }); 31 | }); 32 | }catch(e){ 33 | console.log(e); 34 | console.log(e.stack); 35 | } 36 | 37 | 38 | }; 39 | 40 | /********************************************************************************************************************** 41 | * INITIAL DATA 42 | *********************************************************************************************************************/ 43 | 44 | /*Just initialises the structure of the database 45 | * database has a single table actions 46 | * */ 47 | exports.testAdminWrite1 = function(test){ 48 | $.when(test_utils.assert_admin_can_write("/", 49 | {"auctions":true}, test)).then(test.done); //todo empty actions 50 | }; 51 | 52 | /** 53 | * You should not be able to initialize an action without being the seller 54 | * @param test 55 | */ 56 | exports.testInitializationWrongSeller = function(test){ 57 | $.when(test_utils.assert_cant_write("eric", "/auctions/1", 58 | { 59 | name:"car", 60 | seller:"joe", 61 | modified:firebase.ServerValue.TIMESTAMP, 62 | state:"SELLING", 63 | signal:"sell" 64 | }, test)).then(test.done); 65 | }; 66 | 67 | /** 68 | * You should be able to initialize an action if the seller with the correct timestamp 69 | * @param test 70 | */ 71 | exports.testInitializationWithEffect = function(test){ 72 | $.when(test_utils.assert_can_write("eric", "/auctions/1", 73 | { 74 | name:"car", 75 | seller:"eric", 76 | modified:firebase.ServerValue.TIMESTAMP, 77 | state:"SELLING", 78 | signal:"SELL" 79 | }, test)).then(test.done); 80 | }; 81 | 82 | 83 | /*Clear database 84 | * */ 85 | exports.testAdminWrite2 = function(test){ 86 | $.when(test_utils.assert_admin_can_write("/", 87 | {"auctions":true}, test)).then(test.done); //todo empty actions 88 | }; 89 | 90 | 91 | /** 92 | * You should be not be able to call the parametrized version of Sell if you omit the parameter instantiation 93 | * @param test 94 | */ 95 | exports.testInitializationFailWithExecute = function(test){ 96 | $.when(test_utils.assert_cant_write("eric", "/auctions/1", 97 | { 98 | name:"car", 99 | seller:"eric", 100 | modified:firebase.ServerValue.TIMESTAMP, 101 | state:"SELLING", 102 | signal:"SELL(item_name)" 103 | }, test)).then(test.done); 104 | }; 105 | 106 | /** 107 | * You should be able to call the parametrized version of Sell if you include the parameter instantiation 108 | * @param test 109 | */ 110 | exports.testInitializationWithExecute = function(test){ 111 | $.when(test_utils.assert_can_write("eric", "/auctions/1", 112 | { 113 | name:"car", 114 | seller:"eric", 115 | modified:firebase.ServerValue.TIMESTAMP, 116 | state:"SELLING", 117 | signal:"SELL(item_name)", 118 | item_name:"car" 119 | }, test)).then(test.done); 120 | }; 121 | 122 | /*Clear database 123 | * */ 124 | exports.testAdminWrite3 = function(test){ 125 | $.when(test_utils.assert_admin_can_write("/", 126 | {"auctions":true}, test)).then(test.done); //todo empty actions 127 | }; -------------------------------------------------------------------------------- /test/authentication_test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A complete test of permissions and authentication 3 | * The first test uploads the rules (based on API documentation: https://www.firebase.com/docs/security/security-rules.html) 4 | "rules":{ 5 | "users":{ 6 | "$user":{ 7 | ".read" :"$user == auth.username", 8 | ".write":"$user == auth.username", 9 | "public":{ 10 | ".read":true 11 | } 12 | } 13 | } 14 | } 15 | * 16 | * It then tests admin can ignore rules by loading test data 17 | * Two people with different values in public and private areas 18 | * users: { 19 | bill:{private:1, public:2}, 20 | ted :{private:3, public:4} 21 | } 22 | * 23 | * The test suite then tests bill can't read teds private data etc. for all classes of authenticated user & permission combinations 24 | */ 25 | 26 | 27 | /** 28 | * Send the hand crafted rules to Firebase, important this occurs first to setup test suite with the rules we want to test 29 | */ 30 | exports.testWriteAuthenticationRulesValid = function(test){ 31 | var firebase_io = require('../src/firebase_io.js'); 32 | var $ = require('jquery-deferred'); 33 | var fs = require('fs'); 34 | 35 | //standard user rules, only authenticated user can read and write to their account, 36 | //unless the data is in the public portion of their account 37 | var rules = '{'+ 38 | ' "rules": {'+ 39 | ' "users": {'+ 40 | ' "$user": {'+ 41 | ' ".read": "$user == auth.username",'+ 42 | ' ".write": "$user == auth.username",'+ 43 | ' "public":{".read":true}'+ 44 | ' }'+ 45 | ' }'+ 46 | ' }'+ 47 | '}'; 48 | 49 | $.when(firebase_io.setValidationRules(rules)) 50 | .then(function(){ 51 | test.ok(true, "these rules should have been accepted"); 52 | test.done(); 53 | },function(error){ //deferred error handler should not be called 54 | test.ok(false, "these rules should not have been rejected"); 55 | test.done(); 56 | }); 57 | }; 58 | 59 | /** 60 | * tests that admins ignore normal write rules, by creating two users, bill and ted, with both secret and public numbers 61 | * @param test 62 | */ 63 | exports.testAdminWrite = function(test){ 64 | var test_utils = require("../test/test_utils.js"); 65 | var $ = require('jquery-deferred'); 66 | 67 | $.when(test_utils.assert_admin_can_write("/", 68 | {users:{ 69 | bill:{private:1, public:2}, 70 | ted :{private:3, public:4} 71 | }}, test)).then(test.done); 72 | }; 73 | 74 | /** 75 | * tests that unlogged in people can't write anything 76 | * @param test 77 | */ 78 | exports.testNonAdminCantWrite = function(test){ 79 | var firebase_io = require('../src/firebase_io.js'); 80 | 81 | //logout 82 | firebase_io.sandbox.unauth(); 83 | 84 | firebase_io.sandbox.child("users").set({ 85 | bill:{private:1, public:2}, 86 | ted :{private:3, public:4} 87 | }, function(error){ 88 | test.ok(error!=null, "there should be a permission error but there isn't"); 89 | test.done(); 90 | }); 91 | }; 92 | 93 | /** 94 | * tests that a logged in user can read their own data 95 | * @param test 96 | */ 97 | exports.testAuthReadPrivate = function(test){ 98 | var test_utils = require("../test/test_utils.js"); 99 | var $ = require('jquery-deferred'); 100 | 101 | $.when(test_utils.assert_can_read("bill", "users/bill/private", 1, test)) 102 | .then(test.done); 103 | }; 104 | 105 | /** 106 | * tests that a logged in user cant read another's private data 107 | * @param test 108 | */ 109 | exports.testAuthCantReadPrivate = function(test){ 110 | var test_utils = require("../test/test_utils.js"); 111 | var $ = require('jquery-deferred'); 112 | 113 | $.when(test_utils.assert_cant_read("bill", "users/ted/private", test)) 114 | .then(test.done); 115 | }; 116 | 117 | /** 118 | * tests that a logged in user can read another persons public data 119 | * @param test 120 | */ 121 | exports.testAuthReadPublic = function(test){ 122 | var test_utils = require("../test/test_utils.js"); 123 | var $ = require('jquery-deferred'); 124 | 125 | $.when(test_utils.assert_can_read("bill", "users/ted/public", 4, test)) 126 | .then(test.done); 127 | }; 128 | 129 | /** 130 | * tests that a logged in user can write to their private data 131 | * @param test 132 | */ 133 | exports.testAuthWritePrivate = function(test){ 134 | var test_utils = require("../test/test_utils.js"); 135 | var $ = require('jquery-deferred'); 136 | 137 | $.when(test_utils.assert_can_write("bill", "users/bill/private", 1, test)) 138 | .then(test.done); 139 | }; 140 | 141 | /** 142 | * tests that a logged in user can write to another's public data 143 | * @param test 144 | */ 145 | exports.testAuthCantWritePublic = function(test){ 146 | var test_utils = require("../test/test_utils.js"); 147 | var $ = require('jquery-deferred'); 148 | 149 | $.when(test_utils.assert_cant_write("ted", "users/bill/public", 1, test)) 150 | .then(test.done); 151 | }; 152 | 153 | -------------------------------------------------------------------------------- /test/closure_test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Testing functionality related to Google closure compiler 3 | */ 4 | 5 | var compiler = require("closure-compiler"); 6 | var fs = require('fs'); 7 | 8 | exports.call_closure = function(test){ 9 | var options = 10 | { 11 | compilation_level : 'ADVANCED_OPTIMIZATIONS', 12 | language_in : 'ECMASCRIPT5_STRICT', 13 | jscomp_error: "es5Strict" 14 | //create_source_map: "./test/assets/deadcodemap" 15 | }; 16 | 17 | function post_compile (err, stdout, stderr) { 18 | console.log(stderr); 19 | if (err){ 20 | test.ok(false); 21 | } 22 | var mycompiledcode = stdout; 23 | 24 | console.log(mycompiledcode); 25 | 26 | test.ok(true); 27 | test.done(); 28 | } 29 | 30 | compiler.compile(fs.readFileSync('./test/assets/deadcode.js'), options, post_compile) 31 | }; -------------------------------------------------------------------------------- /test/exhaustive_hsm_test.js: -------------------------------------------------------------------------------- 1 | /** 2 | *This test uses the same HSM topology described in "Practical UML statecharts in C/C++", by Miro Samek, to create 3 | * a test case that has every class of interesting transition in a HSM. 4 | * 5 | * We are not the only people to notice the value in this topology as a test case. We are reuseing the test case 6 | * developed by David Durman in his "statechart" library which replicates a similar topology. 7 | * 8 | * Thus we can use David's state machine as a controller for testing our implementation of the topology. 9 | */ 10 | 11 | Statechart = require('../lib/statechart.js'); 12 | _ = require('underscore'); 13 | 14 | 15 | 16 | /** 17 | * Samek exhaustive case machine taken out of David Durman's repository 18 | * statechart 19 | * @type {*} 20 | */ 21 | var machine = _.extend({ 22 | // slots 23 | myFoo: false, 24 | 25 | // machine 26 | initialState: "S0", 27 | 28 | states: { 29 | S0: { 30 | init: "S1", 31 | "E": { target: "S211" }, 32 | states: { 33 | S1: { 34 | init: "S11", 35 | "A": { target: "S1" }, 36 | "B": { target: "S11" }, 37 | "C": { target: "S2" }, 38 | "D": { target: "S0" }, 39 | "F": { target: "S211" }, 40 | states: { 41 | S11: { 42 | "G": { target: "S211" }, 43 | "H": { 44 | guard: function() { return this.myFoo; }, 45 | action: function() { this.myFoo = false; } 46 | } 47 | } 48 | } 49 | }, 50 | S2: { 51 | init: "S21", 52 | "C": { target: "S1" }, 53 | "F": { target: "S11" }, 54 | states: { 55 | S21: { 56 | init: "S211", 57 | "B": { target: "S211" }, 58 | "H": { 59 | guard: function() { return !this.myFoo; }, 60 | action: function() { this.myFoo = true; }, 61 | target: "S21" 62 | }, 63 | states: { 64 | S211: { 65 | "D": { target: "S21" }, 66 | "G": { target: "S0" } 67 | } 68 | } 69 | } 70 | } 71 | } 72 | } 73 | } 74 | } 75 | 76 | }, Statechart); 77 | 78 | 79 | /** 80 | * Load our version of this statemachine into Firebase 81 | */ 82 | exports.testLoadConvertedHSM = function(test){ 83 | var converter = require('../src/hsm_to_rules.js'); 84 | var $ = require('jquery-deferred'); 85 | var fs = require('fs'); 86 | var firebase_io = require('../src/firebase_io.js'); 87 | 88 | //load hsm rules from file 89 | var hsm_def = fs.readFileSync("./models/exhaustive.hsm", "utf8"); 90 | 91 | //transform hsm into rules 92 | try { 93 | var rules = converter.convert(hsm_def); 94 | console.log("\n", rules); 95 | test.ok(true, "rules did not convert ok"); 96 | 97 | //upload to server 98 | $.when(firebase_io.setValidationRules(rules)).then(function(){ 99 | test.ok(true, "rules did not upload"); 100 | test.done() 101 | },function(error){ 102 | test.ok(false, "rules did not upload"); 103 | test.done() 104 | }); 105 | 106 | }catch(e){ 107 | console.log("\n", e.message); 108 | console.log("\n", e.stack); 109 | test.ok(false, "should not have errors"); 110 | test.done() 111 | } 112 | }; 113 | 114 | 115 | 116 | /** 117 | * clear database 118 | */ 119 | exports.testAdminWrite = function(test){ 120 | var test_utils = require("../test/test_utils.js"); 121 | var $ = require('jquery-deferred'); 122 | 123 | $.when(test_utils.assert_admin_can_write("/",{}, test)).then(test.done); 124 | }; 125 | 126 | /** 127 | * test initial condition goes in according to spec 128 | * @param test 129 | */ 130 | exports.testInitialize = function(test){ 131 | var test_utils = require("../test/test_utils.js"); 132 | var $ = require('jquery-deferred'); 133 | 134 | //use HSM from statechart to determine first state 135 | var test_machine = _.clone(machine); 136 | test_machine.run(); 137 | var initial_state = test_machine.myState.name; 138 | 139 | console.log("\ninitial_state=", initial_state); 140 | $.when(test_utils.assert_can_write("anybody", "/",{ 141 | state:initial_state, 142 | foo:false 143 | }, test)).then(test.done); 144 | }; 145 | 146 | /** 147 | * tests all outgoing states from S11 match implementation 148 | * @param test 149 | */ 150 | exports.testS11FooFalseTransition = function(test){ 151 | var test_utils = require("../test/test_utils.js"); 152 | var $ = require('jquery-deferred'); 153 | 154 | var signals = ["A", "B", "C", "D", "E", "F", "G", "H"]; 155 | var valids = [true,true,true,true,true,true,true,false]; 156 | 157 | var test_tail = $.Deferred().resolve(); 158 | 159 | for(var signal_id in signals){ 160 | var signal = signals[signal_id]; 161 | var valid = valids[signal_id]; 162 | 163 | //machine starts in S11 164 | var test_machine = _.clone(machine); test_machine.run(); 165 | test_machine.dispatch(signal);//see what happens next 166 | var end_state = test_machine.myState.name; 167 | 168 | //we chain our test functions to they run in serial 169 | test_tail = test_tail.then(getTransitionTestFun( 170 | { 171 | state:"S11", 172 | foo:false 173 | },{ 174 | state:end_state, 175 | foo:false, 176 | signal: signal 177 | },valid, 178 | "\ntest S11 -> signal: " + signal + " state: " + test_machine.myState.name + " foo: " + test_machine.myFoo, 179 | test 180 | )); 181 | } 182 | //finally the test is over once all tests have run 183 | test_tail.then(test.done); 184 | }; 185 | 186 | /** 187 | * tests all outgoing states from S11 match implementation 188 | * @param test 189 | */ 190 | exports.testS211FooFalseTransition = function(test){ 191 | var test_utils = require("../test/test_utils.js"); 192 | var $ = require('jquery-deferred'); 193 | 194 | var signals = ["A", "B", "C", "D", "E", "F", "G", "H"]; 195 | var valids = [false,true,true,true,true,true,true,true]; 196 | 197 | var test_tail = $.Deferred().resolve(); 198 | 199 | for(var signal_id in signals){ 200 | var signal = signals[signal_id]; 201 | var valid = valids[signal_id]; 202 | 203 | //machine starts in S11 204 | var test_machine = _.clone(machine); test_machine.run(); 205 | test_machine.dispatch("C"); //goes to state S211 206 | test_machine.dispatch(signal);//see what happens next 207 | 208 | //we chain our test functions to they run in serial 209 | test_tail = test_tail.then(getTransitionTestFun( 210 | { 211 | state:"S211", 212 | foo:false 213 | },{ 214 | state:test_machine.myState.name, 215 | foo:test_machine.myFoo, 216 | signal: signal 217 | }, 218 | valid, 219 | "\ntest S211 -> signal: " + signal + " state: " + test_machine.myState.name + " foo: " + test_machine.myFoo 220 | ,test 221 | )); 222 | } 223 | //finally the test is over once all tests have run 224 | test_tail.then(test.done); 225 | }; 226 | 227 | /** 228 | * tests all outgoing states from S11 with foo true match implementation 229 | * @param test 230 | */ 231 | exports.testS211FooTrueTransition = function(test){ 232 | var test_utils = require("../test/test_utils.js"); 233 | var $ = require('jquery-deferred'); 234 | 235 | var signals = ["A", "B", "C", "D", "E", "F", "G", "H"]; 236 | var valids = [false,true,true,true,true,true,true,false]; 237 | 238 | var test_tail = $.Deferred().resolve(); 239 | 240 | for(var signal_id in signals){ 241 | var signal = signals[signal_id]; 242 | var valid = valids[signal_id]; 243 | 244 | //machine starts in S11 245 | var test_machine = _.clone(machine); test_machine.run(); 246 | test_machine.dispatch("C"); //goes to state S211 247 | test_machine.dispatch("H"); //goes to state S211, foo == true 248 | test_machine.dispatch(signal);//see what happens next 249 | 250 | //we chain our test functions to they run in serial 251 | test_tail = test_tail.then(getTransitionTestFun( 252 | { 253 | state:"S211", 254 | foo:true 255 | },{ 256 | state:test_machine.myState.name, 257 | foo:test_machine.myFoo, 258 | signal: signal 259 | }, 260 | valid, 261 | "\ntest S211 -> signal: " + signal + " state: " + test_machine.myState.name + " foo: " + test_machine.myFoo 262 | ,test 263 | )); 264 | } 265 | //finally the test is over once all tests have run 266 | test_tail.then(test.done); 267 | }; 268 | 269 | /** 270 | * tests all outgoing states from S11 with foo true match implementation 271 | * @param test 272 | */ 273 | exports.testS11FooTrueTransition = function(test){ 274 | var test_utils = require("../test/test_utils.js"); 275 | var $ = require('jquery-deferred'); 276 | 277 | var signals = ["A", "B", "C", "D", "E", "F", "G", "H"]; 278 | var valids = [true,true,true,true,true,true,true,true]; 279 | 280 | var test_tail = $.Deferred().resolve(); 281 | 282 | for(var signal_id in signals){ 283 | var signal = signals[signal_id]; 284 | var valid = valids[signal_id]; 285 | 286 | //machine starts in S11 287 | var test_machine = _.clone(machine); test_machine.run(); 288 | test_machine.dispatch("C"); //goes to state S211 289 | test_machine.dispatch("H"); //goes to state S211, foo == true 290 | test_machine.dispatch("C"); //goes to state S11, foo == true 291 | test.ok(test_machine.myFoo, "foo should be true"); 292 | test.ok(test_machine.myState.name==="S11", "state should be S11"); 293 | test_machine.dispatch(signal);//see what happens next 294 | 295 | //we chain our test functions to they run in serial 296 | test_tail = test_tail.then(getTransitionTestFun( 297 | { 298 | state:"S11", 299 | foo:true 300 | },{ 301 | state:test_machine.myState.name, 302 | foo:test_machine.myFoo, 303 | signal: signal 304 | }, 305 | valid, 306 | "\ntest S11 -> signal: " + signal + " state: " + test_machine.myState.name + " foo: " + test_machine.myFoo 307 | ,test 308 | )); 309 | } 310 | //finally the test is over once all tests have run 311 | test_tail.then(test.done); 312 | }; 313 | 314 | 315 | /** 316 | * creates a function, that tests from the initial state, can you get to the end_state? 317 | * the valid parameter sets whether permission shoudl be allowed or not 318 | * a msg field allow you to decide to debug message anything 319 | * the function returned works of deferred, so you can chain it in "then"s 320 | */ 321 | var getTransitionTestFun = function(init_state, end_state, valid, msg, test){ 322 | var test_utils = require("../test/test_utils.js"); 323 | var $ = require('jquery-deferred'); 324 | 325 | return function(){ 326 | var test_case = $.Deferred(); 327 | $.when(test_utils.assert_admin_can_write("/", 328 | init_state, test)).then(function(){ 329 | console.log(msg); 330 | if(valid){ 331 | $.when(test_utils.assert_can_write("anybody", "/", 332 | end_state, test)).then(function(){ 333 | test_case.resolve(); 334 | }); 335 | }else{ 336 | $.when(test_utils.assert_cant_write("anybody", "/", 337 | end_state, test)).then(function(){ 338 | test_case.resolve(); 339 | }); 340 | } 341 | 342 | }); 343 | return test_case; 344 | } 345 | }; 346 | 347 | 348 | 349 | -------------------------------------------------------------------------------- /test/firebase_test.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Test we can read previously written rules. 4 | * We write rules with some random elementsw to test that exact phrase comes back 5 | */ 6 | exports.testReadRules = function(test){ 7 | var firebase_io = require('../src/firebase_io.js'); 8 | var $ = require('jquery-deferred'); 9 | var random = Math.random(); //random element to double check our rules are really written 10 | 11 | //note, Firebase reformats rule's white space, 12 | //so we have to be carfull, the formatter might change unexpectadly Firebase end 13 | var rules = '{ "rules": { ".read": "data.val() == '+random+'"} }'; 14 | 15 | //1st we write our randomly seeded rules 16 | $.when(firebase_io.setValidationRules(rules)) 17 | .then(function(){ 18 | test.ok(true, "these rules should not have been accepted"); 19 | //once that is ok we read the current rules 20 | $.when(firebase_io.getValidationRules()).then(function(data){ 21 | test.equals(data, rules); //check the rules match! 22 | test.done(); 23 | }, function(error){ 24 | test.ok(false, "something when wrong with response to getValidationRules"); 25 | test.done(); 26 | }); 27 | },function(error){ 28 | test.ok(false, "these rules should not have been rejected"); 29 | test.done(); 30 | }); 31 | }; 32 | 33 | /** 34 | * Test valid rules can be written to Firebase, this also resets the permissions to read all for subsequent testing 35 | */ 36 | exports.testWriteRulesValid = function(test){ 37 | var firebase_io = require('../src/firebase_io.js'); 38 | var $ = require('jquery-deferred'); 39 | 40 | $.when(firebase_io.setValidationRules('{ "rules": { ".read": true } }')) 41 | .then(function(){ 42 | test.ok(true, "these rules should have been accepted"); 43 | test.done(); 44 | },function(error){ //deferred error handler should not be called 45 | test.ok(false, "these rules should not have been rejected"); 46 | test.done(); 47 | }); 48 | }; 49 | 50 | 51 | /** 52 | * This connects to the Firebase provided test firebase and prints out its root element 53 | * 54 | */ 55 | exports.testRootRef = function(test){ 56 | var Firebase = require('firebase'); 57 | var rootRef = new Firebase('https://myprojectname.firebaseIO-demo.com/'); 58 | 59 | rootRef.once('value', function(snapshot) { 60 | console.log('\nfirebase root value is ' + snapshot.val()); 61 | test.done(); 62 | }); 63 | }; 64 | 65 | /** 66 | * This connects to our projects sandbox Firebase and prints the root element 67 | * 68 | */ 69 | exports.testSandboxRootRef = function(test){ 70 | var firebase_io = require('../src/firebase_io.js'); 71 | 72 | firebase_io.sandbox.once('value', function(snapshot) { 73 | console.log('\nsandbox root value is ' + snapshot.val()); 74 | test.ok(true, "the API should at least returned"); 75 | test.done(); 76 | }); 77 | }; 78 | 79 | 80 | /** 81 | * Test invalid rules are rejected by Firebase 82 | */ 83 | exports.testWriteRulesInvalid = function(test){ 84 | var firebase_io = require('../src/firebase_io.js'); 85 | var $ = require('jquery-deferred'); 86 | 87 | $.when(firebase_io.setValidationRules('{ "rules": { ".read": true')) 88 | .then(function(){ 89 | test.ok(false, "these rules should not have been accepted"); 90 | test.done(); 91 | },function(error){//deferred error handler should be called 92 | test.ok(true, "these rules should have been rejected"); 93 | test.done(); 94 | }); 95 | }; 96 | 97 | 98 | -------------------------------------------------------------------------------- /test/hsm_to_rules_test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test we create send_item.rules from send_item.hsm and upload to server (which checks syntax) 3 | */ 4 | exports.testSendItemConversion = function(test){ 5 | var converter = require('../src/hsm_to_rules.js'); 6 | var $ = require('jquery-deferred'); 7 | var fs = require('fs'); 8 | var firebase_io = require('../src/firebase_io.js'); 9 | 10 | //load hsm rules from file 11 | var hsm_def = fs.readFileSync("./models/send_item.hsm", "utf8"); 12 | 13 | //transform hsm into rules 14 | try { 15 | var rules = converter.convert(hsm_def); 16 | console.log("\n",rules); 17 | 18 | test.ok(true, "rules did not convert ok"); 19 | 20 | //upload to server 21 | $.when(firebase_io.setValidationRules(rules)).then(function(){ 22 | test.ok(true, "rules did not upload"); 23 | test.done() 24 | },function(error){ 25 | test.ok(false, "rules did not upload"); 26 | test.done() 27 | }); 28 | 29 | }catch(e){ 30 | console.log("\n", e) 31 | console.log("\n", e.message); 32 | console.log("\n", e.stack); 33 | test.ok(false, "should not have errors"); 34 | test.done() 35 | } 36 | }; -------------------------------------------------------------------------------- /test/injection_test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Tests certain weird things 3 | */ 4 | 5 | 6 | /********************************************************************************************************************** 7 | * INITIAL RULES 8 | ********************************************************************************************************************* 9 | * Send the hand crafted rules to Firebase, important this occurs first to setup test suite with the rules we want to test 10 | */ 11 | exports.testWriteSendXRulesValid = function(test){ 12 | var firebase_io = require('../src/firebase_io.js'); 13 | var $ = require('jquery-deferred'); 14 | var fs = require('fs'); 15 | 16 | var rules = fs.readFileSync("./models/injection.rules", "utf8"); 17 | 18 | $.when(firebase_io.setValidationRules(rules)) 19 | .then(function(){ 20 | test.ok(true, "these rules should have been accepted"); 21 | test.done(); 22 | },function(error){ //deferred error handler should not be called 23 | test.ok(false, "these rules should not have been rejected"); 24 | test.done(); 25 | }); 26 | }; 27 | 28 | /********************************************************************************************************************** 29 | * INITIAL DATA 30 | *********************************************************************************************************************/ 31 | 32 | /** 33 | * tests you can't put weird keys in data base 34 | * @param test 35 | */ 36 | /* 37 | exports.testAdminUnescapedKeysWrite = function(test){ 38 | try{ 39 | var test_utils = require("../test/test_utils.js"); 40 | var $ = require('jquery-deferred'); 41 | var fs = require('fs'); 42 | 43 | var data = JSON.parse(fs.readFileSync("./models/injection.data", "utf8")); 44 | $.when(test_utils.assert_admin_can_write("/", data, test)).then(test.done, test.done); 45 | }catch(e){ 46 | test.done() 47 | } 48 | }; 49 | */ 50 | 51 | /** 52 | * tests you can put weird values in data base 53 | * @param test 54 | */ 55 | exports.testAdminUnescapedValueWrite = function(test){ 56 | try{ 57 | var test_utils = require("../test/test_utils.js"); 58 | var $ = require('jquery-deferred'); 59 | var fs = require('fs'); 60 | 61 | var data = JSON.parse(fs.readFileSync("./models/injection.data", "utf8")); 62 | $.when(test_utils.assert_admin_can_write("/", { 63 | "key":"fred/secret" 64 | }, test)).then(test.done, test.done); 65 | }catch(e){ 66 | test.done() 67 | } 68 | }; -------------------------------------------------------------------------------- /test/send_item_test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test suite for hand-written validation rules for safe sending of an item from a sender to a receiver user 3 | * 4 | * We do a normal trade flow, with attacks a we go, and store the firebase into certain checkpoints 5 | * We then roll back to the state at the checkpoints to investigate alternative execution orders 6 | */ 7 | 8 | 9 | var firebase_io = require('../src/firebase_io.js'); 10 | var test_utils = require("../test/test_utils.js"); 11 | var $ = require('jquery-deferred'); 12 | var fs = require('fs'); 13 | 14 | //the important strictly ordered stages in executing a trade 15 | var IDLE_IDLE_checkpoint, 16 | TX_IDLE_checkpoint, 17 | TX_RX_checkpoint, 18 | ACK_TX_RX_checkpoint, 19 | ACK_TX_ACK_RX_checkpoint; //after double ack either party should be able to update their inventory, so ordering back to IDLE is indeterminate 20 | 21 | 22 | /********************************************************************************************************************** 23 | * INITIAL RULES 24 | ********************************************************************************************************************* 25 | * Send the hand crafted rules to Firebase, important this occurs first to setup test suite with the rules we want to test 26 | */ 27 | exports.testWriteSendXRulesValid = function(test){ 28 | //load rules as hsm file 29 | var hsm_def = fs.readFileSync("./models/send_item.hsm", "utf8"); 30 | 31 | //convert into normal rules 32 | var converter = require('../src/hsm_to_rules.js'); 33 | var rules = converter.convert(hsm_def); 34 | 35 | $.when(firebase_io.setValidationRules(rules)) 36 | .then(function(){ 37 | 38 | fs.writeFileSync("./models/send_item.rules", rules); 39 | 40 | test.ok(true, "these rules should have been accepted"); 41 | test.done(); 42 | },function(error){ //deferred error handler should not be called 43 | test.ok(false, "these rules should not have been rejected"); 44 | test.done(); 45 | }); 46 | }; 47 | 48 | /********************************************************************************************************************** 49 | * INITIAL DATA 50 | *********************************************************************************************************************/ 51 | 52 | /** 53 | * tests that admins ignore normal write rules, by creating two users, sender and receiver, 54 | * sender is setup ready to send 55 | * receiver is not setup and is there to test initialization 56 | * @param test 57 | */ 58 | exports.testAdminWrite = function(test){ 59 | $.when(test_utils.assert_admin_can_write("/", 60 | {users:{ 61 | sender:{ 62 | state:"IDLE", 63 | item :"GOLD" 64 | }, 65 | receiver :{ 66 | 67 | } 68 | }}, test)).then(test.done); 69 | }; 70 | 71 | /********************************************************************************************************************** 72 | * INITIALIZATION TESTS 73 | *********************************************************************************************************************/ 74 | 75 | /** 76 | * You should not be able to initialize the players file with anything other than an empty inventory 77 | * @param test 78 | */ 79 | exports.testInitializationInvalidFail = function(test){ 80 | $.when(test_utils.assert_cant_write("receiver", "/users/receiver", 81 | { 82 | state:"IDLE", 83 | item :"GOLD" 84 | }, test)).then(test.done); 85 | }; 86 | 87 | /** 88 | * You should able to initialize the a player if they don't have a state 89 | * @param test 90 | */ 91 | exports.testInitialization = function(test){ 92 | $.when(test_utils.assert_can_write("receiver", "/users/receiver", 93 | { 94 | state:"IDLE" 95 | }, test)).then(test.done); 96 | }; 97 | 98 | /** 99 | * You can't initialize twice 100 | * @param test 101 | */ 102 | exports.testReInitializationFail = function(test){ 103 | $.when(test_utils.assert_cant_write("receiver", "/users/receiver", 104 | { 105 | state:"IDLE" 106 | }, test)).then(test.done); 107 | }; 108 | 109 | /********************************************************************************************************************** 110 | * BOTH PLAYERS ARE INITIALIZED 111 | *********************************************************************************************************************/ 112 | exports.testIDLE_IDLE_checkpoint = function(test){ 113 | $.when(IDLE_IDLE_checkpoint = test_utils.checkpoint(test)).then(function(){ 114 | test.done(); 115 | }); 116 | }; 117 | 118 | /** 119 | * Tests the sender can't miss important information when sending 120 | * @param test 121 | */ 122 | exports.testSendIncompleteFail = function(test){ 123 | $.when(test_utils.assert_cant_write("sender", "/users/sender", 124 | { 125 | state:"TX" 126 | }, test)).then(test.done); 127 | }; 128 | 129 | /** 130 | * Test the sender can't try and send an item they do not own 131 | * @param test 132 | */ 133 | exports.testSendSwitchItemFail = function(test){ 134 | $.when(test_utils.assert_cant_write("sender", "/users/sender", 135 | { 136 | state:"TX", 137 | item:null, 138 | tx_itm:"XXX",//don't own! 139 | tx_ptr:"receiver" 140 | }, test)).then(test.done); 141 | }; 142 | 143 | /** 144 | * Test the sender can't try and backup an item they do not own 145 | * @param test 146 | */ 147 | exports.testSendWrongAddressFail = function(test){ 148 | $.when(test_utils.assert_cant_write("sender", "/users/sender", 149 | { 150 | state:"TX", 151 | item:null, 152 | tx_itm:"GOLD", 153 | tx_ptr:"fsdfs" //not a user! 154 | }, test)).then(test.done); 155 | }; 156 | 157 | /** 158 | * Test a correctly formed rx transition doesn't work before the sender is ready 159 | * @param test 160 | */ 161 | exports.testReceiveOutOfOrderFail = function(test){ 162 | $.when(test_utils.assert_cant_write("receiver", "/users/receiver", 163 | { 164 | state:"RX", 165 | rx_itm:"GOLD", 166 | rx_ptr:"sender" 167 | }, test)).then(test.done); 168 | }; 169 | 170 | 171 | /** 172 | * Test a correctly formed sender transition cannot be transitioned by an incorrect user 173 | * @param test 174 | */ 175 | exports.testSendWrongUserFail = function(test){ 176 | $.when(test_utils.assert_cant_write("receiver", "/users/sender", //wrong user 177 | { 178 | state:"TX", 179 | item:null, 180 | tx_itm:"GOLD", 181 | tx_ptr:"receiver" 182 | }, test)).then(test.done); 183 | }; 184 | 185 | /** 186 | * Test user can't sneak an object in during transition to TX 187 | * @param test 188 | */ 189 | exports.testSendInsertItemFail = function(test){ 190 | $.when(test_utils.assert_cant_write("sender", "/users/sender", 191 | { 192 | state:"TX", 193 | item:"GOLD", 194 | tx_itm:"GOLD", 195 | tx_ptr:"receiver" 196 | }, test)).then(test.done); 197 | }; 198 | 199 | /** 200 | * Test a correctly formed sender transition works 201 | * @param test 202 | */ 203 | exports.testSendTransition = function(test){ 204 | $.when(test_utils.assert_can_write("sender", "/users/sender", 205 | { 206 | state:"TX", 207 | item:null, 208 | tx_itm:"GOLD", 209 | tx_ptr:"receiver" 210 | }, test)).then(test.done); 211 | }; 212 | 213 | /********************************************************************************************************************** 214 | * SENDER IS READY TO SEND 215 | *********************************************************************************************************************/ 216 | exports.testTX_IDLE_checkpoint = function(test){ 217 | $.when(TX_IDLE_checkpoint = test_utils.checkpoint(test)).then(function(){ 218 | test.done(); 219 | }); 220 | }; 221 | 222 | /** 223 | * Test an object substitution fails on RX 224 | * @param test 225 | */ 226 | exports.testReceiveCheatFail = function(test){ 227 | $.when(test_utils.assert_cant_write("receiver", "/users/receiver", 228 | { 229 | state:"RX", 230 | rx_itm:"XXX", //wrong! 231 | rx_ptr:"sender" 232 | }, test)).then(test.done); 233 | }; 234 | 235 | /** 236 | * Test an object substitution fails on RX 237 | * @param test 238 | */ 239 | exports.testReceivePaddingRxFail = function(test){ 240 | $.when(test_utils.assert_cant_write("receiver", "/users/receiver", 241 | { 242 | state:"RX", 243 | rx_itm:"GOLD", 244 | rx_ptr:"sender", 245 | tx_ptr:"sender" //extra info 246 | }, test)).then(test.done); 247 | }; 248 | 249 | /** 250 | * Test an object substitution fails on RX 251 | * @param test 252 | */ 253 | exports.testReceivePaddingItemFail = function(test){ 254 | $.when(test_utils.assert_cant_write("receiver", "/users/receiver", 255 | { 256 | state:"RX", 257 | rx_itm:"GOLD", 258 | rx_ptr:"sender", 259 | item:"GOLD" //extra info 260 | }, test)).then(test.done); 261 | }; 262 | 263 | /** 264 | * Test a correctly formed ack_rx transition doesn't work before a receive 265 | * @param test 266 | */ 267 | exports.testAckRXOutOfOrderFail = function(test){ 268 | $.when(test_utils.assert_cant_write("sender", "/users/receiver", 269 | { 270 | state:"ACK_RX", 271 | rx_itm:"GOLD", 272 | rx_ptr:"sender" 273 | }, test)).then(test.done); 274 | }; 275 | 276 | /** 277 | * Test a user can't sneak it an alteration to tx while Rxing 278 | * @param test 279 | */ 280 | 281 | /* TODO: broken, need to fix lock code 282 | exports.testReceiveInsertTxFail = function(test){ 283 | var test_utils = require("../test/test_utils.js"); 284 | var $ = require('jquery-deferred'); 285 | 286 | $.when(test_utils.assert_cant_write("receiver", "/users/receiver", 287 | { 288 | state:"RX", 289 | rx_itm:"GOLD", 290 | tx_itm:"GOLD", 291 | rx_ptr:"sender" 292 | }, test)).then(test.done); 293 | };*/ 294 | 295 | /** 296 | * Test a correctly formed rx transition works 297 | * @param test 298 | */ 299 | exports.testReceiveTransition = function(test){ 300 | $.when(test_utils.assert_can_write("receiver", "/users/receiver", 301 | { 302 | state:"RX", 303 | rx_itm:"GOLD", 304 | rx_ptr:"sender" 305 | }, test)).then(test.done); 306 | }; 307 | 308 | /********************************************************************************************************************** 309 | * RECEIVER IS READY TO RECEIVE 310 | *********************************************************************************************************************/ 311 | exports.testTX_RX_checkpoint = function(test){ 312 | $.when(TX_RX_checkpoint = test_utils.checkpoint(test)).then(function(){ 313 | test.done(); 314 | }); 315 | }; 316 | 317 | /** 318 | * Test a empty ack_rx fails 319 | * (note "It's not a bug. Validate rules are only run for non-empty data new data." 320 | * Andrew Lee (Firebase Developer) 321 | * https://groups.google.com/forum/#!topic/firebase-talk/TbCK_zHyghg) 322 | * @param test 323 | */ 324 | exports.testAckRXEmptyFail = function(test){ 325 | $.when(test_utils.assert_cant_write("sender", "/users/receiver", 326 | { 327 | }, test)).then(test.done); 328 | }; 329 | 330 | /** 331 | * Test a incomplete formed ack_rx transition fails 332 | * @param test 333 | */ 334 | exports.testAckRXIncompleteFail = function(test){ 335 | $.when(test_utils.assert_cant_write("sender", "/users/receiver", 336 | { 337 | state:"ACK_RX", 338 | rx_itm:"GOLD" 339 | }, test)).then(test.done); 340 | }; 341 | 342 | /** 343 | * Test a correctly formed ack_rx can't be spoofed by receiver 344 | * @param test 345 | */ 346 | exports.testAckRXWrongUserFail = function(test){ 347 | $.when(test_utils.assert_cant_write("receiver", "/users/receiver", 348 | { 349 | state:"ACK_RX", 350 | rx_itm:"GOLD", 351 | rx_ptr:"sender" 352 | }, test)).then(test.done); 353 | }; 354 | 355 | 356 | /** 357 | * Test a correctly formed ack_rx transition doesn't work before RX ACK 358 | * @param test 359 | */ 360 | exports.testAckTXOutOfOrderFail = function(test){ 361 | $.when(test_utils.assert_cant_write("receiver", "/users/sender", 362 | { 363 | state:"ACK_TX", 364 | item:null, 365 | tx_itm:"GOLD", 366 | tx_ptr:"receiver" 367 | }, test)).then(test.done); 368 | }; 369 | 370 | /** 371 | * Test a correctly formed ack_rx transition works 372 | * @param test 373 | */ 374 | exports.testAckRXTransition = function(test){ 375 | $.when(test_utils.assert_can_write("sender", "/users/receiver", 376 | { 377 | state:"ACK_RX", 378 | rx_itm:"GOLD", 379 | rx_ptr:"sender" 380 | }, test)).then(test.done); 381 | }; 382 | 383 | /********************************************************************************************************************** 384 | * SENDER has ACK on receiver's data, next step if for receiver to ACK 385 | *********************************************************************************************************************/ 386 | exports.testACK_TX_RX_checkpoint = function(test){ 387 | $.when(ACK_TX_RX_checkpoint = test_utils.checkpoint(test)).then(function(){ 388 | test.done(); 389 | }); 390 | }; 391 | 392 | /** 393 | * Test we cant commit the Tx early 394 | * @param test 395 | */ 396 | exports.testCommitTxOutOfOrderFail = function(test){ 397 | $.when(test_utils.assert_cant_write("sender", "/users/sender", 398 | { 399 | state:"IDLE" //we have sent all our stuff 400 | }, test)).then(test.done); 401 | }; 402 | 403 | /** 404 | * Test we cant commit the Rx early 405 | * @param test 406 | */ 407 | exports.testCommitRxOutOfOrderFail = function(test){ 408 | $.when(test_utils.assert_cant_write("receiver", "/users/receiver", 409 | { 410 | state:"IDLE", 411 | item:"GOLD" 412 | }, test)).then(test.done); 413 | }; 414 | 415 | /** 416 | * Test a correctly formed ack_rx transition works 417 | * @param test 418 | */ 419 | exports.testAckTXTransition = function(test){ 420 | $.when(test_utils.assert_can_write("receiver", "/users/sender", 421 | { 422 | state:"ACK_TX", 423 | item:null, 424 | tx_itm:"GOLD", 425 | tx_ptr:"receiver" 426 | }, test)).then(test.done); 427 | }; 428 | 429 | /********************************************************************************************************************** 430 | * RECEIVER has ACK on sender's data, transaction is complete! Now each can go to the IDLE state after receiving goods 431 | *********************************************************************************************************************/ 432 | exports.testACK_TX_ACK_RX_checkpoint = function(test){ 433 | $.when(ACK_TX_ACK_RX_checkpoint = test_utils.checkpoint(test)).then(function(){ 434 | test.done(); 435 | }); 436 | }; 437 | 438 | /** 439 | * Test we can now null the senders inventory, the trade is complete for the sender 440 | * @param test 441 | */ 442 | exports.testCommitTxTransition = function(test){ 443 | $.when(test_utils.assert_can_write("sender", "/users/sender", 444 | { 445 | state:"IDLE" //we have sent all our stuff 446 | }, test)).then(test.done); 447 | }; 448 | 449 | /** 450 | * Test we can now add the item to the receiver's inventory, the trade is complete for the receiver 451 | * @param test 452 | */ 453 | exports.testCommitRxTransition = function(test){ 454 | $.when(test_utils.assert_can_write("receiver", "/users/receiver", 455 | { 456 | state:"IDLE", 457 | item:"GOLD" 458 | }, test)).then(test.done); 459 | }; 460 | 461 | 462 | /********************************************************************************************************************** 463 | * Normal Trade complete! 464 | *********************************************************************************************************************/ 465 | 466 | /** 467 | * double check the receiver has the gold and the sender doesn't 468 | * @param test 469 | */ 470 | exports.testTradeComplete0 = function(test){ 471 | $.when(test_utils.assert_can_read("receiver", "/users", 472 | { 473 | sender:{ 474 | state:"IDLE" 475 | }, 476 | receiver:{ 477 | state:"IDLE", 478 | item:"GOLD" 479 | } 480 | }, test)).then(test.done); 481 | }; 482 | 483 | /********************************************************************************************************************** 484 | * Alternative ending, lets check the trade is completed with the different users doing the final transactions 485 | *********************************************************************************************************************/ 486 | exports.testRestoreAlternativeEnding1Checkpoint = function(test){ 487 | $.when(test_utils.rollback(ACK_TX_ACK_RX_checkpoint, test)).then(test.done); 488 | }; 489 | 490 | 491 | /** 492 | * Test the Tx can be committed by the receiver instead of the sender 493 | * @param test 494 | */ 495 | exports.testCommitTxTransition1 = function(test){ 496 | $.when(test_utils.assert_can_write("receiver", "/users/sender", 497 | { 498 | state:"IDLE" //we have sent all our stuff 499 | }, test)).then(test.done); 500 | }; 501 | 502 | 503 | /** 504 | * Test the Rx can commit by the sender instead of the receiver 505 | * @param test 506 | */ 507 | exports.testCommitRxTransition1 = function(test){ 508 | $.when(test_utils.assert_can_write("sender", "/users/receiver", 509 | { 510 | state:"IDLE", 511 | item:"GOLD" 512 | }, test)).then(test.done); 513 | }; 514 | 515 | /** 516 | * double check the receiver has the gold and the sender doesn't 517 | * @param test 518 | */ 519 | exports.testTradeComplete1 = function(test){ 520 | $.when(test_utils.assert_can_read("receiver", "/users", 521 | { 522 | sender:{ 523 | state:"IDLE" 524 | }, 525 | receiver:{ 526 | state:"IDLE", 527 | item:"GOLD" 528 | } 529 | }, test)).then(test.done); 530 | }; -------------------------------------------------------------------------------- /test/test_utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A number of utilities to test read and write permissions for the sandbox firebase 3 | * These take a nodeunit test as a parameter, and assert a number of things that should work 4 | * They DO modify the firebase's data, so you can use their side effects to write specific data into the firebase and it 5 | * double checks the the transaction as it progresses 6 | * 7 | * A second utility is for the tests to checkpoint the firebase, so that it can be rolled back to previous states 8 | * 9 | * All functions return deferred objects as many of the methods are async 10 | * 11 | * read permissions also want the expected value in the read location 12 | */ 13 | 14 | 15 | /** 16 | * This tests that admin can write independent of read/write rules by writing to a specific location (can be /) 17 | * @param where the firebase path e.g. "/" for writing the whole firebase 18 | * @param value the value to put in the firebase e.g. {users:{tom:{..}, ...}} 19 | * @param test the nodeunit to check invariants 20 | * @return {*} Deferred object 21 | */ 22 | exports.assert_admin_can_write = function(where, value, test){ 23 | var $ = require('jquery-deferred'); 24 | var def = $.Deferred(); 25 | var firebase_io = require('../src/firebase_io.js'); 26 | 27 | $.when(firebase_io.loginAs("anAdmin", true)).then(function(){ 28 | firebase_io.sandbox.child(where).set(value, function(error){ 29 | test.ok(error==null, "there should not be an error but there was"); 30 | def.resolve(); 31 | }); 32 | }, function(error){ 33 | test.ok(false, "can't login"); 34 | def.resolve(); 35 | }); 36 | return def; 37 | }; 38 | 39 | 40 | exports.assert_can_read = function(who, where, expected, test){ 41 | var $ = require('jquery-deferred'); 42 | var def = $.Deferred(); 43 | var firebase_io = require('../src/firebase_io.js'); 44 | 45 | $.when(firebase_io.loginAs(who, false)).then(function(){ 46 | //user is logged in 47 | firebase_io.sandbox.child(where).once('value', function(data){ 48 | test.deepEqual(data.val(), expected); 49 | def.resolve(); 50 | }, function(error){ 51 | test.ok(error==null, "the set should be error free but isn't"); 52 | def.resolve(); 53 | }); 54 | }, function(error){ 55 | test.ok(false, "can't login"); 56 | def.resolve(); 57 | }); 58 | return def; 59 | }; 60 | 61 | exports.assert_cant_read = function(who, where, test){ 62 | var $ = require('jquery-deferred'); 63 | var def = $.Deferred(); 64 | var firebase_io = require('../src/firebase_io.js'); 65 | 66 | $.when(firebase_io.loginAs(who, false)).then(function(){ 67 | //user is logged in 68 | firebase_io.sandbox.child(where).once('value', function(data){ 69 | test.ok(false, "should not be able to read"); 70 | def.resolve(); 71 | }, function(error){ 72 | test.ok(error!=null, "there should be a permission error but there isn't"); 73 | def.resolve(); 74 | }); 75 | }, function(error){ 76 | test.ok(false, "can't login"); 77 | def.resolve(); 78 | }); 79 | return def; 80 | }; 81 | 82 | exports.assert_can_write = function(who, where, value, test){ 83 | var $ = require('jquery-deferred'); 84 | var def = $.Deferred(); 85 | var firebase_io = require('../src/firebase_io.js'); 86 | 87 | $.when(firebase_io.loginAs(who, false)).then(function(){ 88 | firebase_io.sandbox.child(where).set(value, function(error){ 89 | test.ok(error==null, "there should not be an error but there was"); 90 | def.resolve(); 91 | }); 92 | }, function(error){ 93 | test.ok(false, "can't login"); 94 | def.resolve(); 95 | }); 96 | return def; 97 | }; 98 | 99 | exports.assert_cant_write = function(who, where, value, test){ 100 | var $ = require('jquery-deferred'); 101 | var def = $.Deferred(); 102 | var firebase_io = require('../src/firebase_io.js'); 103 | 104 | $.when(firebase_io.loginAs(who, false)).then(function(){ 105 | firebase_io.sandbox.child(where).set(value, function(error){ 106 | test.ok(error!=null, "there should be an error but there was not"); 107 | def.resolve(); 108 | }); 109 | }, function(error){ 110 | test.ok(false, "can't login"); 111 | def.resolve(); 112 | }); 113 | return def; 114 | }; 115 | 116 | 117 | /** 118 | * makes and object that represents a snapshot of the current state of the firebase 119 | * pass this object to rollback later 120 | * @param test 121 | */ 122 | exports.checkpoint = function(test){ 123 | var $ = require('jquery-deferred'); 124 | var def = $.Deferred(); 125 | var firebase_io = require('../src/firebase_io.js'); 126 | 127 | $.when(firebase_io.loginAs("MrCheckpoint", true)).then(function(){ 128 | firebase_io.sandbox.once('value', function(data){ 129 | def.resolve(data); 130 | }, function(error){ 131 | test.ok(error==null, "the set should be error free but isn't"); 132 | def.reject(); 133 | }); 134 | }, function(error){ 135 | test.ok(false, "can't login"); 136 | def.reject(); 137 | }); 138 | return def; 139 | }; 140 | 141 | /** 142 | * rolls back the Firebase to an earlier checkpoint state 143 | * @param test 144 | */ 145 | exports.rollback = function(checkpoint, test){ 146 | var $ = require('jquery-deferred'); 147 | var def = $.Deferred(); 148 | 149 | $.when(checkpoint).then( 150 | function(data){ 151 | console.log("\ngot data: ", data.val()); 152 | $.when(exports.assert_admin_can_write("/", data.val(), test)) 153 | .then(def.resolve); 154 | },function(error){ 155 | test.ok(false, "could not read checkpoint data"); 156 | def.resolve(); 157 | }); 158 | return def; 159 | }; --------------------------------------------------------------------------------