<commit_before>module.exports = function (_Firebase) {
  Firebase = _Firebase
  return Node
}

var debug = require('debug')('fireflower')
var events = require('events')
var inherits = require('inherits')
var SimplePeer = require('simple-peer')
var Firebase = null

var CONNECTION_TIMEOUT = 5000

inherits(Node, events.EventEmitter)

function Node (url, opts) {
  if (!(this instanceof Node)) {
    return new Node(url, opts)
  }

  this.url = url
  this.opts = opts || {}
  this.root = this.opts.root
  this.reportInterval = this.opts.reportInterval
  this.channelConfig = this.opts.channelConfig || {}
  this.config = {}
  this.state = 'disconnected'
  this.upstream = null
  this.downstream = {}

  // firebase refs
  this._ref = new Firebase(this.url)
  this._configRef = this._ref.child('configuration')
  this._requestsRef = this._ref.child('requests')
  this._reports = this._ref.child('reports')

  // set a random id if one was not provided
  this.id = this.opts.id || this._requestsRef.push().key()

  // bind callbacks
  this._onconfig = this._onconfig.bind(this)
  this._doconnect = this._doconnect.bind(this)
  this._onrequest = this._onrequest.bind(this)
  this._onresponse = this._onresponse.bind(this)
  this._onmaskupdate = this._onmaskupdate.bind(this)
  this._onreportNeeded = this._onreportNeeded.bind(this)

  events.EventEmitter.call(this)
}

Node.prototype.connect = function () {
  if (this.state !== 'disconnected') {
    throw new Error('invalid state for connect')
  }

  this._preventReconnect = false

  // reporting?
  if (this.reportInterval && !this._reportInterval) {
    this._reportInterval = setInterval(this._onreportNeeded, this.reportInterval)
    this._onreportNeeded()
  }

  // change state -> requesting
  debug(this.id + ' requesting connection')
  this.state = 'requesting'
  this.emit('statechange')

  // watch config
  if (!this._watchingConfig) {
    this._configRef.on('value', this._onconfig)
    this._watchingConfig = true
  }

  // ensure required config info
  if (!this.config.K) {
    this.once('configure', this._doconnect)
    return this
  }

  this._doconnect()

  return this
}

Node.prototype.disconnect = function () {
  this.state = 'disconnected'
  this._preventReconnect = true
  this._watchingConfig = false

  // remove some listeners
  this.removeListener('configure', this._doconnect)
  this._configRef.off('value', this._onconfig)
  this._requestsRef.off('child_added', this._onrequest)

  // stop reporting
  clearInterval(this._reportInterval)
  delete this._reportInterval

  // remove outstanding request / response listener
  if (this._requestRef) {
    this._requestRef.remove()
    this._responsesRef.off('child_added', this._onresponse)
  }

  // destroy upstream connection
  if (this.upstream) {
    this.upstream.destroy()
    this.upstream = null
  }

  // destroy downstream connections
  for (var i in this.downstream) {
    this.downstream[i].destroy()
  }
  this.downstream = {}

  return this
}

// private api below

Node.prototype._onconfig = function (snapshot) {
  var data = snapshot.val()

  if (!data) {
    this.emit('error', new Error('missing configuration'))
    return
  }

  if (!data.K || isNaN(data.K)) {
    this.emit('error', new Error('configuration did not supply valid value for K'))
    return
  }

  this.config = data
  debug(this.id + ' updated configuration')
  this.emit('configure')
}

Node.prototype._doconnect = function () {
  var self = this

  if (this.root) {

    // emit connect but in nextTick
    setTimeout(function () {
      // change state -> connected
      debug(self.id + ' connected as root')
      self.state = 'connected'
      self.emit('statechange')

      // emit connect
      self.emit('connect')

      // start responding to requests
      self._requestsRef.on('child_added', self._onrequest)
    })

    return
  }

  // we are not root so publish a connection request
  this._dorequest()
}

Node.prototype._dorequest = function () {
  var self = this

  this._requestRef = this._requestsRef.push({
    id: this.id,
    removal_flag: {
      removed: false
    }
  })

  // make sure no one removes our request until we're connected
  this._requestRef.child('removal_flag').once('child_removed', function () {
    if (self.state === 'requesting') {
      self._responsesRef.off('child_added', self._onresponse)
      self._dorequest()
    }
  })

  // listen for a response
  this._responsesRef = this._requestRef.child('responses')
  this._responsesRef.once('child_added', this._onresponse)
}

Node.prototype._onrequest = function (snapshot) {
  if (this.state !== 'connected') {
    return // can't respond to requests unless we are connected
  }

  if (Object.keys(this.downstream).length >= this.config.K) {
    return // can't respond to requests if we've hit K peers
  }

  var self = this
  var requestRef = snapshot.ref()
  var requestId = snapshot.key()
  var request = snapshot.val()
  var peerId = request.id

  // responders may accidentally recreate requests
  // these won't have an id though and can be removed
  if (!peerId) {
    requestRef.remove()
    return
  }

  // prevent circles
  if (peerId === this._mask) {
    return
  }

  // since we review requests after every connect / disconnect
  // it is possible to see a peer we know about
  var knownPeer = this.downstream[peerId]
  if (knownPeer) {
    if (knownPeer.requestId !== requestId) {
      // if request ids don't match, the peer must have disconnected without us noticing
      knownPeer.destroy()
    } else {
      return
    }
  }

  debug(this.id + ' saw request by ' + peerId)

  var responseRef = requestRef.child('responses/' + this.id)

  // initiate peer connection
  // we have to do this before actually writing our response because
  // firebase can trigger events in the same tick which could circumvent
  // the K check at the top of this method
  this._connectToPeer(true, peerId, requestId, responseRef)

  // publish response
  responseRef.update({
    id: this.id
  })

  // watch for request withdrawal
  responseRef.once('child_removed', function () {
    var peer = self.downstream[peerId]
    if (peer && !peer.didConnect) {
      peer.requestWithdrawn = true
      peer.destroy()
    }
  })
}

Node.prototype._onresponse = function (snapshot) {
  if (this.state !== 'requesting') {
    return
  }

  var responseRef = snapshot.ref()
  var response = snapshot.val()
  var peerId = response.id

  if (!peerId) {
    return
  }

  // change state -> connecting (this prevents accepting multiple responses)
  debug(this.id + ' got response from ' + peerId)
  this.state = 'connecting'
  this.emit('statechange')

  // stop watching for request removal
  this._requestRef.child('removal_flag').off()

  // attempt a connection
  this._connectToPeer(false, peerId, null, responseRef)
}

Node.prototype._connectToPeer = function (initiator, peerId, requestId, responseRef) {
  var self = this
  var localSignals = responseRef.child(initiator ? 'responderSignals' : 'requesterSignals')
  var remoteSignals = responseRef.child(initiator ? 'requesterSignals' : 'responderSignals')

  var peer = new SimplePeer({
    initiator: initiator,
    config: this.opts.peerConfig
  })

  peer.id = peerId

  if (initiator) {
    this.downstream[peer.id] = peer
    peer.notifications = peer._pc.createDataChannel('notifications')
    peer.requestId = requestId
  } else {
    var oldondatachannel = peer._pc.ondatachannel
    peer._pc.ondatachannel = function (evt) {
      if (evt.channel.label === 'notifications') {
        peer.notifications = evt.channel
        peer.notifications.onmessage = self._onmaskupdate
      } else {
        oldondatachannel(evt)
      }
    }
  }

  peer.on('connect', this._onpeerConnect.bind(this, peer, remoteSignals))
  peer.on('close', this._onpeerDisconnect.bind(this, peer, remoteSignals))

  peer.on('error', function (err) {
    debug(this.id + ' saw peer connection error', err)
  })

  peer.on('signal', function (signal) {
    if (initiator && self.state !== 'connected') return
    signal = JSON.parse(JSON.stringify(signal))
    localSignals.push(signal)
  })

  remoteSignals.on('child_added', function (snapshot) {
    if (initiator && self.state !== 'connected') return
    var signal = snapshot.val()
    peer.signal(signal)
  })

  // timeout connections
  setTimeout(function () {
    if (!peer.didConnect) {
      peer.destroy()
    }
  }, CONNECTION_TIMEOUT)
}

Node.prototype._onpeerConnect = function (peer, remoteSignals) {
  peer.didConnect = true
  peer.removeAllListeners('connect')
  peer.removeAllListeners('signal')
  remoteSignals.off()

  peer._channel.maxPacketLifeTime = this.channelConfig.maxPacketLifeTime || null
  peer._channel.maxRetransmits = this.channelConfig.maxRetransmits || null
  peer._channel.ordered = this.channelConfig.ordered === false ? false : true

  if (this.downstream[peer.id]) {
    this._ondownstreamConnect(peer)
  } else {
    this._onupstreamConnect(peer)
  }
}

Node.prototype._onpeerDisconnect = function (peer, remoteSignals) {
  peer.removeAllListeners()
  remoteSignals.off()

  if (this.downstream[peer.id]) {
    this._ondownstreamDisconnect(peer)
  } else {
    this._onupstreamDisconnect(peer)
  }
}

Node.prototype._onupstreamConnect = function (peer) {
  // remove our request
  this._requestRef.remove()

  // already got connected by someone else
  if (this.state === 'connected') {
    debug(this.id + ' rejected upstream connection by ' + peer.id)
    peer.destroy()
    return
  }

  this.upstream = peer

  // change state -> connected
  debug(this.id + ' established upstream connection to ' + peer.id)
  this.state = 'connected'
  this.emit('statechange')
  this.emit('connect', peer)

  // begin responding to requests
  this._reviewRequests()
}

Node.prototype._onupstreamDisconnect = function (peer) {
  // stop responding to new requests
  this._requestsRef.off('child_added', this._onrequest)

  // remove request
  this._requestRef.remove()

  this.upstream = null

  // change state -> disconnected
  if (this.state !== 'disconnected') {
    debug(this.id + ' lost upstream connection to ' + peer.id)
  }

  this.state = 'disconnected'
  this.emit('statechange')

  // emit disconnect if we were connected
  if (peer.didConnect) {
    this.emit('disconnect', peer)
  }

  // attempt to reconnect if we were not disconnected intentionally
  if (!this._preventReconnect) {

    // mask off our descendants
    this._onmaskupdate({ data: this.id })

    // give our mask update a tiny head start
    var self = this
    setTimeout(function () {
      if (!self._preventReconnect) {
        self.connect()
      }
    }, 100)
  }
}

Node.prototype._ondownstreamConnect = function (peer) {
  // emit peerconnect
  debug(this.id + ' established downstream connection to ' + peer.id)
  this.emit('peerconnect', peer)

  // stop responding to requests if peers > K
  if (Object.keys(this.downstream).length >= this.config.K) {
    this._requestsRef.off('child_added', this._onrequest)
  }

  // make sure downstream has the most up to date mask
  if (this._mask && peer.notifications.readyState === 'open') {
    peer.notifications.send(this._mask)
  }
}

Node.prototype._ondownstreamDisconnect = function (peer) {
  // remove from lookup
  delete this.downstream[peer.id]

  // emit events and potentially remove stale requests
  if (peer.didConnect) {
    if (this.state !== 'disconnected') {
      debug(this.id + ' lost downstream connection to ' + peer.id)
    }

    this.emit('peerdisconnect', peer)
  } else {
    if (peer.requestWithdrawn) {
      debug(this.id + ' saw request withdrawn by ' + peer.id)
    } else {
      debug(this.id + ' removing stale request by ' + peer.id)
      this._requestsRef.child(peer.requestId).remove()
    }
  }

  this._reviewRequests()
}

Node.prototype._onmaskupdate = function (evt) {
  this._mask = evt.data

  debug(this.id + ' set mask to ' + this._mask)

  // oops we made a circle, fix that
  if (this.downstream[this._mask]) {
    debug(this.id + ' destroying accidental circle back to ' + this._mask)
    this.downstream[this._mask].destroy()
  }

  for (var i in this.downstream) {
    var notifications = this.downstream[i].notifications
    if (notifications.readyState === 'open') {
      notifications.send(this._mask)
    }
  }
}

Node.prototype._onreportNeeded = function () {
  var report = {
    state: this.state,
    upstream: this.upstream ? this.upstream.id : null,
    timestamp: Firebase.ServerValue.TIMESTAMP
  }

  if (this.root) {
    report.root = true
  }

  this._reports
    .child(this.id)
    .update(report)
}

Node.prototype._reviewRequests = function () {
  if (this.state === 'connected' && Object.keys(this.downstream).length < this.config.K) {
    this._requestsRef.off('child_added', this._onrequest)
    this._requestsRef.on('child_added', this._onrequest)
  }
}
<commit_msg>simple-peer now accepts channelConfig<commit_after>module.exports = function (_Firebase) {
  Firebase = _Firebase
  return Node
}

var debug = require('debug')('fireflower')
var events = require('events')
var inherits = require('inherits')
var SimplePeer = require('simple-peer')
var Firebase = null

var CONNECTION_TIMEOUT = 5000

inherits(Node, events.EventEmitter)

function Node (url, opts) {
  if (!(this instanceof Node)) {
    return new Node(url, opts)
  }

  this.url = url
  this.opts = opts || {}
  this.root = this.opts.root
  this.reportInterval = this.opts.reportInterval
  this.config = {}
  this.state = 'disconnected'
  this.upstream = null
  this.downstream = {}

  // firebase refs
  this._ref = new Firebase(this.url)
  this._configRef = this._ref.child('configuration')
  this._requestsRef = this._ref.child('requests')
  this._reports = this._ref.child('reports')

  // set a random id if one was not provided
  this.id = this.opts.id || this._requestsRef.push().key()

  // bind callbacks
  this._onconfig = this._onconfig.bind(this)
  this._doconnect = this._doconnect.bind(this)
  this._onrequest = this._onrequest.bind(this)
  this._onresponse = this._onresponse.bind(this)
  this._onmaskupdate = this._onmaskupdate.bind(this)
  this._onreportNeeded = this._onreportNeeded.bind(this)

  events.EventEmitter.call(this)
}

Node.prototype.connect = function () {
  if (this.state !== 'disconnected') {
    throw new Error('invalid state for connect')
  }

  this._preventReconnect = false

  // reporting?
  if (this.reportInterval && !this._reportInterval) {
    this._reportInterval = setInterval(this._onreportNeeded, this.reportInterval)
    this._onreportNeeded()
  }

  // change state -> requesting
  debug(this.id + ' requesting connection')
  this.state = 'requesting'
  this.emit('statechange')

  // watch config
  if (!this._watchingConfig) {
    this._configRef.on('value', this._onconfig)
    this._watchingConfig = true
  }

  // ensure required config info
  if (!this.config.K) {
    this.once('configure', this._doconnect)
    return this
  }

  this._doconnect()

  return this
}

Node.prototype.disconnect = function () {
  this.state = 'disconnected'
  this._preventReconnect = true
  this._watchingConfig = false

  // remove some listeners
  this.removeListener('configure', this._doconnect)
  this._configRef.off('value', this._onconfig)
  this._requestsRef.off('child_added', this._onrequest)

  // stop reporting
  clearInterval(this._reportInterval)
  delete this._reportInterval

  // remove outstanding request / response listener
  if (this._requestRef) {
    this._requestRef.remove()
    this._responsesRef.off('child_added', this._onresponse)
  }

  // destroy upstream connection
  if (this.upstream) {
    this.upstream.destroy()
    this.upstream = null
  }

  // destroy downstream connections
  for (var i in this.downstream) {
    this.downstream[i].destroy()
  }
  this.downstream = {}

  return this
}

// private api below

Node.prototype._onconfig = function (snapshot) {
  var data = snapshot.val()

  if (!data) {
    this.emit('error', new Error('missing configuration'))
    return
  }

  if (!data.K || isNaN(data.K)) {
    this.emit('error', new Error('configuration did not supply valid value for K'))
    return
  }

  this.config = data
  debug(this.id + ' updated configuration')
  this.emit('configure')
}

Node.prototype._doconnect = function () {
  var self = this

  if (this.root) {

    // emit connect but in nextTick
    setTimeout(function () {
      // change state -> connected
      debug(self.id + ' connected as root')
      self.state = 'connected'
      self.emit('statechange')

      // emit connect
      self.emit('connect')

      // start responding to requests
      self._requestsRef.on('child_added', self._onrequest)
    })

    return
  }

  // we are not root so publish a connection request
  this._dorequest()
}

Node.prototype._dorequest = function () {
  var self = this

  this._requestRef = this._requestsRef.push({
    id: this.id,
    removal_flag: {
      removed: false
    }
  })

  // make sure no one removes our request until we're connected
  this._requestRef.child('removal_flag').once('child_removed', function () {
    if (self.state === 'requesting') {
      self._responsesRef.off('child_added', self._onresponse)
      self._dorequest()
    }
  })

  // listen for a response
  this._responsesRef = this._requestRef.child('responses')
  this._responsesRef.once('child_added', this._onresponse)
}

Node.prototype._onrequest = function (snapshot) {
  if (this.state !== 'connected') {
    return // can't respond to requests unless we are connected
  }

  if (Object.keys(this.downstream).length >= this.config.K) {
    return // can't respond to requests if we've hit K peers
  }

  var self = this
  var requestRef = snapshot.ref()
  var requestId = snapshot.key()
  var request = snapshot.val()
  var peerId = request.id

  // responders may accidentally recreate requests
  // these won't have an id though and can be removed
  if (!peerId) {
    requestRef.remove()
    return
  }

  // prevent circles
  if (peerId === this._mask) {
    return
  }

  // since we review requests after every connect / disconnect
  // it is possible to see a peer we know about
  var knownPeer = this.downstream[peerId]
  if (knownPeer) {
    if (knownPeer.requestId !== requestId) {
      // if request ids don't match, the peer must have disconnected without us noticing
      knownPeer.destroy()
    } else {
      return
    }
  }

  debug(this.id + ' saw request by ' + peerId)

  var responseRef = requestRef.child('responses/' + this.id)

  // initiate peer connection
  // we have to do this before actually writing our response because
  // firebase can trigger events in the same tick which could circumvent
  // the K check at the top of this method
  this._connectToPeer(true, peerId, requestId, responseRef)

  // publish response
  responseRef.update({
    id: this.id
  })

  // watch for request withdrawal
  responseRef.once('child_removed', function () {
    var peer = self.downstream[peerId]
    if (peer && !peer.didConnect) {
      peer.requestWithdrawn = true
      peer.destroy()
    }
  })
}

Node.prototype._onresponse = function (snapshot) {
  if (this.state !== 'requesting') {
    return
  }

  var responseRef = snapshot.ref()
  var response = snapshot.val()
  var peerId = response.id

  if (!peerId) {
    return
  }

  // change state -> connecting (this prevents accepting multiple responses)
  debug(this.id + ' got response from ' + peerId)
  this.state = 'connecting'
  this.emit('statechange')

  // stop watching for request removal
  this._requestRef.child('removal_flag').off()

  // attempt a connection
  this._connectToPeer(false, peerId, null, responseRef)
}

Node.prototype._connectToPeer = function (initiator, peerId, requestId, responseRef) {
  var self = this
  var localSignals = responseRef.child(initiator ? 'responderSignals' : 'requesterSignals')
  var remoteSignals = responseRef.child(initiator ? 'requesterSignals' : 'responderSignals')

  var peer = new SimplePeer({
    initiator: initiator,
    config: this.opts.peerConfig,
    channelConfig: this.opts.channelConfig
  })

  peer.id = peerId

  if (initiator) {
    this.downstream[peer.id] = peer
    peer.notifications = peer._pc.createDataChannel('notifications')
    peer.requestId = requestId
  } else {
    var oldondatachannel = peer._pc.ondatachannel
    peer._pc.ondatachannel = function (evt) {
      if (evt.channel.label === 'notifications') {
        peer.notifications = evt.channel
        peer.notifications.onmessage = self._onmaskupdate
      } else {
        oldondatachannel(evt)
      }
    }
  }

  peer.on('connect', this._onpeerConnect.bind(this, peer, remoteSignals))
  peer.on('close', this._onpeerDisconnect.bind(this, peer, remoteSignals))

  peer.on('error', function (err) {
    debug(this.id + ' saw peer connection error', err)
  })

  peer.on('signal', function (signal) {
    if (initiator && self.state !== 'connected') return
    signal = JSON.parse(JSON.stringify(signal))
    localSignals.push(signal)
  })

  remoteSignals.on('child_added', function (snapshot) {
    if (initiator && self.state !== 'connected') return
    var signal = snapshot.val()
    peer.signal(signal)
  })

  // timeout connections
  setTimeout(function () {
    if (!peer.didConnect) {
      peer.destroy()
    }
  }, CONNECTION_TIMEOUT)
}

Node.prototype._onpeerConnect = function (peer, remoteSignals) {
  peer.didConnect = true
  peer.removeAllListeners('connect')
  peer.removeAllListeners('signal')
  remoteSignals.off()

  if (this.downstream[peer.id]) {
    this._ondownstreamConnect(peer)
  } else {
    this._onupstreamConnect(peer)
  }
}

Node.prototype._onpeerDisconnect = function (peer, remoteSignals) {
  peer.removeAllListeners()
  remoteSignals.off()

  if (this.downstream[peer.id]) {
    this._ondownstreamDisconnect(peer)
  } else {
    this._onupstreamDisconnect(peer)
  }
}

Node.prototype._onupstreamConnect = function (peer) {
  // remove our request
  this._requestRef.remove()

  // already got connected by someone else
  if (this.state === 'connected') {
    debug(this.id + ' rejected upstream connection by ' + peer.id)
    peer.destroy()
    return
  }

  this.upstream = peer

  // change state -> connected
  debug(this.id + ' established upstream connection to ' + peer.id)
  this.state = 'connected'
  this.emit('statechange')
  this.emit('connect', peer)

  // begin responding to requests
  this._reviewRequests()
}

Node.prototype._onupstreamDisconnect = function (peer) {
  // stop responding to new requests
  this._requestsRef.off('child_added', this._onrequest)

  // remove request
  this._requestRef.remove()

  this.upstream = null

  // change state -> disconnected
  if (this.state !== 'disconnected') {
    debug(this.id + ' lost upstream connection to ' + peer.id)
  }

  this.state = 'disconnected'
  this.emit('statechange')

  // emit disconnect if we were connected
  if (peer.didConnect) {
    this.emit('disconnect', peer)
  }

  // attempt to reconnect if we were not disconnected intentionally
  if (!this._preventReconnect) {

    // mask off our descendants
    this._onmaskupdate({ data: this.id })

    // give our mask update a tiny head start
    var self = this
    setTimeout(function () {
      if (!self._preventReconnect) {
        self.connect()
      }
    }, 100)
  }
}

Node.prototype._ondownstreamConnect = function (peer) {
  // emit peerconnect
  debug(this.id + ' established downstream connection to ' + peer.id)
  this.emit('peerconnect', peer)

  // stop responding to requests if peers > K
  if (Object.keys(this.downstream).length >= this.config.K) {
    this._requestsRef.off('child_added', this._onrequest)
  }

  // make sure downstream has the most up to date mask
  if (this._mask && peer.notifications.readyState === 'open') {
    peer.notifications.send(this._mask)
  }
}

Node.prototype._ondownstreamDisconnect = function (peer) {
  // remove from lookup
  delete this.downstream[peer.id]

  // emit events and potentially remove stale requests
  if (peer.didConnect) {
    if (this.state !== 'disconnected') {
      debug(this.id + ' lost downstream connection to ' + peer.id)
    }

    this.emit('peerdisconnect', peer)
  } else {
    if (peer.requestWithdrawn) {
      debug(this.id + ' saw request withdrawn by ' + peer.id)
    } else {
      debug(this.id + ' removing stale request by ' + peer.id)
      this._requestsRef.child(peer.requestId).remove()
    }
  }

  this._reviewRequests()
}

Node.prototype._onmaskupdate = function (evt) {
  this._mask = evt.data

  debug(this.id + ' set mask to ' + this._mask)

  // oops we made a circle, fix that
  if (this.downstream[this._mask]) {
    debug(this.id + ' destroying accidental circle back to ' + this._mask)
    this.downstream[this._mask].destroy()
  }

  for (var i in this.downstream) {
    var notifications = this.downstream[i].notifications
    if (notifications.readyState === 'open') {
      notifications.send(this._mask)
    }
  }
}

Node.prototype._onreportNeeded = function () {
  var report = {
    state: this.state,
    upstream: this.upstream ? this.upstream.id : null,
    timestamp: Firebase.ServerValue.TIMESTAMP
  }

  if (this.root) {
    report.root = true
  }

  this._reports
    .child(this.id)
    .update(report)
}

Node.prototype._reviewRequests = function () {
  if (this.state === 'connected' && Object.keys(this.downstream).length < this.config.K) {
    this._requestsRef.off('child_added', this._onrequest)
    this._requestsRef.on('child_added', this._onrequest)
  }
}
<|endoftext|>


<commit_before>/* jshint asi: true, expr: true, globalstrict: true */
/* global require, Buffer, process, module */
"use strict";

var fs                          = require('fs'),
    path                        = require('path'),
    rng                         = require('crypto').rng,
    _                           = require('underscore'),
    q                           = require('kew'),
    through                     = require('through'),
    nodeResolve                 = require('resolve'),
    browserResolve              = require('browser-resolve'),
    aggregate                   = require('stream-aggregate-promise'),
    asStream                    = require('as-stream'),
    transformResolve            = resolveWith.bind(null, nodeResolve),
    all                         = q.all,
    isString                    = _.isString,
    extend                      = _.extend,
    extractDependencies = require('./transforms/deps')

/**
 * Resolve module by id
 *
 * @param {Function<String, Object, Callback>} resolve
 * @param {String} id
 * @param {Object} parent
 * @return {Promise<Object>} an object with id, filename and package keys
 */
function resolveWith(resolve, id, parent) {
  var p = q.defer()
  resolve(id, parent, function(err, filename, pkg) {
    err ? p.reject(err) : p.resolve({id: id, filename: filename, package: pkg})
  })
  return p
}

/**
 * Run stream transform over module
 *
 * Such transforms are compatible with transforms for browserify
 *
 * @param {Function<String>} transform a transform factory
 * @param {Object} mod a module to run transform over
 * @return {Promise<Object>} a transformed module
 */
function runStreamTransform(transform, mod) {
  return aggregate(asStream(mod.source).pipe(transform(mod.filename)))
    .then(function(source) {
      mod.source = source
      return mod
    })
}

/**
 * Run transform over module
 *
 * @param {Function<String>} transform a transform factory
 * @param {Object} mod a module to run transform over
 * @return {Promise<Object>} a transformed module
 */
function runTransform(transform, mod, opts) {
  return q.resolve(transform(mod, opts)).then(function(transformed) {
    if (!transformed) return mod
    if (transformed.source) mod.source = transformed.source
    if (transformed.deps) mod.deps = extend(mod.deps, transformed.deps)
    return mod
  })
}

/**
 * Load transform from a package
 *
 * @param {Object} pkg a package to load transform from
 * @param {Array<String>} key an array of strings represents a path in a package
 * @return {Array<String>} a list of transform ids
 */
function getTransform(pkg, key) {
  key.forEach(function (k) { if (pkg && typeof pkg === 'object') pkg = pkg[k] })
  return [].concat(pkg).filter(Boolean)
}

/**
 * Load transform relative to specified module
 *
 * It tries to resolve transform relative to module's filename first and then
 * fallback's to process.cwd()
 *
 * @param {Object} mod a module
 * @param {String} transform a transform module id
 * @return {Promise<Function>} a loaded transform function
 */
function loadTransform(mod, transform) {
  if (!isString(transform)) return q.resolve(transform)

  return transformResolve(transform, {basedir: path.dirname(mod.filename)})
    .fail(function() {
      return transformResolve(transform, {basedir: process.cwd()})
    })
    .then(function(res) {
      if (!res)
        throw new Error([
          'cannot find transform module ', transform,
          ' while transforming ', mod.filename
        ].join(''))
      return require(res.filename)
    })
    .fail(function(err) {
      err.message += ' which is required as a transform'
      throw err
    })
}

/**
 * Convert module object into a format suitable for browser-pack
 *
 * @param {Object} mod an object representing some module
 * @return {Object} an object suitable for browser-pack consumption
 */
function moduleToResult(mod) {
  if (!mod.filename) return mod
  var result = {id: mod.filename, source: mod.source, deps: {}}
  if (Buffer.isBuffer(result.source))
    result.source = result.source.toString()
  if (mod.entry)
    result.entry = true
  for (var k in mod.deps)
    result.deps[k] = mod.deps[k].filename
  return result
}

/**
 * Resolve a graph of dependencies for a specified set of modules
 *
 * This is compatible with module-deps implementation with a few addition
 * features
 *
 * @param {Array<String|Stream>} mains a list of module ids, paths or streams
 * @param {Object} opts an options object compatible with module-deps
 * @return {Stream<Object>} a stream of resolved modules with dependencies
 */
module.exports = function(mains, opts) {
  opts = opts || {}

  var moduleProto = {
    resolve: function(id, parent) {
      if (opts.filter && !opts.filter(id))
        return q.resolve({id: id, filename: false})
      else
        return resolver(id, parent || this)
    },
    resolveMany: function(ids, parent) {
      return all(ids.map(function(id) {return this.resolve(id, parent)}, this))
        .then(function(resolved) {
          var result = {}
          resolved.forEach(function(r) { result[r.id] = r })
          return result
        })
    }
  }

  var output = through(),
      basedir = opts.basedir || process.cwd(),
      resolve = resolveWith.bind(null, opts.resolve || browserResolve),
      resolveCache = {},
      seen = {},
      entries = [].concat(mains).filter(Boolean).map(makeMainModule)

  all(entries.map(function(mod) {return walk(mod, {filename: '/', id: '/'})}))
    .fail(output.emit.bind(output, 'error'))
    .fin(output.queue.bind(output, null))

  output.asPromise = function() { return aggregate(this) }

  return output

  function emit(mod) {
    output.queue(moduleToResult(mod))
    return mod
  }

  function walk(mod, parent) {
    if (isString(mod)) mod = resolveCache[mod]
    if (seen[mod.filename]) return
    seen[mod.filename] = true

    var cached = checkExternalCache(mod, parent)
    if (cached) {
      emit(cached)
      return walkDeps(cached, parent)
    }

    return applyTransforms(mod).then(emit).then(walkDeps)
  }

  function walkDeps(mod) {
    return all(Object.keys(mod.deps)
      .filter(function(id) { return mod.deps[id] })
      .map(function(id) { return walk(mod.deps[id], mod) }))
  }

  function makeModule(mod) {
    var result = Object.create(moduleProto)
    extend(result, mod)
    result.deps = mod.deps || {}
    return result
  }

  function makeMainModule(m) {
    var filename,
        mod = makeModule({entry: true})

    if (typeof m.pipe === 'function') {
      filename = path.join(basedir, rng(8).toString('hex') + '.js')
      mod.id = filename
      mod.filename = filename
      mod.sourcePromise = aggregate(m)
    } else {
      filename = path.resolve(m)
      mod.id = filename
      mod.filename = filename
    }
    return mod
  }

  // Wrapper around browser-resolve which passes configurations
  function resolver(id, parent) {
    parent.packageFilter = opts.packageFilter
    parent.extensions = opts.extensions
    parent.modules = opts.modules
    parent.paths = []
    return resolve(id, parent).then(makeModule)
      .then(function(mod) { return resolveCache[mod.id] = mod })
      .fail(function(err) {
        err.message += [', module required from', parent.filename].join(' ')
        throw err
      })
  }

  // Allows to store module and package definitions in a opts.cache and
  // opts.packageCache respectively, this is how module-deps does it
  function checkExternalCache(mod, parent) {
    if (!(opts.cache && opts.cache[parent.filename])) return
    var curFilename = opts.cache[parent.filename].deps[mod.id]
    var result = opts.cache[curFilename]
    if (!result) return
    return {id: mod.id, filename: curFilename,
      deps: result.deps, source: result.source, entry: result.entry,
      package: opts.packageCache && opts.packageCache[result.id]}
  }

  function isTopLevel(mod) {
    return entries.some(function (entry) {
      return path.relative(path.dirname(entry.filename), mod.filename)
        .split('/').indexOf('node_modules') < 0
    })
  }

  // Apply global and per-package transforms on a module
  // It automatically inserts extractDependencies transform
  function applyTransforms(mod) {
    var txs = [extractDependencies],
        p = (mod.sourcePromise || aggregate(fs.createReadStream(mod.filename)))
            .then(function(source) {
              mod.source = source
              return mod
            })

    if (isTopLevel(mod))
      txs = txs.concat(opts.transform)

    if (mod.package && opts.transformKey)
      txs = txs.concat(getTransform(mod.package, opts.transformKey))

    txs = txs.filter(Boolean).map(loadTransform.bind(null, mod))

    return all(txs).then(function(txs) {
      txs.forEach(function(t) {
        p = p.then(function(p) {
          if (t.length === 1)
            return runStreamTransform(t, p)
          else
            return runTransform(t, p, opts)
        })
      })
      return p
    })
  }
}
<commit_msg>gardening<commit_after>/* jshint asi: true, expr: true, globalstrict: true */
/* global require, Buffer, process, module */
"use strict";

var fs                          = require('fs'),
    path                        = require('path'),
    rng                         = require('crypto').rng,
    _                           = require('underscore'),
    q                           = require('kew'),
    through                     = require('through'),
    nodeResolve                 = require('resolve'),
    browserResolve              = require('browser-resolve'),
    aggregate                   = require('stream-aggregate-promise'),
    asStream                    = require('as-stream'),
    transformResolve            = resolveWith.bind(null, nodeResolve),
    all                         = q.all,
    isString                    = _.isString,
    extend                      = _.extend,
    extractDependencies = require('./transforms/deps')

/**
 * Resolve module by id
 *
 * @param {Function<String, Object, Callback>} resolve
 * @param {String} id
 * @param {Object} parent
 * @return {Promise<Object>} an object with id, filename and package keys
 */
function resolveWith(resolve, id, parent) {
  var p = q.defer()
  resolve(id, parent, function(err, filename, pkg) {
    if (err)
      p.reject(err)
    else
      p.resolve({id: id, filename: filename, package: pkg})
  })
  return p
}

/**
 * Run stream transform over module
 *
 * Such transforms are compatible with transforms for browserify
 *
 * @param {Function<String>} transform a transform factory
 * @param {Object} mod a module to run transform over
 * @return {Promise<Object>} a transformed module
 */
function runStreamTransform(transform, mod) {
  return aggregate(asStream(mod.source).pipe(transform(mod.filename)))
    .then(function(source) {
      mod.source = source
      return mod
    })
}

/**
 * Run transform over module
 *
 * @param {Function<String>} transform a transform factory
 * @param {Object} mod a module to run transform over
 * @return {Promise<Object>} a transformed module
 */
function runTransform(transform, mod, opts) {
  return q.resolve(transform(mod, opts)).then(function(transformed) {
    if (!transformed) return mod
    if (transformed.source) mod.source = transformed.source
    if (transformed.deps) mod.deps = extend(mod.deps, transformed.deps)
    return mod
  })
}

/**
 * Load transform from a package
 *
 * @param {Object} pkg a package to load transform from
 * @param {Array<String>} key an array of strings represents a path in a package
 * @return {Array<String>} a list of transform ids
 */
function getTransform(pkg, key) {
  key.forEach(function (k) { if (pkg && typeof pkg === 'object') pkg = pkg[k] })
  return [].concat(pkg).filter(Boolean)
}

/**
 * Load transform relative to specified module
 *
 * It tries to resolve transform relative to module's filename first and then
 * fallback's to process.cwd()
 *
 * @param {Object} mod a module
 * @param {String} transform a transform module id
 * @return {Promise<Function>} a loaded transform function
 */
function loadTransform(mod, transform) {
  if (!isString(transform)) return q.resolve(transform)

  return transformResolve(transform, {basedir: path.dirname(mod.filename)})
    .fail(function() {
      return transformResolve(transform, {basedir: process.cwd()})
    })
    .then(function(res) {
      if (!res)
        throw new Error([
          'cannot find transform module ', transform,
          ' while transforming ', mod.filename
        ].join(''))
      return require(res.filename)
    })
    .fail(function(err) {
      err.message += ' which is required as a transform'
      throw err
    })
}

/**
 * Convert module object into a format suitable for browser-pack
 *
 * @param {Object} mod an object representing some module
 * @return {Object} an object suitable for browser-pack consumption
 */
function moduleToResult(mod) {
  if (!mod.filename) return mod
  var result = {id: mod.filename, source: mod.source, deps: {}}
  if (Buffer.isBuffer(result.source))
    result.source = result.source.toString()
  if (mod.entry)
    result.entry = true
  for (var k in mod.deps)
    result.deps[k] = mod.deps[k].filename
  return result
}

/**
 * Resolve a graph of dependencies for a specified set of modules
 *
 * This is compatible with module-deps implementation with a few addition
 * features
 *
 * @param {Array<String|Stream>} mains a list of module ids, paths or streams
 * @param {Object} opts an options object compatible with module-deps
 * @return {Stream<Object>} a stream of resolved modules with dependencies
 */
module.exports = function(mains, opts) {
  opts = opts || {}

  var moduleProto = {
    resolve: function(id, parent) {
      if (opts.filter && !opts.filter(id))
        return q.resolve({id: id, filename: false})
      else
        return resolver(id, parent || this)
    },
    resolveMany: function(ids, parent) {
      return all(ids.map(function(id) {return this.resolve(id, parent)}, this))
        .then(function(resolved) {
          var result = {}
          resolved.forEach(function(r) { result[r.id] = r })
          return result
        })
    }
  }

  var output = through(),
      basedir = opts.basedir || process.cwd(),
      resolve = resolveWith.bind(null, opts.resolve || browserResolve),
      resolveCache = {},
      seen = {},
      entries = [].concat(mains).filter(Boolean).map(makeMainModule)

  all(entries.map(function(mod) {return walk(mod, {filename: '/', id: '/'})}))
    .fail(output.emit.bind(output, 'error'))
    .fin(output.queue.bind(output, null))

  output.asPromise = function() { return aggregate(this) }

  return output

  function emit(mod) {
    output.queue(moduleToResult(mod))
    return mod
  }

  function walk(mod, parent) {
    if (isString(mod)) mod = resolveCache[mod]
    if (seen[mod.filename]) return
    seen[mod.filename] = true

    var cached = checkExternalCache(mod, parent)
    if (cached) {
      emit(cached)
      return walkDeps(cached, parent)
    }

    return applyTransforms(mod).then(emit).then(walkDeps)
  }

  function walkDeps(mod) {
    return all(Object.keys(mod.deps)
      .filter(function(id) { return mod.deps[id] })
      .map(function(id) { return walk(mod.deps[id], mod) }))
  }

  function makeModule(mod) {
    var result = Object.create(moduleProto)
    extend(result, mod)
    result.deps = mod.deps || {}
    return result
  }

  function makeMainModule(m) {
    var filename,
        mod = makeModule({entry: true})

    if (typeof m.pipe === 'function') {
      filename = path.join(basedir, rng(8).toString('hex') + '.js')
      mod.id = filename
      mod.filename = filename
      mod.sourcePromise = aggregate(m)
    } else {
      filename = path.resolve(m)
      mod.id = filename
      mod.filename = filename
    }
    return mod
  }

  // Wrapper around browser-resolve which passes configurations
  function resolver(id, parent) {
    parent.packageFilter = opts.packageFilter
    parent.extensions = opts.extensions
    parent.modules = opts.modules
    parent.paths = []
    return resolve(id, parent).then(makeModule)
      .then(function(mod) { return resolveCache[mod.id] = mod })
      .fail(function(err) {
        err.message += [', module required from', parent.filename].join(' ')
        throw err
      })
  }

  // Allows to store module and package definitions in a opts.cache and
  // opts.packageCache respectively, this is how module-deps does it
  function checkExternalCache(mod, parent) {
    if (!(opts.cache && opts.cache[parent.filename])) return
    var curFilename = opts.cache[parent.filename].deps[mod.id]
    var result = opts.cache[curFilename]
    if (!result) return
    return {id: mod.id, filename: curFilename,
      deps: result.deps, source: result.source, entry: result.entry,
      package: opts.packageCache && opts.packageCache[result.id]}
  }

  function isTopLevel(mod) {
    return entries.some(function (entry) {
      return path.relative(path.dirname(entry.filename), mod.filename)
        .split('/').indexOf('node_modules') < 0
    })
  }

  // Apply global and per-package transforms on a module
  // It automatically inserts extractDependencies transform
  function applyTransforms(mod) {
    var txs = [extractDependencies],
        p = (mod.sourcePromise || aggregate(fs.createReadStream(mod.filename)))
            .then(function(source) {
              mod.source = source
              return mod
            })

    if (isTopLevel(mod))
      txs = txs.concat(opts.transform)

    if (mod.package && opts.transformKey)
      txs = txs.concat(getTransform(mod.package, opts.transformKey))

    txs = txs.filter(Boolean).map(loadTransform.bind(null, mod))

    return all(txs).then(function(txs) {
      txs.forEach(function(t) {
        p = p.then(function(p) {
          if (t.length === 1)
            return runStreamTransform(t, p)
          else
            return runTransform(t, p, opts)
        })
      })
      return p
    })
  }
}
<|endoftext|>


<commit_before>/*    Copyright 2016 - 2021 Firewalla Inc
 *
 *    This program is free software: you can redistribute it and/or  modify
 *    it under the terms of the GNU Affero General Public License, version 3,
 *    as published by the Free Software Foundation.
 *
 *    This program is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *    GNU Affero General Public License for more details.
 *
 *    You should have received a copy of the GNU Affero General Public License
 *    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

'use strict';

const log = require('../../../net2/logger.js')(__filename);
const fs = require('fs');
const f = require('../../../net2/Firewalla.js');
const VPNClient = require('../VPNClient.js');
const Promise = require('bluebird');
Promise.promisifyAll(fs);
const exec = require('child-process-promise').exec;
const {Address4, Address6} = require('ip-address');
const {BigInteger} = require('jsbn');
const sysManager = require('../../../net2/SysManager.js');
const YAML = require('../../../vendor_lib/yaml');
const iptables = require('../../../net2/Iptables.js');
const wrapIptables = iptables.wrapIptables;
const routing = require('../../routing/routing.js');
const scheduler = require('../../../util/scheduler.js');
const _ = require('lodash');

class DockerBaseVPNClient extends VPNClient {

  static async listProfileIds() {
    const dirPath = f.getHiddenFolder() + `/run/docker_vpn_client/${this.getProtocol()}`;
    const files = await fs.readdirAsync(dirPath).catch(() => []); // return empty array if dir not exists
    const profileIds = files.filter(filename => filename.endsWith('.settings')).map(filename => filename.slice(0, filename.length - ".settings".length));
    return profileIds;
  }

  _getSettingsPath() {
    return `${f.getHiddenFolder()}/run/docker_vpn_client/${this.constructor.getProtocol()}/${this.profileId}.settings`;
  }

  _getSubnetFilePath() {
    return `${f.getHiddenFolder()}/run/docker_vpn_client/${this.constructor.getProtocol()}/${this.profileId}.subnet`;
  }

  async _getRemoteIP() {
    const subnet = await this._getSubnet();
    if (subnet) {
      return Address4.fromBigInteger(new Address4(subnet).bigInteger().add(new BigInteger("2"))).correctForm(); // IPv4 address of gateway in container always uses second address in subnet
    }
    return null;
  }

  async _getSubnet() {
    return await fs.readFileAsync(this._getSubnetFilePath(), {encoding: "utf8"}).then(content => content.trim()).catch((err) => null);
  }

  async _getOrGenerateSubnet() {
    let subnet = await this._getSubnet();
    if (!subnet) {
      subnet = this._generateRandomNetwork(); // this returns a /30 subnet
      await fs.writeFileAsync(this._getSubnetFilePath(), subnet, {encoding: "utf8"}).catch((err) => {});
    }
    return subnet;
  }

  async destroy() {
    await super.destroy();
    await fs.unlinkAsync(this._getSettingsPath()).catch((err) => {});
    await fs.unlinkAsync(this._getSubnetFilePath()).catch((err) => {});
    await exec(`rm -rf ${this._getConfigDirectory()}`).catch((err) => {
      log.error(`Failed to remove config directory ${this._getConfigDirectory()}`, err.message);
    });
    await exec(`rm -rf ${this._getWorkingDirectory()}`).catch((err) => {
      log.error(`Failed to remove working directory ${this._getWorkingDirectory()}`, err.message);
    });
  }

  async getVpnIP4s() {
    const subnet = await this._getSubnet();
    if (subnet)
      return Address4.fromBigInteger(new Address4(subnet).bigInteger().add(new BigInteger("1"))).correctForm(); // bridge ipv4 address always uses first address in subnet
  }

  _generateRandomNetwork() {
    const ipRangeRandomMap = {
      "10.0.0.0/8": 22,
      "172.16.0.0/12": 18,
      "192.168.0.0/16": 14
    };
    let index = 0;
    while (true) {
      index = index % 3;
      const startAddress = Object.keys(ipRangeRandomMap)[index]
      const randomBits = ipRangeRandomMap[startAddress];
      const randomOffsets = Math.floor(Math.random() * Math.pow(2, randomBits)) * 4; // align with 2-bit, i.e., /30
      const subnet = Address4.fromBigInteger(new Address4(startAddress).bigInteger().add(new BigInteger(randomOffsets.toString()))).correctForm();
      if (!sysManager.inMySubnets4(subnet))
        return subnet + "/30";
      else
        index++;
    }
  }

  async _createNetwork() {
    // sudo docker network create -o "com.docker.network.bridge.name"="vpn_sslx" --subnet 10.53.204.108/30 vpn_sslx
    try {
      log.info(`Creating network ${this._getDockerNetworkName()} for vpn ${this.profileId} ...`);
      const subnet = await this._getOrGenerateSubnet();
      const cmd = `sudo bash -c "docker network inspect ${this._getDockerNetworkName()} || docker network create -o com.docker.network.bridge.name=${this.getInterfaceName()} --subnet ${subnet} ${this._getDockerNetworkName()}" &>/dev/null`;
      await exec(cmd);
    } catch(err) {
      log.error(`Got error when creating network ${this._getDockerNetworkName()} for ${this.profileId}, err:`, err.message);
    }
  }

  async _removeNetwork() {
    try {
      const cmd = `sudo docker network rm ${this._getDockerNetworkName()}`;
      await exec(cmd);
    } catch(err) {
      log.error(`Got error when rm network ${this._getDockerNetworkName()} for ${this.profileId}, err:`, err.message);
    }
  }

  _getDockerNetworkName() {
    return `n_${this.getInterfaceName()}`;
  }

  async _updateComposeYAML() {
    // update docker-compose.yaml in working directory, main purpose is to generate randomized subnet for docker bridge network
    const composeFilePath = this._getWorkingDirectory() + "/docker-compose.yaml";
    const config = await fs.readFileAsync(composeFilePath, {encoding: "utf8"}).then(content => YAML.parse(content)).catch((err) => {
      log.error(`Failed to read docker-compose.yaml from ${composeFilePath}`, err.message);
      return;
    });

    // update network config
    if (!config) {
      log.error("docker compose template file not found")
      return;
    }

    config.networks = {};
    config.networks[this._getDockerNetworkName()] = {external: true};

    const serviceNames = Object.keys(config.services);
    if(!_.isEmpty(serviceNames) && serviceNames.length === 1) {
      const serviceName = serviceNames[0];
      const service = config.services[serviceName];
      service.networks = {};
      service.networks[this._getDockerNetworkName()] = {
        "ipv4_address": await this._getRemoteIP()
      }

      service["container_name"] = this.getInterfaceName();

      // do not automatically restart container
      // set restart to "no" will cause docker compose yml parsing error
      //     "restart contains an invalid type, it should be a string"
      if(service["restart"]) delete service["restart"];

      await fs.writeFileAsync(composeFilePath, YAML.stringify(config), {encoding: "utf8"});

    } else {
      log.error("docker compose should only contain one service")
    }
  }

  async _start() {
    await this.__prepareAssets();
    await exec(`mkdir -p ${this._getWorkingDirectory()}`);
    await exec(`cp -f -r ${this._getConfigDirectory()}/* ${this._getWorkingDirectory()}`);
    await this._createNetwork();
    await this._updateComposeYAML();
    await exec(`sudo systemctl start docker-compose@${this.profileId}`);
    const remoteIP = await this._getRemoteIP();
    if (remoteIP)
      await exec(wrapIptables(`sudo iptables -w -t nat -A FW_POSTROUTING -s ${remoteIP} -j MASQUERADE`));
    let t = 0;
    while (t < 30) {
      const carrier = await fs.readFileAsync(`/sys/class/net/${this.getInterfaceName()}/carrier`, {encoding: "utf8"}).then(content => content.trim()).catch((err) => null);
      if (carrier === "1") {
        const remoteIP = await this._getRemoteIP();
        if (remoteIP) {
          // add the container IP to wan_routable so that packets from wan interfaces can be routed to the container
          await routing.addRouteToTable(remoteIP, null, this.getInterfaceName(), "wan_routable", 1024, 4);
        }
        break;
      }
      t++;
      await scheduler.delay(1000);
    }
  }

  async _stop() {
    const remoteIP = await this._getRemoteIP();
    if (remoteIP)
      await exec(wrapIptables(`sudo iptables -w -t nat -D FW_POSTROUTING -s ${remoteIP} -j MASQUERADE`)).catch((err) => {});
    await exec(`sudo systemctl stop docker-compose@${this.profileId}`);
    await this._removeNetwork();
  }

  async getRoutedSubnets() {
    const isLinkUp = await this._isLinkUp();
    if (isLinkUp) {
      const results = [];
      // no need to add the whole subnet to the routed subnets, only need to route the container's IP address
      const remoteIP = await this._getRemoteIP();
      if (remoteIP)
        results.push(remoteIP);
      return results;
    } else {
      return [];
    }
  }

  _getWorkingDirectory() {
    return `${f.getHiddenFolder()}/run/docker/${this.profileId}`;
  }

  _getConfigDirectory() {
    return `${f.getHiddenFolder()}/run/docker_vpn_client/${this.constructor.getProtocol()}/${this.profileId}`;
  }

  getUserConfigPath() {
    return `${this._getConfigDirectory()}/user_config.json`;
  }

  async saveOriginalUserConfig(config) {
    const file = this.getUserConfigPath();
    log.info(`[${this.profileId}] Saving user origin config to ${file}...`);
    await fs.writeFileAsync(file, JSON.stringify(config));
  }

  async loadOriginalUserConfig() {
    log.info(`[${this.profileId}] Loading user origin config...`);
    try {
      const file = this.getUserConfigPath();
      const raw = await fs.readFileAsync(file, {encoding: 'utf8'});
      return JSON.parse(raw);
    } catch (err) {
      log.error("Got error when loading user config, err:", err);
      return {};
    }
  }

  async checkAndSaveProfile(value) {
    const protocol = this.constructor.getProtocol();
    const config = value && value.config || {};

    log.info(`[${this.profileId}][${protocol}] saving user config file...`);

    await exec(`mkdir -p ${this._getConfigDirectory()}`);
    await this.saveOriginalUserConfig(config);
  }

  async _isLinkUp() {
    const serviceUp = await exec(`sudo docker container ls -f "name=${this.getInterfaceName()}" --format "{{.Status}}"`).then(result => result.stdout.trim().startsWith("Up ")).catch((err) => {
      log.error(`Failed to run docker container ls on ${this.profileId}`, err.message);
      return false;
    });
    if (serviceUp)
      return this.__isLinkUpInsideContainer().catch(() => false);
    else
      return false;
  }

  async getAttributes(includeContent = false) {
    const attributes = await super.getAttributes();

    const userConfig = await this.loadOriginalUserConfig();

    attributes.config = userConfig;
    attributes.type = this.constructor.getProtocol();
    attributes.remoteIP = await this._getRemoteIP();
    attributes.dnsPort = this.constructor.getDNSPort();
    return attributes;
  }

  // this needs to be implemented by child class
  async __isLinkUpInsideContainer() {
    return true;
  }

  // docker-based vpn client need to implement this function to fetch files and put them to config directory, e.g., docker-compose.yaml, corresponding files/directories to be mapped as volumes,
  // they will be put in the same directory so relative path can still be used in docker-compose
  async __prepareAssets() {

  }

  // this needs to be implemented by child class
  async _getDNSServers() {

  }

  // this needs to be implemented by child class
  static getProtocol() {

  }

  // only usable when this docker is configured to be DNS upstream server
  static getDNSPort() {
    return 53;
  }

}

module.exports = DockerBaseVPNClient;
<commit_msg>change dns port to non-static<commit_after>/*    Copyright 2016 - 2021 Firewalla Inc
 *
 *    This program is free software: you can redistribute it and/or  modify
 *    it under the terms of the GNU Affero General Public License, version 3,
 *    as published by the Free Software Foundation.
 *
 *    This program is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *    GNU Affero General Public License for more details.
 *
 *    You should have received a copy of the GNU Affero General Public License
 *    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

'use strict';

const log = require('../../../net2/logger.js')(__filename);
const fs = require('fs');
const f = require('../../../net2/Firewalla.js');
const VPNClient = require('../VPNClient.js');
const Promise = require('bluebird');
Promise.promisifyAll(fs);
const exec = require('child-process-promise').exec;
const {Address4, Address6} = require('ip-address');
const {BigInteger} = require('jsbn');
const sysManager = require('../../../net2/SysManager.js');
const YAML = require('../../../vendor_lib/yaml');
const iptables = require('../../../net2/Iptables.js');
const wrapIptables = iptables.wrapIptables;
const routing = require('../../routing/routing.js');
const scheduler = require('../../../util/scheduler.js');
const _ = require('lodash');

class DockerBaseVPNClient extends VPNClient {

  static async listProfileIds() {
    const dirPath = f.getHiddenFolder() + `/run/docker_vpn_client/${this.getProtocol()}`;
    const files = await fs.readdirAsync(dirPath).catch(() => []); // return empty array if dir not exists
    const profileIds = files.filter(filename => filename.endsWith('.settings')).map(filename => filename.slice(0, filename.length - ".settings".length));
    return profileIds;
  }

  _getSettingsPath() {
    return `${f.getHiddenFolder()}/run/docker_vpn_client/${this.constructor.getProtocol()}/${this.profileId}.settings`;
  }

  _getSubnetFilePath() {
    return `${f.getHiddenFolder()}/run/docker_vpn_client/${this.constructor.getProtocol()}/${this.profileId}.subnet`;
  }

  async _getRemoteIP() {
    const subnet = await this._getSubnet();
    if (subnet) {
      return Address4.fromBigInteger(new Address4(subnet).bigInteger().add(new BigInteger("2"))).correctForm(); // IPv4 address of gateway in container always uses second address in subnet
    }
    return null;
  }

  async _getSubnet() {
    return await fs.readFileAsync(this._getSubnetFilePath(), {encoding: "utf8"}).then(content => content.trim()).catch((err) => null);
  }

  async _getOrGenerateSubnet() {
    let subnet = await this._getSubnet();
    if (!subnet) {
      subnet = this._generateRandomNetwork(); // this returns a /30 subnet
      await fs.writeFileAsync(this._getSubnetFilePath(), subnet, {encoding: "utf8"}).catch((err) => {});
    }
    return subnet;
  }

  async destroy() {
    await super.destroy();
    await fs.unlinkAsync(this._getSettingsPath()).catch((err) => {});
    await fs.unlinkAsync(this._getSubnetFilePath()).catch((err) => {});
    await exec(`rm -rf ${this._getConfigDirectory()}`).catch((err) => {
      log.error(`Failed to remove config directory ${this._getConfigDirectory()}`, err.message);
    });
    await exec(`rm -rf ${this._getWorkingDirectory()}`).catch((err) => {
      log.error(`Failed to remove working directory ${this._getWorkingDirectory()}`, err.message);
    });
  }

  async getVpnIP4s() {
    const subnet = await this._getSubnet();
    if (subnet)
      return Address4.fromBigInteger(new Address4(subnet).bigInteger().add(new BigInteger("1"))).correctForm(); // bridge ipv4 address always uses first address in subnet
  }

  _generateRandomNetwork() {
    const ipRangeRandomMap = {
      "10.0.0.0/8": 22,
      "172.16.0.0/12": 18,
      "192.168.0.0/16": 14
    };
    let index = 0;
    while (true) {
      index = index % 3;
      const startAddress = Object.keys(ipRangeRandomMap)[index]
      const randomBits = ipRangeRandomMap[startAddress];
      const randomOffsets = Math.floor(Math.random() * Math.pow(2, randomBits)) * 4; // align with 2-bit, i.e., /30
      const subnet = Address4.fromBigInteger(new Address4(startAddress).bigInteger().add(new BigInteger(randomOffsets.toString()))).correctForm();
      if (!sysManager.inMySubnets4(subnet))
        return subnet + "/30";
      else
        index++;
    }
  }

  async _createNetwork() {
    // sudo docker network create -o "com.docker.network.bridge.name"="vpn_sslx" --subnet 10.53.204.108/30 vpn_sslx
    try {
      log.info(`Creating network ${this._getDockerNetworkName()} for vpn ${this.profileId} ...`);
      const subnet = await this._getOrGenerateSubnet();
      const cmd = `sudo bash -c "docker network inspect ${this._getDockerNetworkName()} || docker network create -o com.docker.network.bridge.name=${this.getInterfaceName()} --subnet ${subnet} ${this._getDockerNetworkName()}" &>/dev/null`;
      await exec(cmd);
    } catch(err) {
      log.error(`Got error when creating network ${this._getDockerNetworkName()} for ${this.profileId}, err:`, err.message);
    }
  }

  async _removeNetwork() {
    try {
      const cmd = `sudo docker network rm ${this._getDockerNetworkName()}`;
      await exec(cmd);
    } catch(err) {
      log.error(`Got error when rm network ${this._getDockerNetworkName()} for ${this.profileId}, err:`, err.message);
    }
  }

  _getDockerNetworkName() {
    return `n_${this.getInterfaceName()}`;
  }

  async _updateComposeYAML() {
    // update docker-compose.yaml in working directory, main purpose is to generate randomized subnet for docker bridge network
    const composeFilePath = this._getWorkingDirectory() + "/docker-compose.yaml";
    const config = await fs.readFileAsync(composeFilePath, {encoding: "utf8"}).then(content => YAML.parse(content)).catch((err) => {
      log.error(`Failed to read docker-compose.yaml from ${composeFilePath}`, err.message);
      return;
    });

    // update network config
    if (!config) {
      log.error("docker compose template file not found")
      return;
    }

    config.networks = {};
    config.networks[this._getDockerNetworkName()] = {external: true};

    const serviceNames = Object.keys(config.services);
    if(!_.isEmpty(serviceNames) && serviceNames.length === 1) {
      const serviceName = serviceNames[0];
      const service = config.services[serviceName];
      service.networks = {};
      service.networks[this._getDockerNetworkName()] = {
        "ipv4_address": await this._getRemoteIP()
      }

      service["container_name"] = this.getInterfaceName();

      // do not automatically restart container
      // set restart to "no" will cause docker compose yml parsing error
      //     "restart contains an invalid type, it should be a string"
      if(service["restart"]) delete service["restart"];

      await fs.writeFileAsync(composeFilePath, YAML.stringify(config), {encoding: "utf8"});

    } else {
      log.error("docker compose should only contain one service")
    }
  }

  async _start() {
    await this.__prepareAssets();
    await exec(`mkdir -p ${this._getWorkingDirectory()}`);
    await exec(`cp -f -r ${this._getConfigDirectory()}/* ${this._getWorkingDirectory()}`);
    await this._createNetwork();
    await this._updateComposeYAML();
    await exec(`sudo systemctl start docker-compose@${this.profileId}`);
    const remoteIP = await this._getRemoteIP();
    if (remoteIP)
      await exec(wrapIptables(`sudo iptables -w -t nat -A FW_POSTROUTING -s ${remoteIP} -j MASQUERADE`));
    let t = 0;
    while (t < 30) {
      const carrier = await fs.readFileAsync(`/sys/class/net/${this.getInterfaceName()}/carrier`, {encoding: "utf8"}).then(content => content.trim()).catch((err) => null);
      if (carrier === "1") {
        const remoteIP = await this._getRemoteIP();
        if (remoteIP) {
          // add the container IP to wan_routable so that packets from wan interfaces can be routed to the container
          await routing.addRouteToTable(remoteIP, null, this.getInterfaceName(), "wan_routable", 1024, 4);
        }
        break;
      }
      t++;
      await scheduler.delay(1000);
    }
  }

  async _stop() {
    const remoteIP = await this._getRemoteIP();
    if (remoteIP)
      await exec(wrapIptables(`sudo iptables -w -t nat -D FW_POSTROUTING -s ${remoteIP} -j MASQUERADE`)).catch((err) => {});
    await exec(`sudo systemctl stop docker-compose@${this.profileId}`);
    await this._removeNetwork();
  }

  async getRoutedSubnets() {
    const isLinkUp = await this._isLinkUp();
    if (isLinkUp) {
      const results = [];
      // no need to add the whole subnet to the routed subnets, only need to route the container's IP address
      const remoteIP = await this._getRemoteIP();
      if (remoteIP)
        results.push(remoteIP);
      return results;
    } else {
      return [];
    }
  }

  _getWorkingDirectory() {
    return `${f.getHiddenFolder()}/run/docker/${this.profileId}`;
  }

  _getConfigDirectory() {
    return `${f.getHiddenFolder()}/run/docker_vpn_client/${this.constructor.getProtocol()}/${this.profileId}`;
  }

  getUserConfigPath() {
    return `${this._getConfigDirectory()}/user_config.json`;
  }

  async saveOriginalUserConfig(config) {
    const file = this.getUserConfigPath();
    log.info(`[${this.profileId}] Saving user origin config to ${file}...`);
    await fs.writeFileAsync(file, JSON.stringify(config));
  }

  async loadOriginalUserConfig() {
    log.info(`[${this.profileId}] Loading user origin config...`);
    try {
      const file = this.getUserConfigPath();
      const raw = await fs.readFileAsync(file, {encoding: 'utf8'});
      return JSON.parse(raw);
    } catch (err) {
      log.error("Got error when loading user config, err:", err);
      return {};
    }
  }

  async checkAndSaveProfile(value) {
    const protocol = this.constructor.getProtocol();
    const config = value && value.config || {};

    log.info(`[${this.profileId}][${protocol}] saving user config file...`);

    await exec(`mkdir -p ${this._getConfigDirectory()}`);
    await this.saveOriginalUserConfig(config);
  }

  async _isLinkUp() {
    const serviceUp = await exec(`sudo docker container ls -f "name=${this.getInterfaceName()}" --format "{{.Status}}"`).then(result => result.stdout.trim().startsWith("Up ")).catch((err) => {
      log.error(`Failed to run docker container ls on ${this.profileId}`, err.message);
      return false;
    });
    if (serviceUp)
      return this.__isLinkUpInsideContainer().catch(() => false);
    else
      return false;
  }

  async getAttributes(includeContent = false) {
    const attributes = await super.getAttributes();

    const userConfig = await this.loadOriginalUserConfig();

    attributes.config = userConfig;
    attributes.type = this.constructor.getProtocol();
    attributes.remoteIP = await this._getRemoteIP();
    attributes.dnsPort = this.getDNSPort();
    return attributes;
  }

  // this needs to be implemented by child class
  async __isLinkUpInsideContainer() {
    return true;
  }

  // docker-based vpn client need to implement this function to fetch files and put them to config directory, e.g., docker-compose.yaml, corresponding files/directories to be mapped as volumes,
  // they will be put in the same directory so relative path can still be used in docker-compose
  async __prepareAssets() {

  }

  // this needs to be implemented by child class
  async _getDNSServers() {

  }

  // this needs to be implemented by child class
  static getProtocol() {

  }

  // only usable when this docker is configured to be DNS upstream server
  getDNSPort() {
    return 53;
  }

}

module.exports = DockerBaseVPNClient;
<|endoftext|>


<commit_before>var express = require("express");
var request = require("request");
var bodyParser = require("body-parser");

var app = express();
app.use(bodyParser.urlencoded({extended: false}));
app.use(bodyParser.json());
app.listen((process.env.PORT || 5000));

// Server index page
app.get("/", function (req, res) {
  res.send("Deployed!");
});

//init
var Glassdoor = require('node-glassdoor').initGlassdoor({
    partnerId: 208235,
    partnerKey: "cZU1Vm4x2ls"
});

// Facebook Webhook
// Used for verification
app.get("/webhook", function (req, res) {
  if (req.query["hub.verify_token"] === "team-zzzz") {
    console.log("Verified webhook");
    res.status(200).send(req.query["hub.challenge"]);
  } else {
    console.error("Verification failed. The tokens do not match.");
    res.sendStatus(403);
  }
});

// All callbacks for Messenger will be POST-ed here
/*app.post("/webhook", function (req, res) {
  // Make sure this is a page subscription
  if (req.body.object == "page") {
		console.log("test1");
		// Iterate over each entry
    // There may be multiple entries if batched
    req.body.entry.forEach(function(entry) {
			console.log("test2");
      // Iterate over each messaging event
      entry.messaging.forEach(function(event) {
				//var senderId = event.sender.id;
				//var payload = event.postback.payload;
				//sendMessage(senderId, {text: "hi back"});
				//console.log("dskfskndskfnds");
        if (event.postback) {
          processPostback(event);
					console.log("test");
        } else if (event.message) {
					processMessage(event);
				}
				}
      });
    })
    res.sendStatus(200);
		console.log("test3");
  }
	console.log('test4');
});*/

// All callbacks for Messenger will be POST-ed here
app.post("/webhook", function (req, res) {
  // Make sure this is a page subscription
	console.log("hi");
  if (req.body.object == "page") {
		console.log("hey");
    // Iterate over each entry
    // There may be multiple entries if batched
    req.body.entry.forEach(function(entry) {
			console.log("hiho");
      // Iterate over each messaging event
      entry.messaging.forEach(function(event) {
				console.log("watsup");
        if (event.postback) {
					console.log("bye");
          processPostback(event);
        } else if (event.message) {
					console.log("hihi");
          processMessage(event);
        }
      });
    });
		console.log("byebye");
    res.sendStatus(200);
  }
});

function processPostback(event) {
  var senderId = event.sender.id;
  var payload = event.postback.payload;
	console.log(senderId);
  if (payload === "Greeting") {
    // Get user's first name from the User Profile API
    // and include it in the greeting
    request({
      url: "https://graph.facebook.com/v2.6/" + senderId,
      qs: {
        access_token: process.env.PAGE_ACCESS_TOKEN,
        fields: "first_name"
      },
      method: "GET"
    }, function(error, response, body) {
      var greeting = "";
      if (error) {
        console.log("Error getting user's name: " +  error);
      } else {
        var bodyObj = JSON.parse(body);
        name = bodyObj.first_name;
        greeting = "Hi " + name + ". ";
      }
      var message = greeting + "My name is SP Movie Bot. I can tell you various details regarding movies. What movie would you like to know about?";
      sendMessage(senderId, {text: message});
    });
  }
}

// sends message to user
function sendMessage(recipientId, message) {
  request({
    url: "https://graph.facebook.com/v2.6/me/messages",
    qs: {access_token: process.env.PAGE_ACCESS_TOKEN},
    method: "POST",
    json: {
      recipient: {id: recipientId},
      message: message,
    }
  }, function(error, response, body) {
    if (error) {
      console.log("Error sending message: " + response.error);
    }
  });
}

function sendURL(recipientId){
	request({
    url: "https://graph.facebook.com/v2.6/me/messages",
    qs: {access_token: process.env.PAGE_ACCESS_TOKEN},
    method: "POST",
    json: {
      recipient: {id: recipientId},
      message:     message: {
			      attachment: {
			        type: "template",
			        payload: {
			          template_type: "generic",
			          elements: [{
			            title: "rift",
			            subtitle: "Next-generation virtual reality",
			            item_url: "https://www.oculus.com/en-us/rift/",
			            image_url: "http://messengerdemo.parseapp.com/img/rift.png",
			            buttons: [{
			              type: "web_url",
			              url: "https://www.oculus.com/en-us/rift/",
			              title: "Open Web URL"
			            }, {
			              type: "postback",
			              title: "Call Postback",
			              payload: "Payload for first bubble",
			            }],
			          }, {
			            title: "touch",
			            subtitle: "Your Hands, Now in VR",
			            item_url: "https://www.oculus.com/en-us/touch/",
			            image_url: "http://messengerdemo.parseapp.com/img/touch.png",
			            buttons: [{
			              type: "web_url",
			              url: "https://www.oculus.com/en-us/touch/",
			              title: "Open Web URL"
			            }, {
			              type: "postback",
			              title: "Call Postback",
			              payload: "Payload for second bubble",
			            }]
			          }]
			        }
			      }
			    }
    }
  }, function(error, response, body) {
    if (error) {
      console.log("Error sending message: " + response.error);
    }
  });
}

/*function sendGenericMessage(recipientId) {
  var messageData = {
    recipient: {
      id: recipientId
    },
    message: {
      attachment: {
        type: "template",
        payload: {
          template_type: "generic",
          elements: [{
            title: "rift",
            subtitle: "Next-generation virtual reality",
            item_url: "https://www.oculus.com/en-us/rift/",
            image_url: "http://messengerdemo.parseapp.com/img/rift.png",
            buttons: [{
              type: "web_url",
              url: "https://www.oculus.com/en-us/rift/",
              title: "Open Web URL"
            }, {
              type: "postback",
              title: "Call Postback",
              payload: "Payload for first bubble",
            }],
          }, {
            title: "touch",
            subtitle: "Your Hands, Now in VR",
            item_url: "https://www.oculus.com/en-us/touch/",
            image_url: "http://messengerdemo.parseapp.com/img/touch.png",
            buttons: [{
              type: "web_url",
              url: "https://www.oculus.com/en-us/touch/",
              title: "Open Web URL"
            }, {
              type: "postback",
              title: "Call Postback",
              payload: "Payload for second bubble",
            }]
          }]
        }
      }
    }
  };

  callSendAPI(messageData);
}*/

function processMessage(event) {
	console.log("here");
	console.log(event.message);
	event.message.is_echo = false;
  if (!event.message.is_echo) {
    var message = event.message;
    var senderId = event.sender.id;

    console.log("Received message from senderId: " + senderId);
    console.log("Message is: " + JSON.stringify(message));

    // You may get a text or attachment but not both
    if (message.text) {
      var formattedMsg = message.text.toLowerCase().trim();

      // If we receive a text message, check to see if it matches any special
      // keywords and send back the corresponding movie detail.
      // Otherwise, search for new movie.
			if(formattedMsg.includes("yenni")){
				sendMessage(senderId, {text: "How can I help?"});
			} else if (formattedMsg.includes("uni") || formattedMsg.includes("think")) {
				sendMessage(senderId, {text: "How did you do on your exams?"});
			} else if (formattedMsg.includes("maths")) {
				//sendMessage(senderId, {text: "Ok cool. Let me have a look."});
				sendMessage(senderId, {text: "Here are some options based on your grades: Data Analyst, Statistician, Accountant"});
				sendMessage(senderId, {text: "Here are some options based on your interests: Photographer, Graphic Designer, Comic Book Artist, Traveller, Accountant"});
			} else if (formattedMsg.includes("accountant")) {
				sendMessage(senderId, {text: "An accountant is a professional person who performs accounting functions such as audits or financial statement analysis."});
				//sendMessage(senderId, {text: "Accountants are given certifications by national professional associations, after meeting state-specific requirements, although non-qualified persons can still work under other accountants, or independently."});
				sendMessage(senderId, {text: "The benefits of being an Accountant are: potential for flexible working, travel, utilises your strengths in maths."});
				sendMessage(senderId, {text: "The transferable skills you will gain from being an Accountant are: time management, project management, communication skills. You currently have skills in: commnication, mathematics and tax legislation"});

				//sendMessage(senderId, {text: "So what do you think?"});
			} else if (formattedMsg.includes("companies")) {
				//sendMessage(senderId, {text: "Happy to help."});
				sendURL(senderId);
			}
      }
    } else if (message.attachments) {
      sendMessage(senderId, {text: "Sorry, I don't understand your request."});
    }
  }
<commit_msg>debug<commit_after>var express = require("express");
var request = require("request");
var bodyParser = require("body-parser");

var app = express();
app.use(bodyParser.urlencoded({extended: false}));
app.use(bodyParser.json());
app.listen((process.env.PORT || 5000));

// Server index page
app.get("/", function (req, res) {
  res.send("Deployed!");
});

//init
var Glassdoor = require('node-glassdoor').initGlassdoor({
    partnerId: 208235,
    partnerKey: "cZU1Vm4x2ls"
});

// Facebook Webhook
// Used for verification
app.get("/webhook", function (req, res) {
  if (req.query["hub.verify_token"] === "team-zzzz") {
    console.log("Verified webhook");
    res.status(200).send(req.query["hub.challenge"]);
  } else {
    console.error("Verification failed. The tokens do not match.");
    res.sendStatus(403);
  }
});

// All callbacks for Messenger will be POST-ed here
/*app.post("/webhook", function (req, res) {
  // Make sure this is a page subscription
  if (req.body.object == "page") {
		console.log("test1");
		// Iterate over each entry
    // There may be multiple entries if batched
    req.body.entry.forEach(function(entry) {
			console.log("test2");
      // Iterate over each messaging event
      entry.messaging.forEach(function(event) {
				//var senderId = event.sender.id;
				//var payload = event.postback.payload;
				//sendMessage(senderId, {text: "hi back"});
				//console.log("dskfskndskfnds");
        if (event.postback) {
          processPostback(event);
					console.log("test");
        } else if (event.message) {
					processMessage(event);
				}
				}
      });
    })
    res.sendStatus(200);
		console.log("test3");
  }
	console.log('test4');
});*/

// All callbacks for Messenger will be POST-ed here
app.post("/webhook", function (req, res) {
  // Make sure this is a page subscription
	console.log("hi");
  if (req.body.object == "page") {
		console.log("hey");
    // Iterate over each entry
    // There may be multiple entries if batched
    req.body.entry.forEach(function(entry) {
			console.log("hiho");
      // Iterate over each messaging event
      entry.messaging.forEach(function(event) {
				console.log("watsup");
        if (event.postback) {
					console.log("bye");
          processPostback(event);
        } else if (event.message) {
					console.log("hihi");
          processMessage(event);
        }
      });
    });
		console.log("byebye");
    res.sendStatus(200);
  }
});

function processPostback(event) {
  var senderId = event.sender.id;
  var payload = event.postback.payload;
	console.log(senderId);
  if (payload === "Greeting") {
    // Get user's first name from the User Profile API
    // and include it in the greeting
    request({
      url: "https://graph.facebook.com/v2.6/" + senderId,
      qs: {
        access_token: process.env.PAGE_ACCESS_TOKEN,
        fields: "first_name"
      },
      method: "GET"
    }, function(error, response, body) {
      var greeting = "";
      if (error) {
        console.log("Error getting user's name: " +  error);
      } else {
        var bodyObj = JSON.parse(body);
        name = bodyObj.first_name;
        greeting = "Hi " + name + ". ";
      }
      var message = greeting + "My name is SP Movie Bot. I can tell you various details regarding movies. What movie would you like to know about?";
      sendMessage(senderId, {text: message});
    });
  }
}

// sends message to user
function sendMessage(recipientId, message) {
  request({
    url: "https://graph.facebook.com/v2.6/me/messages",
    qs: {access_token: process.env.PAGE_ACCESS_TOKEN},
    method: "POST",
    json: {
      recipient: {id: recipientId},
      message: message,
    }
  }, function(error, response, body) {
    if (error) {
      console.log("Error sending message: " + response.error);
    }
  });
}

function sendURL(recipientId){
	request({
    url: "https://graph.facebook.com/v2.6/me/messages",
    qs: {access_token: process.env.PAGE_ACCESS_TOKEN},
    method: "POST",
    json: {
      recipient: {id: recipientId},
      message: {
			      attachment: {
			        type: "template",
			        payload: {
			          template_type: "generic",
			          elements: [{
			            title: "rift",
			            subtitle: "Next-generation virtual reality",
			            item_url: "https://www.oculus.com/en-us/rift/",
			            image_url: "http://messengerdemo.parseapp.com/img/rift.png",
			            buttons: [{
			              type: "web_url",
			              url: "https://www.oculus.com/en-us/rift/",
			              title: "Open Web URL"
			            }, {
			              type: "postback",
			              title: "Call Postback",
			              payload: "Payload for first bubble",
			            }],
			          }, {
			            title: "touch",
			            subtitle: "Your Hands, Now in VR",
			            item_url: "https://www.oculus.com/en-us/touch/",
			            image_url: "http://messengerdemo.parseapp.com/img/touch.png",
			            buttons: [{
			              type: "web_url",
			              url: "https://www.oculus.com/en-us/touch/",
			              title: "Open Web URL"
			            }, {
			              type: "postback",
			              title: "Call Postback",
			              payload: "Payload for second bubble",
			            }]
			          }]
			        }
			      }
			    }
    }
  }, function(error, response, body) {
    if (error) {
      console.log("Error sending message: " + response.error);
    }
  });
}

/*function sendGenericMessage(recipientId) {
  var messageData = {
    recipient: {
      id: recipientId
    },
    message: {
      attachment: {
        type: "template",
        payload: {
          template_type: "generic",
          elements: [{
            title: "rift",
            subtitle: "Next-generation virtual reality",
            item_url: "https://www.oculus.com/en-us/rift/",
            image_url: "http://messengerdemo.parseapp.com/img/rift.png",
            buttons: [{
              type: "web_url",
              url: "https://www.oculus.com/en-us/rift/",
              title: "Open Web URL"
            }, {
              type: "postback",
              title: "Call Postback",
              payload: "Payload for first bubble",
            }],
          }, {
            title: "touch",
            subtitle: "Your Hands, Now in VR",
            item_url: "https://www.oculus.com/en-us/touch/",
            image_url: "http://messengerdemo.parseapp.com/img/touch.png",
            buttons: [{
              type: "web_url",
              url: "https://www.oculus.com/en-us/touch/",
              title: "Open Web URL"
            }, {
              type: "postback",
              title: "Call Postback",
              payload: "Payload for second bubble",
            }]
          }]
        }
      }
    }
  };

  callSendAPI(messageData);
}*/

function processMessage(event) {
	console.log("here");
	console.log(event.message);
	event.message.is_echo = false;
  if (!event.message.is_echo) {
    var message = event.message;
    var senderId = event.sender.id;

    console.log("Received message from senderId: " + senderId);
    console.log("Message is: " + JSON.stringify(message));

    // You may get a text or attachment but not both
    if (message.text) {
      var formattedMsg = message.text.toLowerCase().trim();

      // If we receive a text message, check to see if it matches any special
      // keywords and send back the corresponding movie detail.
      // Otherwise, search for new movie.
			if(formattedMsg.includes("yenni")){
				sendMessage(senderId, {text: "How can I help?"});
			} else if (formattedMsg.includes("uni") || formattedMsg.includes("think")) {
				sendMessage(senderId, {text: "How did you do on your exams?"});
			} else if (formattedMsg.includes("maths")) {
				//sendMessage(senderId, {text: "Ok cool. Let me have a look."});
				sendMessage(senderId, {text: "Here are some options based on your grades: Data Analyst, Statistician, Accountant"});
				sendMessage(senderId, {text: "Here are some options based on your interests: Photographer, Graphic Designer, Comic Book Artist, Traveller, Accountant"});
			} else if (formattedMsg.includes("accountant")) {
				sendMessage(senderId, {text: "An accountant is a professional person who performs accounting functions such as audits or financial statement analysis."});
				//sendMessage(senderId, {text: "Accountants are given certifications by national professional associations, after meeting state-specific requirements, although non-qualified persons can still work under other accountants, or independently."});
				sendMessage(senderId, {text: "The benefits of being an Accountant are: potential for flexible working, travel, utilises your strengths in maths."});
				sendMessage(senderId, {text: "The transferable skills you will gain from being an Accountant are: time management, project management, communication skills. You currently have skills in: commnication, mathematics and tax legislation"});

				//sendMessage(senderId, {text: "So what do you think?"});
			} else if (formattedMsg.includes("companies")) {
				//sendMessage(senderId, {text: "Happy to help."});
				sendURL(senderId);
			}
      }
    } else if (message.attachments) {
      sendMessage(senderId, {text: "Sorry, I don't understand your request."});
    }
  }
<|endoftext|>


<commit_before>'use strict';

var months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]

var express = require('express');
var app = express();

app.get('/', function(req,res) {
	res.writeHead(200, {'content-type': 'text/html'})
	res.end(`

API Basejump: Timestamp Microservice

	Add a string to the end of the URL to use the microservice...

	For example:

	https://mysterious-reef-70509.herokuapp.com/April 11, 1977`)
})

app.get('/:date', function(req, res) {
	var timestamp = {
		"unix": null,
		"natural": null
	}

	var inputDate = req.params.date
	var inputDateAsDate = new Date()
	var d = Date.parse(inputDate)
	var naturalDate

	inputDateAsDate.setTime(inputDate * 1000) // convert to milliseconds...

	res.writeHead(200, {'Accept': 'application/json'})

	if ( isNaN(d) ) {
		// date is not valid as a natural date - check to see if it was a unix date..
		// d = inputDateAsDate
		var unixDate = Date.parse(inputDateAsDate)
		if ( isNaN(unixDate) ) {
			// date is invalid
			timestamp.unix = null
			timestamp.natural = null
			console.log('Invalid Date by time...')
			res.end(JSON.stringify(timestamp))
		}
		else {
			// date is valid...
			naturalDate = new Date(unixDate)
			timestamp.unix = inputDate
			timestamp.natural = months[naturalDate.getMonth()] + ' ' + naturalDate.getDate() + ', ' + naturalDate.getFullYear()
			res.end(JSON.stringify(timestamp))
		}
	}
	else {
		// date is valid
		naturalDate = new Date(d)
		timestamp.unix = d / 1000
		timestamp.natural = months[naturalDate.getMonth()] + ' ' + naturalDate.getDate() + ', ' + naturalDate.getFullYear()
		res.end(JSON.stringify(timestamp))
	}

	console.log('Input:', inputDate)
	console.log('Unix:', timestamp.unix)
	console.log('Natural:', timestamp.natural)
})

var port = process.env.PORT || 8080;
app.listen(port,  function () {
	console.log('Node.js listening on port ' + port + '...');
});<commit_msg>req change - use utc date<commit_after>'use strict';

var months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]

var express = require('express');
var app = express();

app.get('/', function(req,res) {
	res.redirect(`/${new Date()}`)
})

app.get('/:date', function(req, res) {
	var timestamp = {
		"unix": null,
		"utc": null
	}

	var inputDate = req.params.date
	var inputDateAsDate = new Date()
	var d = Date.parse(inputDate)
	var utcDate

	inputDateAsDate.setTime(inputDate * 1000) // convert to milliseconds...

	res.writeHead(200, {'Accept': 'application/json'})

	if ( isNaN(d) ) {
		// date is not valid as a natural date - check to see if it was a unix date..
		// d = inputDateAsDate
		var unixDate = Date.parse(inputDateAsDate)
		if ( isNaN(unixDate) ) {
			// date is invalid
			timestamp.unix = null
			timestamp.utc= null
			console.log('Invalid Date by time...')
			res.end(JSON.stringify(timestamp))
		}
		else {
			// date is valid...
			timestamp.unix = inputDate
			timestamp.utc = new Date(unixDate).toUTCString()
			res.end(JSON.stringify(timestamp))
		}
	}
	else {
		// date is valid
		timestamp.unix = d / 1000
		timestamp.utc = new Date(d).toUTCString()
		res.end(JSON.stringify(timestamp))
	}

	console.log('Input:', inputDate)
	console.log('Unix:', timestamp.unix)
	console.log('UTC:', timestamp.utc)
})

var port = process.env.PORT || 8080;
app.listen(port,  function () {
	console.log('Node.js listening on port ' + port + '...');
});<|endoftext|>