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,128 @@
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
+ module PDTP
12
+ # Handle a file buffer, which may be written to and read from randomly
13
+ class FileBuffer
14
+ def initialize(io = nil)
15
+ @io = io
16
+ @written = 0
17
+ @entries = []
18
+ end
19
+
20
+ # Write data starting at start_pos. Overwrites any existing data in that block
21
+ def write(start_pos, data)
22
+ return if data.size == 0
23
+
24
+ # create and entry and attempt to combine it with old entries
25
+ new_entry = Entry.new(start_pos,data)
26
+
27
+ intersections = true
28
+ while intersections
29
+ intersections = false
30
+ @entries.each do |e|
31
+ if intersects?(new_entry, e)
32
+ new_entry = combine(e, new_entry)
33
+ @entries.delete(e)
34
+ intersections = true
35
+ end
36
+ end
37
+ end
38
+
39
+ # Add entry to the local store
40
+ @entries << new_entry
41
+
42
+ # Write contiguous blocks we receive to our internal IO cursor
43
+ if @io and start_pos == @written
44
+ data_begin = @written - new_entry.start_pos
45
+ bytes_written = @io.write(new_entry.data[data_begin..new_entry.data.length])
46
+ @written += bytes_written
47
+ else
48
+ bytes_written = data.size
49
+ end
50
+
51
+ bytes_written
52
+ end
53
+
54
+ # Returns a string containing the desired data.
55
+ # Returns nil if the data is not all there
56
+ def read(range)
57
+ return nil if range.first>range.last
58
+ current_byte=range.first
59
+
60
+ buffer = ''
61
+
62
+ while current_byte <= range.last do
63
+ # find an entry that contains this byte
64
+
65
+ found=false
66
+ @entries.each do |e|
67
+ if e.range.include?(current_byte)
68
+ internal_start=current_byte-e.start_pos #start position inside this entry's data
69
+ internal_end=(range.last<e.end_pos ? range.last : e.end_pos) - e.start_pos
70
+ buffer << e.data[internal_start..internal_end]
71
+ current_byte+=internal_end-internal_start+1
72
+ found=true
73
+ break if current_byte>range.last
74
+ end
75
+ end
76
+ return nil if found==false
77
+ end
78
+
79
+ buffer
80
+ end
81
+
82
+ # Returns true if two entries intersect
83
+ def intersects?(entry1, entry2)
84
+ first, last = entry1.start_pos <= entry2.start_pos ? [entry1, entry2] : [entry2, entry1]
85
+ first.end_pos + 1 >= last.start_pos
86
+ end
87
+
88
+ # Takes two Entries
89
+ # Returns nil if there is no intersection
90
+ # Returns the union if they intersect
91
+ def combine(old_entry, new_entry)
92
+ start = old_entry.start_pos < new_entry.start_pos ? old_entry.start_pos: new_entry.start_pos
93
+
94
+ stringio = StringIO.new
95
+ stringio.seek(old_entry.start_pos - start)
96
+ stringio.write(old_entry.data)
97
+ stringio.seek(new_entry.start_pos - start)
98
+ stringio.write(new_entry.data)
99
+ return Entry.new(start, stringio.string)
100
+ end
101
+
102
+ # Return number of bytes currently in the buffer
103
+ def bytes_stored
104
+ bytes=0
105
+ @entries.each do |e|
106
+ bytes=bytes+e.data.size
107
+ end
108
+ return bytes
109
+ end
110
+
111
+ # Container for an entry in the buffer
112
+ class Entry
113
+ def initialize(start_pos,data)
114
+ @start_pos,@data=start_pos,data
115
+ end
116
+
117
+ attr_accessor :start_pos, :data
118
+
119
+ def end_pos
120
+ @start_pos + data.length - 1
121
+ end
122
+
123
+ def range
124
+ Range.new(@start_pos,end_pos)
125
+ end
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,154 @@
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__) + '/file_buffer'
12
+
13
+ describe PDTP::FileBuffer do
14
+ before(:each) do
15
+ @b = PDTP::FileBuffer.new
16
+ end
17
+
18
+ it "returns nil if read when empty" do
19
+ @b.bytes_stored.should == 0
20
+ @b.read(0..1).should == nil
21
+ end
22
+ end
23
+
24
+ describe PDTP::FileBuffer, "with one entry" do
25
+ before(:each) do
26
+ @b = PDTP::FileBuffer.new
27
+ @b.write 0, 'hello'
28
+ end
29
+
30
+ it "calculates bytes stored correctly" do
31
+ @b.bytes_stored.should == 5
32
+ end
33
+
34
+ it "reads stored data correctly" do
35
+ @b.read(0..4).should == "hello"
36
+ @b.read(1..1).should == "e"
37
+ @b.read(-1..2).should == nil
38
+ @b.read(0..5).should == nil
39
+ end
40
+ end
41
+
42
+ describe PDTP::FileBuffer, "with two overlapping entries" do
43
+ before(:each) do
44
+ @b = PDTP::FileBuffer.new
45
+ @b.write(3,"hello")
46
+ @b.write(7,"World")
47
+ end
48
+
49
+ it "calculates bytes stored correctly" do
50
+ @b.bytes_stored.should == 9
51
+ end
52
+
53
+ it "reads stored data correctly" do
54
+ @b.read(3..12).should == nil
55
+ @b.read(3..11).should == "hellWorld"
56
+ @b.read(3..1).should == nil
57
+ @b.read(2..4).should == nil
58
+ end
59
+
60
+ end
61
+
62
+ describe PDTP::FileBuffer, "with three overlapping entries" do
63
+ before(:each) do
64
+ @b = PDTP::FileBuffer.new
65
+ @b.write(3,"hello")
66
+ @b.write(7,"World")
67
+ @b.write(2,"123456789ABCDEF")
68
+ end
69
+
70
+ it "calculates bytes stored correctly" do
71
+ @b.bytes_stored.should == 15
72
+ end
73
+
74
+ it "reads stored data correctly" do
75
+ @b.read(2..16).should == "123456789ABCDEF"
76
+ @b.read(2..17).should == nil
77
+ end
78
+ end
79
+
80
+ describe PDTP::FileBuffer, "with two tangential entries" do
81
+ before(:each) do
82
+ @b = PDTP::FileBuffer.new
83
+ @b.write(3,"hello")
84
+ @b.write(8,"World")
85
+ end
86
+
87
+ it "calculates bytes stored correctly" do
88
+ @b.bytes_stored.should == 10
89
+ end
90
+
91
+ it "reads stored data correctly" do
92
+ @b.read(3..12).should == "helloWorld"
93
+ end
94
+ end
95
+
96
+ describe PDTP::FileBuffer, "with a chain of overlapping entries" do
97
+ before(:each) do
98
+ @b = PDTP::FileBuffer.new
99
+ @b.write(3,"a123")
100
+ @b.write(4,"b4")
101
+ @b.write(0,"012c")
102
+
103
+ #___a123
104
+ #___ab43
105
+ #012cb43
106
+
107
+ end
108
+
109
+ it "calculates bytes stored correctly" do
110
+ @b.bytes_stored.should == 7
111
+ end
112
+
113
+ it "reads stored data correctly" do
114
+ @b.read(0..6).should == "012cb43"
115
+ @b.read(3..6).should == "cb43"
116
+ end
117
+ end
118
+
119
+ describe PDTP::FileBuffer, "with an associated IO object" do
120
+ before(:each) do
121
+ @io = mock(:io)
122
+ @b = PDTP::FileBuffer.new @io
123
+ end
124
+
125
+ it "writes received data to the IO object" do
126
+ @io.should_receive(:write).once.with('foo').and_return(3)
127
+ @b.write(0, "foo")
128
+ end
129
+
130
+ it "writes successively received data to the IO object" do
131
+ @io.should_receive(:write).once.with('foo').and_return(3)
132
+ @b.write(0, "foo")
133
+
134
+ @io.should_receive(:write).once.with('bar').and_return(3)
135
+ @b.write(3, "bar")
136
+
137
+ @io.should_receive(:write).once.with('baz').and_return(3)
138
+ @b.write(6, "baz")
139
+ end
140
+
141
+ it "reassembles single-byte out-of-order data and writes it to the IO object" do
142
+ @io.should_receive(:write).once.with('bar').and_return(3)
143
+ @b.write(1, 'a')
144
+ @b.write(2, 'r')
145
+ @b.write(0, 'b')
146
+ end
147
+
148
+ it "reassembles multibyte out-of-order data and writes it to the IO object" do
149
+ @io.should_receive(:write).once.with('foobar').and_return(6)
150
+ @b.write(2, 'ob')
151
+ @b.write(4, 'ar')
152
+ @b.write(0, 'fo')
153
+ end
154
+ end
@@ -0,0 +1,60 @@
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 'uri'
12
+ require 'pathname'
13
+ require File.dirname(__FILE__) + '/../common/file_service.rb'
14
+ require File.dirname(__FILE__) + '/file_buffer.rb'
15
+
16
+ module PDTP
17
+ class Client < Mongrel::HttpHandler
18
+ # The client specific file utilities. Most importantly, handling
19
+ # the data buffer.
20
+ class FileInfo < PDTP::FileInfo
21
+ def initialize(filename)
22
+ @buffer = FileBuffer.new open(filename, 'w')
23
+ @lock = Mutex.new
24
+ end
25
+
26
+ # Write data into buffer starting at start_pos
27
+ def write(start_pos,data)
28
+ @lock.synchronize { @buffer.write start_pos, data }
29
+ end
30
+
31
+ # Read a range of data out of buffer. Takes a ruby Range object
32
+ def read(range)
33
+ begin
34
+ @lock.synchronize { @buffer.read range }
35
+ rescue nil
36
+ end
37
+ end
38
+
39
+ # Return the number of bytes currently stored
40
+ def bytes_downloaded
41
+ @lock.synchronize { @buffer.bytes_stored }
42
+ end
43
+ end
44
+
45
+ # Container class for file data
46
+ class FileService < PDTP::FileService
47
+ def initialize
48
+ @files = {}
49
+ end
50
+
51
+ def get_info(url)
52
+ @files[url] rescue nil
53
+ end
54
+
55
+ def set_info(url, info)
56
+ @files[url] = info
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,66 @@
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
+ module PDTP
12
+ class Client < Mongrel::HttpHandler
13
+ # Implementation of a ruby test client for the pdtp protocol
14
+ class Protocol < PDTP::Protocol
15
+ def initialize *args
16
+ super
17
+ end
18
+
19
+ # Called after a connection to the server has been established
20
+ def connection_completed
21
+ begin
22
+ listen_port = @@config[:listen_port]
23
+
24
+ #create the client
25
+ client = PDTP::Client.new
26
+ PDTP::Protocol.listener = client
27
+ client.server_connection = self
28
+ client.generate_client_id listen_port
29
+ client.file_service = PDTP::Client::FileService.new
30
+
31
+ # Start a mongrel server on the specified port. If it isnt available, keep trying higher ports
32
+ begin
33
+ mongrel_server = Mongrel::HttpServer.new('0.0.0.0', listen_port)
34
+ rescue Exception => e
35
+ listen_port += 1
36
+ retry
37
+ end
38
+
39
+ @@log.info "listening on port #{listen_port}"
40
+ mongrel_server.register '/', client
41
+ mongrel_server.run
42
+
43
+ # Tell the server about ourself
44
+ send_message :client_info, :listen_port => listen_port, :client_id => client.my_id
45
+
46
+ # Ask the server for some information on the file we want
47
+ send_message :ask_info, :url => @@config[:request_url]
48
+
49
+ # Request the file
50
+ send_message :request, :url => @@config[:request_url]
51
+
52
+ @@log.info "This client is requesting"
53
+ rescue Exception=>e
54
+ puts "Exception in connection_completed: #{e}"
55
+ puts e.backtrace.join("\n")
56
+ exit
57
+ end
58
+ end
59
+
60
+ def unbind
61
+ super
62
+ puts 'Disconnected from PDTP server.'
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,229 @@
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__) + '/file_service'
12
+ require "thread"
13
+ require "net/http"
14
+ require "uri"
15
+ require "digest/sha2"
16
+
17
+ module PDTP
18
+ class Client < Mongrel::HttpHandler
19
+ module Transfer
20
+ # Generic HTTP Exception to be used on error
21
+ class HTTPException < Exception
22
+ attr_accessor :code
23
+ def initialize(code,message)
24
+ super(message)
25
+ @code = code
26
+ end
27
+ end
28
+
29
+ # The base information and methods needed by client transfers
30
+ class Base
31
+ attr_reader :peer, :peer_id, :url, :byte_range
32
+ attr_reader :server_connection, :file_service
33
+ attr_reader :method, :client, :hash
34
+
35
+ # Returns true if a server message matches this transfer
36
+ def matches_message?(message)
37
+ @peer == message["peer"] and
38
+ @url == message["url"] and
39
+ @byte_range == message["range"] and
40
+ @peer_id == message["peer_id"]
41
+ end
42
+
43
+ # Takes an HTTP range and returns a ruby Range object
44
+ def parse_http_range(string)
45
+ begin
46
+ raise "Can't parse range string: #{string}" unless string =~ /bytes=([0-9]+)-([0-9]+)/
47
+ (($1).to_i)..(($2).to_i)
48
+ rescue nil
49
+ end
50
+ end
51
+
52
+ # Notify the server of transfer completion.
53
+ # Hash field is used to denote success or failure
54
+ def send_completed_message(hash)
55
+ @server_connection.send_message(:completed,
56
+ :url => @url,
57
+ :peer => @peer,
58
+ :range => @byte_range,
59
+ :peer_id => @peer_id,
60
+ :hash => hash
61
+ )
62
+ end
63
+
64
+ def send_ask_verify_message
65
+ @server_connection.send_message(:ask_verify,
66
+ :url => @url,
67
+ :peer => @peer,
68
+ :range => @byte_range,
69
+ :peer_id => @peer_id
70
+ )
71
+ end
72
+ end
73
+
74
+ # Implements the listening end (the server) of a peer to peer http connection
75
+ class Listener < Base
76
+ attr :request, :response
77
+
78
+ # Called with the request and response parameters given by Mongrel
79
+ def initialize(request,response,server_connection,file_service,client)
80
+ @request,@response=request,response
81
+ @server_connection,@file_service=server_connection,file_service
82
+ @authorized=false
83
+ @client=client
84
+ end
85
+
86
+ # Send an HTTP error response to requester
87
+ def write_http_exception(e)
88
+ if e.class == HTTPException
89
+ @response.start(e.code) do |head,out|
90
+ out.write(e.to_s + "\n\n" + e.backtrace.join("\n") )
91
+ end
92
+ else
93
+ @@log.info("MONGREL SERVER ERROR: exception:" + e.to_s+"\n\n"+e.backtrace.join("\n"))
94
+ @response.start(500) do |head,out|
95
+ out.write("Server error, unknown exception:"+e.to_s + "\n\n" + e.backtrace.join("\n") )
96
+ end
97
+ end
98
+ end
99
+
100
+ # Parse the HTTP header and ask for verification of transfer
101
+ # Thread is stopped after asking for verification and will
102
+ # be restarted when verification arrives
103
+ def handle_header
104
+ @thread=Thread.current
105
+
106
+ @@log.debug "params=#{@request.params.inspect}"
107
+
108
+ @method=@request.params["REQUEST_METHOD"].downcase
109
+ @peer=@request.params["REMOTE_ADDR"]
110
+
111
+ #here we construct the GUID for this file
112
+ path=@request.params["REQUEST_PATH"]
113
+ vhost=@request.params["HTTP_HOST"]
114
+ @url="http://"+vhost+path
115
+
116
+ @byte_range=parse_http_range(request.params["HTTP_RANGE"])
117
+ @peer_id=@request.params["HTTP_X_PDTP_PEER_ID"]
118
+
119
+ #sanity checking
120
+ raise HTTPException.new(400, "Missing X-PDTP-Peer-Id header") if @peer_id.nil?
121
+ raise HTTPException.new(400, "Missing Host header") if vhost.nil?
122
+ raise HTTPException.new(400, "Missing Range header") if @byte_range.nil?
123
+
124
+ send_ask_verify_message
125
+ Thread.stop
126
+ after_verification
127
+ end
128
+
129
+ # Called after receiving verification message from the server
130
+ # Set the authorized status and restart the thread
131
+ # This throws us into after_verification
132
+ def tell_verify(authorized)
133
+ @authorized=authorized
134
+ @thread.run
135
+ end
136
+
137
+ # Perform the transfer if verification was successful
138
+ def after_verification
139
+ #check if the server authorized us
140
+ unless @authorized
141
+ raise HTTPException.new(403,"Forbidden: the server did not authorize this transfer")
142
+ end
143
+
144
+ info = @file_service.get_info(@url)
145
+ if @method == "put"
146
+ #we are the taker
147
+ @@log.debug("Body Downloaded: url=#{@url} range=#{@byte_range} peer=#{@peer}")
148
+
149
+ @file_service.set_info(FileInfo.new) if info.nil?
150
+ info.write(@byte_range.first, @request.body.read)
151
+ @hash=Digest::SHA256.hexdigest(res.body) rescue nil
152
+
153
+ # Stock HTTP OK response
154
+ @response.start(200) do |head,out|
155
+ end
156
+ elsif @method=="get"
157
+ #we are the giver
158
+ raise HTTPException.new(404,"File not found: #{@url}") if info.nil?
159
+ data=info.read(@byte_range)
160
+ raise HTTPException.new(416,"Invalid range: #{@byte_range.inspect}") if data.nil?
161
+
162
+ #Request was GET, so now we need to send the data
163
+ @response.start(206) do |head, out|
164
+ head['Content-Type'] = 'application/octet-stream'
165
+ head['Content-Range'] = "bytes #{@byte_range.first}-#{@byte_range.last}/*"
166
+ #FIXME must include a DATE header according to http
167
+
168
+ out.write(data)
169
+ end
170
+ else
171
+ raise HTTPException.new(405,"Invalid method: #{@method}")
172
+ end
173
+
174
+ end
175
+
176
+ end
177
+
178
+ # Implements http transfer between two peers from the connector's (client) perspective
179
+ class Connector < Base
180
+ def initialize(message,server_connection,file_service,client)
181
+ @server_connection,@file_service=server_connection,file_service
182
+ @peer,@port=message["host"],message["port"]
183
+ @method = message["method"]
184
+ @url=message["url"]
185
+ @byte_range=message["range"]
186
+ @peer_id=message["peer_id"]
187
+ @client=client
188
+ end
189
+
190
+ # Perform the transfer
191
+ def run
192
+ hash=nil
193
+
194
+ info=@file_service.get_info(@url)
195
+
196
+ #compute the vhost and path
197
+ #FIXME work with ports
198
+ uri=URI.split(@url)
199
+ path=uri[5]
200
+ vhost=uri[2]
201
+
202
+ if @method == "get"
203
+ req = Net::HTTP::Get.new(path)
204
+ body = nil
205
+ elsif @method == "put"
206
+ req = Net::HTTP::Put.new(path)
207
+ body = info.read(@byte_range)
208
+ else
209
+ raise HTTPException.new(405,"Invalid method: #{@method}")
210
+ end
211
+
212
+ req.add_field("Range", "bytes=#{@byte_range.begin}-#{@byte_range.end}")
213
+ req.add_field("Host",vhost)
214
+ req.add_field("X-PDTP-Peer-Id",@client.my_id)
215
+ res = Net::HTTP.start(@peer,@port) {|http| http.request(req,body) }
216
+
217
+ if res.code == '206' and @method == 'get'
218
+ #we are the taker
219
+ @@log.debug("Body Downloaded: url=#{@url} range=#{@byte_range} peer=#{@peer}:#{@port}")
220
+ info.write(@byte_range.first,res.body)
221
+ @hash=Digest::SHA256.hexdigest(res.body) rescue nil
222
+ else
223
+ raise "HTTP RESPONSE: code=#{res.code} body=#{res.body}"
224
+ end
225
+ end
226
+ end
227
+ end
228
+ end
229
+ end