fargo 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,161 @@
1
+ require 'active_support/callbacks'
2
+ require 'active_support/configurable'
3
+ require 'active_support/core_ext/object/try'
4
+
5
+ module Fargo
6
+ class Client
7
+
8
+ include ActiveSupport::Callbacks
9
+ include ActiveSupport::Configurable
10
+
11
+ define_callbacks :setup
12
+
13
+ include Fargo::Publisher
14
+ include Fargo::Supports::Chat
15
+ include Fargo::Supports::Uploads
16
+ include Fargo::Supports::NickList
17
+ include Fargo::Supports::Searches
18
+ include Fargo::Supports::Downloads
19
+ include Fargo::Supports::Persistence
20
+ include Fargo::Supports::Timeout
21
+ include Fargo::Supports::FileList
22
+
23
+ # Options
24
+ # :hub_port
25
+ # :hub_address
26
+ # :search_port
27
+ # :active_port
28
+ # :nick
29
+ # :password
30
+ # :email
31
+ # :speed
32
+ # :passive
33
+ # :download_slots
34
+ # :download_dir
35
+ # :slots
36
+ configure do |config|
37
+ config.download_dir = '/tmp/fargo/downloads'
38
+ config.version = '0.75'
39
+ # default the address to the address of this machine
40
+ config.address = IPSocket.getaddress(Socket.gethostname)
41
+ config.passive = true
42
+ config.nick = 'fargo'
43
+ end
44
+
45
+ attr_reader :hub, :searcher, :active_server
46
+
47
+ def initialize
48
+ @connection_timeout_threads = {}
49
+ end
50
+
51
+ # Don't do this in initialization so we have time to set all the options
52
+ def setup
53
+ @hub = Fargo::Connection::Hub.new self
54
+ @hub.config.port = config.hub_port if config.hub_port
55
+ @hub.config.address = config.hub_address if config.hub_address
56
+
57
+ unless config.passive
58
+ # TODO: get this working again
59
+ # Always create a search connection for this.
60
+ # searcher_options = new_options.merge :port => search_port,
61
+ # :connection => Fargo::Connection::Search
62
+ # @searcher = Fargo::Server.new searcher_options
63
+ #
64
+ # # For now, being active means that you can only download things. Always make a
65
+ # # connection which downloads things.
66
+ # active_options = new_options.merge :port => active_port,
67
+ # :connection => Fargo::Connection::Download
68
+ # @active_server = Fargo::Server.new active_options
69
+ end
70
+
71
+ run_callbacks :setup
72
+ end
73
+
74
+ def get_info nick
75
+ hub.write "$GetINFO #{nick} #{config.nick}"
76
+ end
77
+
78
+ def get_ip *nicks
79
+ hub.write "$UserIP #{nicks.flatten.join '$$'}"
80
+ end
81
+
82
+ def connect_with nick
83
+ @connection_timeout_threads[nick] = Thread.start do
84
+ sleep 10
85
+ connection_timeout! nick
86
+ end
87
+
88
+ if config.passive
89
+ hub.write "$RevConnectToMe #{self.config.nick} #{nick}"
90
+ else
91
+ hub.write "$ConnectToMe #{nick} #{config.address}:#{config.active_port}"
92
+ end
93
+ end
94
+
95
+ def connected_with! nick
96
+ return unless @connection_timeout_threads.has_key?(nick)
97
+ @connection_timeout_threads.delete(nick).exit
98
+ end
99
+
100
+ def connect
101
+ setup if hub.nil?
102
+
103
+ # connect all our associated servers
104
+ hub.connect
105
+
106
+ unless config.passive
107
+ searcher.connect
108
+ active_server.connect
109
+ end
110
+
111
+ true
112
+ end
113
+
114
+ def connected?
115
+ hub.try :connected?
116
+ end
117
+
118
+ def disconnect
119
+ return if hub.nil?
120
+
121
+ Fargo.logger.info "Disconnecting from hub."
122
+ hub.disconnect
123
+ unless config.passive
124
+ searcher.disconnect
125
+ active_server.disconnect
126
+ end
127
+ end
128
+
129
+ def search_hub query
130
+ raise ConnectionError.new('Not connected Yet!') if hub.nil?
131
+
132
+ if config.passive
133
+ location = "Hub:#{config.nick}"
134
+ else
135
+ location = "#{config.address}:#{config.search_port}"
136
+ end
137
+
138
+ hub.write "$Search #{location} #{query.to_s}"
139
+ end
140
+
141
+ # see hub/parser#@@search for what's passed in
142
+ # searches this client's files based on those options and returns an array
143
+ # of SearchResult(s)
144
+ def search_files options
145
+ # TODO: implement me
146
+ []
147
+ end
148
+
149
+ def description
150
+ "<++ V:#{config.version},M:#{config.passive ? 'P' : 'A'},H:1/0/0,S:#{open_slots},Dt:1.2.0/W>"
151
+ end
152
+
153
+ private
154
+
155
+ def connection_timeout! nick
156
+ @connection_timeout_threads.delete(nick)
157
+ publish :connection_timeout, :nick => nick
158
+ end
159
+
160
+ end
161
+ end
@@ -0,0 +1,133 @@
1
+ require 'active_support/configurable'
2
+ require 'active_support/callbacks'
3
+
4
+ module Fargo
5
+ class ConnectionError < RuntimeError; end
6
+
7
+ module Connection
8
+ class Base
9
+
10
+ include ActiveSupport::Configurable
11
+ include ActiveSupport::Callbacks
12
+ include Fargo::Publisher
13
+
14
+ attr_accessor :socket
15
+ define_callbacks :listen
16
+
17
+ def initialize client
18
+ @outgoing = Queue.new
19
+ @client = client
20
+ config.quit_on_disconnect = true
21
+ end
22
+
23
+ def connect
24
+ Fargo.logger.info(
25
+ "#{self}: Opening connection with #{config.address}, #{config.port}"
26
+ )
27
+
28
+ open_socket
29
+ listen
30
+
31
+ connection_type = self.class.name.split('::').last.downcase
32
+ @client.publish :"#{connection_type}_connection_opened"
33
+ end
34
+
35
+ def receive
36
+ raise 'Implement me!'
37
+ end
38
+
39
+ def open_socket
40
+ @socket ||= TCPSocket.open config.address, config.port
41
+ rescue Errno::ECONNREFUSED
42
+ raise Fargo::ConnectionError.new "Couldn't open a connection to #{config.address}:#{config.port}"
43
+ end
44
+
45
+ def connected?
46
+ !@socket.nil?
47
+ end
48
+
49
+ def listen
50
+ return unless @threads.nil? || @threads.size == 0
51
+
52
+ run_callbacks :listen do
53
+ @threads = []
54
+
55
+ # Start a thread to read the socket
56
+ @threads << Thread.start { loop { read_data } }
57
+
58
+ # Start a thread to send information from the queue
59
+ @threads << Thread.start { loop { write_data @outgoing.pop } }
60
+
61
+ @threads.each { |t| t.abort_on_exception = true }
62
+ end
63
+ end
64
+
65
+ def disconnect
66
+ Fargo.logger.debug "#{self}: Disconnecting connection"
67
+
68
+ write "$Quit #{@client.config.nick}" if config.quit_on_disconnect
69
+
70
+ if @threads
71
+ @threads.each &:exit
72
+ @threads.clear
73
+ end
74
+
75
+ if @socket
76
+ begin
77
+ @socket.close
78
+ rescue => e
79
+ Fargo.logger.error "Error closing socket: #{e}"
80
+ end
81
+ end
82
+
83
+ @socket = nil
84
+ @outgoing.clear
85
+
86
+ connection_type = self.class.name.split('::').last.downcase
87
+ @client.publish :"#{connection_type}_disconnected"
88
+ end
89
+
90
+ def write string
91
+ string << '|' unless string.end_with?('|')
92
+ @outgoing << string # append this to the queue of things to be written
93
+ true
94
+ end
95
+
96
+ private
97
+
98
+ def read_data
99
+ if @socket.closed?
100
+ Fargo.logger.debug 'When reading data, socket was already closed!'
101
+ disconnect
102
+ else
103
+ begin
104
+ data = @socket.gets '|'
105
+ raise ConnectionError.new("Received nil data!") if data.nil?
106
+ rescue => e
107
+ Fargo.logger.warn "#{self}: Error reading data, disconnecting: #{e}"
108
+ disconnect
109
+ end
110
+
111
+ Fargo.logger.debug "#{self} Received: #{data.inspect}"
112
+ receive data.chomp('|')
113
+ end
114
+ end
115
+
116
+ def write_data data
117
+ if @socket.closed?
118
+ Fargo.logger.debug "When writing data, socket was already closed!"
119
+ disconnect
120
+ else
121
+ begin
122
+ Fargo.logger.debug "#{self} Sending: #{data.inspect}"
123
+ @socket << data
124
+ rescue
125
+ @client.publish :write_error
126
+ disconnect
127
+ end
128
+ end
129
+ end
130
+
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,302 @@
1
+ require 'zlib'
2
+
3
+ module Fargo
4
+ module Connection
5
+ class Download < Base
6
+
7
+ include Fargo::Utils
8
+ include Fargo::Parser
9
+
10
+ set_callback :listen, :before, :pre_listen
11
+ set_callback :listen, :after do |connection|
12
+ send_lock if connection.config.first
13
+ end
14
+
15
+ attr_accessor :download
16
+
17
+ def pre_listen
18
+ Fargo.logger.debug "Initiating connection on: #{config.address}:#{config.port}"
19
+
20
+ config.quit_on_disconnect = false
21
+ @lock, @pk = generate_lock
22
+ @handshake_step = 0
23
+
24
+ @buffer_size = (2 << 12).freeze
25
+ end
26
+
27
+ def send_lock
28
+ write "$MyNick #{@client.config.nick}|$Lock #{@lock} Pk=#{@pk}"
29
+ end
30
+
31
+ def read_data
32
+ # only download if we're at the correct time
33
+ return super if @handshake_step != 6
34
+
35
+ @exit_time = 20 # reset our timeout time
36
+
37
+ data = @socket.readpartial @buffer_size
38
+
39
+ if @zlib
40
+ @zs = Zlib::Inflate.new if @zs.nil?
41
+ data = @zs.inflate data
42
+ end
43
+
44
+ @file << data
45
+ @recvd += data.length
46
+
47
+ if @recvd == @length
48
+ download_finished!
49
+ elsif @recvd > @length
50
+ error "#{self} #{@recvd} > #{@length}!!!"
51
+ download_finished!
52
+ else
53
+ publish :download_progress, :percent => @recvd.to_f / @length,
54
+ :file => download_path,
55
+ :nick => @other_nick,
56
+ :download => @download,
57
+ :size => @length,
58
+ :compressed => @zlib
59
+ end
60
+ rescue IOError => e
61
+ error "#{self}: IOError, disconnecting #{e}"
62
+ end
63
+
64
+ def receive data
65
+ message = parse_message data
66
+
67
+ case message[:type]
68
+ when :mynick
69
+ if @handshake_step == 0
70
+ @handshake_step = 1
71
+ @other_nick = message[:nick]
72
+
73
+ @client.connected_with! @other_nick
74
+ @client.lock_connection_with! @other_nick, self
75
+ @download = @client.lock_next_download! @other_nick, self
76
+
77
+ if @download.try(:file).nil?
78
+ error "Nothing to download from:#{@other_nick}!"
79
+ end
80
+ else
81
+ error 'Premature disconnect when mynick received'
82
+ end
83
+
84
+ when :lock
85
+ if @handshake_step == 1
86
+ @remote_lock = message[:lock]
87
+ @handshake_step = 2
88
+ send_lock unless config.first
89
+ out = ''
90
+ out << '$Supports TTHF ADCGet ZLIG|'
91
+ out << "$Direction Download #{@my_num = rand(10000)}|"
92
+ out << "$Key #{generate_key @remote_lock}|"
93
+ write out
94
+ else
95
+ error 'Premature disconnect when lock received'
96
+ end
97
+
98
+ when :supports
99
+ if @handshake_step == 2
100
+ @client_extensions = message[:extensions]
101
+ @handshake_step = 3
102
+ else
103
+ error 'Premature disconnect when supports received'
104
+ end
105
+
106
+ when :direction
107
+ if @handshake_step == 3 && message[:direction] == 'upload'
108
+ @client_num = message[:number]
109
+ @handshake_step = 4
110
+ else
111
+ error 'Premature disconnect when direction received'
112
+ end
113
+
114
+ when :key
115
+ if @handshake_step == 4 && generate_key(@lock) == message[:key]
116
+
117
+ FileUtils.mkdir_p File.dirname(download_path), :mode => 0755
118
+
119
+ begin_download!
120
+
121
+ else
122
+ error 'Premature disconnect when key received'
123
+ end
124
+
125
+ when :file_length, :adcsnd
126
+ if @handshake_step == 5
127
+ @recvd = 0
128
+ @handshake_step = 6
129
+
130
+ @zlib = message[:zlib]
131
+ @length = message[:size]
132
+
133
+ write "$Send" unless @client_extensions.include? 'ADCGet'
134
+
135
+ publish :download_started, :file => download_path,
136
+ :download => @download,
137
+ :nick => @other_nick
138
+ else
139
+ error "Premature disconnect when #{message[:type]} received"
140
+ end
141
+
142
+ when :noslots
143
+ if @download
144
+ Fargo.logger.debug "#{self}: No Slots for #{self[:download]}"
145
+
146
+ download_failed! 'No Slots'
147
+ end
148
+
149
+ when :error
150
+ error "#{self}: Error! #{message[:message]}"
151
+
152
+ # This wasn't handled by us, proxy it on up to the client
153
+ else
154
+ @client.publish message[:type], message
155
+
156
+ end
157
+ end
158
+
159
+ def begin_download!
160
+ @file = File.new download_path, File::CREAT | File::WRONLY
161
+
162
+ @file.seek @download.offset
163
+ @file.sync = true
164
+ @socket.sync = true
165
+ @handshake_step = 5
166
+
167
+ if @download.file_list?
168
+ if @client_extensions.include? 'XmlBZList'
169
+ @download.file = 'files.xml.bz2'
170
+ elsif @client_extensions.include? 'BZList'
171
+ @download.file = 'MyList.bz2'
172
+ else
173
+ @download.file = 'MyList.DcLst' # TODO: support this?
174
+ end
175
+ end
176
+
177
+ if @client_extensions.include? 'ADCGet'
178
+ download_query = @download.file
179
+ if @download.tth && @client_extensions.include?('TTHF')
180
+ download_query = @download.tth.gsub ':', '/'
181
+ end
182
+
183
+ zlig = ''
184
+ if @client_extensions.include? 'ZLIG'
185
+ zlig = 'ZL1'
186
+ Fargo.logger.debug "Enabling zlib compression on: #{@download.file}"
187
+ end
188
+
189
+ write "$ADCGET file #{download_query} #{@download.offset} #{@download.size} #{zlig}"
190
+ else
191
+ write "$Get #{@download.file}$#{@download.offset + 1}"
192
+ end
193
+
194
+ # This is the thread for the timeout of a connection. The @exit_time
195
+ # variable is reset to 20 after every bit of information is received.
196
+ @exit_time = 20
197
+ @exit_thread = Thread.start {
198
+ while @exit_time > 0
199
+ sleep 1
200
+ @exit_time -= 1
201
+ Fargo.logger.debug "#{self} time out in #{@exit_time} seconds"
202
+ end
203
+
204
+ download_failed! 'Download timeout!'
205
+ }
206
+
207
+ Fargo.logger.debug "#{self}: Beginning download of #{@download}"
208
+ end
209
+
210
+ def download_failed! msg, opts = {}
211
+ Fargo.logger.debug "#{self}: #{msg} #{@download}"
212
+
213
+ # cache because publishing must be at end of method and we're about to
214
+ # clear these
215
+ path, download = download_path, @download
216
+
217
+ reset_download
218
+
219
+ publish :download_failed, opts.merge(:nick => @other_nick,
220
+ :download => download,
221
+ :file => path,
222
+ :last_error => msg)
223
+
224
+ @exit_thread = nil
225
+ end
226
+
227
+ def download_finished!
228
+ Fargo.logger.debug "#{self}: Finished download of #{@download}"
229
+
230
+ # cache because publishing must be at end of method and we're about to
231
+ # clear these
232
+ path, download = download_path, @download
233
+
234
+ reset_download
235
+
236
+ publish :download_finished, :file => path, :download => download,
237
+ :nick => @other_nick
238
+ end
239
+
240
+ def disconnect
241
+ Fargo.logger.debug "#{self} Disconnecting from: #{@other_nick}"
242
+
243
+ super
244
+
245
+ if @download
246
+ download_failed! @last_error, :recvd => @recvd, :length => @length
247
+ end
248
+
249
+ reset_download
250
+ end
251
+
252
+ private
253
+ def reset_download
254
+ @file.close unless @file.nil? || @file.closed?
255
+ if @file_path && File.exists?(@file_path) && File.size(@file_path) == 0
256
+ File.delete(@file_path)
257
+ end
258
+
259
+ if @socket
260
+ @socket.sync = false
261
+ @socket.flush
262
+ end
263
+
264
+ # If this was called from exit thread, don't kill it
265
+ if @exit_thread != Thread.current
266
+ @exit_thread.exit if @exit_thread && @exit_thread.alive?
267
+ @exit_thread = nil
268
+ end
269
+
270
+ # clear out these variables
271
+ @zs = @file_path = @zlib = @download = @length = @recvd = nil
272
+
273
+ # Go back to the get step
274
+ @handshake_step = 5
275
+ end
276
+
277
+ def download_path
278
+ return nil if @download.try(:file).nil?
279
+
280
+ @file_path ||= begin
281
+ prefix = @client.config.download_dir
282
+ filename = File.basename @download.file.gsub("\\", '/')
283
+ path = File.join(prefix, @other_nick, filename)
284
+
285
+ i = 0
286
+ while File.exists?(path)
287
+ i += 1
288
+ path = File.join(prefix, @other_nick, "#{i}-#{filename}")
289
+ end
290
+
291
+ path
292
+ end
293
+ end
294
+
295
+ def error message
296
+ Fargo.logger.warn @last_error = message
297
+ disconnect
298
+ end
299
+
300
+ end
301
+ end
302
+ end
@@ -0,0 +1,120 @@
1
+ module Fargo
2
+ module Connection
3
+ class Hub < Base
4
+
5
+ include Fargo::Utils
6
+ include Fargo::Parser
7
+
8
+ attr_reader :hubname
9
+
10
+ configure do |config|
11
+ config.address = '127.0.0.1'
12
+ config.port = 7314
13
+ end
14
+
15
+ # See <http://www.teamfair.info/DC-Protocol.htm> for specifics on
16
+ # the DC protocol
17
+ def receive data
18
+ message = parse_message data
19
+
20
+ case message[:type]
21
+ when :lock
22
+ @validated = false
23
+ write "$Key #{generate_key message[:lock]}"
24
+ when :hubname
25
+ @hubname = message[:name]
26
+ write "$ValidateNick #{@client.config.nick}" unless @validated
27
+ when :getpass
28
+ write "$MyPass #{@client.password}"
29
+ when :badpass, :hubfull
30
+ Fargo.logger.warn "Disconnecting because of: #{message.inspect}"
31
+ disconnect
32
+ when :hello
33
+ if message[:who] == @client.config.nick
34
+ Fargo.logger.info "Connected to DC Hub #{@hubname} (#{config.address}:#{config.port})"
35
+ @validated = true
36
+
37
+ write '$Version 1,0091'
38
+ write '$GetNickList'
39
+ write "$MyINFO $ALL #{@client.config.nick} " +
40
+ "#{@client.description}$ $#{@client.config.speed || 'DSL'}" +
41
+ "#{@status || 1.chr}$#{@client.config.email}" +
42
+ "$#{@client.share_size}$"
43
+ end
44
+
45
+ when :connect_to_me
46
+ if !@client.nicks.include?(message[:nick])
47
+ Fargo.logger.info "Invalid connect_to_me request from: #{message[:nick]}"
48
+ return
49
+ end
50
+
51
+ @client_connections ||= []
52
+
53
+ connection = Fargo::Connection::Download.new @client
54
+ connection.config.address = message[:address]
55
+ connection.config.port = message[:port]
56
+ # we're going to initiate the download
57
+ connection.config.first = true
58
+
59
+ # proxy all messages from them back to the client and delete the
60
+ # connection if necessary
61
+ connection.subscribe { |*args|
62
+ @client.publish *args
63
+ @client_connections.delete connection unless connection.connected?
64
+ }
65
+
66
+ # establish the connection. This will also listen for data to be
67
+ # read/written
68
+ connection.connect
69
+
70
+ # keep track of who we're downloading from
71
+ @client_connections << connection
72
+
73
+ when :search
74
+ # Make sure we received a valid search request
75
+ if message[:searcher].nil? || !@client.nicks.include?(message[:searcher])
76
+ Fargo.logger.info "Invalid search request: #{message.inspect}"
77
+ return
78
+ end
79
+
80
+ # Let the client handle the results
81
+ @results = @client.search_files message
82
+
83
+ # Send all the results to the peer. Take care of active/passive
84
+ # connections
85
+ @results.each { |r|
86
+ if message[:address]
87
+ r.active_send @client.config.nick, message[:ip], message[:port]
88
+ else
89
+ write "$SR #{@client.config.nick} #{r}"
90
+ end
91
+ }
92
+
93
+ when :revconnect
94
+ # TODO: Don't send RevConnectToMe when we're passive and
95
+ # receiving is passive
96
+ if @client.config.passive
97
+ write "$RevConnectToMe #{@client.config.nick} #{message[:who]}"
98
+ else
99
+ write "$ConnectToMe #{@client.config.nick} #{@client.config.address}:#{@client.config.extport}"
100
+ end
101
+
102
+ # proxy this message on up the stack if we don't handle it
103
+ else
104
+ @client.publish message[:type], message
105
+
106
+ end
107
+ end
108
+
109
+ def disconnect
110
+ if @client_connections
111
+ @client_connections.each &:disconnect
112
+ @client_connections.clear
113
+ end
114
+
115
+ super
116
+ end
117
+
118
+ end # Hub
119
+ end # Connection
120
+ end # Fargo