Adafruit-WebIDE/server.js

564 lines
17 KiB
JavaScript

var express = require('express'),
session = require('express-session'),
tty = require('tty.js'),
app = express(),
util = require('util'),
io = require('socket.io'),
passport = require('passport'),
util = require('util'),
BitbucketStrategy = require('passport-bitbucket').Strategy,
GitHubStrategy = require('passport-github').Strategy,
site = require('./controllers/site'),
editor = require('./controllers/editor'),
user = require('./controllers/user'),
jsDAV = require("jsDAV/lib/jsdav"),
fs = require('fs'),
path = require('path'),
updater = require('./helpers/updater'),
scheduler = require('./helpers/scheduler'),
editor_setup = require('./helpers/editor_setup'),
git_helper = require('./helpers/git_helper'),
exec_helper = require('./helpers/exec_helper'),
fs_helper = require('./helpers/fs_helper'),
exec_helper = require('./helpers/exec_helper'),
request_helper = require('./helpers/request_helper'),
debug_helper = require('./helpers/python/debug_helper'),
RedisStore = require('connect-redis')(session),
redis = require("redis"),
client = redis.createClient(),
config = require('./config/config'),
winston = require('winston');
var davServer,
HOSTNAME,
IS_PASSPORT_SETUP = false,
REPOSITORY_PATH = path.resolve(__dirname + "/repositories");
winston.info("REPOSITORY_PATH", REPOSITORY_PATH);
//exec_helper.spawn_ipython();
//check for the existence of the logs directory, if it doesn't
//exist, create it prior to starting the child process.
var exists = fs.existsSync(__dirname + '/logs');
if (!exists) {
fs.mkdirSync(__dirname + '/logs', 0755);
winston.info('created logs folder');
}
winston.add(winston.transports.File, { filename: __dirname + '/logs/output.log', json: false });
winston.handleExceptions(new winston.transports.File({ filename: __dirname + '/logs/errors.log', json: false }));
winston.remove(winston.transports.Console);
// Passport session setup.
// To support persistent login sessions, Passport needs to be able to
// serialize users into and deserialize users out of the session. Typically,
// this will be as simple as storing the user ID when serializing, and finding
// the user by ID when deserializing. However, since this example does not
// have a database of user records, the complete Bitbucket profile is
// serialized and deserialized.
// Use the BitbucketStrategy within Passport.
// Strategies in passport require a `verify` function, which accept
// credentials (in this case, a token, tokenSecret, and Bitbucket profile),
// and invoke a callback with a user object.
function setup_bitbucket_passport(consumer_key, consumer_secret) {
winston.info("http://" + HOSTNAME + "/auth/bitbucket/callback");
passport.use(new BitbucketStrategy({
consumerKey: consumer_key,
consumerSecret: consumer_secret,
callbackURL: "http://" + HOSTNAME + "/auth/bitbucket/callback"
},
function(token, tokenSecret, profile, done) {
// asynchronous verification, for effect...
process.nextTick(function () {
profile.token = token;
profile.token_secret = tokenSecret;
profile.consumer_key = consumer_key;
profile.consumer_secret = consumer_secret;
return done(null, profile);
});
}
));
}
// Use the GitHubStrategy within Passport.
// Strategies in passport require a `verify` function, which accept
// credentials (in this case, a token, tokenSecret, and Github profile),
// and invoke a callback with a user object.
function setup_github_passport(consumer_key, consumer_secret) {
winston.info("http://" + HOSTNAME + "/auth/github/callback");
passport.use(new GitHubStrategy({
clientID: consumer_key,
clientSecret: consumer_secret,
callbackURL: "http://" + HOSTNAME + "/auth/github/callback",
userAgent: HOSTNAME
},
function(accessToken, refreshToken, profile, done) {
// asynchronous verification, for effect...
process.nextTick(function () {
profile.token = accessToken;
profile.refresh_token = refreshToken;
profile.consumer_key = consumer_key;
profile.consumer_secret = consumer_secret;
return done(null, profile);
});
}
));
}
passport.serializeUser(function(user, done) {
client.set(user.username, JSON.stringify(user));
done(null, user.username);
});
passport.deserializeUser(function(obj, done) {
client.get(obj, function(err, reply) {
done(null, JSON.parse(reply));
});
});
//redirect anything with /filesystem in the url to the WebDav server.
app.use(function(req, res, next) {
//res.header('Cache-Control', 'no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0');
if (req.path.indexOf("/filesystem") != -1) {
davServer.exec(req, res);
} else {
next();
}
});
var sessionStore = new RedisStore();
app.set('view engine', 'jade');
app.set('views', __dirname + '/views');
app.use(express.logger());
//app.use(express.logger("dev"));
app.use(express.static(__dirname + '/public'));
app.use(express.static(__dirname + '/node_modules/tty.js/static'));
app.use(express.cookieParser());
app.use(session({
store: sessionStore,
key: 'sid',
secret: 'cat nap',
resave: true,
saveUninitialized: true
}));
app.use(express.bodyParser());
app.use(express.methodOverride());
app.use(passport.initialize());
app.use(passport.session());
app.use(function(req, res, next) {
res.locals.session = req.session;
next();
});
app.use(app.router);
app.use(errorHandler);
app.get('/', ensureAuthenticated, site.index);
app.get('/editor', ensureAuthenticated, editor.index);
app.get('/editor/image', ensureAuthenticated, editor.image);
app.post('/editor/upload', ensureAuthenticated, editor.upload_file);
app.post('/create/repository', ensureAuthenticated, editor.create_repository);
app.get('/setup', user.setup);
app.post('/setup', user.submit_setup);
app.get('/config', user.config);
app.post('/config', user.submit_config);
app.get('/set-datetime', user.set_datetime);
app.get('/login', ensureOauth, user.login);
app.get('/logout', user.logout);
// GET /auth/bitbucket
// Use passport.authenticate() as route middleware to authenticate the
// request. The first step in Bitbucket authentication will involve redirecting
// the user to bitbucket.org. After authorization, Bitbucket will redirect the user
// back to this application at /auth/bitbucket/callback
app.get('/auth/bitbucket',
passport.authenticate('bitbucket'),
function(req, res){
// The request will be redirected to Bitbucket for authentication, so this
// function will not be called.
});
// GET /auth/bitbucket/callback
// Use passport.authenticate() as route middleware to authenticate the
// request. If authentication fails, the user will be redirected back to the
// login page. Otherwise, the primary route function function will be calsled,
// which, in this example, will redirect the user to the home page.
app.get('/auth/bitbucket/callback',
passport.authenticate('bitbucket', { failureRedirect: '/login' }),
function(req, res) {
res.redirect('/editor');
});
// GET /auth/github
app.get('/auth/github',
passport.authenticate('github'),
function(req, res){
// The request will be redirected to Github for authentication, so this
// function will not be called.
});
// GET /auth/github/callback
app.get('/auth/github/callback',
passport.authenticate('github', { failureRedirect: '/login' }),
function(req, res) {
res.redirect('/editor');
});
serverInitialization(app);
function errorHandler(err, req, res, next) {
winston.error(err);
if (err.name === "InternalOAuthError") {
res.status(500);
res.render('oauth_error', { error: err });
} else {
res.status(500);
res.render('error', { error: err });
}
}
// Simple route middleware to ensure user is authenticated.
// Use this route middleware on any resource that needs to be protected. If
// the request is authenticated (typically via a persistent login session),
// the request will proceed. Otherwise, the user will be redirected to the
// login page.
function ensureAuthenticated(req, res, next) {
setHostName(req);
if (config.editor.offline) {
req.user = { provider: 'offline',
username: 'offline user' };
return next();
}
function authRoute(req, res, next) {
if (req.isAuthenticated()) {
return next();
}
if (!IS_PASSPORT_SETUP) {
res.redirect('/setup');
} else {
res.redirect('/login');
}
}
//use the correct key for redis, either github or bitbucket
var oauth_key = 'bitbucket_oauth';
if (config.editor.github) {
oauth_key = 'github_oauth';
}
if (!IS_PASSPORT_SETUP) {
//need to setup passport on server startup, if the oauth is already setup
client.hgetall(oauth_key, function (err, oauth) {
if (oauth) {
if (config.editor.github) {
setup_github_passport(oauth.consumer_key, oauth.consumer_secret);
} else {
setup_bitbucket_passport(oauth.consumer_key, oauth.consumer_secret);
}
IS_PASSPORT_SETUP = true;
}
authRoute(req, res, next);
});
} else {
authRoute(req, res, next);
}
}
function ensureOauth(req, res, next) {
setHostName(req);
if (config.editor.offline) {
req.user = { provider: 'offline',
username: 'offline user' };
return next();
}
//use the correct key for redis, either github or bitbucket
var oauth_key = 'bitbucket_oauth';
if (config.editor.github) {
oauth_key = 'github_oauth';
}
client.hgetall(oauth_key, function (err, oauth) {
if (!oauth) {
res.redirect('/setup');
} else {
if (config.editor.github) {
setup_github_passport(oauth.consumer_key, oauth.consumer_secret);
} else {
setup_bitbucket_passport(oauth.consumer_key, oauth.consumer_secret);
}
if (!IS_PASSPORT_SETUP) {
IS_PASSPORT_SETUP = true;
res.redirect('/login');
} else {
next();
}
}
});
}
function setHostName(req) {
//set it each time, it's quick, and hostname may change (internal IP vs external IP).
HOSTNAME = req.headers.host;
}
function serverInitialization(app) {
//setup repositories path
var exists = fs.existsSync(REPOSITORY_PATH);
if (!exists) {
fs.mkdirSync(REPOSITORY_PATH, 0777);
winston.info('created repositories folder');
}
//setup symlink to webide home, if it exists:
var has_webide_path = fs.existsSync("/home/webide");
if (has_webide_path) {
//Creating symbolic link to repositories path
winston.info('Linked repository paths: /home/webide/repositories');
if (!fs.existsSync("/home/webide/repositories")) {
fs.symlinkSync(REPOSITORY_PATH, "/home/webide/repositories", 'dir');
}
}
scheduler.initialize_jobs();
start_server(function(server) {
socket_listeners();
mount_dav(server);
});
}
function start_server(cb) {
server = require('http').createServer(app);
io = io.listen(server);
io.configure(function() {
io.enable('browser client minification');
io.enable('browser client etag');
io.set('transports', ['websocket', 'htmlfile', 'xhr-polling', 'jsonp-polling']);
});
new tty.Server(config.term, app, server, io);
client.hgetall('server', function (err, server_data) {
var port;
if (server_data && server_data.port) {
port = server_data.port;
} else {
port = config.editor.port;
}
if (server_data && server_data.offline) {
config.editor.offline = (server_data.offline == 1) ? true : false;
}
if (server_data && server_data.github) {
config.editor.github = (server_data.github == 1) ? true : false;
}
winston.info('listening on port ' + port);
cb(server.listen(port));
});
}
function socket_listeners() {
io.sockets.authorization(function(handshakeData, callback) {
if (!handshakeData.headers.cookie) return callback('socket.io: cookie not found.', false);
var signedCookies = require('cookie').parse(handshakeData.headers.cookie);
handshakeData.cookies = require('connect/lib/utils').parseSignedCookies(signedCookies, 'cat nap');
sessionStore.get(handshakeData.cookies['sid'], function(err, session) {
if (config.editor.offline) {
handshakeData.session = { provider: 'offline', username: 'offline user' };
return callback(null, true);
} else {
client.get(session.passport.user, function(err, user) {
if (err || !session) return callback('socket.io: session not found.', false);
handshakeData.session = JSON.parse(user);
if (handshakeData.session) {
return callback(null, true);
} else {
return callback('socket.io: session user not found', false);
}
});
}
});
});
io.sockets.on('connection', function (socket) {
socket.set('username', socket.handshake.session.username);
//emit on first connection
socket.emit('cwd-init', {dirname: REPOSITORY_PATH});
scheduler.emit_scheduled_jobs(socket.handshake.session.username, socket);
socket.on('disconnect', function() {
debug_helper.client_disconnect();
debug_helper.kill_debug();
});
//listen for events
socket.on('git-delete', function(data) {
git_helper.remove_commit_push(data.file, socket.handshake.session, function(err, status) {
socket.emit('git-delete-complete', {err: err, status: status});
});
});
//listen for events
socket.on('git-pull', function(data) {
console.log(data);
var name = data.file ? data.file.name : "";
git_helper.pull(name, "origin", "master", function(err, status) {
socket.emit('git-pull-complete', {err: err, status: status});
});
});
//listen for events
socket.on('git-is-modified', function(data) {
git_helper.is_modified(data.file, function(err, status) {
socket.emit('git-is-modified-complete', {is_modified: status});
});
});
socket.on('commit-file', function (data) {
var commit_message = "";
if (data.message) {
commit_message = data.message;
} else {
commit_message = "Modified " + data.file.name;
}
git_helper.commit_push_and_save(data.file, commit_message, socket.handshake.session, function(err, status) {
socket.emit('commit-file-complete', {err: err, status: status});
});
});
socket.on('move-file', function (data) {
git_helper.move_commit_push(data.file, socket.handshake.session, function(err) {
console.log('move-file', err);
socket.emit('move-file-complete', {err: err});
});
});
socket.on('self-check-request', function() {
editor_setup.health_check(socket, socket.handshake.session);
});
socket.on('editor-check-updates', function() {
updater.check_for_updates(socket);
});
socket.on('editor-update', function() {
updater.update(socket);
});
socket.on('trace-file', function(data) {
exec_helper.trace_program(data.file, socket);
});
socket.on('debug-command', function(data) {
debug_helper.debug_command(data, socket);
});
socket.on('debug-file', function(data) {
debug_helper.start_debug(data.file, socket);
});
socket.on('commit-run-file', function(data) {
if (data && data.file) {
data.file.username = socket.handshake.session.username;
}
exec_helper.execute_program(data.file, false);
git_helper.commit_push_and_save(data.file, "Modified " + data.file.name, socket.handshake.session, function(err, status) {
socket.emit('commit-file-complete', {message: "Save was successful"});
});
});
socket.on('stop-script-execution', function(data) {
exec_helper.stop_program(data.file, false);
});
socket.on('submit-schedule', function(schedule) {
scheduler.add_schedule(schedule, socket, socket.handshake.session);
});
socket.on('schedule-delete-job', function(key) {
scheduler.delete_job(key, socket, socket.handshake.session);
});
socket.on('schedule-toggle-job', function(key) {
scheduler.toggle_job(key, socket, socket.handshake.session);
});
socket.on('set-settings', function(value) {
client.hmset("editor:settings", value, function(err) {
if (err) winston.error(err);
});
});
});
}
io.sockets.on('disconnect', function(socket) {
exec_helper.set_sockets(io.sockets.sockets);
});
function mount_dav(server) {
var jsDAV_Tree_Filesystem = require("jsDAV/lib/DAV/tree/filesystem").jsDAV_Tree_Filesystem;
//jsDAV.debugMode = true;
davServer = jsDAV.mount({
path: REPOSITORY_PATH,
mount: '/filesystem',
plugins: ["codesearch", "tree", "filelist", "filesearch", "locks", "mount", "temporaryfilefilter"],
server: server,
standalone: false,
tree: new jsDAV_Tree_Filesystem(REPOSITORY_PATH)
});
winston.info('webdav filesystem mounted');
}
exports.get_socket = function (username, cb) {
for (var socketId in io.sockets.sockets) {
io.sockets.sockets[socketId].get('username', function(err, sock_username) {
if (username === sock_username) {
cb(io.sockets.sockets[socketId]);
}
});
}
};
process.on('SIGINT', function() {
winston.info("\nShutting down from SIGINT");
// some other closing procedures go here
debug_helper.kill_debug(false, function() {
//no need to wait for this
});
process.exit();
});
process.on('uncaughtException', function(err) {
debug_helper.kill_debug(false, function() {
//no need to wait for this
});
});