<commit_before>var bisect = require('./bisect');
var EE = require('events').EventEmitter;
var inherits = require('inherits');

function quantile() {
    var self = this;
    function getQuantile(q) {
        if (!self.length) return NaN;
        var p = (self.length - 1) * q;
        var i = Math.floor(p);
        var j = Math.ceil(p);
        return (self[i] + self[j])/2;
    }
    switch (arguments.length) {
        case 0:
            return;
        case 1:
            return getQuantile(arguments[0]);
        default:
            return Array.prototype.map.call(arguments, getQuantile);
    }
}

function Sample() {
    this.values = [];
    this.quantile = quantile.bind(this.values);
    Object.defineProperty(this, 'median', {
        enumerable: true,
        get: this.getMedian
    });
    Object.defineProperty(this, 'iqr', {
        enumerable: true,
        get: this.getIQR
    });
    Object.defineProperty(this, 'lo', {
        enumerable: true,
        get: this.getLo
    });
    Object.defineProperty(this, 'hi', {
        enumerable: true,
        get: this.getHi
    });
    Object.defineProperty(this, 'range', {
        enumerable: true,
        get: this.getRange
    });
}

inherits(Sample, EE);

Sample.prototype.getMedian = function getMedian() {
    return this.quantile(0.5);
};

Sample.prototype.getIQR = function getIQR() {
    if (this.values.length < 3) return NaN;
    var qs = this.quantile(0.25, 0.75);
    return qs[1] - qs[0];
};

Sample.prototype.getLo = function getLo() {
    if (this.values.length < 3) return NaN;
    var qs = this.quantile(0.25, 0.50, 0.75);
    var iqr = qs[2] - qs[0];
    var tol = 3 * iqr / 2;
    return qs[0] - tol;
};

Sample.prototype.getHi = function getHi() {
    if (this.values.length < 3) return NaN;
    var qs = this.quantile(0.25, 0.50, 0.75);
    var iqr = qs[2] - qs[0];
    var tol = 3 * iqr / 2;
    return qs[2] + tol;
};

Sample.prototype.getRange = function getRange() {
    if (this.values.length < 3) return [NaN, NaN, NaN];
    var qs = this.quantile(0.25, 0.50, 0.75);
    var iqr = qs[2] - qs[0];
    var tol = 3 * iqr / 2;
    return [qs[0] - tol, qs[1], qs[2] + tol];
};

Sample.prototype.add = function addValue(value) {
    var i = bisect.right(this.values, value);
    this.values.splice(i, 0, value);
    this.emit('change');
};

module.exports = Sample;
<commit_msg>sample: track order<commit_after>var bisect = require('./bisect');
var EE = require('events').EventEmitter;
var inherits = require('inherits');

function quantile() {
    var self = this;
    function getQuantile(q) {
        if (!self.length) return NaN;
        var p = (self.length - 1) * q;
        var i = Math.floor(p);
        var j = Math.ceil(p);
        return (self[i] + self[j])/2;
    }
    switch (arguments.length) {
        case 0:
            return;
        case 1:
            return getQuantile(arguments[0]);
        default:
            return Array.prototype.map.call(arguments, getQuantile);
    }
}

function Sample() {
    this.order = [];
    this.values = [];
    this.quantile = quantile.bind(this.values);
    Object.defineProperty(this, 'median', {
        enumerable: true,
        get: this.getMedian
    });
    Object.defineProperty(this, 'iqr', {
        enumerable: true,
        get: this.getIQR
    });
    Object.defineProperty(this, 'lo', {
        enumerable: true,
        get: this.getLo
    });
    Object.defineProperty(this, 'hi', {
        enumerable: true,
        get: this.getHi
    });
    Object.defineProperty(this, 'range', {
        enumerable: true,
        get: this.getRange
    });
}

inherits(Sample, EE);

Sample.prototype.getMedian = function getMedian() {
    return this.quantile(0.5);
};

Sample.prototype.getIQR = function getIQR() {
    if (this.values.length < 3) return NaN;
    var qs = this.quantile(0.25, 0.75);
    return qs[1] - qs[0];
};

Sample.prototype.getLo = function getLo() {
    if (this.values.length < 3) return NaN;
    var qs = this.quantile(0.25, 0.50, 0.75);
    var iqr = qs[2] - qs[0];
    var tol = 3 * iqr / 2;
    return qs[0] - tol;
};

Sample.prototype.getHi = function getHi() {
    if (this.values.length < 3) return NaN;
    var qs = this.quantile(0.25, 0.50, 0.75);
    var iqr = qs[2] - qs[0];
    var tol = 3 * iqr / 2;
    return qs[2] + tol;
};

Sample.prototype.getRange = function getRange() {
    if (this.values.length < 3) return [NaN, NaN, NaN];
    var qs = this.quantile(0.25, 0.50, 0.75);
    var iqr = qs[2] - qs[0];
    var tol = 3 * iqr / 2;
    return [qs[0] - tol, qs[1], qs[2] + tol];
};

Sample.prototype.add = function addValue(value) {
    var i = bisect.right(this.values, value);
    this.values.splice(i, 0, value);
    for (var o=this.order, n=o.length, j=0; j<n; j++)
        if (o[j] >= i) o[j]++;
    this.order.push(i);
    this.emit('change');
};

Sample.prototype.inOrder = function inOrder() {
    var ar = new Array(this.order.length);
    for (var o=this.order, n=o.length, i=0; i<n; i++)
        ar[i] = this.values[o[i]];
    return ar;
};

module.exports = Sample;
<|endoftext|>


<commit_before>'use strict';

var Immutable         = require('immutable');
var {is, fromJS,
    Map, OrderedMap}  = Immutable;
var invariant         = require('./invariant');
var messages          = require('./messages');
var emptyFunction     = require('./emptyFunction');
var ValidationResult  = require('./ValidationResult');

var EMPTY_MAP = Map();

/**
 * Node represent schema for data.
 *
 * Hierarchy is the following:
 *
 *                       ScalarNode
 *                      /
 *   Node - ConcreteNode               MappingNode
 *                      \             /
 *                       CompositeNode
 *                                    \
 *                                     ListNode
 */
class Node {

  constructor(props) {
    this.props = props;
  }

  equals(node) {
    if (
      node === null || node === undefined ||
      node.constructor !== undefined &&
      node.constructor !== this.constructor
    ) {
      return false;
    }
    return is(this.props, node.props) && is(this.children, node.children);
  }

  get defaultValue() {
    if (this.__defaultValue === undefined) {
      this.__defaultValue = fromJS(this.props.get('defaultValue'));
    }
    return this.__defaultValue;
  }

  instantiate(value) {
    throw new Error('instantiate(value): not implemented');
  }

  validate(value, childrenValidation) {
    throw new Error('validate(value, childrenValidation): not implemented');
  }

  toString() {
    var props = this.props.map((v, k) => `${k}: ${v}`).toList().join(', ');
    return `${this.constructor.name} { ${props} }`;
  }

  static create(props) {
    props = Map(props || {});
    return new this(props);
  }
}

class ConcreteNode extends Node {

  validate(value, childrenValidation) {
    if (value == null) {
      return;
    }
    if (!childrenValidation.isSuccess) {
      return childrenValidation;
    }
    var result;
    var validate = this.props.get('validate');
    // `validate` could be an array for convenienve
    if (Array.isArray(validate)) {
      for (var i = 0, len = validate.length; i < len; i++) {
        result = validate[i](this, value);
        if (result instanceof Error) {
          result;
        }
      }
    } else  if (validate) {
      result = validate(this, value);
    }
    return result;
  }

  instantiate(value) {
    return {node: this, value};
  }
}

class CompositeNode extends ConcreteNode {

  get children() {
    if (this.__children === undefined) {
      this.__children = this.getChildren();
    }
    return this.__children;
  }

  getChildren() {
    throw new Error('getChildren(): not implemented');
  }

  get(key) {
    throw new Error('get(key): not implemented');
  }

  has(key) {
    throw new Error('has(key): not implemented');
  }

  keys(value) {
    throw new Error('keys(value): not implemented');
  }
}

class ScalarNode extends ConcreteNode {

  validate(value, childrenValidation) {
    if (value == null) {
      return;
    }
    value = this.deserialize(value);
    if (value instanceof Error) {
      return value;
    } else {
      return super(value, childrenValidation)
    }
  }

  serialize(value) {
    return value === null ? '' : value;
  }

  deserialize(value) {
    return value === '' ? null : value;
  }
}

class NumberNode extends ScalarNode {

  deserialize(value) {
    if (value === '') {
      return null;
    // based on http://stackoverflow.com/a/1830844/182954
    } else if (!isNaN(parseFloat(value)) && isFinite(value)) {
      return parseFloat(value);
    } else {
      return new Error(messages.INVALID_VALUE);
    }
  }
}

class BoolNode extends ScalarNode {

  serialize(value) {
    return value;
  }

  deserialize(value) {
    return value;
  }

}

class ArrayNode extends ScalarNode {

  serialize(value) {
    return value ? value : [];
  }

  deserialize(value) {
    return value;
  }
}

var IS_DATE_RE =  /^\d\d\d\d-\d\d-\d\d$/;

class DateNode extends ScalarNode {

  serialize(value) {
    if (value == null) {
      return '';
    }
    var year = value.getFullYear();
    var month = value.getMonth() + 1;
    var day = value.getDate();
    return `${year}-${this.pad(month, 2)}-${this.pad(day, 2)}`;
  }

  deserialize(value) {
    if (value === '') {
      return null;
    }

    if (value instanceof Date) {
      return value;
    }

    if (!IS_DATE_RE.exec(value)) {
      return new Error(messages.IS_NOT_A_DATE);
    }

    value = new Date(value);

    if (isNaN(value.getTime())) {
      return new Error(messages.INVALID_VALUE);
    }

    return value;
  }

  pad(num, size) {
    return ('0000' + num).substr(-size);
  }
}

function Scalar(props) {
  switch (props && props.type || 'string') {
    case 'string':
      return ScalarNode.create(props);
    case 'number':
      return NumberNode.create(props);
    case 'array':
      return ArrayNode.create(props);
    case 'date':
      return DateNode.create(props);
    case 'bool':
      return BoolNode.create(props);
    default:
      invariant(
        false,
        `invalid type "${props.type}" supplied to Scalar`
      );
  }
}

var VALUE_IS_REQUIRED = ValidationResult.error(messages.VALUE_IS_REQUIRED);

class MappingNode extends CompositeNode {

  constructor(props) {
    super(props);
    this.__requiredKeys = props.get('children', EMPTY_MAP)
      .toSeq()
      .filter(child => child.props.get('required'))
      .keySeq()
      .toSet();
  }

  validate(value, childrenValidation) {
    var missingKeys = this.__requiredKeys.subtract(value.filter(v => v != null).keys());
    if (missingKeys.size > 0) {
      var missingKeysValidation = Map().asMutable();
      missingKeys.forEach(k =>
        missingKeysValidation = missingKeysValidation.set(k, VALUE_IS_REQUIRED));
      return ValidationResult.children(childrenValidation.children.merge(missingKeysValidation));
    }
    return super(value, childrenValidation);
  }

  getChildren() {
    return this.props.get('children');
  }

  get(key) {
    return this.children.get(key);
  }

  has(key) {
    return this.children.has(key);
  }

  keys(value) {
    return this.children.keys();
  }
}

function Mapping(props, children) {
  if (children === undefined) {
    children = props;
    children = OrderedMap(children);
    props = {children};
  } else {
    children = OrderedMap(children);
    var newProps = {children};
    for (var k in props) {
      if (props.hasOwnProperty(k)) {
        newProps[k] = props[k];
      }
    }
    props = newProps;
  }
  props = Map(props);
  return new MappingNode(props);
}

class ListNode extends CompositeNode {

  getChildren() {
    return this.props.get('children');
  }

  get(key) {
    return this.children;
  }

  has(key) {
    return true;
  }

  keys(value) {
    return value.keys();
  }
}

function List(props, children) {
  if (children === undefined) {
    children = props;
    props = {children};
  } else {
    var newProps = {};
    for (var k in props) {
      if (props.hasOwnProperty(k)) {
        newProps[k] = props[k];
      }
      newProps.children = children;
    }
    props = newProps;
  }
  props = Map(props);
  return new ListNode(props);
}

module.exports = {
  Node,
  ScalarNode, NumberNode, ArrayNode,
  CompositeNode,
  MappingNode, ListNode,
  Scalar, Mapping, List
};
<commit_msg>schema: getDefaultProps()<commit_after>'use strict';

var Immutable         = require('immutable');
var {is, fromJS,
    Map, OrderedMap}  = Immutable;
var invariant         = require('./invariant');
var messages          = require('./messages');
var emptyFunction     = require('./emptyFunction');
var ValidationResult  = require('./ValidationResult');

var EMPTY_MAP = Map();

/**
 * Node represent schema for data.
 *
 * Hierarchy is the following:
 *
 *                       ScalarNode
 *                      /
 *   Node - ConcreteNode               MappingNode
 *                      \             /
 *                       CompositeNode
 *                                    \
 *                                     ListNode
 */
class Node {

  constructor(props) {
    props = Map(this.getDefaultProps()).merge(props);
    this.props = props;
  }

  getDefaultProps() {
    return EMPTY_MAP;
  }

  equals(node) {
    if (
      node === null || node === undefined ||
      node.constructor !== undefined &&
      node.constructor !== this.constructor
    ) {
      return false;
    }
    return is(this.props, node.props) && is(this.children, node.children);
  }

  get defaultValue() {
    if (this.__defaultValue === undefined) {
      this.__defaultValue = fromJS(this.props.get('defaultValue'));
    }
    return this.__defaultValue;
  }

  instantiate(value) {
    throw new Error('instantiate(value): not implemented');
  }

  validate(value, childrenValidation) {
    throw new Error('validate(value, childrenValidation): not implemented');
  }

  toString() {
    var props = this.props.map((v, k) => `${k}: ${v}`).toList().join(', ');
    return `${this.constructor.name} { ${props} }`;
  }

  static create(props) {
    props = Map(props || {});
    return new this(props);
  }
}

class ConcreteNode extends Node {

  validate(value, childrenValidation) {
    if (value == null) {
      return;
    }
    if (!childrenValidation.isSuccess) {
      return childrenValidation;
    }
    var result;
    var validate = this.props.get('validate');
    // `validate` could be an array for convenienve
    if (Array.isArray(validate)) {
      for (var i = 0, len = validate.length; i < len; i++) {
        result = validate[i](this, value);
        if (result instanceof Error) {
          result;
        }
      }
    } else  if (validate) {
      result = validate(this, value);
    }
    return result;
  }

  instantiate(value) {
    return {node: this, value};
  }
}

class CompositeNode extends ConcreteNode {

  get children() {
    if (this.__children === undefined) {
      this.__children = this.getChildren();
    }
    return this.__children;
  }

  getChildren() {
    throw new Error('getChildren(): not implemented');
  }

  get(key) {
    throw new Error('get(key): not implemented');
  }

  has(key) {
    throw new Error('has(key): not implemented');
  }

  keys(value) {
    throw new Error('keys(value): not implemented');
  }
}

class ScalarNode extends ConcreteNode {

  validate(value, childrenValidation) {
    if (value == null) {
      return;
    }
    value = this.deserialize(value);
    if (value instanceof Error) {
      return value;
    } else {
      return super(value, childrenValidation)
    }
  }

  serialize(value) {
    return value === null ? '' : value;
  }

  deserialize(value) {
    return value === '' ? null : value;
  }
}

class NumberNode extends ScalarNode {

  deserialize(value) {
    if (value === '') {
      return null;
    // based on http://stackoverflow.com/a/1830844/182954
    } else if (!isNaN(parseFloat(value)) && isFinite(value)) {
      return parseFloat(value);
    } else {
      return new Error(messages.INVALID_VALUE);
    }
  }
}

class BoolNode extends ScalarNode {

  serialize(value) {
    return value;
  }

  deserialize(value) {
    return value;
  }

}

class ArrayNode extends ScalarNode {

  serialize(value) {
    return value ? value : [];
  }

  deserialize(value) {
    return value;
  }
}

var IS_DATE_RE =  /^\d\d\d\d-\d\d-\d\d$/;

class DateNode extends ScalarNode {

  serialize(value) {
    if (value == null) {
      return '';
    }
    var year = value.getFullYear();
    var month = value.getMonth() + 1;
    var day = value.getDate();
    return `${year}-${this.pad(month, 2)}-${this.pad(day, 2)}`;
  }

  deserialize(value) {
    if (value === '') {
      return null;
    }

    if (value instanceof Date) {
      return value;
    }

    if (!IS_DATE_RE.exec(value)) {
      return new Error(messages.IS_NOT_A_DATE);
    }

    value = new Date(value);

    if (isNaN(value.getTime())) {
      return new Error(messages.INVALID_VALUE);
    }

    return value;
  }

  pad(num, size) {
    return ('0000' + num).substr(-size);
  }
}

function Scalar(props) {
  switch (props && props.type || 'string') {
    case 'string':
      return ScalarNode.create(props);
    case 'number':
      return NumberNode.create(props);
    case 'array':
      return ArrayNode.create(props);
    case 'date':
      return DateNode.create(props);
    case 'bool':
      return BoolNode.create(props);
    default:
      invariant(
        false,
        `invalid type "${props.type}" supplied to Scalar`
      );
  }
}

var VALUE_IS_REQUIRED = ValidationResult.error(messages.VALUE_IS_REQUIRED);

class MappingNode extends CompositeNode {

  constructor(props) {
    super(props);
    this.__requiredKeys = props.get('children', EMPTY_MAP)
      .toSeq()
      .filter(child => child.props.get('required'))
      .keySeq()
      .toSet();
  }

  validate(value, childrenValidation) {
    var missingKeys = this.__requiredKeys.subtract(value.filter(v => v != null).keys());
    if (missingKeys.size > 0) {
      var missingKeysValidation = Map().asMutable();
      missingKeys.forEach(k =>
        missingKeysValidation = missingKeysValidation.set(k, VALUE_IS_REQUIRED));
      return ValidationResult.children(childrenValidation.children.merge(missingKeysValidation));
    }
    return super(value, childrenValidation);
  }

  getChildren() {
    return this.props.get('children');
  }

  get(key) {
    return this.children.get(key);
  }

  has(key) {
    return this.children.has(key);
  }

  keys(value) {
    return this.children.keys();
  }
}

function Mapping(props, children) {
  if (children === undefined) {
    children = props;
    children = OrderedMap(children);
    props = {children};
  } else {
    children = OrderedMap(children);
    var newProps = {children};
    for (var k in props) {
      if (props.hasOwnProperty(k)) {
        newProps[k] = props[k];
      }
    }
    props = newProps;
  }
  props = Map(props);
  return new MappingNode(props);
}

class ListNode extends CompositeNode {

  getChildren() {
    return this.props.get('children');
  }

  get(key) {
    return this.children;
  }

  has(key) {
    return true;
  }

  keys(value) {
    return value.keys();
  }
}

function List(props, children) {
  if (children === undefined) {
    children = props;
    props = {children};
  } else {
    var newProps = {};
    for (var k in props) {
      if (props.hasOwnProperty(k)) {
        newProps[k] = props[k];
      }
      newProps.children = children;
    }
    props = newProps;
  }
  props = Map(props);
  return new ListNode(props);
}

module.exports = {
  Node,
  ScalarNode, NumberNode, ArrayNode,
  CompositeNode,
  MappingNode, ListNode,
  Scalar, Mapping, List
};
<|endoftext|>


<commit_before>const express = require('express');
const app = express();
const compression = require('compression');
const cors = require('cors');
const fs = require('fs');
const spawn = require('child_process').spawn;
const ffmpeg = require('fluent-ffmpeg');

module.exports = (directory, format, listSize, storageSize, port) => {
  // Create the camera output directory if it doesn't already exist
  // Sync, because this is only run once at startup and everything depends on it
  if (fs.existsSync(directory) === false) fs.mkdirSync(directory);

  // Start the camera stream
  const raspividOptions = ['-o', '-', '-t', '0', '-vf', '-w', '1280', '-h', '720', '-fps', '25'];
  const cameraStream = spawn('raspivid', raspividOptions);

  // Setup up a special shutdown function that's called when encountering an error
  // so that we always shut down the camera stream properly
  const kill = (err) => {
    cameraStream.kill();
    throw err;
  };

  // Set up camera stream conversion
  let conversionStream = ffmpeg(cameraStream.stdout)
    .noAudio();

  if (format === 'hls') {
    const outputOptions = [
      '-hls_list_size',
      listSize,
      '-hls_delete_threshold',
      storageSize,
      '-hls_flags',
      'delete_segments',
      '-hls_segment_type',
      'fmp4'
    ];

    conversionStream
      .videoCodec('copy')
      .format('hls')
      .inputOptions(['-re'])
      .outputOptions(outputOptions)
      .output(`${directory}/livestream.m3u8`);
  }
  else if (format === 'dash') {
    const outputOptions = [
      '-seg_duration',
      2,
      '-window_size',
      listSize,
      '-extra_window_size',
      storageSize
    ];

    conversionStream
      .videoCodec('copy')
      .format('dash')
      .inputOptions(['-re'])
      .outputOptions(outputOptions)
      .output(`${directory}/livestream.mpd`);
  }
  else {
    kill(Error('unsupported format'));
  }

  // Start stream processing
  conversionStream
    .on('error', (err, stdout, stderr) => kill(err))
    .on('start', (commandLine) => console.log('started video processing: ' + commandLine))
    .on('stderr', (stderrLine) => console.log('conversion: ' + stderrLine))
    .run();

  // Endpoint the streaming files will be available on
  const endpoint = '/camera';

  // Setup express server
  app.use(cors());
  app.use(compression({ level: 9 }));
  app.use(endpoint, express.static(directory));
  app.listen(port);

  console.log('camera stream server started');
};

<commit_msg>changed segment file names to be more similar between the formats<commit_after>const express = require('express');
const app = express();
const compression = require('compression');
const cors = require('cors');
const path = require('path');
const fs = require('fs');
const spawn = require('child_process').spawn;
const ffmpeg = require('fluent-ffmpeg');

module.exports = (directory, format, listSize, storageSize, port) => {
  // Create the camera output directory if it doesn't already exist
  // Sync, because this is only run once at startup and everything depends on it
  if (fs.existsSync(directory) === false) fs.mkdirSync(directory);

  // Start the camera stream
  const raspividOptions = ['-o', '-', '-t', '0', '-vf', '-w', '1280', '-h', '720', '-fps', '25'];
  const cameraStream = spawn('raspivid', raspividOptions);

  // Setup up a special shutdown function that's called when encountering an error
  // so that we always shut down the camera stream properly
  const kill = (err) => {
    cameraStream.kill();
    throw err;
  };

  // Set up camera stream conversion
  let conversionStream = ffmpeg(cameraStream.stdout)
    .noAudio();

  if (format === 'hls') {
    const outputOptions = [
      '-hls_list_size',
      listSize,
      '-hls_delete_threshold',
      storageSize,
      '-hls_flags',
      'delete_segments',
      '-use_localtime',
      1,
      '-hls_segment_filename',
      path.join(directory, '%s.m4s'),
      '-hls_segment_type',
      'fmp4'
    ];

    conversionStream
      .videoCodec('copy')
      .format('hls')
      .inputOptions(['-re'])
      .outputOptions(outputOptions)
      .output(path.join(directory, 'livestream.m3u8'));
  }
  else if (format === 'dash') {
    const outputOptions = [
      '-seg_duration',
      2,
      '-window_size',
      listSize,
      '-extra_window_size',
      storageSize,
      '-init_seg_name',
      'init.m4s',
      '-media_seg_name',
      '$Time$.m4s'
    ];

    conversionStream
      .videoCodec('copy')
      .format('dash')
      .inputOptions(['-re'])
      .outputOptions(outputOptions)
      .output(path.join(directory, 'livestream.mpd'));
  }
  else {
    kill(Error('unsupported format'));
  }

  // Start stream processing
  conversionStream
    .on('error', (err, stdout, stderr) => kill(err))
    .on('start', (commandLine) => console.log('started video processing: ' + commandLine))
    .on('stderr', (stderrLine) => console.log('conversion: ' + stderrLine))
    .run();

  // Endpoint the streaming files will be available on
  const endpoint = '/camera';

  // Setup express server
  app.use(cors());
  app.use(compression({ level: 9 }));
  app.use(endpoint, express.static(directory));
  app.listen(port);

  console.log('camera stream server started');
};

<|endoftext|>


<commit_before>/**
 * Provides an HTTP server for relaying queries from clients to the [Akismet](https://akismet.com) service.
 * @module server
 */
'use strict';

// Module dependencies.
const bodyParser = require('body-parser');
const cors = require('cors');
const EventEmitter = require('events').EventEmitter;
const express = require('express');
const http = require('http');
const https = require('https');
const url = require('url');

const clt = require('./client');
const cmt = require('./comment');
const enums = require('./enums');

/**
 * An HTTP server that acts as a proxy between clients and the [Akismet](https://akismet.com) service.
 */
class Server extends EventEmitter {

  /**
   * Emitted when the server closes.
   * @event close
   */

  /**
   * Emitted each time the server experiences an error.
   * @event error
   * @param {Error} err The emitted error event.
   */

  /**
   * Emitted when the server has been bound after calling `listen` method.
   * @event listening
   */

  /**
   * Emitted each time there is a request.
   * @event request
   * @param {http.IncomingMessage} req The request sent by the client.
   */

  /**
   * Initializes a new instance of the class.
   * @param {object} [options] An object specifying values used to initialize this instance.
   */
  constructor(options) {
    super();

    /**
     * The underlying [Express](http://expressjs.com/api.html#express) application used for routing requests.
     * @var {function}
     * @private
     */
    this._express = express();
    this._express.disable('x-powered-by');
    this._registerMiddleware();
    this._registerRoutes();

    /**
     * The underlying HTTP(S) service listening for requests.
     * @var {http.Server|https.Server}
     * @private
     */
    this._httpService = null;

    /**
     * The server settings.
     * @var {object}
     * @private
     */
    this._options = (options || {});
  }

  /**
   * The host that the server is listening on.
   * @var {string}
   * @readonly
   * @default "0.0.0.0"
   */
  get host() {
    return typeof this._options.host == 'string' ? this._options.host : '0.0.0.0';
  }

  /**
   * The port that the server is listening on.
   * @var {number}
   * @readonly
   * @default 3000
   */
  get port() {
    return typeof this._options.port == 'number' ? this._options.port : 3000;
  }

  /**
   * The URL to redirect the user when a request is unhandled.
   * If this property is `null`, a 404 status code is sent instead of redirecting.
   * @var {string}
   * @readonly
   */
  get redirectUrl() {
    return typeof this._options.redirectUrl == 'string' ? this._options.redirectUrl : null;
  }

  /**
   * Checks a comment against the service database, and prints a value indicating whether it is spam.
   * @param {express.Request} req The request sent by a client.
   * @param {express.Response} res The response sent by the server.
   * @return {Promise} Completes once the comment has been checked.
   * @throws {Error} The specified request is invalid.
   */
  checkComment(req, res) {
    if(!('akismet' in req) || !('client' in req.akismet)) throw new Error('Invalid request.');

    let client = req.akismet.client;
    return client.checkComment(req.akismet.comment).then(
      isSpam => Server._sendResponse(res, isSpam ? 'true' : 'false'),
      err => { Server._sendResponse(res, 'invalid', err); this.emit('error', err); }
    );
  }

  /**
   * Stops the server from accepting new connections.
   * @return {Promise} Completes when the server is finally closed.
   */
  close() {
    if(!this._httpService) return Promise.resolve();

    return new Promise(resolve => this._httpService.close(() => {
      this._httpService = null;
      this.emit('close');
      resolve();
    }));
  }

  /**
   * Begin accepting connections.
   * @param {number} [port] The port that the server should run on.
   * @param {string} [host] The host that the server should run on.
   * @return {Promise} Completes when the server has been started.
   */
  listen(port, host) {
    if(typeof port != 'number') port = this.port;
    if(typeof host != 'string') host = this.host;

    this._httpService = (
      'ssl' in this._options ?
      https.createServer(this._options.ssl, this._express) :
      http.createServer(this._express)
    );

    this._httpService.on('clientError', err => this.emit('error', err));
    this._httpService.on('request', req => this.emit('request', req));

    return new Promise(resolve => this._httpService.listen(port, host, () => {
      this._options.host = host;
      this._options.port = port;
      this.emit('listening');
      resolve();
    }));
  }

  /**
   * Submits a comment that was incorrectly marked as spam but should not have been.
   * @param {express.Request} req The request sent by a client.
   * @param {express.Response} res The response sent by the server.
   * @return {Promise} Completes once the comment has been submitted.
   * @throws {Error} The specified request is invalid.
   */
  submitHam(req, res) {
    if(!('akismet' in req) || !('client' in req.akismet)) throw new Error('Invalid request.');

    let client = req.akismet.client;
    return client.submitHam(req.akismet.comment).then(
      () => Server._sendResponse(res, 'Thanks for making the web a better place.'),
      err => { Server._sendResponse(res, '', err); this.emit('error', err); }
    );
  }

  /**
   * Submits a comment that was not marked as spam but should have been.
   * @param {express.Request} req The request sent by a client.
   * @param {express.Response} res The response sent by the server.
   * @return {Promise} Completes once the comment has been submitted.
   * @throws {Error} The specified request is invalid.
   */
  submitSpam(req, res) {
    if(!('akismet' in req) || !('client' in req.akismet)) throw new Error('Invalid request.');

    let client = req.akismet.client;
    return client.submitSpam(req.akismet.comment).then(
      () => Server._sendResponse(res, 'Thanks for making the web a better place.'),
      err => { Server._sendResponse(res, '', err); this.emit('error', err); }
    );
  }

  /**
   * Checks an API key against the service database, and prints a value indicating whether it is a valid key.
   * @param {express.Request} req The request sent by a client.
   * @param {express.Response} res The response sent by the server. The response sent by the server.
   * @return {Promise} Completes once the API key has been checked.
   * @throws {Error} The specified request is invalid.
   */
  verifyKey(req, res) {
    if(!('akismet' in req) || !('client' in req.akismet)) throw new Error('Invalid request.');

    let client = req.akismet.client;
    return client.verifyKey().then(
      isValid => Server._sendResponse(res, isValid ? 'valid' : 'invalid'),
      err => { Server._sendResponse(res, 'invalid', err); this.emit('error', err); }
    );
  }

  /**
   * Processes the specified request body.
   * @param {express.Request} req The request sent by a client.
   * @param {express.Response} res The response sent by the server.
   * @param {Function} next The callback used to pass the request off to the next middleware.
   * @private
   */
  static _processRequest(req, res, next) {
    // Parse client arguments.
    let apiKey = ('key' in req.body ? req.body.key : req.hostname.split('.')[0]);
    let userAgent = (enums.HTTPHeaders.X_USER_AGENT in req.headers ? req.headers[enums.HTTPHeaders.X_USER_AGENT] : req.headers['user-agent']);
    let client = new clt.Client(apiKey, clt.Blog.fromJSON(req.body), {userAgent});

    // Parse comment values.
    let comment = cmt.Comment.fromJSON(req.body);
    if(!comment.author) comment.author = new cmt.Author();
    if(!comment.author.ipAddress) comment.author.ipAddress = req.connection.remoteAddress;

    // Update request properties.
    req.akismet = {client, comment};
    next();
  }

  /**
   * Registers the middleware.
   * @private
   */
  _registerMiddleware() {
    this._express.use(cors({
      allowedHeaders: [enums.HTTPHeaders.X_REQUESTED_WITH, enums.HTTPHeaders.X_USER_AGENT],
      exposedHeaders: [enums.HTTPHeaders.X_AKISMET_DEBUG_HELP],
      methods: ['GET', 'POST'],
      origin: '*'
    }));

    this._express.use(bodyParser.urlencoded({extended: true}));
    this._express.use(Server._processRequest);
  }

  /**
   * Registers the route table.
   * @private
   */
  _registerRoutes() {
    this._express.post(enums.EndPoints.CHECK_COMMENT, this.checkComment.bind(this));
    this._express.post(enums.EndPoints.SUBMIT_HAM, this.submitHam.bind(this));
    this._express.post(enums.EndPoints.SUBMIT_SPAM, this.submitSpam.bind(this));
    this._express.post(enums.EndPoints.VERIFY_KEY, this.verifyKey.bind(this));

    this._express.all('*', (req, res) => {
      if(req.method == 'OPTIONS')
        res.status(204).end();
      else {
        let redirect = this.redirectUrl;
        if(typeof redirect == 'string') res.redirect(301, url.resolve(redirect, req.url));
        else res.status(404).send(http.STATUS_CODES[404]);
      }
    });
  }

  /**
   * Sends the specified response body.
   * @param {express.Response} res The response sent by the server.
   * @param {string} body The response body to send to the client.
   * @param {object} [error] An error message to add to the response headers as `HTTPHeaders.X_AKISMET_DEBUG_HELP`.
   * @private
   */
  static _sendResponse(res, body, error) {
    res.type('text/plain; charset=utf-8');
    if(typeof error != 'undefined') res.set(enums.HTTPHeaders.X_AKISMET_DEBUG_HELP, error.toString());
    res.send(body);
  }
}

// Public interface.
module.exports = Server;
<commit_msg>Updated the documentation comments<commit_after>/**
 * Provides an HTTP server for relaying queries from clients to the [Akismet](https://akismet.com) service.
 * @module server
 */
'use strict';

// Module dependencies.
const bodyParser = require('body-parser');
const cors = require('cors');
const EventEmitter = require('events').EventEmitter;
const express = require('express');
const http = require('http');
const https = require('https');
const url = require('url');

const clt = require('./client');
const cmt = require('./comment');
const enums = require('./enums');

/**
 * An HTTP server that acts as a proxy between clients and the [Akismet](https://akismet.com) service.
 */
class Server extends EventEmitter {

  /**
   * Emitted when the server closes.
   * @event close
   */

  /**
   * Emitted each time the server experiences an error.
   * @event error
   * @param {Error} err The emitted error event.
   */

  /**
   * Emitted when the server has been bound after calling `listen` method.
   * @event listening
   */

  /**
   * Emitted each time there is a request.
   * @event request
   * @param {http.IncomingMessage} req The request sent by the client.
   */

  /**
   * Initializes a new instance of the class.
   * @param {object} [options] An object specifying values used to initialize this instance.
   */
  constructor(options) {
    super();

    /**
     * The underlying [Express](http://expressjs.com/api.html#express) application used for routing requests.
     * @var {function}
     * @private
     */
    this._express = express();
    this._express.disable('x-powered-by');
    this._registerMiddleware();
    this._registerRoutes();

    /**
     * The underlying HTTP(S) service listening for requests.
     * @var {http.Server|https.Server}
     * @private
     */
    this._httpService = null;

    /**
     * The server settings.
     * @var {object}
     * @private
     */
    this._options = (options || {});
  }

  /**
   * The host that the server is listening on.
   * @var {string}
   * @default "0.0.0.0"
   */
  get host() {
    return typeof this._options.host == 'string' ? this._options.host : '0.0.0.0';
  }

  /**
   * The port that the server is listening on.
   * @var {number}
   * @default 3000
   */
  get port() {
    return typeof this._options.port == 'number' ? this._options.port : 3000;
  }

  /**
   * The URL to redirect the user when a request is unhandled.
   * If this property is `null`, a 404 status code is sent instead of redirecting.
   * @var {string}
   */
  get redirectUrl() {
    return typeof this._options.redirectUrl == 'string' ? this._options.redirectUrl : null;
  }

  /**
   * Checks a comment against the service database, and prints a value indicating whether it is spam.
   * @param {express.Request} req The request sent by a client.
   * @param {express.Response} res The response sent by the server.
   * @return {Promise} Completes once the comment has been checked.
   * @throws {Error} The specified request is invalid.
   */
  checkComment(req, res) {
    if(!('akismet' in req) || !('client' in req.akismet)) throw new Error('Invalid request.');

    let client = req.akismet.client;
    return client.checkComment(req.akismet.comment).then(
      isSpam => Server._sendResponse(res, isSpam ? 'true' : 'false'),
      err => { Server._sendResponse(res, 'invalid', err); this.emit('error', err); }
    );
  }

  /**
   * Stops the server from accepting new connections.
   * @return {Promise} Completes when the server is finally closed.
   */
  close() {
    if(!this._httpService) return Promise.resolve();

    return new Promise(resolve => this._httpService.close(() => {
      this._httpService = null;
      this.emit('close');
      resolve();
    }));
  }

  /**
   * Begin accepting connections.
   * @param {number} [port] The port that the server should run on.
   * @param {string} [host] The host that the server should run on.
   * @return {Promise} Completes when the server has been started.
   */
  listen(port, host) {
    if(typeof port != 'number') port = this.port;
    if(typeof host != 'string') host = this.host;

    this._httpService = (
      'ssl' in this._options ?
      https.createServer(this._options.ssl, this._express) :
      http.createServer(this._express)
    );

    this._httpService.on('clientError', err => this.emit('error', err));
    this._httpService.on('request', req => this.emit('request', req));

    return new Promise(resolve => this._httpService.listen(port, host, () => {
      this._options.host = host;
      this._options.port = port;
      this.emit('listening');
      resolve();
    }));
  }

  /**
   * Submits a comment that was incorrectly marked as spam but should not have been.
   * @param {express.Request} req The request sent by a client.
   * @param {express.Response} res The response sent by the server.
   * @return {Promise} Completes once the comment has been submitted.
   * @throws {Error} The specified request is invalid.
   */
  submitHam(req, res) {
    if(!('akismet' in req) || !('client' in req.akismet)) throw new Error('Invalid request.');

    let client = req.akismet.client;
    return client.submitHam(req.akismet.comment).then(
      () => Server._sendResponse(res, 'Thanks for making the web a better place.'),
      err => { Server._sendResponse(res, '', err); this.emit('error', err); }
    );
  }

  /**
   * Submits a comment that was not marked as spam but should have been.
   * @param {express.Request} req The request sent by a client.
   * @param {express.Response} res The response sent by the server.
   * @return {Promise} Completes once the comment has been submitted.
   * @throws {Error} The specified request is invalid.
   */
  submitSpam(req, res) {
    if(!('akismet' in req) || !('client' in req.akismet)) throw new Error('Invalid request.');

    let client = req.akismet.client;
    return client.submitSpam(req.akismet.comment).then(
      () => Server._sendResponse(res, 'Thanks for making the web a better place.'),
      err => { Server._sendResponse(res, '', err); this.emit('error', err); }
    );
  }

  /**
   * Checks an API key against the service database, and prints a value indicating whether it is a valid key.
   * @param {express.Request} req The request sent by a client.
   * @param {express.Response} res The response sent by the server. The response sent by the server.
   * @return {Promise} Completes once the API key has been checked.
   * @throws {Error} The specified request is invalid.
   */
  verifyKey(req, res) {
    if(!('akismet' in req) || !('client' in req.akismet)) throw new Error('Invalid request.');

    let client = req.akismet.client;
    return client.verifyKey().then(
      isValid => Server._sendResponse(res, isValid ? 'valid' : 'invalid'),
      err => { Server._sendResponse(res, 'invalid', err); this.emit('error', err); }
    );
  }

  /**
   * Processes the specified request body.
   * @param {express.Request} req The request sent by a client.
   * @param {express.Response} res The response sent by the server.
   * @param {Function} next The callback used to pass the request off to the next middleware.
   * @private
   */
  static _processRequest(req, res, next) {
    // Parse client arguments.
    let apiKey = ('key' in req.body ? req.body.key : req.hostname.split('.')[0]);
    let userAgent = (enums.HTTPHeaders.X_USER_AGENT in req.headers ? req.headers[enums.HTTPHeaders.X_USER_AGENT] : req.headers['user-agent']);
    let client = new clt.Client(apiKey, clt.Blog.fromJSON(req.body), {userAgent});

    // Parse comment values.
    let comment = cmt.Comment.fromJSON(req.body);
    if(!comment.author) comment.author = new cmt.Author();
    if(!comment.author.ipAddress) comment.author.ipAddress = req.connection.remoteAddress;

    // Update request properties.
    req.akismet = {client, comment};
    next();
  }

  /**
   * Registers the middleware.
   * @private
   */
  _registerMiddleware() {
    this._express.use(cors({
      allowedHeaders: [enums.HTTPHeaders.X_REQUESTED_WITH, enums.HTTPHeaders.X_USER_AGENT],
      exposedHeaders: [enums.HTTPHeaders.X_AKISMET_DEBUG_HELP],
      methods: ['GET', 'POST'],
      origin: '*'
    }));

    this._express.use(bodyParser.urlencoded({extended: true}));
    this._express.use(Server._processRequest);
  }

  /**
   * Registers the route table.
   * @private
   */
  _registerRoutes() {
    this._express.post(enums.EndPoints.CHECK_COMMENT, this.checkComment.bind(this));
    this._express.post(enums.EndPoints.SUBMIT_HAM, this.submitHam.bind(this));
    this._express.post(enums.EndPoints.SUBMIT_SPAM, this.submitSpam.bind(this));
    this._express.post(enums.EndPoints.VERIFY_KEY, this.verifyKey.bind(this));

    this._express.all('*', (req, res) => {
      if(req.method == 'OPTIONS')
        res.status(204).end();
      else {
        let redirect = this.redirectUrl;
        if(typeof redirect == 'string') res.redirect(301, url.resolve(redirect, req.url));
        else res.status(404).send(http.STATUS_CODES[404]);
      }
    });
  }

  /**
   * Sends the specified response body.
   * @param {express.Response} res The response sent by the server.
   * @param {string} body The response body to send to the client.
   * @param {object} [error] An error message to add to the response headers as `HTTPHeaders.X_AKISMET_DEBUG_HELP`.
   * @private
   */
  static _sendResponse(res, body, error) {
    res.type('text/plain; charset=utf-8');
    if(typeof error != 'undefined') res.set(enums.HTTPHeaders.X_AKISMET_DEBUG_HELP, error.toString());
    res.send(body);
  }
}

// Public interface.
module.exports = Server;
<|endoftext|>


<commit_before>/*
 * This implements a OAuth2 server methods, as specified at:
 *  http://tools.ietf.org/html/draft-ietf-oauth-v2-10
 *
 * Only features the "web server" schema:
 *  http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-1.4.1
 *
 * Terminology:
 *  http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-1.2
 *
 */
var url = require('url')
  , querystring = require('querystring')

  , randomString = require('nodetk/random_str').randomString
  , SecureSerializer = require('nodetk/serializer').SecureSerializer
  , tools = require('nodetk/server_tools')
  , router = require('connect').router

  , oauth2 = require('./common')
  ;

var SERVER = exports;
// To be set by connect middleware: SERVER.RFactory, SERVER.authentication

SERVER.oauth_error = function(res, type, id) {
  /* Render a particula error.
   *
   * Arguments:
   *  - res
   *  - type: the class of the error ('eua' or 'oat').
   *  - id: the id of the error (invalid_request, invalid_client...).
   */
  res.writeHead(400, {'Content-Type': 'text/plain'});
  res.end(JSON.stringify({error: {
    type: 'OAuthException',
    message: id + ': ' + oauth2.ERRORS[type][id],
  }}));
};

var unknown_error = exports.unknown_error = function(res, err) {
  /* To call when an unknown error happens (server error).
   */
  // console.log(err.message);
  // console.log(err.stack);
  tools.server_error(res, err);
};

// Parameters we must/can have in different kinds of requests:
var PARAMS = exports.PARAMS = {
  eua: { // eua = end user authorization
    mandatory: ['client_id', 'response_type'],
    optional: ['state', 'scope', 'redirect_uri'],
    // possible values for response_type param:
    response_types: {'token': 1, 'code': 1, 'code_and_token': 1},
  },

  oat: { // oat = Obtaining an access token
    mandatory: ['grant_type', 'client_id', 'code', 'redirect_uri'],
    // client_secret might be provided with the 'Authorization: Basic ...' header
    optional: ['scope', 'client_secret'],
  },
};
PARAMS.eua.all = PARAMS.eua.mandatory.concat(PARAMS.eua.optional);

exports.send_grant = function(res, R, user_id, client_data, additional_info) {
  /* Create a grant and send it to the user.
   * The code sent is of the form: grand.id + '.' + grant.code
   *
   * Arguments:
   *  - req
   *  - res
   *  - R: rest-mongo instance
   *  - user_id: id of the user
   *  - client_data: hash, data about the client:
   *      - client_id
   *      - redirect_uri
   *      - state: str, optional, if given, will be sent with the grant.
   *  - additional_info: optional hash, anything you want to be stored with
   *    the grant.
   *
   */
  var grant = new R.Grant({
    client_id: client_data.client_id,
    time: Date.now(),
    user_id: user_id,
    code: randomString(128),
    redirect_uri: client_data.redirect_uri
  });
  if(additional_info) grant.additional_info = additional_info;
  grant.save(function() {
    var qs = {code: grant.id + '.' + grant.code};
    if(client_data.state) qs.state = client_data.state;
    qs = querystring.stringify(qs);
    tools.redirect(res, client_data.redirect_uri + '?' + qs);
  }, function(err) {
    unknown_error(res, err);
  });
};

SERVER.valid_grant = function(R, data, callback, fallback) {
  /* Valid the grant, call callback(token|null) or fallback(err),
   * token being a JSON object.
   * If valid, the grant is invalidated and cannot be used anymore.
   *
   * To be valid, a grant must exist, not be deprecated and have the right
   * associated client.
   *
   * Arguments:
   *  - R: rest-mongo instance
   *  - data:
   *   - code: grant code given by client.
   *   - client_id: the client id giving the grant
   *   - redirect_uri: the redirect_uri given with the grant
   *  - callback: to be called with a token if the grant was valid,
   *    or null otherwise.
   *  - fallback: to be called in case of error (an invalid grant is not
   *    an error).
   *
   */
  var id_code = data.code.split('.');
  if(id_code.length != 2) return callback(null);
  R.Grant.get({ids: id_code[0]}, function(grant) {
    var minute_ago = Date.now() - 60000;
    if(!grant || grant.time < minute_ago ||
       grant.client_id != data.client_id ||
       grant.code != id_code[1] ||
       grant.redirect_uri != data.redirect_uri
       ) return callback(null);
    var additional_info = grant.additional_info;
    // Delete the grant so that it cannot be used anymore:
    grant.delete_(function() {
      // Generate and send an access_token to the client:
      var token = {
        access_token: oauth2.create_access_token(grant.user_id, grant.client_id,
                                                 additional_info)
        // optional: expires_in, refresh_token, scope
      };
      callback(token);
    }, fallback);
  }, fallback);
};

var token_endpoint = exports.token_endpoint = function(req, res) {
  /* OAuth2 token endpoint.
   * Check the authorization_code, uri_redirect and client secret, issue a token.
   *
   * POST to config.oauth2.token_url
   *
   * Arguments:
   *  - req
   *  - res
   *
   */
  if(!req.form) return SERVER.oauth_error(res, 'oat', 'invalid_request');
  req.form.complete(function(err, params, files) {
    if(err) return SERVER.oauth_error(res, 'oat', 'invalid_request');
    var R = SERVER.RFactory();

    // We check there is no invalid_requet error:
    var error = false;
    params && PARAMS.oat.mandatory.forEach(function(param) {
      if(!params[param]) error = true;
    });
    if(error) return SERVER.oauth_error(res, 'oat', 'invalid_request');

    // We do only support 'authorization_code' as grant_type:
    if(params.grant_type != 'authorization_code')
      return SERVER.oauth_error(res, 'oat', 'unsupported_grant_type');

    // Check the client_secret is given once (and only once),
    // either by HTTP basic auth, or by client_secret parameter:
    var secret = req.headers['authorization'];
    if(secret) {
      if(params.client_secret)
        return SERVER.oauth_error(res, 'oat', 'invalid_request');
      params.client_secret = secret.slice(6); // remove the leading 'Basic'
    }
    else if(!params.client_secret) {
      return SERVER.oauth_error(res, 'oat', 'invalid_request');
    }

    // Check the client_id exists and does have correct client_secret:
    R.Client.get({ids: params.client_id}, function(client) {
      if(!client || client.secret != params.client_secret)
        return SERVER.oauth_error(res, 'oat', 'invalid_client');

      var data = {code: params.code, client_id: client.id, redirect_uri: params.redirect_uri};
      SERVER.valid_grant(R, data, function(token) {
        if(!token) return SERVER.oauth_error(res, 'oat', 'invalid_grant');
        res.writeHead(200, { 'Content-Type': 'application/json'
                           , 'Cache-Control': 'no-store'
                           });
        res.end(JSON.stringify(token));
      }, function(err) { unknown_error(res, err) });
    }, function(err) { unknown_error(res, err) });
  });
};

SERVER.authorize = function(params, req, res) {
  /* OAuth2 Authorize function.
   * Serve an authentication form to the end user at browser.
   *
   *  This function should only be called by oauth2.authorize
   *
   * Arguments:
   *  - params:
   *  - req
   *  - res
   *
   */
  // We check there is no invalid_requet error:
  var error = false;
  PARAMS.eua.mandatory.forEach(function(param) {
    if(!params[param]) error = true;
  });
  if(error) return SERVER.oauth_error(res, 'eua', 'invalid_request');
  if(!PARAMS.eua.response_types[params.response_type])
    return SERVER.oauth_error(res, 'eua', 'unsupported_response_type');

  // XXX: For now, we only support 'code' response type
  // which is used in case of a web server (Section 1.4.1 in oauth2 spec draft 10)
  // TODO: make it more compliant with the specs
  if(params.response_type != "code") {
    res.writeHead(501, {'Content-Type': 'text/plain'});
    res.end('Only code request type supported for now ' +
            '(schema 1.4.1 in oauth2 spec draft 10).');
    return;
  }

  var R = SERVER.RFactory();
  R.Client.get({ids: params.client_id}, function(client) {
    if(!client) return SERVER.oauth_error(res, 'eua', 'invalid_client');
    // Check the redirect_uri is the one we know about (if we do):
    if(client.redirect_uri && params.redirect_uri
       && client.redirect_uri != params.redirect_uri) {
      return SERVER.oauth_error(res, 'eua', 'redirect_uri_mismatch');
    }
    var redirect_uri = client.redirect_uri || params.redirect_uri;
    // Eveything is allright, ask the user to sign in.
    SERVER.authentication.login(req, res, {
      client_id: client.id,
      client_name: client.name,
      redirect_uri: redirect_uri,
      state: params.state
    });
  }, function(err) { unknown_error(res, err) });
};

SERVER.authorize_endpoint = function(req, res) {
  /* OAuth2 Authorize end-point.
   * Serve an authentication form to the end user at browser.
   *
   *  GET or POST on config.oauth2.authorize_url
   *
   * Arguments:
   *  - req
   *  - res
   *
   */
  if (req.method == 'GET') {
    var params = url.parse(req.url, true).query;
    return SERVER.authorize(params, req, res);
  } else {
    if(!req.form) return SERVER.oauth_error(res, 'eua', 'invalid_request');
    req.form.complete(function(err, fields, files) {
      if(err) return SERVER.oauth_error(res, 'eua', 'invalid_request');
      SERVER.authorize(fields, req, res);
    });
  }
};

exports.connector = function(config, RFactory, authentication) {
  /* Returns Oauth2 server connect middleware.
   *
   * This middleware will intercept requests aiming at OAuth2 server
   * and treat them.
   *
   * Arguments:
   *  - config, hash containing:
   *    - authorize_url: end-user authorization endpoint,
   *      the URL the end-user must be redirected to to be served the
   *      authentication form.
   *    - token_url: OAuth2 token endpoint,
   *      the URL the client will use to check the authorization_code given by
   *      user and get a token.
   *    - crypt_key: string, encryption key used to crypt information contained
   *      in the issued tokens. This is a symmetric key and must be kept secret.
   *    - sign_key: string, signature key used to sign issued tokens.
   *      This is a symmetric key and must be kept secret.
   *  - RFactory: RFactory object initialized whith minimal schema info
   *    (Grant and Client objects). cf. README file for more info.
   *  - authentication: module (or object) defining the following functions:
   *    * login: function to render the login page to end user in browser.
   *    * process_login: to process the credentials given by user.
   *      This function should use the send_grant function once user is
   *      authenticated.
   *
   */
  var sserializer = new SecureSerializer(config.crypt_key, config.sign_key);
  oauth2.set_serializer(sserializer);
  SERVER.RFactory = RFactory;
  SERVER.authentication = authentication;
  return router(function(app) {
    app.get(config.authorize_url, SERVER.authorize_endpoint);
    app.post(config.authorize_url, SERVER.authorize_endpoint);
    app.post(config.process_login_url, authentication.process_login);
    app.post(config.token_url, token_endpoint);
  });
};

var common = require('./common');

exports.set_serializer = common.set_serializer;
exports.ERROR = common.ERRORS;
exports.create_access_token = common.create_access_token;
exports.token_info = common.token_info;
exports.check_token = common.check_token;
<commit_msg>Fix typo.<commit_after>/*
 * This implements a OAuth2 server methods, as specified at:
 *  http://tools.ietf.org/html/draft-ietf-oauth-v2-10
 *
 * Only features the "web server" schema:
 *  http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-1.4.1
 *
 * Terminology:
 *  http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-1.2
 *
 */
var url = require('url')
  , querystring = require('querystring')

  , randomString = require('nodetk/random_str').randomString
  , SecureSerializer = require('nodetk/serializer').SecureSerializer
  , tools = require('nodetk/server_tools')
  , router = require('connect').router

  , oauth2 = require('./common')
  ;

var SERVER = exports;
// To be set by connect middleware: SERVER.RFactory, SERVER.authentication

SERVER.oauth_error = function(res, type, id) {
  /* Render a particula error.
   *
   * Arguments:
   *  - res
   *  - type: the class of the error ('eua' or 'oat').
   *  - id: the id of the error (invalid_request, invalid_client...).
   */
  res.writeHead(400, {'Content-Type': 'text/plain'});
  res.end(JSON.stringify({error: {
    type: 'OAuthException',
    message: id + ': ' + oauth2.ERRORS[type][id],
  }}));
};

var unknown_error = exports.unknown_error = function(res, err) {
  /* To call when an unknown error happens (server error).
   */
  // console.log(err.message);
  // console.log(err.stack);
  tools.server_error(res, err);
};

// Parameters we must/can have in different kinds of requests:
var PARAMS = exports.PARAMS = {
  eua: { // eua = end user authorization
    mandatory: ['client_id', 'response_type'],
    optional: ['state', 'scope', 'redirect_uri'],
    // possible values for response_type param:
    response_types: {'token': 1, 'code': 1, 'code_and_token': 1},
  },

  oat: { // oat = Obtaining an access token
    mandatory: ['grant_type', 'client_id', 'code', 'redirect_uri'],
    // client_secret might be provided with the 'Authorization: Basic ...' header
    optional: ['scope', 'client_secret'],
  },
};
PARAMS.eua.all = PARAMS.eua.mandatory.concat(PARAMS.eua.optional);

exports.send_grant = function(res, R, user_id, client_data, additional_info) {
  /* Create a grant and send it to the user.
   * The code sent is of the form: grand.id + '.' + grant.code
   *
   * Arguments:
   *  - req
   *  - res
   *  - R: rest-mongo instance
   *  - user_id: id of the user
   *  - client_data: hash, data about the client:
   *      - client_id
   *      - redirect_uri
   *      - state: str, optional, if given, will be sent with the grant.
   *  - additional_info: optional hash, anything you want to be stored with
   *    the grant.
   *
   */
  var grant = new R.Grant({
    client_id: client_data.client_id,
    time: Date.now(),
    user_id: user_id,
    code: randomString(128),
    redirect_uri: client_data.redirect_uri
  });
  if(additional_info) grant.additional_info = additional_info;
  grant.save(function() {
    var qs = {code: grant.id + '.' + grant.code};
    if(client_data.state) qs.state = client_data.state;
    qs = querystring.stringify(qs);
    tools.redirect(res, client_data.redirect_uri + '?' + qs);
  }, function(err) {
    unknown_error(res, err);
  });
};

SERVER.valid_grant = function(R, data, callback, fallback) {
  /* Valid the grant, call callback(token|null) or fallback(err),
   * token being a JSON object.
   * If valid, the grant is invalidated and cannot be used anymore.
   *
   * To be valid, a grant must exist, not be deprecated and have the right
   * associated client.
   *
   * Arguments:
   *  - R: rest-mongo instance
   *  - data:
   *   - code: grant code given by client.
   *   - client_id: the client id giving the grant
   *   - redirect_uri: the redirect_uri given with the grant
   *  - callback: to be called with a token if the grant was valid,
   *    or null otherwise.
   *  - fallback: to be called in case of error (an invalid grant is not
   *    an error).
   *
   */
  var id_code = data.code.split('.');
  if(id_code.length != 2) return callback(null);
  R.Grant.get({ids: id_code[0]}, function(grant) {
    var minute_ago = Date.now() - 60000;
    if(!grant || grant.time < minute_ago ||
       grant.client_id != data.client_id ||
       grant.code != id_code[1] ||
       grant.redirect_uri != data.redirect_uri
       ) return callback(null);
    var additional_info = grant.additional_info;
    // Delete the grant so that it cannot be used anymore:
    grant.delete_(function() {
      // Generate and send an access_token to the client:
      var token = {
        access_token: oauth2.create_access_token(grant.user_id, grant.client_id,
                                                 additional_info)
        // optional: expires_in, refresh_token, scope
      };
      callback(token);
    }, fallback);
  }, fallback);
};

var token_endpoint = exports.token_endpoint = function(req, res) {
  /* OAuth2 token endpoint.
   * Check the authorization_code, uri_redirect and client secret, issue a token.
   *
   * POST to config.oauth2.token_url
   *
   * Arguments:
   *  - req
   *  - res
   *
   */
  if(!req.form) return SERVER.oauth_error(res, 'oat', 'invalid_request');
  req.form.complete(function(err, params, files) {
    if(err) return SERVER.oauth_error(res, 'oat', 'invalid_request');
    var R = SERVER.RFactory();

    // We check there is no invalid_requet error:
    var error = false;
    params && PARAMS.oat.mandatory.forEach(function(param) {
      if(!params[param]) error = true;
    });
    if(error) return SERVER.oauth_error(res, 'oat', 'invalid_request');

    // We do only support 'authorization_code' as grant_type:
    if(params.grant_type != 'authorization_code')
      return SERVER.oauth_error(res, 'oat', 'unsupported_grant_type');

    // Check the client_secret is given once (and only once),
    // either by HTTP basic auth, or by client_secret parameter:
    var secret = req.headers['authorization'];
    if(secret) {
      if(params.client_secret)
        return SERVER.oauth_error(res, 'oat', 'invalid_request');
      params.client_secret = secret.slice(6); // remove the leading 'Basic'
    }
    else if(!params.client_secret) {
      return SERVER.oauth_error(res, 'oat', 'invalid_request');
    }

    // Check the client_id exists and does have correct client_secret:
    R.Client.get({ids: params.client_id}, function(client) {
      if(!client || client.secret != params.client_secret)
        return SERVER.oauth_error(res, 'oat', 'invalid_client');

      var data = {code: params.code, client_id: client.id, redirect_uri: params.redirect_uri};
      SERVER.valid_grant(R, data, function(token) {
        if(!token) return SERVER.oauth_error(res, 'oat', 'invalid_grant');
        res.writeHead(200, { 'Content-Type': 'application/json'
                           , 'Cache-Control': 'no-store'
                           });
        res.end(JSON.stringify(token));
      }, function(err) { unknown_error(res, err) });
    }, function(err) { unknown_error(res, err) });
  });
};

SERVER.authorize = function(params, req, res) {
  /* OAuth2 Authorize function.
   * Serve an authentication form to the end user at browser.
   *
   *  This function should only be called by oauth2.authorize
   *
   * Arguments:
   *  - params:
   *  - req
   *  - res
   *
   */
  // We check there is no invalid_requet error:
  var error = false;
  PARAMS.eua.mandatory.forEach(function(param) {
    if(!params[param]) error = true;
  });
  if(error) return SERVER.oauth_error(res, 'eua', 'invalid_request');
  if(!PARAMS.eua.response_types[params.response_type])
    return SERVER.oauth_error(res, 'eua', 'unsupported_response_type');

  // XXX: For now, we only support 'code' response type
  // which is used in case of a web server (Section 1.4.1 in oauth2 spec draft 10)
  // TODO: make it more compliant with the specs
  if(params.response_type != "code") {
    res.writeHead(501, {'Content-Type': 'text/plain'});
    res.end('Only code request type supported for now ' +
            '(schema 1.4.1 in oauth2 spec draft 10).');
    return;
  }

  var R = SERVER.RFactory();
  R.Client.get({ids: params.client_id}, function(client) {
    if(!client) return SERVER.oauth_error(res, 'eua', 'invalid_client');
    // Check the redirect_uri is the one we know about (if we do):
    if(client.redirect_uri && params.redirect_uri
       && client.redirect_uri != params.redirect_uri) {
      return SERVER.oauth_error(res, 'eua', 'redirect_uri_mismatch');
    }
    var redirect_uri = client.redirect_uri || params.redirect_uri;
    // Eveything is allright, ask the user to sign in.
    SERVER.authentication.login(req, res, {
      client_id: client.id,
      client_name: client.name,
      redirect_uri: redirect_uri,
      state: params.state
    });
  }, function(err) { unknown_error(res, err) });
};

SERVER.authorize_endpoint = function(req, res) {
  /* OAuth2 Authorize end-point.
   * Serve an authentication form to the end user at browser.
   *
   *  GET or POST on config.oauth2.authorize_url
   *
   * Arguments:
   *  - req
   *  - res
   *
   */
  if (req.method == 'GET') {
    var params = url.parse(req.url, true).query;
    return SERVER.authorize(params, req, res);
  } else {
    if(!req.form) return SERVER.oauth_error(res, 'eua', 'invalid_request');
    req.form.complete(function(err, fields, files) {
      if(err) return SERVER.oauth_error(res, 'eua', 'invalid_request');
      SERVER.authorize(fields, req, res);
    });
  }
};

exports.connector = function(config, RFactory, authentication) {
  /* Returns Oauth2 server connect middleware.
   *
   * This middleware will intercept requests aiming at OAuth2 server
   * and treat them.
   *
   * Arguments:
   *  - config, hash containing:
   *    - authorize_url: end-user authorization endpoint,
   *      the URL the end-user must be redirected to to be served the
   *      authentication form.
   *    - token_url: OAuth2 token endpoint,
   *      the URL the client will use to check the authorization_code given by
   *      user and get a token.
   *    - crypt_key: string, encryption key used to crypt information contained
   *      in the issued tokens. This is a symmetric key and must be kept secret.
   *    - sign_key: string, signature key used to sign issued tokens.
   *      This is a symmetric key and must be kept secret.
   *  - RFactory: RFactory object initialized whith minimal schema info
   *    (Grant and Client objects). cf. README file for more info.
   *  - authentication: module (or object) defining the following functions:
   *    * login: function to render the login page to end user in browser.
   *    * process_login: to process the credentials given by user.
   *      This function should use the send_grant function once user is
   *      authenticated.
   *
   */
  var sserializer = new SecureSerializer(config.crypt_key, config.sign_key);
  oauth2.set_serializer(sserializer);
  SERVER.RFactory = RFactory;
  SERVER.authentication = authentication;
  return router(function(app) {
    app.get(config.authorize_url, SERVER.authorize_endpoint);
    app.post(config.authorize_url, SERVER.authorize_endpoint);
    app.post(config.process_login_url, authentication.process_login);
    app.post(config.token_url, token_endpoint);
  });
};

var common = require('./common');

exports.set_serializer = common.set_serializer;
exports.ERRORS = common.ERRORS;
exports.create_access_token = common.create_access_token;
exports.token_info = common.token_info;
exports.check_token = common.check_token;
<|endoftext|>


<commit_before>var util = require('./util');
var restify = require('restify');
var EventEmitter = require('events').EventEmitter;
var WebSocketServer = require('ws').Server;
var url = require('url');

function PeerServer(options) {
  if (!(this instanceof PeerServer)) return new PeerServer(options);
  EventEmitter.call(this);

  this._options = util.extend({
    port: 80,
    debug: false,
    timeout: 5000,
    key: 'peerjs',
    ip_limit: 5000,
    concurrent_limit: 5000,
    ssl: {},
    path: '/',
    allow_discovery: false
  }, options);

  util.debug = this._options.debug;

  // Print warning if only one of the two is given.
  if (Object.keys(this._options.ssl).length === 1) {
    util.prettyError('Warning: PeerServer will not run on an HTTPS server'
        + ' because either the key or the certificate has not been provided.');
  }

  this._options.ssl['name'] = 'PeerServer';

  if (this._options.path[0] !== '/') {
    this._options.path = '/' + this._options.path;
  }
  if (this._options.path[this._options.path.length - 1] !== '/') {
    this._options.path += '/';
  }

  this._app = restify.createServer(this._options.ssl);

  // Connected clients
  this._clients = {};

  // Messages waiting for another peer.
  this._outstanding = {};

  // Initailize WebSocket server handlers.
  this._initializeWSS();

  // Initialize HTTP routes. This is only used for the first few milliseconds
  // before a socket is opened for a Peer.
  this._initializeHTTP();

  // Mark concurrent users per ip
  this._ips = {};

  this._setCleanupIntervals();
}

util.inherits(PeerServer, EventEmitter);

/** Initialize WebSocket server. */
PeerServer.prototype._initializeWSS = function() {
  var self = this;

  // Create WebSocket server as well.
  this._wss = new WebSocketServer({ path: this._options.path + 'peerjs', server: this._app});

  this._wss.on('connection', function(socket) {
    var query = url.parse(socket.upgradeReq.url, true).query;
    var id = query.id;
    var token = query.token;
    var key = query.key;
    var ip = socket.upgradeReq.socket.remoteAddress;

    if (!id || !token || !key) {
      socket.send(JSON.stringify({ type: 'ERROR', payload: { msg: 'No id, token, or key supplied to websocket server' } }));
      socket.close();
      return;
    }

    if (!self._clients[key] || !self._clients[key][id]) {
      self._checkKey(key, ip, function(err) {
        if (!err) {
          if (!self._clients[key][id]) {
            self._clients[key][id] = { token: token, ip: ip };
            self._ips[ip]++;
            socket.send(JSON.stringify({ type: 'OPEN' }));
          }
          self._configureWS(socket, key, id, token);
        } else {
          socket.send(JSON.stringify({ type: 'ERROR', payload: { msg: err } }));
        }
      });
    } else {
      self._configureWS(socket, key, id, token);
    }
  });
};

PeerServer.prototype._configureWS = function(socket, key, id, token) {
  var self = this;
  var client = this._clients[key][id];

  if (token === client.token) {
    // res 'close' event will delete client.res for us
    client.socket = socket;
    // Client already exists
    if (client.res) {
      client.res.end();
    }
  } else {
    // ID-taken, invalid token
    socket.send(JSON.stringify({ type: 'ID-TAKEN', payload: { msg: 'ID is taken' } }));
    socket.close();
    return;
  }

  this._processOutstanding(key, id);

  // Cleanup after a socket closes.
  socket.on('close', function() {
    util.log('Socket closed:', id);
    if (client.socket == socket) {
      self._removePeer(key, id);
    }
  });

  // Handle messages from peers.
  socket.on('message', function(data) {
    try {
      var message = JSON.parse(data);

      if (['LEAVE', 'CANDIDATE', 'OFFER', 'ANSWER'].indexOf(message.type) !== -1) {
        self._handleTransmission(key, {
          type: message.type,
          src: id,
          dst: message.dst,
          payload: message.payload
        });
      } else {
        util.prettyError('Message unrecognized');
      }
    } catch(e) {
      util.log('Invalid message', data);
      throw e;
    }
  });

  // We're going to emit here, because for XHR we don't *know* when someone
  // disconnects.
  this.emit('connection', id);
};

PeerServer.prototype._checkAllowsDiscovery = function(key, cb) {
  cb(this._options.allow_discovery);
};

PeerServer.prototype._checkKey = function(key, ip, cb) {
  if (key == this._options.key) {
    if (!this._clients[key]) {
      this._clients[key] = {};
    }
    if (!this._outstanding[key]) {
      this._outstanding[key] = {};
    }
    if (!this._ips[ip]) {
      this._ips[ip] = 0;
    }
    // Check concurrent limit
    if (Object.keys(this._clients[key]).length >= this._options.concurrent_limit) {
      cb('Server has reached its concurrent user limit');
      return;
    }
    if (this._ips[ip] >= this._options.ip_limit) {
      cb(ip + ' has reached its concurrent user limit');
      return;
    }
    cb(null);
  } else {
    cb('Invalid key provided');
  }
};

/** Initialize HTTP server routes. */
PeerServer.prototype._initializeHTTP = function() {
  var self = this;

  this._app.use(restify.bodyParser({ mapParams: false }));
  this._app.use(restify.queryParser());
  this._app.use(util.allowCrossDomain);

  // Retrieve guaranteed random ID.
  this._app.get(this._options.path + ':key/id', function(req, res, next) {
    res.contentType = 'text/html';
    res.send(self._generateClientId(req.params.key));
    return next();
  });

  // Server sets up HTTP streaming when you get post an ID.
  this._app.post(this._options.path + ':key/:id/:token/id', function(req, res, next) {
    var id = req.params.id;
    var token = req.params.token;
    var key = req.params.key;
    var ip = req.connection.remoteAddress;

    if (!self._clients[key] || !self._clients[key][id]) {
      self._checkKey(key, ip, function(err) {
        if (!err && !self._clients[key][id]) {
          self._clients[key][id] = { token: token, ip: ip };
          self._ips[ip]++;
          self._startStreaming(res, key, id, token, true);
        } else {
          res.send(JSON.stringify({ type: 'HTTP-ERROR' }));
        }
      });
    } else {
      self._startStreaming(res, key, id, token);
    }
    return next();
  });

  // Get a list of all peers for a key, enabled by the `allowDiscovery` flag.
  this._app.get(this._options.path + ':key/peers', function(req, res, next) {
    var key = req.params.key;
    if (self._clients[key]) {
      self._checkAllowsDiscovery(key, function(isAllowed) {
        if (isAllowed) {
          res.send(Object.keys(self._clients[key]));
        } else {
          res.send(401);
        }
        return next();
      });
    } else {
      res.send(404);
      return next();
    }
  });

  var handle = function(req, res, next) {
    var key = req.params.key;
    var id = req.params.id;

    var client;
    if (!self._clients[key] || !(client = self._clients[key][id])) {
      if (req.params.retry) {
        res.send(401);
        return next();
      } else {
        // Retry this request
        req.params.retry = true;
        setTimeout(handle, 25, req, res);
        return;
      }
    }

    // Auth the req
    if (req.params.token !== client.token) {
      res.send(401);
      return;
    } else {
      self._handleTransmission(key, {
        type: req.body.type,
        src: id,
        dst: req.body.dst,
        payload: req.body.payload
      });
      res.send(200);
    }
    return next();
  };

  this._app.post(this._options.path + ':key/:id/:token/offer', handle);

  this._app.post(this._options.path + ':key/:id/:token/candidate', handle);

  this._app.post(this._options.path + ':key/:id/:token/answer', handle);

  this._app.post(this._options.path + ':key/:id/:token/leave', handle);

  // Listen on user-specified port and IP address.
  if (this._options.ip) {
    this._app.listen(this._options.port, this._options.ip);
  } else {
    this._app.listen(this._options.port);
  }

};

/** Saves a streaming response and takes care of timeouts and headers. */
PeerServer.prototype._startStreaming = function(res, key, id, token, open) {
  var self = this;

  res.writeHead(200, {'Content-Type': 'application/octet-stream'});

  var pad = '00';
  for (var i = 0; i < 10; i++) {
    pad += pad;
  }
  res.write(pad + '\n');

  if (open) {
    res.write(JSON.stringify({ type: 'OPEN' }) + '\n');
  }

  var client = this._clients[key][id];

  if (token === client.token) {
    // Client already exists
    res.on('close', function() {
      if (client.res === res) {
        if (!client.socket) {
          // No new request yet, peer dead
          self._removePeer(key, id);
          return;
        }
        delete client.res;
      }
    });
    client.res = res;
    this._processOutstanding(key, id);
  } else {
    // ID-taken, invalid token
    res.end(JSON.stringify({ type: 'HTTP-ERROR' }));
  }
};

PeerServer.prototype._pruneOutstanding = function() {
  var keys = Object.keys(this._outstanding);
  for (var k = 0, kk = keys.length; k < kk; k += 1) {
    var key = keys[k];
    var dsts = Object.keys(this._outstanding[key]);
    for (var i = 0, ii = dsts.length; i < ii; i += 1) {
      var offers = this._outstanding[key][dsts[i]];
      var seen = {};
      for (var j = 0, jj = offers.length; j < jj; j += 1) {
        var message = offers[j];
        if (!seen[message.src]) {
          this._handleTransmission(key, { type: 'EXPIRE', src: message.dst, dst: message.src });
          seen[message.src] = true;
        }
      }
    }
    this._outstanding[key] = {};
  }
};

/** Cleanup */
PeerServer.prototype._setCleanupIntervals = function() {
  var self = this;

  // Clean up ips every 10 minutes
  setInterval(function() {
    var keys = Object.keys(self._ips);
    for (var i = 0, ii = keys.length; i < ii; i += 1) {
      var key = keys[i];
      if (self._ips[key] === 0) {
        delete self._ips[key];
      }
    }
  }, 600000);

  // Clean up outstanding messages every 5 seconds
  setInterval(function() {
    self._pruneOutstanding();
  }, 5000);
};

/** Process outstanding peer offers. */
PeerServer.prototype._processOutstanding = function(key, id) {
  var offers = this._outstanding[key][id];
  if (!offers) {
    return;
  }
  for (var j = 0, jj = offers.length; j < jj; j += 1) {
    this._handleTransmission(key, offers[j]);
  }
  delete this._outstanding[key][id];
};

PeerServer.prototype._removePeer = function(key, id) {
  if (this._clients[key] && this._clients[key][id]) {
    this._ips[this._clients[key][id].ip]--;
    delete this._clients[key][id];
    this.emit('disconnect', id);
  }
};

/** Handles passing on a message. */
PeerServer.prototype._handleTransmission = function(key, message) {
  var type = message.type;
  var src = message.src;
  var dst = message.dst;
  var data = JSON.stringify(message);

  var destination = this._clients[key][dst];

  // User is connected!
  if (destination) {
    try {
      util.log(type, 'from', src, 'to', dst);
      if (destination.socket) {
        destination.socket.send(data);
      } else if (destination.res) {
        data += '\n';
        destination.res.write(data);
      } else {
        // Neither socket no res available. Peer dead?
        throw "Peer dead";
      }
    } catch (e) {
      // This happens when a peer disconnects without closing connections and
      // the associated WebSocket has not closed.
      util.prettyError(e);
      // Tell other side to stop trying.
      this._removePeer(key, dst);
      this._handleTransmission(key, {
        type: 'LEAVE',
        src: dst,
        dst: src
      });
    }
  } else {
    // Wait for this client to connect/reconnect (XHR) for important
    // messages.
    if (type !== 'LEAVE' && type !== 'EXPIRE' && dst) {
      var self = this;
      if (!this._outstanding[key][dst]) {
        this._outstanding[key][dst] = [];
      }
      this._outstanding[key][dst].push(message);
    } else if (type === 'LEAVE' && !dst) {
      this._removePeer(key, src);
    } else {
      // Unavailable destination specified with message LEAVE or EXPIRE
      // Ignore
    }
  }
};

PeerServer.prototype._generateClientId = function(key) {
  var clientId = util.randomId();
  if (!this._clients[key]) {
    return clientId;
  }
  while (!!this._clients[key][clientId]) {
    clientId = util.randomId();
  }
  return clientId;
};

exports.PeerServer = PeerServer;
<commit_msg>Hack to fix #49 until we can move to express.<commit_after>var util = require('./util');
var restify = require('restify');
var EventEmitter = require('events').EventEmitter;
var WebSocketServer = require('ws').Server;
var url = require('url');

function PeerServer(options) {
  if (!(this instanceof PeerServer)) return new PeerServer(options);
  EventEmitter.call(this);

  this._options = util.extend({
    port: 80,
    debug: false,
    timeout: 5000,
    key: 'peerjs',
    ip_limit: 5000,
    concurrent_limit: 5000,
    ssl: {},
    path: '/',
    allow_discovery: false
  }, options);

  util.debug = this._options.debug;

  // Print warning if only one of the two is given.
  if (Object.keys(this._options.ssl).length === 1) {
    util.prettyError('Warning: PeerServer will not run on an HTTPS server'
        + ' because either the key or the certificate has not been provided.');
  }

  this._options.ssl['name'] = 'PeerServer';

  if (this._options.path[0] !== '/') {
    this._options.path = '/' + this._options.path;
  }
  if (this._options.path[this._options.path.length - 1] !== '/') {
    this._options.path += '/';
  }

  this._app = restify.createServer(this._options.ssl);

  // Connected clients
  this._clients = {};

  // Messages waiting for another peer.
  this._outstanding = {};

  // Initailize WebSocket server handlers.
  this._initializeWSS();

  // Initialize HTTP routes. This is only used for the first few milliseconds
  // before a socket is opened for a Peer.
  this._initializeHTTP();

  // Mark concurrent users per ip
  this._ips = {};

  this._setCleanupIntervals();
}

util.inherits(PeerServer, EventEmitter);

/** Initialize WebSocket server. */
PeerServer.prototype._initializeWSS = function() {
  var self = this;

  // Create WebSocket server as well.
  this._wss = new WebSocketServer({ path: this._options.path + 'peerjs', server: this._app});

  this._wss.on('connection', function(socket) {
    var query = url.parse(socket.upgradeReq.url, true).query;
    var id = query.id;
    var token = query.token;
    var key = query.key;
    var ip = socket.upgradeReq.socket.remoteAddress;

    if (!id || !token || !key) {
      socket.send(JSON.stringify({ type: 'ERROR', payload: { msg: 'No id, token, or key supplied to websocket server' } }));
      socket.close();
      return;
    }

    if (!self._clients[key] || !self._clients[key][id]) {
      self._checkKey(key, ip, function(err) {
        if (!err) {
          if (!self._clients[key][id]) {
            self._clients[key][id] = { token: token, ip: ip };
            self._ips[ip]++;
            socket.send(JSON.stringify({ type: 'OPEN' }));
          }
          self._configureWS(socket, key, id, token);
        } else {
          socket.send(JSON.stringify({ type: 'ERROR', payload: { msg: err } }));
        }
      });
    } else {
      self._configureWS(socket, key, id, token);
    }
  });
};

PeerServer.prototype._configureWS = function(socket, key, id, token) {
  var self = this;
  var client = this._clients[key][id];

  if (token === client.token) {
    // res 'close' event will delete client.res for us
    client.socket = socket;
    // Client already exists
    if (client.res) {
      client.res.end();
    }
  } else {
    // ID-taken, invalid token
    socket.send(JSON.stringify({ type: 'ID-TAKEN', payload: { msg: 'ID is taken' } }));
    socket.close();
    return;
  }

  this._processOutstanding(key, id);

  // Cleanup after a socket closes.
  socket.on('close', function() {
    util.log('Socket closed:', id);
    if (client.socket == socket) {
      self._removePeer(key, id);
    }
  });

  // Handle messages from peers.
  socket.on('message', function(data) {
    try {
      var message = JSON.parse(data);

      if (['LEAVE', 'CANDIDATE', 'OFFER', 'ANSWER'].indexOf(message.type) !== -1) {
        self._handleTransmission(key, {
          type: message.type,
          src: id,
          dst: message.dst,
          payload: message.payload
        });
      } else {
        util.prettyError('Message unrecognized');
      }
    } catch(e) {
      util.log('Invalid message', data);
      throw e;
    }
  });

  // We're going to emit here, because for XHR we don't *know* when someone
  // disconnects.
  this.emit('connection', id);
};

PeerServer.prototype._checkAllowsDiscovery = function(key, cb) {
  cb(this._options.allow_discovery);
};

PeerServer.prototype._checkKey = function(key, ip, cb) {
  if (key == this._options.key) {
    if (!this._clients[key]) {
      this._clients[key] = {};
    }
    if (!this._outstanding[key]) {
      this._outstanding[key] = {};
    }
    if (!this._ips[ip]) {
      this._ips[ip] = 0;
    }
    // Check concurrent limit
    if (Object.keys(this._clients[key]).length >= this._options.concurrent_limit) {
      cb('Server has reached its concurrent user limit');
      return;
    }
    if (this._ips[ip] >= this._options.ip_limit) {
      cb(ip + ' has reached its concurrent user limit');
      return;
    }
    cb(null);
  } else {
    cb('Invalid key provided');
  }
};

/** Initialize HTTP server routes. */
PeerServer.prototype._initializeHTTP = function() {
  var self = this;

  this._app.use(restify.bodyParser({ mapParams: false }));
  this._app.use(restify.queryParser());
  this._app.use(util.allowCrossDomain);

  /** Hack from https://github.com/mcavage/node-restify/issues/284, until we switch to express */
  function unknownMethodHandler(req, res) {
    if (req.method.toLowerCase() === 'options') {
      var allowHeaders = ['Accept', 'Accept-Version', 'Content-Type', 'Api-Version'];

      if (res.methods.indexOf('OPTIONS') === -1) res.methods.push('OPTIONS');

      res.header('Access-Control-Allow-Credentials', true);
      res.header('Access-Control-Allow-Headers', allowHeaders.join(', '));
      res.header('Access-Control-Allow-Methods', res.methods.join(', '));
      res.header('Access-Control-Allow-Origin', req.headers.origin);

      return res.send(204);
    } else {
      return res.send(new restify.MethodNotAllowedError());
    }
  }

  this._app.on('MethodNotAllowed', unknownMethodHandler);

  // Retrieve guaranteed random ID.
  this._app.get(this._options.path + ':key/id', function(req, res, next) {
    res.contentType = 'text/html';
    res.send(self._generateClientId(req.params.key));
    return next();
  });

  // Server sets up HTTP streaming when you get post an ID.
  this._app.post(this._options.path + ':key/:id/:token/id', function(req, res, next) {
    var id = req.params.id;
    var token = req.params.token;
    var key = req.params.key;
    var ip = req.connection.remoteAddress;

    if (!self._clients[key] || !self._clients[key][id]) {
      self._checkKey(key, ip, function(err) {
        if (!err && !self._clients[key][id]) {
          self._clients[key][id] = { token: token, ip: ip };
          self._ips[ip]++;
          self._startStreaming(res, key, id, token, true);
        } else {
          res.send(JSON.stringify({ type: 'HTTP-ERROR' }));
        }
      });
    } else {
      self._startStreaming(res, key, id, token);
    }
    return next();
  });

  // Get a list of all peers for a key, enabled by the `allowDiscovery` flag.
  this._app.get(this._options.path + ':key/peers', function(req, res, next) {
    var key = req.params.key;
    if (self._clients[key]) {
      self._checkAllowsDiscovery(key, function(isAllowed) {
        if (isAllowed) {
          res.send(Object.keys(self._clients[key]));
        } else {
          res.send(401);
        }
        return next();
      });
    } else {
      res.send(404);
      return next();
    }
  });

  var handle = function(req, res, next) {
    var key = req.params.key;
    var id = req.params.id;

    var client;
    if (!self._clients[key] || !(client = self._clients[key][id])) {
      if (req.params.retry) {
        res.send(401);
        return next();
      } else {
        // Retry this request
        req.params.retry = true;
        setTimeout(handle, 25, req, res);
        return;
      }
    }

    // Auth the req
    if (req.params.token !== client.token) {
      res.send(401);
      return;
    } else {
      self._handleTransmission(key, {
        type: req.body.type,
        src: id,
        dst: req.body.dst,
        payload: req.body.payload
      });
      res.send(200);
    }
    return next();
  };

  this._app.post(this._options.path + ':key/:id/:token/offer', handle);

  this._app.post(this._options.path + ':key/:id/:token/candidate', handle);

  this._app.post(this._options.path + ':key/:id/:token/answer', handle);

  this._app.post(this._options.path + ':key/:id/:token/leave', handle);

  // Listen on user-specified port and IP address.
  if (this._options.ip) {
    this._app.listen(this._options.port, this._options.ip);
  } else {
    this._app.listen(this._options.port);
  }

};

/** Saves a streaming response and takes care of timeouts and headers. */
PeerServer.prototype._startStreaming = function(res, key, id, token, open) {
  var self = this;

  res.writeHead(200, {'Content-Type': 'application/octet-stream'});

  var pad = '00';
  for (var i = 0; i < 10; i++) {
    pad += pad;
  }
  res.write(pad + '\n');

  if (open) {
    res.write(JSON.stringify({ type: 'OPEN' }) + '\n');
  }

  var client = this._clients[key][id];

  if (token === client.token) {
    // Client already exists
    res.on('close', function() {
      if (client.res === res) {
        if (!client.socket) {
          // No new request yet, peer dead
          self._removePeer(key, id);
          return;
        }
        delete client.res;
      }
    });
    client.res = res;
    this._processOutstanding(key, id);
  } else {
    // ID-taken, invalid token
    res.end(JSON.stringify({ type: 'HTTP-ERROR' }));
  }
};

PeerServer.prototype._pruneOutstanding = function() {
  var keys = Object.keys(this._outstanding);
  for (var k = 0, kk = keys.length; k < kk; k += 1) {
    var key = keys[k];
    var dsts = Object.keys(this._outstanding[key]);
    for (var i = 0, ii = dsts.length; i < ii; i += 1) {
      var offers = this._outstanding[key][dsts[i]];
      var seen = {};
      for (var j = 0, jj = offers.length; j < jj; j += 1) {
        var message = offers[j];
        if (!seen[message.src]) {
          this._handleTransmission(key, { type: 'EXPIRE', src: message.dst, dst: message.src });
          seen[message.src] = true;
        }
      }
    }
    this._outstanding[key] = {};
  }
};

/** Cleanup */
PeerServer.prototype._setCleanupIntervals = function() {
  var self = this;

  // Clean up ips every 10 minutes
  setInterval(function() {
    var keys = Object.keys(self._ips);
    for (var i = 0, ii = keys.length; i < ii; i += 1) {
      var key = keys[i];
      if (self._ips[key] === 0) {
        delete self._ips[key];
      }
    }
  }, 600000);

  // Clean up outstanding messages every 5 seconds
  setInterval(function() {
    self._pruneOutstanding();
  }, 5000);
};

/** Process outstanding peer offers. */
PeerServer.prototype._processOutstanding = function(key, id) {
  var offers = this._outstanding[key][id];
  if (!offers) {
    return;
  }
  for (var j = 0, jj = offers.length; j < jj; j += 1) {
    this._handleTransmission(key, offers[j]);
  }
  delete this._outstanding[key][id];
};

PeerServer.prototype._removePeer = function(key, id) {
  if (this._clients[key] && this._clients[key][id]) {
    this._ips[this._clients[key][id].ip]--;
    delete this._clients[key][id];
    this.emit('disconnect', id);
  }
};

/** Handles passing on a message. */
PeerServer.prototype._handleTransmission = function(key, message) {
  var type = message.type;
  var src = message.src;
  var dst = message.dst;
  var data = JSON.stringify(message);

  var destination = this._clients[key][dst];

  // User is connected!
  if (destination) {
    try {
      util.log(type, 'from', src, 'to', dst);
      if (destination.socket) {
        destination.socket.send(data);
      } else if (destination.res) {
        data += '\n';
        destination.res.write(data);
      } else {
        // Neither socket no res available. Peer dead?
        throw "Peer dead";
      }
    } catch (e) {
      // This happens when a peer disconnects without closing connections and
      // the associated WebSocket has not closed.
      util.prettyError(e);
      // Tell other side to stop trying.
      this._removePeer(key, dst);
      this._handleTransmission(key, {
        type: 'LEAVE',
        src: dst,
        dst: src
      });
    }
  } else {
    // Wait for this client to connect/reconnect (XHR) for important
    // messages.
    if (type !== 'LEAVE' && type !== 'EXPIRE' && dst) {
      var self = this;
      if (!this._outstanding[key][dst]) {
        this._outstanding[key][dst] = [];
      }
      this._outstanding[key][dst].push(message);
    } else if (type === 'LEAVE' && !dst) {
      this._removePeer(key, src);
    } else {
      // Unavailable destination specified with message LEAVE or EXPIRE
      // Ignore
    }
  }
};

PeerServer.prototype._generateClientId = function(key) {
  var clientId = util.randomId();
  if (!this._clients[key]) {
    return clientId;
  }
  while (!!this._clients[key][clientId]) {
    clientId = util.randomId();
  }
  return clientId;
};

exports.PeerServer = PeerServer;
<|endoftext|>