distribustream 0.1.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.
@@ -0,0 +1,68 @@
1
+ #--
2
+ # Copyright (C) 2006-07 ClickCaster, Inc. (info@clickcaster.com)
3
+ # All rights reserved. See COPYING for permissions.
4
+ #
5
+ # This source file is distributed as part of the
6
+ # DistribuStream file transfer system.
7
+ #
8
+ # See http://distribustream.rubyforge.org/
9
+ #++
10
+
11
+ require File.dirname(__FILE__) + '/protocol'
12
+
13
+ describe PDTP::Protocol, 'obj_matches_type?' do
14
+ it "identifies :url objects" do
15
+ PDTP::Protocol.obj_matches_type?("http://bla.com/test3.mp3",:url).should == true
16
+ PDTP::Protocol.obj_matches_type?(4,:url).should == false
17
+ end
18
+
19
+ it "identifies :range objects" do
20
+ PDTP::Protocol.obj_matches_type?(0..4,:range).should == true
21
+ PDTP::Protocol.obj_matches_type?(4,:range).should == false
22
+ PDTP::Protocol.obj_matches_type?( {"min"=>0,"max"=>4} , :range ).should == true
23
+ end
24
+
25
+ it "identifies :ip objects" do
26
+ PDTP::Protocol.obj_matches_type?("127.0.0.1", :ip).should == true
27
+ PDTP::Protocol.obj_matches_type?("127.0.0.1.1", :ip).should == false
28
+ end
29
+
30
+ it "identifies :int objects" do
31
+ PDTP::Protocol.obj_matches_type?(4,:int).should == true
32
+ PDTP::Protocol.obj_matches_type?("hi",:int).should == false
33
+ end
34
+
35
+ it "identifies :bool objects" do
36
+ PDTP::Protocol.obj_matches_type?(true, :bool).should == true
37
+ PDTP::Protocol.obj_matches_type?(0,:bool).should == false
38
+ end
39
+
40
+ it "identifies :string objects" do
41
+ PDTP::Protocol.obj_matches_type?("hi", :string).should == true
42
+ PDTP::Protocol.obj_matches_type?(6, :string).should == false
43
+ end
44
+
45
+ end
46
+
47
+ describe PDTP::Protocol, 'validate_message' do
48
+ it "supports optional parameters" do
49
+ msg1 = ["request", {"url"=>"pdtp://bla.com/test.txt", "range"=>0..4 }]
50
+ msg2 = ["request", {"url"=>"pdtp://bla.com/test.txt" }]
51
+ msg3 = ["request", {"range"=> "hi", "url"=>"pdtp://bla.com/test.txt" }]
52
+
53
+ proc { PDTP::Protocol.validate_message(msg1)}.should_not raise_error
54
+ proc { PDTP::Protocol.validate_message(msg2)}.should_not raise_error
55
+ proc { PDTP::Protocol.validate_message(msg3)}.should raise_error
56
+ end
57
+
58
+ it "validates required parameters" do
59
+ msg1 = ["ask_info"]
60
+ msg2 = ["ask_info", {"url"=>"pdtp://bla.com/test.txt"}]
61
+ msg3 = ["ask_info", {"url"=>42 }]
62
+
63
+ proc { PDTP::Protocol.validate_message(msg1)}.should raise_error
64
+ proc { PDTP::Protocol.validate_message(msg2)}.should_not raise_error
65
+ proc { PDTP::Protocol.validate_message(msg3)}.should raise_error
66
+ end
67
+
68
+ end
@@ -0,0 +1,368 @@
1
+ #--
2
+ # Copyright (C) 2006-07 ClickCaster, Inc. (info@clickcaster.com)
3
+ # All rights reserved. See COPYING for permissions.
4
+ #
5
+ # This source file is distributed as part of the
6
+ # DistribuStream file transfer system.
7
+ #
8
+ # See http://distribustream.rubyforge.org/
9
+ #++
10
+
11
+ require File.dirname(__FILE__) + '/common/protocol'
12
+ require File.dirname(__FILE__) + '/common/common_init'
13
+ require File.dirname(__FILE__) + '/server/file_service'
14
+ require File.dirname(__FILE__) + '/server/client_info'
15
+ require File.dirname(__FILE__) + '/server/transfer'
16
+
17
+ require 'thread'
18
+ require 'erb'
19
+
20
+ module PDTP
21
+ # PDTP server implementation
22
+ class Server
23
+ attr_reader :connections
24
+ attr_accessor :file_service
25
+ def initialize
26
+ @connections = Array.new
27
+ @stats_mutex=Mutex.new
28
+ @used_client_ids=Hash.new #keeps a list of client ids in use, they must be unique
29
+ @updated_clients=Hash.new #a set of clients that have been modified and need transfers spawned
30
+ end
31
+
32
+ #called by pdtp_protocol when a connection is created
33
+ def connection_created(connection)
34
+ @stats_mutex.synchronize do
35
+ @@log.info "Client connected: #{connection.get_peer_info.inspect}"
36
+ connection.user_data = ClientInfo.new
37
+ @connections << connection
38
+ end
39
+ end
40
+
41
+ #called by pdtp_protocol when a connection is destroyed
42
+ def connection_destroyed(connection)
43
+ @stats_mutex.synchronize do
44
+ @@log.info "Client connection closed: #{connection.get_peer_info.inspect}"
45
+ @connections.delete connection
46
+ end
47
+ end
48
+
49
+ # returns the ClientInfo object associated with this connection
50
+ def client_info(connection)
51
+ connection.user_data ||= ClientInfo.new
52
+ end
53
+
54
+ # called when a transfer either finishes, successfully or not
55
+ def transfer_completed(transfer,connection,chunk_hash,send_response=true)
56
+ # did the transfer complete successfully?
57
+ local_hash=@file_service.get_chunk_hash(transfer.url,transfer.chunkid)
58
+
59
+ c1=client_info(transfer.taker)
60
+ c2=client_info(transfer.giver)
61
+
62
+ if connection==transfer.taker
63
+ success= (chunk_hash==local_hash)
64
+
65
+ if success
66
+ #the taker now has the file, so he can provide it
67
+ client_info(transfer.taker).chunk_info.provide(transfer.url,transfer.chunkid..transfer.chunkid)
68
+ c1.trust.success(c2.trust)
69
+ else
70
+ #transfer failed, the client still wants the chunk
71
+ client_info(transfer.taker).chunk_info.request(transfer.url,transfer.chunkid..transfer.chunkid)
72
+ c1.trust.failure(c2.trust)
73
+ end
74
+
75
+ transfer.taker.send_message(:hash_verify,
76
+ :url => transfer.url,
77
+ :range => transfer.byte_range,
78
+ :hash_ok => success
79
+ ) if send_response
80
+ end
81
+
82
+ #outstr="#{@ids[transfer.giver]}->#{@ids[transfer.taker]} transfer completed: #{transfer}"
83
+ #outstr=outstr+" t->g=#{c1.trust.weight(c2.trust)} g->t=#{c2.trust.weight(c1.trust)}"
84
+ #outstr=outstr+"sent_by: "+ ( connection==transfer.taker ? "taker" : "giver" )
85
+ #outstr=outstr+" success=#{success} "
86
+ #@@log.debug(outstr)
87
+
88
+ #remove this transfer from whoever sent it
89
+ client_info(connection).transfers.delete(transfer.transfer_id)
90
+ @updated_clients[connection]=true #flag this client for transfer creation
91
+ end
92
+
93
+ #Creates a new transfer between two peers
94
+ #returns true on success, or false if the specified transfer is already in progress
95
+ def begin_transfer(taker, giver, url, chunkid)
96
+ byte_range = @file_service.get_info(url).chunk_range(chunkid)
97
+ t = Transfer.new(taker, giver, url, chunkid, byte_range)
98
+
99
+ #make sure this transfer doesnt already exist
100
+ t1 = client_info(taker).transfers[t.transfer_id]
101
+ t2 = client_info(giver).transfers[t.transfer_id]
102
+ return false unless t1.nil? and t2.nil?
103
+
104
+ client_info(taker).chunk_info.transfer(url, chunkid..chunkid)
105
+ client_info(taker).transfers[t.transfer_id] = t
106
+ client_info(giver).transfers[t.transfer_id] = t
107
+
108
+ #send transfer message to the connector
109
+ addr, port = t.acceptor.get_peer_info
110
+
111
+ t.connector.send_message(:transfer,
112
+ :host => addr,
113
+ :port => t.acceptor.user_data.listen_port,
114
+ :method => t.connector == t.taker ? "get" : "put",
115
+ :url => url,
116
+ :range => byte_range,
117
+ :peer_id => client_info(t.acceptor).client_id
118
+ )
119
+ true
120
+ end
121
+
122
+ #this function removes all stalled transfers from the list
123
+ #and spawns new transfers as appropriate
124
+ #it must be called periodically by EventMachine
125
+ def clear_all_stalled_transfers
126
+ @connections.each { |connection| clear_stalled_transfers_for_client connection }
127
+ spawn_all_transfers
128
+ end
129
+
130
+ #removes all stalled transfers that this client is a part of
131
+ def clear_stalled_transfers_for_client(client_connection)
132
+ client_info(client_connection).get_stalled_transfers.each do |transfer|
133
+ transfer_completed transfer, client_connection, nil, false
134
+ end
135
+ end
136
+
137
+ #spawns uploads and downloads for this client.
138
+ #should be called every time there is a change that would affect
139
+ #what this client has or wants
140
+ def spawn_transfers_for_client(client_connection)
141
+ info = client_info client_connection
142
+
143
+ while info.wants_download? do
144
+ break if spawn_download_for_client(client_connection) == false
145
+ end
146
+
147
+ while info.wants_upload? do
148
+ break if spawn_upload_for_client(client_connection) == false
149
+ end
150
+ end
151
+
152
+ #creates a single download for the specified client
153
+ #returns true on success, false on failure
154
+ def spawn_download_for_client(client_connection)
155
+ feasible_peers=[]
156
+
157
+ c1info=client_info(client_connection)
158
+ begin
159
+ url,chunkid=c1info.chunk_info.high_priority_chunk
160
+ rescue
161
+ return false
162
+ end
163
+
164
+ @connections.each do |c2|
165
+ next if client_connection==c2
166
+ next if client_info(c2).wants_upload? == false
167
+ if client_info(c2).chunk_info.provided?(url,chunkid)
168
+ feasible_peers << c2
169
+ break if feasible_peers.size > 5
170
+ end
171
+ end
172
+
173
+ # we now have a list of clients that have the requested chunk.
174
+ # pick one and start the transfer
175
+ if feasible_peers.size > 0
176
+ #FIXME base this on the trust model
177
+ giver=feasible_peers[rand(feasible_peers.size)]
178
+ return begin_transfer(client_connection,giver,url,chunkid)
179
+ #FIXME should we try again if begin_transfer fails?
180
+ end
181
+
182
+ false
183
+ end
184
+
185
+ #creates a single upload for the specified client
186
+ #returns true on success, false on failure
187
+ def spawn_upload_for_client(client_connection)
188
+ c1info=client_info(client_connection)
189
+
190
+ @connections.each do |c2|
191
+ next if client_connection==c2
192
+ next if client_info(c2).wants_download? == false
193
+
194
+ begin
195
+ url,chunkid=client_info(c2).chunk_info.high_priority_chunk
196
+ rescue
197
+ next
198
+ end
199
+
200
+ if c1info.chunk_info.provided?(url,chunkid)
201
+ return begin_transfer(c2,client_connection,url,chunkid)
202
+ end
203
+ end
204
+
205
+ false
206
+ end
207
+
208
+ #called by pdtp_protocol for each message that comes in from the wire
209
+ def dispatch_message(command, message, connection)
210
+ @stats_mutex.synchronize do
211
+ dispatch_message_needslock command, message, connection
212
+ end
213
+ end
214
+
215
+ #creates new transfers for all clients that have been updated
216
+ def spawn_all_transfers
217
+ while @updated_clients.size > 0 do
218
+ tmp=@updated_clients
219
+ @updated_clients=Hash.new
220
+ tmp.each do |client,true_key|
221
+ spawn_transfers_for_client(client)
222
+ end
223
+ end
224
+ end
225
+
226
+ #handles the request, provide, unrequest, unprovide messages
227
+ def handle_requestprovide(connection,message)
228
+ type=message["type"]
229
+ url=message["url"]
230
+ info=@file_service.get_info(url) rescue nil
231
+ raise ProtocolWarn.new("Requested URL: '#{url}' not found") if info.nil?
232
+
233
+ exclude_partial= (type=="provide") #only exclude partial chunks from provides
234
+ range=info.chunk_range_from_byte_range(message["range"],exclude_partial)
235
+
236
+ #call request, provide, unrequest, or unprovide
237
+ client_info(connection).chunk_info.send( type.to_sym, url, range)
238
+ @updated_clients[connection]=true #add to the list of client that need new transfers
239
+ end
240
+
241
+ #handles all incoming messages from clients
242
+ def dispatch_message_needslock(command, message, connection)
243
+ # store the command in the message hash
244
+ message["type"] = command
245
+
246
+ #require the client to be logged in with a client id
247
+ if command != "client_info" and client_info(connection).client_id.nil?
248
+ raise ProtocolError.new("You need to send a 'client_info' message first")
249
+ end
250
+
251
+ case command
252
+ when "client_info"
253
+ cid = message["client_id"]
254
+ #make sure this id isnt in use
255
+ if @used_client_ids[cid]
256
+ raise ProtocolError.new("Your client id: #{cid} is already in use.")
257
+ end
258
+
259
+ @used_client_ids[cid] = true
260
+ client_info(connection).listen_port = message["listen_port"]
261
+ client_info(connection).client_id = cid
262
+ when "ask_info"
263
+ info = file_service.get_info(message["url"])
264
+ response = { :url => message["url"] }
265
+ unless info.nil?
266
+ response[:size] = info.file_size
267
+ response[:chunk_size] = info.base_chunk_size
268
+ response[:streaming] = info.streaming
269
+ end
270
+ connection.send_message :tell_info, response
271
+ when "request", "provide", "unrequest", "unprovide"
272
+ handle_requestprovide connection, message
273
+ when "ask_verify"
274
+ #check if the specified transfer is a real one
275
+ my_id = client_info(connection).client_id
276
+ transfer_id=Transfer.gen_transfer_id(my_id,message["peer_id"],message["url"],message["range"])
277
+ ok = !!client_info(connection).transfers[transfer_id]
278
+ client_info(connection).transfers[transfer_id].verification_asked=true if ok
279
+ @@log.debug "AskVerify not ok: id=#{transfer_id}" unless ok
280
+ connection.send_message(:tell_verify,
281
+ :url => message["url"],
282
+ :peer_id => message["peer_id"],
283
+ :range => message["range"],
284
+ :peer => message["peer"],
285
+ :is_authorized=>ok
286
+ )
287
+ when "completed"
288
+ my_id = client_info(connection).client_id
289
+ transfer_id= Transfer::gen_transfer_id(
290
+ my_id,message["peer_id"],
291
+ message["url"],
292
+ message["range"]
293
+ )
294
+ transfer=client_info(connection).transfers[transfer_id]
295
+ @@log.debug("Completed: id=#{transfer_id} ok=#{transfer != nil}" )
296
+ if transfer
297
+ transfer_completed(transfer,connection,message["hash"])
298
+ else
299
+ raise ProtocolWarn.new("You sent me a transfer completed message for unknown transfer: #{transfer_id}")
300
+ end
301
+ when 'protocol_error', 'protocol_warn' #ignore
302
+ else raise ProtocolError.new("Unhandled message type: #{command}")
303
+ end
304
+
305
+ spawn_all_transfers
306
+ end
307
+
308
+ #returns a string representing the specified connection
309
+ def connection_name(c)
310
+ #host,port=c.get_peer_info
311
+ #return "#{get_id(c)}: #{host}:#{port}"
312
+ client_info(c).client_id
313
+ end
314
+
315
+ def generate_html_stats
316
+ @stats_mutex.synchronize { generate_html_stats_needslock }
317
+ end
318
+
319
+ #builds an html page with information about the server's internal workings
320
+ def generate_html_stats_needslock
321
+ s = ERB.new <<EOF
322
+ <html><head><title>DistribuStream Statistics</title></head>
323
+ <body>
324
+ <h1>DistribuStream Statistics</h1>
325
+ Time=<%= Time.new %><br> Connected Clients=<%= @connections.size %>
326
+ <center><table border=1>
327
+ <tr><th>Client</th><th>Transfers</th><th>Files</th></tr>
328
+ <% @connections.each do |c| %>
329
+ <tr><td>
330
+ <% host, port = c.get_peer_info %>
331
+ <%= connection_name(c) %><br><%= host %>:<%= port %>
332
+ </td>
333
+ <td>
334
+ <%
335
+ client_info(c).transfers.each do |key,t|
336
+ if c==t.giver
337
+ type="UP: "
338
+ peer=t.taker
339
+ else
340
+ type="DOWN: "
341
+ peer=t.giver
342
+ end
343
+ %>
344
+ <%= type %> id=<%= t.transfer_id %><br>
345
+ <%
346
+ end
347
+ %>
348
+ </td>
349
+ <td>
350
+ <%
351
+ client_info(c).chunk_info.get_file_stats.each do |fs|
352
+ %>
353
+ <%= fs.url %> size=<%= fs.file_chunks %> req=<%= fs.chunks_requested %>
354
+ prov=<%= fs.chunks_provided %> transf=<%= fs.chunks_transferring %><br>
355
+ <%
356
+ end
357
+ %>
358
+ </td></tr>
359
+ <%
360
+ end
361
+ %>
362
+ </table>
363
+ </body></html>
364
+ EOF
365
+ s.result binding
366
+ end
367
+ end
368
+ end
@@ -0,0 +1,140 @@
1
+ #--
2
+ # Copyright (C) 2006-07 ClickCaster, Inc. (info@clickcaster.com)
3
+ # All rights reserved. See COPYING for permissions.
4
+ #
5
+ # This source file is distributed as part of the
6
+ # DistribuStream file transfer system.
7
+ #
8
+ # See http://distribustream.rubyforge.org/
9
+ #++
10
+
11
+ require File.dirname(__FILE__)+'/trust.rb'
12
+
13
+ module PDTP
14
+ #stores information about a single connected client
15
+ class ClientInfo
16
+ attr_accessor :chunk_info, :trust
17
+ attr_accessor :listen_port, :client_id
18
+ attr_accessor :transfers
19
+
20
+ def initialize
21
+ @chunk_info=ChunkInfo.new
22
+ @listen_port=6000 #default
23
+ @trust=Trust.new
24
+ @transfers=Hash.new
25
+ end
26
+
27
+ # returns true if this client wants the server to spawn a transfer for it
28
+ def wants_download?
29
+ transfer_state_allowed=5
30
+ total_allowed=10
31
+ transferring=0
32
+ @transfers.each do |key, t|
33
+ transferring=transferring+1 if t.verification_asked
34
+ return false if transferring >= transfer_state_allowed
35
+ end
36
+
37
+ @transfers.size < total_allowed
38
+ end
39
+
40
+ #this could have a different definition, but it works fine to use wants_download?
41
+ alias_method :wants_upload?, :wants_download?
42
+
43
+ #returns a list of all the stalled transfers this client is a part of
44
+ def get_stalled_transfers
45
+ stalled=[]
46
+ timeout=20.0
47
+ now=Time.now
48
+ @transfers.each do |key,t|
49
+ #only delete if we are the acceptor to prevent race conditions
50
+ next if t.acceptor.user_data != self
51
+ if now-t.creation_time > timeout and not t.verification_asked
52
+ stalled << t
53
+ end
54
+ end
55
+ stalled
56
+ end
57
+ end
58
+
59
+ #stores information about the chunks requested or provided by a client
60
+ class ChunkInfo
61
+ def initialize
62
+ @files={}
63
+ end
64
+
65
+ #each chunk can either be provided, requested, transfer, or none
66
+ def provide(filename,range); set(filename,range,:provided) ; end
67
+ def unprovide(filename,range); set(filename,range, :none); end
68
+ def request(filename,range); set(filename,range, :requested); end
69
+ def unrequest(filename,range); set(filename,range, :none); end
70
+ def transfer(filename,range); set(filename,range, :transfer); end
71
+
72
+ def provided?(filename,chunk); get(filename,chunk) == :provided; end
73
+ def requested?(filename,chunk); get(filename,chunk) == :requested; end
74
+
75
+ #returns a high priority requested chunk
76
+ def high_priority_chunk
77
+ #right now return any chunk
78
+ @files.each do |name,file|
79
+ file.each_index do |i|
80
+ return [name,i] if file[i]==:requested
81
+ end
82
+ end
83
+
84
+ nil
85
+ end
86
+
87
+ #calls a block for each chunk of the specified type
88
+ def each_chunk_of_type(type)
89
+ @files.each do |name,file|
90
+ file.each_index do |i|
91
+ yield(name,i) if file[i]==type
92
+ end
93
+ end
94
+ end
95
+
96
+ class FileStats
97
+ attr_accessor :file_chunks, :chunks_requested,:url
98
+ attr_accessor :chunks_provided, :chunks_transferring
99
+
100
+ def initialize
101
+ @url=""
102
+ @file_chunks=0
103
+ @chunks_requested=0
104
+ @chunks_provided=0
105
+ @chunks_transferring=0
106
+ end
107
+ end
108
+
109
+ #returns an array of FileStats objects for debug output
110
+ def get_file_stats
111
+ stats=[]
112
+ @files.each do |name,file|
113
+ fs=FileStats.new
114
+ fs.file_chunks=file.size
115
+ fs.url=name
116
+ file.each do |chunk|
117
+ fs.chunks_requested+=1 if chunk==:requested
118
+ fs.chunks_provided+=1 if chunk==:provided
119
+ fs.chunks_transferring+=1 if chunk==:transfer
120
+ end
121
+ stats << fs
122
+ end
123
+
124
+ stats
125
+ end
126
+
127
+ #########
128
+ protected
129
+ #########
130
+
131
+ def get(filename,chunk)
132
+ @files[filename][chunk] rescue :neither
133
+ end
134
+
135
+ def set(filename,range,state)
136
+ chunks=@files[filename]||=Array.new
137
+ range.each { |i| chunks[i]=state }
138
+ end
139
+ end
140
+ end