2015-02-10 19:41:24 +00:00
|
|
|
"use strict";
|
|
|
|
|
|
|
|
plugin.consumes = [
|
|
|
|
"api",
|
|
|
|
"passport",
|
|
|
|
"connect",
|
|
|
|
"connect.render",
|
|
|
|
"connect.render.ejs",
|
|
|
|
"connect.remote-address",
|
2015-06-19 11:17:56 +00:00
|
|
|
"vfs.cache",
|
|
|
|
"analytics"
|
2015-02-10 19:41:24 +00:00
|
|
|
];
|
|
|
|
plugin.provides = [
|
|
|
|
"vfs.server"
|
|
|
|
];
|
|
|
|
|
|
|
|
module.exports = plugin;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* VFS session:
|
|
|
|
* - unique vfsid
|
|
|
|
* - bound to the client sessionId
|
|
|
|
* - either readonly or read/write
|
|
|
|
* - auto disposes after N seconds of idle time
|
|
|
|
* - keeps:
|
|
|
|
* - vfs-ssh instance
|
|
|
|
* - engine.io instance (only one socket connected at a time)
|
|
|
|
* - vfs rest API instance
|
|
|
|
*
|
|
|
|
* - authentication using tokens or auth headers (no cookies)
|
|
|
|
*/
|
|
|
|
function plugin(options, imports, register) {
|
|
|
|
var api = imports.api;
|
|
|
|
var cache = imports["vfs.cache"];
|
|
|
|
var passport = imports.passport;
|
|
|
|
var connect = imports.connect;
|
|
|
|
var render = imports["connect.render"];
|
2015-06-19 11:17:56 +00:00
|
|
|
var analytics = imports["analytics"];
|
2015-06-20 18:52:44 +00:00
|
|
|
var async = require("async");
|
2015-02-10 19:41:24 +00:00
|
|
|
|
|
|
|
var Types = require("frontdoor").Types;
|
|
|
|
var error = require("http-error");
|
|
|
|
var kaefer = require("kaefer");
|
|
|
|
var ratelimit = require("c9/ratelimit");
|
|
|
|
var requestTimeout = require("c9/request_timeout");
|
|
|
|
|
|
|
|
var section = api.section("vfs");
|
|
|
|
|
2015-06-19 11:17:56 +00:00
|
|
|
var VFS_ACTIVITY_WINDOW = 1000 * 60 * 60;
|
2015-02-10 19:41:24 +00:00
|
|
|
|
|
|
|
section.registerType("vfsid", new Types.RegExp(/[a-zA-Z0-9]{16}/));
|
|
|
|
section.registerType("pid", new Types.Number(0));
|
|
|
|
|
|
|
|
// admin interface
|
|
|
|
api.use(render.setTemplatePath(__dirname + "/views"));
|
|
|
|
api.get("/:status", {
|
|
|
|
params: {
|
|
|
|
status: {
|
|
|
|
type: /^vfs(?:\.(?:json|html))?$/,
|
|
|
|
source: "url"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}, [
|
|
|
|
api.ensureAdmin(),
|
|
|
|
function(req, res, next) {
|
|
|
|
var type = req.params.status.split(".")[1] || "html";
|
|
|
|
|
|
|
|
var entries = cache.getAll();
|
|
|
|
var data = {
|
|
|
|
entries: []
|
|
|
|
};
|
|
|
|
for (var key in entries) {
|
|
|
|
var entry = entries[key];
|
|
|
|
|
|
|
|
data.entries.push({
|
|
|
|
vfsid: entry.vfsid,
|
|
|
|
pid: entry.pid,
|
|
|
|
uid: entry.user.id,
|
|
|
|
ttl: entry.ttl,
|
|
|
|
readonly: entry.vfs ? entry.vfs.readonly : "",
|
|
|
|
state: entry.vfs ? "connected" : "connecting",
|
|
|
|
startTime: entry.startTime,
|
|
|
|
connectTime: entry.connectTime || -1
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
if (type == "json")
|
|
|
|
res.json(data);
|
|
|
|
else
|
|
|
|
res.render("status.html.ejs", data, next);
|
|
|
|
}
|
|
|
|
]);
|
|
|
|
|
|
|
|
// creates a new connection for the specified project
|
|
|
|
section.post("/:pid", {
|
|
|
|
params: {
|
|
|
|
"pid": {
|
|
|
|
type: "pid"
|
|
|
|
},
|
|
|
|
"version": {
|
|
|
|
type: "string",
|
|
|
|
source: "body",
|
|
|
|
optional: false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}, [
|
|
|
|
api.authenticate(),
|
|
|
|
ratelimit("pid", 60 * 1000, 30),
|
|
|
|
function(req, res, next) {
|
|
|
|
var pid = req.params.pid;
|
|
|
|
var version = req.params.version;
|
|
|
|
var user = req.user;
|
|
|
|
|
2015-09-15 01:17:33 +00:00
|
|
|
trackActivity(user, req);
|
2015-06-20 18:54:31 +00:00
|
|
|
|
2015-02-10 19:41:24 +00:00
|
|
|
if (version != kaefer.version.protocol) {
|
|
|
|
var err = new error.PreconditionFailed("Wrong VFS protocol version. Expected version '" + kaefer.version.protocol + "' but found '" + version + "'");
|
|
|
|
err.subtype = "protocol_mismatch";
|
|
|
|
err.clientVersion = version;
|
|
|
|
err.serverVersion = kaefer.version.protocol;
|
|
|
|
return next(err);
|
|
|
|
}
|
|
|
|
|
|
|
|
var done = false;
|
|
|
|
var cancel = cache.create(pid, user, function(err, entry) {
|
|
|
|
if (done) return;
|
|
|
|
if (err) return next(err);
|
|
|
|
|
|
|
|
res.json({
|
|
|
|
pid: pid,
|
|
|
|
uid: user.id,
|
|
|
|
readonly: entry.vfs.readonly,
|
|
|
|
vfsid: entry.vfsid
|
|
|
|
}, null, 201);
|
|
|
|
});
|
|
|
|
|
|
|
|
// if the clients aborts the request we have to kill the ssh process
|
|
|
|
req.on("close", function() {
|
|
|
|
done = true;
|
|
|
|
cancel();
|
|
|
|
res.json({}, 0, 500);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
]);
|
|
|
|
|
|
|
|
// checks if the connection exists and returns connection meta data
|
|
|
|
section.get("/:pid/:vfsid", {
|
|
|
|
params: {
|
|
|
|
"pid": {
|
|
|
|
type: "pid"
|
|
|
|
},
|
|
|
|
"vfsid": {
|
|
|
|
type: "vfsid"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}, function(req, res, next) {
|
|
|
|
var pid = req.params.pid;
|
|
|
|
var vfsid = req.params.vfsid;
|
|
|
|
|
|
|
|
var entry = cache.get(vfsid);
|
|
|
|
if (!entry) {
|
|
|
|
var err = new error.PreconditionFailed("VFS connection does not exist");
|
|
|
|
err.code = 499;
|
|
|
|
return next(err);
|
|
|
|
}
|
|
|
|
|
|
|
|
res.json({
|
|
|
|
pid: pid,
|
|
|
|
vfsid: vfsid,
|
|
|
|
uid: entry.user.id
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
// read only rest interface
|
|
|
|
section.get("/:pid/plugins/:access_token/:path*", {
|
|
|
|
"access_token": {
|
|
|
|
type: "string"
|
|
|
|
},
|
|
|
|
"pid": {
|
|
|
|
type: "pid"
|
|
|
|
},
|
|
|
|
"path": {
|
|
|
|
type: "string"
|
|
|
|
}
|
|
|
|
}, [
|
|
|
|
requestTimeout(15*60*1000),
|
|
|
|
connect.getModule().compress(),
|
|
|
|
function(req, res, next) {
|
|
|
|
req.query = {
|
|
|
|
access_token: req.params["access_token"]
|
|
|
|
};
|
|
|
|
passport.authenticate("bearer", { session: false }, function(err, user) {
|
|
|
|
if (err) return next(err);
|
|
|
|
|
|
|
|
req.user = user || { id: -1};
|
|
|
|
next();
|
|
|
|
})(req, res, next);
|
|
|
|
},
|
|
|
|
function(req, res, next) {
|
|
|
|
var pid = req.params.pid;
|
|
|
|
var path = req.params.path;
|
|
|
|
var user = req.user;
|
|
|
|
|
2015-09-15 01:17:33 +00:00
|
|
|
trackActivity(user, req);
|
2015-06-20 18:54:31 +00:00
|
|
|
|
2015-02-10 19:41:24 +00:00
|
|
|
if (path.indexOf("../") !== -1)
|
|
|
|
return next(new error.BadRequest("invalid path"));
|
|
|
|
|
|
|
|
cache.readonlyRest(pid, user, "/.c9/plugins/" + path, "home", req, res, next);
|
|
|
|
}
|
|
|
|
]);
|
|
|
|
|
|
|
|
section.get("/:pid/preview/:path*", {
|
|
|
|
"pid": {
|
|
|
|
type: "pid"
|
|
|
|
},
|
|
|
|
"path": {
|
|
|
|
type: "string"
|
|
|
|
}
|
|
|
|
}, [
|
|
|
|
requestTimeout(15*60*1000),
|
|
|
|
connect.getModule().compress(),
|
|
|
|
function(req, res, next) {
|
|
|
|
passport.authenticate("bearer", { session: false }, function(err, user) {
|
|
|
|
if (err) return next(err);
|
|
|
|
|
|
|
|
req.user = user || { id: -1};
|
|
|
|
next();
|
|
|
|
})(req, res, next);
|
|
|
|
},
|
|
|
|
function(req, res, next) {
|
|
|
|
var pid = req.params.pid;
|
|
|
|
var path = req.params.path;
|
|
|
|
var user = req.user;
|
|
|
|
|
|
|
|
cache.readonlyRest(pid, user, path, "workspace", req, res, next);
|
|
|
|
}
|
|
|
|
]);
|
|
|
|
|
|
|
|
// disconnects VFS connection
|
|
|
|
section.delete("/:pid/:vfsid", {
|
|
|
|
params: {
|
|
|
|
"pid": {
|
|
|
|
type: "pid"
|
|
|
|
},
|
|
|
|
"vfsid": {
|
|
|
|
type: "vfsid"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}, function(req, res, next) {
|
|
|
|
var vfsid = req.params.vfsid;
|
|
|
|
|
|
|
|
cache.remove(vfsid);
|
|
|
|
res.json({}, null, 201);
|
|
|
|
});
|
|
|
|
|
|
|
|
// REST API
|
|
|
|
// serves all files with mime type "text/plain"
|
|
|
|
// real mime type will be in "X-C9-ContentType"
|
|
|
|
section.all("/:pid/:vfsid/:scope/:path*", {
|
|
|
|
params: {
|
|
|
|
"pid": {
|
|
|
|
type: "pid"
|
|
|
|
},
|
|
|
|
"vfsid": {
|
|
|
|
type: "vfsid"
|
|
|
|
},
|
|
|
|
"scope": {
|
|
|
|
type: /^(home|workspace)$/
|
|
|
|
},
|
|
|
|
"path": {
|
|
|
|
type: "string"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}, [
|
|
|
|
function(req, res, next) {
|
|
|
|
var vfsid = req.params.vfsid;
|
|
|
|
var scope = req.params.scope;
|
|
|
|
var path = req.params.path;
|
|
|
|
|
|
|
|
var entry = cache.get(vfsid);
|
|
|
|
if (!entry) {
|
|
|
|
var err = new error.PreconditionFailed("VFS connection does not exist");
|
|
|
|
err.code = 499;
|
|
|
|
return next(err);
|
|
|
|
}
|
2015-06-20 07:04:31 +00:00
|
|
|
// TODO: use an interval to make sure this fires
|
|
|
|
// even when this REST api is not used for a day
|
2015-09-15 01:17:33 +00:00
|
|
|
trackActivity(entry.user, req);
|
2015-02-10 19:41:24 +00:00
|
|
|
entry.vfs.handleRest(scope, path, req, res, next);
|
|
|
|
}
|
|
|
|
]);
|
|
|
|
|
|
|
|
// engine.io endpoint of the VFS server
|
|
|
|
section.all("/:pid/:vfsid/socket/:path*", {
|
|
|
|
params: {
|
|
|
|
"pid": {
|
|
|
|
type: "pid"
|
|
|
|
},
|
|
|
|
"vfsid": {
|
|
|
|
type: "vfsid"
|
|
|
|
},
|
|
|
|
"path": {
|
|
|
|
type: "string"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}, [
|
|
|
|
requestTimeout(15*60*1000),
|
|
|
|
function handleEngine(req, res, next) {
|
|
|
|
var vfsid = req.params.vfsid;
|
|
|
|
|
|
|
|
var entry = cache.get(vfsid);
|
|
|
|
if (!entry) {
|
|
|
|
var err = new error.PreconditionFailed("VFS connection does not exist");
|
|
|
|
err.code = 499;
|
|
|
|
return next(err);
|
|
|
|
}
|
2015-06-19 11:17:56 +00:00
|
|
|
|
2015-02-10 19:41:24 +00:00
|
|
|
entry.vfs.handleEngine(req, res, next);
|
|
|
|
}
|
|
|
|
]);
|
2015-06-19 11:17:56 +00:00
|
|
|
|
2015-09-15 01:17:33 +00:00
|
|
|
function trackActivity(user, req) {
|
2015-06-20 18:52:44 +00:00
|
|
|
if (user.id === -1)
|
|
|
|
return;
|
|
|
|
|
2015-06-19 11:43:12 +00:00
|
|
|
if (new Date(user.lastVfsAccess).getDate() != new Date().getDate() ||
|
|
|
|
Date.now() > user.lastVfsAccess + VFS_ACTIVITY_WINDOW) {
|
2015-09-15 01:22:26 +00:00
|
|
|
|
2015-09-16 08:32:08 +00:00
|
|
|
analytics.superagent && analytics.superagent
|
2015-09-15 01:17:33 +00:00
|
|
|
.post(options.apiBaseUrl + "/metric/usage/" + req.params.pid + "?access_token=" + req.query.access_token)
|
|
|
|
.end(function() {});
|
|
|
|
|
2015-06-19 11:43:12 +00:00
|
|
|
user.lastVfsAccess = Date.now();
|
|
|
|
user.save(function() {});
|
|
|
|
}
|
2015-06-19 11:17:56 +00:00
|
|
|
}
|
2015-02-10 19:41:24 +00:00
|
|
|
|
|
|
|
register(null, {
|
|
|
|
"vfs.server": {
|
|
|
|
get section() { return section; }
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|