rubytorrent 0.3
Sign up to get free protection for your applications and to get access to all the features.
- data/COPYING +340 -0
- data/README +21 -0
- data/ReleaseNotes.txt +25 -0
- data/doc/api.txt +289 -0
- data/doc/design.txt +59 -0
- data/dump-metainfo.rb +55 -0
- data/dump-peers.rb +45 -0
- data/lib/rubytorrent.rb +94 -0
- data/lib/rubytorrent/bencoding.rb +174 -0
- data/lib/rubytorrent/controller.rb +610 -0
- data/lib/rubytorrent/message.rb +128 -0
- data/lib/rubytorrent/metainfo.rb +214 -0
- data/lib/rubytorrent/package.rb +595 -0
- data/lib/rubytorrent/peer.rb +536 -0
- data/lib/rubytorrent/server.rb +166 -0
- data/lib/rubytorrent/tracker.rb +225 -0
- data/lib/rubytorrent/typedstruct.rb +132 -0
- data/lib/rubytorrent/util.rb +186 -0
- data/make-metainfo.rb +211 -0
- data/rtpeer-ncurses.rb +340 -0
- data/rtpeer.rb +125 -0
- metadata +78 -0
@@ -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
|