From b9a3e09ad5a7673f64556d1dba122ed4c4fac980 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Mon, 7 Mar 2016 11:33:59 +0100 Subject: Prepare folders structure for angular app --- server/controllers/api/v1/index.js | 17 ++ server/controllers/api/v1/pods.js | 93 +++++++ server/controllers/api/v1/remoteVideos.js | 53 ++++ server/controllers/api/v1/videos.js | 144 +++++++++++ server/controllers/index.js | 11 + server/controllers/views.js | 27 ++ server/helpers/customValidators.js | 32 +++ server/helpers/logger.js | 40 +++ server/helpers/peertubeCrypto.js | 147 +++++++++++ server/helpers/requests.js | 109 ++++++++ server/helpers/utils.js | 16 ++ server/initializers/checker.js | 46 ++++ server/initializers/constants.js | 42 +++ server/initializers/database.js | 29 +++ server/lib/friends.js | 228 ++++++++++++++++ server/lib/poolRequests.js | 221 ++++++++++++++++ server/lib/videos.js | 50 ++++ server/lib/webtorrent.js | 157 ++++++++++++ server/lib/webtorrentProcess.js | 92 +++++++ server/middlewares/cache.js | 23 ++ server/middlewares/index.js | 15 ++ server/middlewares/reqValidators/index.js | 15 ++ server/middlewares/reqValidators/pods.js | 39 +++ server/middlewares/reqValidators/remote.js | 43 ++++ server/middlewares/reqValidators/utils.js | 25 ++ server/middlewares/reqValidators/videos.js | 74 ++++++ server/middlewares/secure.js | 49 ++++ server/models/pods.js | 88 +++++++ server/models/poolRequests.js | 55 ++++ server/models/videos.js | 234 +++++++++++++++++ server/tests/api/checkParams.js | 300 ++++++++++++++++++++++ server/tests/api/fixtures/video_short.mp4 | Bin 0 -> 38783 bytes server/tests/api/fixtures/video_short.ogv | Bin 0 -> 140849 bytes server/tests/api/fixtures/video_short.webm | Bin 0 -> 218910 bytes server/tests/api/fixtures/video_short1.webm | Bin 0 -> 572456 bytes server/tests/api/fixtures/video_short2.webm | Bin 0 -> 942961 bytes server/tests/api/fixtures/video_short3.webm | Bin 0 -> 292677 bytes server/tests/api/fixtures/video_short_fake.webm | 1 + server/tests/api/friendsAdvanced.js | 250 ++++++++++++++++++ server/tests/api/friendsBasic.js | 185 +++++++++++++ server/tests/api/index.js | 8 + server/tests/api/multiplePods.js | 328 ++++++++++++++++++++++++ server/tests/api/singlePod.js | 146 +++++++++++ server/tests/api/utils.js | 185 +++++++++++++ server/tests/index.js | 6 + 45 files changed, 3623 insertions(+) create mode 100644 server/controllers/api/v1/index.js create mode 100644 server/controllers/api/v1/pods.js create mode 100644 server/controllers/api/v1/remoteVideos.js create mode 100644 server/controllers/api/v1/videos.js create mode 100644 server/controllers/index.js create mode 100644 server/controllers/views.js create mode 100644 server/helpers/customValidators.js create mode 100644 server/helpers/logger.js create mode 100644 server/helpers/peertubeCrypto.js create mode 100644 server/helpers/requests.js create mode 100644 server/helpers/utils.js create mode 100644 server/initializers/checker.js create mode 100644 server/initializers/constants.js create mode 100644 server/initializers/database.js create mode 100644 server/lib/friends.js create mode 100644 server/lib/poolRequests.js create mode 100644 server/lib/videos.js create mode 100644 server/lib/webtorrent.js create mode 100644 server/lib/webtorrentProcess.js create mode 100644 server/middlewares/cache.js create mode 100644 server/middlewares/index.js create mode 100644 server/middlewares/reqValidators/index.js create mode 100644 server/middlewares/reqValidators/pods.js create mode 100644 server/middlewares/reqValidators/remote.js create mode 100644 server/middlewares/reqValidators/utils.js create mode 100644 server/middlewares/reqValidators/videos.js create mode 100644 server/middlewares/secure.js create mode 100644 server/models/pods.js create mode 100644 server/models/poolRequests.js create mode 100644 server/models/videos.js create mode 100644 server/tests/api/checkParams.js create mode 100644 server/tests/api/fixtures/video_short.mp4 create mode 100644 server/tests/api/fixtures/video_short.ogv create mode 100644 server/tests/api/fixtures/video_short.webm create mode 100644 server/tests/api/fixtures/video_short1.webm create mode 100644 server/tests/api/fixtures/video_short2.webm create mode 100644 server/tests/api/fixtures/video_short3.webm create mode 100644 server/tests/api/fixtures/video_short_fake.webm create mode 100644 server/tests/api/friendsAdvanced.js create mode 100644 server/tests/api/friendsBasic.js create mode 100644 server/tests/api/index.js create mode 100644 server/tests/api/multiplePods.js create mode 100644 server/tests/api/singlePod.js create mode 100644 server/tests/api/utils.js create mode 100644 server/tests/index.js (limited to 'server') diff --git a/server/controllers/api/v1/index.js b/server/controllers/api/v1/index.js new file mode 100644 index 000000000..07a68ed9d --- /dev/null +++ b/server/controllers/api/v1/index.js @@ -0,0 +1,17 @@ +'use strict' + +var express = require('express') + +var router = express.Router() + +var podsController = require('./pods') +var remoteVideosController = require('./remoteVideos') +var videosController = require('./videos') + +router.use('/pods', podsController) +router.use('/remotevideos', remoteVideosController) +router.use('/videos', videosController) + +// --------------------------------------------------------------------------- + +module.exports = router diff --git a/server/controllers/api/v1/pods.js b/server/controllers/api/v1/pods.js new file mode 100644 index 000000000..c93a86ee8 --- /dev/null +++ b/server/controllers/api/v1/pods.js @@ -0,0 +1,93 @@ +'use strict' + +var express = require('express') +var fs = require('fs') + +var logger = require('../../../helpers/logger') +var friends = require('../../../lib/friends') +var middleware = require('../../../middlewares') +var cacheMiddleware = middleware.cache +var peertubeCrypto = require('../../../helpers/peertubeCrypto') +var Pods = require('../../../models/pods') +var reqValidator = middleware.reqValidators.pods +var secureMiddleware = middleware.secure +var secureRequest = middleware.reqValidators.remote.secureRequest +var Videos = require('../../../models/videos') + +var router = express.Router() + +router.get('/', cacheMiddleware.cache(false), listPods) +router.post('/', reqValidator.podsAdd, cacheMiddleware.cache(false), addPods) +router.get('/makefriends', reqValidator.makeFriends, cacheMiddleware.cache(false), makeFriends) +router.get('/quitfriends', cacheMiddleware.cache(false), quitFriends) +// Post because this is a secured request +router.post('/remove', secureRequest, secureMiddleware.decryptBody, removePods) + +// --------------------------------------------------------------------------- + +module.exports = router + +// --------------------------------------------------------------------------- + +function addPods (req, res, next) { + var informations = req.body.data + Pods.add(informations, function (err) { + if (err) return next(err) + + Videos.addRemotes(informations.videos) + + fs.readFile(peertubeCrypto.getCertDir() + 'peertube.pub', 'utf8', function (err, cert) { + if (err) { + logger.error('Cannot read cert file.') + return next(err) + } + + Videos.listOwned(function (err, videos_list) { + if (err) { + logger.error('Cannot get the list of owned videos.') + return next(err) + } + + res.json({ cert: cert, videos: videos_list }) + }) + }) + }) +} + +function listPods (req, res, next) { + Pods.list(function (err, pods_list) { + if (err) return next(err) + + res.json(pods_list) + }) +} + +function makeFriends (req, res, next) { + friends.makeFriends(function (err) { + if (err) return next(err) + + res.sendStatus(204) + }) +} + +function removePods (req, res, next) { + var url = req.body.signature.url + Pods.remove(url, function (err) { + if (err) return next(err) + + Videos.removeAllRemotesOf(url, function (err) { + if (err) logger.error('Cannot remove all remote videos of %s.', url) + else logger.info('%s pod removed.', url) + + res.sendStatus(204) + }) + }) +} + +function quitFriends (req, res, next) { + friends.quitFriends(function (err) { + if (err) return next(err) + + res.sendStatus(204) + }) +} diff --git a/server/controllers/api/v1/remoteVideos.js b/server/controllers/api/v1/remoteVideos.js new file mode 100644 index 000000000..475a874cf --- /dev/null +++ b/server/controllers/api/v1/remoteVideos.js @@ -0,0 +1,53 @@ +'use strict' + +var express = require('express') +var pluck = require('lodash-node/compat/collection/pluck') + +var middleware = require('../../../middlewares') +var secureMiddleware = middleware.secure +var cacheMiddleware = middleware.cache +var reqValidator = middleware.reqValidators.remote +var videos = require('../../../models/videos') + +var router = express.Router() + +router.post('/add', + reqValidator.secureRequest, + secureMiddleware.decryptBody, + reqValidator.remoteVideosAdd, + cacheMiddleware.cache(false), + addRemoteVideos +) + +router.post('/remove', + reqValidator.secureRequest, + secureMiddleware.decryptBody, + reqValidator.remoteVideosRemove, + cacheMiddleware.cache(false), + removeRemoteVideo +) + +// --------------------------------------------------------------------------- + +module.exports = router + +// --------------------------------------------------------------------------- + +function addRemoteVideos (req, res, next) { + videos.addRemotes(req.body.data, function (err, videos) { + if (err) return next(err) + + res.json(videos) + }) +} + +function removeRemoteVideo (req, res, next) { + var url = req.body.signature.url + var magnetUris = pluck(req.body.data, 'magnetUri') + + videos.removeRemotesOfByMagnetUris(url, magnetUris, function (err) { + if (err) return next(err) + + res.sendStatus(204) + }) +} diff --git a/server/controllers/api/v1/videos.js b/server/controllers/api/v1/videos.js new file mode 100644 index 000000000..620711925 --- /dev/null +++ b/server/controllers/api/v1/videos.js @@ -0,0 +1,144 @@ +'use strict' + +var config = require('config') +var crypto = require('crypto') +var express = require('express') +var multer = require('multer') + +var logger = require('../../../helpers/logger') +var friends = require('../../../lib/friends') +var middleware = require('../../../middlewares') +var cacheMiddleware = middleware.cache +var reqValidator = middleware.reqValidators.videos +var Videos = require('../../../models/videos') // model +var videos = require('../../../lib/videos') +var webtorrent = require('../../../lib/webtorrent') + +var router = express.Router() +var uploads = config.get('storage.uploads') + +// multer configuration +var storage = multer.diskStorage({ + destination: function (req, file, cb) { + cb(null, uploads) + }, + + filename: function (req, file, cb) { + var extension = '' + if (file.mimetype === 'video/webm') extension = 'webm' + else if (file.mimetype === 'video/mp4') extension = 'mp4' + else if (file.mimetype === 'video/ogg') extension = 'ogv' + crypto.pseudoRandomBytes(16, function (err, raw) { + var fieldname = err ? undefined : raw.toString('hex') + cb(null, fieldname + '.' + extension) + }) + } +}) + +var reqFiles = multer({ storage: storage }).fields([{ name: 'input_video', maxCount: 1 }]) + +router.get('/', cacheMiddleware.cache(false), listVideos) +router.post('/', reqFiles, reqValidator.videosAdd, cacheMiddleware.cache(false), addVideo) +router.get('/:id', reqValidator.videosGet, cacheMiddleware.cache(false), getVideos) +router.delete('/:id', reqValidator.videosRemove, cacheMiddleware.cache(false), removeVideo) +router.get('/search/:name', reqValidator.videosSearch, cacheMiddleware.cache(false), searchVideos) + +// --------------------------------------------------------------------------- + +module.exports = router + +// --------------------------------------------------------------------------- + +function addVideo (req, res, next) { + var video_file = req.files.input_video[0] + var video_infos = req.body + + videos.seed(video_file.path, function (err, torrent) { + if (err) { + logger.error('Cannot seed this video.') + return next(err) + } + + var video_data = { + name: video_infos.name, + namePath: video_file.filename, + description: video_infos.description, + magnetUri: torrent.magnetURI + } + + Videos.add(video_data, function (err) { + if (err) { + // TODO unseed the video + logger.error('Cannot insert this video in the database.') + return next(err) + } + + // Now we'll add the video's meta data to our friends + friends.addVideoToFriends(video_data) + + // TODO : include Location of the new video + res.sendStatus(201) + }) + }) +} + +function getVideos (req, res, next) { + Videos.get(req.params.id, function (err, video) { + if (err) return next(err) + + if (video === null) { + return res.sendStatus(404) + } + + res.json(video) + }) +} + +function listVideos (req, res, next) { + Videos.list(function (err, videos_list) { + if (err) return next(err) + + res.json(videos_list) + }) +} + +function removeVideo (req, res, next) { + var video_id = req.params.id + Videos.get(video_id, function (err, video) { + if (err) return next(err) + + removeTorrent(video.magnetUri, function () { + Videos.removeOwned(req.params.id, function (err) { + if (err) return next(err) + + var params = { + name: video.name, + magnetUri: video.magnetUri + } + + friends.removeVideoToFriends(params) + res.sendStatus(204) + }) + }) + }) +} + +function searchVideos (req, res, next) { + Videos.search(req.params.name, function (err, videos_list) { + if (err) return next(err) + + res.json(videos_list) + }) +} + +// --------------------------------------------------------------------------- + +// Maybe the torrent is not seeded, but we catch the error to don't stop the removing process +function removeTorrent (magnetUri, callback) { + try { + webtorrent.remove(magnetUri, callback) + } catch (err) { + logger.warn('Cannot remove the torrent from WebTorrent', { err: err }) + return callback(null) + } +} diff --git a/server/controllers/index.js b/server/controllers/index.js new file mode 100644 index 000000000..858f493da --- /dev/null +++ b/server/controllers/index.js @@ -0,0 +1,11 @@ +'use strict' + +var constants = require('../initializers/constants') + +var apiController = require('./api/' + constants.API_VERSION) +var viewsController = require('./views') + +module.exports = { + api: apiController, + views: viewsController +} diff --git a/server/controllers/views.js b/server/controllers/views.js new file mode 100644 index 000000000..aa9718079 --- /dev/null +++ b/server/controllers/views.js @@ -0,0 +1,27 @@ +'use strict' + +var express = require('express') + +var cacheMiddleware = require('../middlewares').cache + +var router = express.Router() + +router.get(/^\/(index)?$/, cacheMiddleware.cache(), getIndex) +router.get('/partials/:directory/:name', cacheMiddleware.cache(), getPartial) + +// --------------------------------------------------------------------------- + +module.exports = router + +// --------------------------------------------------------------------------- + +function getIndex (req, res) { + res.render('index') +} + +function getPartial (req, res) { + var directory = req.params.directory + var name = req.params.name + + res.render('partials/' + directory + '/' + name) +} diff --git a/server/helpers/customValidators.js b/server/helpers/customValidators.js new file mode 100644 index 000000000..20c41f5da --- /dev/null +++ b/server/helpers/customValidators.js @@ -0,0 +1,32 @@ +'use strict' + +var validator = require('validator') + +var customValidators = { + eachIsRemoteVideosAddValid: eachIsRemoteVideosAddValid, + eachIsRemoteVideosRemoveValid: eachIsRemoteVideosRemoveValid, + isArray: isArray +} + +function eachIsRemoteVideosAddValid (values) { + return values.every(function (val) { + return validator.isLength(val.name, 1, 50) && + validator.isLength(val.description, 1, 50) && + validator.isLength(val.magnetUri, 10) && + validator.isURL(val.podUrl) + }) +} + +function eachIsRemoteVideosRemoveValid (values) { + return values.every(function (val) { + return validator.isLength(val.magnetUri, 10) + }) +} + +function isArray (value) { + return Array.isArray(value) +} + +// --------------------------------------------------------------------------- + +module.exports = customValidators diff --git a/server/helpers/logger.js b/server/helpers/logger.js new file mode 100644 index 000000000..67f69a875 --- /dev/null +++ b/server/helpers/logger.js @@ -0,0 +1,40 @@ +// Thanks http://tostring.it/2014/06/23/advanced-logging-with-nodejs/ +'use strict' + +var config = require('config') +var path = require('path') +var winston = require('winston') +winston.emitErrs = true + +var logDir = path.join(__dirname, '..', config.get('storage.logs')) +var logger = new winston.Logger({ + transports: [ + new winston.transports.File({ + level: 'debug', + filename: path.join(logDir, 'all-logs.log'), + handleExceptions: true, + json: true, + maxsize: 5242880, + maxFiles: 5, + colorize: false + }), + new winston.transports.Console({ + level: 'debug', + handleExceptions: true, + humanReadableUnhandledException: true, + json: false, + colorize: true + }) + ], + exitOnError: true +}) + +logger.stream = { + write: function (message, encoding) { + logger.info(message) + } +} + +// --------------------------------------------------------------------------- + +module.exports = logger diff --git a/server/helpers/peertubeCrypto.js b/server/helpers/peertubeCrypto.js new file mode 100644 index 000000000..29b9d79c9 --- /dev/null +++ b/server/helpers/peertubeCrypto.js @@ -0,0 +1,147 @@ +'use strict' + +var config = require('config') +var crypto = require('crypto') +var fs = require('fs') +var openssl = require('openssl-wrapper') +var path = require('path') +var ursa = require('ursa') + +var logger = require('./logger') + +var certDir = path.join(__dirname, '..', config.get('storage.certs')) +var algorithm = 'aes-256-ctr' + +var peertubeCrypto = { + checkSignature: checkSignature, + createCertsIfNotExist: createCertsIfNotExist, + decrypt: decrypt, + encrypt: encrypt, + getCertDir: getCertDir, + sign: sign +} + +function checkSignature (public_key, raw_data, hex_signature) { + var crt = ursa.createPublicKey(public_key) + var is_valid = crt.hashAndVerify('sha256', new Buffer(raw_data).toString('hex'), hex_signature, 'hex') + return is_valid +} + +function createCertsIfNotExist (callback) { + certsExist(function (exist) { + if (exist === true) { + return callback(null) + } + + createCerts(function (err) { + return callback(err) + }) + }) +} + +function decrypt (key, data, callback) { + fs.readFile(getCertDir() + 'peertube.key.pem', function (err, file) { + if (err) return callback(err) + + var my_private_key = ursa.createPrivateKey(file) + var decrypted_key = my_private_key.decrypt(key, 'hex', 'utf8') + var decrypted_data = symetricDecrypt(data, decrypted_key) + + return callback(null, decrypted_data) + }) +} + +function encrypt (public_key, data, callback) { + var crt = ursa.createPublicKey(public_key) + + symetricEncrypt(data, function (err, dataEncrypted) { + if (err) return callback(err) + + var key = crt.encrypt(dataEncrypted.password, 'utf8', 'hex') + var encrypted = { + data: dataEncrypted.crypted, + key: key + } + + callback(null, encrypted) + }) +} + +function getCertDir () { + return certDir +} + +function sign (data) { + var myKey = ursa.createPrivateKey(fs.readFileSync(certDir + 'peertube.key.pem')) + var signature = myKey.hashAndSign('sha256', data, 'utf8', 'hex') + + return signature +} + +// --------------------------------------------------------------------------- + +module.exports = peertubeCrypto + +// --------------------------------------------------------------------------- + +function certsExist (callback) { + fs.exists(certDir + 'peertube.key.pem', function (exists) { + return callback(exists) + }) +} + +function createCerts (callback) { + certsExist(function (exist) { + if (exist === true) { + var string = 'Certs already exist.' + logger.warning(string) + return callback(new Error(string)) + } + + logger.info('Generating a RSA key...') + openssl.exec('genrsa', { 'out': certDir + 'peertube.key.pem', '2048': false }, function (err) { + if (err) { + logger.error('Cannot create private key on this pod.') + return callback(err) + } + logger.info('RSA key generated.') + + logger.info('Manage public key...') + openssl.exec('rsa', { 'in': certDir + 'peertube.key.pem', 'pubout': true, 'out': certDir + 'peertube.pub' }, function (err) { + if (err) { + logger.error('Cannot create public key on this pod.') + return callback(err) + } + + logger.info('Public key managed.') + return callback(null) + }) + }) + }) +} + +function generatePassword (callback) { + crypto.randomBytes(32, function (err, buf) { + if (err) return callback(err) + + callback(null, buf.toString('utf8')) + }) +} + +function symetricDecrypt (text, password) { + var decipher = crypto.createDecipher(algorithm, password) + var dec = decipher.update(text, 'hex', 'utf8') + dec += decipher.final('utf8') + return dec +} + +function symetricEncrypt (text, callback) { + generatePassword(function (err, password) { + if (err) return callback(err) + + var cipher = crypto.createCipher(algorithm, password) + var crypted = cipher.update(text, 'utf8', 'hex') + crypted += cipher.final('hex') + callback(null, { crypted: crypted, password: password }) + }) +} diff --git a/server/helpers/requests.js b/server/helpers/requests.js new file mode 100644 index 000000000..e19afa5ca --- /dev/null +++ b/server/helpers/requests.js @@ -0,0 +1,109 @@ +'use strict' + +var async = require('async') +var config = require('config') +var request = require('request') +var replay = require('request-replay') + +var constants = require('../initializers/constants') +var logger = require('./logger') +var peertubeCrypto = require('./peertubeCrypto') + +var http = config.get('webserver.https') ? 'https' : 'http' +var host = config.get('webserver.host') +var port = config.get('webserver.port') + +var requests = { + makeMultipleRetryRequest: makeMultipleRetryRequest +} + +function makeMultipleRetryRequest (all_data, pods, callbackEach, callback) { + if (!callback) { + callback = callbackEach + callbackEach = null + } + + var url = http + '://' + host + ':' + port + var signature + + // Add signature if it is specified in the params + if (all_data.method === 'POST' && all_data.data && all_data.sign === true) { + signature = peertubeCrypto.sign(url) + } + + // Make a request for each pod + async.each(pods, function (pod, callback_each_async) { + function callbackEachRetryRequest (err, response, body, url, pod) { + if (callbackEach !== null) { + callbackEach(err, response, body, url, pod, function () { + callback_each_async() + }) + } else { + callback_each_async() + } + } + + var params = { + url: pod.url + all_data.path, + method: all_data.method + } + + // Add data with POST requst ? + if (all_data.method === 'POST' && all_data.data) { + // Encrypt data ? + if (all_data.encrypt === true) { + // TODO: ES6 with let + ;(function (copy_params, copy_url, copy_pod, copy_signature) { + peertubeCrypto.encrypt(pod.publicKey, JSON.stringify(all_data.data), function (err, encrypted) { + if (err) return callback(err) + + copy_params.json = { + data: encrypted.data, + key: encrypted.key + } + + makeRetryRequest(copy_params, copy_url, copy_pod, copy_signature, callbackEachRetryRequest) + }) + })(params, url, pod, signature) + } else { + params.json = { data: all_data.data } + makeRetryRequest(params, url, pod, signature, callbackEachRetryRequest) + } + } else { + makeRetryRequest(params, url, pod, signature, callbackEachRetryRequest) + } + }, callback) +} + +// --------------------------------------------------------------------------- + +module.exports = requests + +// --------------------------------------------------------------------------- + +function makeRetryRequest (params, from_url, to_pod, signature, callbackEach) { + // Append the signature + if (signature) { + params.json.signature = { + url: from_url, + signature: signature + } + } + + logger.debug('Make retry requests to %s.', to_pod.url) + + replay( + request.post(params, function (err, response, body) { + callbackEach(err, response, body, params.url, to_pod) + }), + { + retries: constants.REQUEST_RETRIES, + factor: 3, + maxTimeout: Infinity, + errorCodes: [ 'EADDRINFO', 'ETIMEDOUT', 'ECONNRESET', 'ESOCKETTIMEDOUT', 'ENOTFOUND', 'ECONNREFUSED' ] + } + ).on('replay', function (replay) { + logger.info('Replaying request to %s. Request failed: %d %s. Replay number: #%d. Will retry in: %d ms.', + params.url, replay.error.code, replay.error.message, replay.number, replay.delay) + }) +} diff --git a/server/helpers/utils.js b/server/helpers/utils.js new file mode 100644 index 000000000..d2c9ad8b2 --- /dev/null +++ b/server/helpers/utils.js @@ -0,0 +1,16 @@ +'use strict' + +var logger = require('./logger') + +var utils = { + cleanForExit: cleanForExit +} + +function cleanForExit (webtorrent_process) { + logger.info('Gracefully exiting.') + process.kill(-webtorrent_process.pid) +} + +// --------------------------------------------------------------------------- + +module.exports = utils diff --git a/server/initializers/checker.js b/server/initializers/checker.js new file mode 100644 index 000000000..ec7bc0ad2 --- /dev/null +++ b/server/initializers/checker.js @@ -0,0 +1,46 @@ +'use strict' + +var config = require('config') +var mkdirp = require('mkdirp') +var path = require('path') + +var checker = { + checkConfig: checkConfig, + createDirectoriesIfNotExist: createDirectoriesIfNotExist +} + +// Check the config files +function checkConfig () { + var required = [ 'listen.port', + 'webserver.https', 'webserver.host', 'webserver.port', + 'database.host', 'database.port', 'database.suffix', + 'storage.certs', 'storage.uploads', 'storage.logs', + 'network.friends' ] + var miss = [] + + for (var key of required) { + if (!config.has(key)) { + miss.push(key) + } + } + + return miss +} + +// Create directories for the storage if it doesn't exist +function createDirectoriesIfNotExist () { + var storages = config.get('storage') + + for (var key of Object.keys(storages)) { + var dir = storages[key] + try { + mkdirp.sync(path.join(__dirname, '..', dir)) + } catch (error) { + throw new Error('Cannot create ' + path + ':' + error) + } + } +} + +// --------------------------------------------------------------------------- + +module.exports = checker diff --git a/server/initializers/constants.js b/server/initializers/constants.js new file mode 100644 index 000000000..16e50443b --- /dev/null +++ b/server/initializers/constants.js @@ -0,0 +1,42 @@ +'use strict' + +// API version of our pod +var API_VERSION = 'v1' + +// Score a pod has when we create it as a friend +var FRIEND_BASE_SCORE = 100 + +// Time to wait between requests to the friends +var INTERVAL = 60000 + +// Number of points we add/remove from a friend after a successful/bad request +var PODS_SCORE = { + MALUS: -10, + BONUS: 10 +} + +// Number of retries we make for the make retry requests (to friends...) +var REQUEST_RETRIES = 10 + +// Special constants for a test instance +if (isTestInstance() === true) { + FRIEND_BASE_SCORE = 20 + INTERVAL = 10000 + REQUEST_RETRIES = 2 +} + +// --------------------------------------------------------------------------- + +module.exports = { + API_VERSION: API_VERSION, + FRIEND_BASE_SCORE: FRIEND_BASE_SCORE, + INTERVAL: INTERVAL, + PODS_SCORE: PODS_SCORE, + REQUEST_RETRIES: REQUEST_RETRIES +} + +// --------------------------------------------------------------------------- + +function isTestInstance () { + return (process.env.NODE_ENV === 'test') +} diff --git a/server/initializers/database.js b/server/initializers/database.js new file mode 100644 index 000000000..a917442ec --- /dev/null +++ b/server/initializers/database.js @@ -0,0 +1,29 @@ +'use strict' + +var config = require('config') +var mongoose = require('mongoose') + +var logger = require('../helpers/logger') + +var dbname = 'peertube' + config.get('database.suffix') +var host = config.get('database.host') +var port = config.get('database.port') + +var database = { + connect: connect +} + +function connect () { + mongoose.connect('mongodb://' + host + ':' + port + '/' + dbname) + mongoose.connection.on('error', function () { + throw new Error('Mongodb connection error.') + }) + + mongoose.connection.on('open', function () { + logger.info('Connected to mongodb.') + }) +} + +// --------------------------------------------------------------------------- + +module.exports = database diff --git a/server/lib/friends.js b/server/lib/friends.js new file mode 100644 index 000000000..006a64404 --- /dev/null +++ b/server/lib/friends.js @@ -0,0 +1,228 @@ +'use strict' + +var async = require('async') +var config = require('config') +var fs = require('fs') +var request = require('request') + +var constants = require('../initializers/constants') +var logger = require('../helpers/logger') +var peertubeCrypto = require('../helpers/peertubeCrypto') +var Pods = require('../models/pods') +var poolRequests = require('../lib/poolRequests') +var requests = require('../helpers/requests') +var Videos = require('../models/videos') + +var http = config.get('webserver.https') ? 'https' : 'http' +var host = config.get('webserver.host') +var port = config.get('webserver.port') + +var pods = { + addVideoToFriends: addVideoToFriends, + hasFriends: hasFriends, + makeFriends: makeFriends, + quitFriends: quitFriends, + removeVideoToFriends: removeVideoToFriends +} + +function addVideoToFriends (video) { + // To avoid duplicates + var id = video.name + video.magnetUri + // ensure namePath is null + video.namePath = null + poolRequests.addRequest(id, 'add', video) +} + +function hasFriends (callback) { + Pods.count(function (err, count) { + if (err) return callback(err) + + var has_friends = (count !== 0) + callback(null, has_friends) + }) +} + +function makeFriends (callback) { + var pods_score = {} + + logger.info('Make friends!') + fs.readFile(peertubeCrypto.getCertDir() + 'peertube.pub', 'utf8', function (err, cert) { + if (err) { + logger.error('Cannot read public cert.') + return callback(err) + } + + var urls = config.get('network.friends') + + async.each(urls, function (url, callback) { + computeForeignPodsList(url, pods_score, callback) + }, function (err) { + if (err) return callback(err) + + logger.debug('Pods scores computed.', { pods_score: pods_score }) + var pods_list = computeWinningPods(urls, pods_score) + logger.debug('Pods that we keep computed.', { pods_to_keep: pods_list }) + + makeRequestsToWinningPods(cert, pods_list, callback) + }) + }) +} + +function quitFriends (callback) { + // Stop pool requests + poolRequests.deactivate() + // Flush pool requests + poolRequests.forceSend() + + Pods.list(function (err, pods) { + if (err) return callback(err) + + var request = { + method: 'POST', + path: '/api/' + constants.API_VERSION + '/pods/remove', + sign: true, + encrypt: true, + data: { + url: 'me' // Fake data + } + } + + // Announce we quit them + requests.makeMultipleRetryRequest(request, pods, function () { + Pods.removeAll(function (err) { + poolRequests.activate() + + if (err) return callback(err) + + logger.info('Broke friends, so sad :(') + + Videos.removeAllRemotes(function (err) { + if (err) return callback(err) + + logger.info('Removed all remote videos.') + callback(null) + }) + }) + }) + }) +} + +function removeVideoToFriends (video) { + // To avoid duplicates + var id = video.name + video.magnetUri + poolRequests.addRequest(id, 'remove', video) +} + +// --------------------------------------------------------------------------- + +module.exports = pods + +// --------------------------------------------------------------------------- + +function computeForeignPodsList (url, pods_score, callback) { + // Let's give 1 point to the pod we ask the friends list + pods_score[url] = 1 + + getForeignPodsList(url, function (err, foreign_pods_list) { + if (err) return callback(err) + if (foreign_pods_list.length === 0) return callback() + + async.each(foreign_pods_list, function (foreign_pod, callback_each) { + var foreign_url = foreign_pod.url + + if (pods_score[foreign_url]) pods_score[foreign_url]++ + else pods_score[foreign_url] = 1 + + callback_each() + }, function () { + callback() + }) + }) +} + +function computeWinningPods (urls, pods_score) { + // Build the list of pods to add + // Only add a pod if it exists in more than a half base pods + var pods_list = [] + var base_score = urls.length / 2 + Object.keys(pods_score).forEach(function (pod) { + if (pods_score[pod] > base_score) pods_list.push({ url: pod }) + }) + + return pods_list +} + +function getForeignPodsList (url, callback) { + var path = '/api/' + constants.API_VERSION + '/pods' + + request.get(url + path, function (err, response, body) { + if (err) return callback(err) + + callback(null, JSON.parse(body)) + }) +} + +function makeRequestsToWinningPods (cert, pods_list, callback) { + // Stop pool requests + poolRequests.deactivate() + // Flush pool requests + poolRequests.forceSend() + + // Get the list of our videos to send to our new friends + Videos.listOwned(function (err, videos_list) { + if (err) { + logger.error('Cannot get the list of videos we own.') + return callback(err) + } + + var data = { + url: http + '://' + host + ':' + port, + publicKey: cert, + videos: videos_list + } + + requests.makeMultipleRetryRequest( + { method: 'POST', path: '/api/' + constants.API_VERSION + '/pods/', data: data }, + + pods_list, + + function eachRequest (err, response, body, url, pod, callback_each_request) { + // We add the pod if it responded correctly with its public certificate + if (!err && response.statusCode === 200) { + Pods.add({ url: pod.url, publicKey: body.cert, score: constants.FRIEND_BASE_SCORE }, function (err) { + if (err) { + logger.error('Error with adding %s pod.', pod.url, { error: err }) + return callback_each_request() + } + + Videos.addRemotes(body.videos, function (err) { + if (err) { + logger.error('Error with adding videos of pod.', pod.url, { error: err }) + return callback_each_request() + } + + logger.debug('Adding remote videos from %s.', pod.url, { videos: body.videos }) + return callback_each_request() + }) + }) + } else { + logger.error('Error with adding %s pod.', pod.url, { error: err || new Error('Status not 200') }) + return callback_each_request() + } + }, + + function endRequests (err) { + // Now we made new friends, we can re activate the pool of requests + poolRequests.activate() + + if (err) { + logger.error('There was some errors when we wanted to make friends.') + return callback(err) + } + + logger.debug('makeRequestsToWinningPods finished.') + return callback(null) + } + ) + }) +} diff --git a/server/lib/poolRequests.js b/server/lib/poolRequests.js new file mode 100644 index 000000000..f786c3c7a --- /dev/null +++ b/server/lib/poolRequests.js @@ -0,0 +1,221 @@ +'use strict' + +var async = require('async') +var pluck = require('lodash-node/compat/collection/pluck') + +var constants = require('../initializers/constants') +var logger = require('../helpers/logger') +var Pods = require('../models/pods') +var PoolRequests = require('../models/poolRequests') +var requests = require('../helpers/requests') +var Videos = require('../models/videos') + +var timer = null + +var poolRequests = { + activate: activate, + addRequest: addRequest, + deactivate: deactivate, + forceSend: forceSend +} + +function activate () { + logger.info('Pool requests activated.') + timer = setInterval(makePoolRequests, constants.INTERVAL) +} + +function addRequest (id, type, request) { + logger.debug('Add request to the pool requests.', { id: id, type: type, request: request }) + + PoolRequests.findById(id, function (err, entity) { + if (err) { + logger.error('Cannot find one pool request.', { error: err }) + return // Abort + } + + if (entity) { + if (entity.type === type) { + logger.error('Cannot insert two same requests.') + return // Abort + } + + // Remove the request of the other type + PoolRequests.removeRequestById(id, function (err) { + if (err) { + logger.error('Cannot remove a pool request.', { error: err }) + return // Abort + } + }) + } else { + PoolRequests.create(id, type, request, function (err) { + if (err) logger.error('Cannot create a pool request.', { error: err }) + return // Abort + }) + } + }) +} + +function deactivate () { + logger.info('Pool requests deactivated.') + clearInterval(timer) +} + +function forceSend () { + logger.info('Force pool requests sending.') + makePoolRequests() +} + +// --------------------------------------------------------------------------- + +module.exports = poolRequests + +// --------------------------------------------------------------------------- + +function makePoolRequest (type, requests_to_make, callback) { + if (!callback) callback = function () {} + + Pods.list(function (err, pods) { + if (err) return callback(err) + + var params = { + encrypt: true, + sign: true, + method: 'POST', + path: null, + data: requests_to_make + } + + if (type === 'add') { + params.path = '/api/' + constants.API_VERSION + '/remotevideos/add' + } else if (type === 'remove') { + params.path = '/api/' + constants.API_VERSION + '/remotevideos/remove' + } else { + return callback(new Error('Unkown pool request type.')) + } + + var bad_pods = [] + var good_pods = [] + + requests.makeMultipleRetryRequest(params, pods, callbackEachPodFinished, callbackAllPodsFinished) + + function callbackEachPodFinished (err, response, body, url, pod, callback_each_pod_finished) { + if (err || (response.statusCode !== 200 && response.statusCode !== 204)) { + bad_pods.push(pod._id) + logger.error('Error sending secure request to %s pod.', url, { error: err || new Error('Status code not 20x') }) + } else { + good_pods.push(pod._id) + } + + return callback_each_pod_finished() + } + + function callbackAllPodsFinished (err) { + if (err) return callback(err) + + updatePodsScore(good_pods, bad_pods) + callback(null) + } + }) +} + +function makePoolRequests () { + logger.info('Making pool requests to friends.') + + PoolRequests.list(function (err, pool_requests) { + if (err) { + logger.error('Cannot get the list of pool requests.', { err: err }) + return // Abort + } + + if (pool_requests.length === 0) return + + var requests_to_make = { + add: { + ids: [], + requests: [] + }, + remove: { + ids: [], + requests: [] + } + } + + async.each(pool_requests, function (pool_request, callback_each) { + if (pool_request.type === 'add') { + requests_to_make.add.requests.push(pool_request.request) + requests_to_make.add.ids.push(pool_request._id) + } else if (pool_request.type === 'remove') { + requests_to_make.remove.requests.push(pool_request.request) + requests_to_make.remove.ids.push(pool_request._id) + } else { + logger.error('Unkown pool request type.', { request_type: pool_request.type }) + return // abort + } + + callback_each() + }, function () { + // Send the add requests + if (requests_to_make.add.requests.length !== 0) { + makePoolRequest('add', requests_to_make.add.requests, function (err) { + if (err) logger.error('Errors when sent add pool requests.', { error: err }) + + PoolRequests.removeRequests(requests_to_make.add.ids) + }) + } + + // Send the remove requests + if (requests_to_make.remove.requests.length !== 0) { + makePoolRequest('remove', requests_to_make.remove.requests, function (err) { + if (err) logger.error('Errors when sent remove pool requests.', { error: err }) + + PoolRequests.removeRequests(requests_to_make.remove.ids) + }) + } + }) + }) +} + +function removeBadPods () { + Pods.findBadPods(function (err, pods) { + if (err) { + logger.error('Cannot find bad pods.', { error: err }) + return // abort + } + + if (pods.length === 0) return + + var urls = pluck(pods, 'url') + var ids = pluck(pods, '_id') + + Videos.removeAllRemotesOf(urls, function (err, r) { + if (err) { + logger.error('Cannot remove videos from a pod that we removing.', { error: err }) + } else { + var videos_removed = r.result.n + logger.info('Removed %d videos.', videos_removed) + } + + Pods.removeAllByIds(ids, function (err, r) { + if (err) { + logger.error('Cannot remove bad pods.', { error: err }) + } else { + var pods_removed = r.result.n + logger.info('Removed %d pods.', pods_removed) + } + }) + }) + }) +} + +function updatePodsScore (good_pods, bad_pods) { + logger.info('Updating %d good pods and %d bad pods scores.', good_pods.length, bad_pods.length) + + Pods.incrementScores(good_pods, constants.PODS_SCORE.BONUS, function (err) { + if (err) logger.error('Cannot increment scores of good pods.') + }) + + Pods.incrementScores(bad_pods, constants.PODS_SCORE.MALUS, function (err) { + if (err) logger.error('Cannot increment scores of bad pods.') + removeBadPods() + }) +} diff --git a/server/lib/videos.js b/server/lib/videos.js new file mode 100644 index 000000000..2d7d9500d --- /dev/null +++ b/server/lib/videos.js @@ -0,0 +1,50 @@ +'use strict' + +var async = require('async') +var config = require('config') +var path = require('path') +var webtorrent = require('../lib/webtorrent') + +var logger = require('../helpers/logger') +var Videos = require('../models/videos') + +var uploadDir = path.join(__dirname, '..', config.get('storage.uploads')) + +var videos = { + seed: seed, + seedAllExisting: seedAllExisting +} + +function seed (path, callback) { + logger.info('Seeding %s...', path) + + webtorrent.seed(path, function (torrent) { + logger.info('%s seeded (%s).', path, torrent.magnetURI) + + return callback(null, torrent) + }) +} + +function seedAllExisting (callback) { + Videos.listOwned(function (err, videos_list) { + if (err) { + logger.error('Cannot get list of the videos to seed.') + return callback(err) + } + + async.each(videos_list, function (video, each_callback) { + seed(uploadDir + video.namePath, function (err) { + if (err) { + logger.error('Cannot seed this video.') + return callback(err) + } + + each_callback(null) + }) + }, callback) + }) +} + +// --------------------------------------------------------------------------- + +module.exports = videos diff --git a/server/lib/webtorrent.js b/server/lib/webtorrent.js new file mode 100644 index 000000000..cb641fead --- /dev/null +++ b/server/lib/webtorrent.js @@ -0,0 +1,157 @@ +'use strict' + +var config = require('config') +var ipc = require('node-ipc') +var pathUtils = require('path') +var spawn = require('electron-spawn') + +var logger = require('../helpers/logger') + +var host = config.get('webserver.host') +var port = config.get('webserver.port') +var nodeKey = 'webtorrentnode' + port +var processKey = 'webtorrentprocess' + port +ipc.config.silent = true +ipc.config.id = nodeKey + +var webtorrent = { + add: add, + app: null, // Pid of the app + create: create, + remove: remove, + seed: seed, + silent: false // Useful for beautiful tests +} + +function create (options, callback) { + if (typeof options === 'function') { + callback = options + options = {} + } + + // Override options + if (options.host) host = options.host + if (options.port) { + port = options.port + nodeKey = 'webtorrentnode' + port + processKey = 'webtorrentprocess' + port + ipc.config.id = nodeKey + } + + ipc.serve(function () { + if (!webtorrent.silent) logger.info('IPC server ready.') + + // Run a timeout of 30s after which we exit the process + var timeout_webtorrent_process = setTimeout(function () { + throw new Error('Timeout : cannot run the webtorrent process. Please ensure you have electron-prebuilt npm package installed with xvfb-run.') + }, 30000) + + ipc.server.on(processKey + '.ready', function () { + if (!webtorrent.silent) logger.info('Webtorrent process ready.') + clearTimeout(timeout_webtorrent_process) + callback() + }) + + ipc.server.on(processKey + '.exception', function (data) { + throw new Error('Received exception error from webtorrent process.' + data.exception) + }) + + var webtorrent_process = spawn(pathUtils.join(__dirname, 'webtorrentProcess.js'), host, port, { detached: true }) + webtorrent_process.stderr.on('data', function (data) { + // logger.debug('Webtorrent process stderr: ', data.toString()) + }) + + webtorrent_process.stdout.on('data', function (data) { + // logger.debug('Webtorrent process:', data.toString()) + }) + + webtorrent.app = webtorrent_process + }) + + ipc.server.start() +} + +function seed (path, callback) { + var extension = pathUtils.extname(path) + var basename = pathUtils.basename(path, extension) + var data = { + _id: basename, + args: { + path: path + } + } + + if (!webtorrent.silent) logger.debug('Node wants to seed %s.', data._id) + + // Finish signal + var event_key = nodeKey + '.seedDone.' + data._id + ipc.server.on(event_key, function listener (received) { + if (!webtorrent.silent) logger.debug('Process seeded torrent %s.', received.magnetUri) + + // This is a fake object, we just use the magnetUri in this project + var torrent = { + magnetURI: received.magnetUri + } + + ipc.server.off(event_key) + callback(torrent) + }) + + ipc.server.broadcast(processKey + '.seed', data) +} + +function add (magnetUri, callback) { + var data = { + _id: magnetUri, + args: { + magnetUri: magnetUri + } + } + + if (!webtorrent.silent) logger.debug('Node wants to add ' + data._id) + + // Finish signal + var event_key = nodeKey + '.addDone.' + data._id + ipc.server.on(event_key, function (received) { + if (!webtorrent.silent) logger.debug('Process added torrent.') + + // This is a fake object, we just use the magnetUri in this project + var torrent = { + files: received.files + } + + ipc.server.off(event_key) + callback(torrent) + }) + + ipc.server.broadcast(processKey + '.add', data) +} + +function remove (magnetUri, callback) { + var data = { + _id: magnetUri, + args: { + magnetUri: magnetUri + } + } + + if (!webtorrent.silent) logger.debug('Node wants to stop seeding %s.', data._id) + + // Finish signal + var event_key = nodeKey + '.removeDone.' + data._id + ipc.server.on(event_key, function (received) { + if (!webtorrent.silent) logger.debug('Process removed torrent %s.', data._id) + + var err = null + if (received.err) err = received.err + + ipc.server.off(event_key) + callback(err) + }) + + ipc.server.broadcast(processKey + '.remove', data) +} + +// --------------------------------------------------------------------------- + +module.exports = webtorrent diff --git a/server/lib/webtorrentProcess.js b/server/lib/webtorrentProcess.js new file mode 100644 index 000000000..7da52523a --- /dev/null +++ b/server/lib/webtorrentProcess.js @@ -0,0 +1,92 @@ +'use strict' + +var WebTorrent = require('webtorrent') +var ipc = require('node-ipc') + +function webtorrent (args) { + if (args.length !== 3) { + throw new Error('Wrong arguments number: ' + args.length + '/3') + } + + var host = args[1] + var port = args[2] + var nodeKey = 'webtorrentnode' + port + var processKey = 'webtorrentprocess' + port + + ipc.config.silent = true + ipc.config.id = processKey + + if (host === 'client' && port === '1') global.WEBTORRENT_ANNOUNCE = [] + else global.WEBTORRENT_ANNOUNCE = 'ws://' + host + ':' + port + '/tracker/socket' + var wt = new WebTorrent({ dht: false }) + + function seed (data) { + var args = data.args + var path = args.path + var _id = data._id + + wt.seed(path, { announceList: '' }, function (torrent) { + var to_send = { + magnetUri: torrent.magnetURI + } + + ipc.of[nodeKey].emit(nodeKey + '.seedDone.' + _id, to_send) + }) + } + + function add (data) { + var args = data.args + var magnetUri = args.magnetUri + var _id = data._id + + wt.add(magnetUri, function (torrent) { + var to_send = { + files: [] + } + + torrent.files.forEach(function (file) { + to_send.files.push({ path: file.path }) + }) + + ipc.of[nodeKey].emit(nodeKey + '.addDone.' + _id, to_send) + }) + } + + function remove (data) { + var args = data.args + var magnetUri = args.magnetUri + var _id = data._id + + try { + wt.remove(magnetUri, callback) + } catch (err) { + console.log('Cannot remove the torrent from WebTorrent.') + return callback(null) + } + + function callback () { + var to_send = {} + ipc.of[nodeKey].emit(nodeKey + '.removeDone.' + _id, to_send) + } + } + + console.log('Configuration: ' + host + ':' + port) + console.log('Connecting to IPC...') + + ipc.connectTo(nodeKey, function () { + ipc.of[nodeKey].on(processKey + '.seed', seed) + ipc.of[nodeKey].on(processKey + '.add', add) + ipc.of[nodeKey].on(processKey + '.remove', remove) + + ipc.of[nodeKey].emit(processKey + '.ready') + console.log('Ready.') + }) + + process.on('uncaughtException', function (e) { + ipc.of[nodeKey].emit(processKey + '.exception', { exception: e }) + }) +} + +// --------------------------------------------------------------------------- + +module.exports = webtorrent diff --git a/server/middlewares/cache.js b/server/middlewares/cache.js new file mode 100644 index 000000000..0d3da0075 --- /dev/null +++ b/server/middlewares/cache.js @@ -0,0 +1,23 @@ +'use strict' + +var cacheMiddleware = { + cache: cache +} + +function cache (cache) { + return function (req, res, next) { + // If we want explicitly a cache + // Or if we don't specify if we want a cache or no and we are in production + if (cache === true || (cache !== false && process.env.NODE_ENV === 'production')) { + res.setHeader('Cache-Control', 'public') + } else { + res.setHeader('Cache-Control', 'no-cache, no-store, max-age=0, must-revalidate') + } + + next() + } +} + +// --------------------------------------------------------------------------- + +module.exports = cacheMiddleware diff --git a/server/middlewares/index.js b/server/middlewares/index.js new file mode 100644 index 000000000..c85899b0c --- /dev/null +++ b/server/middlewares/index.js @@ -0,0 +1,15 @@ +'use strict' + +var cacheMiddleware = require('./cache') +var reqValidatorsMiddleware = require('./reqValidators') +var secureMiddleware = require('./secure') + +var middlewares = { + cache: cacheMiddleware, + reqValidators: reqValidatorsMiddleware, + secure: secureMiddleware +} + +// --------------------------------------------------------------------------- + +module.exports = middlewares diff --git a/server/middlewares/reqValidators/index.js b/server/middlewares/reqValidators/index.js new file mode 100644 index 000000000..345dbd0e2 --- /dev/null +++ b/server/middlewares/reqValidators/index.js @@ -0,0 +1,15 @@ +'use strict' + +var podsReqValidators = require('./pods') +var remoteReqValidators = require('./remote') +var videosReqValidators = require('./videos') + +var reqValidators = { + pods: podsReqValidators, + remote: remoteReqValidators, + videos: videosReqValidators +} + +// --------------------------------------------------------------------------- + +module.exports = reqValidators diff --git a/server/middlewares/reqValidators/pods.js b/server/middlewares/reqValidators/pods.js new file mode 100644 index 000000000..ef09d51cf --- /dev/null +++ b/server/middlewares/reqValidators/pods.js @@ -0,0 +1,39 @@ +'use strict' + +var checkErrors = require('./utils').checkErrors +var friends = require('../../lib/friends') +var logger = require('../../helpers/logger') + +var reqValidatorsPod = { + makeFriends: makeFriends, + podsAdd: podsAdd +} + +function makeFriends (req, res, next) { + friends.hasFriends(function (err, has_friends) { + if (err) { + logger.error('Cannot know if we have friends.', { error: err }) + res.sendStatus(500) + } + + if (has_friends === true) { + // We need to quit our friends before make new ones + res.sendStatus(409) + } else { + return next() + } + }) +} + +function podsAdd (req, res, next) { + req.checkBody('data.url', 'Should have an url').notEmpty().isURL({ require_protocol: true }) + req.checkBody('data.publicKey', 'Should have a public key').notEmpty() + + logger.debug('Checking podsAdd parameters', { parameters: req.body }) + + checkErrors(req, res, next) +} + +// --------------------------------------------------------------------------- + +module.exports = reqValidatorsPod diff --git a/server/middlewares/reqValidators/remote.js b/server/middlewares/reqValidators/remote.js new file mode 100644 index 000000000..88de16b49 --- /dev/null +++ b/server/middlewares/reqValidators/remote.js @@ -0,0 +1,43 @@ +'use strict' + +var checkErrors = require('./utils').checkErrors +var logger = require('../../helpers/logger') + +var reqValidatorsRemote = { + remoteVideosAdd: remoteVideosAdd, + remoteVideosRemove: remoteVideosRemove, + secureRequest: secureRequest +} + +function remoteVideosAdd (req, res, next) { + req.checkBody('data').isArray() + req.checkBody('data').eachIsRemoteVideosAddValid() + + logger.debug('Checking remoteVideosAdd parameters', { parameters: req.body }) + + checkErrors(req, res, next) +} + +function remoteVideosRemove (req, res, next) { + req.checkBody('data').isArray() + req.checkBody('data').eachIsRemoteVideosRemoveValid() + + logger.debug('Checking remoteVideosRemove parameters', { parameters: req.body }) + + checkErrors(req, res, next) +} + +function secureRequest (req, res, next) { + req.checkBody('signature.url', 'Should have a signature url').isURL() + req.checkBody('signature.signature', 'Should have a signature').notEmpty() + req.checkBody('key', 'Should have a key').notEmpty() + req.checkBody('data', 'Should have data').notEmpty() + + logger.debug('Checking secureRequest parameters', { parameters: { data: req.body.data, keyLength: req.body.key.length } }) + + checkErrors(req, res, next) +} + +// --------------------------------------------------------------------------- + +module.exports = reqValidatorsRemote diff --git a/server/middlewares/reqValidators/utils.js b/server/middlewares/reqValidators/utils.js new file mode 100644 index 000000000..46c982571 --- /dev/null +++ b/server/middlewares/reqValidators/utils.js @@ -0,0 +1,25 @@ +'use strict' + +var util = require('util') + +var logger = require('../../helpers/logger') + +var reqValidatorsUtils = { + checkErrors: checkErrors +} + +function checkErrors (req, res, next, status_code) { + if (status_code === undefined) status_code = 400 + var errors = req.validationErrors() + + if (errors) { + logger.warn('Incorrect request parameters', { path: req.originalUrl, err: errors }) + return res.status(status_code).send('There have been validation errors: ' + util.inspect(errors)) + } + + return next() +} + +// --------------------------------------------------------------------------- + +module.exports = reqValidatorsUtils diff --git a/server/middlewares/reqValidators/videos.js b/server/middlewares/reqValidators/videos.js new file mode 100644 index 000000000..4e5f4391f --- /dev/null +++ b/server/middlewares/reqValidators/videos.js @@ -0,0 +1,74 @@ +'use strict' + +var checkErrors = require('./utils').checkErrors +var logger = require('../../helpers/logger') +var Videos = require('../../models/videos') + +var reqValidatorsVideos = { + videosAdd: videosAdd, + videosGet: videosGet, + videosRemove: videosRemove, + videosSearch: videosSearch +} + +function videosAdd (req, res, next) { + req.checkFiles('input_video[0].originalname', 'Should have an input video').notEmpty() + req.checkFiles('input_video[0].mimetype', 'Should have a correct mime type').matches(/video\/(webm)|(mp4)|(ogg)/i) + req.checkBody('name', 'Should have a name').isLength(1, 50) + req.checkBody('description', 'Should have a description').isLength(1, 250) + + logger.debug('Checking videosAdd parameters', { parameters: req.body, files: req.files }) + + checkErrors(req, res, next) +} + +function videosGet (req, res, next) { + req.checkParams('id', 'Should have a valid id').notEmpty().isMongoId() + + logger.debug('Checking videosGet parameters', { parameters: req.params }) + + checkErrors(req, res, function () { + Videos.getVideoState(req.params.id, function (err, state) { + if (err) { + logger.error('Error in videosGet request validator.', { error: err }) + res.sendStatus(500) + } + + if (state.exist === false) return res.status(404).send('Video not found') + + next() + }) + }) +} + +function videosRemove (req, res, next) { + req.checkParams('id', 'Should have a valid id').notEmpty().isMongoId() + + logger.debug('Checking videosRemove parameters', { parameters: req.params }) + + checkErrors(req, res, function () { + Videos.getVideoState(req.params.id, function (err, state) { + if (err) { + logger.error('Error in videosRemove request validator.', { error: err }) + res.sendStatus(500) + } + + if (state.exist === false) return res.status(404).send('Video not found') + else if (state.owned === false) return res.status(403).send('Cannot remove video of another pod') + + next() + }) + }) +} + +function videosSearch (req, res, next) { + req.checkParams('name', 'Should have a name').notEmpty() + + logger.debug('Checking videosSearch parameters', { parameters: req.params }) + + checkErrors(req, res, next) +} + +// --------------------------------------------------------------------------- + +module.exports = reqValidatorsVideos diff --git a/server/middlewares/secure.js b/server/middlewares/secure.js new file mode 100644 index 000000000..bfd28316a --- /dev/null +++ b/server/middlewares/secure.js @@ -0,0 +1,49 @@ +'use strict' + +var logger = require('../helpers/logger') +var peertubeCrypto = require('../helpers/peertubeCrypto') +var Pods = require('../models/pods') + +var secureMiddleware = { + decryptBody: decryptBody +} + +function decryptBody (req, res, next) { + var url = req.body.signature.url + Pods.findByUrl(url, function (err, pod) { + if (err) { + logger.error('Cannot get signed url in decryptBody.', { error: err }) + return res.sendStatus(500) + } + + if (pod === null) { + logger.error('Unknown pod %s.', url) + return res.sendStatus(403) + } + + logger.debug('Decrypting body from %s.', url) + + var signature_ok = peertubeCrypto.checkSignature(pod.publicKey, url, req.body.signature.signature) + + if (signature_ok === true) { + peertubeCrypto.decrypt(req.body.key, req.body.data, function (err, decrypted) { + if (err) { + logger.error('Cannot decrypt data.', { error: err }) + return res.sendStatus(500) + } + + req.body.data = JSON.parse(decrypted) + delete req.body.key + + next() + }) + } else { + logger.error('Signature is not okay in decryptBody for %s.', req.body.signature.url) + return res.sendStatus(403) + } + }) +} + +// --------------------------------------------------------------------------- + +module.exports = secureMiddleware diff --git a/server/models/pods.js b/server/models/pods.js new file mode 100644 index 000000000..57ed20292 --- /dev/null +++ b/server/models/pods.js @@ -0,0 +1,88 @@ +'use strict' + +var mongoose = require('mongoose') + +var constants = require('../initializers/constants') +var logger = require('../helpers/logger') + +// --------------------------------------------------------------------------- + +var podsSchema = mongoose.Schema({ + url: String, + publicKey: String, + score: { type: Number, max: constants.FRIEND_BASE_SCORE } +}) +var PodsDB = mongoose.model('pods', podsSchema) + +// --------------------------------------------------------------------------- + +var Pods = { + add: add, + count: count, + findByUrl: findByUrl, + findBadPods: findBadPods, + incrementScores: incrementScores, + list: list, + remove: remove, + removeAll: removeAll, + removeAllByIds: removeAllByIds +} + +// TODO: check if the pod is not already a friend +function add (data, callback) { + if (!callback) callback = function () {} + var params = { + url: data.url, + publicKey: data.publicKey, + score: constants.FRIEND_BASE_SCORE + } + + PodsDB.create(params, callback) +} + +function count (callback) { + return PodsDB.count(callback) +} + +function findBadPods (callback) { + PodsDB.find({ score: 0 }, callback) +} + +function findByUrl (url, callback) { + PodsDB.findOne({ url: url }, callback) +} + +function incrementScores (ids, value, callback) { + if (!callback) callback = function () {} + PodsDB.update({ _id: { $in: ids } }, { $inc: { score: value } }, { multi: true }, callback) +} + +function list (callback) { + PodsDB.find(function (err, pods_list) { + if (err) { + logger.error('Cannot get the list of the pods.') + return callback(err) + } + + return callback(null, pods_list) + }) +} + +function remove (url, callback) { + if (!callback) callback = function () {} + PodsDB.remove({ url: url }, callback) +} + +function removeAll (callback) { + if (!callback) callback = function () {} + PodsDB.remove(callback) +} + +function removeAllByIds (ids, callback) { + if (!callback) callback = function () {} + PodsDB.remove({ _id: { $in: ids } }, callback) +} + +// --------------------------------------------------------------------------- + +module.exports = Pods diff --git a/server/models/poolRequests.js b/server/models/poolRequests.js new file mode 100644 index 000000000..970315597 --- /dev/null +++ b/server/models/poolRequests.js @@ -0,0 +1,55 @@ +'use strict' + +var mongoose = require('mongoose') + +var logger = require('../helpers/logger') + +// --------------------------------------------------------------------------- + +var poolRequestsSchema = mongoose.Schema({ + type: String, + id: String, // Special id to find duplicates (video created we want to remove...) + request: mongoose.Schema.Types.Mixed +}) +var PoolRequestsDB = mongoose.model('poolRequests', poolRequestsSchema) + +// --------------------------------------------------------------------------- + +var PoolRequests = { + create: create, + findById: findById, + list: list, + removeRequestById: removeRequestById, + removeRequests: removeRequests +} + +function create (id, type, request, callback) { + PoolRequestsDB.create({ id: id, type: type, request: request }, callback) +} + +function findById (id, callback) { + PoolRequestsDB.findOne({ id: id }, callback) +} + +function list (callback) { + PoolRequestsDB.find({}, { _id: 1, type: 1, request: 1 }, callback) +} + +function removeRequestById (id, callback) { + PoolRequestsDB.remove({ id: id }, callback) +} + +function removeRequests (ids) { + PoolRequestsDB.remove({ _id: { $in: ids } }, function (err) { + if (err) { + logger.error('Cannot remove requests from the pool requests database.', { error: err }) + return // Abort + } + + logger.info('Pool requests flushed.') + }) +} + +// --------------------------------------------------------------------------- + +module.exports = PoolRequests diff --git a/server/models/videos.js b/server/models/videos.js new file mode 100644 index 000000000..5e2eeae07 --- /dev/null +++ b/server/models/videos.js @@ -0,0 +1,234 @@ +'use strict' + +var async = require('async') +var config = require('config') +var dz = require('dezalgo') +var fs = require('fs') +var mongoose = require('mongoose') +var path = require('path') + +var logger = require('../helpers/logger') + +var http = config.get('webserver.https') === true ? 'https' : 'http' +var host = config.get('webserver.host') +var port = config.get('webserver.port') +var uploadDir = path.join(__dirname, '..', config.get('storage.uploads')) + +// --------------------------------------------------------------------------- + +var videosSchema = mongoose.Schema({ + name: String, + namePath: String, + description: String, + magnetUri: String, + podUrl: String +}) +var VideosDB = mongoose.model('videos', videosSchema) + +// --------------------------------------------------------------------------- + +var Videos = { + add: add, + addRemotes: addRemotes, + get: get, + getVideoState: getVideoState, + isOwned: isOwned, + list: list, + listOwned: listOwned, + removeOwned: removeOwned, + removeAllRemotes: removeAllRemotes, + removeAllRemotesOf: removeAllRemotesOf, + removeRemotesOfByMagnetUris: removeRemotesOfByMagnetUris, + search: search +} + +function add (video, callback) { + logger.info('Adding %s video to database.', video.name) + + var params = video + params.podUrl = http + '://' + host + ':' + port + + VideosDB.create(params, function (err, video) { + if (err) { + logger.error('Cannot insert this video into database.') + return callback(err) + } + + callback(null) + }) +} + +// TODO: avoid doublons +function addRemotes (videos, callback) { + if (!callback) callback = function () {} + + var to_add = [] + + async.each(videos, function (video, callback_each) { + callback_each = dz(callback_each) + logger.debug('Add remote video from pod: %s', video.podUrl) + + var params = { + name: video.name, + namePath: null, + description: video.description, + magnetUri: video.magnetUri, + podUrl: video.podUrl + } + + to_add.push(params) + + callback_each() + }, function () { + VideosDB.create(to_add, function (err, videos) { + if (err) { + logger.error('Cannot insert this remote video.') + return callback(err) + } + + return callback(null, videos) + }) + }) +} + +function get (id, callback) { + VideosDB.findById(id, function (err, video) { + if (err) { + logger.error('Cannot get this video.') + return callback(err) + } + + return callback(null, video) + }) +} + +function getVideoState (id, callback) { + get(id, function (err, video) { + if (err) return callback(err) + + var exist = (video !== null) + var owned = false + if (exist === true) { + owned = (video.namePath !== null) + } + + return callback(null, { exist: exist, owned: owned }) + }) +} + +function isOwned (id, callback) { + VideosDB.findById(id, function (err, video) { + if (err || !video) { + if (!err) err = new Error('Cannot find this video.') + logger.error('Cannot find this video.') + return callback(err) + } + + if (video.namePath === null) { + var error_string = 'Cannot remove the video of another pod.' + logger.error(error_string) + return callback(new Error(error_string), false, video) + } + + callback(null, true, video) + }) +} + +function list (callback) { + VideosDB.find(function (err, videos_list) { + if (err) { + logger.error('Cannot get the list of the videos.') + return callback(err) + } + + return callback(null, videos_list) + }) +} + +function listOwned (callback) { + // If namePath is not null this is *our* video + VideosDB.find({ namePath: { $ne: null } }, function (err, videos_list) { + if (err) { + logger.error('Cannot get the list of owned videos.') + return callback(err) + } + + return callback(null, videos_list) + }) +} + +function removeOwned (id, callback) { + VideosDB.findByIdAndRemove(id, function (err, video) { + if (err) { + logger.error('Cannot remove the torrent.') + return callback(err) + } + + fs.unlink(uploadDir + video.namePath, function (err) { + if (err) { + logger.error('Cannot remove this video file.') + return callback(err) + } + + callback(null) + }) + }) +} + +function removeAllRemotes (callback) { + VideosDB.remove({ namePath: null }, callback) +} + +function removeAllRemotesOf (fromUrl, callback) { + VideosDB.remove({ podUrl: fromUrl }, callback) +} + +// Use the magnet Uri because the _id field is not the same on different servers +function removeRemotesOfByMagnetUris (fromUrl, magnetUris, callback) { + if (callback === undefined) callback = function () {} + + VideosDB.find({ magnetUri: { $in: magnetUris } }, function (err, videos) { + if (err || !videos) { + logger.error('Cannot find the torrent URI of these remote videos.') + return callback(err) + } + + var to_remove = [] + async.each(videos, function (video, callback_async) { + callback_async = dz(callback_async) + + if (video.podUrl !== fromUrl) { + logger.error('The pod %s has not the rights on the video of %s.', fromUrl, video.podUrl) + } else { + to_remove.push(video._id) + } + + callback_async() + }, function () { + VideosDB.remove({ _id: { $in: to_remove } }, function (err) { + if (err) { + logger.error('Cannot remove the remote videos.') + return callback(err) + } + + logger.info('Removed remote videos from %s.', fromUrl) + callback(null) + }) + }) + }) +} + +function search (name, callback) { + VideosDB.find({ name: new RegExp(name) }, function (err, videos) { + if (err) { + logger.error('Cannot search the videos.') + return callback(err) + } + + return callback(null, videos) + }) +} + +// --------------------------------------------------------------------------- + +module.exports = Videos diff --git a/server/tests/api/checkParams.js b/server/tests/api/checkParams.js new file mode 100644 index 000000000..1c1ec71b3 --- /dev/null +++ b/server/tests/api/checkParams.js @@ -0,0 +1,300 @@ +'use strict' + +var async = require('async') +var chai = require('chai') +var expect = chai.expect +var pathUtils = require('path') +var request = require('supertest') + +var utils = require('./utils') + +describe('Test parameters validator', function () { + var app = null + var url = '' + + function makePostRequest (path, fields, attach, done, fail) { + var status_code = 400 + if (fail !== undefined && fail === false) status_code = 200 + + var req = request(url) + .post(path) + .set('Accept', 'application/json') + + Object.keys(fields).forEach(function (field) { + var value = fields[field] + req.field(field, value) + }) + + req.expect(status_code, done) + } + + function makePostBodyRequest (path, fields, done, fail) { + var status_code = 400 + if (fail !== undefined && fail === false) status_code = 200 + + request(url) + .post(path) + .set('Accept', 'application/json') + .send(fields) + .expect(status_code, done) + } + + // --------------------------------------------------------------- + + before(function (done) { + this.timeout(20000) + + async.series([ + function (next) { + utils.flushTests(next) + }, + function (next) { + utils.runServer(1, function (app1, url1) { + app = app1 + url = url1 + next() + }) + } + ], done) + }) + + describe('Of the pods API', function () { + var path = '/api/v1/pods/' + + describe('When adding a pod', function () { + it('Should fail with nothing', function (done) { + var data = {} + makePostBodyRequest(path, data, done) + }) + + it('Should fail without public key', function (done) { + var data = { + data: { + url: 'http://coucou.com' + } + } + makePostBodyRequest(path, data, done) + }) + + it('Should fail without an url', function (done) { + var data = { + data: { + publicKey: 'mysuperpublickey' + } + } + makePostBodyRequest(path, data, done) + }) + + it('Should fail with an incorrect url', function (done) { + var data = { + data: { + url: 'coucou.com', + publicKey: 'mysuperpublickey' + } + } + makePostBodyRequest(path, data, function () { + data.data.url = 'http://coucou' + makePostBodyRequest(path, data, function () { + data.data.url = 'coucou' + makePostBodyRequest(path, data, done) + }) + }) + }) + + it('Should succeed with the correct parameters', function (done) { + var data = { + data: { + url: 'http://coucou.com', + publicKey: 'mysuperpublickey' + } + } + makePostBodyRequest(path, data, done, false) + }) + }) + }) + + describe('Of the videos API', function () { + var path = '/api/v1/videos/' + + describe('When searching a video', function () { + it('Should fail with nothing', function (done) { + request(url) + .get(pathUtils.join(path, 'search')) + .set('Accept', 'application/json') + .expect(400, done) + }) + }) + + describe('When adding a video', function () { + it('Should fail with nothing', function (done) { + var data = {} + var attach = {} + makePostRequest(path, data, attach, done) + }) + + it('Should fail without name', function (done) { + var data = { + description: 'my super description' + } + var attach = { + 'input_video': pathUtils.join(__dirname, 'fixtures', 'video_short.webm') + } + makePostRequest(path, data, attach, done) + }) + + it('Should fail with a long name', function (done) { + var data = { + name: 'My very very very very very very very very very very very very very very very very long name', + description: 'my super description' + } + var attach = { + 'input_video': pathUtils.join(__dirname, 'fixtures', 'video_short.webm') + } + makePostRequest(path, data, attach, done) + }) + + it('Should fail without description', function (done) { + var data = { + name: 'my super name' + } + var attach = { + 'input_video': pathUtils.join(__dirname, 'fixtures', 'video_short.webm') + } + makePostRequest(path, data, attach, done) + }) + + it('Should fail with a long description', function (done) { + var data = { + name: 'my super name', + description: 'my super description which is very very very very very very very very very very very very very very' + + 'very very very very very very very very very very very very very very very very very very very very very' + + 'very very very very very very very very very very very very very very very long' + } + var attach = { + 'input_video': pathUtils.join(__dirname, 'fixtures', 'video_short.webm') + } + makePostRequest(path, data, attach, done) + }) + + it('Should fail without an input file', function (done) { + var data = { + name: 'my super name', + description: 'my super description' + } + var attach = {} + makePostRequest(path, data, attach, done) + }) + + it('Should fail without an incorrect input file', function (done) { + var data = { + name: 'my super name', + description: 'my super description' + } + var attach = { + 'input_video': pathUtils.join(__dirname, '..', 'fixtures', 'video_short_fake.webm') + } + makePostRequest(path, data, attach, done) + }) + + it('Should succeed with the correct parameters', function (done) { + var data = { + name: 'my super name', + description: 'my super description' + } + var attach = { + 'input_video': pathUtils.join(__dirname, 'fixtures', 'video_short.webm') + } + makePostRequest(path, data, attach, function () { + attach.input_video = pathUtils.join(__dirname, 'fixtures', 'video_short.mp4') + makePostRequest(path, data, attach, function () { + attach.input_video = pathUtils.join(__dirname, 'fixtures', 'video_short.ogv') + makePostRequest(path, data, attach, done, true) + }, true) + }, true) + }) + }) + + describe('When getting a video', function () { + it('Should return the list of the videos with nothing', function (done) { + request(url) + .get(path) + .set('Accept', 'application/json') + .expect(200) + .expect('Content-Type', /json/) + .end(function (err, res) { + if (err) throw err + + expect(res.body).to.be.an('array') + expect(res.body.length).to.equal(0) + + done() + }) + }) + + it('Should fail without a mongodb id', function (done) { + request(url) + .get(path + 'coucou') + .set('Accept', 'application/json') + .expect(400, done) + }) + + it('Should return 404 with an incorrect video', function (done) { + request(url) + .get(path + '123456789012345678901234') + .set('Accept', 'application/json') + .expect(404, done) + }) + + it('Should succeed with the correct parameters') + }) + + describe('When removing a video', function () { + it('Should have 404 with nothing', function (done) { + request(url) + .delete(path) + .expect(404, done) + }) + + it('Should fail without a mongodb id', function (done) { + request(url) + .delete(path + 'hello') + .expect(400, done) + }) + + it('Should fail with a video which does not exist', function (done) { + request(url) + .delete(path + '123456789012345678901234') + .expect(404, done) + }) + + it('Should fail with a video of another pod') + + it('Should succeed with the correct parameters') + }) + }) + + describe('Of the remote videos API', function () { + describe('When making a secure request', function () { + it('Should check a secure request') + }) + + describe('When adding a video', function () { + it('Should check when adding a video') + }) + + describe('When removing a video', function () { + it('Should check when removing a video') + }) + }) + + after(function (done) { + process.kill(-app.pid) + + // Keep the logs if the test failed + if (this.ok) { + utils.flushTests(done) + } else { + done() + } + }) +}) diff --git a/server/tests/api/fixtures/video_short.mp4 b/server/tests/api/fixtures/video_short.mp4 new file mode 100644 index 000000000..35678362b Binary files /dev/null and b/server/tests/api/fixtures/video_short.mp4 differ diff --git a/server/tests/api/fixtures/video_short.ogv b/server/tests/api/fixtures/video_short.ogv new file mode 100644 index 000000000..9e253da82 Binary files /dev/null and b/server/tests/api/fixtures/video_short.ogv differ diff --git a/server/tests/api/fixtures/video_short.webm b/server/tests/api/fixtures/video_short.webm new file mode 100644 index 000000000..bf4b0ab6c Binary files /dev/null and b/server/tests/api/fixtures/video_short.webm differ diff --git a/server/tests/api/fixtures/video_short1.webm b/server/tests/api/fixtures/video_short1.webm new file mode 100644 index 000000000..70ac0c644 Binary files /dev/null and b/server/tests/api/fixtures/video_short1.webm differ diff --git a/server/tests/api/fixtures/video_short2.webm b/server/tests/api/fixtures/video_short2.webm new file mode 100644 index 000000000..13d72dff7 Binary files /dev/null and b/server/tests/api/fixtures/video_short2.webm differ diff --git a/server/tests/api/fixtures/video_short3.webm b/server/tests/api/fixtures/video_short3.webm new file mode 100644 index 000000000..cde5dcd58 Binary files /dev/null and b/server/tests/api/fixtures/video_short3.webm differ diff --git a/server/tests/api/fixtures/video_short_fake.webm b/server/tests/api/fixtures/video_short_fake.webm new file mode 100644 index 000000000..d85290ae5 --- /dev/null +++ b/server/tests/api/fixtures/video_short_fake.webm @@ -0,0 +1 @@ +this is a fake video mouahahah diff --git a/server/tests/api/friendsAdvanced.js b/server/tests/api/friendsAdvanced.js new file mode 100644 index 000000000..9838d890f --- /dev/null +++ b/server/tests/api/friendsAdvanced.js @@ -0,0 +1,250 @@ +'use strict' + +var async = require('async') +var chai = require('chai') +var expect = chai.expect + +var utils = require('./utils') + +describe('Test advanced friends', function () { + var apps = [] + var urls = [] + + function makeFriends (pod_number, callback) { + return utils.makeFriends(urls[pod_number - 1], callback) + } + + function quitFriends (pod_number, callback) { + return utils.quitFriends(urls[pod_number - 1], callback) + } + + function getFriendsList (pod_number, end) { + return utils.getFriendsList(urls[pod_number - 1], end) + } + + function uploadVideo (pod_number, callback) { + var name = 'my super video' + var description = 'my super description' + var fixture = 'video_short.webm' + + return utils.uploadVideo(urls[pod_number - 1], name, description, fixture, callback) + } + + function getVideos (pod_number, callback) { + return utils.getVideosList(urls[pod_number - 1], callback) + } + + // --------------------------------------------------------------- + + before(function (done) { + this.timeout(30000) + utils.flushAndRunMultipleServers(6, function (apps_run, urls_run) { + apps = apps_run + urls = urls_run + done() + }) + }) + + it('Should make friends with two pod each in a different group', function (done) { + this.timeout(20000) + + async.series([ + // Pod 3 makes friend with the first one + function (next) { + makeFriends(3, next) + }, + // Pod 4 makes friend with the second one + function (next) { + makeFriends(4, next) + }, + // Now if the fifth wants to make friends with the third et the first + function (next) { + makeFriends(5, next) + }, + function (next) { + setTimeout(next, 11000) + }], + function (err) { + if (err) throw err + + // It should have 0 friends + getFriendsList(5, function (err, res) { + if (err) throw err + + expect(res.body.length).to.equal(0) + + done() + }) + } + ) + }) + + it('Should quit all friends', function (done) { + this.timeout(10000) + + async.series([ + function (next) { + quitFriends(1, next) + }, + function (next) { + quitFriends(2, next) + }], + function (err) { + if (err) throw err + + async.each([ 1, 2, 3, 4, 5, 6 ], function (i, callback) { + getFriendsList(i, function (err, res) { + if (err) throw err + + expect(res.body.length).to.equal(0) + + callback() + }) + }, done) + } + ) + }) + + it('Should make friends with the pods 1, 2, 3', function (done) { + this.timeout(150000) + + async.series([ + // Pods 1, 2, 3 and 4 become friends + function (next) { + makeFriends(2, next) + }, + function (next) { + makeFriends(1, next) + }, + function (next) { + makeFriends(4, next) + }, + // Kill pod 4 + function (next) { + apps[3].kill() + next() + }, + // Expulse pod 4 from pod 1 and 2 + function (next) { + uploadVideo(1, next) + }, + function (next) { + uploadVideo(2, next) + }, + function (next) { + setTimeout(next, 11000) + }, + function (next) { + uploadVideo(1, next) + }, + function (next) { + uploadVideo(2, next) + }, + function (next) { + setTimeout(next, 20000) + }, + // Rerun server 4 + function (next) { + utils.runServer(4, function (app, url) { + apps[3] = app + next() + }) + }, + function (next) { + getFriendsList(4, function (err, res) { + if (err) throw err + + // Pod 4 didn't know pod 1 and 2 removed it + expect(res.body.length).to.equal(3) + + next() + }) + }, + // Pod 6 ask pod 1, 2 and 3 + function (next) { + makeFriends(6, next) + }], + function (err) { + if (err) throw err + + getFriendsList(6, function (err, res) { + if (err) throw err + + // Pod 4 should not be our friend + var result = res.body + expect(result.length).to.equal(3) + for (var pod of result) { + expect(pod.url).not.equal(urls[3]) + } + + done() + }) + } + ) + }) + + it('Should pod 1 quit friends', function (done) { + this.timeout(25000) + + async.series([ + // Upload a video on server 3 for aditionnal tests + function (next) { + uploadVideo(3, next) + }, + function (next) { + setTimeout(next, 15000) + }, + function (next) { + quitFriends(1, next) + }, + // Remove pod 1 from pod 2 + function (next) { + getVideos(1, function (err, res) { + if (err) throw err + expect(res.body).to.be.an('array') + expect(res.body.length).to.equal(2) + + next() + }) + }], + function (err) { + if (err) throw err + + getVideos(2, function (err, res) { + if (err) throw err + expect(res.body).to.be.an('array') + expect(res.body.length).to.equal(3) + done() + }) + } + ) + }) + + it('Should make friends between pod 1 and 2 and exchange their videos', function (done) { + this.timeout(20000) + makeFriends(1, function () { + setTimeout(function () { + getVideos(1, function (err, res) { + if (err) throw err + + expect(res.body).to.be.an('array') + expect(res.body.length).to.equal(5) + + done() + }) + }, 5000) + }) + }) + + after(function (done) { + apps.forEach(function (app) { + process.kill(-app.pid) + }) + + if (this.ok) { + utils.flushTests(done) + } else { + done() + } + }) +}) diff --git a/server/tests/api/friendsBasic.js b/server/tests/api/friendsBasic.js new file mode 100644 index 000000000..328724936 --- /dev/null +++ b/server/tests/api/friendsBasic.js @@ -0,0 +1,185 @@ +'use strict' + +var async = require('async') +var chai = require('chai') +var expect = chai.expect +var request = require('supertest') + +var utils = require('./utils') + +describe('Test basic friends', function () { + var apps = [] + var urls = [] + + function testMadeFriends (urls, url_to_test, callback) { + var friends = [] + for (var i = 0; i < urls.length; i++) { + if (urls[i] === url_to_test) continue + friends.push(urls[i]) + } + + utils.getFriendsList(url_to_test, function (err, res) { + if (err) throw err + + var result = res.body + var result_urls = [ result[0].url, result[1].url ] + expect(result).to.be.an('array') + expect(result.length).to.equal(2) + expect(result_urls[0]).to.not.equal(result_urls[1]) + + var error_string = 'Friends url do not correspond for ' + url_to_test + expect(friends).to.contain(result_urls[0], error_string) + expect(friends).to.contain(result_urls[1], error_string) + callback() + }) + } + + // --------------------------------------------------------------- + + before(function (done) { + this.timeout(20000) + utils.flushAndRunMultipleServers(3, function (apps_run, urls_run) { + apps = apps_run + urls = urls_run + done() + }) + }) + + it('Should not have friends', function (done) { + async.each(urls, function (url, callback) { + utils.getFriendsList(url, function (err, res) { + if (err) throw err + + var result = res.body + expect(result).to.be.an('array') + expect(result.length).to.equal(0) + callback() + }) + }, done) + }) + + it('Should make friends', function (done) { + this.timeout(10000) + + var path = '/api/v1/pods/makefriends' + + async.series([ + // The second pod make friend with the third + function (next) { + request(urls[1]) + .get(path) + .set('Accept', 'application/json') + .expect(204) + .end(next) + }, + // Wait for the request between pods + function (next) { + setTimeout(next, 1000) + }, + // The second pod should have the third as a friend + function (next) { + utils.getFriendsList(urls[1], function (err, res) { + if (err) throw err + + var result = res.body + expect(result).to.be.an('array') + expect(result.length).to.equal(1) + expect(result[0].url).to.be.equal(urls[2]) + + next() + }) + }, + // Same here, the third pod should have the second pod as a friend + function (next) { + utils.getFriendsList(urls[2], function (err, res) { + if (err) throw err + + var result = res.body + expect(result).to.be.an('array') + expect(result.length).to.equal(1) + expect(result[0].url).to.be.equal(urls[1]) + + next() + }) + }, + // Finally the first pod make friend with the second pod + function (next) { + request(urls[0]) + .get(path) + .set('Accept', 'application/json') + .expect(204) + .end(next) + }, + // Wait for the request between pods + function (next) { + setTimeout(next, 1000) + } + ], + // Now each pod should be friend with the other ones + function (err) { + if (err) throw err + async.each(urls, function (url, callback) { + testMadeFriends(urls, url, callback) + }, done) + }) + }) + + it('Should not be allowed to make friend again', function (done) { + utils.makeFriends(urls[1], 409, done) + }) + + it('Should quit friends of pod 2', function (done) { + async.series([ + // Pod 1 quit friends + function (next) { + utils.quitFriends(urls[1], next) + }, + // Pod 1 should not have friends anymore + function (next) { + utils.getFriendsList(urls[1], function (err, res) { + if (err) throw err + + var result = res.body + expect(result).to.be.an('array') + expect(result.length).to.equal(0) + + next() + }) + }, + // Other pods shouldn't have pod 1 too + function (next) { + async.each([ urls[0], urls[2] ], function (url, callback) { + utils.getFriendsList(url, function (err, res) { + if (err) throw err + + var result = res.body + expect(result).to.be.an('array') + expect(result.length).to.equal(1) + expect(result[0].url).not.to.be.equal(urls[1]) + callback() + }) + }, next) + } + ], done) + }) + + it('Should allow pod 2 to make friend again', function (done) { + utils.makeFriends(urls[1], function () { + async.each(urls, function (url, callback) { + testMadeFriends(urls, url, callback) + }, done) + }) + }) + + after(function (done) { + apps.forEach(function (app) { + process.kill(-app.pid) + }) + + if (this.ok) { + utils.flushTests(done) + } else { + done() + } + }) +}) diff --git a/server/tests/api/index.js b/server/tests/api/index.js new file mode 100644 index 000000000..9c4fdd48a --- /dev/null +++ b/server/tests/api/index.js @@ -0,0 +1,8 @@ +'use strict' + +// Order of the tests we want to execute +require('./checkParams') +require('./friendsBasic') +require('./singlePod') +require('./multiplePods') +require('./friendsAdvanced') diff --git a/server/tests/api/multiplePods.js b/server/tests/api/multiplePods.js new file mode 100644 index 000000000..9fdd0f308 --- /dev/null +++ b/server/tests/api/multiplePods.js @@ -0,0 +1,328 @@ +'use strict' + +var async = require('async') +var chai = require('chai') +var expect = chai.expect +var pathUtils = require('path') + +var utils = require('./utils') +var webtorrent = require(pathUtils.join(__dirname, '../../lib/webtorrent')) +webtorrent.silent = true + +describe('Test multiple pods', function () { + var apps = [] + var urls = [] + var to_remove = [] + + before(function (done) { + this.timeout(30000) + + async.series([ + // Run servers + function (next) { + utils.flushAndRunMultipleServers(3, function (apps_run, urls_run) { + apps = apps_run + urls = urls_run + next() + }) + }, + // The second pod make friend with the third + function (next) { + utils.makeFriends(urls[1], next) + }, + // Wait for the request between pods + function (next) { + setTimeout(next, 10000) + }, + // Pod 1 make friends too + function (next) { + utils.makeFriends(urls[0], next) + }, + function (next) { + webtorrent.create({ host: 'client', port: '1' }, next) + } + ], done) + }) + + it('Should not have videos for all pods', function (done) { + async.each(urls, function (url, callback) { + utils.getVideosList(url, function (err, res) { + if (err) throw err + + expect(res.body).to.be.an('array') + expect(res.body.length).to.equal(0) + + callback() + }) + }, done) + }) + + describe('Should upload the video and propagate on each pod', function () { + it('Should upload the video on pod 1 and propagate on each pod', function (done) { + this.timeout(15000) + + async.series([ + function (next) { + utils.uploadVideo(urls[0], 'my super name for pod 1', 'my super description for pod 1', 'video_short1.webm', next) + }, + function (next) { + setTimeout(next, 11000) + }], + // All pods should have this video + function (err) { + if (err) throw err + + async.each(urls, function (url, callback) { + var base_magnet = null + + utils.getVideosList(url, function (err, res) { + if (err) throw err + + var videos = res.body + expect(videos).to.be.an('array') + expect(videos.length).to.equal(1) + var video = videos[0] + expect(video.name).to.equal('my super name for pod 1') + expect(video.description).to.equal('my super description for pod 1') + expect(video.podUrl).to.equal('http://localhost:9001') + expect(video.magnetUri).to.exist + + // All pods should have the same magnet Uri + if (base_magnet === null) { + base_magnet = video.magnetUri + } else { + expect(video.magnetUri).to.equal.magnetUri + } + + callback() + }) + }, done) + } + ) + }) + + it('Should upload the video on pod 2 and propagate on each pod', function (done) { + this.timeout(15000) + + async.series([ + function (next) { + utils.uploadVideo(urls[1], 'my super name for pod 2', 'my super description for pod 2', 'video_short2.webm', next) + }, + function (next) { + setTimeout(next, 11000) + }], + // All pods should have this video + function (err) { + if (err) throw err + + async.each(urls, function (url, callback) { + var base_magnet = null + + utils.getVideosList(url, function (err, res) { + if (err) throw err + + var videos = res.body + expect(videos).to.be.an('array') + expect(videos.length).to.equal(2) + var video = videos[1] + expect(video.name).to.equal('my super name for pod 2') + expect(video.description).to.equal('my super description for pod 2') + expect(video.podUrl).to.equal('http://localhost:9002') + expect(video.magnetUri).to.exist + + // All pods should have the same magnet Uri + if (base_magnet === null) { + base_magnet = video.magnetUri + } else { + expect(video.magnetUri).to.equal.magnetUri + } + + callback() + }) + }, done) + } + ) + }) + + it('Should upload two videos on pod 3 and propagate on each pod', function (done) { + this.timeout(30000) + + async.series([ + function (next) { + utils.uploadVideo(urls[2], 'my super name for pod 3', 'my super description for pod 3', 'video_short3.webm', next) + }, + function (next) { + utils.uploadVideo(urls[2], 'my super name for pod 3-2', 'my super description for pod 3-2', 'video_short.webm', next) + }, + function (next) { + setTimeout(next, 22000) + }], + function (err) { + if (err) throw err + + var base_magnet = null + // All pods should have this video + async.each(urls, function (url, callback) { + utils.getVideosList(url, function (err, res) { + if (err) throw err + + var videos = res.body + expect(videos).to.be.an('array') + expect(videos.length).to.equal(4) + var video = videos[2] + expect(video.name).to.equal('my super name for pod 3') + expect(video.description).to.equal('my super description for pod 3') + expect(video.podUrl).to.equal('http://localhost:9003') + expect(video.magnetUri).to.exist + + video = videos[3] + expect(video.name).to.equal('my super name for pod 3-2') + expect(video.description).to.equal('my super description for pod 3-2') + expect(video.podUrl).to.equal('http://localhost:9003') + expect(video.magnetUri).to.exist + + // All pods should have the same magnet Uri + if (base_magnet === null) { + base_magnet = video.magnetUri + } else { + expect(video.magnetUri).to.equal.magnetUri + } + + callback() + }) + }, done) + } + ) + }) + }) + + describe('Should seed the uploaded video', function () { + it('Should add the file 1 by asking pod 3', function (done) { + // Yes, this could be long + this.timeout(200000) + + utils.getVideosList(urls[2], function (err, res) { + if (err) throw err + + var video = res.body[0] + to_remove.push(res.body[2]._id) + to_remove.push(res.body[3]._id) + + webtorrent.add(video.magnetUri, function (torrent) { + expect(torrent.files).to.exist + expect(torrent.files.length).to.equal(1) + expect(torrent.files[0].path).to.exist.and.to.not.equal('') + + done() + }) + }) + }) + + it('Should add the file 2 by asking pod 1', function (done) { + // Yes, this could be long + this.timeout(200000) + + utils.getVideosList(urls[0], function (err, res) { + if (err) throw err + + var video = res.body[1] + + webtorrent.add(video.magnetUri, function (torrent) { + expect(torrent.files).to.exist + expect(torrent.files.length).to.equal(1) + expect(torrent.files[0].path).to.exist.and.to.not.equal('') + + done() + }) + }) + }) + + it('Should add the file 3 by asking pod 2', function (done) { + // Yes, this could be long + this.timeout(200000) + + utils.getVideosList(urls[1], function (err, res) { + if (err) throw err + + var video = res.body[2] + + webtorrent.add(video.magnetUri, function (torrent) { + expect(torrent.files).to.exist + expect(torrent.files.length).to.equal(1) + expect(torrent.files[0].path).to.exist.and.to.not.equal('') + + done() + }) + }) + }) + + it('Should add the file 3-2 by asking pod 1', function (done) { + // Yes, this could be long + this.timeout(200000) + + utils.getVideosList(urls[0], function (err, res) { + if (err) throw err + + var video = res.body[3] + + webtorrent.add(video.magnetUri, function (torrent) { + expect(torrent.files).to.exist + expect(torrent.files.length).to.equal(1) + expect(torrent.files[0].path).to.exist.and.to.not.equal('') + + done() + }) + }) + }) + + it('Should remove the file 3 and 3-2 by asking pod 3', function (done) { + this.timeout(15000) + + async.series([ + function (next) { + utils.removeVideo(urls[2], to_remove[0], next) + }, + function (next) { + utils.removeVideo(urls[2], to_remove[1], next) + }], + function (err) { + if (err) throw err + setTimeout(done, 11000) + } + ) + }) + + it('Should have videos 1 and 3 on each pod', function (done) { + async.each(urls, function (url, callback) { + utils.getVideosList(url, function (err, res) { + if (err) throw err + + var videos = res.body + expect(videos).to.be.an('array') + expect(videos.length).to.equal(2) + expect(videos[0]._id).not.to.equal(videos[1]._id) + expect(videos[0]._id).not.to.equal(to_remove[0]) + expect(videos[1]._id).not.to.equal(to_remove[0]) + expect(videos[0]._id).not.to.equal(to_remove[1]) + expect(videos[1]._id).not.to.equal(to_remove[1]) + + callback() + }) + }, done) + }) + }) + + after(function (done) { + apps.forEach(function (app) { + process.kill(-app.pid) + }) + process.kill(-webtorrent.app.pid) + + // Keep the logs if the test failed + if (this.ok) { + utils.flushTests(done) + } else { + done() + } + }) +}) diff --git a/server/tests/api/singlePod.js b/server/tests/api/singlePod.js new file mode 100644 index 000000000..3dd72c01b --- /dev/null +++ b/server/tests/api/singlePod.js @@ -0,0 +1,146 @@ +'use strict' + +var async = require('async') +var chai = require('chai') +var expect = chai.expect +var fs = require('fs') +var pathUtils = require('path') + +var webtorrent = require(pathUtils.join(__dirname, '../../lib/webtorrent')) +webtorrent.silent = true + +var utils = require('./utils') + +describe('Test a single pod', function () { + var app = null + var url = '' + var video_id = -1 + + before(function (done) { + this.timeout(20000) + + async.series([ + function (next) { + utils.flushTests(next) + }, + function (next) { + utils.runServer(1, function (app1, url1) { + app = app1 + url = url1 + next() + }) + }, + function (next) { + webtorrent.create({ host: 'client', port: '1' }, next) + } + ], done) + }) + + it('Should not have videos', function (done) { + utils.getVideosList(url, function (err, res) { + if (err) throw err + + expect(res.body).to.be.an('array') + expect(res.body.length).to.equal(0) + + done() + }) + }) + + it('Should upload the video', function (done) { + this.timeout(5000) + utils.uploadVideo(url, 'my super name', 'my super description', 'video_short.webm', done) + }) + + it('Should seed the uploaded video', function (done) { + // Yes, this could be long + this.timeout(60000) + + utils.getVideosList(url, function (err, res) { + if (err) throw err + + expect(res.body).to.be.an('array') + expect(res.body.length).to.equal(1) + + var video = res.body[0] + expect(video.name).to.equal('my super name') + expect(video.description).to.equal('my super description') + expect(video.podUrl).to.equal('http://localhost:9001') + expect(video.magnetUri).to.exist + + video_id = video._id + + webtorrent.add(video.magnetUri, function (torrent) { + expect(torrent.files).to.exist + expect(torrent.files.length).to.equal(1) + expect(torrent.files[0].path).to.exist.and.to.not.equal('') + + done() + }) + }) + }) + + it('Should search the video', function (done) { + utils.searchVideo(url, 'my', function (err, res) { + if (err) throw err + + expect(res.body).to.be.an('array') + expect(res.body.length).to.equal(1) + + var video = res.body[0] + expect(video.name).to.equal('my super name') + expect(video.description).to.equal('my super description') + expect(video.podUrl).to.equal('http://localhost:9001') + expect(video.magnetUri).to.exist + + done() + }) + }) + + it('Should not find a search', function (done) { + utils.searchVideo(url, 'hello', function (err, res) { + if (err) throw err + + expect(res.body).to.be.an('array') + expect(res.body.length).to.equal(0) + + done() + }) + }) + + it('Should remove the video', function (done) { + utils.removeVideo(url, video_id, function (err) { + if (err) throw err + + fs.readdir(pathUtils.join(__dirname, '../../test1/uploads/'), function (err, files) { + if (err) throw err + + expect(files.length).to.equal(0) + done() + }) + }) + }) + + it('Should not have videos', function (done) { + utils.getVideosList(url, function (err, res) { + if (err) throw err + + expect(res.body).to.be.an('array') + expect(res.body.length).to.equal(0) + + done() + }) + }) + + after(function (done) { + process.kill(-app.pid) + process.kill(-webtorrent.app.pid) + + // Keep the logs if the test failed + if (this.ok) { + utils.flushTests(done) + } else { + done() + } + }) +}) diff --git a/server/tests/api/utils.js b/server/tests/api/utils.js new file mode 100644 index 000000000..47b706294 --- /dev/null +++ b/server/tests/api/utils.js @@ -0,0 +1,185 @@ +'use strict' + +var child_process = require('child_process') +var exec = child_process.exec +var fork = child_process.fork +var pathUtils = require('path') +var request = require('supertest') + +var testUtils = { + flushTests: flushTests, + getFriendsList: getFriendsList, + getVideosList: getVideosList, + makeFriends: makeFriends, + quitFriends: quitFriends, + removeVideo: removeVideo, + flushAndRunMultipleServers: flushAndRunMultipleServers, + runServer: runServer, + searchVideo: searchVideo, + uploadVideo: uploadVideo +} + +// ---------------------- Export functions -------------------- + +function flushTests (callback) { + exec(pathUtils.join(__dirname, '../../scripts/clean_test.sh'), callback) +} + +function getFriendsList (url, end) { + var path = '/api/v1/pods/' + + request(url) + .get(path) + .set('Accept', 'application/json') + .expect(200) + .expect('Content-Type', /json/) + .end(end) +} + +function getVideosList (url, end) { + var path = '/api/v1/videos' + + request(url) + .get(path) + .set('Accept', 'application/json') + .expect(200) + .expect('Content-Type', /json/) + .end(end) +} + +function makeFriends (url, expected_status, callback) { + if (!callback) { + callback = expected_status + expected_status = 204 + } + + var path = '/api/v1/pods/makefriends' + + // The first pod make friend with the third + request(url) + .get(path) + .set('Accept', 'application/json') + .expect(expected_status) + .end(function (err, res) { + if (err) throw err + + // Wait for the request between pods + setTimeout(callback, 1000) + }) +} + +function quitFriends (url, callback) { + var path = '/api/v1/pods/quitfriends' + + // The first pod make friend with the third + request(url) + .get(path) + .set('Accept', 'application/json') + .expect(204) + .end(function (err, res) { + if (err) throw err + + // Wait for the request between pods + setTimeout(callback, 1000) + }) +} + +function removeVideo (url, id, end) { + var path = '/api/v1/videos' + + request(url) + .delete(path + '/' + id) + .set('Accept', 'application/json') + .expect(204) + .end(end) +} + +function flushAndRunMultipleServers (total_servers, serversRun) { + var apps = [] + var urls = [] + var i = 0 + + function anotherServerDone (number, app, url) { + apps[number - 1] = app + urls[number - 1] = url + i++ + if (i === total_servers) { + serversRun(apps, urls) + } + } + + flushTests(function () { + for (var j = 1; j <= total_servers; j++) { + (function (k) { // TODO: ES6 with let + // For the virtual buffer + setTimeout(function () { + runServer(k, function (app, url) { + anotherServerDone(k, app, url) + }) + }, 1000 * k) + })(j) + } + }) +} + +function runServer (number, callback) { + var port = 9000 + number + var server_run_string = { + 'Connected to mongodb': false, + 'Server listening on port': false + } + + // Share the environment + var env = Object.create(process.env) + env.NODE_ENV = 'test' + env.NODE_APP_INSTANCE = number + var options = { + silent: true, + env: env, + detached: true + } + + var app = fork(pathUtils.join(__dirname, '../../server.js'), [], options) + app.stdout.on('data', function onStdout (data) { + var dont_continue = false + // Check if all required sentences are here + for (var key of Object.keys(server_run_string)) { + if (data.toString().indexOf(key) !== -1) server_run_string[key] = true + if (server_run_string[key] === false) dont_continue = true + } + + // If no, there is maybe one thing not already initialized (mongodb...) + if (dont_continue === true) return + + app.stdout.removeListener('data', onStdout) + callback(app, 'http://localhost:' + port) + }) +} + +function searchVideo (url, search, end) { + var path = '/api/v1/videos' + + request(url) + .get(path + '/search/' + search) + .set('Accept', 'application/json') + .expect(200) + .expect('Content-Type', /json/) + .end(end) +} + +function uploadVideo (url, name, description, fixture, end) { + var path = '/api/v1/videos' + + request(url) + .post(path) + .set('Accept', 'application/json') + .field('name', name) + .field('description', description) + .attach('input_video', pathUtils.join(__dirname, 'fixtures', fixture)) + .expect(201) + .end(end) +} + +// --------------------------------------------------------------------------- + +module.exports = testUtils diff --git a/server/tests/index.js b/server/tests/index.js new file mode 100644 index 000000000..ccebbfe51 --- /dev/null +++ b/server/tests/index.js @@ -0,0 +1,6 @@ +;(function () { + 'use strict' + + // Order of the tests we want to execute + require('./api/') +})() -- cgit v1.2.3