firecracker 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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