diff options
Diffstat (limited to 'lib')
-rw-r--r-- | lib/auth.js | 49 | ||||
-rw-r--r-- | lib/db.js | 146 | ||||
-rw-r--r-- | lib/note.js | 60 | ||||
-rw-r--r-- | lib/realtime.js | 392 | ||||
-rw-r--r-- | lib/response.js | 211 | ||||
-rw-r--r-- | lib/user.js | 83 |
6 files changed, 941 insertions, 0 deletions
diff --git a/lib/auth.js b/lib/auth.js new file mode 100644 index 00000000..e7b0dc7c --- /dev/null +++ b/lib/auth.js @@ -0,0 +1,49 @@ +//auth +//external modules +var passport = require('passport'); +var FacebookStrategy = require('passport-facebook').Strategy; +var TwitterStrategy = require('passport-twitter').Strategy; +var GithubStrategy = require('passport-github').Strategy; +var DropboxStrategy = require('passport-dropbox-oauth2').Strategy; + +//core +var User = require('./user.js') +var config = require('../config.js') + +function callback(accessToken, refreshToken, profile, done) { + //console.log(profile.displayName || profile.username); + User.findOrNewUser(profile.id, profile, function (err, user) { + if (err || user == null) { + console.log('auth callback failed: ' + err); + } else { + if(config.debug && user) + console.log('user login: ' + user._id); + done(null, user); + } + }); +} + +//facebook +module.exports = passport.use(new FacebookStrategy({ + clientID: config.facebook.clientID, + clientSecret: config.facebook.clientSecret, + callbackURL: config.domain + config.facebook.callbackPath +}, callback)); +//twitter +passport.use(new TwitterStrategy({ + consumerKey: config.twitter.consumerKey, + consumerSecret: config.twitter.consumerSecret, + callbackURL: config.domain + config.twitter.callbackPath +}, callback)); +//github +passport.use(new GithubStrategy({ + clientID: config.github.clientID, + clientSecret: config.github.clientSecret, + callbackURL: config.domain + config.github.callbackPath +}, callback)); +//dropbox +passport.use(new DropboxStrategy({ + clientID: config.dropbox.clientID, + clientSecret: config.dropbox.clientSecret, + callbackURL: config.domain + config.dropbox.callbackPath +}, callback));
\ No newline at end of file diff --git a/lib/db.js b/lib/db.js new file mode 100644 index 00000000..d763eb83 --- /dev/null +++ b/lib/db.js @@ -0,0 +1,146 @@ +//db +//external modules +var pg = require('pg'); +var fs = require('fs'); +var util = require('util'); + +//core +var config = require("../config.js"); + +//public +var db = { + readFromFile: readFromDB, + saveToFile: saveToFile, + newToDB: newToDB, + readFromDB: readFromDB, + saveToDB: saveToDB, + countFromDB: countFromDB +}; + +function getDBClient() { + if (config.debug) + return new pg.Client(config.postgresqlstring); + else + return new pg.Client(process.env.DATABASE_URL); +} + +function readFromFile(callback) { + fs.readFile('hackmd', 'utf8', function (err, data) { + if (err) throw err; + callback(data); + }); +} + +function saveToFile(doc) { + fs.writeFile('hackmd', doc, function (err) { + if (err) throw err; + }); +} + +var updatequery = "UPDATE notes SET title='%s', content='%s', update_time=NOW() WHERE id='%s';"; +var insertquery = "INSERT INTO notes (id, owner, content) VALUES ('%s', '%s', '%s');"; +var insertifnotexistquery = "INSERT INTO notes (id, owner, content) \ +SELECT '%s', '%s', '%s' \ +WHERE NOT EXISTS (SELECT 1 FROM notes WHERE id='%s') RETURNING *;"; +var selectquery = "SELECT * FROM notes WHERE id='%s';"; +var countquery = "SELECT count(*) FROM notes;"; + +function newToDB(id, owner, body, callback) { + var client = getDBClient(); + client.connect(function (err) { + if (err) { + callback(err, null); + return console.error('could not connect to postgres', err); + } + var newnotequery = util.format(insertquery, id, owner, body); + //console.log(newnotequery); + client.query(newnotequery, function (err, result) { + if (err) { + callback(err, null); + return console.error("new note to db failed: " + err); + } else { + if (config.debug) + console.log("new note to db success"); + callback(null, result); + client.end(); + } + }); + }); +} + +function readFromDB(id, callback) { + var client = getDBClient(); + client.connect(function (err) { + if (err) { + callback(err, null); + return console.error('could not connect to postgres', err); + } + var readquery = util.format(selectquery, id); + //console.log(readquery); + client.query(readquery, function (err, result) { + if (err) { + callback(err, null); + return console.error("read from db failed: " + err); + } else { + //console.log(result.rows); + if (result.rows.length <= 0) { + callback("not found note in db", null); + } else { + console.log("read from db success"); + callback(null, result); + client.end(); + } + } + }); + }); +} + +function saveToDB(id, title, data, callback) { + var client = getDBClient(); + client.connect(function (err) { + if (err) { + callback(err, null); + return console.error('could not connect to postgres', err); + } + var savequery = util.format(updatequery, title, data, id); + //console.log(savequery); + client.query(savequery, function (err, result) { + if (err) { + callback(err, null); + return console.error("save to db failed: " + err); + } else { + if (config.debug) + console.log("save to db success"); + callback(null, result); + client.end(); + } + }); + }); +} + +function countFromDB(callback) { + var client = getDBClient(); + client.connect(function (err) { + if (err) { + callback(err, null); + return console.error('could not connect to postgres', err); + } + client.query(countquery, function (err, result) { + if (err) { + callback(err, null); + return console.error("count from db failed: " + err); + } else { + //console.log(result.rows); + if (result.rows.length <= 0) { + callback("not found note in db", null); + } else { + console.log("count from db success"); + callback(null, result); + client.end(); + } + } + }); + }); +} + +module.exports = db;
\ No newline at end of file diff --git a/lib/note.js b/lib/note.js new file mode 100644 index 00000000..1212e1a6 --- /dev/null +++ b/lib/note.js @@ -0,0 +1,60 @@ +//note +//external modules +var LZString = require('lz-string'); +var marked = require('marked'); +var cheerio = require('cheerio'); + +//others +var db = require("./db.js"); + +//public +var note = { + checkNoteIdValid: checkNoteIdValid, + checkNoteExist: checkNoteExist, + getNoteTitle: getNoteTitle +}; + +function checkNoteIdValid(noteId) { + try { + //console.log(noteId); + var id = LZString.decompressFromBase64(noteId); + if (!id) return false; + var uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; + var result = id.match(uuidRegex); + if (result && result.length == 1) + return true; + else + return false; + } catch (err) { + console.error(err); + return false; + } +} + +function checkNoteExist(noteId) { + try { + //console.log(noteId); + var id = LZString.decompressFromBase64(noteId); + db.readFromDB(id, function (err, result) { + if (err) return false; + return true; + }); + } catch (err) { + console.error(err); + return false; + } +} + +//get title +function getNoteTitle(body) { + var $ = cheerio.load(marked(body)); + var h1s = $("h1"); + var title = ""; + if (h1s.length > 0) + title = h1s.first().text(); + else + title = "Untitled"; + return title; +} + +module.exports = note;
\ No newline at end of file diff --git a/lib/realtime.js b/lib/realtime.js new file mode 100644 index 00000000..303eb6a6 --- /dev/null +++ b/lib/realtime.js @@ -0,0 +1,392 @@ +//realtime +//external modules +var cookie = require('cookie'); +var cookieParser = require('cookie-parser'); +var url = require('url'); +var async = require('async'); +var LZString = require('lz-string'); +var shortId = require('shortid'); +var randomcolor = require("randomcolor"); + +//core +var config = require("../config.js"); + +//others +var db = require("./db.js"); +var Note = require("./note.js"); +var User = require("./user.js"); + +//public +var realtime = { + secure: secure, + connection: connection, + getStatus: getStatus +}; + +function secure(socket, next) { + try { + var handshakeData = socket.request; + if (handshakeData.headers.cookie) { + handshakeData.cookie = cookie.parse(handshakeData.headers.cookie); + handshakeData.sessionID = cookieParser.signedCookie(handshakeData.cookie[config.sessionname], config.sessionsecret); + if (handshakeData.cookie[config.sessionname] == handshakeData.sessionID) { + next(new Error('AUTH failed: Cookie is invalid.')); + } + } else { + next(new Error('AUTH failed: No cookie transmitted.')); + } + if (config.debug) + console.log("AUTH success cookie: " + handshakeData.sessionID); + + next(); + } catch (ex) { + next(new Error("AUTH failed:" + JSON.stringify(ex))); + } +} + +//actions +var users = {}; +var notes = {}; +var updater = setInterval(function () { + async.each(Object.keys(notes), function (key, callback) { + var note = notes[key]; + if (note.isDirty) { + if (config.debug) + console.log("updater found dirty note: " + key); + var title = Note.getNoteTitle(LZString.decompressFromBase64(note.body)); + db.saveToDB(key, title, note.body, + function (err, result) {}); + note.isDirty = false; + } + callback(); + }, function (err) { + if (err) return console.error('updater error', err); + }); +}, 5000); + +function getStatus(callback) { + db.countFromDB(function (err, data) { + if (err) return console.log(err); + var regusers = 0; + var distinctregusers = 0; + var distinctaddresses = []; + Object.keys(users).forEach(function (key) { + var value = users[key]; + if(value.login) + regusers++; + var found = false; + for (var i = 0; i < distinctaddresses.length; i++) { + if (value.address == distinctaddresses[i]) { + found = true; + break; + } + } + if (!found) + distinctaddresses.push(value.address); + if(!found && value.login) + distinctregusers++; + }); + User.getUserCount(function (err, regcount) { + if (err) { + console.log('get status failed: ' + err); + return; + } + if (callback) + callback({ + onlineNotes: Object.keys(notes).length, + onlineUsers: Object.keys(users).length, + distinctOnlineUsers: distinctaddresses.length, + notesCount: data.rows[0].count, + registeredUsers: regcount, + onlineRegisteredUsers: regusers, + distinctOnlineRegisteredUsers: distinctregusers + }); + }); + }); +} + +function getNotenameFromSocket(socket) { + var hostUrl = url.parse(socket.handshake.headers.referer); + var notename = hostUrl.pathname.split('/')[1]; + if (notename == config.featuresnotename) { + return notename; + } + if (!Note.checkNoteIdValid(notename)) { + socket.emit('info', { + code: 404 + }); + return socket.disconnect(); + } + notename = LZString.decompressFromBase64(notename); + return notename; +} + +function emitOnlineUsers(socket) { + var notename = getNotenameFromSocket(socket); + if (!notename || !notes[notename]) return; + var users = []; + Object.keys(notes[notename].users).forEach(function (key) { + var user = notes[notename].users[key]; + if (user) + users.push({ + id: user.id, + color: user.color, + cursor: user.cursor + }); + }); + notes[notename].socks.forEach(function (sock) { + sock.emit('online users', { + count: notes[notename].socks.length, + users: users + }); + }); +} + +var isConnectionBusy = false; +var connectionSocketQueue = []; +var isDisconnectBusy = false; +var disconnectSocketQueue = []; + +function finishConnection(socket, notename) { + notes[notename].users[socket.id] = users[socket.id]; + notes[notename].socks.push(socket); + emitOnlineUsers(socket); + socket.emit('refresh', { + body: notes[notename].body + }); + + //clear finished socket in queue + for (var i = 0; i < connectionSocketQueue.length; i++) { + if (connectionSocketQueue[i].id == socket.id) + connectionSocketQueue.splice(i, 1); + } + //seek for next socket + isConnectionBusy = false; + if (connectionSocketQueue.length > 0) + startConnection(connectionSocketQueue[0]); + + if (config.debug) { + console.log('SERVER connected a client to [' + notename + ']:'); + console.log(JSON.stringify(users[socket.id])); + //console.log(notes); + getStatus(function (data) { + console.log(JSON.stringify(data)); + }); + } +} + +function startConnection(socket) { + if (isConnectionBusy) return; + isConnectionBusy = true; + + var notename = getNotenameFromSocket(socket); + if (!notename) return; + + if (!notes[notename]) { + db.readFromDB(notename, function (err, data) { + if (err) { + socket.emit('info', { + code: 404 + }); + socket.disconnect(); + //clear err socket in queue + for (var i = 0; i < connectionSocketQueue.length; i++) { + if (connectionSocketQueue[i].id == socket.id) + connectionSocketQueue.splice(i, 1); + } + isConnectionBusy = false; + return console.error(err); + } + var body = data.rows[0].content; + notes[notename] = { + socks: [], + body: body, + isDirty: false, + users: {} + }; + finishConnection(socket, notename); + }); + } else { + finishConnection(socket, notename); + } +} + +function disconnect(socket) { + if (isDisconnectBusy) return; + isDisconnectBusy = true; + + if (config.debug) { + console.log("SERVER disconnected a client"); + console.log(JSON.stringify(users[socket.id])); + } + var notename = getNotenameFromSocket(socket); + if (!notename) return; + if (users[socket.id]) { + delete users[socket.id]; + } + if (notes[notename]) { + delete notes[notename].users[socket.id]; + var index = notes[notename].socks.indexOf(socket); + if (index > -1) { + notes[notename].socks.splice(index, 1); + } + if (Object.keys(notes[notename].users).length <= 0) { + var title = Note.getNoteTitle(LZString.decompressFromBase64(notes[notename].body)); + db.saveToDB(notename, title, notes[notename].body, + function (err, result) { + delete notes[notename]; + if (config.debug) { + //console.log(notes); + getStatus(function (data) { + console.log(JSON.stringify(data)); + }); + } + }); + } + } + emitOnlineUsers(socket); + + //clear finished socket in queue + for (var i = 0; i < disconnectSocketQueue.length; i++) { + if (disconnectSocketQueue[i].id == socket.id) + disconnectSocketQueue.splice(i, 1); + } + //seek for next socket + isDisconnectBusy = false; + if (disconnectSocketQueue.length > 0) + disconnect(disconnectSocketQueue[0]); + + if (config.debug) { + //console.log(notes); + getStatus(function (data) { + console.log(JSON.stringify(data)); + }); + } +} + + +function connection(socket) { + users[socket.id] = { + id: socket.id, + address: socket.handshake.address, + 'user-agent': socket.handshake.headers['user-agent'], + otk: shortId.generate(), + color: randomcolor({ + luminosity: 'light' + }), + cursor: null, + login: false + }; + + connectionSocketQueue.push(socket); + startConnection(socket); + + //when a new client coming or received a client refresh request + socket.on('refresh', function (body_) { + var notename = getNotenameFromSocket(socket); + if (!notename) return; + if (config.debug) + console.log('SERVER received [' + notename + '] data updated: ' + socket.id); + if (notes[notename].body != body_) { + notes[notename].body = body_; + notes[notename].isDirty = true; + } + }); + + socket.on('user status', function (data) { + if(data) + users[socket.id].login = data.login; + }); + + socket.on('online users', function () { + emitOnlineUsers(socket); + }); + + socket.on('version', function () { + socket.emit('version', config.version); + }); + + socket.on('cursor focus', function (data) { + var notename = getNotenameFromSocket(socket); + if (!notename || !notes[notename]) return; + users[socket.id].cursor = data; + notes[notename].socks.forEach(function (sock) { + if (sock != socket) { + var out = { + id: socket.id, + color: users[socket.id].color, + cursor: data + }; + sock.emit('cursor focus', out); + } + }); + }); + + socket.on('cursor activity', function (data) { + var notename = getNotenameFromSocket(socket); + if (!notename || !notes[notename]) return; + users[socket.id].cursor = data; + notes[notename].socks.forEach(function (sock) { + if (sock != socket) { + var out = { + id: socket.id, + color: users[socket.id].color, + cursor: data + }; + sock.emit('cursor activity', out); + } + }); + }); + + socket.on('cursor blur', function () { + var notename = getNotenameFromSocket(socket); + if (!notename || !notes[notename]) return; + users[socket.id].cursor = null; + notes[notename].socks.forEach(function (sock) { + if (sock != socket) { + var out = { + id: socket.id + }; + if (sock != socket) { + sock.emit('cursor blur', out); + } + } + }); + }); + + //when a new client disconnect + socket.on('disconnect', function () { + disconnectSocketQueue.push(socket); + disconnect(socket); + }); + + //when received client change data request + socket.on('change', function (op) { + var notename = getNotenameFromSocket(socket); + if (!notename) return; + op = LZString.decompressFromBase64(op); + if (op) + op = JSON.parse(op); + if (config.debug) + console.log('SERVER received [' + notename + '] data changed: ' + socket.id + ', op:' + JSON.stringify(op)); + switch (op.origin) { + case '+input': + case '+delete': + case 'paste': + case 'cut': + case 'undo': + case 'redo': + case 'drag': + notes[notename].socks.forEach(function (sock) { + if (sock != socket) { + if (config.debug) + console.log('SERVER emit sync data out [' + notename + ']: ' + sock.id + ', op:' + JSON.stringify(op)); + sock.emit('change', LZString.compressToBase64(JSON.stringify(op))); + } + }); + break; + } + }); +} + +module.exports = realtime;
\ No newline at end of file diff --git a/lib/response.js b/lib/response.js new file mode 100644 index 00000000..458ed01f --- /dev/null +++ b/lib/response.js @@ -0,0 +1,211 @@ +//response +//external modules +var ejs = require('ejs'); +var fs = require('fs'); +var path = require('path'); +var uuid = require('node-uuid'); +var markdownpdf = require("markdown-pdf"); +var LZString = require('lz-string'); + +//core +var config = require("../config.js"); + +//others +var db = require("./db.js"); +var Note = require("./note.js"); + +//public +var response = { + errorForbidden: function (res) { + res.status(403).send("Forbidden, oh no.") + }, + errorNotFound: function (res) { + responseError(res, "404", "Not Found", "oops.") + }, + errorInternalError: function (res) { + responseError(res, "500", "Internal Error", "wtf.") + }, + errorServiceUnavailable: function (res) { + res.status(503).send("I'm busy right now, try again later.") + }, + newNote: newNote, + showFeatures: showFeatures, + showNote: showNote, + noteActions: noteActions +}; + +function responseError(res, code, detail, msg) { + res.writeHead(code, { + 'Content-Type': 'text/html' + }); + var content = ejs.render(fs.readFileSync(config.errorpath, 'utf8'), { + cache: !config.debug, + filename: config.errorpath, + code: code, + detail: detail, + msg: msg + }); + res.write(content); + res.end(); +} + +function responseHackMD(res) { + res.writeHead(200, { + 'Content-Type': 'text/html' + }); + var content = ejs.render(fs.readFileSync(config.hackmdpath, 'utf8'), { + cache: !config.debug, + filename: config.hackmdpath + }); + res.write(content); + res.end(); +} + +function newNote(req, res, next) { + var newId = uuid.v4(); + var body = fs.readFileSync(config.defaultnotepath, 'utf8'); + body = LZString.compressToBase64(body); + var owner = null; + if (req.isAuthenticated()) { + owner = req.session.passport.user; + } + db.newToDB(newId, owner, body, function (err, result) { + if (err) { + responseError(res, "500", "Internal Error", "wtf."); + return; + } + res.redirect("/" + LZString.compressToBase64(newId)); + }); +} + +function showFeatures(req, res, next) { + db.readFromDB(config.featuresnotename, function (err, data) { + if (err) { + var body = fs.readFileSync(config.defaultfeaturespath, 'utf8'); + body = LZString.compressToBase64(body); + db.newToDB(config.featuresnotename, null, body, function (err, result) { + if (err) { + responseError(res, "500", "Internal Error", "wtf."); + return; + } + responseHackMD(res); + }); + } else { + responseHackMD(res); + } + }); +} + +function showNote(req, res, next) { + var noteId = req.params.noteId; + if (!Note.checkNoteIdValid(noteId)) { + responseError(res, "404", "Not Found", "oops."); + return; + } + responseHackMD(res); +} + +function actionPretty(req, res, noteId) { + db.readFromDB(noteId, function (err, data) { + if (err) { + responseError(res, "404", "Not Found", "oops."); + return; + } + var body = data.rows[0].content; + var template = config.prettypath; + var compiled = ejs.compile(fs.readFileSync(template, 'utf8')); + var origin = "//" + req.headers.host; + var html = compiled({ + url: origin, + body: body + }); + var buf = html; + res.writeHead(200, { + 'Content-Type': 'text/html; charset=UTF-8', + 'Cache-Control': 'private', + 'Content-Length': buf.length + }); + res.end(buf); + }); +} + +function actionDownload(req, res, noteId) { + db.readFromDB(noteId, function (err, data) { + if (err) { + responseError(res, "404", "Not Found", "oops."); + return; + } + var body = LZString.decompressFromBase64(data.rows[0].content); + var title = Note.getNoteTitle(body); + res.writeHead(200, { + 'Content-Type': 'text/markdown; charset=UTF-8', + 'Cache-Control': 'private', + 'Content-disposition': 'attachment; filename=' + title + '.md', + 'Content-Length': body.length + }); + res.end(body); + }); +} + +function actionPDF(req, res, noteId) { + db.readFromDB(noteId, function (err, data) { + if (err) { + responseError(res, "404", "Not Found", "oops."); + return; + } + var body = LZString.decompressFromBase64(data.rows[0].content); + var title = Note.getNoteTitle(body); + + if (!fs.existsSync(config.tmppath)) { + fs.mkdirSync(config.tmppath); + } + var path = config.tmppath + Date.now() + '.pdf'; + markdownpdf().from.string(body).to(path, function () { + var stream = fs.createReadStream(path); + var filename = title; + // Be careful of special characters + filename = encodeURIComponent(filename); + // Ideally this should strip them + res.setHeader('Content-disposition', 'attachment; filename="' + filename + '.pdf"'); + res.setHeader('Cache-Control', 'private'); + res.setHeader('Content-Type', 'application/pdf; charset=UTF-8'); + stream.pipe(res); + fs.unlink(path); + }); + }); +} + +function noteActions(req, res, next) { + var noteId = req.params.noteId; + if (noteId != config.featuresnotename) { + if (!Note.checkNoteIdValid(noteId)) { + responseError(res, "404", "Not Found", "oops."); + return; + } + noteId = LZString.decompressFromBase64(noteId); + if (!noteId) { + responseError(res, "404", "Not Found", "oops."); + return; + } + } + var action = req.params.action; + switch (action) { + case "pretty": + actionPretty(req, res, noteId); + break; + case "download": + actionDownload(req, res, noteId); + break; + case "pdf": + actionPDF(req, res, noteId); + break; + default: + if (noteId != config.featuresnotename) + res.redirect('/' + LZString.compressToBase64(noteId)); + else + res.redirect('/' + noteId); + break; + } +} + +module.exports = response;
\ No newline at end of file diff --git a/lib/user.js b/lib/user.js new file mode 100644 index 00000000..1d7f11d2 --- /dev/null +++ b/lib/user.js @@ -0,0 +1,83 @@ +//user +//external modules +var mongoose = require('mongoose'); + +//core +var config = require("../config.js"); + +// create a user model +var model = mongoose.model('user', { + id: String, + profile: String, + history: String, + created: Date +}); + +//public +var user = { + model: model, + findUser: findUser, + newUser: newUser, + findOrNewUser: findOrNewUser, + getUserCount: getUserCount +}; + +function getUserCount(callback) { + model.count(function(err, count){ + if(err) callback(err, null); + else callback(null, count); + }); +} + +function findUser(id, callback) { + model.findOne({ + id: id + }, function (err, user) { + if (err) { + console.log('find user failed: ' + err); + callback(err, null); + } + if (!err && user != null) { + callback(null, user); + } else { + console.log('find user failed: ' + err); + callback(err, null); + }; + }); +} + +function newUser(id, profile, callback) { + var user = new model({ + id: id, + profile: JSON.stringify(profile), + created: Date.now() + }); + user.save(function (err) { + if (err) { + console.log('new user failed: ' + err); + callback(err, null); + } else { + console.log("new user success: " + user.id); + callback(null, user); + }; + }); +} + +function findOrNewUser(id, profile, callback) { + findUser(id, function(err, user) { + if(err || user == null) { + newUser(id, profile, function(err, user) { + if(err) { + console.log('find or new user failed: ' + err); + callback(err, null); + } else { + callback(null, user); + } + }); + } else { + callback(null, user); + } + }); +} + +module.exports = user;
\ No newline at end of file |