<commit_before>var assert = require('assert')
    , aws = require('aws-sdk')
    , LRU = require('lru-cache')
    ;

var dynamo;
var lru; // LRU cache of token revocation information

exports.init = function (options) {
    assert.ok(options);
    assert.ok(options.config);
    assert.ok(options.logger);

    if (!lru) {
        lru = LRU({
            max: +options.config.max_revocation_cache_size.current,
            maxAge: 1000 * +options.config.max_revocation_cache_age.current
        });
        options.config.max_revocation_cache_size.on('modified', function (new_max) {
            lru.max = +new_max;
            options.logger.info({ max: +new_max }, 'setting new maximum size of the revocation check LRU cache');
        });
        options.config.max_revocation_cache_age.on('modified', function (new_age) {
            lru._maxAge = 1000 * +new_age;
            options.logger.info({ max_age: +new_age }, 'setting new maximum age in seconds of the revocation check LRU cache');
        });
    }

    if (!dynamo) {
        // Create DynamoDB client
        aws.config.accessKeyId = options.config.aws_access_key_id.current;
        aws.config.secretAccessKey = options.config.aws_secret_access_key.current;
        aws.config.region = options.config.aws_revocation_region.current;
        aws.config.sslEnabled = true;
        dynamo = new aws.DynamoDB({ params: { TableName: options.config.aws_revocation_table.current }});
    }

    return {

        check_revocation: function (req_id, jwt, callback) {
            // Check revocation status of the JWT token and its issuance chain
            var ca = [];
            if (jwt.payload.ca)
                jwt.payload.ca.forEach(function (jti) { ca.push(jti); });
            if (jwt.payload.jti)
                ca.push(jwt.payload.jti);
            var valid = true;
            async.each(ca, function (jti, callback) {
                if (options.config.revocation_whitelist.list[jti]) {
                    // Whitelisted jti
                    options.logger.info({
                        req_id: req_id,
                        jti: jti
                    }, 'revocation check whitelist match');
                    return callback();
                }
                var jti_valid = lru.get(jti);
                if (jti_valid === undefined) {
                    // Revocation info not in cache, check in Dynamo

                    options.logger.info({
                        req_id: req_id,
                        jti: jti,
                    }, 'starting revocation check in DB');

                    var start = Date.now();
                    dynamo.query({
                        KeyConditions: {
                            jti: {
                                ComparisonOperator: 'EQ',
                                AttributeValueList: [ { S: jti } ]
                            }
                        }
                    }, function (err, data) {
                        if (err) {
                            options.logger.warn({
                                req_id: req_id,
                                jti: jti,
                                dynamo_req_id: this && this.requestId,
                                revocation_check_latency: Date.now() - start,
                                error: err.message
                            }, 'error checking revocation in DB');
                            return callback(err);
                        }

                        if (data.Count > 0)
                            valid = jti_valid = false;
                        else
                            jti_valid = true;

                        lru.set(jti, jti_valid);

                        options.logger.info({
                            req_id: req_id,
                            jti: jti,
                            valid: jti_valid,
                            dynamo_req_id: this && this.requestId,
                            revocation_check_latency: Date.now() - start
                        }, 'completed revocation check in DB');

                        return callback();
                    });
                }
                else {
                    // Use revocation info from cache
                    options.logger.info({
                        req_id: req_id,
                        jti: jti,
                        valid: jti_valid
                    }, 'revocation check cache result');
                    if (!jti_valid)
                        valid = false;
                    return callback();
                }
            }, function (error) {
                callback(error, valid);
            });
        },

        revoke_token: function (jti, jwt, callback) {
            lru.del(jti);
            dynamo.putItem({
                Item: {
                    jti: { S: jti },
                    jwt: { S: jwt },
                    t: { S: (new Date()).toString() }
                }
            }, function (error) {
                lru.del(jti);
                callback(error);
            });
        },
    };
};
<commit_msg>add missing module<commit_after>var assert = require('assert')
    , async = require('async')
    , aws = require('aws-sdk')
    , LRU = require('lru-cache')
    ;

var dynamo;
var lru; // LRU cache of token revocation information

exports.init = function (options) {
    assert.ok(options);
    assert.ok(options.config);
    assert.ok(options.logger);

    if (!lru) {
        lru = LRU({
            max: +options.config.max_revocation_cache_size.current,
            maxAge: 1000 * +options.config.max_revocation_cache_age.current
        });
        options.config.max_revocation_cache_size.on('modified', function (new_max) {
            lru.max = +new_max;
            options.logger.info({ max: +new_max }, 'setting new maximum size of the revocation check LRU cache');
        });
        options.config.max_revocation_cache_age.on('modified', function (new_age) {
            lru._maxAge = 1000 * +new_age;
            options.logger.info({ max_age: +new_age }, 'setting new maximum age in seconds of the revocation check LRU cache');
        });
    }

    if (!dynamo) {
        // Create DynamoDB client
        aws.config.accessKeyId = options.config.aws_access_key_id.current;
        aws.config.secretAccessKey = options.config.aws_secret_access_key.current;
        aws.config.region = options.config.aws_revocation_region.current;
        aws.config.sslEnabled = true;
        dynamo = new aws.DynamoDB({ params: { TableName: options.config.aws_revocation_table.current }});
    }

    return {

        check_revocation: function (req_id, jwt, callback) {
            // Check revocation status of the JWT token and its issuance chain
            var ca = [];
            if (jwt.payload.ca)
                jwt.payload.ca.forEach(function (jti) { ca.push(jti); });
            if (jwt.payload.jti)
                ca.push(jwt.payload.jti);
            var valid = true;
            async.each(ca, function (jti, callback) {
                if (options.config.revocation_whitelist.list[jti]) {
                    // Whitelisted jti
                    options.logger.info({
                        req_id: req_id,
                        jti: jti
                    }, 'revocation check whitelist match');
                    return callback();
                }
                var jti_valid = lru.get(jti);
                if (jti_valid === undefined) {
                    // Revocation info not in cache, check in Dynamo

                    options.logger.info({
                        req_id: req_id,
                        jti: jti,
                    }, 'starting revocation check in DB');

                    var start = Date.now();
                    dynamo.query({
                        KeyConditions: {
                            jti: {
                                ComparisonOperator: 'EQ',
                                AttributeValueList: [ { S: jti } ]
                            }
                        }
                    }, function (err, data) {
                        if (err) {
                            options.logger.warn({
                                req_id: req_id,
                                jti: jti,
                                dynamo_req_id: this && this.requestId,
                                revocation_check_latency: Date.now() - start,
                                error: err.message
                            }, 'error checking revocation in DB');
                            return callback(err);
                        }

                        if (data.Count > 0)
                            valid = jti_valid = false;
                        else
                            jti_valid = true;

                        lru.set(jti, jti_valid);

                        options.logger.info({
                            req_id: req_id,
                            jti: jti,
                            valid: jti_valid,
                            dynamo_req_id: this && this.requestId,
                            revocation_check_latency: Date.now() - start
                        }, 'completed revocation check in DB');

                        return callback();
                    });
                }
                else {
                    // Use revocation info from cache
                    options.logger.info({
                        req_id: req_id,
                        jti: jti,
                        valid: jti_valid
                    }, 'revocation check cache result');
                    if (!jti_valid)
                        valid = false;
                    return callback();
                }
            }, function (error) {
                callback(error, valid);
            });
        },

        revoke_token: function (jti, jwt, callback) {
            lru.del(jti);
            dynamo.putItem({
                Item: {
                    jti: { S: jti },
                    jwt: { S: jwt },
                    t: { S: (new Date()).toString() }
                }
            }, function (error) {
                lru.del(jti);
                callback(error);
            });
        },
    };
};
<|endoftext|>


<commit_before>'use strict';

const _ = require('lodash');
const knex = require('../knex');
const options = require('../config');
const RequestBroker = require('../classes/requestbroker');
const MetaDataFetcher = require('../classes/metadatafetcher');
const WebMentionBroker = require('../classes/webmentionbroker');
const Entries = require('../classes/entries');
const mfTools = require('../utils/mf-tools');
const urlTools = require('../utils/url-tools');
const request = require('../utils/request');
const url = require('url');
const qs = require('querystring');
const PGPubsub = require('pg-pubsub');
const EventEmitter = require('events').EventEmitter;
const cors = require('cors');
let pubsub,
  mentionsArguments,
  eventVariants,
  pingSuccessResponse,
  pingErrorResponse,
  isSingleTarget,
  handleCuttingEdgeEmbed,
  handleLegacyEmbed;
const mentionsEmitter = new EventEmitter();
const requestBroker = new RequestBroker();
const entryCollection = new Entries({
  knex: knex,
  requestBroker: requestBroker
});
const metaDataFetcher = new MetaDataFetcher(entryCollection);
const broker = new WebMentionBroker(requestBroker);

metaDataFetcher.on('metadata', function (entry, context) {
  if (context.responsesFor) {
    _(entry.getData().raw.microformats.items || [])
      .map(function (item) {
        return (mfTools.getUValues(item, 'url') || [])[0];
      })
      .filter()
      .forEach(function (url) {
        requestBroker.add('metadata', url, {
          commentOf: context.responsesFor
        });
      });

    return;
  }

  var target = context.target || context.commentOf;
  var direct = !!context.target;

  if (context.salmentionUpdate || context.webmention) {
    // We want to let them all through
  } else if (target && !entry.addTarget(target, direct)) {
    return;
  }

  entry.save().then(function () {
    if (context.webmention) {
      entry.ping(context.webmention);
    } else if (context.salmentionUpdate) {
      entry.pingMentions();
    } else {
      entry.findSalmentionTargets();
    }
  });
});

requestBroker.on('metadata', function (url, context) {
  // Fetch the metadata and then process it differently depending on context

  // TODO: Check if its already fetched before? Or just fetch always?

  metaDataFetcher.add(url, context);
});

requestBroker.on('webmention', function (url, context) {
  request.post({
    url: url,
    form: {
      source: context.source,
      target: context.target
    }
  }, function (err, res) {
    if (err || res.statusCode > 299) {
      console.error('Failed to ping webmention:', err || ('Invalid status code ' + res.statusCode));
      return;
    }
    // TODO: log the success or something
  });
});

mentionsEmitter.setMaxListeners(0);

pubsub = new PGPubsub(options.db);
pubsub.addChannel('mentions', function (message) {
  if (!message.eid) {
    return;
  }

  console.log('Mention notification!', message);

  entryCollection.get(message.eid).then(function (mention) {
    if (!mention) {
      return;
    }
    _.union(mention.targets, mention.removedTargets).forEach(function (target) {
      var normalizedTarget = urlTools.normalizeUrl(target);
      var targetHost = url.parse(normalizedTarget).hostname;

      mentionsEmitter.emit('url:' + normalizedTarget, mention);
      mentionsEmitter.emit('site:' + targetHost, mention);
    });
  });
});

// Utility functions

mentionsArguments = function (req) {
  return {
    example: req.query.example !== undefined ? true : undefined,
    url: req.query.url,
    site: req.query.site,
    path: req.query.path
  };
};

eventVariants = function (target) {
  var variants = [];

  if (!_.isEmpty(target.url)) {
    [].concat(target.url).forEach(function (targetUrl) {
      variants.push('url:' + urlTools.normalizeUrl(targetUrl));
    });
  }
  if (!_.isEmpty(target.site)) {
    [].concat(target.site).forEach(function (targetSite) {
      variants.push('site:' + targetSite);
    });
  }
  if (!_.isEmpty(target.path)) {
    // TODO: Enable listeners for this as well!
  }

  return variants;
};

pingSuccessResponse = function (res, syncType) {
  if (res.headersSent) {
    return;
  }

  var statusCode = syncType === 'sync' ? 200 : 202;

  res.format({
    'application/json': function () {
      res.send(statusCode, { pending: true, message: 'WebMention accepted' });
    },
    'text/plain': function () {
      res.send(statusCode, 'WebMention accepted');
    },
    default: function () {
      res.send(statusCode, undefined);
    }
  });
};

pingErrorResponse = function (res, err) {
  if (err.name !== 'WebMentionPingError') {
    console.warn(err);
    console.log(err.stack);
  }

  if (res.headersSent) {
    // The response has already been sent so not much else we can do with the error
    return;
  }

  const response = { error: true };
  const status = err.status || 500;

  if (err.status > 399 && err.status < 500 || err.status === 503) {
    response.message = err.message;
  }

  if (err.status === 503 && err.retryAfter) {
    res.set('Retry-After', err.retryAfter);
  }

  res.format({
    'application/json': function () {
      res.send(status, response);
    },
    'text/plain': function () {
      res.send(status, response.message);
    },
    default: function () {
      res.send(status, undefined);
    }
  });
};

isSingleTarget = function (req) {
  return !req.query.path && !req.query.site && (!_.isArray(req.query.url) || !req.query.url[1]);
};

handleCuttingEdgeEmbed = function (app, req, res, next) {
  var nofacepile = req.query.nofacepile !== undefined;

  Promise.all([
    entryCollection.queryByTarget(mentionsArguments(req), {
      interactions: nofacepile ? undefined : false,
      sort: req.query.sort ? req.query.sort.toLowerCase() : undefined,
      distillTargets: true
    }),
    nofacepile ? Promise.resolve([]) : entryCollection.queryByTarget(mentionsArguments(req), {
      interactions: true,
      sort: req.query.sort ? req.query.sort.toLowerCase() : undefined
    })
  ]).then(function (result) {
    const mentions = result[0];
    const interactions = result[1];
    const query = _.clone(req.query);
    const singleTarget = isSingleTarget(req);

    delete query.version;

    const templateOptions = {
      baseUrl: (options.https ? 'https://' : 'http://') + options.hostname,
      query: qs.stringify(query),
      singleTarget: singleTarget,
      showContext: !req.query.nocontext && !singleTarget
    };

    if (nofacepile) {
      templateOptions.nofacepile = true;
    }

    app.get('theme engine')
      .render('cutting-edge-embed', {
        interactions: interactions,
        mentions: mentions,
        options: templateOptions
      })
      .then(function (result) {
        res.setHeader('Content-Type', 'text/javascript');
        res.send(result);
      })
      .catch(next);
  });
};

handleLegacyEmbed = function (app, req, res, next) {
  entryCollection.queryByTarget(mentionsArguments(req), {
    sort: req.query.sort ? req.query.sort.toLowerCase() : undefined
  }).then(function (mentions) {
    // Remove unneeded data
    mentions = _.map(mentions, function (mention) {
      return _.omit(mention, ['targets', 'interactions']);
    });

    // And render the embed script
    app.get('theme engine')
      .render('embed', {
        mentions: mentions,
        options: {
          baseUrl: (options.https ? 'https://' : 'http://') + options.hostname,
          query: qs.stringify(req.query)
        }
      })
      .then(function (result) {
        res.setHeader('Content-Type', 'text/javascript');
        res.send(result);
      })
      .catch(next);
  });
};

// Route setup

module.exports = function (app) {
  app.get('/api/embed', function (req, res, next) {
    var version = req.query.version;

    if (version === 'cutting-edge') {
      handleCuttingEdgeEmbed(app, req, res, next);
    } else {
      handleLegacyEmbed(app, req, res, next);
    }
  });

  app.get('/api/mentions', cors(), function (req, res) {
    var format = (req.query.format || 'json').toLowerCase();
    var singleTarget = isSingleTarget(req);
    var options = {};

    if (_.isString(req.query.interactions)) {
      options.interactions = req.query.interactions !== '0';
    }

    if (_.isString(req.query.sort)) {
      options.sort = req.query.sort.toLowerCase();
    } else if (format !== 'json') {
      options.sort = 'desc';
    }

    entryCollection.queryByTarget(mentionsArguments(req), options)
      .then(function (mentions) {
        if (format === 'json') { return mentions; }

        if (format !== 'html') {
          res.sendStatus(400);
          return;
        }

        return app.get('theme engine').recursiveRenderer({
          templateWrappers: ['page'],
          children: [{
            template: 'mentions',
            mentions: mentions,
            singleTarget: singleTarget,
            showContext: !req.query.nocontext && !singleTarget,
            mentionsArguments: mentionsArguments(req)
          }]
        });
      })
      .then(function (mentions) {
        if (mentions) {
          res.send(mentions);
        }
      })
      .catch(function (err) {
        console.log(err);
        res.send(500, { error: 'An error occurred' });
      });
  });

  var liveRequests = [];

  app.get('/api/mentions/live', cors(), function (req, res) {
    res.writeHead(200, {
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache',
      'Connection': 'keep-alive'
    });

    // TODO: Send ID:s to make reconnect gapless

    var closeDown = function () {
      var pos = liveRequests.indexOf(closeDown);

      if (pos !== -1) {
        liveRequests.splice(pos, 1);
      }

      console.log('Disconnecting eventsource request! %d left', liveRequests.length);

      res.end();
      clearTimeout(heartbeatTimer);
      events.forEach(function (event) {
        mentionsEmitter.removeListener(event, sendEvent);
      });
    };

    liveRequests.push(closeDown);

    // To keep connections alive at eg. Heroku send something at least every 30s
    // but also make sure that we still close possibly dead connections every now and then
    var startTime = Date.now();
    var heartbeatTimer, heartbeat;
    heartbeat = function () {
      if (Date.now() - startTime > 5 * 60 * 1000) {
        console.log('Closing connection after 5 minutes');
        closeDown();
        return;
      }
      res.write('\n');
      heartbeatTimer = setTimeout(heartbeat, 25 * 1000);
    };

    const targetQuery = mentionsArguments(req);
    const events = eventVariants(targetQuery);
    let lastMention;

    var sendEvent = function (mention) {
      if (mention === lastMention) {
        return;
      }
      lastMention = mention;
      // TODO: Should not access private method
      mention = entryCollection._distillTargets(mention, targetQuery);
      res.write('event: mention\n');
      res.write('data: ' + JSON.stringify(mention) + '\n\n');
    };

    events.forEach(function (event) {
      mentionsEmitter.on(event, sendEvent);
    });

    req.socket.setTimeout(10 * 60 * 1000);
    req.once('close', closeDown);
    console.log('Starting eventsource request %d!', liveRequests.length);

    heartbeat();
  });

  app.get('/api/webmention', function (req, res, next) {
    app.get('theme engine')
      .recursiveRenderer({
        templateWrappers: ['page'],
        children: [{ template: 'info' }]
      })
      .then(res.send.bind(res))
      .catch(next);
  });

  app.post('/api/webmention', function (req, res) {
    const syncFetching = req.query.sync !== undefined;
    const successResponse = pingSuccessResponse.bind(undefined, res);
    const errorResponse = pingErrorResponse.bind(undefined, res);

    // Validate the request

    if (!req.body.source || !req.body.target) {
      res.json(400, {
        error: true,
        message: 'You need to specify both a "source" and a "target" URL'
      });
      return;
    }

    // Process the ping

    broker
      .addToQueue(req.body.source, req.body.target, syncFetching)
      .then(successResponse, errorResponse);
  });

  return {
    close: function () {
      pubsub.close();
      requestBroker.close();

      while (liveRequests.length) {
        liveRequests[0]();
      }
    }
  };
};
<commit_msg>refactor(api): some es6 alterations<commit_after>'use strict';

const _ = require('lodash');
const knex = require('../knex');
const options = require('../config');
const RequestBroker = require('../classes/requestbroker');
const MetaDataFetcher = require('../classes/metadatafetcher');
const WebMentionBroker = require('../classes/webmentionbroker');
const Entries = require('../classes/entries');
const mfTools = require('../utils/mf-tools');
const urlTools = require('../utils/url-tools');
const request = require('../utils/request');
const url = require('url');
const qs = require('querystring');
const PGPubsub = require('pg-pubsub');
const EventEmitter = require('events').EventEmitter;
const cors = require('cors');
let pubsub,
  mentionsArguments,
  eventVariants,
  pingSuccessResponse,
  pingErrorResponse,
  isSingleTarget,
  handleCuttingEdgeEmbed,
  handleLegacyEmbed;
const mentionsEmitter = new EventEmitter();
const requestBroker = new RequestBroker();
const entryCollection = new Entries({
  knex,
  requestBroker
});
const metaDataFetcher = new MetaDataFetcher(entryCollection);
const broker = new WebMentionBroker(requestBroker);

metaDataFetcher.on('metadata', function (entry, context) {
  if (context.responsesFor) {
    _(entry.getData().raw.microformats.items || [])
      .map(function (item) {
        return (mfTools.getUValues(item, 'url') || [])[0];
      })
      .filter()
      .forEach(function (url) {
        requestBroker.add('metadata', url, {
          commentOf: context.responsesFor
        });
      });

    return;
  }

  var target = context.target || context.commentOf;
  var direct = !!context.target;

  if (context.salmentionUpdate || context.webmention) {
    // We want to let them all through
  } else if (target && !entry.addTarget(target, direct)) {
    return;
  }

  entry.save().then(function () {
    if (context.webmention) {
      entry.ping(context.webmention);
    } else if (context.salmentionUpdate) {
      entry.pingMentions();
    } else {
      entry.findSalmentionTargets();
    }
  });
});

requestBroker.on('metadata', function (url, context) {
  // Fetch the metadata and then process it differently depending on context

  // TODO: Check if its already fetched before? Or just fetch always?

  metaDataFetcher.add(url, context);
});

requestBroker.on('webmention', function (url, context) {
  request.post({
    url: url,
    form: {
      source: context.source,
      target: context.target
    }
  }, function (err, res) {
    if (err || res.statusCode > 299) {
      console.error('Failed to ping webmention:', err || ('Invalid status code ' + res.statusCode));
      return;
    }
    // TODO: log the success or something
  });
});

mentionsEmitter.setMaxListeners(0);

pubsub = new PGPubsub(options.db);
pubsub.addChannel('mentions', function (message) {
  if (!message.eid) {
    return;
  }

  console.log('Mention notification!', message);

  entryCollection.get(message.eid).then(function (mention) {
    if (!mention) {
      return;
    }
    _.union(mention.targets, mention.removedTargets).forEach(function (target) {
      var normalizedTarget = urlTools.normalizeUrl(target);
      var targetHost = url.parse(normalizedTarget).hostname;

      mentionsEmitter.emit('url:' + normalizedTarget, mention);
      mentionsEmitter.emit('site:' + targetHost, mention);
    });
  });
});

// Utility functions

mentionsArguments = function (req) {
  return {
    example: req.query.example !== undefined ? true : undefined,
    url: req.query.url,
    site: req.query.site,
    path: req.query.path
  };
};

eventVariants = function (target) {
  var variants = [];

  if (!_.isEmpty(target.url)) {
    [].concat(target.url).forEach(function (targetUrl) {
      variants.push('url:' + urlTools.normalizeUrl(targetUrl));
    });
  }
  if (!_.isEmpty(target.site)) {
    [].concat(target.site).forEach(function (targetSite) {
      variants.push('site:' + targetSite);
    });
  }
  if (!_.isEmpty(target.path)) {
    // TODO: Enable listeners for this as well!
  }

  return variants;
};

pingSuccessResponse = function (res, syncType) {
  if (res.headersSent) {
    return;
  }

  var statusCode = syncType === 'sync' ? 200 : 202;

  res.format({
    'application/json': function () {
      res.send(statusCode, { pending: true, message: 'WebMention accepted' });
    },
    'text/plain': function () {
      res.send(statusCode, 'WebMention accepted');
    },
    default: function () {
      res.send(statusCode, undefined);
    }
  });
};

pingErrorResponse = function (res, err) {
  if (err.name !== 'WebMentionPingError') {
    console.warn(err);
    console.log(err.stack);
  }

  if (res.headersSent) {
    // The response has already been sent so not much else we can do with the error
    return;
  }

  const response = { error: true };
  const status = err.status || 500;

  if (err.status > 399 && err.status < 500 || err.status === 503) {
    response.message = err.message;
  }

  if (err.status === 503 && err.retryAfter) {
    res.set('Retry-After', err.retryAfter);
  }

  res.format({
    'application/json': function () {
      res.send(status, response);
    },
    'text/plain': function () {
      res.send(status, response.message);
    },
    default: function () {
      res.send(status, undefined);
    }
  });
};

isSingleTarget = function (req) {
  return !req.query.path && !req.query.site && (!_.isArray(req.query.url) || !req.query.url[1]);
};

handleCuttingEdgeEmbed = function (app, req, res, next) {
  var nofacepile = req.query.nofacepile !== undefined;

  Promise.all([
    entryCollection.queryByTarget(mentionsArguments(req), {
      interactions: nofacepile ? undefined : false,
      sort: req.query.sort ? req.query.sort.toLowerCase() : undefined,
      distillTargets: true
    }),
    nofacepile ? Promise.resolve([]) : entryCollection.queryByTarget(mentionsArguments(req), {
      interactions: true,
      sort: req.query.sort ? req.query.sort.toLowerCase() : undefined
    })
  ]).then(function (result) {
    const mentions = result[0];
    const interactions = result[1];
    const query = _.clone(req.query);
    const singleTarget = isSingleTarget(req);

    delete query.version;

    const templateOptions = {
      baseUrl: (options.https ? 'https://' : 'http://') + options.hostname,
      query: qs.stringify(query),
      singleTarget: singleTarget,
      showContext: !req.query.nocontext && !singleTarget
    };

    if (nofacepile) {
      templateOptions.nofacepile = true;
    }

    app.get('theme engine')
      .render('cutting-edge-embed', {
        interactions,
        mentions,
        options: templateOptions
      })
      .then(function (result) {
        res.setHeader('Content-Type', 'text/javascript');
        res.send(result);
      })
      .catch(next);
  });
};

handleLegacyEmbed = function (app, req, res, next) {
  entryCollection.queryByTarget(mentionsArguments(req), {
    sort: req.query.sort ? req.query.sort.toLowerCase() : undefined
  }).then(function (mentions) {
    // Remove unneeded data
    mentions = _.map(mentions, function (mention) {
      return _.omit(mention, ['targets', 'interactions']);
    });

    // And render the embed script
    app.get('theme engine')
      .render('embed', {
        mentions,
        options: {
          baseUrl: (options.https ? 'https://' : 'http://') + options.hostname,
          query: qs.stringify(req.query)
        }
      })
      .then(function (result) {
        res.setHeader('Content-Type', 'text/javascript');
        res.send(result);
      })
      .catch(next);
  });
};

// Route setup

module.exports = function (app) {
  app.get('/api/embed', function (req, res, next) {
    var version = req.query.version;

    if (version === 'cutting-edge') {
      handleCuttingEdgeEmbed(app, req, res, next);
    } else {
      handleLegacyEmbed(app, req, res, next);
    }
  });

  app.get('/api/mentions', cors(), function (req, res) {
    var format = (req.query.format || 'json').toLowerCase();
    var singleTarget = isSingleTarget(req);
    var options = {};

    if (_.isString(req.query.interactions)) {
      options.interactions = req.query.interactions !== '0';
    }

    if (_.isString(req.query.sort)) {
      options.sort = req.query.sort.toLowerCase();
    } else if (format !== 'json') {
      options.sort = 'desc';
    }

    entryCollection.queryByTarget(mentionsArguments(req), options)
      .then(function (mentions) {
        if (format === 'json') { return mentions; }

        if (format !== 'html') {
          res.sendStatus(400);
          return;
        }

        return app.get('theme engine').recursiveRenderer({
          templateWrappers: ['page'],
          children: [{
            template: 'mentions',
            mentions,
            singleTarget,
            showContext: !req.query.nocontext && !singleTarget,
            mentionsArguments: mentionsArguments(req)
          }]
        });
      })
      .then(function (mentions) {
        if (mentions) {
          res.send(mentions);
        }
      })
      .catch(function (err) {
        console.log(err);
        res.send(500, { error: 'An error occurred' });
      });
  });

  var liveRequests = [];

  app.get('/api/mentions/live', cors(), function (req, res) {
    res.writeHead(200, {
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache',
      'Connection': 'keep-alive'
    });

    // TODO: Send ID:s to make reconnect gapless

    var closeDown = function () {
      var pos = liveRequests.indexOf(closeDown);

      if (pos !== -1) {
        liveRequests.splice(pos, 1);
      }

      console.log('Disconnecting eventsource request! %d left', liveRequests.length);

      res.end();
      clearTimeout(heartbeatTimer);
      events.forEach(function (event) {
        mentionsEmitter.removeListener(event, sendEvent);
      });
    };

    liveRequests.push(closeDown);

    // To keep connections alive at eg. Heroku send something at least every 30s
    // but also make sure that we still close possibly dead connections every now and then
    var startTime = Date.now();
    var heartbeatTimer, heartbeat;
    heartbeat = function () {
      if (Date.now() - startTime > 5 * 60 * 1000) {
        console.log('Closing connection after 5 minutes');
        closeDown();
        return;
      }
      res.write('\n');
      heartbeatTimer = setTimeout(heartbeat, 25 * 1000);
    };

    const targetQuery = mentionsArguments(req);
    const events = eventVariants(targetQuery);
    let lastMention;

    var sendEvent = function (mention) {
      if (mention === lastMention) {
        return;
      }
      lastMention = mention;
      // TODO: Should not access private method
      mention = entryCollection._distillTargets(mention, targetQuery);
      res.write('event: mention\n');
      res.write('data: ' + JSON.stringify(mention) + '\n\n');
    };

    events.forEach(function (event) {
      mentionsEmitter.on(event, sendEvent);
    });

    req.socket.setTimeout(10 * 60 * 1000);
    req.once('close', closeDown);
    console.log('Starting eventsource request %d!', liveRequests.length);

    heartbeat();
  });

  app.get('/api/webmention', function (req, res, next) {
    app.get('theme engine')
      .recursiveRenderer({
        templateWrappers: ['page'],
        children: [{ template: 'info' }]
      })
      .then(res.send.bind(res))
      .catch(next);
  });

  app.post('/api/webmention', function (req, res) {
    const syncFetching = req.query.sync !== undefined;
    const successResponse = pingSuccessResponse.bind(undefined, res);
    const errorResponse = pingErrorResponse.bind(undefined, res);

    // Validate the request

    if (!req.body.source || !req.body.target) {
      res.json(400, {
        error: true,
        message: 'You need to specify both a "source" and a "target" URL'
      });
      return;
    }

    // Process the ping

    broker
      .addToQueue(req.body.source, req.body.target, syncFetching)
      .then(successResponse, errorResponse);
  });

  return {
    close: function () {
      pubsub.close();
      requestBroker.close();

      while (liveRequests.length) {
        liveRequests[0]();
      }
    }
  };
};
<|endoftext|>


<commit_before>/*
 * saint-five
 * https://github.com/randallagordon/saint-five
 *
 * Copyright (c) 2013 Randall A. Gordon <randall@randallagordon.com>
 * Licensed under the MIT license.
*/

var Spacebrew = require('spacebrew.js').Spacebrew;

var sb = {},
    ranges = {},
    booleans = {},
    strings = {},
    customs = {};

var defaults = {
  server: 'sandbox.spacebrew.cc',
  name: 'SAINT Number 5',
  description: 'Strategic Artificially Intelligent Nodeular Transport Number 5'
};

module.exports.connect = function( opts ) {
  opts = opts || {};

  sb = new Spacebrew.Client( opts.server || defaults.server,
                             opts.name || defaults.name,
                             opts.description || defaults.description );

  sb.connect();

  sb.onRangeMessage = function( name, value ) {
    ranges[ name ]( value );
  };

  sb.onBooleanMessage = function( name, value ) {
    booleans[ name ]( value );
  };

  sb.onStringMessage = function( name, value ) {
    strings[ name ]( value );
  };

  sb.onCustomMessage = function( name, value, type ) {
    customs[ name ]( value );
  };
};

module.exports.led = function( led ) {
  led = led || this;

  var ledID = 'Led' + led.pin;
  sb.addSubscribe( ledID, 'boolean' );

  booleans[ ledID ] = function( value ) {
    if ( value ) {
      led.on();
    } else {
      led.off();
    }
  };
};

module.exports.button = function( button ) {
  button = button || this;

  var buttonID = 'Button' + button.pin;
  sb.addPublish( buttonID, 'boolean', 'false' );

  button.on( 'down', function() {
    sb.send( buttonID, 'boolean', 'true' );
  });

  button.on( 'up', function() {
    sb.send( buttonID, 'boolean', 'false' );
  });
};

module.exports.servo = function( servo ) {
  servo = servo || this;

  var servoRange = [ 0, 180 ];

  var servoID = 'Servo' + servo.pin;
  sb.addSubscribe( servoID + 'min', 'boolean' );
  sb.addSubscribe( servoID + 'max', 'boolean' );

  booleans[ servoID + 'min' ] = function( value ) {
    if( value ) {
      servo.min();
    } else {
      servo.center();
    }
  };

  booleans[ servoID + 'max' ] = function( value ) {
    if( value ) {
      servo.max();
    } else {
      servo.center();
    }
  };

};

module.exports.nodebot = function( nodebot ) {
  nodebot = nodebot || this;

  var nodebotID = 'Nodebot ' + nodebot.servos.right.pin +
                  '/' + nodebot.servos.left.pin;

  sb.addSubscribe( nodebotID, 'orientation' );

  customs[ nodebotID ] = function( value ) {
    nodebot.move( 90 + value.gamma + value.beta, 90 - value.gamma + value.beta );
  };
};

module.exports.wire = module.exports.fire = function( j5obj ) {
  module.exports[ j5obj.constructor.name.toLowerCase() ]( j5obj );
};

module.exports.arm = function( five ) {
  [ 'Led', 'Button', 'Servo', 'Nodebot' ].forEach( function( constructor ) {
    five[ constructor ].prototype.fire = module.exports[ constructor.toLowerCase() ];
  });
};

<commit_msg>Nodebot control via device orientation data<commit_after>/*
 * saint-five
 * https://github.com/randallagordon/saint-five
 *
 * Copyright (c) 2013 Randall A. Gordon <randall@randallagordon.com>
 * Licensed under the MIT license.
*/

var Spacebrew = require('spacebrew.js').Spacebrew;

var sb = {},
    ranges = {},
    booleans = {},
    strings = {},
    customs = {};

var defaults = {
  server: 'sandbox.spacebrew.cc',
  name: 'SAINT Number 5',
  description: 'Strategic Artificially Intelligent Nodeular Transport Number 5'
};

module.exports.connect = function( opts ) {
  opts = opts || {};

  sb = new Spacebrew.Client( opts.server || defaults.server,
                             opts.name || defaults.name,
                             opts.description || defaults.description );

  sb.connect();

  sb.onRangeMessage = function( name, value ) {
    ranges[ name ]( value );
  };

  sb.onBooleanMessage = function( name, value ) {
    booleans[ name ]( value );
  };

  sb.onStringMessage = function( name, value ) {
    strings[ name ]( value );
  };

  sb.onCustomMessage = function( name, value, type ) {
    customs[ name ]( value );
  };
};

module.exports.led = function( led ) {
  led = led || this;

  var ledID = 'Led' + led.pin;
  sb.addSubscribe( ledID, 'boolean' );

  booleans[ ledID ] = function( value ) {
    if ( value ) {
      led.on();
    } else {
      led.off();
    }
  };
};

module.exports.button = function( button ) {
  button = button || this;

  var buttonID = 'Button' + button.pin;
  sb.addPublish( buttonID, 'boolean', 'false' );

  button.on( 'down', function() {
    sb.send( buttonID, 'boolean', 'true' );
  });

  button.on( 'up', function() {
    sb.send( buttonID, 'boolean', 'false' );
  });
};

module.exports.servo = function( servo ) {
  servo = servo || this;

  var servoRange = [ 0, 180 ];

  var servoID = 'Servo' + servo.pin;
  sb.addSubscribe( servoID + 'min', 'boolean' );
  sb.addSubscribe( servoID + 'max', 'boolean' );

  booleans[ servoID + 'min' ] = function( value ) {
    if( value ) {
      servo.min();
    } else {
      servo.center();
    }
  };

  booleans[ servoID + 'max' ] = function( value ) {
    if( value ) {
      servo.max();
    } else {
      servo.center();
    }
  };

};

module.exports.nodebot = function( nodebot ) {
  nodebot = nodebot || this;

  var nodebotID = 'Nodebot ' + nodebot.servos.right.pin +
                  '/' + nodebot.servos.left.pin;

  sb.addSubscribe( nodebotID, 'orientation' );

  customs[ nodebotID ] = function( value ) {
    var gamma = ( Math.abs( value.gamma ) > 45 ) ? 90 : value.gamma * 2;
    var beta  = ( Math.abs( value.beta  ) > 45 ) ? 90 : value.beta  * 2;
    nodebot.move( 90 + gamma + beta, 90 - gamma + beta );
  };
};

module.exports.wire = module.exports.fire = function( j5obj ) {
  module.exports[ j5obj.constructor.name.toLowerCase() ]( j5obj );
};

module.exports.arm = function( five ) {
  [ 'Led', 'Button', 'Servo', 'Nodebot' ].forEach( function( constructor ) {
    five[ constructor ].prototype.fire = module.exports[ constructor.toLowerCase() ];
  });
};

<|endoftext|>


<commit_before>/**
 * @depend util/core.js
 * @depend spy.js
 * @depend behavior.js
 */
/*jslint eqeqeq: false, onevar: false*/
/*global module, require, sinon*/
/**
 * Stub functions
 *
 * @author Christian Johansen (christian@cjohansen.no)
 * @license BSD
 *
 * Copyright (c) 2010-2013 Christian Johansen
 */
"use strict";

(function (sinon) {
    function makeApi(sinon) {
        function stub(object, property, func) {
            if (!!func && typeof func != "function") {
                throw new TypeError("Custom stub should be function");
            }

            var wrapper;

            if (func) {
                wrapper = sinon.spy && sinon.spy.create ? sinon.spy.create(func) : func;
            } else {
                wrapper = stub.create();
            }

            if (!object && typeof property === "undefined") {
                return sinon.stub.create();
            }

            if (typeof property === "undefined" && typeof object == "object") {
                for (var prop in object) {
                    if (typeof object[prop] === "function") {
                        stub(object, prop);
                    }
                }

                return object;
            }

            return sinon.wrapMethod(object, property, wrapper);
        }

        function getDefaultBehavior(stub) {
            return stub.defaultBehavior || getParentBehaviour(stub) || sinon.behavior.create(stub);
        }

        function getParentBehaviour(stub) {
            return (stub.parent && getCurrentBehavior(stub.parent));
        }

        function getCurrentBehavior(stub) {
            var behavior = stub.behaviors[stub.callCount - 1];
            return behavior && behavior.isPresent() ? behavior : getDefaultBehavior(stub);
        }

        var uuid = 0;

        var proto = {
            create: function create() {
                var functionStub = function () {
                    return getCurrentBehavior(functionStub).invoke(this, arguments);
                };

                functionStub.id = "stub#" + uuid++;
                var orig = functionStub;
                functionStub = sinon.spy.create(functionStub);
                functionStub.func = orig;

                sinon.extend(functionStub, stub);
                functionStub._create = sinon.stub.create;
                functionStub.displayName = "stub";
                functionStub.toString = sinon.functionToString;

                functionStub.defaultBehavior = null;
                functionStub.behaviors = [];

                return functionStub;
            },

            resetBehavior: function () {
                var i;

                this.defaultBehavior = null;
                this.behaviors = [];

                delete this.returnValue;
                delete this.returnArgAt;
                this.returnThis = false;

                if (this.fakes) {
                    for (i = 0; i < this.fakes.length; i++) {
                        this.fakes[i].resetBehavior();
                    }
                }
            },

            onCall: function(index) {
                if (!this.behaviors[index]) {
                    this.behaviors[index] = sinon.behavior.create(this);
                }

                return this.behaviors[index];
            },

            onFirstCall: function() {
                return this.onCall(0);
            },

            onSecondCall: function() {
                return this.onCall(1);
            },

            onThirdCall: function() {
                return this.onCall(2);
            }
        };

        for (var method in sinon.behavior) {
            if (sinon.behavior.hasOwnProperty(method) &&
                !proto.hasOwnProperty(method) &&
                method != 'create' &&
                method != 'withArgs' &&
                method != 'invoke') {
                proto[method] = (function(behaviorMethod) {
                    return function() {
                        this.defaultBehavior = this.defaultBehavior || sinon.behavior.create(this);
                        this.defaultBehavior[behaviorMethod].apply(this.defaultBehavior, arguments);
                        return this;
                    };
                }(method));
            }
        }

        sinon.extend(stub, proto);
        sinon.stub = stub;

        return stub;
    }

    var isNode = typeof module !== "undefined" && module.exports && typeof require == "function";
    var isAMD = typeof define === 'function' && typeof define.amd === 'object' && define.amd;

    function loadDependencies(require, exports, module) {
        var sinon = require('./util/core');
        require('./behavior');
        require('./spy');
        module.exports = makeApi(sinon);
    }

    if (isAMD) {
        define(loadDependencies);
    } else if (isNode) {
        loadDependencies(require, module.exports, module);
    } else if (!sinon) {
        return;
    } else {
        makeApi(sinon);
    }
}(typeof sinon == "object" && sinon || null));
<commit_msg>Lint lib/sinon/stub.js<commit_after>/**
 * @depend util/core.js
 * @depend spy.js
 * @depend behavior.js
 */
/**
 * Stub functions
 *
 * @author Christian Johansen (christian@cjohansen.no)
 * @license BSD
 *
 * Copyright (c) 2010-2013 Christian Johansen
 */
"use strict";

(function (sinon) {
    function makeApi(sinon) {
        function stub(object, property, func) {
            if (!!func && typeof func != "function") {
                throw new TypeError("Custom stub should be function");
            }

            var wrapper;

            if (func) {
                wrapper = sinon.spy && sinon.spy.create ? sinon.spy.create(func) : func;
            } else {
                wrapper = stub.create();
            }

            if (!object && typeof property === "undefined") {
                return sinon.stub.create();
            }

            if (typeof property === "undefined" && typeof object == "object") {
                for (var prop in object) {
                    if (typeof object[prop] === "function") {
                        stub(object, prop);
                    }
                }

                return object;
            }

            return sinon.wrapMethod(object, property, wrapper);
        }

        function getDefaultBehavior(stub) {
            return stub.defaultBehavior || getParentBehaviour(stub) || sinon.behavior.create(stub);
        }

        function getParentBehaviour(stub) {
            return (stub.parent && getCurrentBehavior(stub.parent));
        }

        function getCurrentBehavior(stub) {
            var behavior = stub.behaviors[stub.callCount - 1];
            return behavior && behavior.isPresent() ? behavior : getDefaultBehavior(stub);
        }

        var uuid = 0;

        var proto = {
            create: function create() {
                var functionStub = function () {
                    return getCurrentBehavior(functionStub).invoke(this, arguments);
                };

                functionStub.id = "stub#" + uuid++;
                var orig = functionStub;
                functionStub = sinon.spy.create(functionStub);
                functionStub.func = orig;

                sinon.extend(functionStub, stub);
                functionStub._create = sinon.stub.create;
                functionStub.displayName = "stub";
                functionStub.toString = sinon.functionToString;

                functionStub.defaultBehavior = null;
                functionStub.behaviors = [];

                return functionStub;
            },

            resetBehavior: function () {
                var i;

                this.defaultBehavior = null;
                this.behaviors = [];

                delete this.returnValue;
                delete this.returnArgAt;
                this.returnThis = false;

                if (this.fakes) {
                    for (i = 0; i < this.fakes.length; i++) {
                        this.fakes[i].resetBehavior();
                    }
                }
            },

            onCall: function onCall(index) {
                if (!this.behaviors[index]) {
                    this.behaviors[index] = sinon.behavior.create(this);
                }

                return this.behaviors[index];
            },

            onFirstCall: function onFirstCall() {
                return this.onCall(0);
            },

            onSecondCall: function onSecondCall() {
                return this.onCall(1);
            },

            onThirdCall: function onThirdCall() {
                return this.onCall(2);
            }
        };

        for (var method in sinon.behavior) {
            if (sinon.behavior.hasOwnProperty(method) &&
                !proto.hasOwnProperty(method) &&
                method != "create" &&
                method != "withArgs" &&
                method != "invoke") {
                proto[method] = (function (behaviorMethod) {
                    return function () {
                        this.defaultBehavior = this.defaultBehavior || sinon.behavior.create(this);
                        this.defaultBehavior[behaviorMethod].apply(this.defaultBehavior, arguments);
                        return this;
                    };
                }(method));
            }
        }

        sinon.extend(stub, proto);
        sinon.stub = stub;

        return stub;
    }

    var isNode = typeof module !== "undefined" && module.exports && typeof require == "function";
    var isAMD = typeof define === "function" && typeof define.amd === "object" && define.amd;

    function loadDependencies(require, exports, module) {
        var sinon = require("./util/core");
        require("./behavior");
        require("./spy");
        module.exports = makeApi(sinon);
    }

    if (isAMD) {
        define(loadDependencies);
    } else if (isNode) {
        loadDependencies(require, module.exports, module);
    } else if (!sinon) {
        return;
    } else {
        makeApi(sinon);
    }
}(typeof sinon == "object" && sinon || null));
<|endoftext|>


<commit_before>import { Triangle, Vector3, Line3, Sphere, Plane } from 'three';
import { SeparatingAxisBounds } from './SeparatingAxisBounds.js';
import { closestPointsSegmentToSegment, sphereIntersectTriangle } from './MathUtilities.js';

export class SeparatingAxisTriangle extends Triangle {

	constructor( ...args ) {

		super( ...args );

		this.isSeparatingAxisTriangle = true;
		this.satAxes = new Array( 4 ).fill().map( () => new Vector3() );
		this.satBounds = new Array( 4 ).fill().map( () => new SeparatingAxisBounds() );
		this.points = [ this.a, this.b, this.c ];
		this.sphere = new Sphere();
		this.plane = new Plane();
		this.needsUpdate = false;

	}

	intersectsSphere( sphere ) {

		return sphereIntersectTriangle( sphere, this );

	}

	update() {

		const a = this.a;
		const b = this.b;
		const c = this.c;
		const points = this.points;

		const satAxes = this.satAxes;
		const satBounds = this.satBounds;

		const axis0 = satAxes[ 0 ];
		const sab0 = satBounds[ 0 ];
		this.getNormal( axis0 );
		sab0.setFromPoints( axis0, points );

		const axis1 = satAxes[ 1 ];
		const sab1 = satBounds[ 1 ];
		axis1.subVectors( a, b );
		sab1.setFromPoints( axis1, points );

		const axis2 = satAxes[ 2 ];
		const sab2 = satBounds[ 2 ];
		axis2.subVectors( b, c );
		sab2.setFromPoints( axis2, points );

		const axis3 = satAxes[ 3 ];
		const sab3 = satBounds[ 3 ];
		axis3.subVectors( c, a );
		sab3.setFromPoints( axis3, points );

		this.sphere.setFromPoints( this.points );
		this.plane.setFromNormalAndCoplanarPoint( axis0, a );
		this.needsUpdate = false;

	}

}

SeparatingAxisTriangle.prototype.closestPointToSegment = ( function () {

	const point1 = new Vector3();
	const point2 = new Vector3();
	const edge = new Line3();

	return function distanceToSegment( segment, target1 = null, target2 = null ) {

		const { start, end } = segment;
		const points = this.points;
		let distSq;
		let closestDistanceSq = Infinity;

		// check the triangle edges
		for ( let i = 0; i < 3; i ++ ) {

			const nexti = ( i + 1 ) % 3;
			edge.start.copy( points[ i ] );
			edge.end.copy( points[ nexti ] );

			closestPointsSegmentToSegment( edge, segment, point1, point2 );

			distSq = point1.distanceToSquared( point2 );
			if ( distSq < closestDistanceSq ) {

				closestDistanceSq = distSq;
				if ( target1 ) target1.copy( point1 );
				if ( target2 ) target2.copy( point2 );

			}

		}

		// check end points
		this.closestPointToPoint( start, point1 );
		distSq = start.distanceToSquared( point1 );
		if ( distSq < closestDistanceSq ) {

			closestDistanceSq = distSq;
			if ( target1 ) target1.copy( point1 );
			if ( target2 ) target2.copy( start );

		}

		this.closestPointToPoint( end, point1 );
		distSq = end.distanceToSquared( point1 );
		if ( distSq < closestDistanceSq ) {

			closestDistanceSq = distSq;
			if ( target1 ) target1.copy( point1 );
			if ( target2 ) target2.copy( end );

		}

		return Math.sqrt( closestDistanceSq );

	};

} )();

SeparatingAxisTriangle.prototype.intersectsTriangle = ( function () {

	const saTri2 = new SeparatingAxisTriangle();
	const arr1 = new Array( 3 );
	const arr2 = new Array( 3 );
	const cachedSatBounds = new SeparatingAxisBounds();
	const cachedSatBounds2 = new SeparatingAxisBounds();
	const cachedAxis = new Vector3();
	const dir1 = new Vector3();
	const dir2 = new Vector3();
	const tempDir = new Vector3();
	const edge = new Line3();
	const edge1 = new Line3();
	const edge2 = new Line3();
	return function intersectsTriangle( other, target = null ) {

		if ( this.needsUpdate ) {

			this.update();

		}

		if ( ! other.isSeparatingAxisTriangle ) {

			saTri2.copy( other );
			saTri2.update();
			other = saTri2;

		} else if ( other.needsUpdate ) {

			other.update();

		}

		const satBounds1 = this.satBounds;
		const satAxes1 = this.satAxes;
		arr2[ 0 ] = other.a;
		arr2[ 1 ] = other.b;
		arr2[ 2 ] = other.c;
		for ( let i = 0; i < 4; i ++ ) {

			const sb = satBounds1[ i ];
			const sa = satAxes1[ i ];
			cachedSatBounds.setFromPoints( sa, arr2 );
			if ( sb.isSeparated( cachedSatBounds ) ) return false;

		}

		const satBounds2 = other.satBounds;
		const satAxes2 = other.satAxes;
		arr1[ 0 ] = this.a;
		arr1[ 1 ] = this.b;
		arr1[ 2 ] = this.c;
		for ( let i = 0; i < 4; i ++ ) {

			const sb = satBounds2[ i ];
			const sa = satAxes2[ i ];
			cachedSatBounds.setFromPoints( sa, arr1 );
			if ( sb.isSeparated( cachedSatBounds ) ) return false;

		}

		// check crossed axes
		for ( let i = 0; i < 4; i ++ ) {

			const sa1 = satAxes1[ i ];
			for ( let i2 = 0; i2 < 4; i2 ++ ) {

				const sa2 = satAxes2[ i2 ];
				cachedAxis.crossVectors( sa1, sa2 );
				cachedSatBounds.setFromPoints( cachedAxis, arr1 );
				cachedSatBounds2.setFromPoints( cachedAxis, arr2 );
				if ( cachedSatBounds.isSeparated( cachedSatBounds2 ) ) return false;

			}

		}

		if ( target ) {

			const plane1 = this.plane;
			const plane2 = other.plane;

			// find the edge that intersects the other triangle plane
			const points1 = this.points;
			let found1 = false;
			for ( let i = 0; i < 3; i ++ ) {

				const p1 = points1[ i ];
				const p2 = points1[ ( i + 1 ) % 3 ];

				edge.start.copy( p1 );
				edge.end.copy( p2 );

				if ( plane2.intersectLine( edge, found1 ? edge1.start : edge1.end ) ) {

					found1 = true;

				}

			}

			// find the other triangles edge that intersects this plane
			const points2 = this.points;
			let found2 = false;
			for ( let i = 0; i < 3; i ++ ) {

				const p1 = points2[ i ];
				const p2 = points2[ ( i + 1 ) % 3 ];

				edge.start.copy( p1 );
				edge.end.copy( p2 );

				if ( plane1.intersectLine( edge, found2 ? edge2.start : edge2.end ) ) {

					found2 = true;

				}

			}

			// find swap the second edge so both lines are running the same direction
			edge1.delta( dir1 );
			edge2.delta( dir2 );

			if ( dir1.dot( dir2 ) < 0 ) {

				let tmp = edge2.start;
				edge2.start = edge2.end;
				edge2.end = tmp;

			}

			tempDir.subVectors( edge1.start, edge2.start );
			if ( tempDir.dot( dir1 ) > 0 ) {

				target.start.copy( edge1.start );

			} else {

				target.start.copy( edge2.start );

			}

			tempDir.subVectors( edge1.end, edge2.end );
			if ( tempDir.dot( dir2 ) < 0 ) {

				target.end.copy( edge1.end );

			} else {

				target.end.copy( edge2.end );

			}

		}

		return true;

	};

} )();

SeparatingAxisTriangle.prototype.distanceToPoint = ( function () {

	const target = new Vector3();
	return function distanceToPoint( point ) {

		this.closestPointToPoint( point, target );
		return point.distanceTo( target );

	};

} )();

SeparatingAxisTriangle.prototype.distanceToTriangle = ( function () {

	const point = new Vector3();
	const point2 = new Vector3();
	const cornerFields = [ 'a', 'b', 'c' ];
	const line1 = new Line3();
	const line2 = new Line3();

	return function distanceToTriangle( other, target1 = null, target2 = null ) {

		const lineTarget = target1 || target2 ? line1 : null;
		if ( this.intersectsTriangle( other, lineTarget ) ) {

			if ( target1 || target2 ) {

				if ( target1 ) lineTarget.getCenter( target1 );
				if ( target2 ) lineTarget.getCenter( target2 );

			}

			return 0;

		}

		let closestDistanceSq = Infinity;

		// check all point distances
		for ( let i = 0; i < 3; i ++ ) {

			let dist;
			const field = cornerFields[ i ];
			const otherVec = other[ field ];
			this.closestPointToPoint( otherVec, point );

			dist = otherVec.distanceToSquared( point );

			if ( dist < closestDistanceSq ) {

				closestDistanceSq = dist;
				if ( target1 ) target1.copy( point );
				if ( target2 ) target2.copy( otherVec );

			}

			const thisVec = this[ field ];
			other.closestPointToPoint( thisVec, point );

			dist = thisVec.distanceToSquared( point );

			if ( dist < closestDistanceSq ) {

				closestDistanceSq = dist;
				if ( target1 ) target1.copy( thisVec );
				if ( target2 ) target2.copy( point );

			}

		}

		for ( let i = 0; i < 3; i ++ ) {

			const f11 = cornerFields[ i ];
			const f12 = cornerFields[ ( i + 1 ) % 3 ];
			line1.set( this[ f11 ], this[ f12 ] );
			for ( let i2 = 0; i2 < 3; i2 ++ ) {

				const f21 = cornerFields[ i2 ];
				const f22 = cornerFields[ ( i2 + 1 ) % 3 ];
				line2.set( other[ f21 ], other[ f22 ] );

				closestPointsSegmentToSegment( line1, line2, point, point2 );

				const dist = point.distanceToSquared( point2 );
				if ( dist < closestDistanceSq ) {

					closestDistanceSq = dist;
					if ( target1 ) target1.copy( point );
					if ( target2 ) target2.copy( point2 );

				}

			}

		}

		return Math.sqrt( closestDistanceSq );

	};

} )();

<commit_msg>get edge intersection working<commit_after>import { Triangle, Vector3, Line3, Sphere, Plane } from 'three';
import { SeparatingAxisBounds } from './SeparatingAxisBounds.js';
import { closestPointsSegmentToSegment, sphereIntersectTriangle } from './MathUtilities.js';

export class SeparatingAxisTriangle extends Triangle {

	constructor( ...args ) {

		super( ...args );

		this.isSeparatingAxisTriangle = true;
		this.satAxes = new Array( 4 ).fill().map( () => new Vector3() );
		this.satBounds = new Array( 4 ).fill().map( () => new SeparatingAxisBounds() );
		this.points = [ this.a, this.b, this.c ];
		this.sphere = new Sphere();
		this.plane = new Plane();
		this.needsUpdate = false;

	}

	intersectsSphere( sphere ) {

		return sphereIntersectTriangle( sphere, this );

	}

	update() {

		const a = this.a;
		const b = this.b;
		const c = this.c;
		const points = this.points;

		const satAxes = this.satAxes;
		const satBounds = this.satBounds;

		const axis0 = satAxes[ 0 ];
		const sab0 = satBounds[ 0 ];
		this.getNormal( axis0 );
		sab0.setFromPoints( axis0, points );

		const axis1 = satAxes[ 1 ];
		const sab1 = satBounds[ 1 ];
		axis1.subVectors( a, b );
		sab1.setFromPoints( axis1, points );

		const axis2 = satAxes[ 2 ];
		const sab2 = satBounds[ 2 ];
		axis2.subVectors( b, c );
		sab2.setFromPoints( axis2, points );

		const axis3 = satAxes[ 3 ];
		const sab3 = satBounds[ 3 ];
		axis3.subVectors( c, a );
		sab3.setFromPoints( axis3, points );

		this.sphere.setFromPoints( this.points );
		this.plane.setFromNormalAndCoplanarPoint( axis0, a );
		this.needsUpdate = false;

	}

}

SeparatingAxisTriangle.prototype.closestPointToSegment = ( function () {

	const point1 = new Vector3();
	const point2 = new Vector3();
	const edge = new Line3();

	return function distanceToSegment( segment, target1 = null, target2 = null ) {

		const { start, end } = segment;
		const points = this.points;
		let distSq;
		let closestDistanceSq = Infinity;

		// check the triangle edges
		for ( let i = 0; i < 3; i ++ ) {

			const nexti = ( i + 1 ) % 3;
			edge.start.copy( points[ i ] );
			edge.end.copy( points[ nexti ] );

			closestPointsSegmentToSegment( edge, segment, point1, point2 );

			distSq = point1.distanceToSquared( point2 );
			if ( distSq < closestDistanceSq ) {

				closestDistanceSq = distSq;
				if ( target1 ) target1.copy( point1 );
				if ( target2 ) target2.copy( point2 );

			}

		}

		// check end points
		this.closestPointToPoint( start, point1 );
		distSq = start.distanceToSquared( point1 );
		if ( distSq < closestDistanceSq ) {

			closestDistanceSq = distSq;
			if ( target1 ) target1.copy( point1 );
			if ( target2 ) target2.copy( start );

		}

		this.closestPointToPoint( end, point1 );
		distSq = end.distanceToSquared( point1 );
		if ( distSq < closestDistanceSq ) {

			closestDistanceSq = distSq;
			if ( target1 ) target1.copy( point1 );
			if ( target2 ) target2.copy( end );

		}

		return Math.sqrt( closestDistanceSq );

	};

} )();

SeparatingAxisTriangle.prototype.intersectsTriangle = ( function () {

	const saTri2 = new SeparatingAxisTriangle();
	const arr1 = new Array( 3 );
	const arr2 = new Array( 3 );
	const cachedSatBounds = new SeparatingAxisBounds();
	const cachedSatBounds2 = new SeparatingAxisBounds();
	const cachedAxis = new Vector3();
	const dir1 = new Vector3();
	const dir2 = new Vector3();
	const tempDir = new Vector3();
	const edge = new Line3();
	const edge1 = new Line3();
	const edge2 = new Line3();
	return function intersectsTriangle( other, target = null ) {

		if ( this.needsUpdate ) {

			this.update();

		}

		if ( ! other.isSeparatingAxisTriangle ) {

			saTri2.copy( other );
			saTri2.update();
			other = saTri2;

		} else if ( other.needsUpdate ) {

			other.update();

		}

		const satBounds1 = this.satBounds;
		const satAxes1 = this.satAxes;
		arr2[ 0 ] = other.a;
		arr2[ 1 ] = other.b;
		arr2[ 2 ] = other.c;
		for ( let i = 0; i < 4; i ++ ) {

			const sb = satBounds1[ i ];
			const sa = satAxes1[ i ];
			cachedSatBounds.setFromPoints( sa, arr2 );
			if ( sb.isSeparated( cachedSatBounds ) ) return false;

		}

		const satBounds2 = other.satBounds;
		const satAxes2 = other.satAxes;
		arr1[ 0 ] = this.a;
		arr1[ 1 ] = this.b;
		arr1[ 2 ] = this.c;
		for ( let i = 0; i < 4; i ++ ) {

			const sb = satBounds2[ i ];
			const sa = satAxes2[ i ];
			cachedSatBounds.setFromPoints( sa, arr1 );
			if ( sb.isSeparated( cachedSatBounds ) ) return false;

		}

		// check crossed axes
		for ( let i = 0; i < 4; i ++ ) {

			const sa1 = satAxes1[ i ];
			for ( let i2 = 0; i2 < 4; i2 ++ ) {

				const sa2 = satAxes2[ i2 ];
				cachedAxis.crossVectors( sa1, sa2 );
				cachedSatBounds.setFromPoints( cachedAxis, arr1 );
				cachedSatBounds2.setFromPoints( cachedAxis, arr2 );
				if ( cachedSatBounds.isSeparated( cachedSatBounds2 ) ) return false;

			}

		}

		if ( target ) {

			const plane1 = this.plane;
			const plane2 = other.plane;

			// find the edge that intersects the other triangle plane
			const points1 = this.points;
			let found1 = false;
			for ( let i = 0; i < 3; i ++ ) {

				const p1 = points1[ i ];
				const p2 = points1[ ( i + 1 ) % 3 ];

				edge.start.copy( p1 );
				edge.end.copy( p2 );

				if ( plane2.intersectLine( edge, found1 ? edge1.start : edge1.end ) ) {

					found1 = true;

				}

			}

			// find the other triangles edge that intersects this plane
			const points2 = other.points;
			let found2 = false;
			for ( let i = 0; i < 3; i ++ ) {

				const p1 = points2[ i ];
				const p2 = points2[ ( i + 1 ) % 3 ];

				edge.start.copy( p1 );
				edge.end.copy( p2 );

				if ( plane1.intersectLine( edge, found2 ? edge2.start : edge2.end ) ) {

					found2 = true;

				}

			}

			// find swap the second edge so both lines are running the same direction
			edge1.delta( dir1 );
			edge2.delta( dir2 );

			if ( dir1.dot( dir2 ) < 0 ) {

				let tmp = edge2.start;
				edge2.start = edge2.end;
				edge2.end = tmp;

			}

			tempDir.subVectors( edge1.start, edge2.start );
			if ( tempDir.dot( dir1 ) > 0 ) {

				target.start.copy( edge1.start );

			} else {

				target.start.copy( edge2.start );

			}

			tempDir.subVectors( edge1.end, edge2.end );
			if ( tempDir.dot( dir2 ) < 0 ) {

				target.end.copy( edge1.end );

			} else {

				target.end.copy( edge2.end );

			}

		}

		return true;

	};

} )();

SeparatingAxisTriangle.prototype.distanceToPoint = ( function () {

	const target = new Vector3();
	return function distanceToPoint( point ) {

		this.closestPointToPoint( point, target );
		return point.distanceTo( target );

	};

} )();

SeparatingAxisTriangle.prototype.distanceToTriangle = ( function () {

	const point = new Vector3();
	const point2 = new Vector3();
	const cornerFields = [ 'a', 'b', 'c' ];
	const line1 = new Line3();
	const line2 = new Line3();

	return function distanceToTriangle( other, target1 = null, target2 = null ) {

		const lineTarget = target1 || target2 ? line1 : null;
		if ( this.intersectsTriangle( other, lineTarget ) ) {

			if ( target1 || target2 ) {

				if ( target1 ) lineTarget.getCenter( target1 );
				if ( target2 ) lineTarget.getCenter( target2 );

			}

			return 0;

		}

		let closestDistanceSq = Infinity;

		// check all point distances
		for ( let i = 0; i < 3; i ++ ) {

			let dist;
			const field = cornerFields[ i ];
			const otherVec = other[ field ];
			this.closestPointToPoint( otherVec, point );

			dist = otherVec.distanceToSquared( point );

			if ( dist < closestDistanceSq ) {

				closestDistanceSq = dist;
				if ( target1 ) target1.copy( point );
				if ( target2 ) target2.copy( otherVec );

			}

			const thisVec = this[ field ];
			other.closestPointToPoint( thisVec, point );

			dist = thisVec.distanceToSquared( point );

			if ( dist < closestDistanceSq ) {

				closestDistanceSq = dist;
				if ( target1 ) target1.copy( thisVec );
				if ( target2 ) target2.copy( point );

			}

		}

		for ( let i = 0; i < 3; i ++ ) {

			const f11 = cornerFields[ i ];
			const f12 = cornerFields[ ( i + 1 ) % 3 ];
			line1.set( this[ f11 ], this[ f12 ] );
			for ( let i2 = 0; i2 < 3; i2 ++ ) {

				const f21 = cornerFields[ i2 ];
				const f22 = cornerFields[ ( i2 + 1 ) % 3 ];
				line2.set( other[ f21 ], other[ f22 ] );

				closestPointsSegmentToSegment( line1, line2, point, point2 );

				const dist = point.distanceToSquared( point2 );
				if ( dist < closestDistanceSq ) {

					closestDistanceSq = dist;
					if ( target1 ) target1.copy( point );
					if ( target2 ) target2.copy( point2 );

				}

			}

		}

		return Math.sqrt( closestDistanceSq );

	};

} )();

<|endoftext|>


<commit_before>var PassThrough = require('stream').PassThrough;
var wsParser = require('ws-parser');
var util = require('./util');
var Buffer = require('safe-buffer').Buffer;

var proxy;
var index = 0;
var MAX_PAYLOAD = 1024 * 128;
var conns = {};

function getFrameId() {
  ++index;
  if (index > 999) {
    index = 0;
  }
  if (index > 99) {
    return Date.now() + '-' + index;
  }
  if (index > 9) {
    return Date.now() + '-0' + index;
  }
  return Date.now() + '-00' + index;
}

exports = module.exports = function(p) {
  proxy = p;
};

function handleClose(req, res, reqReceiver, resReceiver) {
  var closed;
  var callback = function(err) {
    if (closed) {
      return;
    }
    closed = true;
    delete conns[req.reqId];
    proxy.emit('frame', {
      reqId:  req.reqId,
      frameId: getFrameId(),
      closed: true,
      err: err && err.stack
    });
    reqReceiver && reqReceiver.cleanup();
    resReceiver && resReceiver.cleanup();
  };
  util.onSocketEnd(req, callback);
  util.onSocketEnd(res, callback);
}

function getStatus(ctx, status, name) {
  status = parseInt(status, 10);
  name = name || 'receiveStatus';
  var oldStatus = ctx[name] || 0;
  status = ctx[name] = (status > 0 || status < 3) ? status : 0;
  return status !== oldStatus ? status : -1;
}

function setConnStatus(ctx, status, statusObj, name, emitData) {
  statusObj.pause = statusObj.ignore = undefined;
  status = getStatus(ctx, status, name);
  if (status === 1) {
    statusObj.pause = true;
    return;
  }
  if (status === 2) {
    statusObj.ignore = true;
    emitData && statusObj.chunk && emitData(statusObj.chunk, true, statusObj.ignore);
    statusObj.chunk = null;
  }
  if (statusObj.callback) {
    statusObj.callback(null, statusObj.chunk);
    statusObj.callback = null;
    statusObj.chunk = null;
  }
}

exports.handleConnect = function(req, res) {
  var hasEvent = util.listenerCount(proxy, 'tunnelRequest');
  var isConn = req.inspectFrames;
  if (hasEvent || isConn) {
    var url = req.fullUrl;
    var reqId = req.reqId;
    var charset = util.getCharset(req.headers && req.headers['content-type']) || '';
    var sendStatus = {};
    var receiveStatus = {};

    var emitData = function(chunk, fromClient, ignore) {
      proxy.emit('frame', {
        reqId: reqId,
        frameId: getFrameId(),
        isClient: fromClient,
        charset: charset,
        length: chunk.length,
        ignore: ignore,
        bin: chunk
      });
    };
    if (isConn) {
      var ctx = conns[reqId] = {
        req: req,
        res: res,
        charset: charset,
        setSendStatus: function(status) {
          setConnStatus(ctx, status, sendStatus, 'sendStatus', emitData);
        },
        setReceiveStatus: function(status) {
          setConnStatus(ctx, status, receiveStatus, null, emitData);
        },
        sendToServer: function(data) {
          data = data.data;
          res.write(data);
          emitData(data, true);
        },
        sendToClient: function(data) {
          data = data.data;
          req.write(data);
          emitData(data);
        }
      };
    }
    var reqTrans = new PassThrough();
    reqTrans.on('error', function(err) {
      req.emit('error', err);
    });
    reqTrans._transform = function(chunk, encoding, cb) {
      hasEvent && proxy.emit('tunnelRequest', url);
      if (isConn) {
        if (sendStatus.pause) {
          sendStatus.chunk = chunk;
          sendStatus.callback = cb;
          return;
        }
        emitData(chunk, true, sendStatus.ignore);
        if (sendStatus.ignore) {
          chunk = null;
        }
      }
      cb(null, chunk);
    };
    req.pipe(reqTrans).pipe(res);

    var resTrans = new PassThrough();
    resTrans.on('error', function(err) {
      req.emit('error', err);
    });

    resTrans._transform = function(chunk, encoding, cb) {
      hasEvent && proxy.emit('tunnelRequest', url);
      if (isConn) {
        if (receiveStatus.pause) {
          receiveStatus.chunk = chunk;
          receiveStatus.callback = cb;
          return;
        }
        emitData(chunk, undefined, receiveStatus.ignore);
        if (receiveStatus.ignore) {
          chunk = null;
        }
      }
      cb(null, chunk);
    };
    res.pipe(resTrans).pipe(req);
    if (isConn) {
      handleClose(req, res);
    }
  } else {
    req.pipe(res).pipe(req);
  }
};

function getBinary(data, len) {
  return len >MAX_PAYLOAD ? data.slice(0, MAX_PAYLOAD) : data;
}

function drainData(status, socket, receiver) {
  if (!status.sender) {
    status.sender = wsParser.getSender(socket);
  }
  status.data.forEach(function(item) {
    status.sender.send(item.data, item);
    receiver.add(item.data, item);
  });
}

function handleFrame(receiver, socket, status, chunk, cb) {
  if (!receiver.existsCacheData) {
    if (status.pause) {
      status.callback = cb;
      status.chunk = chunk;
      drainData(status, socket, receiver);
      return;
    }
    if (status.ignore) {
      status.ignoring = true;
      drainData(status, socket, receiver);
    }
  }
  var toRead = receiver.add(chunk);
  if (status.ignoring) {
    if (!status.ignore && toRead >= 0) {
      status.ignoring = false;
      socket.write(chunk.slice(toRead));
    }
    chunk = null;
  } else if (toRead >= 0 && (status.pause ||
    status.ignore || status.data.length)) {
    if (toRead) {
      var readAll = toRead === chunk.length;
      status.chunk = readAll ? null : chunk.slice(toRead);
      socket.write(readAll ? chunk : chunk.slice(0, toRead));
    } else {
      status.chunk = chunk;
    }
    if (status.pause) {
      status.callback = cb;
      drainData(status, socket, receiver);
      return;
    }
    if (status.ignore) {
      status.ignoring = true;
      chunk = null;
      drainData(status, socket, receiver);
    }
  }
  cb(null, chunk);
}

exports.handleWebsocket = function(req, res) {
  var url = req.fullUrl;
  var hasEvent = util.listenerCount(proxy, 'wsRequest');
  var reqId = req.reqId;
  var sendStatus = { data: [] };
  var receiveStatus = { data: [] };

  res.headers = res.headers || {};
  var charset = util.getCharset(res.headers['content-type']) || '';
  var ctx = conns[reqId] = {
    isWs: true,
    req: req,
    res: res,
    charset: charset,
    setSendStatus: function(status) {
      setConnStatus(ctx, status, sendStatus, 'sendStatus');
    },
    setReceiveStatus: function(status) {
      setConnStatus(ctx, status, receiveStatus);
    },
    sendToServer: function(data) {
      sendStatus.data.push(data);
      if (sendStatus.ignoring || sendStatus.callback) {
        drainData(sendStatus, res, reqReceiver);
      }
    },
    sendToClient: function(data) {
      receiveStatus.data.push(data);
      if (receiveStatus.ignoring || receiveStatus.callback) {
        drainData(receiveStatus, req, resReceiver);
      }
    }
  };
  var reqTrans = new PassThrough();
  reqTrans.on('error', function(err) {
    req.emit('error', err);
  });
  var reqReceiver = wsParser.getReceiver(res);
  reqReceiver.onData = function(data, opts) {
    proxy.emit('frame', {
      reqId: reqId,
      frameId: getFrameId(),
      isClient: true,
      charset: charset,
      mask: opts.mask,
      bin: getBinary(data, opts.length),
      compressed: opts.compressed,
      length: opts.length,
      opcode: opts.opcode
    });
  };
  reqReceiver.onerror = function(err) {
    proxy.emit('frame', {
      reqId: reqId,
      frameId: getFrameId(),
      isClient: true,
      err: err.stack,
      bin: ''
    });
  };
  reqTrans._transform = function(chunk, encoding, cb) {
    hasEvent && proxy.emit('wsRequest', url);
    handleFrame(reqReceiver, res, sendStatus, chunk, cb);
  };
  req.pipe(reqTrans).pipe(res);

  var resTrans = new PassThrough();
  resTrans.on('error', function(err) {
    req.emit('error', err);
  });
  var resReceiver =  wsParser.getReceiver(res, true);
  resReceiver.onData = function(data, opts) {
    proxy.emit('frame', {
      charset: charset,
      reqId: reqId,
      frameId: getFrameId(),
      bin: getBinary(data, opts.length),
      mask: opts.mask,
      compressed: opts.compressed,
      length: opts.length,
      opcode: opts.opcode
    });
  };
  resReceiver.onerror = function(err) {
    proxy.emit('frame', {
      reqId: reqId,
      frameId: getFrameId(),
      err: err.stack,
      bin: ''
    });
  };
  resTrans._transform = function(chunk, encoding, cb) {
    hasEvent && proxy.emit('wsRequest', url);
    handleFrame(resReceiver, req, receiveStatus, chunk, cb);
  };
  res.pipe(resTrans).pipe(req);
  handleClose(req, res, reqReceiver, resReceiver);
};

exports.abort = function(reqId) {
  var ctx = conns[reqId];
  if (!ctx) {
    return;
  }
  delete conns[reqId];
  ctx.req.destroy();
  ctx.res.destroy();
};

exports.getStatus = function(reqId) {
  var ctx = reqId && conns[reqId];
  if (!ctx) {
    return;
  }
  return {
    sendStatus: ctx.sendStatus,
    receiveStatus: ctx.receiveStatus
  };
};

exports.changeStatus = function(data) {
  var ctx = conns[data.reqId];
  if (!ctx) {
    return;
  }
  if (data.sendStatus >= 0) {
    ctx.setSendStatus(data.sendStatus);
  } else {
    ctx.setReceiveStatus(data.receiveStatus);
  }
  return true;
};

function getBuffer(data, charset) {
  if (data.base64) {
    try {
      return Buffer.from(data.base64, 'base64');
    } catch(e) {}
  } else if (data.text) {
    return util.toBuffer(data.text, charset);
  }
}

exports.sendData = function(data) {
  var ctx = conns[data.reqId];
  if (!ctx) {
    return;
  }
  var buf = getBuffer(data, ctx.charset);
  if (!buf) {
    return;
  }
  var isServer = data.target === 'server';
  var binary = data.type === 'bin';
  data = {
    binary: binary,
    opcode: binary ? 2 : 1,
    length: buf.length,
    data: buf
  };
  if (isServer) {
    data.mask = true;
    ctx.sendToServer(data);
  } else {
    ctx.sendToClient(data);
  }
};
<commit_msg>feat: handle websocket frame data<commit_after>var PassThrough = require('stream').PassThrough;
var wsParser = require('ws-parser');
var util = require('./util');
var Buffer = require('safe-buffer').Buffer;

var proxy;
var index = 0;
var MAX_PAYLOAD = 1024 * 128;
var conns = {};

function getFrameId() {
  ++index;
  if (index > 999) {
    index = 0;
  }
  if (index > 99) {
    return Date.now() + '-' + index;
  }
  if (index > 9) {
    return Date.now() + '-0' + index;
  }
  return Date.now() + '-00' + index;
}

exports = module.exports = function(p) {
  proxy = p;
};

function handleClose(req, res, reqReceiver, resReceiver) {
  var closed;
  var callback = function(err) {
    if (closed) {
      return;
    }
    closed = true;
    delete conns[req.reqId];
    proxy.emit('frame', {
      reqId:  req.reqId,
      frameId: getFrameId(),
      closed: true,
      err: err && err.stack
    });
    reqReceiver && reqReceiver.cleanup();
    resReceiver && resReceiver.cleanup();
  };
  util.onSocketEnd(req, callback);
  util.onSocketEnd(res, callback);
}

function getStatus(ctx, status, name) {
  status = parseInt(status, 10);
  name = name || 'receiveStatus';
  var oldStatus = ctx[name] || 0;
  status = ctx[name] = (status > 0 || status < 3) ? status : 0;
  return status !== oldStatus ? status : -1;
}

function setConnStatus(ctx, status, statusObj, name, emitData) {
  statusObj.pause = statusObj.ignore = undefined;
  status = getStatus(ctx, status, name);
  if (status === 1) {
    statusObj.pause = true;
    return;
  }
  if (status === 2) {
    statusObj.ignore = true;
    emitData && statusObj.chunk && emitData(statusObj.chunk, true, statusObj.ignore);
    statusObj.chunk = null;
  }
  if (statusObj.callback) {
    statusObj.callback(null, statusObj.chunk);
    statusObj.callback = null;
    statusObj.chunk = null;
  }
}

exports.handleConnect = function(req, res) {
  var hasEvent = util.listenerCount(proxy, 'tunnelRequest');
  var isConn = req.inspectFrames;
  if (hasEvent || isConn) {
    var url = req.fullUrl;
    var reqId = req.reqId;
    var charset = util.getCharset(req.headers && req.headers['content-type']) || '';
    var sendStatus = {};
    var receiveStatus = {};

    var emitData = function(chunk, fromClient, ignore) {
      proxy.emit('frame', {
        reqId: reqId,
        frameId: getFrameId(),
        isClient: fromClient,
        charset: charset,
        length: chunk.length,
        ignore: ignore,
        bin: chunk
      });
    };
    if (isConn) {
      var ctx = conns[reqId] = {
        req: req,
        res: res,
        charset: charset,
        setSendStatus: function(status) {
          setConnStatus(ctx, status, sendStatus, 'sendStatus', emitData);
        },
        setReceiveStatus: function(status) {
          setConnStatus(ctx, status, receiveStatus, null, emitData);
        },
        sendToServer: function(data) {
          data = data.data;
          res.write(data);
          emitData(data, true);
        },
        sendToClient: function(data) {
          data = data.data;
          req.write(data);
          emitData(data);
        }
      };
    }
    var reqTrans = new PassThrough();
    reqTrans.on('error', function(err) {
      req.emit('error', err);
    });
    reqTrans._transform = function(chunk, encoding, cb) {
      hasEvent && proxy.emit('tunnelRequest', url);
      if (isConn) {
        if (sendStatus.pause) {
          sendStatus.chunk = chunk;
          sendStatus.callback = cb;
          return;
        }
        emitData(chunk, true, sendStatus.ignore);
        if (sendStatus.ignore) {
          chunk = null;
        }
      }
      cb(null, chunk);
    };
    req.pipe(reqTrans).pipe(res);

    var resTrans = new PassThrough();
    resTrans.on('error', function(err) {
      req.emit('error', err);
    });

    resTrans._transform = function(chunk, encoding, cb) {
      hasEvent && proxy.emit('tunnelRequest', url);
      if (isConn) {
        if (receiveStatus.pause) {
          receiveStatus.chunk = chunk;
          receiveStatus.callback = cb;
          return;
        }
        emitData(chunk, undefined, receiveStatus.ignore);
        if (receiveStatus.ignore) {
          chunk = null;
        }
      }
      cb(null, chunk);
    };
    res.pipe(resTrans).pipe(req);
    if (isConn) {
      handleClose(req, res);
    }
  } else {
    req.pipe(res).pipe(req);
  }
};

function getBinary(data, len) {
  return len >MAX_PAYLOAD ? data.slice(0, MAX_PAYLOAD) : data;
}

function drainData(status, socket, receiver) {
  if (!status.sender) {
    status.sender = wsParser.getSender(socket);
  }
  status.data.forEach(function(item) {
    status.sender.send(item.data, item);
    receiver.onData(item.data, item);
  });
}

function handleFrame(receiver, socket, status, chunk, cb) {
  if (!receiver.existsCacheData) {
    if (status.pause) {
      status.callback = cb;
      status.chunk = chunk;
      drainData(status, socket, receiver);
      return;
    }
    if (status.ignore) {
      status.ignoring = true;
      drainData(status, socket, receiver);
    }
  }
  var toRead = receiver.add(chunk);
  if (status.ignoring) {
    if (!status.ignore && toRead >= 0) {
      status.ignoring = false;
      socket.write(chunk.slice(toRead));
    }
    chunk = null;
  } else if (toRead >= 0 && (status.pause ||
    status.ignore || status.data.length)) {
    if (toRead) {
      var readAll = toRead === chunk.length;
      status.chunk = readAll ? null : chunk.slice(toRead);
      socket.write(readAll ? chunk : chunk.slice(0, toRead));
    } else {
      status.chunk = chunk;
    }
    if (status.pause) {
      status.callback = cb;
      drainData(status, socket, receiver);
      return;
    }
    if (status.ignore) {
      status.ignoring = true;
      chunk = null;
      drainData(status, socket, receiver);
    }
  }
  cb(null, chunk);
}

exports.handleWebsocket = function(req, res) {
  var url = req.fullUrl;
  var hasEvent = util.listenerCount(proxy, 'wsRequest');
  var reqId = req.reqId;
  var sendStatus = { data: [] };
  var receiveStatus = { data: [] };

  res.headers = res.headers || {};
  var charset = util.getCharset(res.headers['content-type']) || '';
  var ctx = conns[reqId] = {
    isWs: true,
    req: req,
    res: res,
    charset: charset,
    setSendStatus: function(status) {
      setConnStatus(ctx, status, sendStatus, 'sendStatus');
    },
    setReceiveStatus: function(status) {
      setConnStatus(ctx, status, receiveStatus);
    },
    sendToServer: function(data) {
      sendStatus.data.push(data);
      if (sendStatus.ignoring || sendStatus.callback || !reqReceiver.existsCacheData) {
        drainData(sendStatus, res, reqReceiver);
      }
    },
    sendToClient: function(data) {
      receiveStatus.data.push(data);
      if (receiveStatus.ignoring || receiveStatus.callback || !resReceiver.existsCacheData) {
        drainData(receiveStatus, req, resReceiver);
      }
    }
  };
  var reqTrans = new PassThrough();
  reqTrans.on('error', function(err) {
    req.emit('error', err);
  });
  var reqReceiver = wsParser.getReceiver(res);
  reqReceiver.onData = function(data, opts) {
    proxy.emit('frame', {
      reqId: reqId,
      frameId: getFrameId(),
      isClient: true,
      charset: charset,
      mask: opts.mask,
      bin: getBinary(data, opts.length),
      compressed: opts.compressed,
      length: opts.length,
      opcode: opts.opcode
    });
  };
  reqReceiver.onerror = function(err) {
    proxy.emit('frame', {
      reqId: reqId,
      frameId: getFrameId(),
      isClient: true,
      err: err.stack,
      bin: ''
    });
  };
  reqTrans._transform = function(chunk, encoding, cb) {
    hasEvent && proxy.emit('wsRequest', url);
    handleFrame(reqReceiver, res, sendStatus, chunk, cb);
  };
  req.pipe(reqTrans).pipe(res);

  var resTrans = new PassThrough();
  resTrans.on('error', function(err) {
    req.emit('error', err);
  });
  var resReceiver =  wsParser.getReceiver(res, true);
  resReceiver.onData = function(data, opts) {
    proxy.emit('frame', {
      charset: charset,
      reqId: reqId,
      frameId: getFrameId(),
      bin: getBinary(data, opts.length),
      mask: opts.mask,
      compressed: opts.compressed,
      length: opts.length,
      opcode: opts.opcode
    });
  };
  resReceiver.onerror = function(err) {
    proxy.emit('frame', {
      reqId: reqId,
      frameId: getFrameId(),
      err: err.stack,
      bin: ''
    });
  };
  resTrans._transform = function(chunk, encoding, cb) {
    hasEvent && proxy.emit('wsRequest', url);
    handleFrame(resReceiver, req, receiveStatus, chunk, cb);
  };
  res.pipe(resTrans).pipe(req);
  handleClose(req, res, reqReceiver, resReceiver);
};

exports.abort = function(reqId) {
  var ctx = conns[reqId];
  if (!ctx) {
    return;
  }
  delete conns[reqId];
  ctx.req.destroy();
  ctx.res.destroy();
};

exports.getStatus = function(reqId) {
  var ctx = reqId && conns[reqId];
  if (!ctx) {
    return;
  }
  return {
    sendStatus: ctx.sendStatus,
    receiveStatus: ctx.receiveStatus
  };
};

exports.changeStatus = function(data) {
  var ctx = conns[data.reqId];
  if (!ctx) {
    return;
  }
  if (data.sendStatus >= 0) {
    ctx.setSendStatus(data.sendStatus);
  } else {
    ctx.setReceiveStatus(data.receiveStatus);
  }
  return true;
};

function getBuffer(data, charset) {
  if (data.base64) {
    try {
      return Buffer.from(data.base64, 'base64');
    } catch(e) {}
  } else if (data.text) {
    return util.toBuffer(data.text, charset);
  }
}

exports.sendData = function(data) {
  var ctx = conns[data.reqId];
  if (!ctx) {
    return;
  }
  var buf = getBuffer(data, ctx.charset);
  if (!buf) {
    return;
  }
  var isServer = data.target === 'server';
  var binary = data.type === 'bin';
  data = {
    binary: binary,
    opcode: binary ? 2 : 1,
    length: buf.length,
    data: buf
  };
  if (isServer) {
    data.mask = true;
    ctx.sendToServer(data);
  } else {
    ctx.sendToClient(data);
  }
};
<|endoftext|>