diff --git a/lib/document_handler.js b/lib/document_handler.js index 68985ec..dfe58e9 100644 --- a/lib/document_handler.js +++ b/lib/document_handler.js @@ -47,6 +47,20 @@ DocumentHandler.prototype.handleRawGet = function(key, response, skipExpire) { }, skipExpire); }; +DocumentHandler.prototype.handleDelete = function (key, secret, response) { + this.store.remove(key, secret, function (ret) { + if (ret) { + winston.verbose('removed document', { key: key }); + response.writeHead(200, { 'content-type': 'application/json' }); + response.end(JSON.stringify({ message: "Removed document " + key })); + } else { + winston.warn('invalid secret or document not found', { key: key, secret: secret }); + response.writeHead(404, { 'content-type': 'application/json' }); + response.end(JSON.stringify({ message: 'Document not found.' })); + } + }); +}; + // Handle adding a new Document DocumentHandler.prototype.handlePost = function (request, response) { var _this = this; @@ -67,11 +81,11 @@ DocumentHandler.prototype.handlePost = function (request, response) { } // And then save if we should _this.chooseKey(function (key) { - _this.store.set(key, buffer, function (res) { + _this.store.set(key, buffer, function (res, secret) { if (res) { - winston.verbose('added document', { key: key }); + winston.verbose('added document', { key: key, secret: secret }); response.writeHead(200, { 'content-type': 'application/json' }); - response.end(JSON.stringify({ key: key })); + response.end(JSON.stringify({ key: key, secret: secret })); } else { winston.verbose('error adding document'); diff --git a/lib/document_stores/file.js b/lib/document_stores/file.js index 7fd5995..457b71c 100644 --- a/lib/document_stores/file.js +++ b/lib/document_stores/file.js @@ -12,6 +12,11 @@ var FileDocumentStore = function(options) { this.expire = options.expire; }; +// Generate secret (10 chars) +FileDocumentStore.secret = function() { + return Math.random().toString(24).slice(-10); +}; + // Generate md5 of a string FileDocumentStore.md5 = function(str) { var md5sum = crypto.createHash('md5'); @@ -25,13 +30,17 @@ FileDocumentStore.prototype.set = function(key, data, callback, skipExpire) { try { var _this = this; fs.mkdir(this.basePath, '700', function() { + var sc = FileDocumentStore.secret(); var fn = _this.basePath + '/' + FileDocumentStore.md5(key); + var sfn = fn + "-" + sc; fs.writeFile(fn, data, 'utf8', function(err) { if (err) { callback(false); } else { - callback(true); + fs.writeFile(sfn, "A validation file for " + fn, 'utf8', function (err) { + if (!err) callback(true, sc); + }); if (_this.expire && !skipExpire) { winston.warn('file store cannot set expirations on keys'); } @@ -43,6 +52,27 @@ FileDocumentStore.prototype.set = function(key, data, callback, skipExpire) { } }; +FileDocumentStore.prototype.remove = function(str, key, callback) { + var file = FileDocumentStore.md5(str); + var kfn = key; + fs.exists(this.basePath + "/" + file + "-" + kfn, function(exists) { + if (exists) { + try { + fs.unlink(this.basePath + "/" + file, function() { + fs.unlink(this.basePath + "/" + file + "-" + kfn, function() { + callback(true); + }); + }); + } catch(err) { + callback(false); + } + } + else { + callback(false); + } + }); +}; + // Get data from a file from key FileDocumentStore.prototype.get = function(key, callback, skipExpire) { var _this = this; diff --git a/lib/document_stores/memcached.js b/lib/document_stores/memcached.js index 5aac673..fe07ccb 100644 --- a/lib/document_stores/memcached.js +++ b/lib/document_stores/memcached.js @@ -8,7 +8,9 @@ var MemcachedDocumentStore = function(options) { MemcachedDocumentStore.connect(options); } }; - +MemcachedDocumentStore.secret = function() { + return Math.random().toString(24).slice(-10); +}; // Create a connection MemcachedDocumentStore.connect = function(options) { var host = options.host || '127.0.0.1'; @@ -27,7 +29,17 @@ MemcachedDocumentStore.connect = function(options) { MemcachedDocumentStore.prototype.set = function(key, data, callback, skipExpire) { MemcachedDocumentStore.client.set(key, data, function(err, reply) { - err ? callback(false) : callback(true); + if (!err) { + var sc = MemcachedDocumentStore.secret(); + MemcachedDocumentStore.client.set(key + "-" + sc, "", function(err, reply) { + if (!err) { + callback(true, sc); + } + }); + } + else { + callback(false); + } }, skipExpire ? 0 : this.expire); }; @@ -42,4 +54,21 @@ MemcachedDocumentStore.prototype.get = function(key, callback, skipExpire) { }); }; +MemcachedDocumentStore.prototype.remove = function(key, secret, callback) { + MemcachedDocumentStore.client.get(key + "-" + secret, function(err, reply) { + if (!err) { + MemcachedDocumentStore.client.del(key, function(err, reply) { + if (err) return callback(false); + MemcachedDocumentStore.client.del(key + "-" + secret, function(err, reply) { + if (err) return callback(false); + callback(reply); + }); + }); + } + else { + callback(false); + } + }); +}; + module.exports = MemcachedDocumentStore; diff --git a/lib/document_stores/postgres.js b/lib/document_stores/postgres.js index c712e1f..d362e5a 100644 --- a/lib/document_stores/postgres.js +++ b/lib/document_stores/postgres.js @@ -9,6 +9,7 @@ var winston = require('winston'); var PostgresDocumentStore = function (options) { this.expireJS = options.expire; this.connectionUrl = process.env.DATABASE_URL || options.connectionUrl; + this.secret = () => Math.random().toString(24).slice(-10); }; PostgresDocumentStore.prototype = { @@ -19,16 +20,18 @@ PostgresDocumentStore.prototype = { var that = this; this.safeConnect(function (err, client, done) { if (err) { return callback(false); } - client.query('INSERT INTO entries (key, value, expiration) VALUES ($1, $2, $3)', [ + var sc = that.secret(); + client.query('INSERT INTO entries (key, value, expiration, secret) VALUES ($1, $2, $3, $4)', [ key, data, - that.expireJS && !skipExpire ? that.expireJS + now : null + that.expireJS && !skipExpire ? that.expireJS + now : null, + sc ], function (err, result) { if (err) { winston.error('error persisting value to postgres', { error: err }); return callback(false); } - callback(true); + callback(true, sc); done(); }); }); @@ -61,6 +64,32 @@ PostgresDocumentStore.prototype = { }); }); }, + + remove: function (key, secret, callback) { + var now = Math.floor(new Date().getTime() / 1000); + this.safeConnect(function (err, client, done) { + if (err) { return callback(false); } + client.query('SELECT id,value,expiration,secret from entries where KEY = $1 and (expiration IS NULL or expiration > $2)', [key, now], function (err, result) { + if (err) { + winston.error('error retrieving value from postgres', { error: err }); + return callback(false); + } + if (result.rows[0].secret == secret) { + client.query("DELETE FROM entries where KEY = $1", [key], function (err, result) { + if (err) { + winston.error('error removing an item from postgres', { error: err }); + return callback(false); + } + else { + callback(true); + done(); + } + }); + } + done(); + }); + }); + }, // A connection wrapper safeConnect: function (callback) { diff --git a/lib/document_stores/redis.js b/lib/document_stores/redis.js index 3f72d4b..adbf996 100644 --- a/lib/document_stores/redis.js +++ b/lib/document_stores/redis.js @@ -43,6 +43,11 @@ RedisDocumentStore.connect = function(options) { }); }; +// Generate secret (10 chars) +RedisDocumentStore.secret = function() { + return Math.random().toString(24).slice(-10); +}; + // Save file in a key RedisDocumentStore.prototype.set = function(key, data, callback, skipExpire) { var _this = this; @@ -54,7 +59,11 @@ RedisDocumentStore.prototype.set = function(key, data, callback, skipExpire) { if (!skipExpire) { _this.setExpiration(key); } - callback(true); + var sc = RedisDocumentStore.secret(); + RedisDocumentStore.client.set(key + '-' + sc, "", function(err, reply) { + if (err) return callback(false); + callback(true, sc); + }); } }); }; @@ -81,4 +90,21 @@ RedisDocumentStore.prototype.get = function(key, callback, skipExpire) { }); }; +RedisDocumentStore.prototype.remove = function(key, secret, callback) { + RedisDocumentStore.client.get(key + "-" + secret, function(err, reply) { + if (!err) { + RedisDocumentStore.client.del(key, function(err, reply) { + if (err) return callback(false); + RedisDocumentStore.client.del(key + "-" + secret, function(err, reply) { + if (err) return callback(false); + callback(reply); + }); + }); + } + else { + callback(false); + } + }); +}; + module.exports = RedisDocumentStore; diff --git a/server.js b/server.js index 2e138f2..77fab32 100644 --- a/server.js +++ b/server.js @@ -131,6 +131,9 @@ app.use(route(function(router) { skipExpire ); }); + router.get('/documents/:id/remove/:secret', function(request, response, next) { + return documentHandler.handleDelete(request.params.id, request.params.secret, response); + }); })); // Otherwise, try to match static files