fargo 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,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