Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
d7d667b
Wrong module
1v Oct 18, 2015
f1243c5
some tests
1v Oct 18, 2015
80139dd
travis config
1v Oct 18, 2015
44bfd9e
adding rake
1v Oct 18, 2015
25ca6bc
gemspec
1v Oct 18, 2015
6aa718b
gemspec
1v Oct 18, 2015
cc2de1b
some changes
1v Oct 18, 2015
391de42
rakefile added
1v Oct 18, 2015
eec9b41
remove Gemfile.lock
1v Oct 18, 2015
115e5cf
codeclimate-test-reporter
1v Oct 18, 2015
3ba7065
env tocken codeclimate
1v Oct 18, 2015
0a2ac97
codeclimate-test-reporter fix
1v Oct 18, 2015
6804a43
implement actual timeout and tries for HTTP
1v Oct 22, 2015
6e74727
guardfile added
1v Oct 22, 2015
07a8ae7
low down number of tries
1v Oct 22, 2015
762e843
formatting
1v Oct 22, 2015
95b95e2
cut off strange timeouts; refactor retries
1v Oct 22, 2015
ec2cbfa
Merge branch 'master' of https://github.com/1v/torckapi
1v Oct 22, 2015
7cde4a3
fixtures refactoring
1v Oct 22, 2015
3ba47c4
adding tests and fixtures
1v Oct 28, 2015
13bbfce
tests added; fixtures filenames changed
1v Nov 2, 2015
d2045d3
trying to fix 1.9.3 version error
1v Nov 2, 2015
76fe51b
adding parentheses according to https://github.com/bbatsov/ruby-style…
1v Nov 6, 2015
c5f9a26
1.9.3 again; using determination of ruby version
1v Nov 7, 2015
b2cf7dc
messages for exceptions
1v Jan 5, 2016
f2758c3
Refactoring
1v Oct 11, 2016
01f733f
Adding license
1v Oct 11, 2016
ee11de2
New badges
1v Oct 12, 2016
fd3a96a
Adding latest stable ruby
1v Oct 12, 2016
f0f3eda
Codeclimate fix for ruby 1.9.3
1v Oct 14, 2016
73570df
Rake version
1v Oct 15, 2016
baebb54
Codeclimate fixed
1v Nov 7, 2016
cfc2536
Update spec_helper.rb
1v Nov 7, 2016
6337e42
Typo
1v Nov 7, 2016
db431e8
Update torckapi.gemspec
1v Nov 7, 2016
c381149
Update torckapi.gemspec
1v Nov 7, 2016
7c26c3e
Update torckapi.gemspec
1v Nov 8, 2016
ea68720
Update torckapi.gemspec
1v Nov 8, 2016
48ba234
Update torckapi.gemspec
1v Nov 8, 2016
4d57be1
Update torckapi.gemspec
1v Nov 8, 2016
1cd94ed
json
1v Nov 8, 2016
203696c
Update .travis.yml
1v Nov 9, 2016
c989237
Eliminate using ruby's Timeout module
CaptainJet May 21, 2017
b000225
Allow proper loading of failed Bencoded data
CaptainJet May 21, 2017
8802ba3
Return response directly
CaptainJet May 22, 2017
5262935
Revert "Allow proper loading of failed Bencoded data"
CaptainJet May 22, 2017
7632a43
Fix specs
1v May 22, 2017
a3d7e88
Merge pull request #1 from 1v/udp-fix
CaptainJet May 22, 2017
159e916
Revert "Revert "Allow proper loading of failed Bencoded data""
CaptainJet May 22, 2017
6c14553
Merge pull request #1 from CaptainJet/master
1v May 22, 2017
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
.yardoc
doc

Gemfile.lock
coverage
Guardfile
2 changes: 2 additions & 0 deletions .rspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-c
-fd
10 changes: 10 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
rvm:
- 1.9.3
- 2.0.0
- 2.1.10
- 2.2.5
- 2.3.4
- 2.4.1
addons:
code_climate:
repo_token: 3004a7854020df72de40cef4c1f7fc1992ca1969b5420f548df7f4ab6a9b881b
6 changes: 5 additions & 1 deletion Gemfile
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
source 'https://rubygems.org'

gem 'bencode'
group :test do
gem "json", Gem::Version.new(RUBY_VERSION) < Gem::Version.new("2") ? "< 2" : ">= 2"
end

gemspec
10 changes: 0 additions & 10 deletions Gemfile.lock

This file was deleted.

21 changes: 21 additions & 0 deletions LICENSE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2016

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
23 changes: 14 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
# Torckapi — torrent tracker querying API

[![Gem Version](https://img.shields.io/gem/v/torckapi.svg)](https://rubygems.org/gems/torckapi)
[![Dependencies Status](https://img.shields.io/gemnasium/krupenik/torckapi.svg)](https://gemnasium.com/krupenik/torckapi)
[![Code Climate](https://img.shields.io/codeclimate/github/krupenik/torckapi.svg)](https://codeclimate.com/github/krupenik/torckapi)
[![Dependency Status](https://gemnasium.com/badges/github.com/1v/torckapi.svg)](https://gemnasium.com/github.com/1v/torckapi)
[![Build Status](https://travis-ci.org/1v/torckapi.svg?branch=master)](https://travis-ci.org/1v/torckapi)
[![Code Climate](https://codeclimate.com/github/1v/torckapi/badges/gpa.svg)](https://codeclimate.com/github/1v/torckapi)
[![Test Coverage](https://codeclimate.com/github/1v/torckapi/badges/coverage.svg)](https://codeclimate.com/github/1v/torckapi/coverage)

## Description

Expand All @@ -26,14 +28,17 @@ tracker.announce("0123456789ABCDEF0123456789ABCDEF01234567")
# @seeders=1,
# @peers=[["127.0.0.1", 54078], ["127.0.0.2", 43666]]>

tracker.scrape(["0123456789ABCDEF0123456789ABCDEF01234567", "123456789ABCDEF0123456789ABCDEF012345678"])
tracker.scrape(["0123456789ABCDEF0123456789ABCDEF01234567",
"123456789ABCDEF0123456789ABCDEF012345678"])
# => #<Torckapi::Response::Scrape:0x007fa1bc0fe320
# @data={"0123456789ABCDEF0123456789ABCDEF01234567"=>{:seeders=>3, :completed=>0, :leechers=>13},
# "123456789ABCDEF0123456789ABCDEF012345678"=>{:seeders=>4, :completed=>10, :leechers=>8}}>
# @data={"0123456789ABCDEF0123456789ABCDEF01234567"=>{:seeders=>3,
# :completed=>0,
# :leechers=>13},
# "123456789ABCDEF0123456789ABCDEF012345678"=>{:seeders=>4,
# :completed=>10,
# :leechers=>8}}>
```

## TODO

Add tests.

Document everything.
* Add tests.
* Document everything.
11 changes: 11 additions & 0 deletions Rakefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#!/usr/bin/env rake
require 'rake'
require "bundler/gem_tasks"

require 'rspec/core'
require 'rspec/core/rake_task'
RSpec::Core::RakeTask.new(:spec) do |spec|
spec.pattern = FileList['spec/**/*_spec.rb']
end

task :default => :spec
9 changes: 5 additions & 4 deletions lib/torckapi.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,17 @@ module Torckapi
# @param tracker_url [String] tracker announce url
# @param options [Hash] defaults to \\{timeout: 15, tries: 3}
# @return [Torckapi::Tracker::Base] tracker interface instance
def self.tracker tracker_url, options={}
def self.tracker(tracker_url, options = {})
url = URI.parse tracker_url

case url.scheme
when "http"
Torckapi::Tracker::HTTP.new url, options
Torckapi::Tracker::HTTP.new(url, options)
when "udp"
Torckapi::Tracker::UDP.new url, options
Torckapi::Tracker::UDP.new(url, options)
else
raise InvalidSchemeError, "'#{tracker_url}' cannot be recognized as valid tracker url"
raise Torckapi::Tracker::InvalidSchemeError, \
"'#{tracker_url}' cannot be recognized as valid tracker url"
end
end
end
8 changes: 4 additions & 4 deletions lib/torckapi/response/announce.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class Announce < Base
# @param info_hash [String] 40-char hexadecimal string
# @param data [String] UDP response data (omit action and transaction_id)
# @return [Torckapi::Response::Announce] response
def self.from_udp info_hash, data
def self.from_udp(info_hash, data)
new info_hash, *data[4..11].unpack('L>2'), peers_from_compact(data[12..-1] || '')
end

Expand All @@ -29,21 +29,21 @@ def self.from_udp info_hash, data
# @param data [String] HTTP response data (bencoded)
# @return [Torckapi::Response::Announce] response
# @raise [Torckapi::Tracker::MalformedResponseError]
def self.from_http info_hash, data
def self.from_http(info_hash, data)
bdecoded_data = bdecode_and_check data, 'peers'
new info_hash, *bdecoded_data.values_at('incomplete', 'complete'), peers_from_compact(bdecoded_data['peers'])
end

private

def initialize info_hash, leechers, seeders, peers
def initialize(info_hash, leechers, seeders, peers)
@info_hash = info_hash
@leechers = leechers
@seeders = seeders
@peers = peers
end

def self.peers_from_compact data
def self.peers_from_compact(data)
# ipv4 address + tcp/udp port = 6 bytes
data.unpack('a6' * (data.length / 6)).map { |i| [IPAddr.ntop(i[0..3]), i[4..5].unpack('S>')[0]] }
end
Expand Down
8 changes: 4 additions & 4 deletions lib/torckapi/response/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ module Response
class Base
protected

def self.bdecode_and_check data, key
def self.bdecode_and_check(data, key)
begin
bdecoded_data = BEncode.load(data)
bdecoded_data = BEncode.load(data, :ignore_trailing_junk => true)
rescue BEncode::DecodeError
raise Torckapi::Tracker::MalformedResponseError
raise Torckapi::Tracker::MalformedResponseError, "Can't decode '%s'" % data
end

raise Torckapi::Tracker::MalformedResponseError unless bdecoded_data.is_a? Hash and bdecoded_data.has_key? key
raise Torckapi::Tracker::MalformedResponseError, "bdecoded data: '%s'" % bdecoded_data + " didn't contain key: '%s'" % key unless bdecoded_data.is_a? Hash and bdecoded_data.has_key? key

bdecoded_data
end
Expand Down
4 changes: 2 additions & 2 deletions lib/torckapi/response/error.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class Error < Base
# @param info_hashes [String, Array<String>] a 40-char hexadecimal string or an array of those
# @param data [String] UDP response data (omit action and transaction_id)
# @return [Torckapi::Response::Error] response
def self.from_udp info_hashes, data
def self.from_udp(info_hashes, data)
new(info_hashes, data || "")
end

Expand All @@ -26,7 +26,7 @@ def info_hash

private

def initialize info_hashes, message
def initialize(info_hashes, message)
@info_hashes = [*info_hashes]
@message = message
end
Expand Down
12 changes: 6 additions & 6 deletions lib/torckapi/response/scrape.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class Scrape < Base
# @param data [String] UDP response data (omit action and transaction_id)
# @return [Torckapi::Response::Scrape] response
# @raise [Torckapi::Tracker::MalformedResponseError]
def self.from_udp info_hashes, data
def self.from_udp(info_hashes, data)
raise Torckapi::Tracker::MalformedResponseError if data.length != info_hashes.count * 12
new Hash[info_hashes.zip(data.unpack('a12' * info_hashes.count).map { |i| counts_unpacked(i) })]
end
Expand All @@ -21,26 +21,26 @@ def self.from_udp info_hashes, data
# @param data [String] HTTP response data (bencoded)
# @return [Torckapi::Response::Scrape] response
# @raise [Torckapi::Tracker::MalformedResponseError]
def self.from_http data
def self.from_http(data)
bdecoded_data = bdecode_and_check data, 'files'
new Hash[bdecoded_data['files'].map { |info_hash, counts| [info_hash.unpack('H*').join, counts_translated(counts) ]}]
end

private

def self.counts_unpacked data
def self.counts_unpacked(data)
counts_with_block(data) { |data| data.unpack('L>3').map(&:to_i) }
end

def self.counts_translated data
def self.counts_translated(data)
counts_with_block(data) { |data| data.values_at('complete', 'downloaded', 'incomplete') }
end

def self.counts_with_block data, &block
def self.counts_with_block(data, &block)
Hash[[:seeders, :completed, :leechers].zip(yield data)]
end

def initialize data
def initialize(data)
@data = data
end
end
Expand Down
8 changes: 4 additions & 4 deletions lib/torckapi/tracker/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class Base
# @raise [Torckapi::InvalidInfohashError] when supplied with invalid info_hash
# @raise [Torckapi::Tracker::CommunicationFailedError] when tracker haven't responded at all
# @raise [Torckapi::Tracker::MalformedResponseError] when tracker returned junk
def announce info_hash, peer_id=SecureRandom.random_bytes(20)
def announce(info_hash, peer_id = SecureRandom.random_bytes(20))
raise Torckapi::InvalidInfohashError if info_hash !~ /\A[0-9a-f]{40}\z/i
end

Expand All @@ -22,15 +22,15 @@ def announce info_hash, peer_id=SecureRandom.random_bytes(20)
# @raise [Torckapi::InvalidInfohashError] when supplied with invalid info_hash
# @raise [Torckapi::Tracker::CommunicationFailedError] when tracker haven't responded at all
# @raise [Torckapi::Tracker::MalformedResponseError] when tracker returned junk
def scrape info_hashes=[]
def scrape(info_hashes = [])
raise Torckapi::InvalidInfohashError if info_hashes.any? { |i| i !~ /\A[0-9a-f]{40}\z/i }
end

private

def initialize url, options={}
def initialize(url, options = {})
@url = url
@options = {timeout: 15, tries: 7}.merge(options)
@options = {timeout: 15, tries: 2}.merge(options)
end
end
end
Expand Down
34 changes: 25 additions & 9 deletions lib/torckapi/tracker/http.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ module Tracker

class HTTP < Base
# (see Base#announce)
def announce info_hash
def announce(info_hash)
super
Torckapi::Response::Announce.from_http(info_hash, perform_request(url_for(@url.dup, Announce, info_hash)))
end

# (see Base#scrape)
def scrape info_hashes=[]
def scrape(info_hashes = [])
super
Torckapi::Response::Scrape.from_http(perform_request(url_for(@url.dup, Scrape, info_hashes)))
end
Expand All @@ -21,26 +21,42 @@ def scrape info_hashes=[]

REQUEST_ACTIONS = [Announce = 1, Scrape = 2].freeze

def initialize url, options={}
def initialize(url, options = {})
super
@url.query ||= ""
end

def url_for url, action, data
def url_for(url, action, data)
url.query += info_hash_params [*data]
url.path.gsub!(/announce/, 'scrape') if Scrape == action
url
end

def perform_request url
def perform_request(url)

tries = 0

begin
Net::HTTP.get(url)
rescue Errno::ECONNRESET, Errno::ETIMEDOUT, Timeout::Error
raise CommunicationFailedError
timeout = @options[:timeout]
request_url = if Gem::Version.new(RUBY_VERSION) <= Gem::Version.new('1.9.3')
url.to_s
else
url
end
request = Net::HTTP::Get.new(request_url)
Net::HTTP.start(url.host, url.port, open_timeout: timeout, read_timeout: timeout) do |http|
http.request(request).body
end
rescue Errno::ECONNRESET, Errno::ETIMEDOUT, Timeout::Error, Errno::ECONNREFUSED
if (tries += 1) <= @options[:tries]
retry # backs up to just after the "begin"
else
raise CommunicationFailedError
end
end
end

def info_hash_params info_hashes
def info_hash_params(info_hashes)
info_hashes.map { |i| "info_hash=%s" % URI.encode([i].pack('H*')) }.join('&')
end
end
Expand Down
Loading