firecracker 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rspec ADDED
@@ -0,0 +1,5 @@
1
+ --color
2
+ -fs
3
+ -Ilib
4
+ -Ispec
5
+ --require spec_helper
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'http://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in firecracker.gemspec
4
+ gemspec
data/README.md ADDED
@@ -0,0 +1,123 @@
1
+ # Firecracker
2
+
3
+ An implementation of the [UDP](http://bittorrent.org/beps/bep_0015.html)/[TCP](http://wiki.theory.org/BitTorrentSpecification#Tracker_.27scrape.27_Convention) torrent scrape protocol.
4
+
5
+ ## Get started
6
+
7
+ All methods below returns a hash similar to this one.
8
+
9
+ ``` ruby
10
+ {
11
+ seeders: 123,
12
+ leechers: 456,
13
+ downloads: 789
14
+ }
15
+ ```
16
+
17
+ ### Specify a protocol
18
+
19
+ A second argument may be passed to `load`, `raw` and `calculate` to specify which protocol to use.
20
+ An example argument would look like this: `[:tcp, :udp]`, both tcp and udp are defaults.
21
+
22
+ ### A local torrent file
23
+
24
+ ``` ruby
25
+ Firecracker.load("path/to/file.torrent")
26
+ ```
27
+
28
+ ### A raw torrent string
29
+
30
+ ``` ruby
31
+ torrent = RestClient.get("http://mysite.com/file.torrent")
32
+ Firecracker.raw(torrent)
33
+ ```
34
+
35
+ ### A String#bdecode hash
36
+
37
+ ``` ruby
38
+ torrent = RestClient.get("http://mysite.com/file.torrent")
39
+ Firecracker.calculate(torrent.bdecode)
40
+ ```
41
+
42
+ ## Helper methods
43
+
44
+ Ingoing argument (`torrent`) is from now on a [String#bdecode](https://github.com/naquad/bencode_ext) hash.
45
+
46
+ ``` ruby
47
+ require "bencode_ext"
48
+ torrent = File.read("path/to/file.torrent").bdecode
49
+ ```
50
+
51
+ ### Generate a info_hash string
52
+
53
+ ``` ruby
54
+ Firecracker.hash(torrent)
55
+ # => "03db8637a8e16f7d5e3e4f7557d5d87b1905dc16"
56
+ ```
57
+
58
+ ### A list of TCP/UDP trackers
59
+
60
+ ``` ruby
61
+ Firecracker.udp_trackers(torrent)
62
+ # => ["udp://tracker.openbittorrent.com:80", "..."]
63
+
64
+ Firecracker.tcp_trackers(torrent)
65
+ # => ["http://torrent.ubuntu.com:6969/scrape", "..."]
66
+ ```
67
+
68
+ ## UDP/TCP requests
69
+
70
+ If you want to define your own server or/and protocol you can do this using the [TCPScraper](https://github.com/oleander/firecracker/blob/master/lib/firecracker/tcp_scraper.rb) and [UDPScraper](https://github.com/oleander/firecracker/blob/master/lib/firecracker/udp_scraper.rb) classes.
71
+
72
+ The hash being passed is a [info_hash](http://wiki.theory.org/BitTorrent_Tracker_Protocol) string.
73
+
74
+ You can in theory pass up to 72 hashes in one request.
75
+
76
+ Keep in mind that if one of the passed hashes is invalid or doesn't exist, the requested server might return 404 or 400.
77
+ It's therefore recommended to make one request for each hash.
78
+
79
+ ### TCP
80
+
81
+ ``` ruby
82
+ Firecracker::TCPScraper.new({
83
+ tracker: "exodus.desync.com:6969/announce",
84
+ hashes: ["c2cff4acc8f5b49fd6b93b88fc0423467fbb08b0"]
85
+ }).process!
86
+
87
+ # {
88
+ # c2cff4acc8f5b49fd6b93b88fc0423467fbb08b0: {
89
+ # seeders: 123,
90
+ # leechers, 456
91
+ # downloads: 789
92
+ # }
93
+ # }
94
+ ```
95
+
96
+ ### UDP
97
+
98
+ ``` ruby
99
+ Firecracker::UDPScraper.new({
100
+ tracker: "tracker.openbittorrent.com",
101
+ hashes: ["523d83e8aee1a979e66584b5304d2e8fdc9a1675"]
102
+ }).process!
103
+
104
+ # {
105
+ # 523d83e8aee1a979e66584b5304d2e8fdc9a1675: {
106
+ # seeders: 123,
107
+ # leechers, 456
108
+ # downloads: 789
109
+ # }
110
+ # }
111
+ ```
112
+
113
+ ## How to install
114
+
115
+ [sudo] gem install firecracker
116
+
117
+ ## Requirements
118
+
119
+ Ruby *1.9.2*.
120
+
121
+ ## License
122
+
123
+ *Firecracker* is released under the *MIT license*.
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
@@ -0,0 +1,26 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path("../lib/firecracker/version", __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Linus Oleander"]
6
+ gem.email = ["linus@oleander.nu"]
7
+ gem.description = %q{An implementation of the UDP/TCP torrent scrape protocol}
8
+ gem.summary = %q{An implementation of the UDP/TCP torrent scrape protocol}
9
+ gem.homepage = "https://github.com/oleander.nu/firecracker-rb"
10
+
11
+ gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
12
+ gem.files = `git ls-files`.split("\n")
13
+ gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
14
+ gem.name = "firecracker"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = Firecracker::VERSION
17
+
18
+ gem.add_dependency("rest-client")
19
+ gem.add_dependency("bencode_ext")
20
+
21
+ gem.add_development_dependency("vcr")
22
+ gem.add_development_dependency("rspec")
23
+ gem.add_development_dependency("webmock")
24
+
25
+ gem.required_ruby_version = "~> 1.9.0"
26
+ end
@@ -0,0 +1,108 @@
1
+ require "firecracker/udp_scraper"
2
+ require "firecracker/tcp_scraper"
3
+ require "digest/sha1"
4
+ require "bencode_ext"
5
+ require "uri"
6
+
7
+ module Firecracker
8
+ #
9
+ # @torrent String Path to a torrent file
10
+ # @protocols Array<Symbol> Protocols that should be used. UDP is the fastest.
11
+ # @return Hash Seeders, leechers and the amounts of downloads
12
+ #
13
+ def self.load(torrent, protocols = [:udp, :tcp])
14
+ Firecracker.raw(File.read(torrent))
15
+ end
16
+
17
+ #
18
+ # @torrent String A raw torrent file.
19
+ # @protocols Array<Symbol> Protocols that should be used. UDP is the fastest.
20
+ # @return Hash Seeders, leechers and the amounts of downloads
21
+ #
22
+ def self.raw(raw, protocols = [:udp, :tcp])
23
+ Firecracker.calculate(raw.bdecode)
24
+ end
25
+
26
+ #
27
+ # @torrent Hash A Torrent hash generated using String#bdecode
28
+ # @protocols Array<Symbol> Protocols that should be used. UDP is the fastest.
29
+ # @return Hash Seeders, leechers and the amounts of downloads
30
+ #
31
+ def self.calculate(torrent, protocols = [:udp, :tcp])
32
+ raise "At least one protocol needs to be passed" if protocols.empty?
33
+
34
+ # UDP related trackers
35
+ if protocols.include?(:udp)
36
+ trackers = udp_trackers(torrent)
37
+ udp_results = trackers.map do |tracker|
38
+ begin
39
+ Firecracker::UDPScraper.new({
40
+ tracker: tracker,
41
+ hashes: [hash(torrent)]
42
+ }).process!
43
+ rescue
44
+ # raise $! unless silent
45
+ end
46
+ end.reject(&:nil?).map(&:values).flatten
47
+ end
48
+
49
+ # TCP related trackers
50
+ if protocols.include?(:tcp)
51
+ trackers = tcp_trackers(torrent)
52
+ tcp_results = trackers.map do |tracker|
53
+ begin
54
+ Firecracker::TCPScraper.new({
55
+ tracker: tracker,
56
+ hashes: [hash(torrent)]
57
+ }).process!
58
+ rescue
59
+ # raise $! unless silent
60
+ end
61
+ end.reject(&:nil?).map(&:values).flatten
62
+ end
63
+
64
+ (tcp_results + udp_results).inject{ |memo, el| memo.merge(el){ |k, old_v, new_v| old_v + new_v } }
65
+ end
66
+
67
+ #
68
+ # @torrent Hash A Torrent hash generated using String#bdecode
69
+ # @return Array<String> A list of UDP trackers for the given torrent
70
+ #
71
+ def self.udp_trackers(torrent)
72
+ announce = torrent["announce"]
73
+ announce_list = torrent["announce-list"]
74
+
75
+ trackers = announce_list.flatten.select{|tracker| tracker.match(/^udp:\/\//)}
76
+ if announce.match(/^udp:\/\//)
77
+ trackers << announce
78
+ end
79
+
80
+ return trackers
81
+ end
82
+
83
+ #
84
+ # @torrent Hash A Torrent hash generated using String#bdecode
85
+ # @return Array<String> A list of TCP trackers for the given torrent
86
+ #
87
+ def self.tcp_trackers(torrent)
88
+ announce = torrent["announce"]
89
+ announce_list = torrent["announce-list"]
90
+
91
+ trackers = announce_list.flatten.select{|tracker| tracker.match(/^http:\/\//)}
92
+ if announce.match(/^http:\/\//)
93
+ trackers << announce
94
+ end
95
+
96
+ # TPBs tracker is no longer active
97
+ return trackers.map{|t| t.gsub(/announce/, "scrape")}.reject{|t| t.match(%r{thepiratebay})}
98
+ end
99
+
100
+ #
101
+ # @torrent Hash A Torrent hash generated using String#bdecode
102
+ # @return String An info_hash. Read more about it here:
103
+ # http://wiki.theory.org/BitTorrent_Tracker_Protocol
104
+ #
105
+ def self.hash(torrent)
106
+ Digest::SHA1.hexdigest(torrent["info"].bencode)
107
+ end
108
+ end
@@ -0,0 +1,56 @@
1
+ require "bencode_ext"
2
+ require "timeout"
3
+
4
+ module Firecracker
5
+ class Base
6
+ #
7
+ # @args Hash A bunch of options
8
+ # Example:
9
+ # {
10
+ # tracker: "tracker.ccc.de",
11
+ # hashes: ["2bbf3d63e6b313ecf2655067b51e93f17eeeb135"],
12
+ # debug: false
13
+ # }
14
+ #
15
+ def initialize(args = {})
16
+ @options = {
17
+ debug: false,
18
+ timeout: 2,
19
+ tracker: nil,
20
+ hashes: []
21
+ }.merge(args)
22
+ end
23
+
24
+ #
25
+ # @return Do we have everyting that's needed
26
+ # to do the request.
27
+ #
28
+ def valid?
29
+ [
30
+ @options[:tracker],
31
+ @options[:hashes].count.between?(1, 72)
32
+ ].all?
33
+ end
34
+
35
+ #
36
+ # @return Should we print debug ouput?
37
+ #
38
+ def debug?
39
+ @options[:debug]
40
+ end
41
+
42
+ #
43
+ # @return Array<String> A list of hashes
44
+ #
45
+ def hashes
46
+ @options[:hashes]
47
+ end
48
+
49
+ #
50
+ # @return Integer Global timeout limit
51
+ #
52
+ def timeout
53
+ @options[:timeout]
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,95 @@
1
+ require "rest-client"
2
+ require "uri"
3
+ require_relative "base"
4
+
5
+ module Firecracker
6
+ class TCPScraper < Firecracker::Base
7
+ #
8
+ # @return Hash
9
+ # Example: {
10
+ # c2cff4acc8f5b49fd6b93b88fc0423467fbb08b0: {
11
+ # downloads: 123,
12
+ # complete: 456,
13
+ # incomplete: 789
14
+ # }
15
+ # }
16
+ #
17
+ def process!
18
+ raise "both #tracker and #hashes/#hash must be set" unless valid?
19
+
20
+ keys = ["downloaded", "complete", "incomplete"]
21
+ map = {:complete => :seeders, :incomplete => :leechers, :downloaded => :downloads}
22
+ results = Hash.new { |h,k| h[k] = 0 }
23
+
24
+ raise %q{
25
+ Someting went wrong.
26
+ You've passed multiply hashes, but the
27
+ tracker only responed with one result.
28
+ } if files.empty? and not @options[:hashes].one?
29
+
30
+ if files.empty?
31
+ keys.each do |key|
32
+ replace = map[key.to_sym] ? map[key.to_sym] : key
33
+ results[replace.to_sym] += raw_hash[key].to_i
34
+ end
35
+
36
+ return {
37
+ hashes.first => results
38
+ }
39
+ end
40
+
41
+ files.keys.each do |key|
42
+ file = files[key]
43
+ results.merge!({
44
+ key => {
45
+ downloads: file["downloaded"],
46
+ seeders: file["complete"],
47
+ leechers: file["incomplete"]
48
+ }
49
+ })
50
+ end
51
+
52
+ return results
53
+ end
54
+
55
+ private
56
+ def files
57
+ @_files ||= raw_hash["files"] || {}
58
+ end
59
+
60
+ def raw_hash
61
+ @_raw_hash ||= if data.nil? or data.empty?
62
+ {}
63
+ else
64
+ count = 0
65
+ data.gsub(/20:(.+?)d8/) do |m|
66
+ hash = hashes[count]
67
+ res = "#{hash.length}:#{hash}d8"
68
+ count += 1; res
69
+ end.bdecode || {}
70
+ end
71
+ end
72
+
73
+ def random_value(max = 20)
74
+ (0...max).map{ ("a".."z").to_a[rand(26)] }.join
75
+ end
76
+
77
+ def hash_info
78
+ @_hash_info ||= @options[:hashes].map do |hash|
79
+ "info_hash=%s" % URI.encode([hash].pack("H*"))
80
+ end.join("&")
81
+ end
82
+
83
+ #
84
+ # Sometime, I'm not sure when, the timeout value
85
+ # passed to RestClient is ignored. That is why
86
+ # the hole method is wrapped inside a Timeout block.
87
+ # Is there a better solution?
88
+ #
89
+ def data
90
+ Timeout::timeout(timeout) {
91
+ @_data ||= RestClient.get("#{@options[:tracker]}?%s" % hash_info, timeout: timeout)
92
+ }
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,90 @@
1
+ require "socket"
2
+ require_relative "base"
3
+
4
+ module Firecracker
5
+ class UDPScraper < Firecracker::Base
6
+ #
7
+ # @return Hash
8
+ # Example: {
9
+ # c2cff4acc8f5b49fd6b93b88fc0423467fbb08b0: {
10
+ # downloads: 123,
11
+ # complete: 456,
12
+ # incomplete: 789
13
+ # }
14
+ # }
15
+ #
16
+ def process!
17
+ raise "both #tracker and #hashes/#hash must be set" unless valid?
18
+
19
+ # Handshake
20
+ data = send(to_hex(4497486125440, 8) + to_hex(0, 4) + transaction_id)
21
+ request_error! unless data
22
+
23
+ # Main request
24
+ data = send(data[16..31] + to_hex(2, 4) + transaction_id + hashes.join)
25
+ request_error! unless data
26
+
27
+ index = 16
28
+ results = {}
29
+
30
+ loop do
31
+ break unless data[index + 23]
32
+
33
+ completed = data[(index + 8)..index + 15].to_i(16)
34
+ leechers = data[(index + 16)..index + 23].to_i(16)
35
+ seeders = data[index..index + 7].to_i(16)
36
+ hash = hashes[(index - 16)/24]
37
+
38
+ results.merge!({
39
+ hash => {
40
+ downloads: completed,
41
+ seeders: seeders,
42
+ leechers: leechers
43
+ }
44
+ })
45
+
46
+ index += 24
47
+ end
48
+
49
+ return results
50
+ end
51
+
52
+ private
53
+ def socket
54
+ @_socket ||= UDPSocket.open
55
+ end
56
+
57
+ def request_error!
58
+ raise "Request error. UDP server did not respond"
59
+ end
60
+
61
+ def to_hex(value, max)
62
+ value.to_s(16).rjust(max * 2, "0")
63
+ end
64
+
65
+ def tracker
66
+ if @options[:tracker] =~ /^udp:\/\//
67
+ @options[:tracker]
68
+ else
69
+ "udp://#{@options[:tracker]}"
70
+ end
71
+ end
72
+
73
+ def send(data)
74
+ Timeout::timeout(timeout) {
75
+ uri = URI.parse(tracker)
76
+
77
+ socket.send([data].pack("H*"), 0, uri.host, uri.port || 80)
78
+ resp = if select([socket], nil, nil, 3)
79
+ socket.recvfrom_nonblock(65536)
80
+ end
81
+
82
+ resp ? resp.first.unpack("H*").first : nil
83
+ }
84
+ end
85
+
86
+ def transaction_id
87
+ @_transaction_id ||= to_hex(rand(65535), 4)
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,3 @@
1
+ module Firecracker
2
+ VERSION = "1.0.0"
3
+ end
@@ -0,0 +1,18 @@
1
+ describe Firecracker do
2
+ # use_vcr_cassette "example-total" Crashes Ruby for some reason
3
+ before(:all) { @result = Firecracker.load("spec/fixtures/example.torrent") }
4
+
5
+ let(:keys){
6
+ [:seeders, :leechers, :downloads]
7
+ }
8
+
9
+ it "should return seeders, leechers and the amount of downloads" do
10
+ (@result.keys & keys).count.should eq(3)
11
+ end
12
+
13
+ it "should only contain values greater than zero" do
14
+ @result.keys.each do |key|
15
+ @result[key].should > 0
16
+ end
17
+ end
18
+ end
Binary file
File without changes
File without changes
@@ -0,0 +1,339 @@
1
+ ---
2
+ - !ruby/struct:VCR::HTTPInteraction
3
+ request: !ruby/struct:VCR::Request
4
+ method: :get
5
+ uri: http://tracker.blazing.de:80/scrape?info_hash=%03%DB%867%A8%E1o%7D%5E%3EOuW%D5%D8%7B%19%05%DC%16
6
+ body: !!null
7
+ headers:
8
+ accept:
9
+ - ! '*/*; q=0.5, application/xml'
10
+ accept-encoding:
11
+ - gzip, deflate
12
+ timeout:
13
+ - '2'
14
+ response: !ruby/struct:VCR::Response
15
+ status: !ruby/struct:VCR::ResponseStatus
16
+ code: 200
17
+ message: OK
18
+ headers:
19
+ content-type:
20
+ - text/plain
21
+ content-length:
22
+ - '88'
23
+ body: ! "d5:filesd20:\x03Û\x867¨áo}^>OuWÕØ{\x19\x05Ü\x16d8:completei7193e10:downloadedi10e10:incompletei1130eeee"
24
+ http_version: '1.1'
25
+ - !ruby/struct:VCR::HTTPInteraction
26
+ request: !ruby/struct:VCR::Request
27
+ method: :get
28
+ uri: http://tracker.irc.su:80/scrape?info_hash=%03%DB%867%A8%E1o%7D%5E%3EOuW%D5%D8%7B%19%05%DC%16
29
+ body: !!null
30
+ headers:
31
+ accept:
32
+ - ! '*/*; q=0.5, application/xml'
33
+ accept-encoding:
34
+ - gzip, deflate
35
+ timeout:
36
+ - '2'
37
+ response: !ruby/struct:VCR::Response
38
+ status: !ruby/struct:VCR::ResponseStatus
39
+ code: 404
40
+ message: Not Found
41
+ headers:
42
+ content-type:
43
+ - text/html
44
+ server:
45
+ - Microsoft-IIS/7.5
46
+ x-powered-by:
47
+ - ASP.NET
48
+ date:
49
+ - Mon, 19 Dec 2011 23:04:33 GMT
50
+ content-length:
51
+ - '1245'
52
+ body: ! "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">\n<html
53
+ xmlns=\"http://www.w3.org/1999/xhtml\">\n<head>\n<meta http-equiv=\"Content-Type\"
54
+ content=\"text/html; charset=iso-8859-1\"/>\n<title>404 - File or directory
55
+ not found.</title>\n<style type=\"text/css\">\n<!--\nbody{margin:0;font-size:.7em;font-family:Verdana,
56
+ Arial, Helvetica, sans-serif;background:#EEEEEE;}\nfieldset{padding:0 15px 10px
57
+ 15px;} \nh1{font-size:2.4em;margin:0;color:#FFF;}\nh2{font-size:1.7em;margin:0;color:#CC0000;}
58
+ \nh3{font-size:1.2em;margin:10px 0 0 0;color:#000000;} \n#header{width:96%;margin:0
59
+ 0 0 0;padding:6px 2% 6px 2%;font-family:\"trebuchet MS\", Verdana, sans-serif;color:#FFF;\nbackground-color:#555555;}\n#content{margin:0
60
+ 0 0 2%;position:relative;}\n.content-container{background:#FFF;width:96%;margin-top:8px;padding:10px;position:relative;}\n-->\n</style>\n</head>\n<body>\n<div
61
+ id=\"header\"><h1>Server Error</h1></div>\n<div id=\"content\">\n <div class=\"content-container\"><fieldset>\n
62
+ \ <h2>404 - File or directory not found.</h2>\n <h3>The resource you are looking
63
+ for might have been removed, had its name changed, or is temporarily unavailable.</h3>\n
64
+ </fieldset></div>\n</div>\n</body>\n</html>\n"
65
+ http_version: '1.1'
66
+ - !ruby/struct:VCR::HTTPInteraction
67
+ request: !ruby/struct:VCR::Request
68
+ method: :get
69
+ uri: http://tracker.ilibr.org:80/scrape?info_hash=%03%DB%867%A8%E1o%7D%5E%3EOuW%D5%D8%7B%19%05%DC%16
70
+ body: !!null
71
+ headers:
72
+ accept:
73
+ - ! '*/*; q=0.5, application/xml'
74
+ accept-encoding:
75
+ - gzip, deflate
76
+ timeout:
77
+ - '2'
78
+ response: !ruby/struct:VCR::Response
79
+ status: !ruby/struct:VCR::ResponseStatus
80
+ code: 200
81
+ message: OK
82
+ headers:
83
+ content-type:
84
+ - text/plain
85
+ content-length:
86
+ - '88'
87
+ body: ! "d5:filesd20:\x03Û\x867¨áo}^>OuWÕØ{\x19\x05Ü\x16d8:completei7296e10:downloadedi10e10:incompletei1155eeee"
88
+ http_version: '1.1'
89
+ - !ruby/struct:VCR::HTTPInteraction
90
+ request: !ruby/struct:VCR::Request
91
+ method: :get
92
+ uri: http://tracker.csze.com:80/scrape?info_hash=%03%DB%867%A8%E1o%7D%5E%3EOuW%D5%D8%7B%19%05%DC%16
93
+ body: !!null
94
+ headers:
95
+ accept:
96
+ - ! '*/*; q=0.5, application/xml'
97
+ accept-encoding:
98
+ - gzip, deflate
99
+ timeout:
100
+ - '2'
101
+ response: !ruby/struct:VCR::Response
102
+ status: !ruby/struct:VCR::ResponseStatus
103
+ code: 200
104
+ message: OK
105
+ headers:
106
+ content-type:
107
+ - text/plain
108
+ content-length:
109
+ - '88'
110
+ body: ! "d5:filesd20:\x03Û\x867¨áo}^>OuWÕØ{\x19\x05Ü\x16d8:completei7087e10:downloadedi10e10:incompletei1129eeee"
111
+ http_version: '1.1'
112
+ - !ruby/struct:VCR::HTTPInteraction
113
+ request: !ruby/struct:VCR::Request
114
+ method: :get
115
+ uri: http://10.rarbg.com:80/scrape?info_hash=%03%DB%867%A8%E1o%7D%5E%3EOuW%D5%D8%7B%19%05%DC%16
116
+ body: !!null
117
+ headers:
118
+ accept:
119
+ - ! '*/*; q=0.5, application/xml'
120
+ accept-encoding:
121
+ - gzip, deflate
122
+ timeout:
123
+ - '2'
124
+ response: !ruby/struct:VCR::Response
125
+ status: !ruby/struct:VCR::ResponseStatus
126
+ code: 200
127
+ message: OK
128
+ headers:
129
+ content-type:
130
+ - text/plain
131
+ content-length:
132
+ - '88'
133
+ body: ! "d5:filesd20:\x03Û\x867¨áo}^>OuWÕØ{\x19\x05Ü\x16d8:completei7296e10:downloadedi10e10:incompletei1155eeee"
134
+ http_version: '1.1'
135
+ - !ruby/struct:VCR::HTTPInteraction
136
+ request: !ruby/struct:VCR::Request
137
+ method: :get
138
+ uri: http://pow7.com:80/scrape?info_hash=%03%DB%867%A8%E1o%7D%5E%3EOuW%D5%D8%7B%19%05%DC%16
139
+ body: !!null
140
+ headers:
141
+ accept:
142
+ - ! '*/*; q=0.5, application/xml'
143
+ accept-encoding:
144
+ - gzip, deflate
145
+ timeout:
146
+ - '2'
147
+ response: !ruby/struct:VCR::Response
148
+ status: !ruby/struct:VCR::ResponseStatus
149
+ code: 200
150
+ message: OK
151
+ headers:
152
+ content-type:
153
+ - text/plain
154
+ content-length:
155
+ - '88'
156
+ body: ! "d5:filesd20:\x03Û\x867¨áo}^>OuWÕØ{\x19\x05Ü\x16d8:completei7296e10:downloadedi10e10:incompletei1155eeee"
157
+ http_version: '1.1'
158
+ - !ruby/struct:VCR::HTTPInteraction
159
+ request: !ruby/struct:VCR::Request
160
+ method: :get
161
+ uri: http://tracker.token.ro:80/scrape?info_hash=%03%DB%867%A8%E1o%7D%5E%3EOuW%D5%D8%7B%19%05%DC%16
162
+ body: !!null
163
+ headers:
164
+ accept:
165
+ - ! '*/*; q=0.5, application/xml'
166
+ accept-encoding:
167
+ - gzip, deflate
168
+ timeout:
169
+ - '2'
170
+ response: !ruby/struct:VCR::Response
171
+ status: !ruby/struct:VCR::ResponseStatus
172
+ code: 200
173
+ message: OK
174
+ headers:
175
+ content-type:
176
+ - text/plain
177
+ content-length:
178
+ - '88'
179
+ body: ! "d5:filesd20:\x03Û\x867¨áo}^>OuWÕØ{\x19\x05Ü\x16d8:completei7297e10:downloadedi10e10:incompletei1156eeee"
180
+ http_version: '1.1'
181
+ - !ruby/struct:VCR::HTTPInteraction
182
+ request: !ruby/struct:VCR::Request
183
+ method: :get
184
+ uri: http://fr33dom.h33t.com:3310/scrape?info_hash=%03%DB%867%A8%E1o%7D%5E%3EOuW%D5%D8%7B%19%05%DC%16
185
+ body: !!null
186
+ headers:
187
+ accept:
188
+ - ! '*/*; q=0.5, application/xml'
189
+ accept-encoding:
190
+ - gzip, deflate
191
+ timeout:
192
+ - '2'
193
+ response: !ruby/struct:VCR::Response
194
+ status: !ruby/struct:VCR::ResponseStatus
195
+ code: 200
196
+ message: OK
197
+ headers: !!null
198
+ body: d5:filesde5:flagsd20:min_request_intervali1800eee
199
+ http_version: '1.1'
200
+ - !ruby/struct:VCR::HTTPInteraction
201
+ request: !ruby/struct:VCR::Request
202
+ method: :get
203
+ uri: http://t2.pow7.com:80/scrape?info_hash=%03%DB%867%A8%E1o%7D%5E%3EOuW%D5%D8%7B%19%05%DC%16
204
+ body: !!null
205
+ headers:
206
+ accept:
207
+ - ! '*/*; q=0.5, application/xml'
208
+ accept-encoding:
209
+ - gzip, deflate
210
+ timeout:
211
+ - '2'
212
+ response: !ruby/struct:VCR::Response
213
+ status: !ruby/struct:VCR::ResponseStatus
214
+ code: 200
215
+ message: OK
216
+ headers:
217
+ content-type:
218
+ - text/plain
219
+ content-length:
220
+ - '88'
221
+ body: ! "d5:filesd20:\x03Û\x867¨áo}^>OuWÕØ{\x19\x05Ü\x16d8:completei7297e10:downloadedi10e10:incompletei1156eeee"
222
+ http_version: '1.1'
223
+ - !ruby/struct:VCR::HTTPInteraction
224
+ request: !ruby/struct:VCR::Request
225
+ method: :get
226
+ uri: http://tracker.ilibr.org:6969/scrape?info_hash=%03%DB%867%A8%E1o%7D%5E%3EOuW%D5%D8%7B%19%05%DC%16
227
+ body: !!null
228
+ headers:
229
+ accept:
230
+ - ! '*/*; q=0.5, application/xml'
231
+ accept-encoding:
232
+ - gzip, deflate
233
+ timeout:
234
+ - '2'
235
+ response: !ruby/struct:VCR::Response
236
+ status: !ruby/struct:VCR::ResponseStatus
237
+ code: 200
238
+ message: OK
239
+ headers:
240
+ content-type:
241
+ - text/plain
242
+ content-length:
243
+ - '88'
244
+ body: ! "d5:filesd20:\x03Û\x867¨áo}^>OuWÕØ{\x19\x05Ü\x16d8:completei7297e10:downloadedi10e10:incompletei1156eeee"
245
+ http_version: '1.1'
246
+ - !ruby/struct:VCR::HTTPInteraction
247
+ request: !ruby/struct:VCR::Request
248
+ method: :get
249
+ uri: http://t1.pow7.com:80/scrape?info_hash=%03%DB%867%A8%E1o%7D%5E%3EOuW%D5%D8%7B%19%05%DC%16
250
+ body: !!null
251
+ headers:
252
+ accept:
253
+ - ! '*/*; q=0.5, application/xml'
254
+ accept-encoding:
255
+ - gzip, deflate
256
+ timeout:
257
+ - '2'
258
+ response: !ruby/struct:VCR::Response
259
+ status: !ruby/struct:VCR::ResponseStatus
260
+ code: 200
261
+ message: OK
262
+ headers:
263
+ content-type:
264
+ - text/plain
265
+ content-length:
266
+ - '88'
267
+ body: ! "d5:filesd20:\x03Û\x867¨áo}^>OuWÕØ{\x19\x05Ü\x16d8:completei7297e10:downloadedi10e10:incompletei1156eeee"
268
+ http_version: '1.1'
269
+ - !ruby/struct:VCR::HTTPInteraction
270
+ request: !ruby/struct:VCR::Request
271
+ method: :get
272
+ uri: http://ipv4.tracker.harry.lu:80/scrape?info_hash=%03%DB%867%A8%E1o%7D%5E%3EOuW%D5%D8%7B%19%05%DC%16
273
+ body: !!null
274
+ headers:
275
+ accept:
276
+ - ! '*/*; q=0.5, application/xml'
277
+ accept-encoding:
278
+ - gzip, deflate
279
+ timeout:
280
+ - '2'
281
+ response: !ruby/struct:VCR::Response
282
+ status: !ruby/struct:VCR::ResponseStatus
283
+ code: 200
284
+ message: OK
285
+ headers:
286
+ content-type:
287
+ - text/plain
288
+ content-length:
289
+ - '88'
290
+ body: ! "d5:filesd20:\x03Û\x867¨áo}^>OuWÕØ{\x19\x05Ü\x16d8:completei7298e10:downloadedi10e10:incompletei1156eeee"
291
+ http_version: '1.1'
292
+ - !ruby/struct:VCR::HTTPInteraction
293
+ request: !ruby/struct:VCR::Request
294
+ method: :get
295
+ uri: http://tracker.pow7.com:80/scrape?info_hash=%03%DB%867%A8%E1o%7D%5E%3EOuW%D5%D8%7B%19%05%DC%16
296
+ body: !!null
297
+ headers:
298
+ accept:
299
+ - ! '*/*; q=0.5, application/xml'
300
+ accept-encoding:
301
+ - gzip, deflate
302
+ timeout:
303
+ - '2'
304
+ response: !ruby/struct:VCR::Response
305
+ status: !ruby/struct:VCR::ResponseStatus
306
+ code: 200
307
+ message: OK
308
+ headers:
309
+ content-type:
310
+ - text/plain
311
+ content-length:
312
+ - '88'
313
+ body: ! "d5:filesd20:\x03Û\x867¨áo}^>OuWÕØ{\x19\x05Ü\x16d8:completei7298e10:downloadedi10e10:incompletei1156eeee"
314
+ http_version: '1.1'
315
+ - !ruby/struct:VCR::HTTPInteraction
316
+ request: !ruby/struct:VCR::Request
317
+ method: :get
318
+ uri: http://tracker.pow7.com:80/scrape?info_hash=%02%95$%F34%A2%C0%9F3%05w%CA%9A5x%E1Z1%F0%8B
319
+ body: !!null
320
+ headers:
321
+ accept:
322
+ - ! '*/*; q=0.5, application/xml'
323
+ accept-encoding:
324
+ - gzip, deflate
325
+ timeout:
326
+ - '2'
327
+ response: !ruby/struct:VCR::Response
328
+ status: !ruby/struct:VCR::ResponseStatus
329
+ code: 400
330
+ message: Invalid Request
331
+ headers:
332
+ content-type:
333
+ - text/html
334
+ content-length:
335
+ - '31'
336
+ body: ! '<title>Invalid Request</title>
337
+
338
+ '
339
+ http_version: '1.1'
@@ -0,0 +1,19 @@
1
+ require "rspec"
2
+ require "webmock/rspec"
3
+ require "vcr"
4
+ require "yaml"
5
+ require "firecracker"
6
+
7
+ RSpec.configure do |config|
8
+ config.mock_with :rspec
9
+ config.extend VCR::RSpec::Macros
10
+ end
11
+
12
+ VCR.config do |c|
13
+ c.cassette_library_dir = "spec/fixtures/vcr_cassettes"
14
+ c.stub_with :webmock
15
+ c.default_cassette_options = {
16
+ record: :new_episodes
17
+ }
18
+ c.allow_http_connections_when_no_cassette = true
19
+ end
data/spec/tcp_spec.rb ADDED
@@ -0,0 +1,65 @@
1
+ describe Firecracker::TCPScraper do
2
+ describe "single file" do
3
+ # use_vcr_cassette "example1" Doesn't work for some reason
4
+
5
+ let(:hashes) { ["c2cff4acc8f5b49fd6b93b88fc0423467fbb08b0"] }
6
+ let(:raw){
7
+ Firecracker::TCPScraper.new({
8
+ tracker: "exodus.desync.com:6969/announce",
9
+ hashes: hashes,
10
+ debug: true
11
+ }).process!
12
+ }
13
+
14
+ let(:values) { raw[raw.keys.first] }
15
+
16
+ it "should match the number of hashes" do
17
+ raw.should have(1).keys
18
+ raw.keys.first.should eq(hashes.first)
19
+ end
20
+
21
+ it "should have 3 categories" do
22
+ values.keys.each do |key|
23
+ [:downloads, :seeders, :leechers].should include(key)
24
+ end
25
+ end
26
+
27
+ it "should only contain values greater than zero" do
28
+ values.keys.each do |key|
29
+ values[key].should > 0
30
+ end
31
+ end
32
+ end
33
+
34
+ describe "multi file" do
35
+ use_vcr_cassette "example2"
36
+
37
+ let(:hashes) { ["03db8637a8e16f7d5e3e4f7557d5d87b1905dc16"] }
38
+ let(:raw){
39
+ Firecracker::TCPScraper.new({
40
+ tracker: "tracker.pow7.com:80/scrape",
41
+ hashes: hashes,
42
+ debug: true
43
+ }).process!
44
+ }
45
+
46
+ let(:values) { raw[raw.keys.first] }
47
+
48
+ it "should match the number of hashes" do
49
+ raw.should have(1).keys
50
+ raw.keys.first.should eq(hashes.first)
51
+ end
52
+
53
+ it "should have 3 categories" do
54
+ values.keys.each do |key|
55
+ [:seeders, :leechers, :downloads].should include(key)
56
+ end
57
+ end
58
+
59
+ it "should only contain values greater than zero" do
60
+ values.keys.each do |key|
61
+ values[key].should > 0
62
+ end
63
+ end
64
+ end
65
+ end
data/spec/udp_spec.rb ADDED
@@ -0,0 +1,31 @@
1
+ require "spec_helper"
2
+
3
+ describe Firecracker::UDPScraper do
4
+ let(:hashes) { ["523d83e8aee1a979e66584b5304d2e8fdc9a1675"] }
5
+
6
+ let(:raw){
7
+ Firecracker::UDPScraper.new({
8
+ tracker: "tracker.openbittorrent.com",
9
+ hashes: hashes
10
+ }).process!
11
+ }
12
+
13
+ let(:values) { raw[raw.keys.first] }
14
+
15
+ it "should match the number of hashes" do
16
+ raw.should have(1).keys
17
+ raw.keys.first.should eq(hashes.first)
18
+ end
19
+
20
+ it "should have 3 categories" do
21
+ values.keys.each do |key|
22
+ [:downloads, :seeders, :leechers].should include(key)
23
+ end
24
+ end
25
+
26
+ it "should only contain values greater than zero" do
27
+ values.keys.each do |key|
28
+ values[key].should > 0
29
+ end
30
+ end
31
+ end
metadata ADDED
@@ -0,0 +1,127 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: firecracker
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Linus Oleander
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-02-12 00:00:00.000000000Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rest-client
16
+ requirement: &70225212402380 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *70225212402380
25
+ - !ruby/object:Gem::Dependency
26
+ name: bencode_ext
27
+ requirement: &70225212401960 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: *70225212401960
36
+ - !ruby/object:Gem::Dependency
37
+ name: vcr
38
+ requirement: &70225212401540 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ type: :development
45
+ prerelease: false
46
+ version_requirements: *70225212401540
47
+ - !ruby/object:Gem::Dependency
48
+ name: rspec
49
+ requirement: &70225212401120 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ type: :development
56
+ prerelease: false
57
+ version_requirements: *70225212401120
58
+ - !ruby/object:Gem::Dependency
59
+ name: webmock
60
+ requirement: &70225212400700 !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ! '>='
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ type: :development
67
+ prerelease: false
68
+ version_requirements: *70225212400700
69
+ description: An implementation of the UDP/TCP torrent scrape protocol
70
+ email:
71
+ - linus@oleander.nu
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - .gitignore
77
+ - .rspec
78
+ - Gemfile
79
+ - README.md
80
+ - Rakefile
81
+ - firecracker.gemspec
82
+ - lib/firecracker.rb
83
+ - lib/firecracker/base.rb
84
+ - lib/firecracker/tcp_scraper.rb
85
+ - lib/firecracker/udp_scraper.rb
86
+ - lib/firecracker/version.rb
87
+ - spec/firecracker_spec.rb
88
+ - spec/fixtures/example.torrent
89
+ - spec/fixtures/vcr_cassettes/example-total.yml
90
+ - spec/fixtures/vcr_cassettes/example1.yml
91
+ - spec/fixtures/vcr_cassettes/example2.yml
92
+ - spec/spec_helper.rb
93
+ - spec/tcp_spec.rb
94
+ - spec/udp_spec.rb
95
+ homepage: https://github.com/oleander.nu/firecracker-rb
96
+ licenses: []
97
+ post_install_message:
98
+ rdoc_options: []
99
+ require_paths:
100
+ - lib
101
+ required_ruby_version: !ruby/object:Gem::Requirement
102
+ none: false
103
+ requirements:
104
+ - - ~>
105
+ - !ruby/object:Gem::Version
106
+ version: 1.9.0
107
+ required_rubygems_version: !ruby/object:Gem::Requirement
108
+ none: false
109
+ requirements:
110
+ - - ! '>='
111
+ - !ruby/object:Gem::Version
112
+ version: '0'
113
+ requirements: []
114
+ rubyforge_project:
115
+ rubygems_version: 1.8.15
116
+ signing_key:
117
+ specification_version: 3
118
+ summary: An implementation of the UDP/TCP torrent scrape protocol
119
+ test_files:
120
+ - spec/firecracker_spec.rb
121
+ - spec/fixtures/example.torrent
122
+ - spec/fixtures/vcr_cassettes/example-total.yml
123
+ - spec/fixtures/vcr_cassettes/example1.yml
124
+ - spec/fixtures/vcr_cassettes/example2.yml
125
+ - spec/spec_helper.rb
126
+ - spec/tcp_spec.rb
127
+ - spec/udp_spec.rb