webservice.js | |
|---|---|
/*
*
* Copyright (C) 2011, The Locker Project
* All rights reserved.
*
* Please see the LICENSE file for more information.
*
*/
var url = require("url");
var http = require('http');
var request = require('request');
var lscheduler = require("lscheduler");
var levents = require("levents");
var lutil = require('lutil');
var serviceManager = require("lservicemanager");
var syncManager = require('lsyncmanager');
var express = require('express');
var connect = require('connect');
var request = require('request');
var path = require('path');
var fs = require("fs");
var url = require('url');
var querystring = require("querystring");
var lfs = require(__dirname + "/../Common/node/lfs.js");
var httpProxy = require('http-proxy');
var lpquery = require("lpquery");
var lconfig = require("lconfig");
var logger = require('logger');
var async = require('async');
var lcrypto = require("lcrypto");
var proxy = new httpProxy.RoutingProxy();
var scheduler = lscheduler.masterScheduler;
var airbrake;
var locker = express.createServer( | |
| we only use bodyParser to create .params for callbacks from services, connect should have a better way to do this | function(req, res, next) {
if (req.url.substring(0, 6) == "/core/" || req.url.substring(0, 6) == '/push/' || req.url.substring(0, 6) == '/post/') {
connect.bodyParser()(req, res, next);
} else {
next();
}
},
function(req, res, next) {
if (req.url.substring(0, 6) == '/auth/') {
connect.bodyParser()(req, res, next);
} else {
next();
}
},
connect.cookieParser(),
connect.session({key:'locker.project.id', secret : "locker"})
);
var listeners = {}; // listeners for events
var DEFAULT_QUERY_LIMIT = 20; |
| return the known map of our world | locker.get('/map', function(req, res) {
var copy = {};
lutil.extend(true, copy, serviceManager.map());
Object.keys(copy).forEach(function(key){
if(copy[key].auth) copy[key].auth = {profile:copy[key].auth.profile}; // silence keys
});
res.send(copy);
});
locker.get('/map/profiles', function(req, res) {
var profiles = {};
var map = serviceManager.map();
for(var key in map) {
if(!map[key].auth || !map[key].auth.profile) continue;
var idr = { slashes: true, pathname: '/', host: key }; |
| the type could be named something service-specific, usually 'contact' tho | idr.protocol = (map[key].types && map[key].types.contact) ? map[key].types.contact : 'contact'; |
| generate idrs from profiles, some services have both numeric and username (or more?)! | var ids = map[key].profileIds || ['id'];
for(var i in ids) {
var id = ids[i];
if(!map[key].auth.profile[id]) continue;
idr.hash = map[key].auth.profile[id];
profiles[url.format(idr)] = map[key].auth.profile;
}
}
res.send(profiles);
});
locker.post('/map/upsert', function(req, res) {
logger.info("Upserting " + req.param("manifest"));
res.send(serviceManager.mapUpsert(req.param("manifest")));
});
locker.get("/providers", function(req, res) {
if (!req.param("types")) return res.send([], 400);
res.send(serviceManager.providers(req.param('types').split(',')));
});
locker.get("/provides", function(req, res) {
var services = serviceManager.map();
var ret = {};
for(var i in services) if(services[i].provides) ret[i] = services[i].provides;
res.send(ret);
});
locker.get("/encrypt", function(req, res) {
if (!req.param("s")) {
res.writeHead(400);
res.end();
return;
}
logger.info("encrypting " + req.param("s"));
res.end(lcrypto.encrypt(req.param("s")));
});
locker.get("/decrypt", function(req, res) {
if (!req.param("s")) {
res.writeHead(400);
res.end();
return;
}
res.end(lcrypto.decrypt(req.param("s")));
}); |
| search interface | locker.get("/query/:query", function(req, res) {
if(!url.parse(req.originalUrl).query)
req.originalUrl += "?limit=" + DEFAULT_QUERY_LIMIT;
var data = decodeURIComponent(req.originalUrl.substr(6)).replace(/%21/g, '!').replace(/%27/g, "'").replace(/%28/g, '(').replace(/%29/g, ')').replace(/%2a/ig, '*');
try {
var query = lpquery.buildMongoQuery(lpquery.parse(data));
var providers = serviceManager.map();
var provider;
for (var key in providers) {
if (providers.hasOwnProperty(key) && providers[key].provides && providers[key].provides.indexOf(query.collection) >= 0 )
provider = providers[key];
}
if (provider === undefined) {
res.writeHead(404);
res.end(query.collection + " not found to query");
return;
}
var mongo = require("lmongo");
mongo.init(provider.id, provider.mongoCollections, function(mongo, colls) {
try {
var collection = colls[provider.mongoCollections[0]];
logger.info("Querying " + JSON.stringify(query));
var options = {};
options.limit = query.limit || DEFAULT_QUERY_LIMIT;
if (query.skip) options.skip = query.skip;
if (query.fields) options.fields = query.fields;
if (query.sort) options.sort = query.sort;
collection.find(query.query, options, function(err, foundObjects) {
if (err) {
res.writeHead(500);
res.end(err);
return;
}
foundObjects.toArray(function(err, objects) {
res.end(JSON.stringify(objects));
});
});
} catch (E) {
res.writeHead(500);
res.end('Something broke while trying to query Mongo : ' + E);
}
});
} catch (E) {
res.writeHead(400);
res.end("Invalid query " + req.originalUrl.substr(6) + "<br />" + E);
}
}); |
| let any service schedule to be called, it can only have one per uri | locker.get('/core/:svcId/at', function(req, res) {
var seconds = req.param("at");
var cb = req.param('cb');
var svcId = req.params.svcId;
if (!seconds || !svcId || !cb) {
res.writeHead(400);
res.end("Invalid arguments");
return;
}
if (!serviceManager.map(svcId)) {
res.writeHead(404);
res.end(svcId+" doesn't exist, but does anything really? ");
return;
}
res.writeHead(200, {
'Content-Type': 'text/html'
});
var at = new Date();
at.setTime(seconds * 1000);
scheduler.at(at, svcId, cb);
logger.info("scheduled "+ svcId + " " + cb + " at " + at);
res.end("true");
});
var collectionApis = serviceManager.getCollectionApis();
for(var i in collectionApis) {
locker._oldGet = locker.get;
locker.get = function(path, callback) {
return locker._oldGet('/Me/' + i + path, callback);
};
collectionApis[i].api(locker, collectionApis[i].lockerInfo);
locker.get = locker._oldGet;
locker._oldGet = undefined;
} |
| ME PROXY all of the requests to something installed (proxy them, moar future-safe) | locker.get(/^\/Me\/([^\/]*)(\/?.*)?\/?/, function(req,res, next){ |
| ensure the ending slash - i.e. /Me/foo ==>> /Me/foo/ | if(!req.params[1]) {
var handle = req.params[0];
var service = serviceManager.map(handle); |
| rebuild the url with a / after the /Me/ | var url = "/Me/" + handle + "/";
var qs = querystring.stringify(req.query);
if (qs.length > 0) url += "?" + qs;
if(service && service.type === 'app') {
res.header("Location", url);
return res.send(302);
}
req.url = url;
}
logger.silly("GET proxy of " + req.originalUrl);
proxyRequest('GET', req, res, next);
}); |
| all posts just pass | locker.post('/Me/*', function(req,res, next){
logger.silly("POST proxy of " + req.originalUrl);
proxyRequest('POST', req, res, next);
});
locker.get('/synclets/:id/run', function(req, res) {
syncManager.syncNow(req.params.id, req.query.id, false, function(err) {
if(err) return res.send(err, 500);
res.send(true);
});
}); |
| this will pass the post body to the synclet and run it immediately | locker.post('/post/:id/:synclet', function(req, res) {
syncManager.syncNow(req.params.id, req.params.synclet, req.body, function() {
res.send(true);
});
}); |
| all synclet getCurrent, id, etc stuff | require('synclet/dataaccess')(locker);
function proxyRequest(method, req, res, next) {
var slashIndex = req.url.indexOf("/", 4);
if (slashIndex < 0) slashIndex = req.url.length;
var id = req.url.substring(4, slashIndex);
var ppath = req.url.substring(slashIndex);
var info = serviceManager.map(id);
if (!info) {
logger.error(id + " not found in service map");
return res.send(404);
} |
| if there's synclets, handled by their own built-ins | if (info.synclets) {
req.url = req.url.replace('Me', 'synclets');
return next();
}
if (info.static === true || info.static === "true") { |
| This is a static file we'll try and serve it directly | var fileUrl = url.parse(ppath);
if(fileUrl.pathname.indexOf("/..") >= 0) { // extra sanity check
return res.send(404);
}
fs.stat(path.join(lconfig.lockerDir, info.srcdir, "static", fileUrl.pathname), function(err, stats) {
if (!err && (stats.isFile() || stats.isDirectory())) {
res.sendfile(path.join(lconfig.lockerDir, info.srcdir, "static", fileUrl.pathname));
} else {
fs.stat(path.join(lconfig.lockerDir, info.srcdir, fileUrl.pathname), function(err, stats) {
if (!err && (stats.isFile() || stats.isDirectory())) {
res.sendfile(path.join(lconfig.lockerDir, info.srcdir, fileUrl.pathname));
} else {
logger.warn("Could not find " + path.join(lconfig.lockerDir, info.srcdir, fileUrl.pathname));
res.send(404);
}
});
}
});
logger.silly("Sent static file " + path.join(lconfig.lockerDir, info.srcdir, "static", fileUrl.pathname));
} else {
if (!serviceManager.isRunning(id)) {
logger.info("Having to spawn " + id);
var buffer = httpProxy.buffer(req);
serviceManager.spawn(id,function(){
proxied(method, info, ppath, req, res, buffer);
});
} else {
proxied(method, info, ppath, req, res);
}
}
logger.silly("Proxy complete");
} |
| DIARY Publish a user visible message | locker.get("/core/:svcId/diary", function(req, res) {
var level = req.param("level") || 0;
var message = req.param("message");
var svcId = req.params.svcId;
var now = new Date();
try {
fs.mkdirSync(lconfig.me + "/diary", '0700', function(err) {
if (err && err.errno != process.EEXIST) logger.error("Error creating diary: " + err);
});
} catch (E) { |
| Why do I still have to catch when it has an error callback?! | }
fs.mkdir(lconfig.me + "/diary/" + now.getFullYear(), '0700', function(err) {
fs.mkdir(lconfig.me + "/diary/" + now.getFullYear() + "/" + now.getMonth(), '0700', function(err) {
var fullPath = lconfig.me + "/diary/" + now.getFullYear() + "/" + now.getMonth() + "/" + now.getDate() + ".json";
lfs.appendObjectsToFile(fullPath, [{"timestamp":now, "level":level, "message":message, "service":svcId}]);
res.writeHead(200);
res.end("{}");
});
});
}); |
| Retrieve the current days diary or the given range | locker.get("/diary", function(req, res) {
var now = new Date();
var fullPath = lconfig.me + "/diary/" + now.getFullYear() + "/" + now.getMonth() + "/" + now.getDate() + ".json";
res.writeHead(200, {
"Content-Type": "text/javascript",
"Access-Control-Allow-Origin" : "*"
});
fs.readFile(fullPath, function(err, file) {
if (err) {
res.write("[]");
res.end();
return;
}
var rawLines = file.toString().trim().split("\n");
var diaryLines = rawLines.map(function(line) { return JSON.parse(line); });
res.write(JSON.stringify(diaryLines), "binary");
res.end();
});
});
locker.get('/core/error', function(req, res) {
throw new Error("Hmm...This is a REAL job for STUPENDOUS MAN!");
});
locker.get('/core/revision', function(req, res) {
fs.readFile(path.join(lconfig.lockerDir, 'build.json'), function(err, doc) {
if (err) return logger.error(err);
if (doc) res.send(JSON.parse(doc));
else res.send("unknown");
});
});
locker.get('/core/selftest', function(req, res) {
async.series([
function(callback) {
fs.readdir(lconfig.me, function(err, files) {
if (err) {
callback({ 'Me/*' : err}, null);
} else {
callback(null, { 'Me/*' : files });
}
});
}
],
function(err, results) {
if (err) {
res.send(err, 500);
} else {
res.send(JSON.stringify(results), 200);
}
});
});
locker.get('/core/stats', function(req, res) {
var stats = {
'core' : {
'memoryUsage' : process.memoryUsage()
},
'serviceManager': {}
};
var map = serviceManager.map();
for (var serviceId in map) {
var type = map[serviceId].type;
if (!(type in stats.serviceManager)) {
stats.serviceManager[type] = {
'total' : 0,
'running' : 0
};
}
stats.serviceManager[type].total += 1;
if (serviceManager.isRunning(serviceId))
stats.serviceManager[type].running += 1;
} |
| serviceManager never reports that a connector is running | if ('connector' in stats.serviceManager)
delete stats.serviceManager.connector.running;
res.send(JSON.stringify(stats), 200);
}); |
| EVENTING anybody can listen into any service's events | locker.get('/core/:svcId/listen', function(req, res) {
var type = req.param('type'), cb = req.param('cb');
var svcId = req.params.svcId;
if(!serviceManager.map(svcId)) {
logger.error("Could not find " + svcId);
res.writeHead(404);
res.end(svcId+" doesn't exist, but does anything really? ");
return;
}
if (!type || !cb) {
res.writeHead(400);
res.end("Invalid type or callback");
return;
}
if(cb.substr(0,1) != "/") cb = '/'+cb; // ensure it's a root path
var batching = false;
if (req.param("batch") === "true" || req.param === true) batching = true;
levents.addListener(type, svcId, cb, batching);
res.writeHead(200);
res.end("OKTHXBI");
}); |
| Stop listening to some events | locker.get("/core/:svcId/deafen", function(req, res) {
var type = req.param('type'), cb = req.param('cb');
var svcId = req.params.svcId;
if(!serviceManager.map(svcId)) {
res.writeHead(404);
res.end(svcId+" doesn't exist, but does anything really? ");
return;
}
if (!type || !cb) {
res.writeHead(400);
res.end("Invalid type or callback");
return;
}
if(cb.substr(0,1) != "/") cb = '/'+cb; // ensure it's a root path
levents.removeListener(type, svcId, cb);
res.writeHead(200);
res.end("OKTHXBI");
}); |
| publish an event to any listeners | locker.post('/core/:svcId/event', function(req, res) {
if (!req.body ) {
res.writeHead(400);
res.end("Post data missing");
return;
}
var fromService = serviceManager.map(req.params.svcId);
if(!fromService) {
res.writeHead(404);
res.end(req.params.svcId+" doesn't exist, but does anything really? ");
return;
}
fromService.last = Date.now();
if (!req.body.idr || !req.body.data || !req.body.action) {
res.writeHead(400);
res.end("Invalid, missing idr, data, or action");
return;
}
levents.fireEvent(req.body.idr, req.body.action, req.body.data);
res.writeHead(200);
res.end("OKTHXBI");
}); |
| manually flush any waiting synclets, useful for debugging/testing | locker.get('/flush', function(req, res) {
res.send(true);
syncManager.flushTolerance(function(err){
if(err) logger.error("got error when flushing synclets: "+err);
}, req.query.force);
});
locker.use(express.static(__dirname + '/static')); |
| fallback everything to the dashboard | locker.all('/dashboard*', function(req, res) {
if(!lconfig.ui || !serviceManager.map(lconfig.ui)) return res.send("no dashboard :(", 404);
req.url = '/Me/' + lconfig.ui + '/' + req.url.substring(11);
proxyRequest(req.method, req, res); |
| detect when coming back from idle, and flush any delayed synclets if configured to do so | if(locker.last && lconfig.tolerance.idle && (Date.now() - locker.last) > (lconfig.tolerance.idle * 1000)) syncManager.flushTolerance(function(err){
if(err) logger.error("got error when flushing synclets: "+err);
});
locker.last = Date.now();
});
locker.all("/socket.io*", function(req, res) {
if(!lconfig.ui || !serviceManager.map(lconfig.ui)) return res.send("no dashboard :(", 404);
req.url = '/Me/' + lconfig.ui + req.url;
proxyRequest(req.method, req, res);
});
locker.get('/', function(req, res) {
res.redirect(lconfig.externalBase + '/dashboard/');
});
locker.error(function(err, req, res, next){
if(err.stack) logger.error(err.stack); |
| console.log("locker.error"); console.log(err.stack); | if (airbrake) {
airbrake.notify(err, function(err, url) {
if (url) logger.error(url);
});
}
res.send("Something went wrong.", 500);
});
require('./webservice-push')(locker);
function proxied(method, svc, ppath, req, res, buffer) {
svc.last = Date.now();
if(ppath.substr(0,1) != "/") ppath = "/"+ppath;
logger.verbose("proxying " + method + " " + req.url + " to "+ svc.uriLocal + ppath);
req.url = ppath;
proxy.proxyRequest(req, res, {
host: url.parse(svc.uriLocal).hostname,
port: url.parse(svc.uriLocal).port,
buffer: buffer
});
}
locker.initAirbrake = function(key) {
airbrake = require('airbrake').createClient(key);
};
exports.startService = function(port, ip, cb) {
locker.listen(port, ip, function(){
cb(locker);
});
};
|