'use strict'; /** * Module dependencies */ var path = require('path'), config = require(path.resolve('./config/config')), common = require(path.resolve('./config/lib/common')), dataLog = require(path.resolve('./config/lib/data-log')), mongoose = require('mongoose'), User = mongoose.model('User'), Torrent = mongoose.model('Torrent'), Peer = mongoose.model('Peer'), Complete = mongoose.model('Complete'), Finished = mongoose.model('Finished'), bcode = require(path.resolve('./config/lib/bencode')), benc = require('bncode'), ipRegex = require('ip-regex'), moment = require('moment'), async = require('async'), sprintf = require('sprintf-js').sprintf, traceLogCreate = require(path.resolve('./config/lib/tracelog')).create, scoreUpdate = require(path.resolve('./config/lib/score')).update; var traceConfig = config.meanTorrentConfig.trace; var scoreConfig = config.meanTorrentConfig.score; var hnrConfig = config.meanTorrentConfig.hitAndRun; var announceConfig = config.meanTorrentConfig.announce; var globalSalesConfig = config.meanTorrentConfig.torrentGlobalSales; var appConfig = config.meanTorrentConfig.app; var mtDebug = require(path.resolve('./config/lib/debug')); const FAILURE_REASONS = { 100: 'Invalid request type: client request was not a HTTP GET', 101: 'Missing info_hash', 102: 'Missing peer_id', 103: 'Missing port', 104: 'Missing passkey', 150: 'Invalid infohash: infohash is not 20 bytes long', 151: 'Invalid peerid: peerid is not 20 bytes long', 152: 'Invalid numwant. Client requested more peers than allowed by tracker', 153: 'Passkey length error (length=32)', 154: 'Invalid passkey, if you changed you passkey, please redownload the torrent file from ' + appConfig.domain, 160: 'Invalid torrent info_hash', 161: 'No torrent with that info_hash has been found', 170: 'your account status is banned', 171: 'your account status is inactive', 172: 'your client is not allowed, here is the blacklist: ' + appConfig.domain + announceConfig.clientBlackListUrl, 173: 'this torrent status is not reviewed by administrators, try again later', 174: 'this torrent is only for VIP members', 175: 'your account status is idle', 176: 'you can not seeding an un-download finished torrent', 180: 'You can not open more than 1 downloading processes on the same torrent', 181: 'You can not open more than 3 seeding processes on the same torrent', 182: 'save peer failed', 183: 'save torrent failed', 184: 'save passkeyuser failed', 185: 'get H&R completeTorrent failed', 186: 'create H&R completeTorrent failed', 187: 'Illegal completed event', 190: 'You have more H&R warning, can not download any torrent now!', 191: 'not find this torrent H&R complete data', 200: 'Your total ratio is less than %.2f, can not download anything', 600: 'This tracker only supports compact mode', 900: 'Generic error' }; const EVENT_NONE = 0; const EVENT_COMPLETED = 1; const EVENT_STARTED = 2; const EVENT_STOPPED = 3; const WANT_DEFAULT = 50; const PEERSTATE_SEEDER = 'seeder'; const PEERSTATE_LEECHER = 'leecher'; const PEER_COMPACT_SIZE = 6; const ANNOUNCE_INTERVAL = Math.floor(announceConfig.announceInterval / 1000); const PARAMS_INTEGER = [ 'port', 'uploaded', 'downloaded', 'left', 'compact', 'numwant', 'no_peer_id' ]; const PARAMS_STRING = [ 'event', 'ipv4', 'ipv6' ]; var isGlobalSaleValid = false; /** * event * @param e * @returns {number} */ function event(e) { switch (e) { case 'completed': return EVENT_COMPLETED; case 'started': return EVENT_STARTED; case 'stopped': return EVENT_STOPPED; } return EVENT_NONE; } /** * eventString * @param e * @returns {number} */ function eventString(e) { switch (e) { case 'completed': return 'EVENT_COMPLETED'; case 'started': return 'EVENT_STARTED'; case 'stopped': return 'EVENT_STOPPED'; } return 'EVENT_NONE'; } /** * Failure * @param code * @param reason * @constructor */ function Failure(code, reason) { this.code = code; this.reason = reason; if (reason === undefined && typeof FAILURE_REASONS[this.code] !== 'undefined') this.reason = FAILURE_REASONS[this.code]; else if (this.code == null) this.code = 900; } /** * Failure.prototype * @type {{bencode: Function}} */ Failure.prototype = { bencode: function () { return 'd14:failure reason' + this.reason.length + ':' + this.reason + '12:failure codei' + this.code + 'ee'; } }; /** * info api * @param req * @param res */ exports.announce = function (req, res) { req.torrent = undefined; req.currentPeer = undefined; req.completeTorrent = undefined; req.selfpeer = []; req.seeder = false; mtDebug.debugGreen('\n\n', 'ANNOUNCE', true, req.passkeyuser); mtDebug.debugGreen('================================================================================', 'ANNOUNCE', true, req.passkeyuser); mtDebug.debugGreen(' ANNOUNCE REQUEST ', 'ANNOUNCE', true, req.passkeyuser); mtDebug.debugGreen('================================================================================', 'ANNOUNCE', true, req.passkeyuser); mtDebug.debugBlue(req.url, 'ANNOUNCE', true, req.passkeyuser); var s = req.url.split('?'); var query = common.querystringParse(s[1]); var passkey = req.params.passkey || query.passkey || undefined; async.waterfall([ /*--------------------------------------------------------------- validateQueryCheck ---------------------------------------------------------------*/ function (done) { var i = 0; var p; if (req.method !== 'GET') { done(100); } else if (typeof query.info_hash === 'undefined') { done(101); } else if (typeof query.peer_id === 'undefined') { done(102); } else if (typeof query.port === 'undefined') { done(103); } else if (query.info_hash.length !== 20) { done(150); } else if (query.peer_id.length !== 20) { done(151); } else if (typeof query.compact === 'undefined' || query.compact !== '1') { query.compact = 0; } else { for (i = 0; i < PARAMS_INTEGER.length; i++) { p = PARAMS_INTEGER[i]; if (typeof query[p] !== 'undefined') { query[p] = parseInt(query[p].toString(), 10); } } for (i = 0; i < PARAMS_STRING.length; i++) { p = PARAMS_STRING[i]; if (typeof query[p] !== 'undefined') { query[p] = query[p].toString(); } } if (query.ipv4 === undefined) { query.ipv4 = ''; } if (query.ipv6 === undefined) { query.ipv6 = ''; } if (query.ipv6.toLowerCase().startsWith('fe80')) { query.ipv6 = ''; } //write ip v4 v6 query.ip = req.cf_ip; if (ipRegex.v6({exact: true}).test(query.ip)) { query.ipv6 = query.ip; } else { query.ipv4 = query.ip; } query.info_hash = common.binaryToHex(query.info_hash); req.seeder = (query.left === 0) ? true : false; // console.log(query); done(null); } }, /*--------------------------------------------------------------- validatePasskeyCheck ---------------------------------------------------------------*/ function (done) { if (typeof passkey === 'undefined') { done(104); } else if (passkey.length !== 32) { done(153); } else if (req.passkeyuser === undefined) { done(154); } else { done(null); } }, /*--------------------------------------------------------------- validateUserCheck check normal, banned, idle, inactive ---------------------------------------------------------------*/ function (done) { switch (req.passkeyuser.status) { case 'banned': done(170); break; case 'idle': done(175); break; case 'inactive': done(171); break; default: done(null); } }, /*--------------------------------------------------------------- validateClientCheck check client blacklist ---------------------------------------------------------------*/ function (done) { var ua = req.get('User-Agent'); var inlist = false; if (ua) { config.meanTorrentConfig.clientBlackList.forEach(function (client) { if (ua.toUpperCase().indexOf(client.name.toUpperCase()) >= 0) { inlist = true; } }); } if (inlist) { done(172); } else { done(null); } }, /*--------------------------------------------------------------- getTorrentItemData torrent data include peers ---------------------------------------------------------------*/ function (done) { mtDebug.debugRed('req.passkeyuser._id = ' + req.passkeyuser._id.toString(), 'ANNOUNCE', true, req.passkeyuser); mtDebug.debugRed('req.torrent.info_hash = ' + query.info_hash, 'ANNOUNCE', true, req.passkeyuser); Torrent.findOne({ info_hash: query.info_hash }) .populate('user') .populate({ path: '_peers' }) .exec(function (err, t) { if (err) { done(160); } else if (!t) { done(161); } else if (t.torrent_status === 'new' && !req.seeder) { done(173); } else { req.torrent = t; done(null); } }); }, /*--------------------------------------------------------------- refresh user`s vip status and ratio update torrent isSaling status ---------------------------------------------------------------*/ function (done) { req.passkeyuser.globalUpdateMethod(function (u) { req.passkeyuser = u; if (req.torrent.isSaling) { req.torrent.globalUpdateMethod(function (t) { req.torrent = t; done(null); }); } else { done(null); } }); }, /*--------------------------------------------------------------- check torrent_vip and user_vip status ---------------------------------------------------------------*/ function (done) { if (!req.seeder && req.torrent.torrent_vip && !req.passkeyuser.isVip) { done(174); } else { done(null); } }, /*--------------------------------------------------------------- check whether can seeding if torrent is not exist in user`s finished list ---------------------------------------------------------------*/ function (done) { mtDebug.debugGreen('---------------' + eventString(query.event) + '----------------', 'ANNOUNCE', true, req.passkeyuser); if (req.seeder && event(query.event) !== EVENT_COMPLETED && event(query.event) !== EVENT_STOPPED) { if (announceConfig.seedingInFinishedCheck) { if (!req.passkeyuser._id.equals(req.torrent.user._id)) { Finished.findOne({ user: req.passkeyuser._id, torrent: req.torrent._id }).exec(function (err, fini) { if (!fini) { done(176); } else { done(null); } }); } else { done(null); } } else { done(null); } } else { done(null); } }, /*--------------------------------------------------------------- find complete torrent data if not find and torrent is h&r and user isn`t vip, then create complete record ---------------------------------------------------------------*/ function (done) { if (hnrConfig.enable && req.torrent.torrent_hnr && !req.passkeyuser.isVip) { Complete.findOne({ torrent: req.torrent._id, user: req.passkeyuser._id }) .populate('user') .populate('torrent') .exec(function (err, t) { if (err) { done(185); } else { if (!t) { var comp = new Complete(); comp.torrent = req.torrent; comp.user = req.passkeyuser; comp.complete = req.seeder ? true : false; comp.save(function (err) { if (err) { done(186); } else { req.completeTorrent = comp; done(null); } }); } else { req.completeTorrent = t; done(null); } } }); } else { done(null); } }, /*--------------------------------------------------------------- some time, when user close download client directly, maybe some ghost peer stay in peers table and not in torrent._peers delete them on start event of any user ----------------------------------------------------------------*/ function (done) { if (event(query.event) === EVENT_STARTED) { Peer.remove({ torrent: req.torrent._id, _id: {$nin: req.torrent._peers} }, function (err, removed) { if (removed.n > 0) { mtDebug.debugRed('Removed ' + removed + ' peers not in torrent._peers: ' + req.torrent._id, 'ANNOUNCE', true, req.passkeyuser); } done(null); }); } else { done(null); } }, /*--------------------------------------------------------------- check N&R can download if user has too more H&R warning numbers, can not download any torrent but can continue download the warning status torrent vip user not checked ---------------------------------------------------------------*/ function (done) { if (!req.seeder && !req.passkeyuser.isVip && event(query.event) === EVENT_STARTED) { if (hnrConfig.enable) { if (req.passkeyuser.hnr_warning >= hnrConfig.forbiddenDownloadMinWarningNumber) { if (!req.torrent.torrent_hnr) { done(190); } else { if (!req.completeTorrent) { done(191); } else { if (!req.completeTorrent.hnr_warning) { done(190); } else { done(null); } } } } else { done(null); } } else { done(null); } } else { done(null); } }, /*--------------------------------------------------------------- announce download check ratio check, setting in announce.downloadCheck vip user not checked ---------------------------------------------------------------*/ function (done) { if (!req.seeder && !req.passkeyuser.isVip && !req.passkeyuser.isOper && event(query.event) === EVENT_STARTED) { if (req.passkeyuser.ratio !== -1 && req.passkeyuser.ratio < announceConfig.downloadCheck.ratio) { var checkTimeBegin = moment(req.passkeyuser.created).add(announceConfig.downloadCheck.checkAfterSignupDays, 'd'); if (checkTimeBegin < moment(Date.now())) { var reason = sprintf(FAILURE_REASONS[200], announceConfig.downloadCheck.ratio); done(200, reason); } else { done(null); } } else { done(null); } } else { done(null); } }, /*--------------------------------------------------------------- find myself peers and get current peer with same peer_id ----------------------------------------------------------------*/ function (done) { if (req.torrent._peers.length > 0) { for (var i = req.torrent._peers.length; i > 0; i--) { var p = req.torrent._peers[i - 1]; if (p.user.equals(req.passkeyuser._id)) { if (p.last_announce_at > (Date.now() - announceConfig.announceInterval - announceConfig.announceIdleTime)) { //do not add inactive peer req.selfpeer.push(p); } else if (p.peer_id === query.peer_id) { req.selfpeer.push(p); } } } } getCurrentPeer(function () { mtDebug.debugRed('req.currentPeer.isNewCreated = ' + req.currentPeer.isNewCreated, 'ANNOUNCE', true, req.passkeyuser); mtDebug.debugRed('req.currentPeer._id = ' + req.currentPeer._id.toString(), 'ANNOUNCE', true, req.passkeyuser); mtDebug.debugRed('req.currentPeer.torrent._id = ' + req.currentPeer.torrent._id, 'ANNOUNCE', true, req.passkeyuser); done(null); }); }, /*--------------------------------------------------------------- onEventStarted if downloading, check download peer num only 1 if seeding, check seed peer num can not more than 3 numbers is in setting announceConfig.announceCheck ---------------------------------------------------------------*/ function (done) { mtDebug.debugGreen('---------------CHECK USER SEED/LEECH COUNT----------------', 'ANNOUNCE', true, req.passkeyuser); var lcount = getSelfLeecherCount(); var scount = getSelfSeederCount(); if (lcount > announceConfig.announceCheck.maxLeechNumberPerUserPerTorrent && !req.seeder) { mtDebug.debugRed('getSelfLeecherCount = ' + lcount, 'ANNOUNCE', true, req.passkeyuser); removeCurrPeer(); done(180); } else if (scount > announceConfig.announceCheck.maxSeedNumberPerUserPerTorrent && req.seeder) { mtDebug.debugRed('getSelfSeederCount = ' + scount, 'ANNOUNCE', true, req.passkeyuser); removeCurrPeer(); done(181); } else { done(null); } }, /*--------------------------------------------------------------- writeUpDownData uploaded,downloaded update complete data if completeTorrent is exist if has upload and download data size, write data size, write time of seed(complete) whether or not u/d is 0 ---------------------------------------------------------------*/ function (done) { mtDebug.debugGreen('---------------WRITE_UP_DOWN_DATA----------------', 'ANNOUNCE', true, req.passkeyuser); var curru = 0; var currd = 0; if (!req.currentPeer.isNewCreated) { var udr = getUDRatio(); curru = query.uploaded - req.currentPeer.peer_uploaded; currd = query.downloaded - req.currentPeer.peer_downloaded; if (curru > 0 || currd > 0) { var u = Math.round(curru * udr.ur); var d = Math.round(currd * udr.dr); //check if is vip if (req.passkeyuser.isVip) { u = u * globalSalesConfig.vip.value.Ur; d = d * globalSalesConfig.vip.value.Dr; } //check if is torrent uploader if (req.passkeyuser._id.equals(req.torrent.user._id)) { u = u * globalSalesConfig.uploader.value.Ur; d = d * globalSalesConfig.uploader.value.Dr; } //write user uploaded and downloaded var up_d = { uploaded: u, downloaded: d, true_uploaded: curru, true_downloaded: currd }; mtDebug.debugRed(JSON.stringify(up_d), 'ANNOUNCE', true, req.passkeyuser); if (common.examinationIsValid(req.passkeyuser)) { up_d['examinationData.uploaded'] = u; up_d['examinationData.downloaded'] = d; mtDebug.debugGreen('---------------WRITE EXAMINATION DATA----------------', 'ANNOUNCE', true, req.passkeyuser); mtDebug.debugRed('examinationData.uploaded: ' + u + ', examinationData.downloaded: ' + d, 'ANNOUNCE', true, req.passkeyuser); } req.passkeyuser.update({ $inc: up_d }).exec(); //write peer speed var sp = {}; if (curru > 0) { sp.peer_uspeed = Math.round(curru / (Date.now() - req.currentPeer.last_announce_at) * 1000); sp.peer_cuspeed = Math.round(curru / (Date.now() - req.currentPeer.last_announce_at) * 1000); } else { sp.peer_cuspeed = 0; } if (currd > 0) { sp.peer_dspeed = Math.round(currd / (Date.now() - req.currentPeer.last_announce_at) * 1000); sp.peer_cdspeed = Math.round(currd / (Date.now() - req.currentPeer.last_announce_at) * 1000); } else { sp.peer_cdspeed = 0; } req.currentPeer.update({ $set: sp }).exec(); //update score //upload score and download score var totalScore = 0; var upUnitScore = 1; var downUnitScore = 1; var seederUnit = 1; var lifeUnit = 1; var action = scoreConfig.action.seedUpDownload; var slAction = scoreConfig.action.seedSeederAndLife; if (action.enable) { var uploadScore = 0; var downloadScore = 0; if (curru > 0 && action.uploadEnable) { if (req.torrent.torrent_size > action.additionSize) { upUnitScore = Math.sqrt(req.torrent.torrent_size / action.additionSize); } var upScore = curru / action.perlSize; uploadScore = upUnitScore * action.uploadValue * upScore; //uploader addition if (req.passkeyuser._id.equals(req.torrent.user._id)) { uploadScore = uploadScore * action.uploaderRatio; } } if (currd > 0 && action.downloadEnable) { if (req.torrent.torrent_size > action.additionSize) { downUnitScore = Math.sqrt(req.torrent.torrent_size / action.additionSize); } var downScore = currd / action.perlSize; downloadScore = downUnitScore * action.downloadValue * downScore; } totalScore = uploadScore + downloadScore; if (totalScore > 0) { //vip addition if (action.vipRatio && req.passkeyuser.isVip) { totalScore = totalScore * action.vipRatio; } if (slAction.enable) { //torrent seeders count addition if (req.torrent.torrent_seeds <= slAction.seederCount) { seederUnit = slAction.seederBasicRatio + slAction.seederCoefficient * (slAction.seederCount - req.torrent.torrent_seeds + 1); totalScore = totalScore * seederUnit; } //torrent life addition var life = moment(Date.now()) - moment(req.torrent.createdat); var days = life / (60 * 60 * 1000 * 24); lifeUnit = slAction.lifeBasicRatio + slAction.lifeCoefficientOfDay * days; lifeUnit = lifeUnit > slAction.lifeMaxRatio ? slAction.lifeMaxRatio : lifeUnit; totalScore = totalScore * lifeUnit; } totalScore = Math.round(totalScore * 100) / 100; action.params = { tid: req.torrent._id }; scoreUpdate(req, req.passkeyuser, action, totalScore, false); mtDebug.debugRed('announce score: ' + totalScore, 'ANNOUNCE', true, req.passkeyuser); } } //write logs data into db var logData = { query_uploaded: query.uploaded, query_downloaded: query.downloaded, currentPeer_uploaded: req.currentPeer.peer_uploaded, currentPeer_downloaded: req.currentPeer.peer_downloaded, curr_uploaded: curru, curr_downloaded: currd, write_uploaded: u, write_downloaded: d, write_score: totalScore, isVip: req.passkeyuser.isVip, isUploader: req.passkeyuser._id.equals(req.torrent.user._id), salesSettingValue: { torrentSalesValue: req.torrent.torrent_sale_status, globalSalesValue: isGlobalSaleValid ? globalSalesConfig.global.value : undefined, vipSalesValue: globalSalesConfig.vip.value, uploaderSalesValue: globalSalesConfig.uploader.value }, scoreSettingValue: { upUnitScore: upUnitScore, downUnitScore: downUnitScore, seederUnit: seederUnit, lifeUnit: lifeUnit }, info: { agent: req.get('User-Agent'), ip: req.cf_ip, port: query.port } }; dataLog.announceLog(req.passkeyuser, req.torrent, logData); } else { req.currentPeer.update({ $set: { peer_cuspeed: 0, peer_cdspeed: 0 } }).exec(); } } //write peer data req.currentPeer.update({ $set: { peer_uploaded: query.uploaded, peer_downloaded: query.downloaded, peer_left: query.left } }).exec(); done(null, curru, currd); }, /*--------------------------------------------------------------- write complete data to completeTorrent and refresh completed ratio ---------------------------------------------------------------*/ function (curru, currd, done) { if (curru > 0 || currd > 0) { if (hnrConfig.enable && req.completeTorrent) { mtDebug.debugGreen('---------------WRITE COMPLETE DATA----------------', 'ANNOUNCE', true, req.passkeyuser); req.completeTorrent.update({ $inc: { total_uploaded: curru, total_downloaded: currd } }, function () { done(null); }); } else { done(null); } } else { done(null); } }, /*--------------------------------------------------------------- update H&R completeTorrent.total_seed_time update H&R ratio in save ---------------------------------------------------------------*/ function (done) { if (!req.currentPeer.isNewCreated) { if (hnrConfig.enable && req.completeTorrent && req.completeTorrent.complete && event(query.event) !== EVENT_COMPLETED) { mtDebug.debugGreen('---------------UPDATE H&R COMPLETE TOTAL_SEED_TIME----------------', 'ANNOUNCE', true, req.passkeyuser); req.completeTorrent.update({ $inc: { total_seed_time: (Date.now() - req.currentPeer.last_announce_at) } }, function () { done(null); }); } else { done(null); } } else { done(null); } }, /*--------------------------------------------------------------- upload user getting score through seed timed include torrent seeders count coefficient value and life coefficient value ---------------------------------------------------------------*/ function (done) { if (!req.currentPeer.isNewCreated) { if (req.seeder && event(query.event) !== EVENT_COMPLETED && event(query.event) !== EVENT_STARTED) { mtDebug.debugGreen('---------------UPLOAD SCORE THROUGH SEED TIMED----------------', 'ANNOUNCE', true, req.passkeyuser); if (req.torrent.torrent_status === 'reviewed') { var action = scoreConfig.action.seedTimed; var slAction = scoreConfig.action.seedSeederAndLife; if (action.enable) { var timed = Date.now() - req.currentPeer.last_announce_at; var seedUnit = timed / action.additionTime; var seedScore = seedUnit * action.timedValue; if (seedScore > 0) { //vip addition if (action.vipRatio && req.passkeyuser.isVip) { seedScore = seedScore * action.vipRatio; } if (slAction.enable) { //torrent seeders count addition if (req.torrent.torrent_seeds <= slAction.seederCount) { var seederUnit = slAction.seederBasicRatio + slAction.seederCoefficient * (slAction.seederCount - req.torrent.torrent_seeds + 1); seedScore = seedScore * seederUnit; } //torrent life addition var life = moment(Date.now()) - moment(req.torrent.createdat); var days = life / (60 * 60 * 1000 * 24); var lifeUnit = slAction.lifeBasicRatio + slAction.lifeCoefficientOfDay * days; lifeUnit = lifeUnit > slAction.lifeMaxRatio ? slAction.lifeMaxRatio : lifeUnit; seedScore = seedScore * lifeUnit; } seedScore = Math.round(seedScore * 100) / 100; action.params = { tid: req.torrent._id }; scoreUpdate(req, req.passkeyuser, action, seedScore); mtDebug.debugRed('seed timed score: ' + seedScore, 'ANNOUNCE', true, req.passkeyuser); done(null); } else { done(null); } } else { done(null); } } else { done(null); } } else { done(null); } } else { done(null); } }, /*--------------------------------------------------------------- update currentPeer.last_announce_at update complateTorrent refreshat ---------------------------------------------------------------*/ function (done) { mtDebug.debugGreen('---------------UPDATE LAST_ANNOUNCE_AT----------------', 'ANNOUNCE', true, req.passkeyuser); if (!req.currentPeer.isNewCreated) { req.currentPeer.update({ $set: { last_announce_at: Date.now() } }).exec(); } done(null); }, /*--------------------------------------------------------------- onEventCompleted ---------------------------------------------------------------*/ function (done) { if (event(query.event) === EVENT_COMPLETED) { mtDebug.debugGreen('---------------EVENT_COMPLETED----------------', 'ANNOUNCE', true, req.passkeyuser); if (req.currentPeer.peer_downloaded > 0 || query.downloaded > 0) { doCompleteEvent(function () { done(null); }); } else { done(187); mtDebug.debugRed('Illegal completed event', 'ANNOUNCE', true, req.passkeyuser); } } else { done(null); } }, /*--------------------------------------------------------------- count H&R warning for user on normal up/down process ---------------------------------------------------------------*/ function (done) { if (!req.currentPeer.isNewCreated) { if (hnrConfig.enable && req.completeTorrent && event(query.event) !== EVENT_COMPLETED) { mtDebug.debugGreen('---------------COUNT H&R WARNING FOR USER----------------', 'ANNOUNCE', true, req.passkeyuser); req.completeTorrent.countHnRWarning(false, true); } } done(null); }, /*--------------------------------------------------------------- onEventStopped count H&R warning for user when EVENT_STOPPED delete peers ---------------------------------------------------------------*/ function (done) { if (event(query.event) === EVENT_STOPPED) { mtDebug.debugGreen('---------------EVENT_STOPPED----------------', 'ANNOUNCE', true, req.passkeyuser); if (hnrConfig.enable && req.completeTorrent) { req.completeTorrent.countHnRWarning(true, false); } removeCurrPeer(function () { done(null); }); } else { done(null); } }, /*--------------------------------------------------------------- update torrent and user seeding/leeching count numbers ---------------------------------------------------------------*/ function (done) { mtDebug.debugGreen('---------------COUNT TORRENT SEEDING/LEECHING----------------', 'ANNOUNCE', true, req.passkeyuser); req.torrent.updateSeedLeechNumbers(function (slCount) { req.passkeyuser.updateSeedLeechNumbers(); if (slCount) { mtDebug.debugYellow(JSON.stringify(slCount), 'ANNOUNCE', true, req.passkeyuser); req.torrent.torrent_seeds = slCount.seedCount; req.torrent.torrent_leechers = slCount.leechCount; } done(null); }); }, /*--------------------------------------------------------------- sendPeers compact mode ---------------------------------------------------------------*/ function (done) { var want = WANT_DEFAULT; if (typeof query.numwant !== 'undefined' && query.numwant > 0) want = query.numwant; var peers = getPeers(want, req.torrent._peers); var hasV6ip = hasV6IP(peers); var peersFunction = hasV6ip ? peersDictionary : (query.compact === 0 ? peersDictionary : peersBinary); var resPeers = peersFunction(peers); var resp = bcode.encode({ interval: ANNOUNCE_INTERVAL, complete: req.torrent.torrent_seeds, incomplete: req.torrent.torrent_leechers, downloaded: req.torrent.torrent_finished, peers: resPeers }); mtDebug.debugGreen('---------------SEND RESPONSE TO USER----------------', 'ANNOUNCE', true, req.passkeyuser); if (peers.length > 0) { mtDebug.debug('ip send mode: ' + (hasV6ip ? 'IPv6' : 'IPv4'), 'ANNOUNCE', true, req.passkeyuser); } mtDebug.debug(benc.decode(resp, 'ascii'), 'ANNOUNCE', true, req.passkeyuser); res.writeHead(200, {'Content-Type': 'text/plain'}); res.end(resp, 'ascii'); done(null); }, /** * update user, torrent, peer, complete * @param done */ function (done) { req.passkeyuser.globalUpdateMethod(true); req.torrent.globalUpdateMethod(true); if (req.currentPeer) { req.currentPeer.globalUpdateMethod(true); } if (hnrConfig.enable && req.completeTorrent) { req.completeTorrent.globalUpdateMethod(true); } done(null, 'done'); } ], function (err, reason) { if (err) { sendError(new Failure(err, reason)); } }); /** * getCurrentPeer * @returns {boolean} */ function getCurrentPeer(callback) { req.selfpeer.every(function (p) { if (p.peer_id === query.peer_id) { req.currentPeer = p; req.currentPeer.torrent = req.torrent; req.currentPeer.isNewCreated = false; //if find peer_id, but some time some client (like qbittorrent 4.1.0) the ip or port is changed, update it if ((req.currentPeer.peer_ip !== req.cf_ip || req.currentPeer.peer_ipv4 !== query.ipv4 || req.currentPeer.peer_ipv6 !== query.ipv6 || req.currentPeer.peer_port !== query.port) && query.port !== 0) { req.currentPeer.peer_ip = req.cf_ip; req.currentPeer.peer_ipv4 = query.ipv4; req.currentPeer.peer_ipv6 = query.ipv6; req.currentPeer.peer_port = query.port; req.currentPeer.update({ $set: { peer_ip: req.cf_ip, peer_ipv4: query.ipv4, peer_ipv6: query.ipv6, peer_port: query.port } }).exec(); } if (req.seeder && req.currentPeer.peer_status !== PEERSTATE_SEEDER && event(query.event) !== EVENT_COMPLETED) { mtDebug.debugGreen('---------------PEER STATUS CHANGED: Seeder----------------', 'ANNOUNCE', true, req.passkeyuser); doCompleteEvent(function () { if (callback) return callback(); }); } else { if (callback) return callback(); } } return true; }); //if not found then create req.currentPeer if (!req.currentPeer) { createCurrentPeer(function () { if (callback) return callback(); }); } } /** * getCurrentPeerIpMode * @returns {string} */ function getCurrentPeerIpMode() { if (req.currentPeer) { if (req.currentPeer.isIpV4V6()) { return 'IPV4V6'; } else if (req.currentPeer.isIpV6()) { return 'IPV6'; } else { return 'IPV4'; } } else { return 'unknown'; } } /** * doCompleteEvent */ function doCompleteEvent(callback) { //write completed torrent data into finished var finished = new Finished(); finished.user = req.passkeyuser; finished.torrent = req.torrent; finished.user_ip = req.cf_ip; finished.user_agent = req.get('User-Agent'); finished.user_port = query.port; finished.save(); traceLogCreate(req, traceConfig.action.userAnnounceFinished, { user: req.passkeyuser._id, torrent: req.torrent._id, agent: req.get('User-Agent'), ip: req.cf_ip, port: query.port }); req.currentPeer.update({ $set: { peer_status: PEERSTATE_SEEDER } }).exec(); req.torrent.update({ $inc: { torrent_finished: 1 } }).exec(); req.passkeyuser.update({ $inc: { finished: 1 } }).exec(); //update completeTorrent complete status if (hnrConfig.enable && req.completeTorrent) { req.completeTorrent.update({ $set: { complete: true } }, function () { if (callback) callback(); }); } else { if (callback) callback(); } } /** * createCurrentPeer */ function createCurrentPeer(callback) { var peer = new Peer(); peer.user = req.passkeyuser; peer.torrent = req.torrent; peer.peer_id = query.peer_id; peer.peer_ip = req.cf_ip; peer.peer_ipv4 = query.ipv4; peer.peer_ipv6 = query.ipv6; peer.peer_port = query.port; peer.peer_left = query.left; peer.peer_status = req.seeder ? PEERSTATE_SEEDER : PEERSTATE_LEECHER; peer.user_agent = req.get('User-Agent'); peer.isNewCreated = true; peer.last_announce_at = Date.now(); if (req.seeder) { peer.finishedat = Date.now(); } req.selfpeer.push(peer); req.torrent.update({ $addToSet: {_peers: peer} }).exec(); //save ip to user req.passkeyuser.addLeechedIp(peer.peer_ip); req.passkeyuser.addClientAgent(peer.user_agent); peer.save(function () { req.currentPeer = peer; mtDebug.debugGreen('---------------createCurrentPeer()----------------', 'ANNOUNCE', true, req.passkeyuser); if (callback) callback(); }); } /** * removeCurrPeer */ function removeCurrPeer(callback) { req.selfpeer.splice(req.selfpeer.indexOf(req.currentPeer), 1); req.torrent._peers.forEach(function (_p) { if (_p._id.equals(req.currentPeer._id)) { req.torrent._peers.pull(_p); req.torrent.save(); } }); Peer.findById(req.currentPeer._id, function (err, _p) { _p.remove(function (err) { if (err) { mtDebug.debugGreen('---------------removeCurrPeer(): Error----------------', 'ANNOUNCE', true, req.passkeyuser); } else { mtDebug.debugGreen('---------------removeCurrPeer()----------------', 'ANNOUNCE', true, req.passkeyuser); } req.currentPeer = undefined; if (callback) callback(); }); }); } /** * getSelfLeecherCount * @returns {number} */ function getSelfLeecherCount() { if (req.selfpeer.length === 0) { return 0; } else { var i = 0; req.selfpeer.forEach(function (p) { if (p.peer_status === PEERSTATE_LEECHER) { i++; } }); return i; } } /** * getSelfSeederCount * @returns {number} */ function getSelfSeederCount() { if (req.selfpeer.length === 0) { return 0; } else { var i = 0; req.selfpeer.forEach(function (p) { if (p.peer_status === PEERSTATE_SEEDER) { i++; } }); return i; } } /** * getUDRatio * @returns {{}} */ function getUDRatio() { var udr = {}; var sale = req.torrent.torrent_sale_status; var start = moment(globalSalesConfig.global.startAt, globalSalesConfig.global.timeFormats).valueOf(); var end = start + globalSalesConfig.global.expires; var now = Date.now(); isGlobalSaleValid = (now > start && now < end && globalSalesConfig.global.value) ? true : false; if (isGlobalSaleValid && globalSalesConfig.global.value) { sale = globalSalesConfig.global.value; mtDebug.debugRed('isGlobalSaleValid = ' + isGlobalSaleValid, 'ANNOUNCE', true, req.passkeyuser); mtDebug.debugRed('global sale value = ' + sale, 'ANNOUNCE', true, req.passkeyuser); } switch (sale) { case 'U1/FREE': udr.ur = 1; udr.dr = 0; break; case 'U1/D.3': udr.ur = 1; udr.dr = 0.3; break; case 'U1/D.5': udr.ur = 1; udr.dr = 0.5; break; case 'U1/D.8': udr.ur = 1; udr.dr = 0.8; break; case 'U2/FREE': udr.ur = 2; udr.dr = 0; break; case 'U2/D.3': udr.ur = 2; udr.dr = 0.3; break; case 'U2/D.5': udr.ur = 2; udr.dr = 0.5; break; case 'U2/D.8': udr.ur = 2; udr.dr = 0.8; break; case 'U2/D1': udr.ur = 2; udr.dr = 1; break; case 'U3/FREE': udr.ur = 3; udr.dr = 0; break; case 'U3/D.5': udr.ur = 3; udr.dr = 0.5; break; case 'U3/D.8': udr.ur = 3; udr.dr = 0.8; break; case 'U3/D1': udr.ur = 3; udr.dr = 1; break; default: /* U1D1 */ udr.ur = 1; udr.dr = 1; } return udr; } /** * sendError * @param failure */ function sendError(failure) { var respc = failure.bencode(); mtDebug.debugRed(respc, 'ANNOUNCE', true, req.passkeyuser); res.writeHead(200, { 'Content-Length': respc.length, 'Content-Type': 'text/plain' }); res.end(respc); } /** * getPeers * @param count * @param peers * @returns [] */ function getPeers(count, peers) { var ps = []; if (event(query.event) !== EVENT_STOPPED) { mtDebug.debugGreen('---------------GET PEERS LIST----------------', 'ANNOUNCE', true, req.passkeyuser); mtDebug.debugRed('want.count = ' + count, 'ANNOUNCE', true, req.passkeyuser); mtDebug.debugRed('peers.length = ' + peers.length, 'ANNOUNCE', true, req.passkeyuser); mtDebug.debugRed('user ip mode: ' + getCurrentPeerIpMode(), 'ANNOUNCE', true, req.passkeyuser); var wantedPeers; if (req.currentPeer.isIpV4Only()) { //ipv4 wantedPeers = peers.filter(function (p) { return p.isIpV4(); }).sort(function () { return 0.5 - Math.random(); }).slice(0, count); } else { //ipv6 or v4v6 wantedPeers = peers.slice(0).sort(function () { return 0.5 - Math.random(); }).slice(0, count); } wantedPeers.forEach(function (p) { if (p !== undefined && p !== req.currentPeer) { if (p.last_announce_at > (Date.now() - announceConfig.announceInterval - announceConfig.announceIdleTime)) { //do not send inactive peer var tp; if (p.user.equals(req.passkeyuser._id)) { if (announceConfig.peersCheck.peersSendListIncludeOwnSeed) { tp = { id: p.peer_id, ip: req.currentPeer.isIpV4Only() ? p.peer_ipv4 : (p.isIpV6() ? p.peer_ipv6 : p.peer_ipv4), port: p.peer_port }; ps.push(tp); mtDebug.debug(p._id.toString() + ' - SELF PEER - IP:' + tp.ip + ' PORT:' + tp.port, 'ANNOUNCE', true, req.passkeyuser); } } else { tp = { id: p.peer_id, ip: req.currentPeer.isIpV4Only() ? p.peer_ipv4 : (p.isIpV6() ? p.peer_ipv6 : p.peer_ipv4), port: p.peer_port }; ps.push(tp); mtDebug.debug(p._id.toString() + ' IP:' + tp.ip + ' PORT:' + tp.port, 'ANNOUNCE', true, req.passkeyuser); } } } }); } return ps; } /** * hasV6IP * @param peers * @returns {boolean} */ function hasV6IP(peers) { for (let p of peers) { if (ipRegex.v6({exact: true}).test(p.ip)) { return true; } } return false; } /** * peersDictionary * @param peers */ function peersDictionary(peers) { return peers.map(function (peer) { if (query.no_peer_id === 1) { return { 'ip': peer.ip, 'port': peer.port }; } else { return { 'peer id': peer.id, 'ip': peer.ip, 'port': peer.port }; } }); } /** * peersBinary * @param peers * @returns {string} */ function peersBinary(peers) { var tokens = []; peers.forEach(function (peer) { tokens.push(peerBinary(peer.ip, peer.port)); }); return tokens.join(''); } /** * peerBinary * @param ip * @param port * @returns {string} */ function peerBinary(ip, port) { var tokens = []; var octets = ip.split('.'); if (octets.length !== 4) return ''; octets.forEach(function (octet) { var val = parseInt(octet, 10); if (!isNaN(val)) tokens.push(val); }); if (tokens.length !== 4) return ''; tokens.push((port >> 8) & 0xff); tokens.push(port & 0xff); return String.fromCharCode.apply(tokens, tokens); } }; /** * userByPasskey * @param req * @param res * @param next * @param pk * @returns {*} */ exports.userByPasskey = function (req, res, next, pk) { User.findOne({passkey: pk}) .exec(function (err, u) { if (u) { req.passkeyuser = u; } else { req.passkeyuser = undefined; } next(); }); };