rubytorrent 0.3

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.
@@ -0,0 +1,166 @@
1
+ ## server.rb -- make/receive and handshake all new peer connections.
2
+ ## Copyright 2004 William Morgan.
3
+ ##
4
+ ## This file is part of RubyTorrent. RubyTorrent is free software;
5
+ ## you can redistribute it and/or modify it under the terms of version
6
+ ## 2 of the GNU General Public License as published by the Free
7
+ ## Software Foundation.
8
+ ##
9
+ ## RubyTorrent is distributed in the hope that it will be useful, but
10
+ ## WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12
+ ## General Public License (in the file COPYING) for more details.
13
+
14
+ require 'socket'
15
+ require "rubytorrent/tracker"
16
+ require "rubytorrent/controller"
17
+ require "rubytorrent/peer"
18
+
19
+ module RubyTorrent
20
+
21
+ ## The Server coordinates all Packages available on the machine. It
22
+ ## instantiates one Controller for each Package. It's also responsible
23
+ ## for the creation of all TCP connections---it sets up the TCP
24
+ ## socket, receives incoming connections, validates handshakes, and
25
+ ## hands them off to the appropriate Controller; it also creates
26
+ ## outgoing connections (typically at Controllers' requests) and sends
27
+ ## the handshake.
28
+ class Server
29
+ attr_reader :port, :id, :http_proxy
30
+
31
+ VERSION = 0
32
+ PORT_RANGE=(6881 .. 6889)
33
+
34
+ def initialize(hostname=nil, port=nil, http_proxy=ENV["http_proxy"])
35
+ @http_proxy = http_proxy
36
+ @server = nil
37
+ if port.nil?
38
+ @port = PORT_RANGE.detect do |p|
39
+ begin
40
+ @server = TCPServer.new(hostname, p)
41
+ @port = p
42
+ rescue Errno::EADDRINUSE
43
+ @server = nil
44
+ end
45
+ !@server.nil?
46
+ end
47
+ raise Errno::EADDRINUSE, "ports #{PORT_RANGE}" unless @port
48
+ else
49
+ @server = TCPServer.new(hostname, port)
50
+ @port = port
51
+ end
52
+
53
+ @id = "rubytor" + VERSION.chr + (1 .. 12).map { |x| rand(256).chr }.join
54
+ @controllers = {}
55
+ end
56
+
57
+ def ip; @server.addr[3]; end
58
+
59
+ def add_torrent(mi, package, dlratelim=nil, ulratelim=nil)
60
+ @controllers[mi.info.sha1] = Controller.new(self, package, mi.info.sha1, mi.trackers, dlratelim, ulratelim, @http_proxy)
61
+ @controllers[mi.info.sha1].start
62
+ end
63
+
64
+ def add_connection(name, cont, socket)
65
+ begin
66
+ shake_hands(socket, cont.info_hash)
67
+ peer = PeerConnection.new(name, cont, socket, cont.package)
68
+ cont.add_peer peer
69
+ rescue ProtocolError => e
70
+ socket.close rescue nil
71
+ end
72
+ end
73
+
74
+ def start
75
+ @shutdown = false
76
+ @thread = Thread.new do
77
+ begin
78
+ while !@shutdown; receive; end
79
+ rescue IOError, StandardError
80
+ rt_warning "**** socket receive error, retrying"
81
+ sleep 5
82
+ retry
83
+ end
84
+ end
85
+ self
86
+ end
87
+
88
+ def shutdown
89
+ return if @shutdown
90
+ @shutdown = true
91
+ @server.close rescue nil
92
+
93
+ @thread.join(0.2)
94
+ @controllers.each { |hash, cont| cont.shutdown }
95
+ self
96
+ end
97
+
98
+ def to_s
99
+ "<#{self.class}: port #{port}, peer_id #{@id.inspect}>"
100
+ end
101
+
102
+ private
103
+
104
+ def receive # blocking
105
+ ssocket = @server.accept
106
+ Thread.new do
107
+ socket = ssocket
108
+
109
+ begin
110
+ rt_debug "<= incoming connection from #{socket.peeraddr[2]}:#{socket.peeraddr[1]}"
111
+ hash, peer_id = shake_hands(socket, nil)
112
+ cont = @controllers[hash]
113
+ peer = PeerConnection.new("#{socket.peeraddr[2]}:#{socket.peeraddr[1]}", cont, socket, cont.package)
114
+ cont.add_peer peer
115
+ rescue SystemCallError, ProtocolError => e
116
+ rt_debug "killing incoming connection: #{e}"
117
+ socket.close rescue nil
118
+ end
119
+ end
120
+ end
121
+
122
+ ## if info_hash is nil here, the socket is treated as an incoming
123
+ ## connection---it will wait for the peer's info_hash and respond
124
+ ## with the same if it corresponds to a current download, otherwise
125
+ ## it will raise a ProtocolError.
126
+ ##
127
+ ## if info_hash is not nil, the socket is treated as an outgoing
128
+ ## connection, and it will send the info_hash immediately.
129
+ def shake_hands(sock, info_hash)
130
+ # rt_debug "initiating #{(info_hash.nil? ? 'incoming' : 'outgoing')} handshake..."
131
+ sock.send("\023BitTorrent protocol\0\0\0\0\0\0\0\0", 0);
132
+ sock.send("#{info_hash}#{@id}", 0) unless info_hash.nil?
133
+
134
+ len = sock.recv(1)[0]
135
+ # rt_debug "length #{len.inspect}"
136
+ raise ProtocolError, "invalid handshake length byte #{len.inspect}" unless len == 19
137
+
138
+ name = sock.recv(19)
139
+ # rt_debug "name #{name.inspect}"
140
+ raise ProtocolError, "invalid handshake protocol string #{name.inspect}" unless name == "BitTorrent protocol"
141
+
142
+ reserved = sock.recv(8)
143
+ # rt_debug "reserved: #{reserved.inspect}"
144
+ # ignore for now
145
+
146
+ their_hash = sock.recv(20)
147
+ # rt_debug "their info hash: #{their_hash.inspect}"
148
+
149
+ if info_hash.nil?
150
+ raise ProtocolError, "client requests package we don't have: hash=#{their_hash.inspect}" unless @controllers.has_key? their_hash
151
+ info_hash = their_hash
152
+ sock.send("#{info_hash}#{@id}", 0)
153
+ else
154
+ raise ProtocolError, "mismatched info hashes: us=#{info_hash.inspect}, them=#{their_hash.inspect}" unless info_hash == their_hash
155
+ end
156
+
157
+ peerid = sock.recv(20)
158
+ # rt_debug "peer id: #{peerid.inspect}"
159
+ raise ProtocolError, "connected to self" if peerid == @id
160
+
161
+ # rt_debug "== handshake complete =="
162
+ [info_hash, peerid]
163
+ end
164
+ end
165
+
166
+ end
@@ -0,0 +1,225 @@
1
+ ## tracker.rb -- bittorrent tracker protocol.
2
+ ## Copyright 2004 William Morgan.
3
+ ##
4
+ ## This file is part of RubyTorrent. RubyTorrent is free software;
5
+ ## you can redistribute it and/or modify it under the terms of version
6
+ ## 2 of the GNU General Public License as published by the Free
7
+ ## Software Foundation.
8
+ ##
9
+ ## RubyTorrent is distributed in the hope that it will be useful, but
10
+ ## WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12
+ ## General Public License (in the file COPYING) for more details.
13
+
14
+ require 'open-uri'
15
+ require 'timeout'
16
+ require "rubytorrent"
17
+
18
+ module RubyTorrent
19
+
20
+ module HashAddition
21
+ def +(o)
22
+ ret = self.dup
23
+ o.each { |k, v| ret[k] = v }
24
+ ret
25
+ end
26
+ end
27
+
28
+ ## am i insane or does 'uniq' not use == or === for some insane
29
+ ## reason? wtf is that about?
30
+ module ArrayUniq2
31
+ def uniq2
32
+ ret = []
33
+ each { |x| ret.push x unless ret.member? x }
34
+ ret
35
+ end
36
+ end
37
+
38
+ class TrackerResponsePeer
39
+ attr_writer :tried
40
+
41
+ def initialize(dict=nil)
42
+ @s = TypedStruct.new do |s|
43
+ s.field :peer_id => String, :ip => String, :port => Integer
44
+ s.required :ip, :port
45
+ s.label :peer_id => "peer id"
46
+ end
47
+
48
+ @s.parse(dict) unless dict.nil?
49
+ @connected = false
50
+ @tried = false
51
+ end
52
+
53
+ def tried?; @tried; end
54
+
55
+ def method_missing(meth, *args)
56
+ @s.send(meth, *args)
57
+ end
58
+
59
+ def ==(o); (self.ip == o.ip) && (self.port == o.port); end
60
+
61
+ def to_s
62
+ %{<#{self.class}: ip=#{self.ip}, port=#{self.port}>}
63
+ end
64
+ end
65
+
66
+ class TrackerResponse
67
+ def initialize(dict=nil)
68
+ @s = TypedStruct.new do |s|
69
+ s.field :interval => Integer, :complete => Integer,
70
+ :incomplete => Integer, :peers => TrackerResponsePeer
71
+ s.array :peers
72
+ s.required :peers #:interval, :complete, :incomplete, :peers
73
+ s.coerce :peers => lambda { |x| make_peers x }
74
+ end
75
+
76
+ @s.parse(dict) unless dict.nil?
77
+
78
+ peers.extend ArrayShuffle
79
+ end
80
+
81
+ def method_missing(meth, *args)
82
+ @s.send(meth, *args)
83
+ end
84
+
85
+ private
86
+
87
+ def make_peers(x)
88
+ case x
89
+ when Array
90
+ x.map { |e| TrackerResponsePeer.new e }.extend(ArrayUniq2).uniq2
91
+ when String
92
+ x.unpack("a6" * (x.length / 6)).map do |y|
93
+ TrackerResponsePeer.new({"ip" => (0..3).map { |i| y[i] }.join('.'),
94
+ "port" => (y[4] << 8) + y[5] })
95
+ end.extend(ArrayUniq2).uniq2
96
+ else
97
+ raise "don't know how to make peers array from #{x.class}"
98
+ end
99
+ end
100
+ end
101
+
102
+ class TrackerError < StandardError; end
103
+
104
+ class TrackerConnection
105
+ attr_reader :port, :left, :peer_id, :last_conn_time, :url, :in_force_refresh
106
+ attr_accessor :uploaded, :downloaded, :left, :numwant
107
+
108
+ def initialize(url, info_hash, length, port, peer_id, ip=nil, numwant=50, http_proxy=ENV["http_proxy"])
109
+ @url = url
110
+ @hash = info_hash
111
+ @length = length
112
+ @port = port
113
+ @uploaded = @downloaded = @left = 0
114
+ @ip = ip
115
+ @numwant = numwant
116
+ @peer_id = peer_id
117
+ @http_proxy = http_proxy
118
+ @state = :stopped
119
+ @sent_completed = false
120
+ @last_conn_time = nil
121
+ @tracker_data = nil
122
+ @compact = true
123
+ @in_force_refresh = false
124
+ end
125
+
126
+ def already_completed; @sent_completed = true; end
127
+ def sent_completed?; @sent_completed; end
128
+
129
+ def started
130
+ return unless @state == :stopped
131
+ @state = :started
132
+ @tracker_data = send_tracker "started"
133
+ self
134
+ end
135
+
136
+ def stopped
137
+ return unless @state == :started
138
+ @state = :stopped
139
+ @tracker_data = send_tracker "stopped"
140
+ self
141
+ end
142
+
143
+ def completed
144
+ return if @sent_completed
145
+ @tracker_data = send_tracker "completed"
146
+ @sent_completed = true
147
+ self
148
+ end
149
+
150
+ def refresh
151
+ return unless (Time.now - @last_conn_time) >= (interval || 0)
152
+ @tracker_data = send_tracker nil
153
+ end
154
+
155
+ def force_refresh
156
+ return if @in_force_refresh
157
+ @in_force_refresh = true
158
+ @tracker_data = send_tracker nil
159
+ @in_force_refresh = false
160
+ end
161
+
162
+ [:interval, :seeders, :leechers, :peers].each do |m|
163
+ class_eval %{
164
+ def #{m}
165
+ if @tracker_data then @tracker_data.#{m} else nil end
166
+ end
167
+ }
168
+ end
169
+
170
+ private
171
+
172
+ def send_tracker(event)
173
+ resp = nil
174
+ if @compact
175
+ resp = get_tracker_response({ :event => event, :compact => 1 })
176
+ if resp["failure reason"]
177
+ @compact = false
178
+ end
179
+ end
180
+
181
+ resp = get_tracker_response({ :event => event }) unless resp
182
+ raise TrackerError, "tracker reports error: #{resp['failure reason']}" if resp["failure reason"]
183
+
184
+ TrackerResponse.new(resp)
185
+ end
186
+
187
+ def get_tracker_response(opts)
188
+ target = @url.dup
189
+ opts.extend HashAddition
190
+ opts += {:info_hash => @hash, :peer_id => @peer_id,
191
+ :port => @port, :uploaded => @uploaded, :downloaded => @downloaded,
192
+ :left => @left, :numwant => @numwant, :ip => @ip}
193
+ target.query = opts.map do |k, v|
194
+ unless v.nil?
195
+ ek = URI.escape(k.to_s) # sigh
196
+ ev = URI.escape(v.to_s, /[^a-zA-Z0-9]/)
197
+ "#{ek}=#{ev}"
198
+ end
199
+ end.compact.join "&"
200
+
201
+ rt_debug "connecting to #{target.to_s} ..."
202
+
203
+ ret = nil
204
+ begin
205
+ target.open(:proxy => @http_proxy) do |resp|
206
+ BStream.new(resp).each do |e|
207
+ if ret.nil?
208
+ ret = e
209
+ else
210
+ raise TrackerError, "don't understand tracker response (too many objects)"
211
+ end
212
+ end
213
+ end
214
+ rescue SocketError, EOFError, OpenURI::HTTPError, RubyTorrent::TrackerError, Timeout::Error, SystemCallError, NoMethodError => e
215
+ raise TrackerError, e.message
216
+ end
217
+ @last_conn_time = Time.now
218
+
219
+ raise TrackerError, "empty tracker response" if ret.nil?
220
+ raise TrackerError, "don't understand tracker response (not a dict)" unless ret.kind_of? ::Hash
221
+ ret
222
+ end
223
+ end
224
+
225
+ end
@@ -0,0 +1,132 @@
1
+ ## typedstruct.rb -- type-checking struct, for bencoded objects.
2
+ ## Copyright 2004 William Morgan.
3
+ ##
4
+ ## This file is part of RubyTorrent. RubyTorrent is free software;
5
+ ## you can redistribute it and/or modify it under the terms of version
6
+ ## 2 of the GNU General Public License as published by the Free
7
+ ## Software Foundation.
8
+ ##
9
+ ## RubyTorrent is distributed in the hope that it will be useful, but
10
+ ## WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12
+ ## General Public License (in the file COPYING) for more details.
13
+
14
+ require "rubytorrent/bencoding"
15
+
16
+ module RubyTorrent
17
+
18
+ module ArrayToH
19
+ def to_h
20
+ inject({}) { |h, (k, v)| h[k] = v; h } # found this neat trick on the internet
21
+ end
22
+ end
23
+
24
+ module HashMapHash
25
+ def map_hash
26
+ a = map { |k, v| yield k, v }.extend(ArrayToH).to_h
27
+ end
28
+ end
29
+
30
+ class TypedStructError < StandardError; end
31
+
32
+ ## type-checking struct meant for easy translation from and to
33
+ ## bencoded dicts.
34
+ class TypedStruct
35
+ attr_accessor :dirty
36
+ attr_reader :fields # writer below
37
+
38
+ def initialize
39
+ @required = {}
40
+ @label = {}
41
+ @coerce = {}
42
+ @field = {}
43
+ @array = {}
44
+ @dirty = false
45
+ @values = {}
46
+
47
+ yield self if block_given?
48
+
49
+ @field.each do |f, type|
50
+ @required[f] ||= false
51
+ @label[f] ||= f.to_s
52
+ @array[f] ||= false
53
+ end
54
+ end
55
+
56
+ def method_missing(meth, *args)
57
+ if meth.to_s =~ /^(.*?)=$/
58
+ # p [meth, args]
59
+
60
+ f = $1.intern
61
+ raise ArgumentError, "no such value #{f}" unless @field.has_key? f
62
+
63
+ type = @field[f]
64
+ o = args[0]
65
+ if @array[f]
66
+ raise TypeError, "for #{f}, expecting Array, got #{o.class}" unless o.kind_of? ::Array
67
+ o.each { |e| raise TypeError, "for elements of #{f}, expecting #{type}, got #{e.class}" unless e.kind_of? type }
68
+ @values[f] = o
69
+ @dirty = true
70
+ else
71
+ raise TypeError, "for #{f}, expecting #{type}, got #{o.class}" unless o.kind_of? type
72
+ @values[f] = o
73
+ @dirty = true
74
+ end
75
+ else
76
+ raise ArgumentError, "no such value #{meth}" unless @field.has_key? meth
77
+ # p [meth, @values[meth]]
78
+
79
+ @values[meth]
80
+ end
81
+ end
82
+
83
+ [:required, :array].each do |f|
84
+ class_eval %{
85
+ def #{f}(*args)
86
+ args.each do |x|
87
+ raise %q{unknown field "\#{x}" in #{f} list} unless @field[x]
88
+ @#{f}[x] = true
89
+ end
90
+ end
91
+ }
92
+ end
93
+
94
+ [:field , :label, :coerce].each do |f|
95
+ class_eval %{
96
+ def #{f}(hash)
97
+ hash.each { |k, v| @#{f}[k] = v }
98
+ end
99
+ }
100
+ end
101
+
102
+ ## given a Hash from a bencoded dict, parses it according to the
103
+ ## rules you've set up with field, required, label, etc.
104
+ def parse(dict)
105
+ @required.each do |f, reqd|
106
+ flabel = @label[f]
107
+ raise TypedStructError, "missing required parameter #{flabel} (dict has #{dict.keys.join(', ')})" if reqd && !(dict.member? flabel)
108
+
109
+ if dict.member? flabel
110
+ v = dict[flabel]
111
+ if @coerce.member? f
112
+ v = @coerce[f][v]
113
+ end
114
+ if @array[f]
115
+ raise TypeError, "for #{flabel}, expecting Array, got #{v.class} instead" unless v.kind_of? ::Array
116
+ end
117
+ self.send("#{f}=", v)
118
+ end
119
+ end
120
+
121
+ ## disabled the following line as applications seem to put tons of
122
+ ## weird fields in their .torrent files.
123
+ # dict.each { |k, v| raise TypedStructError, %{unknown field "#{k}"} unless @field.member?(k.to_sym) || @label.values.member?(k) }
124
+ end
125
+
126
+ def to_bencoding
127
+ @required.each { |f, reqd| raise ArgumentError, "missing required parameter #{f}" if reqd && self.send(f).nil? }
128
+ @field.extend(HashMapHash).map_hash { |f, type| [@label[f], self.send(f)] }.to_bencoding
129
+ end
130
+ end
131
+
132
+ end