123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323 |
- /* Import core modules. */
- const _ = require('lodash')
- const debug = require('debug')('shuffle:comm')
- const EventEmitter = require('events').EventEmitter
- const moment = require('moment')
- const WebSocket = require('ws')
- /* Import local modules. */
- const serverMessages = require('./serverMessages.js')
- /**
- * Communications Channel (Class)
- */
- class CommChannel extends EventEmitter {
- constructor (connectionOptions, shuffleRoundInstance) {
- super()
- /* Set instance properties. */
- for (let oneOption in connectionOptions) {
- this[oneOption] = connectionOptions[oneOption]
- }
- /* Set server URI. */
- this.serverUri = connectionOptions.serverUri
- /* Validate server URI. */
- if (!this.serverUri) {
- /* Set connection error. */
- const connectionError = new Error('BAD_SERVER_URI')
- /* Emit connection error. */
- this.emit('connectionError', connectionError)
- }
- /* Initialize Websocket client. */
- this._wsClient = undefined
- /* Initialize server messages. */
- this.msg = serverMessages
- /* Initialize shuffle round. */
- this.round = shuffleRoundInstance
- /* Initialize outbox. */
- // NOTE: Our internal records for sent and received server messages.
- // Used for debugging bad rounds.
- this.outbox = {
- sent: {}
- }
- /* Initialize inbox. */
- this.inbox = {}
- return this
- }
- /**
- * Connect
- *
- * Establish websockets connection with shuffle server.
- */
- async connect () {
- console.info('\nServer Connection', this.serverUri) // eslint-disable-line no-console
- // TODO: This and all communication functionality will be moved to
- // a separate class. The `Round` should only touch messages
- // after they have been parsed, validated, and classified.
- if (typeof window !== 'undefined') {
- /* eslint-disable-next-line no-undef */
- this._wsClient = new window.WebSocket(this.serverUri)
- } else {
- this._wsClient = new WebSocket(this.serverUri, {
- origin: 'http://localhost'
- })
- }
- /* Handle incoming message from CashShuffle server. */
- // this._wsClient.on('message', (someMessageBuffer) => {
- this._wsClient.addEventListener('message', async (someMessageBuffer) => {
- /* Set message. */
- const message = await this.msg.decodeAndClassify(someMessageBuffer)
- // debug('Handling websocket (message):', message)
- /* Initialize message sub-class. */
- let messageSubClass
- /* Validate message. */
- if (message.pruned.messageType === '_unicast') {
- /* Validate round phase. */
- if (this.round.phase.toLowerCase() === 'announcement') {
- messageSubClass = 'incomingEncryptedOutputs'
- } else {
- messageSubClass = 'UNKNOWN'
- }
- }
- /* Change the message type for unicast messages. */
- Object.assign(message.pruned, {
- messageType: messageSubClass || message.pruned.messageType
- })
- /* Add the message to our inbox, in case we need it later. */
- const inboxEntry = {
- messageType: message.pruned.messageType,
- time: new Date().getTime(),
- protobuffMessage: {
- unpacked: message.full,
- components: message.components
- }
- }
- this.inbox[message.pruned.messageType] =
- this.inbox[message.pruned.messageType] ? _.sortBy(
- this.inbox[message.pruned.messageType]
- .concat(
- [inboxEntry]), ['time'], ['desc']) : [inboxEntry]
- /* Initialize packet verification results. */
- const packetVerifyResults = {
- success: [],
- fail: []
- }
- if (message.packets[0].packet.fromKey) {
- /* Retrieve sender. */
- const sender = _.find(this.round.players, {
- verificationKey: message.packets[0].packet.fromKey.key
- })
- debug('Websocket message (this.round.players / sender):',
- this.round.players, sender)
- // debug('Checking signature for',
- // message.pruned.messageType.toUpperCase(),
- // 'message from',
- // (sender ? sender.session + ' ( ' + sender.verificationKey + ' ) ' : 'player with sessionid ' + message.pruned.message.session)
- // )
- }
- /* Loop through ALL message packets. */
- for (let onePacket of message.packets) {
- /* Validate signature. */
- if (onePacket.signature) {
- if (!this.msg.checkPacketSignature(onePacket)) {
- packetVerifyResults.fail.push(onePacket)
- } else {
- packetVerifyResults.success.push(onePacket)
- }
- } else {
- // The signature doesn't need to be verified.
- packetVerifyResults.success.push(onePacket)
- }
- }
- /* Validate packet results. */
- if (!packetVerifyResults.fail.length) {
- this.emit('serverMessage', message)
- } else {
- console.error('Signature check failed!') // eslint-disable-line no-console
- // This event will be piped right into the
- // `assignBlame` method on the `ShuffleClient`
- // class
- this.emit('protocolViolation', {
- reason: 'INVALIDSIGNATURE',
- // accused: _.get(oneSignedPacket, 'packet.fromKey.key'),
- accused: _.get(packetVerifyResults, 'packet.fromKey.key'),
- invalid: packetVerifyResults.fail
- })
- }
- })
- /* Handle a NEW Websockets connection with the CashShuffle server. */
- // this._wsClient.on('open', () => {
- this._wsClient.addEventListener('open', () => {
- // this._wsClient.onopen = () => {
- this._wsConnected = true
- // debug('We are now connected to the cashshuffle server:', this.serverUri)
- this.emit('connected', this._wsClient)
- })
- /* Handle closing the Websockets connection. */
- // this._wsClient.on('close', (details) => {
- this._wsClient.addEventListener('close', (details) => {
- if (!this.round.roundComplete) {
- // FIXME: Should we attempt to automatically reconnect
- // and restart the process??
- debug('Socket connection closed:', details)
- this.emit('disconnected', details)
- }
- })
- /* Handle Websockets errors. */
- // this._wsClient.on('error', (someError) => {
- this._wsClient.addEventListener('error', (someError) => {
- console.error('Socket error!', someError) // eslint-disable-line no-console
- this.emit('connectionError', someError)
- })
- }
- /**
- * Send Message
- */
- sendMessage () {
- /* Set message type. */
- const messageType = arguments[0]
- /* Set message parameters. */
- // const messageParams = [].slice.call(arguments, 1, ) // FIXME: Why this trailing space??
- const messageParams = [].slice.call(arguments, 1) // FIXME: Why this trailing space??
- // debug('Now sending message:', arguments,
- // 'type:', messageType,
- // 'params:', messageParams)
- /* Initialize packet message. */
- let packedMessage = null
- /* Validate message type. */
- if (messageType && typeof this.msg[messageType] === 'function') {
- try {
- packedMessage = this.msg[messageType].apply(this, messageParams)
- } catch (nope) {
- console.error( // eslint-disable-line no-console
- 'Couldnt create', messageType,
- 'message using params', messageParams,
- '\n', nope)
- // TODO: Throw exception?
- }
- } else {
- // TODO: Should we throw an exception now?
- }
- /* Add the message to our outbox, in case we need it later. */
- const outboxEntry = {
- messageType: messageType,
- time: new Date().getTime(),
- protobuffMessage: {
- // packed: packedMessage.packed.toString('base64'),
- unpacked: packedMessage.unpacked.toJSON(),
- components: packedMessage.components
- }
- }
- /* Validate outbox. */
- if (!this.outbox[messageType]) {
- /* Initialize object. */
- const obj = {}
- /* Initialize object's message type. */
- obj[messageType] = []
- /* Update outbox. */
- Object.assign(this.outbox, obj)
- }
- /* Set outbox sent message flag. */
- this.outbox.sent[messageType] = true
- /* Add entry to outbox. */
- this.outbox[messageType].push(outboxEntry)
- /* Send packed message. */
- this._wsClient.send(packedMessage.packed)
- }
- /**
- * Write Debug File
- */
- writeDebugFile () {
- /* Loop through inbox. */
- for (let oneKey in this.inbox) {
- if (_.isArray(this.inbox[oneKey])) {
- this.inbox[oneKey] = _.sortBy(this.inbox[oneKey], ['time'], ['desc'])
- }
- }
- /* Loop through outbox. */
- for (let oneKey in this.outbox) {
- if (_.isArray(this.outbox[oneKey])) {
- this.outbox[oneKey] = _.sortBy(this.outbox[oneKey], ['time'], ['desc'])
- }
- }
- /* Build data package (for disk). */
- const writeThisToDisk = {
- phase: this.round.phase,
- coin: this.round.coin,
- ephemeralKeypair: this.round.ephemeralKeypair,
- encryptionKeypair: this.round.encryptionKeypair,
- shuffled: this.round.shuffled,
- change: this.round.change,
- players: this.round.players,
- equivHashPlaintext: this.round.equivHashPlaintext,
- equivHash: this.round.equivHash,
- shuffleTx: {
- signatures: this.round.shuffleTx.signatures,
- hex: this.round.shuffleTx.hex,
- serialized: this.round.shuffleTx.tx ? this.round.shuffleTx.tx.toObject() : {},
- results: this.round.shuffleTx.results
- },
- inbox: this.inbox,
- outbox: this.outbox
- }
- /* Set data. */
- const data = JSON.stringify(writeThisToDisk, null, 2)
- if (typeof window !== 'undefined') {
- console.error(data) // eslint-disable-line no-console
- } else {
- /* Write data to disk. */
- require('fs').writeFileSync(
- `_failedShuffle-${moment().unix()}.js`,
- 'module.exports = ' + data
- )
- }
- /* Quit application. */
- process.exit(0)
- }
- }
- /* Export module. */
- module.exports = CommChannel
|