distribustream 0.1.0

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