summaryrefslogtreecommitdiff
path: root/lib/ftplib.rb
blob: e79868b0ef5140949c472c60a15146fa68f47c48 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
## ftplib.rb

# Author: Shugo Maeda <shugo@po.aianet.ne.jp>
# Version: $Revision: 1.7 $

## Code:

require "socket"
require "monitor"

class FTPError < Exception; end
class FTPReplyError < FTPError; end
class FTPTempError < FTPError; end
class FTPPermError < FTPError; end
class FTPProtoError < FTPError; end

class FTP
  
  RCS_ID = %q$Id: ftplib.rb,v 1.7 1998/04/13 12:34:24 shugo Exp shugo $ 
  
  include MonitorMixin
  
  FTP_PORT = 21
  CRLF = "\r\n"
  
  attr_accessor :passive, :return_code, :debug_mode
  attr_reader :welcome, :lastresp
  
  def FTP.open(host, user = nil, passwd = nil, acct = nil)
    new(host, user, passwd, acct)
  end
    
  def initialize(host = nil, user = nil, passwd = nil, acct = nil)
    super
    @passive = false
    @return_code = "\n"
    @debug_mode = false
    if host
      connect(host)
      if user
	login(user, passwd, acct)
      end
    end
  end
  
  def open_socket(host, port)
    if defined? SOCKSsocket and ENV["SOCKS_SERVER"]
      @passive = true
      return SOCKSsocket.open(host, port)
    else
      return TCPsocket.open(host, port)
    end
  end
  private :open_socket
   
  def connect(host, port = FTP_PORT)
    if @debug_mode
      print "connect: ", host, ", ", port, "\n"
    end
    synchronize do
      @sock = open_socket(host, port)
      voidresp
    end
  end
  
  def sanitize(s)
    if s =~ /^PASS /i
      return s[0, 5] + "*" * (s.length - 5)
    else
      return s
    end
  end
  private :sanitize
  
  def putline(line)
    if @debug_mode
      print "put: ", sanitize(line), "\n"
    end
    line = line + CRLF
    @sock.write(line)
  end
  private :putline
   
  def getline
    line = @sock.readline # if get EOF, raise EOFError
    if line[-2, 2] == CRLF
      line = line[0 .. -3]
    elsif line[-1] == ?\r or
	line[-1] == ?\n
      line = line[0 .. -2]
    end
    if @debug_mode
      print "get: ", sanitize(line), "\n"
    end
    return line
  end
  private :getline
  
  def getmultiline
    line = getline
    buff = line
    if line[3] == ?-
      code = line[0, 3]
      begin
	line = getline
	buff << "\n" << line
      end until line[0, 3] == code and line[3] != ?-
    end
    return buff << "\n"
  end
  private :getmultiline
  
  def getresp
    resp = getmultiline
    @lastresp = resp[0, 3]
    c = resp[0]
    case c
    when ?1, ?2, ?3
      return resp
    when ?4
      raise FTPTempError, resp
    when ?5
      raise FTPPermError, resp
    else
      raise FTPProtoError, resp
    end
  end
  private :getresp
  
  def voidresp
    resp = getresp
    if resp[0] != ?2
      raise FTPReplyError, resp
    end
  end
  private :voidresp
  
  def sendcmd(cmd)
    synchronize do
      putline(cmd)
      return getresp
    end
  end
   
  def voidcmd(cmd)
    synchronize do
      putline(cmd)
      voidresp
    end
  end
   
  def sendport(host, port)
    hbytes = host.split(".")
    pbytes = [port / 256, port % 256]
    bytes = hbytes + pbytes
    cmd = "PORT " + bytes.join(",")
    voidcmd(cmd)
  end
  private :sendport
   
  def makeport
    sock = TCPserver.open(0)
    port = sock.addr[1]
    host = TCPsocket.getaddress(@sock.addr[2])
    resp = sendport(host, port)
    return sock
  end
  private :makeport
   
  def transfercmd(cmd)
    if @passive
      host, port = parse227(sendcmd("PASV"))
      conn = open_socket(host, port)
      resp = sendcmd(cmd)
      if resp[0] != ?1
	raise FTPReplyError, resp
      end
    else
      sock = makeport
      resp = sendcmd(cmd)
      if resp[0] != ?1
	raise FTPReplyError, resp
      end
      conn = sock.accept
    end
    return conn
  end
  private :transfercmd
   
  def getaddress
    thishost = Socket.gethostname
    if not thishost.index(".")
      thishost = Socket.gethostbyname(thishost)[0]
    end
    if ENV.has_key?("LOGNAME")
      realuser = ENV["LOGNAME"]
    elsif ENV.has_key?("USER")
      realuser = ENV["USER"]
    else
      realuser = "anonymous"
    end
    return realuser + "@" + thishost
  end
  private :getaddress
   
  def login(user = "anonymous", passwd = nil, acct = nil)
    if user == "anonymous" and passwd == nil
      passwd = getaddress
    end
    
    resp = ""
    synchronize do
      resp = sendcmd('USER ' + user)
      if resp[0] == ?3
	resp = sendcmd('PASS ' + passwd)
      end
      if resp[0] == ?3
	resp = sendcmd('ACCT ' + acct)
      end
    end
    if resp[0] != ?2
      raise FTPReplyError, resp
    end
    @welcome = resp
  end
  
  def retrbinary(cmd, blocksize, callback = Proc.new)
    synchronize do
      voidcmd("TYPE I")
      conn = transfercmd(cmd)
      loop do
	data = conn.read(blocksize)
	break if data == nil
	callback.call(data)
      end
      conn.close
      voidresp
    end
  end
   
  def retrlines(cmd, callback = nil)
    if iterator?
      callback = Proc.new
    elsif not callback.is_a?(Proc)
      callback = Proc.new {|line| print line, "\n"}
    end
    synchronize do
      voidcmd("TYPE A")
      conn = transfercmd(cmd)
      loop do
	line = conn.gets
	break if line == nil
	if line[-2, 2] == CRLF
	  line = line[0 .. -3]
	elsif line[-1] == ?\n
	  line = line[0 .. -2]
	end
	callback.call(line)
      end
      conn.close
      voidresp
    end
  end
  
  def storbinary(cmd, file, blocksize, callback = nil)
    if iterator?
      callback = Proc.new
    end
    use_callback = callback.is_a?(Proc)
    synchronize do
      voidcmd("TYPE I")
      conn = transfercmd(cmd)
      loop do
	buf = file.read(blocksize)
	break if buf == nil
	conn.write(buf)
	callback.call(buf) if use_callback
      end
      conn.close
      voidresp
    end
  end
   
  def storlines(cmd, file, callback = nil)
    if iterator?
      callback = Proc.new
    end
    use_callback = callback.is_a?(Proc)
    synchronize do
      voidcmd("TYPE A")
      conn = transfercmd(cmd)
      loop do
	buf = file.gets
	break if buf == nil
	if buf[-2, 2] != CRLF
	  if buf[-1] == ?\r or
	      buf[-1] == ?\n
	    buf = buf[0 .. -2]
	  end
	  buf = buf + CRLF
	end
	conn.write(buf)
	callback.call(buf) if use_callback
      end
      conn.close
      voidresp
    end
  end
  
  def getbinaryfile(remotefile, localfile, blocksize, callback = nil)
    if iterator?
      callback = Proc.new
    end
    use_callback = callback.is_a?(Proc)
    f = open(localfile, "w")
    begin
      f.binmode
      retrbinary("RETR " + remotefile, blocksize) do |data|
	f.write(data)
	callback.call(data) if use_callback
      end
    ensure
      f.close
    end
  end
   
  def gettextfile(remotefile, localfile, callback = nil)
    if iterator?
      callback = Proc.new
    end
    use_callback = callback.is_a?(Proc)
    f = open(localfile, "w")
    begin
      retrlines("RETR " + remotefile) do |line|
	line = line + @return_code
	f.write(line)
	callback.call(line) if use_callback
      end
    ensure
      f.close
    end
  end
   
  def putbinaryfile(localfile, remotefile, blocksize, callback = nil)
    if iterator?
      callback = Proc.new
    end
    use_callback = callback.is_a?(Proc)
    f = open(localfile)
    begin
      f.binmode
      storbinary("STOR " + remotefile, f, blocksize) do |data|
	callback.call(data) if use_callback
      end
    ensure
      f.close
    end
  end
  
  def puttextfile(localfile, remotefile, callback = nil)
    if iterator?
      callback = Proc.new
    end
    use_callback = callback.is_a?(Proc)
    f = open(localfile)
    begin
      storlines("STOR " + remotefile, f) do |line|
	callback.call(line) if use_callback
      end
    ensure
      f.close
    end
  end
   
  def acct(account)
    cmd = "ACCT " + account
    voidcmd(cmd)
  end
  
  def nlst(dir = nil)
    cmd = "NLST"
    if dir
      cmd = cmd + " " + dir
    end
    files = []
    retrlines(cmd) do |line|
      files.push(line)
    end
    return files
  end
  
  def list(*args, &block)
    cmd = "LIST"
    args.each do |arg|
      cmd = cmd + " " + arg
    end
    if block
      retrlines(cmd, &block)
    else
      lines = []
      retrlines(cmd) do |line|
	lines << line
      end
      return lines
    end
  end
  alias ls list
  alias dir list
  
  def rename(fromname, toname)
    resp = sendcmd("RNFR " + fromname)
    if resp[0] != ?3
      raise FTPReplyError, resp
    end
    voidcmd("RNTO " + toname)
  end
  
  def delete(filename)
    resp = sendcmd("DELE " + filename)
    if resp[0, 3] == "250"
      return
    elsif resp[0] == ?5
      raise FTPPermError, resp
    else
      raise FTPReplyError, resp
    end
  end
  
  def chdir(dirname)
    if dirname == ".."
      begin
	voidcmd("CDUP")
	return
      rescue FTPPermError
	if $![0, 3] != "500"
	  raise FTPPermError, $!
	end
      end
    end
    cmd = "CWD " + dirname
    voidcmd(cmd)
  end
   
  def size(filename)
    voidcmd("TYPE I")
    resp = sendcmd("SIZE " + filename)
    if resp[0, 3] != "213" 
      raise FTPReplyError, resp
    end
    return resp[3..-1].strip 
  end

  MDTM_REGEXP = /^(\d\d\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)$/

  def mtime(filename) 
    str = mdtm(filename)
    ary = str.scan(MDTM_REGEXP)[0].collect {|i| i.to_i} 
    return Time.gm(*ary) 
  end

  def mkdir(dirname)
    resp = sendcmd("MKD " + dirname)
    return parse257(resp)
  end
  
  def rmdir(dirname)
    voidcmd("RMD " + dirname)
  end
  
  def pwd
    resp = sendcmd("PWD")
      return parse257(resp)
  end
  alias getdir pwd
  
  def system
    resp = sendcmd("SYST")
    if resp[0, 3] != "215"
      raise FTPReplyError, resp
    end
    return resp[4 .. -1]
  end
  
  def abort
    line = "ABOR" + CRLF
    print "put: ABOR\n" if @debug_mode
    @sock.send(line, Socket::MSG_OOB)
    resp = getmultiline
    unless ["426", "226", "225"].include?(resp[0, 3])
      raise FTPProtoError, resp
    end
    return resp
  end
   
  def status
    line = "STAT" + CRLF
    print "put: STAT\n" if @debug_mode
    @sock.send(line, Socket::MSG_OOB)
    return getresp
  end
  
  def mdtm(filename)
    resp = sendcmd("MDTM " + filename)
    if resp[0, 3] == "213"
      return resp[3 .. -1].strip
    end
  end
  
  def help(arg = nil)
    cmd = "HELP"
    if arg
      cmd = cmd + " " + arg
    end
    sendcmd(cmd)
  end
  
  def quit
    voidcmd("QUIT")
  end
  
  def close
    @sock.close if @sock and not @sock.closed?
  end
  
  def closed?
    @sock == nil or @sock.closed?
  end
  
  def parse227(resp)
    if resp[0, 3] != "227"
      raise FTPReplyError, resp
    end
    left = resp.index("(")
    right = resp.index(")")
    if left == nil or right == nil
      raise FTPProtoError, resp
    end
    numbers = resp[left + 1 .. right - 1].split(",")
    if numbers.length != 6
      raise FTPProtoError, resp
    end
    host = numbers[0, 4].join(".")
    port = (numbers[4].to_i << 8) + numbers[5].to_i
    return host, port
  end
  private :parse227
  
  def parse257(resp)
    if resp[0, 3] != "257"
      raise FTPReplyError, resp
    end
    if resp[3, 2] != ' "'
      return ""
    end
    dirname = ""
    i = 5
    n = resp.length
    while i < n
      c = resp[i, 1]
      i = i + 1
      if c == '"'
	if i > n or resp[i, 1] != '"'
	  break
	end
	i = i + 1
      end
      dirname = dirname + c
    end
    return dirname
  end
  private :parse257
end

## ftplib.rb ends here