Permalink
Browse files

initial release

  • Loading branch information...
indutny committed Nov 22, 2010
0 parents commit 32804094f04b3fe73681bbd6c82d1dbe7691396d
Showing with 403 additions and 0 deletions.
  1. +3 −0 .gitmodules
  2. +44 −0 lib/tracker/bencode.js
  3. +64 −0 lib/tracker/db.js
  4. +11 −0 lib/tracker/error.js
  5. +82 −0 lib/tracker/helpers.js
  6. +1 −0 lib/tracker/index.js
  7. +16 −0 lib/tracker/querydecoder.js
  8. +65 −0 lib/tracker/router.js
  9. +95 −0 lib/tracker/storage.js
  10. +21 −0 run.js
  11. +1 −0 support/cradle
@@ -0,0 +1,3 @@
+[submodule "support/cradle"]
+ path = support/cradle
+ url = git://github.com/cloudhead/cradle.git
@@ -0,0 +1,44 @@
+/**
+* Bencode middleware and encoder
+*/
+
+var bencode = module.exports = function(req, res, next) {
+ // Add bencode method
+ res.bencode = function(data) {
+ res.writeHead(200);
+
+ res.end(encode(data), 'binary');
+ };
+ next();
+}
+
+var encode = exports.encode = function(obj, recursion) {
+ var _type = typeof obj; // simple caching;
+
+ recursion++;
+
+ if (recursion > 100) {
+ throw Error('bencode::encode - Maximum recursion exceeded');
+ }
+
+ if (_type === 'string') {
+ return obj.length + ':' + obj
+ } else if (_type === 'number') {
+ return 'i' + obj + 'e';
+ } else if (Array.isArray(obj)) {
+ var output = [];
+ for (var i = 0, len = obj.length; i < len; i++) {
+ output[i] = encode(obj[i], recursion);
+ }
+ return 'l' + output.join('') + 'e';
+ } else if (obj) {
+ var output = [];
+ for (var i in obj) {
+ if (!obj.hasOwnProperty(i)) continue;
+ output.push(encode(i.toString()) + encode(obj[i]));
+ }
+ return 'd' + output.join('') + 'e';
+ }
+
+ return '0:';
+}
@@ -0,0 +1,64 @@
+/**
+* Init db
+*/
+var cradle = require('../../support/cradle/lib/cradle');
+
+module.exports = function(options, callback) {
+ cradle.setup({
+ host: options.host,
+ port: options.port,
+ options: options.options
+ });
+ var connection = new (cradle.Connection)({auth: options.auth}),
+ db = connection.database(options.database);
+
+ options.setup ? setup(db, callback) : callback(null, db);
+};
+
+function setup(db, callback) {
+ var views = {
+ by_update_time: {
+ map: function(doc) {
+ if (doc.type == 'peer') {
+ emit(doc.updated_at, doc._id);
+ }
+ }
+ },
+ by_hash: {
+ map: function(doc) {
+ if (doc.type == 'peer') {
+ emit(doc.info_hash, {
+ _id: doc._id,
+ ip: doc.ip,
+ port: doc.port
+ });
+ }
+ }
+ },
+ seeders_by_hash: {
+ map: function(doc) {
+ if (doc.type == 'peer' && doc.left) {
+ emit(doc.info_hash, 1);
+ }
+ },
+ reduce: function(keys, values) {
+ return sum(values);
+ }
+ },
+ leechers_by_hash: {
+ map: function(doc) {
+ if (doc.type == 'peer' && !doc.left) {
+ emit(doc.info_hash, 1);
+ }
+ },
+ reduce: function(keys, values) {
+ return sum(values);
+ }
+ }
+ };
+
+ db.save('_design/tracker', views, function(err) {
+ if (err) return callback(err);
+ callback(null, db);
+ });
+}
@@ -0,0 +1,11 @@
+/**
+* Error middleware
+*/
+module.exports = function(req, res, next) {
+ res.writeError = function(error) {
+ res.writeHead(500);
+ console.log(error);
+ res.end((error || '').toString());
+ };
+ next();
+};
@@ -0,0 +1,82 @@
+/**
+* helper functions
+*/
+
+/**
+* Merge contents of 'a' and 'b' objects
+* And return new object, containing merged values
+*/
+exports.merge = function(a, b) {
+ var c = {};
+ if (a) for (var i in a) if (a.hasOwnProperty(i)) c[i] = a[i];
+ if (b) for (var i in b) if (b.hasOwnProperty(i)) c[i] = b[i];
+
+ return c;
+};
+
+/**
+* Like Array.forEach, but for async modes
+*/
+exports.forEachAsync = function(arr, iterator, finish) {
+ var len = arr.length;
+
+ function next(i) {
+ if (i >= len) return finish();
+
+ iterator(function() {
+ next(i + 1);
+ }, arr[i], i, arr);
+ }
+ next(0);
+};
+
+/**
+* Runs through array and executing iterator for every array's element
+* once all iterators have called next() (which is a first argument)
+* finish will be called
+*
+* If one of them will call next(err);
+* Finish will be called with err as first argument
+*/
+exports.asyncEnsure = function(arr, iterator, finish) {
+ var len = arr.length,
+ called = 0,
+ finished = false;
+
+ arr.forEach(function(elem, i) {
+ var once = false;
+
+ iterator(callback, elem, i, arr);
+
+ function callback(err) {
+ if (once || finished) return;
+ once = true;
+
+ if (err) {
+ finished = true;
+ finish(err);
+ }
+
+ if (++called >= len) {
+ finish();
+ };
+ }
+ });
+};
+
+
+/**
+* Compact peer address to binary format
+*/
+exports.compactAddr = function(peer) {
+ var ip = peer.ip.split('.'),
+ port = String.fromCharCode(peer.port >> 8, peer.port % 256);
+ return String.fromCharCode.apply(String, ip) + port;
+};
+
+/**
+* Decode binary hash into a hex
+*/
+exports.decodeInfoHash = function(binary) {
+ return escape(binary);
+};

Some generated files are not rendered by default. Learn more.

Oops, something went wrong.
@@ -0,0 +1,16 @@
+/**
+* Middleware for decoding get query
+*/
+var qs = require('querystring');
+
+module.exports = function(req, res, next) {
+
+ if (match = req.url.match(/\?(.+)$/)) {
+
+ req.query = qs.parse(match[1]);
+
+ } else {
+ req.query = {};
+ }
+ next();
+}
@@ -0,0 +1,65 @@
+/**
+* Create http server with routes
+*/
+var connect = require('connect'),
+ bencode = require('./bencode'),
+ error = require('./error'),
+ querydecoder = require('./querydecoder'),
+ Storage = require('./storage'),
+ helpers = require('./helpers');
+
+module.exports = function(db, options, callback) {
+ var server = connect.createServer(
+ bencode,
+ error,
+ querydecoder,
+ connect.router(router)
+ );
+ server.listen(options.server.port, options.server.host);
+
+ var storage = new Storage(db, options);
+
+ function router(app) {
+ app.get('/reannounce', function(req, res) {
+ var info_hash = req.query.info_hash,
+ event = req.query.event,
+ port = parseInt(req.query.port) || 0,
+ left = parseInt(req.query.left) || 1,
+ ip = req.socket.remoteAddress;
+
+ validateAnnounce(info_hash, port, ip, function(err) {
+ if (err) return res.writeError(err);
+
+ var peer = {
+ info_hash: helpers.decodeInfoHash(info_hash),
+ port: port,
+ left: left,
+ ip: ip
+ };
+
+ storage.route(event, peer, function(err, data) {
+ if (err) return res.writeError(err);
+ res.bencode(data);
+ });
+ });
+ });
+ };
+
+ callback();
+};
+
+/**
+* Validates input data of announce
+*/
+function validateAnnounce(info_hash, port, ip, callback) {
+ if (!info_hash || info_hash.length != 20) {
+ return callback('Wrong info_hash!');
+ }
+ if (!port || port < 0 || port > 65535 ) {
+ return callback('Incorrect port!');
+ }
+ if (!ip || !/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(ip)) {
+ return callback('Incorrect IP address');
+ }
+ callback();
+}
@@ -0,0 +1,95 @@
+/**
+* Peer storage (aka API)
+*/
+var util = require('util'),
+ helpers = require('./helpers');
+
+var Storage = module.exports = function(db, options) {
+ process.EventEmitter.call(this);
+ this._db = db;
+ this._options = options;
+};
+util.inherits(Storage, process.EventEmitter);
+
+/**
+* Route incoming events, supported events:
+* "stopped" - will remove peer from database
+*/
+Storage.prototype.route = function(event, peer, callback) {
+ callback = callback || function() {};
+ if (event == 'stopped') return this.stop(peer, callback);
+
+ this.put(event, peer, callback);
+};
+
+/**
+* Remove peer from database
+*/
+Storage.prototype.stop = function(peer, callback) {
+ var db = this._db,
+ _id = peer_id(peer);
+
+ db.get(_id, function(err, doc) {
+ if (err) return callback(err);
+ db.remove(_id, doc._rev, function(err, res) {
+ if (err || !res.ok) return callback(err || res);
+
+ callback();
+ });
+ });
+};
+
+/**
+* Put peer into database
+*/
+Storage.prototype.put = function(event, peer, callback) {
+ var db = this._db,
+ _id = peer_id(peer),
+ that = this,
+ peerDoc = {
+ type: 'peer',
+ left: peer.left,
+ info_hash: peer.info_hash,
+ ip: peer.ip,
+ port: peer.port,
+ updated_at: +new Date
+ };
+
+ db.save(_id, peerDoc, finish);
+
+ function finish(err, res) {
+ if (err || !res.ok) return callback(err || res);
+
+ that.list(peer, callback);
+ };
+};
+
+/**
+* Get peer list by info_hash (without current peer)
+*/
+Storage.prototype.list = function(peer, callback) {
+ var options = this._options,
+ db = this._db;
+
+ db.view('tracker/by_hash', {key: peer.info_hash}, function(err, rows) {
+ if (err) return callback(err);
+
+ var peers = rows.map(function(peer) {
+ return helpers.compactAddr(peer);
+ }).join('');
+
+ var result = {
+ interval: options.announce_interval,
+ 'min interval': options.announce_min_interval,
+ peers: peers
+ };
+ callback(null, result);
+ });
+};
+
+/**
+* Generate peer_id
+*/
+function peer_id(peer) {
+ return peer.info_hash + ':' + peer.ip + ':' + peer.port;
+};
Oops, something went wrong.

0 comments on commit 3280409

Please sign in to comment.