DEFINITIONS
This source file includes following functions.
1 =begin
2
3 = net/ftp.rb
4
5 written by Shugo Maeda <shugo@ruby-lang.org>
6
7 This library is distributed under the terms of the Ruby license.
8 You can freely distribute/modify this library.
9
10 =end
11
12 require "socket"
13 require "monitor"
14
15 module Net
16
17 class FTPError < StandardError; end
18 class FTPReplyError < FTPError; end
19 class FTPTempError < FTPError; end
20 class FTPPermError < FTPError; end
21 class FTPProtoError < FTPError; end
22
23 class FTP
24 include MonitorMixin
25
26 FTP_PORT = 21
27 CRLF = "\r\n"
28
29 DEFAULT_BLOCKSIZE = 4096
30
31 attr_accessor :binary, :passive, :return_code, :debug_mode, :resume
32 attr_reader :welcome, :lastresp
33
34 def FTP.open(host, user = nil, passwd = nil, acct = nil)
35 new(host, user, passwd, acct)
36 end
37
38 def initialize(host = nil, user = nil, passwd = nil, acct = nil)
39 super()
40 @binary = true
41 @passive = false
42 @return_code = "\n"
43 @debug_mode = false
44 @resume = false
45 if host
46 connect(host)
47 if user
48 login(user, passwd, acct)
49 end
50 end
51 end
52
53 def open_socket(host, port)
54 if defined? SOCKSsocket and ENV["SOCKS_SERVER"]
55 @passive = true
56 return SOCKSsocket.open(host, port)
57 else
58 return TCPSocket.open(host, port)
59 end
60 end
61 private :open_socket
62
63 def connect(host, port = FTP_PORT)
64 if @debug_mode
65 print "connect: ", host, ", ", port, "\n"
66 end
67 synchronize do
68 @sock = open_socket(host, port)
69 voidresp
70 end
71 end
72
73 def set_socket(sock, get_greeting = true)
74 synchronize do
75 @sock = sock
76 if get_greeting
77 voidresp
78 end
79 end
80 end
81
82 def sanitize(s)
83 if s =~ /^PASS /i
84 return s[0, 5] + "*" * (s.length - 5)
85 else
86 return s
87 end
88 end
89 private :sanitize
90
91 def putline(line)
92 if @debug_mode
93 print "put: ", sanitize(line), "\n"
94 end
95 line = line + CRLF
96 @sock.write(line)
97 end
98 private :putline
99
100 def getline
101 line = @sock.readline # if get EOF, raise EOFError
102 if line[-2, 2] == CRLF
103 line = line[0 .. -3]
104 elsif line[-1] == ?\r or
105 line[-1] == ?\n
106 line = line[0 .. -2]
107 end
108 if @debug_mode
109 print "get: ", sanitize(line), "\n"
110 end
111 return line
112 end
113 private :getline
114
115 def getmultiline
116 line = getline
117 buff = line
118 if line[3] == ?-
119 code = line[0, 3]
120 begin
121 line = getline
122 buff << "\n" << line
123 end until line[0, 3] == code and line[3] != ?-
124 end
125 return buff << "\n"
126 end
127 private :getmultiline
128
129 def getresp
130 resp = getmultiline
131 @lastresp = resp[0, 3]
132 c = resp[0]
133 case c
134 when ?1, ?2, ?3
135 return resp
136 when ?4
137 raise FTPTempError, resp
138 when ?5
139 raise FTPPermError, resp
140 else
141 raise FTPProtoError, resp
142 end
143 end
144 private :getresp
145
146 def voidresp
147 resp = getresp
148 if resp[0] != ?2
149 raise FTPReplyError, resp
150 end
151 end
152 private :voidresp
153
154 def sendcmd(cmd)
155 synchronize do
156 putline(cmd)
157 return getresp
158 end
159 end
160
161 def voidcmd(cmd)
162 synchronize do
163 putline(cmd)
164 voidresp
165 end
166 end
167
168 def sendport(host, port)
169 af = (@sock.peeraddr)[0]
170 if af == "AF_INET"
171 hbytes = host.split(".")
172 pbytes = [port / 256, port % 256]
173 bytes = hbytes + pbytes
174 cmd = "PORT " + bytes.join(",")
175 elsif af == "AF_INET6"
176 cmd = "EPRT |2|" + host + "|" + sprintf("%d", port) + "|"
177 else
178 raise FTPProtoError, host
179 end
180 voidcmd(cmd)
181 end
182 private :sendport
183
184 def makeport
185 sock = TCPServer.open(@sock.addr[3], 0)
186 port = sock.addr[1]
187 host = sock.addr[3]
188 resp = sendport(host, port)
189 return sock
190 end
191 private :makeport
192
193 def makepasv
194 if @sock.peeraddr[0] == "AF_INET"
195 host, port = parse227(sendcmd("PASV"))
196 else
197 host, port = parse229(sendcmd("EPSV"))
198 # host, port = parse228(sendcmd("LPSV"))
199 end
200 return host, port
201 end
202 private :makepasv
203
204 def transfercmd(cmd, rest_offset = nil)
205 if @passive
206 host, port = makepasv
207 conn = open_socket(host, port)
208 if @resume and rest_offset
209 resp = sendcmd("REST " + rest_offset.to_s)
210 if resp[0] != ?3
211 raise FTPReplyError, resp
212 end
213 end
214 resp = sendcmd(cmd)
215 if resp[0] != ?1
216 raise FTPReplyError, resp
217 end
218 else
219 sock = makeport
220 if @resume and rest_offset
221 resp = sendcmd("REST " + rest_offset.to_s)
222 if resp[0] != ?3
223 raise FTPReplyError, resp
224 end
225 end
226 resp = sendcmd(cmd)
227 if resp[0] != ?1
228 raise FTPReplyError, resp
229 end
230 conn = sock.accept
231 sock.close
232 end
233 return conn
234 end
235 private :transfercmd
236
237 def getaddress
238 thishost = Socket.gethostname
239 if not thishost.index(".")
240 thishost = Socket.gethostbyname(thishost)[0]
241 end
242 if ENV.has_key?("LOGNAME")
243 realuser = ENV["LOGNAME"]
244 elsif ENV.has_key?("USER")
245 realuser = ENV["USER"]
246 else
247 realuser = "anonymous"
248 end
249 return realuser + "@" + thishost
250 end
251 private :getaddress
252
253 def login(user = "anonymous", passwd = nil, acct = nil)
254 if user == "anonymous" and passwd == nil
255 passwd = getaddress
256 end
257
258 resp = ""
259 synchronize do
260 resp = sendcmd('USER ' + user)
261 if resp[0] == ?3
262 resp = sendcmd('PASS ' + passwd)
263 end
264 if resp[0] == ?3
265 resp = sendcmd('ACCT ' + acct)
266 end
267 end
268 if resp[0] != ?2
269 raise FTPReplyError, resp
270 end
271 @welcome = resp
272 end
273
274 def retrbinary(cmd, blocksize, rest_offset = nil)
275 synchronize do
276 voidcmd("TYPE I")
277 conn = transfercmd(cmd, rest_offset)
278 loop do
279 data = conn.read(blocksize)
280 break if data == nil
281 yield(data)
282 end
283 conn.close
284 voidresp
285 end
286 end
287
288 def retrlines(cmd)
289 synchronize do
290 voidcmd("TYPE A")
291 conn = transfercmd(cmd)
292 loop do
293 line = conn.gets
294 break if line == nil
295 if line[-2, 2] == CRLF
296 line = line[0 .. -3]
297 elsif line[-1] == ?\n
298 line = line[0 .. -2]
299 end
300 yield(line)
301 end
302 conn.close
303 voidresp
304 end
305 end
306
307 def storbinary(cmd, file, blocksize, rest_offset = nil, &block)
308 synchronize do
309 voidcmd("TYPE I")
310 conn = transfercmd(cmd, rest_offset)
311 loop do
312 buf = file.read(blocksize)
313 break if buf == nil
314 conn.write(buf)
315 yield(buf) if block
316 end
317 conn.close
318 voidresp
319 end
320 end
321
322 def storlines(cmd, file, &block)
323 synchronize do
324 voidcmd("TYPE A")
325 conn = transfercmd(cmd)
326 loop do
327 buf = file.gets
328 break if buf == nil
329 if buf[-2, 2] != CRLF
330 buf = buf.chomp + CRLF
331 end
332 conn.write(buf)
333 yield(buf) if block
334 end
335 conn.close
336 voidresp
337 end
338 end
339
340 def getbinaryfile(remotefile, localfile = File.basename(remotefile),
341 blocksize = DEFAULT_BLOCKSIZE, &block)
342 if @resume
343 rest_offset = File.size?(localfile)
344 f = open(localfile, "a")
345 else
346 rest_offset = nil
347 f = open(localfile, "w")
348 end
349 begin
350 f.binmode
351 retrbinary("RETR " + remotefile, blocksize, rest_offset) do |data|
352 f.write(data)
353 yield(data) if block
354 end
355 ensure
356 f.close
357 end
358 end
359
360 def gettextfile(remotefile, localfile = File.basename(remotefile), &block)
361 f = open(localfile, "w")
362 begin
363 retrlines("RETR " + remotefile) do |line|
364 line = line + @return_code
365 f.write(line)
366 yield(line) if block
367 end
368 ensure
369 f.close
370 end
371 end
372
373 def get(localfile, remotefile = File.basename(localfile),
374 blocksize = DEFAULT_BLOCKSIZE, &block)
375 unless @binary
376 gettextfile(localfile, remotefile, &block)
377 else
378 getbinaryfile(localfile, remotefile, blocksize, &block)
379 end
380 end
381
382 def putbinaryfile(localfile, remotefile = File.basename(localfile),
383 blocksize = DEFAULT_BLOCKSIZE, &block)
384 if @resume
385 rest_offset = size(remotefile)
386 else
387 rest_offset = nil
388 end
389 f = open(localfile)
390 begin
391 f.binmode
392 storbinary("STOR " + remotefile, f, blocksize, rest_offset, &block)
393 ensure
394 f.close
395 end
396 end
397
398 def puttextfile(localfile, remotefile = File.basename(localfile), &block)
399 f = open(localfile)
400 begin
401 storlines("STOR " + remotefile, f, &block)
402 ensure
403 f.close
404 end
405 end
406
407 def put(localfile, remotefile = File.basename(localfile),
408 blocksize = DEFAULT_BLOCKSIZE, &block)
409 unless @binary
410 puttextfile(localfile, remotefile, &block)
411 else
412 putbinaryfile(localfile, remotefile, blocksize, &block)
413 end
414 end
415
416 def acct(account)
417 cmd = "ACCT " + account
418 voidcmd(cmd)
419 end
420
421 def nlst(dir = nil)
422 cmd = "NLST"
423 if dir
424 cmd = cmd + " " + dir
425 end
426 files = []
427 retrlines(cmd) do |line|
428 files.push(line)
429 end
430 return files
431 end
432
433 def list(*args, &block)
434 cmd = "LIST"
435 args.each do |arg|
436 cmd = cmd + " " + arg
437 end
438 if block
439 retrlines(cmd, &block)
440 else
441 lines = []
442 retrlines(cmd) do |line|
443 lines << line
444 end
445 return lines
446 end
447 end
448 alias ls list
449 alias dir list
450
451 def rename(fromname, toname)
452 resp = sendcmd("RNFR " + fromname)
453 if resp[0] != ?3
454 raise FTPReplyError, resp
455 end
456 voidcmd("RNTO " + toname)
457 end
458
459 def delete(filename)
460 resp = sendcmd("DELE " + filename)
461 if resp[0, 3] == "250"
462 return
463 elsif resp[0] == ?5
464 raise FTPPermError, resp
465 else
466 raise FTPReplyError, resp
467 end
468 end
469
470 def chdir(dirname)
471 if dirname == ".."
472 begin
473 voidcmd("CDUP")
474 return
475 rescue FTPPermError
476 if $![0, 3] != "500"
477 raise FTPPermError, $!
478 end
479 end
480 end
481 cmd = "CWD " + dirname
482 voidcmd(cmd)
483 end
484
485 def size(filename)
486 voidcmd("TYPE I")
487 resp = sendcmd("SIZE " + filename)
488 if resp[0, 3] != "213"
489 raise FTPReplyError, resp
490 end
491 return resp[3..-1].strip.to_i
492 end
493
494 MDTM_REGEXP = /^(\d\d\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)$/
495
496 def mtime(filename, local = false)
497 str = mdtm(filename)
498 ary = str.scan(MDTM_REGEXP)[0].collect {|i| i.to_i}
499 return local ? Time.local(*ary) : Time.gm(*ary)
500 end
501
502 def mkdir(dirname)
503 resp = sendcmd("MKD " + dirname)
504 return parse257(resp)
505 end
506
507 def rmdir(dirname)
508 voidcmd("RMD " + dirname)
509 end
510
511 def pwd
512 resp = sendcmd("PWD")
513 return parse257(resp)
514 end
515 alias getdir pwd
516
517 def system
518 resp = sendcmd("SYST")
519 if resp[0, 3] != "215"
520 raise FTPReplyError, resp
521 end
522 return resp[4 .. -1]
523 end
524
525 def abort
526 line = "ABOR" + CRLF
527 print "put: ABOR\n" if @debug_mode
528 @sock.send(line, Socket::MSG_OOB)
529 resp = getmultiline
530 unless ["426", "226", "225"].include?(resp[0, 3])
531 raise FTPProtoError, resp
532 end
533 return resp
534 end
535
536 def status
537 line = "STAT" + CRLF
538 print "put: STAT\n" if @debug_mode
539 @sock.send(line, Socket::MSG_OOB)
540 return getresp
541 end
542
543 def mdtm(filename)
544 resp = sendcmd("MDTM " + filename)
545 if resp[0, 3] == "213"
546 return resp[3 .. -1].strip
547 end
548 end
549
550 def help(arg = nil)
551 cmd = "HELP"
552 if arg
553 cmd = cmd + " " + arg
554 end
555 sendcmd(cmd)
556 end
557
558 def quit
559 voidcmd("QUIT")
560 end
561
562 def noop
563 voidcmd("NOOP")
564 end
565
566 def site(arg)
567 cmd = "SITE " + arg
568 voidcmd(cmd)
569 end
570
571 def close
572 @sock.close if @sock and not @sock.closed?
573 end
574
575 def closed?
576 @sock == nil or @sock.closed?
577 end
578
579 def parse227(resp)
580 if resp[0, 3] != "227"
581 raise FTPReplyError, resp
582 end
583 left = resp.index("(")
584 right = resp.index(")")
585 if left == nil or right == nil
586 raise FTPProtoError, resp
587 end
588 numbers = resp[left + 1 .. right - 1].split(",")
589 if numbers.length != 6
590 raise FTPProtoError, resp
591 end
592 host = numbers[0, 4].join(".")
593 port = (numbers[4].to_i << 8) + numbers[5].to_i
594 return host, port
595 end
596 private :parse227
597
598 def parse228(resp)
599 if resp[0, 3] != "228"
600 raise FTPReplyError, resp
601 end
602 left = resp.index("(")
603 right = resp.index(")")
604 if left == nil or right == nil
605 raise FTPProtoError, resp
606 end
607 numbers = resp[left + 1 .. right - 1].split(",")
608 if numbers[0] == "4"
609 if numbers.length != 9 || numbers[1] != "4" || numbers[2 + 4] != "2"
610 raise FTPProtoError, resp
611 end
612 host = numbers[2, 4].join(".")
613 port = (numbers[7].to_i << 8) + numbers[8].to_i
614 elsif numbers[0] == "6"
615 if numbers.length != 21 || numbers[1] != "16" || numbers[2 + 16] != "2"
616 raise FTPProtoError, resp
617 end
618 v6 = ["", "", "", "", "", "", "", ""]
619 for i in 0 .. 7
620 v6[i] = sprintf("%02x%02x", numbers[(i * 2) + 2].to_i,
621 numbers[(i * 2) + 3].to_i)
622 end
623 host = v6[0, 8].join(":")
624 port = (numbers[19].to_i << 8) + numbers[20].to_i
625 end
626 return host, port
627 end
628 private :parse228
629
630 def parse229(resp)
631 if resp[0, 3] != "229"
632 raise FTPReplyError, resp
633 end
634 left = resp.index("(")
635 right = resp.index(")")
636 if left == nil or right == nil
637 raise FTPProtoError, resp
638 end
639 numbers = resp[left + 1 .. right - 1].split(resp[left + 1, 1])
640 if numbers.length != 4
641 raise FTPProtoError, resp
642 end
643 port = numbers[3].to_i
644 host = (@sock.peeraddr())[3]
645 return host, port
646 end
647 private :parse229
648
649 def parse257(resp)
650 if resp[0, 3] != "257"
651 raise FTPReplyError, resp
652 end
653 if resp[3, 2] != ' "'
654 return ""
655 end
656 dirname = ""
657 i = 5
658 n = resp.length
659 while i < n
660 c = resp[i, 1]
661 i = i + 1
662 if c == '"'
663 if i > n or resp[i, 1] != '"'
664 break
665 end
666 i = i + 1
667 end
668 dirname = dirname + c
669 end
670 return dirname
671 end
672 private :parse257
673 end
674
675 end