UNPKG

monaca-lib

Version:

Monaca cloud and localkit API bindings for JavaScript

422 lines (346 loc) 12 kB
(function() { 'use strict'; // imports var qs = require('querystring'), path = require('path'), fs = require('fs'), crypto = require('crypto'), Q = require('q'), rc4 = require(path.join(__dirname, 'rc4')); var PAIRING_KEYS_FILE = path.join( process.env[(process.platform == 'win32') ? 'USERPROFILE' : 'HOME'], '.cordova', 'monaca_pairing_keys.json' ); var Api = function(localkit) { this.localkit = localkit; this.monaca = localkit.monaca; this.routes = { '/api/pairing/request': this.pairingApi.bind(this), '/api/projects': this.projectsApi.bind(this), '/api/project/:project_id/file/tree': this.fileTreeApi.bind(this), '/api/project/:project_id/file/read': this.fileReadApi.bind(this), '/api/project/:project_id/started': this.noop.bind(this), '/api/project/:project_id/stopped': this.noop.bind(this), '/api/local/auth': this.localAuthApi.bind(this) }; }; Api.prototype.noop = function(request, response) { response.end(JSON.stringify({ status: 'ok', code: 200 })); }; Api.prototype.sendJsonResponse = function(response, code, message, result, encrypt, pairingKey) { response.writeHead(code, { 'Content-Type': 'application/json' }); var data = { status: code.toString()[0] === '2' ? 'ok' : 'fail', code: code, message: message }; if (result) { data.result = result; } var msg = JSON.stringify(data); if (this.monaca.debug) { console.debug("Local Response: " + msg); } this.localkit.emit('httpResponse', { // Warning: One more httpResponse emit below response: response, code: code, message: message }); if (encrypt) { response.end(rc4.encrypt(msg, pairingKey)); } else { response.end(msg); } }; Api.prototype.matchRoutes = function(url, route) { var urlParts = url.split('?')[0].split('/'), routeParts = route.split('/'); if (urlParts.length !== routeParts.length) { return null; } var data = {}; for (var i = 0, l = urlParts.length; i < l; i ++) { var urlPart = urlParts[i], routePart = routeParts[i]; if (routePart.charAt(0) === ':') { data[routePart.substr(1)] = urlPart; } else { if (routePart !== urlPart) { return null; } } } return data; }; Api.prototype.getPairingKey = function(request) { var clientId = request.headers['x-monaca-client-id-hash']; return this.localkit.pairingKeys[clientId]; }; Api.prototype.requestHandler = function(request, response) { if (this.monaca.debug) { console.debug("Local Request to: " + request.url); } var keys = Object.keys(this.routes); for (var i = 0, l = keys.length; i < l; i ++) { var data = this.matchRoutes(request.url, keys[i]); if (data) { return this.routes[keys[i]](request, response, data); } } this.sendJsonResponse(response, 404, 'No such route.'); }; Api.prototype.pairingApi = function(request, response) { var data = qs.parse((request.url + '?').split('?')[1]), requestToken = data.requestToken, clientIdHash = data.clientIdHash; if (!requestToken || !clientIdHash) { if (this.localkit.verbose) { console.log('Invalid pairing paramters.'); } return this.sendJsonResponse(400, 'Must specify request token and client id hash.'); } else { this.localkit.requestPairingKey(requestToken, clientIdHash).then( function(pairingKey) { if (this.localkit.verbose) { console.log('Paired with debugger!'); } this.localkit.pairingKeys[clientIdHash] = pairingKey; fs.writeFile(PAIRING_KEYS_FILE, JSON.stringify(this.localkit.pairingKeys), function(error) { if (error) { return this.sendJsonResponse(response, 500, 'Unable to save pairing key.'); } return this.sendJsonResponse(response, 200, 'Received pairing key.', this.localkit._getServerInfo()); }.bind(this)); }.bind(this), function(error) { if (this.localkit.verbose) { console.log('Failed pairing with debugger.'); } return this.sendJsonResponse(response, 400, error); }.bind(this) ); } }; Api.prototype.validatePairing = function(request) { var pairingKey = this.getPairingKey(request); if (!pairingKey) { if (this.localkit.verbose) { console.log('No pairing key associated with this device.'); } return false; } var expectedClientCredential; try { expectedClientCredential = rc4.encrypt(pairingKey.toString("hex"), Buffer.from(pairingKey.toString("hex"), "utf8")); } catch (error) { expectedClientCredential = null; } if (expectedClientCredential === request.headers['x-monaca-client-credential']) { return true; } if (this.localkit.verbose) { console.log('Invalid pairing key.'); } return false; }; Api.prototype.projectsApi = function(request, response) { if (!this.validatePairing(request)) { this.sendJsonResponse(response, 401, 'Not paired with debugger.'); return; } var pairingKey = this.getPairingKey(request); if (this.localkit.verbose) { console.log('Debugger requested project list.'); } this.localkit.getProjects().then( function(projects) { this.sendJsonResponse(response, 200, 'Project list', projects, true, pairingKey); }.bind(this), function(error) { this.sendJsonResponse(response, 400, 'Unable to get project list.', undefined, true, pairingKey); }.bind(this) ); }; Api.prototype.fileTreeApi = function(request, response, data) { if (!this.validatePairing(request)) { this.sendJsonResponse(response, 401, 'Not paired with debugger.'); return; } var pairingKey = this.getPairingKey(request); var projectId = data.project_id; if (this.localkit.verbose) { console.log('Debugger requested file tree.'); } this.localkit.getProjectFiles(projectId).then( function(files) { this.sendJsonResponse(response, 200, 'File list', {items: files}, true, pairingKey); }.bind(this), function(error) { this.sendJsonResponse(response, 400, 'Unable to get project files.', undefined, true, pairingKey); }.bind(this) ); }; Api.prototype.fileReadApi = function(request, response, data) { if (!this.validatePairing(request)) { this.sendJsonResponse(response, 401, 'Not paired with debugger.'); return; } var pairingKey = this.getPairingKey(request); var projectId = data.project_id, body = ''; request.on('data', function (data) { body += data; if (body.length > 1e8) { request.connection.destroy(); } }); request.on('end', function () { try { var data = JSON.parse(rc4.decrypt(body, pairingKey)); if (this.localkit.verbose) { console.log('Debugger requested file: ' + data.path); } this.localkit.readProjectFile(projectId, data.path).then( function(buf) { response.writeHead(200, { 'Content-Type': 'application/octet-stream' }); response.end(rc4.encrypt(buf, pairingKey)); this.localkit.emit('httpResponse', { response: response, code: '200', message: 'File read ' + data.path }); }.bind(this), function(error) { this.sendJsonResponse(response, 500, 'Unable to read file.', undefined, true, pairingKey); }.bind(this) ); } catch (error) { this.sendJsonResponse(response, 400, 'Unable to parse data', undefined, true, pairingKey); } }.bind(this)); }; Api.prototype.localAuthApi = function(request, response) { var passwordHash = request.headers['x-otp-hash'] || ''; var get = function(request) { var body = '', deferred = Q.defer(); request.on('data', function(data) { body += data; if (body.length > 1e8) { request.connection.destroy(); deferred.reject({code: 500, message: ' too large.'}); } }); request.on('end', function() { deferred.resolve(body); }); return deferred.promise; }; var decrypt = function(body, otp) { try { return Q.resolve(rc4.decrypt(body, otp)); } catch (e) { return Q.reject({code: 400, message: 'Unable to decrypt body.'}); } }; var parse = function(body) { try { return Q.resolve(qs.parse(body)); } catch (e) { return Q.reject({code: 400, message: 'Unable to parse body.'}); } }; var validate = function(data) { if (!data.clientId) { return Q.reject({code: 400, message: '"clientId" parameter missing.'}); } else { return Q.resolve(data); } }; var pairing = function(data) { return this.localkit.generateLocalPairingKey() .then( function(pairingKey) { var shasum = crypto.createHash('sha256'); shasum.update(data.clientId); var clientIdHash = shasum.digest('hex'); this.localkit.pairingKeys[clientIdHash] = pairingKey; var writeFile = Q.denodeify(fs.writeFile); return writeFile(PAIRING_KEYS_FILE, JSON.stringify(this.localkit.pairingKeys)) .then( function() { // Pairing completed. return Q.resolve({data: data, pairingKey: pairingKey}); }, function(error) { return Q.reject({code: 500, message: 'Unable to save pairing keys.'}); } ); }.bind(this), function() { return Q.reject({code: 500, message: 'Unable to generate pairing key.'}); } ); }.bind(this); this.localkit.validateOneTimePassword(passwordHash) .then( function(password) { var otp = password.data; get(request) .then( function(body) { return decrypt(body, otp); } ) .then(parse) .then(validate) .then(pairing) .then( function(param) { var data = param.data, pairingKey = param.pairingKey; var serverInfo = this.localkit._getServerInfo(), userInfo = this.monaca.getCurrentUser(); var ip = request.connection.localAddress.replace(/^:.*:/, '');; var data = { type: serverInfo.type, port: serverInfo.port, os: serverInfo.os, serverName: serverInfo.name, serverId: serverInfo.serverId, userHash: serverInfo.userHash, version: serverInfo.version, userId: userInfo.userId, username: userInfo.username, email: userInfo.email, pairingKey: pairingKey.toString('hex') }; this.sendJsonResponse(response, 200, 'Pairing successful.', data, true, otp); }.bind(this), function(error) { this.sendJsonResponse(response, error.code, error.message); }.bind(this) ); }.bind(this), function(error) { return this.sendJsonResponse(response, 401, 'Failed to process the request. The one-time password might have expired.'); }.bind(this) ) }; module.exports = Api; })();