rubytorrent 0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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