-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 2cddf9e
Showing
7 changed files
with
373 additions
and
0 deletions.
There are no files selected for viewing
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
This is a reimplementation of the original RTracker code that should work with Gazelle-based BitTorrent sites from 2008. The code has not been updated since, and may not be compatible with the current release of Gazelle. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,158 @@ | ||
################################################################## | ||
## Ruby BitTorrent Tracker ## | ||
## ## | ||
## ## | ||
## Copyright 2008 Noah ## | ||
## Released under the Creative Commons Attribution License ## | ||
################################################################## | ||
|
||
# Require RubyGems | ||
require 'rubygems' | ||
# Require the memcache gem | ||
require 'memcache' | ||
|
||
class Cache | ||
attr_reader :cache, :mysql, :user, :torrent | ||
|
||
def initialize(options = {}) | ||
raise CacheError, "You must provide a host and a namespace!" if options[:host].nil? or options[:namespace].nil? | ||
@cache = MemCache::new options[:host], :namespace => options[:namespace] | ||
|
||
unless options[:load_subclasses] == false # So we can get access to the cache without having it instantiate user and torrent objects | ||
raise CacheError, "You must provide a MySQL object!" unless options[:mysql].is_a?(Mysql) | ||
@mysql = options[:mysql] | ||
raise CacheError, "You must provide an info_hash!" if options[:info_hash].nil? | ||
@torrent = Torrent.new(cache, mysql, options[:info_hash]) | ||
raise CacheError, "You must provide a passkey and a peer_id!" if options[:passkey].nil? or options[:peer_id].nil? | ||
@user = User.new(cache, mysql, options[:passkey], options[:peer_id], @torrent) | ||
|
||
@torrent.user = @user # Let out classes know about each other | ||
end | ||
end | ||
|
||
def stats | ||
{ :user => cache.get("user_cache_hits"), :torrent => cache.get("torrent_cache_hits") } | ||
end | ||
|
||
class User | ||
attr_reader :cache, :mysql, :passkey, :peer, :user | ||
attr_accessor :torrent | ||
|
||
def initialize(cache, mysql, passkey, peer_id, torrent) | ||
@cache, @mysql, @passkey, @peer_id, @torrent = cache, mysql, passkey, peer_id, torrent | ||
self.find # Either pull the user out of the cache, or reload! it from mysql | ||
end | ||
|
||
def find | ||
@user = cache.get("user_#{passkey}") # Try to pull the user out of the cache | ||
cache.incr("user_cache_hits", 1) unless user.nil? # We've got a hit! | ||
self.reload! if user.nil? # Try and find us in the database | ||
|
||
# Find the torrent's peerlist entry which belongs to us | ||
@peer = torrent.peers.select { |peer| peer['PeerID'] == @peer_id }.first | ||
end | ||
|
||
def reload! | ||
@user = mysql.query( "SELECT um.ID, um.Enabled, um.can_leech, p.Level FROM users_main AS um LEFT JOIN permissions AS p ON um.PermissionID=p.ID WHERE torrent_pass = '#{passkey}'" ).fetch_hash | ||
cache.set("user_#{passkey}", user.merge({ 'Cached' => true }), 60*60*1) # Save the result to the cache, and leave a note saying it was cached | ||
end | ||
|
||
def update(uploaded, downloaded, left) | ||
uploaded, downloaded = uploaded.to_i - peer['Uploaded'].to_i, downloaded.to_i - peer['Downloaded'].to_i # Find out exactly how much they uploaded since their last announce | ||
if uploaded > 0 || downloaded > 0 # Only update if they've uploaded / downloaded something | ||
# Set download = 0 if the torrent is freeleech | ||
downloaded = 0 if torrent.freeleech? | ||
|
||
# Add the user to the update queue | ||
update_list = cache.get("user_update_list") || [] # Set it to a blank array if it's nil | ||
update_list << { :id => user['ID'], :uploaded => uploaded, :downloaded => downloaded, :left => left, :peer => peer['ID'] } | ||
cache.set("user_update_list", update_list) | ||
end | ||
end | ||
|
||
def exists? | ||
!user.nil? && user['Enabled'] == '1' | ||
end | ||
|
||
def can_leech? | ||
user['can_leech'] == '1' | ||
end | ||
|
||
def cached? | ||
!!user['Cached'] | ||
end | ||
|
||
def [](column) | ||
user[column] | ||
end | ||
end | ||
|
||
class Torrent | ||
attr_reader :cache, :mysql, :hash, :torrent, :peers | ||
attr_accessor :user | ||
|
||
def initialize(cache, mysql, info_hash) | ||
@cache, @mysql, @hash = cache, mysql, info_hash | ||
self.find | ||
end | ||
|
||
def find | ||
# Find the torrent | ||
@torrent = @cache.get("torrent_#{hash}") | ||
@cache.incr("torrent_cache_hits", 1) unless @torrent.nil? | ||
self.reload! if @torrent.nil? | ||
|
||
# Find the peers | ||
@peers = @cache.get("torrent_#{hash}_peers") | ||
@cache.incr("peers_cache_hits", 1) unless @peers.nil? | ||
self.reload_peers! if @peers.nil? | ||
end | ||
|
||
def reload! | ||
@torrent = @mysql.query( "SELECT t.ID, t.FreeTorrent, t.Seeders, t.Leechers, t.Snatched, g.Name FROM torrents AS t LEFT JOIN torrents_group AS g ON t.GroupID=g.ID WHERE info_hash = '#{@hash}'" ).fetch_hash | ||
@cache.set("torrent_#{hash}", @torrent.merge({ 'Cached' => true }), 60*60*1) | ||
end | ||
|
||
def reload_peers! | ||
@peers = [] # MySQL is weird when returning lists of things, and memcache doesn't like that, so we're going to convert them to an array | ||
@mysql.query( "SELECT p.ID, p.UserID, p.IP, p.Port, p.PeerID FROM tracker_peers AS p WHERE TorrentID = #{@torrent['ID']} ORDER BY RAND()" ).each_hash do |peer| | ||
@peers << peer | ||
end | ||
@cache.set("torrent_#{hash}_peers", @peers, 60*60*1) | ||
end | ||
|
||
def new_peer(ip, port, left, peer_id) | ||
@mysql.query( "INSERT INTO tracker_peers (UserID, TorrentID, IP, Port, Uploaded, Downloaded, tracker_peers.Left, PeerID) VALUES (#{user['ID']}, #{@torrent['ID']}, '#{ip}', '#{port}', 0, 0, #{left}, '#{peer_id}')" ) | ||
id = @mysql.insert_id | ||
@peers = @cache.get("torrent_#{hash}_peers") || [] | ||
@peers << { 'ID' => id, 'UserID' => user['ID'], 'IP' => ip, 'Port' => port, 'PeerID' => peer_id } | ||
@cache.set("torrent_#{hash}_peers", @peers, 60*60*1) | ||
end | ||
|
||
def delete_peer | ||
@mysql.query( "DELETE FROM tracker_peers WHERE ID = #{user.peer['ID']}" ) | ||
@peers = @cache.get("torrent_#{hash}_peers") || [] | ||
@peers = @peers.reject { |peer| peer['ID'] == user.peer['ID'] } | ||
@cache.set("torrent_#{hash}_peers", @peers, 60*60*1) | ||
end | ||
|
||
def exists? | ||
!@torrent.nil? | ||
end | ||
|
||
def freeleech? | ||
!!@torrent['FreeTorrent'] | ||
end | ||
|
||
def cached? | ||
!!@torrent['Cached'] | ||
end | ||
|
||
def [](column) | ||
@torrent[column] | ||
end | ||
end | ||
end | ||
|
||
# Error class | ||
class CacheError < RuntimeError; end # Used to clearly identify errors |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
require 'sinatra' | ||
require 'daemons' | ||
#require 'schedule' | ||
|
||
Sinatra::Application.default_options.merge!( | ||
:run => false, | ||
:env => :production, | ||
:raise_errors => true, | ||
:app_file => 'tracker.rb' | ||
) | ||
|
||
|
||
#Daemons.call { Schedule.new.run } | ||
require 'tracker' | ||
run Sinatra.application |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
--- | ||
:log_announces: false | ||
:announce_int: 1800 | ||
:min_announce_int: 300 | ||
:whitelist_url: http://example.com/whitelist | ||
:mysql: | ||
:host: localhost | ||
:user: user | ||
:pass: pass | ||
:database: database | ||
:cache: | ||
:host: localhost | ||
:namespace: rtracker |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
################################################################## | ||
## Ruby BitTorrent Tracker ## | ||
## ## | ||
## ## | ||
## Copyright 2008 Noah ## | ||
## Released under the Creative Commons Attribution License ## | ||
################################################################## | ||
|
||
# Require RubyGems | ||
require 'rubygems' | ||
# Require the mysql gem | ||
require 'mysql' | ||
# Require the memcache gem | ||
require 'memcache' | ||
# Require YAML to parse config files | ||
require 'yaml' | ||
|
||
@config = YAML::load( open('config.yml') ) | ||
@cache = MemCache::new(@config[:cache][:host], :namespace => @config[:cache][:namespace]) | ||
@mysql = Mysql::new($config[:mysql][:host], $config[:mysql][:user], $config[:mysql][:pass], $config[:mysql][:database]) | ||
|
||
class Schedule | ||
def run | ||
loop do | ||
update_users | ||
flush_inactive_peers | ||
sleep @config[:user_update_int] | ||
end | ||
end | ||
|
||
def update_users | ||
update_list = @cache.get("user_update_list") | ||
unless update_list.nil? | ||
user_values = update_list.collect { |user| "(#{user[:id]}, #{user[:uploaded]}, #{user[:downloaded]})" }.join(',') | ||
peer_values = update_list.collect { |user| "(#{user[:id]}, #{user[:uploaded]}, #{user[:downloaded]}, #{user[:left]})" }.join(',') | ||
@mysql.query( "INSERT LOW PRIORITY INTO users_main (ID, Uploaded, Downloaded) VALUES #{user_values} ON DUPLICATE KEY UPDATE Uploaded=Uploaded+VALUES(Uploaded), Downloaded=Downloaded+VALUES(Downloaded)") | ||
@mysql.query( "INSERT LOW PRIORITY INTO tracker_peers (ID, Uploaded, Downloaded, Left) VALUES #{peer_values} ON DUPLICATE KEY UPDATE Uploaded=Uploaded+VALUES(Uploaded), Downloaded=Downloaded+VALUES(Downloaded), Left=VALUES(Left), Time=NOW()") | ||
@cache.set("users_update_list", [], 60*60*1) | ||
end | ||
end | ||
|
||
def flush_inactive_peers | ||
@mysql.query( "DELETE FROM tracker_peers WHERE Time < TIMESTAMP(NOW()-INTERVAL 2 HOUR)") | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
################################################################## | ||
## Ruby BitTorrent Tracker ## | ||
## ## | ||
## ## | ||
## Copyright 2008 Noah ## | ||
## Released under the Creative Commons Attribution License ## | ||
################################################################## | ||
|
||
# Require RubyGems | ||
require 'rubygems' | ||
# Require the bencode gem from http://github.com/dasch/ruby-bencode-bindings/tree/master | ||
require 'bencode' | ||
# Require the mysql gem | ||
require 'mysql' | ||
# Require the memcache gem | ||
require 'memcache' | ||
# Require our cache-abstraction class | ||
require 'cache' | ||
# Require the sinatra gem | ||
require 'sinatra' | ||
# Require YAML to parse config files | ||
require 'yaml' | ||
|
||
configure do | ||
# Load the config | ||
$config = YAML::load( open('config.yml') ) | ||
# Connect to MySQL | ||
$db = Mysql::new($config[:mysql][:host], $config[:mysql][:user], $config[:mysql][:pass], $config[:mysql][:database]) | ||
|
||
whitelist = $db.query( "SELECT Peer_ID, Client FROM whitelist" ) | ||
$whitelist = Array.new | ||
whitelist.each_hash { |client| $whitelist << { :regex => /#{client['Peer_ID']}/, :name => client['Client'] } } # Put a RegEx of each peerid into $whitelist | ||
end | ||
|
||
get '/:passkey/announce' do | ||
begin | ||
# Set instance variables for all the query parameters and make sure they exist | ||
required_params = ['passkey', 'info_hash', 'peer_id', 'port', 'uploaded', 'downloaded', 'left'] | ||
optional_params = ['event', 'compact', 'no_peer_id', 'ip', 'numwant'] | ||
(required_params + optional_params).each do |param| | ||
error "Bad Announce" if (params[param].nil? or params[param].empty?) and required_params.include?(param) | ||
self.instance_variable_set("@" + param, escape(params[param])) | ||
end | ||
@event ||= 'none' | ||
@ip ||= escape(@ip) | ||
@numwant ||= 50 | ||
|
||
# Make sure client is whitelisted | ||
whitelisted = $whitelist.map { |client| @peer_id =~ client[:regex] }.include? 0 | ||
error "Your client is banned. Go to #{$config[:whitelist_url]}" unless whitelisted | ||
|
||
# Instantiate a cache object for this request | ||
cache = Cache.new(:host => $config[:cache][:host], :namespace => $config[:cache][:namespace], :mysql => $db, :passkey => @passkey, :peer_id => @peer_id, :info_hash => @info_hash) | ||
|
||
# Find our user | ||
user = cache.user | ||
error "Account not found" unless user.exists? | ||
error "Leeching Disabled" unless user.can_leech? | ||
|
||
# Find our torrent | ||
torrent = cache.torrent | ||
error "Torrent not found" unless torrent.exists? | ||
|
||
# Find peers | ||
peers = torrent.peers[0, @numwant.to_i] | ||
|
||
# Log Announce | ||
$db.query( "INSERT INTO tracker_announce_log (UserID, TorrentID, IP, Port, Event, Uploaded, Downloaded, tracker_announce_log.Left, PeerID, RequestURI, Time) VALUES (#{user['ID']}, #{torrent['ID']}, '#{@ip}', #{@port}, '#{@event}', #{@uploaded}, #{@downloaded}, #{@left}, '#{@peer_id}', '#{escape request.env['QUERY_STRING']}', NOW())" ) if $config[:log_announces] | ||
|
||
# Generate Peerlist | ||
if @compact == '1' # Compact Mode | ||
peer_list = '' | ||
peers.each do |peer| # Go through each peer | ||
ip = peer['IP'].split('.').collect { |octet| octet.to_i }.pack('C*') | ||
port = [peer['Port'].to_i].pack('n*') | ||
peer_list << ip + port | ||
end | ||
else | ||
peer_list = [] | ||
peers.each do |peer| # Go through each peer | ||
peer_hash = { 'ip' => peer['IP'], 'port' => peer['Port'] } | ||
peer_hash.update( { 'peer id' => peer['PeerID'] } ) unless @no_peer_id == 1 | ||
peer_list << peer_hash | ||
end | ||
end | ||
|
||
@resp = { 'interval' => $config[:announce_int], 'min interval' => $config[:min_announce_int], 'peers' => peer_list } | ||
# End peerlist generation | ||
|
||
# Update database / cache | ||
# Update values specific to each event | ||
case @event | ||
when 'started' | ||
# Add the user to the torrents peerlist, then update the seeder / leecher count | ||
torrent.new_peer(@ip, @port, @left, @peer_id) | ||
$db.query( "UPDATE torrents SET #{@left.to_i > 0 ? 'Leechers = Leechers + 1' : 'Seeders = Seeders + 1'} WHERE ID = #{torrent['ID']}" ) | ||
when 'completed' | ||
$db.query( "INSERT INTO tracker_snatches (UserID, TorrentID, IP, Port, Uploaded, Downloaded, PeerID) VALUES (#{user['ID']}, #{torrent['ID']}, '#{@ip}', #{@port}, #{@uploaded}, #{@downloaded}, '#{@peer_id}')" ) | ||
$db.query( "UPDATE torrents SET Seeders = Seeders + 1, Leechers = Leechers - 1, Snatched = Snatched + 1 WHERE ID = #{torrent['ID']}" ) | ||
when 'stopped' | ||
# Update Seeder / Leecher count for torrent, and update snatched list with final upload / download counts, then delete the user from the torrents peerlist | ||
$db.query( "UPDATE torrents AS t, tracker_snatches AS s SET #{@left.to_i > 0 ? 't.Leechers = t.Leechers - 1' : 't.Seeders = t.Seeders - 1'}, s.Uploaded = #{@uploaded}, s.Downloaded = #{@downloaded} WHERE t.ID = #{torrent['ID']} AND (s.UserID = #{user['ID']} AND s.TorrentID = #{torrent['ID']})" ) | ||
torrent.delete_peer | ||
end | ||
|
||
# Add user to the update queue | ||
user.update(@uploaded, @downloaded, @left) | ||
|
||
@resp.bencode | ||
rescue TrackerError => e | ||
e.message | ||
end | ||
end | ||
|
||
get '/:passkey/scrape' do | ||
begin | ||
error "Bad Scrape" if params['info_hash'].nil? or params['info_hash'].empty? | ||
cache = MemCache::new $config[:cache][:host], :namespace => $config[:cache][:namespace] | ||
torrent = Cache::Torrent.new(cache, $db, params['info_hash']) | ||
|
||
{ params['info_hash'] => { 'complete' => torrent['Seeders'], 'incomplete' => torrent['Leechers'], 'downloaded' => torrent['Snatched'], 'name' => torrent['Name'] } }.bencode | ||
rescue TrackerError => e | ||
e.message | ||
end | ||
end | ||
|
||
get '/whitelist' do | ||
$whitelist.collect { |client| client[:name] }.join('<br />') | ||
end | ||
|
||
helpers do | ||
def error reason | ||
raise TrackerError, { 'failure reason' => reason }.bencode | ||
end | ||
def escape string | ||
Mysql::escape_string(string.to_s) | ||
end | ||
end | ||
|
||
# Error class | ||
class TrackerError < RuntimeError; end # Used to clearly identify errors |