<commit_before>var https = require('https');
var proxy = require('proxy-agent');
var qs = require('querystring');
var q = require('q');
var fs = require('fs');
var Readable = require('stream').Readable;
var FormData = require('form-data');
var Attachment = require('./attachment');

var debug = require('debug')('mailgun-js');

var noop = function () {
};

function Request(options) {
  this.host = 'api.mailgun.net';
  this.endpoint = '/v2';
  this.auth = options.auth;
  this.proxy = options.proxy;
}

Request.prototype.request = function (method, resource, data, fn) {
  this.deferred = q.defer();

  if (typeof data === 'function' && !fn) {
    fn = data;
    data = {};
  }

  if (!fn) fn = noop;

  this.callback = fn;

  var path = ''.concat(this.endpoint, resource);

  var params = this.prepareData(data);

  this.payload = '';

  var isMIME = path.indexOf('/messages.mime') >= 0;

  this.headers = {};
  if (method === 'GET' || method === 'DELETE') {
    this.payload = qs.stringify(params);
    if (this.payload) path = path.concat('?', this.payload);
  }
  else {
    this.headers['Content-Type'] = isMIME ? 'multipart/form-data' : 'application/x-www-form-urlencoded';

    if (params && (params.attachment || params.inline || (isMIME && params.message))) {
      this.prepareFormData(params);
    }
    else {
      this.payload = qs.stringify(params);
      var length = this.payload ? this.payload.length : 0;
      this.headers['Content-Length'] = length;
    }
  }

  // check for MIME is true in case of messages GET
  if (method === 'GET' &&
    path.indexOf('/messages') >= 0 &&
    params && params.MIME === true) {
    this.headers['Accept'] = 'message/rfc2822'
  }

  debug('%s %s', method, path);

  var opts = {
    hostname: this.host,
    path: path,
    method: method,
    headers: this.headers,
    auth: this.auth,
    agent: this.proxy ? proxy(this.proxy, true) : false
  };

  this.performRequest(opts);

  return this.deferred.promise;
};

function isSpecialParam(paramKey) {
  var key = paramKey.toLowerCase();
  return ((key === 'vars' || key === 'members' || key === 'recipient-variables')
  || (key.indexOf('v:') === 0));
}

Request.prototype.prepareData = function (data) {
  var params = {};

  for (var key in data) {
    if (isSpecialParam(key) && (typeof data[key] === 'object')) {
      params[key] = JSON.stringify(data[key]);
    }
    else {
      params[key] = data[key];
    }
  }

  return params;
};

Request.prototype.prepareFormData = function (data) {
  this.form = new FormData();
  var self = this;

  for (var key in data) {
    var obj = data[key];
    if (key === 'attachment' || key === 'inline') {
      if (Array.isArray(obj)) {
        for (var i = 0; i < obj.length; i++) {
          this.handleAttachmentObject(key, obj[i]);
        }
      }
      else {
        this.handleAttachmentObject(key, obj);
      }
    }
    else if (key === 'message') {
      this.handleMimeObject(key, obj);
    }
    else if (Array.isArray(obj)) {
      obj.forEach(function (element) {
        self.form.append(key, element);
      });
    }
    else {
      this.form.append(key, obj);
    }
  }

  this.headers = this.form.getHeaders();
};

Request.prototype.handleMimeObject = function (key, obj) {
  var self = this;
  if (typeof obj === 'string') {
    if (fs.existsSync(obj) && fs.statSync(obj).isFile()) {
      self.form.append('message', fs.createReadStream(obj));
    }
    else {
      self.form.append('message', new Buffer(obj), {
        filename: 'message.mime',
        contentType: 'message/rfc822',
        knownLength: obj.length
      });
    }
  }
  else if (obj instanceof Readable) {
    self.form.append('message', obj)
  }
};

Request.prototype.handleAttachmentObject = function (key, obj) {
  if (!this.form) this.form = new FormData();

  if (Buffer.isBuffer(obj)) {
    debug('appending buffer to form data. key: %s', key);
    this.form.append(key, obj, {filename: 'file'});
  }
  else if (typeof obj === 'string') {
    debug('appending stream to form data. key: %s obj: %s', key, obj);
    this.form.append(key, fs.createReadStream(obj));
  }
  else if ((typeof obj === 'object') && (obj instanceof Attachment)) {
    var attachmentType = obj.getType();
    if (attachmentType === 'path') {
      debug('appending attachment stream to form data. key: %s data: %s filename: %s', key, obj.data, obj.filename);
      this.form.append(key, fs.createReadStream(obj.data), {filename: obj.filename || 'file'});
    }
    else if (attachmentType === 'buffer') {
      debug('appending attachment buffer to form data. key: %s filename: %s', key, obj.filename);
      this.form.append(key, obj.data, {filename: obj.filename || 'file'});
    }
    else if (attachmentType === 'stream') {
      if (obj.knownLength && obj.contentType) {
        debug('appending attachment buffer to form data. key: %s filename: %s', key, obj.filename);

        this.form.append(key, obj.data, {
          filename: obj.filename || 'file',
          contentType: obj.contentType,
          knownLength: obj.knownLength
        });
      }
      else {
        debug('missing content type or length for attachment stream. key: %s', key);
      }
    }
  }
  else {
    debug('unknown attachment type. key: %s', key);
  }
};

Request.prototype.handleResponse = function (res) {
  var self = this;
  var chunks = '';
  var error;

  res.on('data', function (chunk) {
    chunks += chunk;
  });

  res.on('error', function (err) {
    error = err;
  });

  res.on('end', function () {
    var body;

    debug('response status code: %s content type: %s', res.statusCode, res.headers['content-type']);

    // FIXME: An ugly hack to overcome invalid response type in mailgun api (see http://bit.ly/1eF30fU).
    // We skip content-type validation for 'campaings' endpoint assuming it is JSON.
    var skipContentTypeCheck = res.req && res.req.path && res.req.path.match(/\/campaigns/);
    if (!error && (skipContentTypeCheck || (res.headers['content-type'].indexOf('application/json') >= 0))) {
      try {
        body = JSON.parse(chunks);
      }
      catch (e) {
        error = e;
      }
    }

    if (!error && res.statusCode !== 200) {
      var msg = body ? body.message || body.response : body || chunks;
      error = new Error(msg);
      error.statusCode = res.statusCode;
    }

    if (error) {
      self.deferred.reject(error);
    }
    else {
      self.deferred.resolve(body);
    }

    return self.callback(error, body);
  });
};

Request.prototype.performRequest = function (options) {
  var self = this;
  var method = options.method;

  if (this.form && (method === 'POST' || method === 'PUT' || method === 'PATCH')) {
    options.port = 443;
    options.protocol = 'https:';

    this.form.submit(options, function (err, res) {
      if (err) {
        self.deferred.reject(err);
        return self.callback(err);
      }

      return self.handleResponse(res);
    });
  }
  else {
    var req = https.request(options, function (res) {
      return self.handleResponse(res);
    });

    req.on('error', function (e) {
      self.deferred.reject(e);
      return self.callback(e);
    });

    if (this.payload && (method === 'POST' || method === 'PUT' || method === 'PATCH')) {
      req.write(this.payload);
    }

    req.end();
  }
};

module.exports = Request;<commit_msg>Update request.js to use mailgun API v3<commit_after>var https = require('https');
var proxy = require('proxy-agent');
var qs = require('querystring');
var q = require('q');
var fs = require('fs');
var Readable = require('stream').Readable;
var FormData = require('form-data');
var Attachment = require('./attachment');

var debug = require('debug')('mailgun-js');

var noop = function () {
};

function Request(options) {
  this.host = 'api.mailgun.net';
  this.endpoint = '/v3';
  this.auth = options.auth;
  this.proxy = options.proxy;
}

Request.prototype.request = function (method, resource, data, fn) {
  this.deferred = q.defer();

  if (typeof data === 'function' && !fn) {
    fn = data;
    data = {};
  }

  if (!fn) fn = noop;

  this.callback = fn;

  var path = ''.concat(this.endpoint, resource);

  var params = this.prepareData(data);

  this.payload = '';

  var isMIME = path.indexOf('/messages.mime') >= 0;

  this.headers = {};
  if (method === 'GET' || method === 'DELETE') {
    this.payload = qs.stringify(params);
    if (this.payload) path = path.concat('?', this.payload);
  }
  else {
    this.headers['Content-Type'] = isMIME ? 'multipart/form-data' : 'application/x-www-form-urlencoded';

    if (params && (params.attachment || params.inline || (isMIME && params.message))) {
      this.prepareFormData(params);
    }
    else {
      this.payload = qs.stringify(params);
      var length = this.payload ? this.payload.length : 0;
      this.headers['Content-Length'] = length;
    }
  }

  // check for MIME is true in case of messages GET
  if (method === 'GET' &&
    path.indexOf('/messages') >= 0 &&
    params && params.MIME === true) {
    this.headers['Accept'] = 'message/rfc2822'
  }

  debug('%s %s', method, path);

  var opts = {
    hostname: this.host,
    path: path,
    method: method,
    headers: this.headers,
    auth: this.auth,
    agent: this.proxy ? proxy(this.proxy, true) : false
  };

  this.performRequest(opts);

  return this.deferred.promise;
};

function isSpecialParam(paramKey) {
  var key = paramKey.toLowerCase();
  return ((key === 'vars' || key === 'members' || key === 'recipient-variables')
  || (key.indexOf('v:') === 0));
}

Request.prototype.prepareData = function (data) {
  var params = {};

  for (var key in data) {
    if (isSpecialParam(key) && (typeof data[key] === 'object')) {
      params[key] = JSON.stringify(data[key]);
    }
    else {
      params[key] = data[key];
    }
  }

  return params;
};

Request.prototype.prepareFormData = function (data) {
  this.form = new FormData();
  var self = this;

  for (var key in data) {
    var obj = data[key];
    if (key === 'attachment' || key === 'inline') {
      if (Array.isArray(obj)) {
        for (var i = 0; i < obj.length; i++) {
          this.handleAttachmentObject(key, obj[i]);
        }
      }
      else {
        this.handleAttachmentObject(key, obj);
      }
    }
    else if (key === 'message') {
      this.handleMimeObject(key, obj);
    }
    else if (Array.isArray(obj)) {
      obj.forEach(function (element) {
        self.form.append(key, element);
      });
    }
    else {
      this.form.append(key, obj);
    }
  }

  this.headers = this.form.getHeaders();
};

Request.prototype.handleMimeObject = function (key, obj) {
  var self = this;
  if (typeof obj === 'string') {
    if (fs.existsSync(obj) && fs.statSync(obj).isFile()) {
      self.form.append('message', fs.createReadStream(obj));
    }
    else {
      self.form.append('message', new Buffer(obj), {
        filename: 'message.mime',
        contentType: 'message/rfc822',
        knownLength: obj.length
      });
    }
  }
  else if (obj instanceof Readable) {
    self.form.append('message', obj)
  }
};

Request.prototype.handleAttachmentObject = function (key, obj) {
  if (!this.form) this.form = new FormData();

  if (Buffer.isBuffer(obj)) {
    debug('appending buffer to form data. key: %s', key);
    this.form.append(key, obj, {filename: 'file'});
  }
  else if (typeof obj === 'string') {
    debug('appending stream to form data. key: %s obj: %s', key, obj);
    this.form.append(key, fs.createReadStream(obj));
  }
  else if ((typeof obj === 'object') && (obj instanceof Attachment)) {
    var attachmentType = obj.getType();
    if (attachmentType === 'path') {
      debug('appending attachment stream to form data. key: %s data: %s filename: %s', key, obj.data, obj.filename);
      this.form.append(key, fs.createReadStream(obj.data), {filename: obj.filename || 'file'});
    }
    else if (attachmentType === 'buffer') {
      debug('appending attachment buffer to form data. key: %s filename: %s', key, obj.filename);
      this.form.append(key, obj.data, {filename: obj.filename || 'file'});
    }
    else if (attachmentType === 'stream') {
      if (obj.knownLength && obj.contentType) {
        debug('appending attachment buffer to form data. key: %s filename: %s', key, obj.filename);

        this.form.append(key, obj.data, {
          filename: obj.filename || 'file',
          contentType: obj.contentType,
          knownLength: obj.knownLength
        });
      }
      else {
        debug('missing content type or length for attachment stream. key: %s', key);
      }
    }
  }
  else {
    debug('unknown attachment type. key: %s', key);
  }
};

Request.prototype.handleResponse = function (res) {
  var self = this;
  var chunks = '';
  var error;

  res.on('data', function (chunk) {
    chunks += chunk;
  });

  res.on('error', function (err) {
    error = err;
  });

  res.on('end', function () {
    var body;

    debug('response status code: %s content type: %s', res.statusCode, res.headers['content-type']);

    // FIXME: An ugly hack to overcome invalid response type in mailgun api (see http://bit.ly/1eF30fU).
    // We skip content-type validation for 'campaings' endpoint assuming it is JSON.
    var skipContentTypeCheck = res.req && res.req.path && res.req.path.match(/\/campaigns/);
    if (!error && (skipContentTypeCheck || (res.headers['content-type'].indexOf('application/json') >= 0))) {
      try {
        body = JSON.parse(chunks);
      }
      catch (e) {
        error = e;
      }
    }

    if (!error && res.statusCode !== 200) {
      var msg = body ? body.message || body.response : body || chunks;
      error = new Error(msg);
      error.statusCode = res.statusCode;
    }

    if (error) {
      self.deferred.reject(error);
    }
    else {
      self.deferred.resolve(body);
    }

    return self.callback(error, body);
  });
};

Request.prototype.performRequest = function (options) {
  var self = this;
  var method = options.method;

  if (this.form && (method === 'POST' || method === 'PUT' || method === 'PATCH')) {
    options.port = 443;
    options.protocol = 'https:';

    this.form.submit(options, function (err, res) {
      if (err) {
        self.deferred.reject(err);
        return self.callback(err);
      }

      return self.handleResponse(res);
    });
  }
  else {
    var req = https.request(options, function (res) {
      return self.handleResponse(res);
    });

    req.on('error', function (e) {
      self.deferred.reject(e);
      return self.callback(e);
    });

    if (this.payload && (method === 'POST' || method === 'PUT' || method === 'PATCH')) {
      req.write(this.payload);
    }

    req.end();
  }
};

module.exports = Request;
<|endoftext|>


<commit_before>// Load modules

var Url = require('url');
var Async = require('async');
var Boom = require('boom');
var Utils = require('./utils');
var Response = require('./response');
var Ext = require('./ext');

// Declare internals

var internals = {};

exports = module.exports = internals.Request = function (server, req, res, options) {

    var self = this;

    // Take measurement as soon as possible

    this._bench = new Utils.Bench();
    var now = Date.now();

    // Public members

    this.server = server;
    this.hapi = require('./');

    this.url = null;
    this.query = null;
    this.path = null;
    this.method = null;
    this.mime = null;                       // Set if payload is parsed

    this.setUrl = this._setUrl;             // Decoration removed after 'onRequest'
    this.setMethod = this._setMethod;

    this.setUrl(req.url);                   // Sets: this.url, this.path, this.query
    this.setMethod(req.method);             // Sets: this.method
    this.headers = req.headers;

    this.id = now + '-' + process.pid + '-' + Math.floor(Math.random() * 0x10000);

    this.app = {};                          // Place for application-specific state without conflicts with hapi, should not be used by plugins
    this.plugins = {};                      // Place for plugins to store state without conflicts with hapi, should be namespaced using plugin name

    this._route = this.server._router.notfound;             // Used prior to routing (only settings are used, not the handler)
    this.route = this._route.settings;

    this.auth = {
        isAuthenticated: false,
        credentials: null,                  // Special keys: 'app', 'user', 'scope', 'tos'
        artifacts: null,                    // Scheme-specific artifacts
        session: null                       // Used by cookie auth { set(), clear() }
    };

    this.session = null;                    // Special key reserved for plugins implementing session support

    this.pre = {};                          // Pre raw values
    this.responses = {};                    // Pre response values

    this.info = {
        received: now,
        remoteAddress: (req.connection && req.connection.remoteAddress) || '',
        remotePort: (req.connection && req.connection.remotePort) || '',
        referrer: req.headers.referrer || req.headers.referer || '',
        host: req.headers.host ? req.headers.host.replace(/\s/g, '') : ''
    };

    // Apply options

    if (options.credentials) {
        this.auth.credentials = options.credentials;
    }

    // Defined elsewhere:

    this.params = null;
    this.rawPayload = null;
    this.payload = null;
    this.state = null;
    this.jsonp = null;
    this.response = null;       // this.response()

    // Semi-public members

    this.raw = {
        req: req,
        res: res
    };

    this.setState = this._setState;                 // Remove once replied
    this.clearState = this._clearState;             // Remove once replied
    this.tail = this.addTail = this._addTail;       // Removed once wagging

    // Private members

    this._states = {};                  // Appended to response states when setting response headers
    this._logger = [];

    this._response = null;
    this._isReplied = false;

    this._tails = {};                   // tail id -> name (tracks pending tails)
    this._tailIds = 0;                  // Used to generate a unique tail id
    this._isWagging = false;            // true when request completed and only waiting on tails to complete

    this._paramsArray = [];             // Array of path parameters in path order

    this._pluginEnv = undefined;        // Set to the plugin env when ext methods are invoked

    // Set socket timeout

    if (req.socket &&
        server.settings.timeout.socket !== undefined) {

        req.socket.setTimeout(server.settings.timeout.socket || 0);
    }

    // Listen to request errors

    this._onClose = function () {

        self.log(['hapi', 'request', 'error', 'closed']);
        self._cleanup();
    };

    this.raw.req.once('close', this._onClose);

    this._onError = function (err) {

        self.log(['hapi', 'request'], err);
        self._cleanup();
    };

    this.raw.req.once('error', this._onError);

    this._onAborted = function () {

        self.log(['hapi', 'request', 'aborted']);
        self._cleanup();
    };

    this.raw.req.once('aborted', this._onAborted);

    // Log request

    var about = {
        id: this.id,
        method: this.method,
        url: this.url.href,
        agent: this.raw.req.headers['user-agent']
    };

    this.log(['hapi', 'received'], about, now);     // Must be last for object to be fully constructed
};

internals.Request.prototype._setUrl = function (url) {

    this.url = Url.parse(url, true);
    this.query = this.url.query || {};
    this.path = this.url.pathname;          // pathname excludes query

    if (this.path &&
        this.server.settings.router.normalizeRequestPath) {

        // Uppercase %encoded values

        var uppercase = this.path.replace(/%[0-9a-fA-F][0-9a-fA-F]/g, function (encoded) {

            return encoded.toUpperCase();
        });

        // Decode non-reserved path characters: a-z A-Z 0-9 _!$&'()*+,;=:@-.~
        // ! (%21) $ (%24) & (%26) ' (%27) ( (%28) ) (%29) * (%2A) + (%2B) , (%2C) - (%2D) . (%2E)
        // 0-9 (%30-39) : (%3A) ; (%3B) = (%3D)
        // @ (%40) A-Z (%41-5A) _ (%5F) a-z (%61-7A) ~ (%7E)

        var decoded = uppercase.replace(/%(?:2[146-9A-E]|3[\dABD]|4[\dA-F]|5[\dAF]|6[1-9A-F]|7[\dAE])/g, function (encoded) {

            return String.fromCharCode(parseInt(encoded.substring(1), 16));
        });

        this.path = decoded;
    }
};

internals.Request.prototype._setMethod = function (method) {

    if (method) {
        this.method = method.toLowerCase();
    }
};

internals.Request.prototype.log = function (tags, data, timestamp) {

    tags = (Array.isArray(tags) ? tags : [tags]);

    // Prepare log item

    var now = (timestamp ? (timestamp instanceof Date ? timestamp.getTime() : timestamp) : Date.now());
    var item = {
        request: this.id,
        timestamp: now,
        tags: tags
    };

    var tagsMap = Utils.mapToObject(item.tags);

    if (data) {
        if (data instanceof Error) {
            item.tags = tags.concat('error');
            tagsMap.error = true;
            item.data = (data.isBoom ? data.decorations() : {});
            item.data.message = data.message;
            if (tagsMap.uncaught) {
                item.data.trace = data.stack;
            }
        }
        else {
            item.data = data;
        }
    }

    // Add to request array

    this._logger.push(item);
    this.server.emit('request', this, item, tagsMap);

    if (this.server.settings.debug &&
        this.server.settings.debug.request &&
        Utils.intersect(tagsMap, this.server.settings.debug.request, true)) {

        console.error('Debug:', item.tags.join(', '), data ? (data.stack || data) : '');
    }
};

internals.Request.prototype.getLog = function (tags) {

    tags = [].concat(tags || []);
    if (!tags.length) {
        return this._logger;
    }

    var filter = Utils.mapToObject(tags);
    var result = [];

    for (var i = 0, il = this._logger.length; i < il; ++i) {
        var event = this._logger[i];
        for (var t = 0, tl = event.tags.length; t < tl; ++t) {
            var tag = event.tags[t];
            if (filter[tag]) {
                result.push(event);
            }
        }
    }

    return result;
};

internals.Request.prototype._execute = function () {

    var self = this;

    // Execute onRequest extensions (can change request method and url)

    this.server._ext.invoke(this, 'onRequest', function (err) {

        // Undecorate request

        self.setUrl = undefined;
        self.setMethod = undefined;

        if (err) {
            self._reply(err);
            return;
        }

        if (!self.path || self.path[0] !== '/') {
            self._reply(Boom.badRequest('Invalid path'));
            return;
        }

        // Lookup route

        self._route = self.server._router.route(self);
        self.route = self._route.settings;

        // Setup timer

        var serverTimeout = self.server.settings.timeout.server;
        if (serverTimeout) {
            serverTimeout -= self._bench.elapsed();                 // Calculate the timeout from when the request was constructed
            var timeoutReply = function () {

                self._reply(Boom.serverTimeout());
            };

            if (serverTimeout <= 0) {
                return timeoutReply();
            }

            self._serverTimeoutId = setTimeout(timeoutReply, serverTimeout);
        }

        Async.forEachSeries(self._route.cycle, function (func, next) {

            if (self._isReplied) {
                self.log(['hapi', 'server', 'timeout']);
                return next(true);                                  // Argument is ignored but aborts the series
            }

            if (typeof func === 'string') {
                self.server._ext.invoke(self, func, next);
                return;
            }

            func(self, next);

        },
        function (err) {

            self._reply(err);
        });
    });
};

internals.Request.prototype._reply = function (exit) {

    var self = this;

    if (this._isReplied) {                                      // Prevent any future responses to this request
        return;
    }

    this._isReplied = true;

    clearTimeout(this._serverTimeoutId);

    self.setState = undefined;
    self.clearState = undefined;

    var process = function () {

        if (self._response &&
            self._response.variety === 'closed') {

            self.raw.res.end();                                 // End the response in case it wasn't already closed
            return Utils.nextTick(finalize)();
        }

        if (exit) {
            override(exit);
        }

        Response._prepare(self._response, self, function (result) {

            self._response = result;

            self.server._ext.invoke(self, 'onPreResponse', function (err) {

                self.response = undefined;

                if (!err) {                                             // err can be valid response override
                    return Response._respond(self._response, self, finalize);
                }

                override(err);
                Response._prepare(self._response, self, function (result) {

                    self._response = result;
                    Response._respond(self._response, self, finalize);
                });
            });
        });
    };

    var override = function (value) {

        if (self._response &&
            !self._response.isBoom &&
            !self._response.varieties.error) {

            // Got error after valid result was already set

            self._route.cache.drop(self.url.path, function (err) {

                self.log(['hapi', 'cache', 'drop'], err);
            });
        }

        self._setResponse(Response._generate(value, self));
    };

    var finalize = function () {

        self.server._dtrace.report('request.finalize', self._response);
        if (self._response &&
            ((self._response.isBoom && self._response.response.code === 500) ||
             (self._response.varieties && self._response.varieties.error && self._response._code === 500))) {

            var error = (self._response.isBoom ? self._response : self._response._err);
            self.server.emit('internalError', self, error);
            self.log(error.isDeveloperError ? ['hapi', 'internal', 'implementation'] : ['hapi', 'internal'], error);
        }

        self.server.emit('response', self);

        self._isWagging = true;
        self.addTail = undefined;
        self.tail = undefined;

        if (Object.keys(self._tails).length === 0) {
            self.server.emit('tail', self);
        }

        self._cleanup();
    };

    process();
};

internals.Request.prototype._cleanup = function () {

    this.raw.req.removeListener('close', this._onClose);
    this.raw.req.removeListener('error', this._onError);
    this.raw.req.removeListener('aborted', this._onAborted);
};

internals.Request.parseJSONP = function (request, next) {

    var jsonp = request.query[request.route.jsonp];
    if (jsonp) {
        if (!jsonp.match(/^[\w\$\[\]\.]+$/)) {
            return next(Boom.badRequest('Invalid JSONP parameter value'));
        }

        request.jsonp = jsonp;
        delete request.query[request.route.jsonp];
    }

    return next();
};

internals.Request.prototype._setResponse = function (response) {

    var self = this;

    this._response = response;
    this.response = this.response || function () {

        return self._response;
    };
};

internals.Request.prototype._addTail = function (name) {

    var self = this;

    name = name || 'unknown';
    var tailId = this._tailIds++;
    this._tails[tailId] = name;
    this.log(['hapi', 'tail', 'add'], { name: name, id: tailId });

    var drop = function () {

        if (!self._tails[tailId]) {
            self.log(['hapi', 'tail', 'remove', 'error'], { name: name, id: tailId });             // Already removed
            return;
        }

        delete self._tails[tailId];

        if (Object.keys(self._tails).length === 0 &&
            self._isWagging) {

            self.log(['hapi', 'tail', 'remove', 'last'], { name: name, id: tailId });
            self.server.emit('tail', self);
        }
        else {
            self.log(['hapi', 'tail', 'remove'], { name: name, id: tailId });
        }
    };

    return drop;
};

internals.Request.prototype._setState = function (name, value, options) {

    if (this._response &&
        this._response.state) {

        this._response.state(name, value, options);
    }
    else {
        Response.Generic.prototype.state.call(this, name, value, options);
    }
};

internals.Request.prototype._clearState = function (name) {

    if (this._response &&
        this._response.unstate) {

        this._response.unstate(name);
    }
    else {
        Response.Generic.prototype.unstate.call(this, name);
    }
};

internals.Request.prototype.generateView = function (template, context, options) {

    var viewsManager = (this._pluginEnv ? this._pluginEnv.views : this._route.env.views) || this.server._views;
    Utils.assert(viewsManager, 'Cannot generate view without a views manager initialized');
    return new Response.View(viewsManager, template, context, options);
};

<commit_msg>log raw.req events. Closes #1229<commit_after>// Load modules

var Url = require('url');
var Async = require('async');
var Boom = require('boom');
var Utils = require('./utils');
var Response = require('./response');
var Ext = require('./ext');

// Declare internals

var internals = {};

exports = module.exports = internals.Request = function (server, req, res, options) {

    var self = this;

    // Take measurement as soon as possible

    this._bench = new Utils.Bench();
    var now = Date.now();

    // Public members

    this.server = server;
    this.hapi = require('./');

    this.url = null;
    this.query = null;
    this.path = null;
    this.method = null;
    this.mime = null;                       // Set if payload is parsed

    this.setUrl = this._setUrl;             // Decoration removed after 'onRequest'
    this.setMethod = this._setMethod;

    this.setUrl(req.url);                   // Sets: this.url, this.path, this.query
    this.setMethod(req.method);             // Sets: this.method
    this.headers = req.headers;

    this.id = now + '-' + process.pid + '-' + Math.floor(Math.random() * 0x10000);

    this.app = {};                          // Place for application-specific state without conflicts with hapi, should not be used by plugins
    this.plugins = {};                      // Place for plugins to store state without conflicts with hapi, should be namespaced using plugin name

    this._route = this.server._router.notfound;             // Used prior to routing (only settings are used, not the handler)
    this.route = this._route.settings;

    this.auth = {
        isAuthenticated: false,
        credentials: null,                  // Special keys: 'app', 'user', 'scope', 'tos'
        artifacts: null,                    // Scheme-specific artifacts
        session: null                       // Used by cookie auth { set(), clear() }
    };

    this.session = null;                    // Special key reserved for plugins implementing session support

    this.pre = {};                          // Pre raw values
    this.responses = {};                    // Pre response values

    this.info = {
        received: now,
        remoteAddress: (req.connection && req.connection.remoteAddress) || '',
        remotePort: (req.connection && req.connection.remotePort) || '',
        referrer: req.headers.referrer || req.headers.referer || '',
        host: req.headers.host ? req.headers.host.replace(/\s/g, '') : ''
    };

    // Apply options

    if (options.credentials) {
        this.auth.credentials = options.credentials;
    }

    // Defined elsewhere:

    this.params = null;
    this.rawPayload = null;
    this.payload = null;
    this.state = null;
    this.jsonp = null;
    this.response = null;       // this.response()

    // Semi-public members

    this.raw = {
        req: req,
        res: res
    };

    this.setState = this._setState;                 // Remove once replied
    this.clearState = this._clearState;             // Remove once replied
    this.tail = this.addTail = this._addTail;       // Removed once wagging

    // Private members

    this._states = {};                  // Appended to response states when setting response headers
    this._logger = [];

    this._response = null;
    this._isReplied = false;

    this._tails = {};                   // tail id -> name (tracks pending tails)
    this._tailIds = 0;                  // Used to generate a unique tail id
    this._isWagging = false;            // true when request completed and only waiting on tails to complete

    this._paramsArray = [];             // Array of path parameters in path order

    this._pluginEnv = undefined;        // Set to the plugin env when ext methods are invoked

    // Set socket timeout

    if (req.socket &&
        server.settings.timeout.socket !== undefined) {

        req.socket.setTimeout(server.settings.timeout.socket || 0);
    }

    // Listen to request errors

    this._onClose = function () {

        self.log(['hapi', 'request', 'error', 'closed']);
    };

    this.raw.req.once('close', this._onClose);

    this._onError = function (err) {

        self.log(['hapi', 'request'], err);
    };

    this.raw.req.once('error', this._onError);

    this._onAborted = function () {

        self.log(['hapi', 'request', 'aborted']);
    };

    this.raw.req.once('aborted', this._onAborted);

    // Log request

    var about = {
        id: this.id,
        method: this.method,
        url: this.url.href,
        agent: this.raw.req.headers['user-agent']
    };

    this.log(['hapi', 'received'], about, now);     // Must be last for object to be fully constructed
};

internals.Request.prototype._setUrl = function (url) {

    this.url = Url.parse(url, true);
    this.query = this.url.query || {};
    this.path = this.url.pathname;          // pathname excludes query

    if (this.path &&
        this.server.settings.router.normalizeRequestPath) {

        // Uppercase %encoded values

        var uppercase = this.path.replace(/%[0-9a-fA-F][0-9a-fA-F]/g, function (encoded) {

            return encoded.toUpperCase();
        });

        // Decode non-reserved path characters: a-z A-Z 0-9 _!$&'()*+,;=:@-.~
        // ! (%21) $ (%24) & (%26) ' (%27) ( (%28) ) (%29) * (%2A) + (%2B) , (%2C) - (%2D) . (%2E)
        // 0-9 (%30-39) : (%3A) ; (%3B) = (%3D)
        // @ (%40) A-Z (%41-5A) _ (%5F) a-z (%61-7A) ~ (%7E)

        var decoded = uppercase.replace(/%(?:2[146-9A-E]|3[\dABD]|4[\dA-F]|5[\dAF]|6[1-9A-F]|7[\dAE])/g, function (encoded) {

            return String.fromCharCode(parseInt(encoded.substring(1), 16));
        });

        this.path = decoded;
    }
};

internals.Request.prototype._setMethod = function (method) {

    if (method) {
        this.method = method.toLowerCase();
    }
};

internals.Request.prototype.log = function (tags, data, timestamp) {

    tags = (Array.isArray(tags) ? tags : [tags]);

    // Prepare log item

    var now = (timestamp ? (timestamp instanceof Date ? timestamp.getTime() : timestamp) : Date.now());
    var item = {
        request: this.id,
        timestamp: now,
        tags: tags
    };

    var tagsMap = Utils.mapToObject(item.tags);

    if (data) {
        if (data instanceof Error) {
            item.tags = tags.concat('error');
            tagsMap.error = true;
            item.data = (data.isBoom ? data.decorations() : {});
            item.data.message = data.message;
            if (tagsMap.uncaught) {
                item.data.trace = data.stack;
            }
        }
        else {
            item.data = data;
        }
    }

    // Add to request array

    this._logger.push(item);
    this.server.emit('request', this, item, tagsMap);

    if (this.server.settings.debug &&
        this.server.settings.debug.request &&
        Utils.intersect(tagsMap, this.server.settings.debug.request, true)) {

        console.error('Debug:', item.tags.join(', '), data ? (data.stack || data) : '');
    }
};

internals.Request.prototype.getLog = function (tags) {

    tags = [].concat(tags || []);
    if (!tags.length) {
        return this._logger;
    }

    var filter = Utils.mapToObject(tags);
    var result = [];

    for (var i = 0, il = this._logger.length; i < il; ++i) {
        var event = this._logger[i];
        for (var t = 0, tl = event.tags.length; t < tl; ++t) {
            var tag = event.tags[t];
            if (filter[tag]) {
                result.push(event);
            }
        }
    }

    return result;
};

internals.Request.prototype._execute = function () {

    var self = this;

    // Execute onRequest extensions (can change request method and url)

    this.server._ext.invoke(this, 'onRequest', function (err) {

        // Undecorate request

        self.setUrl = undefined;
        self.setMethod = undefined;

        if (err) {
            self._reply(err);
            return;
        }

        if (!self.path || self.path[0] !== '/') {
            self._reply(Boom.badRequest('Invalid path'));
            return;
        }

        // Lookup route

        self._route = self.server._router.route(self);
        self.route = self._route.settings;

        // Setup timer

        var serverTimeout = self.server.settings.timeout.server;
        if (serverTimeout) {
            serverTimeout -= self._bench.elapsed();                 // Calculate the timeout from when the request was constructed
            var timeoutReply = function () {

                self._reply(Boom.serverTimeout());
            };

            if (serverTimeout <= 0) {
                return timeoutReply();
            }

            self._serverTimeoutId = setTimeout(timeoutReply, serverTimeout);
        }

        Async.forEachSeries(self._route.cycle, function (func, next) {

            if (self._isReplied) {
                self.log(['hapi', 'server', 'timeout']);
                return next(true);                                  // Argument is ignored but aborts the series
            }

            if (typeof func === 'string') {
                self.server._ext.invoke(self, func, next);
                return;
            }

            func(self, next);

        },
        function (err) {

            self._reply(err);
        });
    });
};

internals.Request.prototype._reply = function (exit) {

    var self = this;

    if (this._isReplied) {                                      // Prevent any future responses to this request
        return;
    }

    this._isReplied = true;

    clearTimeout(this._serverTimeoutId);

    self.setState = undefined;
    self.clearState = undefined;

    var process = function () {

        if (self._response &&
            self._response.variety === 'closed') {

            self.raw.res.end();                                 // End the response in case it wasn't already closed
            return Utils.nextTick(finalize)();
        }

        if (exit) {
            override(exit);
        }

        Response._prepare(self._response, self, function (result) {

            self._response = result;

            self.server._ext.invoke(self, 'onPreResponse', function (err) {

                self.response = undefined;

                if (!err) {                                             // err can be valid response override
                    return Response._respond(self._response, self, finalize);
                }

                override(err);
                Response._prepare(self._response, self, function (result) {

                    self._response = result;
                    Response._respond(self._response, self, finalize);
                });
            });
        });
    };

    var override = function (value) {

        if (self._response &&
            !self._response.isBoom &&
            !self._response.varieties.error) {

            // Got error after valid result was already set

            self._route.cache.drop(self.url.path, function (err) {

                self.log(['hapi', 'cache', 'drop'], err);
            });
        }

        self._setResponse(Response._generate(value, self));
    };

    var finalize = function () {

        self.server._dtrace.report('request.finalize', self._response);
        if (self._response &&
            ((self._response.isBoom && self._response.response.code === 500) ||
             (self._response.varieties && self._response.varieties.error && self._response._code === 500))) {

            var error = (self._response.isBoom ? self._response : self._response._err);
            self.server.emit('internalError', self, error);
            self.log(error.isDeveloperError ? ['hapi', 'internal', 'implementation'] : ['hapi', 'internal'], error);
        }

        self.server.emit('response', self);

        self._isWagging = true;
        self.addTail = undefined;
        self.tail = undefined;

        if (Object.keys(self._tails).length === 0) {
            self.server.emit('tail', self);
        }

        self._cleanup();
    };

    process();
};

internals.Request.prototype._cleanup = function () {

    this.raw.req.removeListener('close', this._onClose);
    this.raw.req.removeListener('error', this._onError);
    this.raw.req.removeListener('aborted', this._onAborted);
};

internals.Request.parseJSONP = function (request, next) {

    var jsonp = request.query[request.route.jsonp];
    if (jsonp) {
        if (!jsonp.match(/^[\w\$\[\]\.]+$/)) {
            return next(Boom.badRequest('Invalid JSONP parameter value'));
        }

        request.jsonp = jsonp;
        delete request.query[request.route.jsonp];
    }

    return next();
};

internals.Request.prototype._setResponse = function (response) {

    var self = this;

    this._response = response;
    this.response = this.response || function () {

        return self._response;
    };
};

internals.Request.prototype._addTail = function (name) {

    var self = this;

    name = name || 'unknown';
    var tailId = this._tailIds++;
    this._tails[tailId] = name;
    this.log(['hapi', 'tail', 'add'], { name: name, id: tailId });

    var drop = function () {

        if (!self._tails[tailId]) {
            self.log(['hapi', 'tail', 'remove', 'error'], { name: name, id: tailId });             // Already removed
            return;
        }

        delete self._tails[tailId];

        if (Object.keys(self._tails).length === 0 &&
            self._isWagging) {

            self.log(['hapi', 'tail', 'remove', 'last'], { name: name, id: tailId });
            self.server.emit('tail', self);
        }
        else {
            self.log(['hapi', 'tail', 'remove'], { name: name, id: tailId });
        }
    };

    return drop;
};

internals.Request.prototype._setState = function (name, value, options) {

    if (this._response &&
        this._response.state) {

        this._response.state(name, value, options);
    }
    else {
        Response.Generic.prototype.state.call(this, name, value, options);
    }
};

internals.Request.prototype._clearState = function (name) {

    if (this._response &&
        this._response.unstate) {

        this._response.unstate(name);
    }
    else {
        Response.Generic.prototype.unstate.call(this, name);
    }
};

internals.Request.prototype.generateView = function (template, context, options) {

    var viewsManager = (this._pluginEnv ? this._pluginEnv.views : this._route.env.views) || this.server._views;
    Utils.assert(viewsManager, 'Cannot generate view without a views manager initialized');
    return new Response.View(viewsManager, template, context, options);
};

<|endoftext|>


<commit_before>/**
# Require
Copyright ©2016-2017 Tonio Loewald

Simple implementation of CommonJS *require*.

Usage:

    const module = require('path/to/module.js');

Relative paths are supported within a file, so, assuming we're inside
`path/to/module.js` and `another_module.js` is also in `path/to/`:

    const another_module = require('./another_module.js');

If you don't want synchronous requires (and, you don't):

    require.lazy('path/to/module.js').then(module => {
      // you're good to go!
    });

And finally, if you need to use some primordial library that wants
to be global:

    require.viaTag('path/to/babylon.min.js').then(() => {
      const engine = new BABYLON.Engine(canvas, true);
      ...
    });

## Quick and Dirty Async Delivery

Naive use of require will result in console spam about how
synchronous XHR requests are bad. Rather than using a compile phase
to avoid them, you can do this in the console:

    require.preloadData();

This will produce an array of data (in essence the inferred dependency
graph unspooled as "waves" of modules that can be loaded asynchronously)
which you can pass to:

    require.preload([ ... ]).then(() => {
      // write the code you would have written before,
      // everything will have loaded asynchronously!
    });

E.g. if module A requires modules B, and C, and module B requires modules
D, and E you might get something like this:

    [
      [C, D, E], // modules with no dependencies
      [B],       // B depends on D and E, already loaded
      [A]        // A relies on B, and C, already loaded
    ]

> ### Room for Improvement
>
> In the preceding example, module C did not need to load in the first
> wave, and could have been loaded in the second wave.
>
> `preload` loads each "wave" of dependencies as a set of promises and
> waits for the entire wave to load before proceeding. A finer-grained
> approach would make the tree of promises finer-grained (so a module
> with no dependencies that can be loaded early doesn't slow down
> modules waiting for other modules)
>
> Right now, nothing I'm working on has noticeable performance issues
> w.r.t. loading to optimizing this further is not a priority.

### That was boring, let's do this all automatically!

    require.autoPreload().then(() => {
      // write the code you would have written before,
      // everything will have loaded asynchronously after the first time!
    });

The first time this runs you'll see all the sync loading warnings, and then
it just goes away. Sometimes a code change will cause the preload data to
be automatically updated. Now, this isn't quite as good as working perfectly the
first time, which would require doing this on the server. It would be pretty
easy to integrate this with your devops workflow if it came to that.

    require.autoPreload(10000).then(...); // you can change the throttle delay

Note that the storing of preloadData is throttled because calculating dependencies
could get expensive for sufficiently complex projects. It defaults to 2000ms which
should be fine for anything but truly byzantine apps. It takes < 1ms on a reasonably
modern device for the b8r demo page, but if I recall correctly calculating the
dependencies is O(n^2).

### Compatibility Hack

It seems that some implementations of require pass module.exports with a truthy
value which the module overwrites. (Why?!?) Anyway, most libraries don't care
whether module.exports exists, but those that do tend to malfunction (set up
a window global) if they don't see something there. Since it seems that more
modules expect module.exports and fail than those that don't see it and fail,
require now passes an empty object.

But, for those modules that malfunction if module.exports is there, you can
suppress it by appending a tilde to your path. In other words:

    require('path/to/module.js'); // module will be {exports:{}}
    require('path/to/module.js~'); // the ~ will be stripped; module will be {}

This allows more libraries to be loaded unmodified without polluting global
namespace and is thus a Good Thing™.

Note that the tilde is supported by `require.lazy`, and the name is preserved
by `require.preloadData()`.
*/
/* jshint latedef:false */
/* global console */

(function(global) {
  'use strict';
  let modules = {};
  const dependency_map = {};
  const modules_loaded_synchronously = [];
  const require_utils = {};
  const _global_require = global.require;
  let autoPreload_enabled = 0; // also used to throttle saving of preload data
  const auto_preload_path = `_require_preloadData_${window.location.pathname}`;

  function define(module_name, source_code) {
    const path = module_name.split('/');
    path.pop();
    /* jshint evil:true */
    if (!modules[module_name]) {
      let module = module_name.substr(-1) === '~' ? {} : {exports:{}};
      let factory = new Function(
          'module', 'require', `${source_code}\n//# sourceURL=${module_name}`);
      /* jshint evil:false */
      const local_require = _relative(path, module_name);
      Object.assign(local_require, require_utils);
      factory(module, local_require);
      modules[module_name] = module;
    }
    return modules[module_name];
  }

  function _relative_path(path, module) {
    if (module.substr(0, 2) === './') {
      module = path.join('/') + module.substr(1);
    } else if (module.substr(0, 3) === '../') {
      const _path = path.slice(0);
      while (module.substr(0, 3) === '../') {
        _path.pop();
        module = module.substr(3);
      }
      module = _path.length ? _path.join('/') + '/' + module : module;
    }
    return module;
  }

  function _relative(path, module_name) {
    let path_string;
    if (typeof path === 'string') {
      path_string = path;
      path = path.split('/');
    } else if (Array.isArray(path)) {
      path_string = path.join('/');
    } else {
      console.error('_relative require needs a path!');
      debugger; // jshint ignore:line
    }

    const _r = (module) => {
      module = _relative_path(path, module);

      // map dependencies
      if (module_name) {
        if (!dependency_map[module_name]) {
          dependency_map[module_name] = [];
        }
        if (dependency_map[module_name].indexOf(module) === -1) {
          dependency_map[module_name].push(module);
        }
      }
      return _require(module);
    };

    _r._path = path_string;
    Object.assign(_r, require_utils);
    _r.lazy = module_name => _lazy(_relative_path(path, module_name));

    return _r;
  }

  function _require(module_name) {
    if (module_name.substr(0, 2) === './') {
      module_name = module_name.substr(2);
    }

    if (!modules[module_name]) {
      let request = new XMLHttpRequest();
      modules_loaded_synchronously.push(module_name);
      console.warn(module_name, 'was loaded synchronously');
      if (autoPreload_enabled) {
        _savePreloadData();
      }
      request.open('GET', module_name.substr(-1) === '~' ? module_name.slice(0,-1) : module_name, false);
      request.send(null);
      if (request.status === 200) {
        define(module_name, request.responseText);
      } else {
        console.error('could not load required module', module_name);
        return {};
      }
    }
    return modules[module_name].exports;
  }

  const _lazy_module_promises = {};
  function _lazy(module_name) {
    if (module_name.substr(0, 2) === './') {
      module_name = module_name.substr(2);
    }

    if (!_lazy_module_promises[module_name]) {
      _lazy_module_promises[module_name] = new Promise(function(resolve, reject) {
        if (!modules[module_name]) {
          let request = new XMLHttpRequest();
          request.open('GET', module_name.substr(-1) === '~' ? module_name.slice(0,-1) : module_name, true);
          request.onload = function(data) {
            if (request.readyState === 4) {
              if (request.status === 200) {
                resolve(define(module_name, request.responseText).exports);
              } else {
                reject(request, data);
              }
            }
          };
          request.onerror = function(data) {
            reject(request, data);
          };
          request.send(null);
        } else {
          setTimeout(() => resolve(modules[module_name].exports));
        }
      });
    }
    return _lazy_module_promises[module_name];
  }

  const scriptTags = {};
  const _viaTag = script_path => {
    if(!scriptTags[script_path]) {
      scriptTags[script_path] = new Promise((resolve) => {
        const script = document.createElement('script');
        script.setAttribute('src', script_path);
        script.addEventListener('load', resolve);
        document.body.appendChild(script);
      }).catch(() => console.error(`${script_path} failed to load`));
    }
    return scriptTags[script_path];
  };

  let savePreloadDataTimeout = 0;
  const _savePreloadData = () => {
    if (savePreloadDataTimeout) {
      clearTimeout(savePreloadDataTimeout);
    }
    savePreloadDataTimeout = setTimeout(() => {
      console.time('savePreloadData');
      localStorage.setItem(auto_preload_path, _preloadData(false));
      console.timeEnd('savePreloadData');
    }, autoPreload_enabled);
  };

  const _preloadData = () => {
    let available = [];
    let module_list = Object.keys(modules);
    const not_available = module => available.indexOf(module) === -1;

    // note that this is O(n^2) so if you have many, many modules it could get slow (but it should never be run by user)
    const no_deps = module => !dependency_map[module] || dependency_map[module].filter(not_available).length === 0;
    const waves = [];
    while(module_list.filter(not_available).length) {
      let wave = module_list.filter(not_available).filter(no_deps);
      waves.push(wave);
      if(wave.length === 0) {
        console.error('dependency deadlock', module_list);
        break;
      }
      available = available.concat(wave);
    }

    return JSON.stringify(waves, false, 2);
  };

  const _preload = waves => {
    function preload(waves) {
      if (waves.length) {
        const wave = waves.length ? waves.shift() : [];
        return Promise.all(wave.map(_lazy)).
               then(() => waves.length ? preload(waves) : null).
               catch(e => {
                  console.error('preload failed', e);
                  if (localStorage.getItem(auto_preload_path)) {
                    localStorage.removeItem(auto_preload_path);
                    window.location.reload();
                  }
               });
      }
    }
    return preload(waves);
  };

  const _autoPreload = (save_preload_data_throttle_ms) => {
    autoPreload_enabled = save_preload_data_throttle_ms || 2000;
    return new Promise(resolve => {
      const waves = localStorage.getItem(auto_preload_path);
      if (waves) {
        console.log('autoPreload data found');
        _preload(JSON.parse(waves)).then(resolve);
      } else {
        console.warn('no preload data found');
        resolve();
      }
    });
  };

  global.__d = define;

  Object.assign(require_utils, {
    report() {
      console.table(modules_loaded_synchronously);
    },
    data () {
      return {
        modules: Object.keys(modules),
        modules_loaded_synchronously,
        dependency_map
      };
    },
    isDefined (module_name) {
      return !!modules[module_name];
    },
    relative: _relative,
    preloadData: _preloadData,
    preload: _preload,
    autoPreload: _autoPreload,
    viaTag: _viaTag,
    electron: global.process && global.process.versions.electron,
    electron_remote: global.process && _global_require('electron').remote,
    globalRequire: _global_require,
    modules: () => modules,
  });

/*
  Electron exposes "module" global that confuses scripts imported via tag
*/
  if (global.module) {
    _require._global_module = global.module;
    delete global.module;
  }

  _require.lazy = _lazy;
  global.require = _require;
  Object.assign(global.require, require_utils);
}(this));
<commit_msg>new — require now differentiates between loading a file and evaluating it new — require can now preload all dependencies at once because above<commit_after>/**
# Require
Copyright ©2016-2017 Tonio Loewald

Simple implementation of CommonJS *require*.

Usage:

    const module = require('path/to/module.js');

Relative paths are supported within a file, so, assuming we're inside
`path/to/module.js` and `another_module.js` is also in `path/to/`:

    const another_module = require('./another_module.js');

If you don't want synchronous requires (and, you don't):

    require.lazy('path/to/module.js').then(module => {
      // you're good to go!
    });

And finally, if you need to use some primordial library that wants
to be global:

    require.viaTag('path/to/babylon.min.js').then(() => {
      const engine = new BABYLON.Engine(canvas, true);
      ...
    });

## Quick and Dirty Async Delivery

Naive use of require will result in console spam about how
synchronous XHR requests are bad. Rather than using a compile phase
to avoid them, you can do this in the console:

    require.preloadData();

This will produce an array of data (in essence the inferred dependency
graph unspooled as "waves" of modules that can be loaded asynchronously)
which you can pass to:

    require.preload([ ... ]).then(() => {
      // write the code you would have written before,
      // everything will have loaded asynchronously!
    });

E.g. if module A requires modules B, and C, and module B requires modules
D, and E you might get something like this:

    [
      [C, D, E], // modules with no dependencies
      [B],       // B depends on D and E, already loaded
      [A]        // A relies on B, and C, already loaded
    ]

> ### Room for Improvement
>
> In the preceding example, module C did not need to load in the first
> wave, and could have been loaded in the second wave.
>
> `preload` loads each "wave" of dependencies as a set of promises and
> waits for the entire wave to load before proceeding. A finer-grained
> approach would make the tree of promises finer-grained (so a module
> with no dependencies that can be loaded early doesn't slow down
> modules waiting for other modules)
>
> Right now, nothing I'm working on has noticeable performance issues
> w.r.t. loading to optimizing this further is not a priority.

### That was boring, let's do this all automatically!

    require.autoPreload().then(() => {
      // write the code you would have written before,
      // everything will have loaded asynchronously after the first time!
    });

The first time this runs you'll see all the sync loading warnings, and then
it just goes away. Sometimes a code change will cause the preload data to
be automatically updated. Now, this isn't quite as good as working perfectly the
first time, which would require doing this on the server. It would be pretty
easy to integrate this with your devops workflow if it came to that.

    require.autoPreload(10000).then(...); // you can change the throttle delay

Note that the storing of preloadData is throttled because calculating dependencies
could get expensive for sufficiently complex projects. It defaults to 2000ms which
should be fine for anything but truly byzantine apps. It takes < 1ms on a reasonably
modern device for the b8r demo page, but if I recall correctly calculating the
dependencies is O(n^2).

### Compatibility Hack

It seems that some implementations of require pass module.exports with a truthy
value which the module overwrites. (Why?!?) Anyway, most libraries don't care
whether module.exports exists, but those that do tend to malfunction (set up
a window global) if they don't see something there. Since it seems that more
modules expect module.exports and fail than those that don't see it and fail,
require now passes an empty object.

But, for those modules that malfunction if module.exports is there, you can
suppress it by appending a tilde to your path. In other words:

    require('path/to/module.js'); // module will be {exports:{}}
    require('path/to/module.js~'); // the ~ will be stripped; module will be {}

This allows more libraries to be loaded unmodified without polluting global
namespace and is thus a Good Thing™.

Note that the tilde is supported by `require.lazy`, and the name is preserved
by `require.preloadData()`.
*/
/* jshint latedef:false */
/* global console */

(function(global) {
  'use strict';
  const modules = {};
  const dependency_map = {};
  const modules_loaded_synchronously = [];
  const require_utils = {};
  const _global_require = global.require;
  let autoPreload_enabled = 0; // also used to throttle saving of preload data
  const auto_preload_path = `_require_preloadData_${window.location.pathname}`;

  function define(module_name, source_code) {
    const path = module_name.split('/');
    path.pop();
    /* jshint evil:true */
    if (! modules[module_name]) {
      let module = module_name.substr(-1) === '~' ? {} : {exports:{}};
      let factory = new Function(
          'module', 'require', `${source_code}\n//# sourceURL=${module_name}`);
      /* jshint evil:false */
      const local_require = _relative(path, module_name);
      Object.assign(local_require, require_utils);
      factory(module, local_require);
      modules[module_name] = module;
    }
    return modules[module_name];
  }

  function _relative_path(path, module) {
    if (module.substr(0, 2) === './') {
      module = path.join('/') + module.substr(1);
    } else if (module.substr(0, 3) === '../') {
      const _path = path.slice(0);
      while (module.substr(0, 3) === '../') {
        _path.pop();
        module = module.substr(3);
      }
      module = _path.length ? _path.join('/') + '/' + module : module;
    }
    // reduce paths, e.g. 'foo//bar//.//baz' => 'foo/bar/baz'
    return module.replace(/\/\//g, '/').replace(/\/\.\//g, '/');
  }

  function _relative(path, module_name) {
    let path_string;
    if (typeof path === 'string') {
      path_string = path;
      path = path.split('/');
    } else if (Array.isArray(path)) {
      path_string = path.join('/');
    } else {
      console.error('_relative require needs a path!');
      debugger; // jshint ignore:line
    }

    const _r = (module) => {
      module = _relative_path(path, module);

      // map dependencies
      if (module_name) {
        if (!dependency_map[module_name]) {
          dependency_map[module_name] = [];
        }
        if (dependency_map[module_name].indexOf(module) === -1) {
          dependency_map[module_name].push(module);
        }
      }
      return _require(module);
    };

    _r._path = path_string;
    Object.assign(_r, require_utils);
    _r.lazy = module_name => _lazy(_relative_path(path, module_name));

    return _r;
  }

  function _require(module_name) {
    if (module_name.substr(0, 2) === './') {
      module_name = module_name.substr(2);
    }

    if (! modules[module_name]) {
      if (! _files[module_name]) {
        let request = new XMLHttpRequest();
        modules_loaded_synchronously.push(module_name);
        console.warn(module_name, 'was loaded synchronously');
        if (autoPreload_enabled) {
          _savePreloadData();
        }
        request.open('GET', module_name.substr(-1) === '~' ? module_name.slice(0,-1) : module_name, false);
        request.send(null);
        if (request.status === 200) {
          define(module_name, request.responseText);
          _files[module_name] = true;
        } else {
          console.error('could not load required module', module_name);
          return {};
        }
      } else {
        define(module_name, _files[module_name]);
        _files[module_name] = true;
      }
    }

    return modules[module_name].exports;
  }

  const _file_promises = {};
  const _files = {};
  function _file(module_name) {
    if (module_name.substr(0, 2) === './') {
      module_name = module_name.substr(2);
    }

    if (!_file_promises[module_name]) {
      _file_promises[module_name] = new Promise(function(resolve, reject) {
      const request = new XMLHttpRequest();
        request.open('GET', module_name.substr(-1) === '~' ? module_name.slice(0,-1) : module_name, true);
        request.onload = function(data) {
          if (request.readyState === 4) {
            if (request.status === 200) {
              if (! modules[module_name]) {
                _files[module_name] = request.responseText;
              }
              resolve(request.responseText);
            } else {
              reject(request, data);
            }
          }
        };
        request.onerror = function(data) {
          reject(request, data);
        };
        request.send(null);
      });
    }

    return _file_promises[module_name];
  }

  const _lazy_module_promises = {};
  function _lazy(module_name) {
    if (module_name.substr(0, 2) === './') {
      module_name = module_name.substr(2);
    }

    if (!_lazy_module_promises[module_name]) {
      _lazy_module_promises[module_name] = new Promise(resolve => {
        if (!modules[module_name]) {
          _file(module_name).then(source => resolve(define(module_name, source).exports));
        } else {
          setTimeout(() => resolve(modules[module_name].exports));
        }
      });
    }
    return _lazy_module_promises[module_name];
  }

  const scriptTags = {};
  const _viaTag = script_path => {
    if(!scriptTags[script_path]) {
      scriptTags[script_path] = new Promise((resolve) => {
        const script = document.createElement('script');
        script.setAttribute('src', script_path);
        script.addEventListener('load', resolve);
        document.body.appendChild(script);
      }).catch(() => console.error(`${script_path} failed to load`));
    }
    return scriptTags[script_path];
  };

  let savePreloadDataTimeout = 0;
  const _savePreloadData = () => {
    if (savePreloadDataTimeout) {
      clearTimeout(savePreloadDataTimeout);
    }
    savePreloadDataTimeout = setTimeout(() => {
      console.time('savePreloadData');
      localStorage.setItem(auto_preload_path, _preloadData(false));
      console.timeEnd('savePreloadData');
    }, autoPreload_enabled);
  };

  setTimeout(() => {
    const unused_modules = Object.keys(_file_promises).filter(module_name => ! modules[module_name]);
    if (unused_modules.length) {
      console.warn(`${unused_modules.join(', ')} were preloaded but not used`);
    }
  }, 5000);

  const _preloadData = () => {
    let available = [];
    let module_list = Object.keys(modules);
    const not_available = module => available.indexOf(module) === -1;

    // note that this is O(n^2) so if you have many, many modules it could get slow (but it should never be run by user)
    const no_deps = module => !dependency_map[module] || dependency_map[module].filter(not_available).length === 0;
    const waves = [];
    while(module_list.filter(not_available).length) {
      let wave = module_list.filter(not_available).filter(no_deps);
      waves.push(wave);
      if(wave.length === 0) {
        console.error('dependency deadlock', module_list);
        break;
      }
      available = available.concat(wave);
    }

    return JSON.stringify(waves, false, 2);
  };

  const _preload = waves => {
    function preload(waves) {
      if (waves.length) {
        const wave = waves.length ? waves.shift() : [];
        return Promise.all(wave.map(_file)).
               then(() => waves.length ? preload(waves) : null).
               catch(e => {
                  console.error('preload failed', e);
                  if (localStorage.getItem(auto_preload_path)) {
                    localStorage.removeItem(auto_preload_path);
                    window.location.reload();
                  }
               });
      }
    }
    return preload([[].concat(...waves)]);
  };

  const _autoPreload = (save_preload_data_throttle_ms) => {
    autoPreload_enabled = save_preload_data_throttle_ms || 2000;
    return new Promise(resolve => {
      const waves = localStorage.getItem(auto_preload_path);
      if (waves) {
        console.log('autoPreload data found');
        _preload(JSON.parse(waves)).then(resolve);
      } else {
        console.warn('no preload data found');
        resolve();
      }
    });
  };

  global.__d = define;

  Object.assign(require_utils, {
    report() {
      console.table(modules_loaded_synchronously);
    },
    data () {
      return {
        modules: Object.keys(modules),
        modules_loaded_synchronously,
        dependency_map
      };
    },
    isDefined (module_name) {
      return !!modules[module_name];
    },
    relative: _relative,
    preloadData: _preloadData,
    preload: _preload,
    autoPreload: _autoPreload,
    viaTag: _viaTag,
    electron: global.process && global.process.versions.electron,
    electron_remote: global.process && _global_require('electron').remote,
    globalRequire: _global_require,
    modules: () => modules,
  });

/*
  Electron exposes "module" global that confuses scripts imported via tag
*/
  if (global.module) {
    _require._global_module = global.module;
    delete global.module;
  }

  _require.lazy = _lazy;
  global.require = _require;
  Object.assign(global.require, require_utils);
}(this));
<|endoftext|>


<commit_before>var common = require('@screeps/common'),
    env = common.storage.env,
    q = require('q'),
    driver = require('./index'),
    path = require('path'),
    engine = require('@screeps/engine'),
    game = engine.game,
    utils = engine.utils,
    fakeConsole = engine.console,
    _ = require('lodash'),
    runtimeUserGlobals = require('./runtime-user-globals'),
    WorldMapGrid = require('./mapgrid'),
    vm = require('vm');

var mapGrid;

var staticTerrainData = {};

var now;

var originalPrototypes = {};

var getSandboxedFunctionWrapperScript = new vm.Script("(function sandboxedFunctionWrapper(fn) { return function() { return fn.apply(this, arguments); } })");

['Number','String','Boolean','Date','Object','Function','Buffer','RangeError','ReferenceError','SyntaxError','TypeError','Error','Array','RegExp','Map','WeakMap','Set','WeakSet','Promise'].forEach(name => {
    originalPrototypes[name] = {
        protoMethods: {},
        globalMethods: {},
        prototype: global[name].prototype
    };

    Object.getOwnPropertyNames(global[name]).forEach(method => {
        try {
            if (typeof global[name][method] != 'function') {
                return;
            }
            originalPrototypes[name].globalMethods[method] = global[name][method];
        }
        catch(e) {}
    });

    Object.getOwnPropertyNames(global[name].prototype).forEach(method => {
        try {
            if (typeof global[name].prototype[method] != 'function') {
                return;
            }
            if(name == 'Function' && method == 'constructor') {
                return;
            }
            originalPrototypes[name].protoMethods[method] = global[name].prototype[method];
        }
        catch(e) {}
    });

});

function checkOriginalPrototypes() {
    for(var name in originalPrototypes) {
        if(global[name].prototype != originalPrototypes[name].prototype) {
            return `${name}.prototype`;
        }
        for(var method in originalPrototypes[name].globalMethods) {
            if(global[name][method] !== originalPrototypes[name].globalMethods[method]) {
                return `${name}.${method}`;
            }
        }
        for(var method in originalPrototypes[name].protoMethods) {
            if(global[name].prototype[method] !== originalPrototypes[name].protoMethods[method]) {
                return `${name}.prototype.${method}`;
            }
        }
    }

    if(Object.getOwnPropertyNames(Object.prototype).some(prop => {
            return prop != '__proto__' && !originalPrototypes.Object.protoMethods.hasOwnProperty(prop);
        })) {
        return 'Object.prototype';
    }

    return false;
}

var connectPromise = driver.connect('runtime')
    .then(() => {
        now = Date.now();
        return driver.getAllTerrainData();
    })
    .then((result) => {
        driver.pathFinder.init(result);

        result.forEach(room => {
            var array = new Uint8Array(2500);
            var char;
            for(var i=0;i<2500;i++) {
                char = room.terrain.charAt(i);
                array[i] = Number(char);
            }
            staticTerrainData[room.room] = array;
        })
    });

process.on('message', (message) => {

    if(!message.userId) return;

    var startDirtyTime = process.hrtime();

    connectPromise
    .then(() => q.all([
        driver.getRuntimeData(message.userId, message.onlyInRoom),
        env.get(env.keys.SCRIPT_CACHED_DATA + message.userId)
    ]))
    .then((data) => {

        let runtimeData = data[0];
        let scriptCachedData = {};
        var activeSegments, publicSegments, defaultPublicSegment, activeForeignSegment;
        scriptCachedData.cachedData = _.mapValues(data[1], i => Buffer.from(i, 'base64'));

        runtimeData.staticTerrainData = staticTerrainData;

        process.send({type: 'stats', name: 'getRuntimeData'});

        var intentCpu = 0.2,
            freeMethods = {say: true};

        var intents = {
            list: {},
            cpu: 0,
            set(id, name, data) {
                this.list[id] = this.list[id] || {};
                if(!freeMethods[name] && !this.list[id][name]) {
                    this.cpu += intentCpu;
                }
                this.list[id][name] = data;
            },
            push(name, data, maxLen) {
                this.list[name] = this.list[name] || [];
                if(maxLen && this.list[name].length >= maxLen) {
                    return false;
                }
                this.list[name].push(data);
                this.cpu += intentCpu;
                return true;
            },
            pushByName(id, name, data, maxLen) {
                this.list[id] = this.list[id] || {};
                this.list[id][name] = this.list[id][name] || [];
                if(maxLen && this.list[id][name].length >= maxLen) {
                    return false;
                }
                this.list[id][name].push(data);
                this.cpu += intentCpu;
                return true;
            },
            remove(id, name) {
                if(this.list[id] && this.list[id][name]) {
                    delete this.list[id][name];
                    return true;
                }
                return false;
            }
        };

        runtimeUserGlobals.checkObsolete(message.userId, runtimeData.userCodeTimestamp);
        runtimeUserGlobals.init(message.userId);

        var startTime = process.hrtime();

        var rawMemory = Object.create(null, {
            get: {
                value: function() {
                    return runtimeData.userMemory.data;
                }
            },
            set: {
                value: function (value) {
                    if (!_.isString(value)) {
                        throw new Error('Raw memory value is not a string');
                    }
                    if (value.length > 2 * 1024 * 1024) {
                        throw new Error('Raw memory length exceeded 2 MB limit');
                    }
                    if (this._parsed) {
                        delete this._parsed;
                    }
                    runtimeData.userMemory.data = value;
                }
            },
            segments: {
                value: Object.create(null)
            },
            setActiveSegments: {
                value: function(ids) {
                    if(!_.isArray(ids)) {
                        throw new Error(`"${ids}" is not an array`);
                    }
                    if(ids.length > 10) {
                        throw new Error('Only 10 memory segments can be active at the same time');
                    }
                    activeSegments = [];
                    for(var i=0; i<ids.length; i++) {
                        var id = parseInt(ids[i]);
                        if(_.isNaN(id) || id > 99 || id < 0) {
                            throw new Error(`"${ids[i]}" is not a valid segment ID`);
                        }
                        activeSegments.push(id);
                    }

                }
            },
            setPublicSegments: {
                value: function(ids) {
                    if(!_.isArray(ids)) {
                        throw new Error(`"${ids}" is not an array`);
                    }
                    publicSegments = [];
                    for(var i=0; i<ids.length; i++) {
                        var id = parseInt(ids[i]);
                        if(_.isNaN(id) || id > 99 || id < 0) {
                            throw new Error(`"${ids[i]}" is not a valid segment ID`);
                        }
                        publicSegments.push(id);
                    }
                }
            },
            setDefaultPublicSegment: {
                value: function(id) {
                    if(id !== null) {
                        id = parseInt(id);
                        if (_.isNaN(id) || id > 99 || id < 0) {
                            throw new Error(`"${id}" is not a valid segment ID`);
                        }
                    }
                    defaultPublicSegment = id;
                }
            },
            setActiveForeignSegment: {
                value: function(username, id) {
                    if(username === null) {
                        activeForeignSegment = null;
                        return;
                    }
                    if(id !== undefined) {
                        id = parseInt(id);
                        if (_.isNaN(id) || id > 99 || id < 0) {
                            throw new Error(`"${id}" is not a valid segment ID`);
                        }
                    }
                    activeForeignSegment = {username, id};
                }
            }
        });

        if(runtimeData.memorySegments) {
            for(var i in runtimeData.memorySegments) {
                rawMemory.segments[i] = runtimeData.memorySegments[i];
            }
        }

        if(runtimeData.foreignMemorySegment) {
            rawMemory.foreignSegment = Object.create(null);
            Object.assign(rawMemory.foreignSegment, runtimeData.foreignMemorySegment);
        }

        var getUsedCpu = function () {
            var elapsed = process.hrtime(startTime);
            elapsed = elapsed[0] * 1e3 + elapsed[1] / 1e6 + intents.cpu;
            return elapsed;
        };

        if(!mapGrid) {
            mapGrid = new WorldMapGrid(
                runtimeData.accessibleRooms,
                runtimeData.staticTerrainData);
        }
        runtimeData.mapGrid = mapGrid;

        var outMessage;

        try {

            var globals = runtimeUserGlobals.get(message.userId);
            var sandboxedFunctionWrapper = getSandboxedFunctionWrapperScript.runInContext(globals);

            Function.prototype.constructor = sandboxedFunctionWrapper().__proto__.constructor;

            game.runCode(
                runtimeUserGlobals.get(message.userId),
                sandboxedFunctionWrapper,
                runtimeData.userCode,
                runtimeData,
                intents,
                rawMemory,
                fakeConsole.makeConsole(runtimeData.user._id, sandboxedFunctionWrapper),
                runtimeData.consoleCommands,
                runtimeData.cpu,
                getUsedCpu,
                function () {
                    startTime = process.hrtime();
                    process.send({type: 'start'});
                },
                function(name) {
                    process.send({type: 'stats', name});
                },
                scriptCachedData);

            if(rawMemory._parsed) {
                runtimeData.userMemory.data = JSON.stringify(rawMemory._parsed);
            }

            outMessage = {
                type: 'done'
            };

            var segmentKeys = Object.keys(rawMemory.segments);
            if(segmentKeys.length > 0) {
                if(segmentKeys.length > 10) {
                    return q.reject('Cannot save more than 10 memory segments on the same tick');
                }
                outMessage.memorySegments = {};
                for(var i=0; i<segmentKeys.length; i++) {
                    var key = parseInt(segmentKeys[i]);
                    if(_.isNaN(key) || key < 0 || key > 99) {
                        return q.reject(`"${segmentKeys[i]}" is not a valid memory segment ID`);
                    }
                    if(typeof rawMemory.segments[segmentKeys[i]] != 'string'){
                        return q.reject(`Memory segment #${segmentKeys[i]} is not a string`);
                    }
                    if(rawMemory.segments[segmentKeys[i]].length > 100*1024) {
                        return q.reject(`Memory segment #${segmentKeys[i]} has exceeded 100 KB length limit`);
                    }
                    outMessage.memorySegments[key] = ""+rawMemory.segments[segmentKeys[i]];
                }
            }

        }
        catch (e) {
            outMessage = {
                type: 'error',
                error: _.isObject(e) && e.stack || e.toString()
            };
        }
        finally {
            Function.prototype.constructor = Function;
        }

        var checkOriginalPrototypesResult = checkOriginalPrototypes();
        if(checkOriginalPrototypesResult) {
            return q.reject('Security policy violation');
        }

        if(scriptCachedData.cachedDataProduced) {
            env.hmset(
                env.keys.SCRIPT_CACHED_DATA + message.userId,
                _.mapValues(scriptCachedData.cachedDataProduced, i => i.toString('base64')))
            .then(() => env.expire(env.keys.SCRIPT_CACHED_DATA + message.userId, 3*3600));

        }
        if(scriptCachedData.cachedDataRejected) {
            env.del(env.keys.SCRIPT_CACHED_DATA + message.userId);
        }

        outMessage.usedTime = process.hrtime(startTime);
        outMessage.usedTime = Math.ceil(outMessage.usedTime[0] * 1e3 + outMessage.usedTime[1] / 1e6 + intents.cpu);

        outMessage.usedDirtyTime = process.hrtime(startDirtyTime);
        outMessage.usedDirtyTime = Math.ceil(outMessage.usedDirtyTime[0] * 1e3 + outMessage.usedDirtyTime[1] / 1e6);

        outMessage.intents = utils.storeIntents(message.userId, intents.list, runtimeData);
        outMessage.intentsCpu = intents.cpu;

        outMessage.memory = runtimeData.userMemory;

        outMessage.console = {
            log: fakeConsole.getMessages(runtimeData.user._id),
            results: fakeConsole.getCommandResults(runtimeData.user._id)
        };

        var visual = fakeConsole.getVisual(runtimeData.user._id);
        for (var roomName in visual) {
            env.setex(
                env.keys.ROOM_VISUAL + runtimeData.user._id + ',' + roomName + ',' + runtimeData.time,
                driver.config.mainLoopResetInterval/1000,
                visual[roomName]);
        }

        outMessage.activeSegments = activeSegments;
        outMessage.activeForeignSegment = activeForeignSegment;
        outMessage.defaultPublicSegment = defaultPublicSegment;

        if(publicSegments) {
            env.set(env.keys.PUBLIC_MEMORY_SEGMENTS+runtimeData.user._id, publicSegments.join(','));
        }

        process.send(outMessage);
    })
    .catch((e) => {
        if(e !== false) {
            console.log('Runtime getRuntimeData error:', e.stack || e);
        }
        process.send({type: 'reject', error: e !== false ? e.toString() : undefined})
    });
});

process.on('disconnect', () => process.exit());
<commit_msg>refact(runtime): change runtime error handling order<commit_after>var common = require('@screeps/common'),
    env = common.storage.env,
    q = require('q'),
    driver = require('./index'),
    path = require('path'),
    engine = require('@screeps/engine'),
    game = engine.game,
    utils = engine.utils,
    fakeConsole = engine.console,
    _ = require('lodash'),
    runtimeUserGlobals = require('./runtime-user-globals'),
    WorldMapGrid = require('./mapgrid'),
    vm = require('vm');

var mapGrid;

var staticTerrainData = {};

var now;

var originalPrototypes = {};

var getSandboxedFunctionWrapperScript = new vm.Script("(function sandboxedFunctionWrapper(fn) { return function() { return fn.apply(this, arguments); } })");

['Number','String','Boolean','Date','Object','Function','Buffer','RangeError','ReferenceError','SyntaxError','TypeError','Error','Array','RegExp','Map','WeakMap','Set','WeakSet','Promise'].forEach(name => {
    originalPrototypes[name] = {
        protoMethods: {},
        globalMethods: {},
        prototype: global[name].prototype
    };

    Object.getOwnPropertyNames(global[name]).forEach(method => {
        try {
            if (typeof global[name][method] != 'function') {
                return;
            }
            originalPrototypes[name].globalMethods[method] = global[name][method];
        }
        catch(e) {}
    });

    Object.getOwnPropertyNames(global[name].prototype).forEach(method => {
        try {
            if (typeof global[name].prototype[method] != 'function') {
                return;
            }
            if(name == 'Function' && method == 'constructor') {
                return;
            }
            originalPrototypes[name].protoMethods[method] = global[name].prototype[method];
        }
        catch(e) {}
    });

});

function checkOriginalPrototypes() {
    for(var name in originalPrototypes) {
        if(global[name].prototype != originalPrototypes[name].prototype) {
            return `${name}.prototype`;
        }
        for(var method in originalPrototypes[name].globalMethods) {
            if(global[name][method] !== originalPrototypes[name].globalMethods[method]) {
                return `${name}.${method}`;
            }
        }
        for(var method in originalPrototypes[name].protoMethods) {
            if(global[name].prototype[method] !== originalPrototypes[name].protoMethods[method]) {
                return `${name}.prototype.${method}`;
            }
        }
    }

    if(Object.getOwnPropertyNames(Object.prototype).some(prop => {
            return prop != '__proto__' && !originalPrototypes.Object.protoMethods.hasOwnProperty(prop);
        })) {
        return 'Object.prototype';
    }

    return false;
}

var connectPromise = driver.connect('runtime')
    .then(() => {
        now = Date.now();
        return driver.getAllTerrainData();
    })
    .then((result) => {
        driver.pathFinder.init(result);

        result.forEach(room => {
            var array = new Uint8Array(2500);
            var char;
            for(var i=0;i<2500;i++) {
                char = room.terrain.charAt(i);
                array[i] = Number(char);
            }
            staticTerrainData[room.room] = array;
        })
    });

process.on('message', (message) => {

    if(!message.userId) return;

    var startDirtyTime = process.hrtime();

    connectPromise
    .then(() => q.all([
        driver.getRuntimeData(message.userId, message.onlyInRoom),
        env.get(env.keys.SCRIPT_CACHED_DATA + message.userId)
    ]))
    .then((data) => {

        let runtimeData = data[0];
        let scriptCachedData = {};
        var activeSegments, publicSegments, defaultPublicSegment, activeForeignSegment;
        scriptCachedData.cachedData = _.mapValues(data[1], i => Buffer.from(i, 'base64'));

        runtimeData.staticTerrainData = staticTerrainData;

        process.send({type: 'stats', name: 'getRuntimeData'});

        var intentCpu = 0.2,
            freeMethods = {say: true};

        var intents = {
            list: {},
            cpu: 0,
            set(id, name, data) {
                this.list[id] = this.list[id] || {};
                if(!freeMethods[name] && !this.list[id][name]) {
                    this.cpu += intentCpu;
                }
                this.list[id][name] = data;
            },
            push(name, data, maxLen) {
                this.list[name] = this.list[name] || [];
                if(maxLen && this.list[name].length >= maxLen) {
                    return false;
                }
                this.list[name].push(data);
                this.cpu += intentCpu;
                return true;
            },
            pushByName(id, name, data, maxLen) {
                this.list[id] = this.list[id] || {};
                this.list[id][name] = this.list[id][name] || [];
                if(maxLen && this.list[id][name].length >= maxLen) {
                    return false;
                }
                this.list[id][name].push(data);
                this.cpu += intentCpu;
                return true;
            },
            remove(id, name) {
                if(this.list[id] && this.list[id][name]) {
                    delete this.list[id][name];
                    return true;
                }
                return false;
            }
        };

        runtimeUserGlobals.checkObsolete(message.userId, runtimeData.userCodeTimestamp);
        runtimeUserGlobals.init(message.userId);

        var startTime = process.hrtime();

        var rawMemory = Object.create(null, {
            get: {
                value: function() {
                    return runtimeData.userMemory.data;
                }
            },
            set: {
                value: function (value) {
                    if (!_.isString(value)) {
                        throw new Error('Raw memory value is not a string');
                    }
                    if (value.length > 2 * 1024 * 1024) {
                        throw new Error('Raw memory length exceeded 2 MB limit');
                    }
                    if (this._parsed) {
                        delete this._parsed;
                    }
                    runtimeData.userMemory.data = value;
                }
            },
            segments: {
                value: Object.create(null)
            },
            setActiveSegments: {
                value: function(ids) {
                    if(!_.isArray(ids)) {
                        throw new Error(`"${ids}" is not an array`);
                    }
                    if(ids.length > 10) {
                        throw new Error('Only 10 memory segments can be active at the same time');
                    }
                    activeSegments = [];
                    for(var i=0; i<ids.length; i++) {
                        var id = parseInt(ids[i]);
                        if(_.isNaN(id) || id > 99 || id < 0) {
                            throw new Error(`"${ids[i]}" is not a valid segment ID`);
                        }
                        activeSegments.push(id);
                    }

                }
            },
            setPublicSegments: {
                value: function(ids) {
                    if(!_.isArray(ids)) {
                        throw new Error(`"${ids}" is not an array`);
                    }
                    publicSegments = [];
                    for(var i=0; i<ids.length; i++) {
                        var id = parseInt(ids[i]);
                        if(_.isNaN(id) || id > 99 || id < 0) {
                            throw new Error(`"${ids[i]}" is not a valid segment ID`);
                        }
                        publicSegments.push(id);
                    }
                }
            },
            setDefaultPublicSegment: {
                value: function(id) {
                    if(id !== null) {
                        id = parseInt(id);
                        if (_.isNaN(id) || id > 99 || id < 0) {
                            throw new Error(`"${id}" is not a valid segment ID`);
                        }
                    }
                    defaultPublicSegment = id;
                }
            },
            setActiveForeignSegment: {
                value: function(username, id) {
                    if(username === null) {
                        activeForeignSegment = null;
                        return;
                    }
                    if(id !== undefined) {
                        id = parseInt(id);
                        if (_.isNaN(id) || id > 99 || id < 0) {
                            throw new Error(`"${id}" is not a valid segment ID`);
                        }
                    }
                    activeForeignSegment = {username, id};
                }
            }
        });

        if(runtimeData.memorySegments) {
            for(var i in runtimeData.memorySegments) {
                rawMemory.segments[i] = runtimeData.memorySegments[i];
            }
        }

        if(runtimeData.foreignMemorySegment) {
            rawMemory.foreignSegment = Object.create(null);
            Object.assign(rawMemory.foreignSegment, runtimeData.foreignMemorySegment);
        }

        var getUsedCpu = function () {
            var elapsed = process.hrtime(startTime);
            elapsed = elapsed[0] * 1e3 + elapsed[1] / 1e6 + intents.cpu;
            return elapsed;
        };

        if(!mapGrid) {
            mapGrid = new WorldMapGrid(
                runtimeData.accessibleRooms,
                runtimeData.staticTerrainData);
        }
        runtimeData.mapGrid = mapGrid;

        var outMessage;

        try {

            var globals = runtimeUserGlobals.get(message.userId);
            var sandboxedFunctionWrapper = getSandboxedFunctionWrapperScript.runInContext(globals);

            Function.prototype.constructor = sandboxedFunctionWrapper().__proto__.constructor;

            game.runCode(
                runtimeUserGlobals.get(message.userId),
                sandboxedFunctionWrapper,
                runtimeData.userCode,
                runtimeData,
                intents,
                rawMemory,
                fakeConsole.makeConsole(runtimeData.user._id, sandboxedFunctionWrapper),
                runtimeData.consoleCommands,
                runtimeData.cpu,
                getUsedCpu,
                function () {
                    startTime = process.hrtime();
                    process.send({type: 'start'});
                },
                function(name) {
                    process.send({type: 'stats', name});
                },
                scriptCachedData);

            outMessage = {
                type: 'done'
            };

        }
        catch (e) {
            outMessage = {
                type: 'error',
                error: _.isObject(e) && e.stack || e.toString()
            };
        }
        finally {

            if(rawMemory._parsed) {
                runtimeData.userMemory.data = JSON.stringify(rawMemory._parsed);
            }

            var segmentKeys = Object.keys(rawMemory.segments);
            if(segmentKeys.length > 0) {
                if(segmentKeys.length > 10) {
                    return q.reject('Cannot save more than 10 memory segments on the same tick');
                }
                outMessage.memorySegments = {};
                for(var i=0; i<segmentKeys.length; i++) {
                    var key = parseInt(segmentKeys[i]);
                    if(_.isNaN(key) || key < 0 || key > 99) {
                        return q.reject(`"${segmentKeys[i]}" is not a valid memory segment ID`);
                    }
                    if(typeof rawMemory.segments[segmentKeys[i]] != 'string'){
                        return q.reject(`Memory segment #${segmentKeys[i]} is not a string`);
                    }
                    if(rawMemory.segments[segmentKeys[i]].length > 100*1024) {
                        return q.reject(`Memory segment #${segmentKeys[i]} has exceeded 100 KB length limit`);
                    }
                    outMessage.memorySegments[key] = ""+rawMemory.segments[segmentKeys[i]];
                }
            }

            Function.prototype.constructor = Function;
        }

        var checkOriginalPrototypesResult = checkOriginalPrototypes();
        if(checkOriginalPrototypesResult) {
            return q.reject('Security policy violation');
        }

        if(scriptCachedData.cachedDataProduced) {
            env.hmset(
                env.keys.SCRIPT_CACHED_DATA + message.userId,
                _.mapValues(scriptCachedData.cachedDataProduced, i => i.toString('base64')))
            .then(() => env.expire(env.keys.SCRIPT_CACHED_DATA + message.userId, 3*3600));

        }
        if(scriptCachedData.cachedDataRejected) {
            env.del(env.keys.SCRIPT_CACHED_DATA + message.userId);
        }

        outMessage.usedTime = process.hrtime(startTime);
        outMessage.usedTime = Math.ceil(outMessage.usedTime[0] * 1e3 + outMessage.usedTime[1] / 1e6 + intents.cpu);

        outMessage.usedDirtyTime = process.hrtime(startDirtyTime);
        outMessage.usedDirtyTime = Math.ceil(outMessage.usedDirtyTime[0] * 1e3 + outMessage.usedDirtyTime[1] / 1e6);

        outMessage.intents = utils.storeIntents(message.userId, intents.list, runtimeData);
        outMessage.intentsCpu = intents.cpu;

        outMessage.memory = runtimeData.userMemory;

        outMessage.console = {
            log: fakeConsole.getMessages(runtimeData.user._id),
            results: fakeConsole.getCommandResults(runtimeData.user._id)
        };

        var visual = fakeConsole.getVisual(runtimeData.user._id);
        for (var roomName in visual) {
            env.setex(
                env.keys.ROOM_VISUAL + runtimeData.user._id + ',' + roomName + ',' + runtimeData.time,
                driver.config.mainLoopResetInterval/1000,
                visual[roomName]);
        }

        outMessage.activeSegments = activeSegments;
        outMessage.activeForeignSegment = activeForeignSegment;
        outMessage.defaultPublicSegment = defaultPublicSegment;

        if(publicSegments) {
            env.set(env.keys.PUBLIC_MEMORY_SEGMENTS+runtimeData.user._id, publicSegments.join(','));
        }

        process.send(outMessage);
    })
    .catch((e) => {
        if(e !== false) {
            console.log('Runtime getRuntimeData error:', e.stack || e);
        }
        process.send({type: 'reject', error: e !== false ? e.toString() : undefined})
    });
});

process.on('disconnect', () => process.exit());
<|endoftext|>