net-ftp 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 144a4e8100ca6ae408868765924fa87dafc243da7672297d9f8830b28c676fde
4
+ data.tar.gz: 2247f4f98c0322f4e4f163863f0a2f4439a652d1c50a3f2ec6cfd4479b9205a1
5
+ SHA512:
6
+ metadata.gz: 39025652132d9af7af55c0f67bb9d28b1e9645c0949dc5886e490d579e67fd51ce72b74c3d41a7255e328c706fd32cb94a415a7ef09b1e82f4f41f20050e0f9c
7
+ data.tar.gz: e5c3e151dca8fb1a06208e8f0f945d81b48a8ee18ce93dd69206988268c7a38ffc97786b6404485265f4445088364d42def0dac88370ee4853a86fe4a8f32468
@@ -0,0 +1,24 @@
1
+ name: ubuntu
2
+
3
+ on: [push, pull_request]
4
+
5
+ jobs:
6
+ build:
7
+ name: build (${{ matrix.ruby }} / ${{ matrix.os }})
8
+ strategy:
9
+ matrix:
10
+ ruby: [ 2.7, 2.6, 2.5, 2.4, head ]
11
+ os: [ ubuntu-latest, macos-latest ]
12
+ runs-on: ${{ matrix.os }}
13
+ steps:
14
+ - uses: actions/checkout@master
15
+ - name: Set up Ruby
16
+ uses: ruby/setup-ruby@v1
17
+ with:
18
+ ruby-version: ${{ matrix.ruby }}
19
+ - name: Install dependencies
20
+ run: |
21
+ gem install bundler --no-document
22
+ bundle install
23
+ - name: Run test
24
+ run: rake test
@@ -0,0 +1,8 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
4
+
5
+ gem "rake"
6
+ gem "test-unit"
@@ -0,0 +1,23 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ net-ftp (0.1.0)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ power_assert (1.1.5)
10
+ rake (13.0.1)
11
+ test-unit (3.3.5)
12
+ power_assert
13
+
14
+ PLATFORMS
15
+ ruby
16
+
17
+ DEPENDENCIES
18
+ net-ftp!
19
+ rake
20
+ test-unit
21
+
22
+ BUNDLED WITH
23
+ 2.1.4
@@ -0,0 +1,56 @@
1
+ # Net::FTP
2
+
3
+ This class implements the File Transfer Protocol. If you have used a
4
+ command-line FTP program, and are familiar with the commands, you will be
5
+ able to use this class easily. Some extra features are included to take
6
+ advantage of Ruby's style and strengths.
7
+
8
+ ## Installation
9
+
10
+ Add this line to your application's Gemfile:
11
+
12
+ ```ruby
13
+ gem 'net-ftp'
14
+ ```
15
+
16
+ And then execute:
17
+
18
+ $ bundle install
19
+
20
+ Or install it yourself as:
21
+
22
+ $ gem install net-ftp
23
+
24
+ ## Usage
25
+
26
+ ### Example 1
27
+
28
+ ```ruby
29
+ ftp = Net::FTP.new('example.com')
30
+ ftp.login
31
+ files = ftp.chdir('pub/lang/ruby/contrib')
32
+ files = ftp.list('n*')
33
+ ftp.getbinaryfile('nif.rb-0.91.gz', 'nif.gz', 1024)
34
+ ftp.close
35
+ ```
36
+
37
+ ### Example 2
38
+
39
+ ```ruby
40
+ Net::FTP.open('example.com') do |ftp|
41
+ ftp.login
42
+ files = ftp.chdir('pub/lang/ruby/contrib')
43
+ files = ftp.list('n*')
44
+ ftp.getbinaryfile('nif.rb-0.91.gz', 'nif.gz', 1024)
45
+ end
46
+ ```
47
+
48
+ ## Development
49
+
50
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
51
+
52
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
53
+
54
+ ## Contributing
55
+
56
+ Bug reports and pull requests are welcome on GitHub at https://github.com/ruby/net-ftp.
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test/lib"
6
+ t.ruby_opts << "-rhelper"
7
+ t.test_files = FileList["test/**/test_*.rb"]
8
+ end
9
+
10
+ task :default => :test
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "net/ftp"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,1533 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ # = net/ftp.rb - FTP Client Library
4
+ #
5
+ # Written by Shugo Maeda <shugo@ruby-lang.org>.
6
+ #
7
+ # Documentation by Gavin Sinclair, sourced from "Programming Ruby" (Hunt/Thomas)
8
+ # and "Ruby In a Nutshell" (Matsumoto), used with permission.
9
+ #
10
+ # This library is distributed under the terms of the Ruby license.
11
+ # You can freely distribute/modify this library.
12
+ #
13
+ # It is included in the Ruby standard library.
14
+ #
15
+ # See the Net::FTP class for an overview.
16
+ #
17
+
18
+ require "socket"
19
+ require "monitor"
20
+ require "net/protocol"
21
+ require "time"
22
+ begin
23
+ require "openssl"
24
+ rescue LoadError
25
+ end
26
+
27
+ module Net
28
+
29
+ # :stopdoc:
30
+ class FTPError < StandardError; end
31
+ class FTPReplyError < FTPError; end
32
+ class FTPTempError < FTPError; end
33
+ class FTPPermError < FTPError; end
34
+ class FTPProtoError < FTPError; end
35
+ class FTPConnectionError < FTPError; end
36
+ # :startdoc:
37
+
38
+ #
39
+ # This class implements the File Transfer Protocol. If you have used a
40
+ # command-line FTP program, and are familiar with the commands, you will be
41
+ # able to use this class easily. Some extra features are included to take
42
+ # advantage of Ruby's style and strengths.
43
+ #
44
+ # == Example
45
+ #
46
+ # require 'net/ftp'
47
+ #
48
+ # === Example 1
49
+ #
50
+ # ftp = Net::FTP.new('example.com')
51
+ # ftp.login
52
+ # files = ftp.chdir('pub/lang/ruby/contrib')
53
+ # files = ftp.list('n*')
54
+ # ftp.getbinaryfile('nif.rb-0.91.gz', 'nif.gz', 1024)
55
+ # ftp.close
56
+ #
57
+ # === Example 2
58
+ #
59
+ # Net::FTP.open('example.com') do |ftp|
60
+ # ftp.login
61
+ # files = ftp.chdir('pub/lang/ruby/contrib')
62
+ # files = ftp.list('n*')
63
+ # ftp.getbinaryfile('nif.rb-0.91.gz', 'nif.gz', 1024)
64
+ # end
65
+ #
66
+ # == Major Methods
67
+ #
68
+ # The following are the methods most likely to be useful to users:
69
+ # - FTP.open
70
+ # - #getbinaryfile
71
+ # - #gettextfile
72
+ # - #putbinaryfile
73
+ # - #puttextfile
74
+ # - #chdir
75
+ # - #nlst
76
+ # - #size
77
+ # - #rename
78
+ # - #delete
79
+ #
80
+ class FTP < Protocol
81
+ include MonitorMixin
82
+ if defined?(OpenSSL::SSL)
83
+ include OpenSSL
84
+ include SSL
85
+ end
86
+
87
+ # :stopdoc:
88
+ FTP_PORT = 21
89
+ CRLF = "\r\n"
90
+ DEFAULT_BLOCKSIZE = BufferedIO::BUFSIZE
91
+ @@default_passive = true
92
+ # :startdoc:
93
+
94
+ # When +true+, transfers are performed in binary mode. Default: +true+.
95
+ attr_reader :binary
96
+
97
+ # When +true+, the connection is in passive mode. Default: +true+.
98
+ attr_accessor :passive
99
+
100
+ # When +true+, all traffic to and from the server is written
101
+ # to +$stdout+. Default: +false+.
102
+ attr_accessor :debug_mode
103
+
104
+ # Sets or retrieves the +resume+ status, which decides whether incomplete
105
+ # transfers are resumed or restarted. Default: +false+.
106
+ attr_accessor :resume
107
+
108
+ # Number of seconds to wait for the connection to open. Any number
109
+ # may be used, including Floats for fractional seconds. If the FTP
110
+ # object cannot open a connection in this many seconds, it raises a
111
+ # Net::OpenTimeout exception. The default value is +nil+.
112
+ attr_accessor :open_timeout
113
+
114
+ # Number of seconds to wait for the TLS handshake. Any number
115
+ # may be used, including Floats for fractional seconds. If the FTP
116
+ # object cannot complete the TLS handshake in this many seconds, it
117
+ # raises a Net::OpenTimeout exception. The default value is +nil+.
118
+ # If +ssl_handshake_timeout+ is +nil+, +open_timeout+ is used instead.
119
+ attr_accessor :ssl_handshake_timeout
120
+
121
+ # Number of seconds to wait for one block to be read (via one read(2)
122
+ # call). Any number may be used, including Floats for fractional
123
+ # seconds. If the FTP object cannot read data in this many seconds,
124
+ # it raises a Timeout::Error exception. The default value is 60 seconds.
125
+ attr_reader :read_timeout
126
+
127
+ # Setter for the read_timeout attribute.
128
+ def read_timeout=(sec)
129
+ @sock.read_timeout = sec
130
+ @read_timeout = sec
131
+ end
132
+
133
+ # The server's welcome message.
134
+ attr_reader :welcome
135
+
136
+ # The server's last response code.
137
+ attr_reader :last_response_code
138
+ alias lastresp last_response_code
139
+
140
+ # The server's last response.
141
+ attr_reader :last_response
142
+
143
+ # When +true+, connections are in passive mode per default.
144
+ # Default: +true+.
145
+ def self.default_passive=(value)
146
+ @@default_passive = value
147
+ end
148
+
149
+ # When +true+, connections are in passive mode per default.
150
+ # Default: +true+.
151
+ def self.default_passive
152
+ @@default_passive
153
+ end
154
+
155
+ #
156
+ # A synonym for <tt>FTP.new</tt>, but with a mandatory host parameter.
157
+ #
158
+ # If a block is given, it is passed the +FTP+ object, which will be closed
159
+ # when the block finishes, or when an exception is raised.
160
+ #
161
+ def FTP.open(host, *args)
162
+ if block_given?
163
+ ftp = new(host, *args)
164
+ begin
165
+ yield ftp
166
+ ensure
167
+ ftp.close
168
+ end
169
+ else
170
+ new(host, *args)
171
+ end
172
+ end
173
+
174
+ # :call-seq:
175
+ # Net::FTP.new(host = nil, options = {})
176
+ #
177
+ # Creates and returns a new +FTP+ object. If a +host+ is given, a connection
178
+ # is made.
179
+ #
180
+ # +options+ is an option hash, each key of which is a symbol.
181
+ #
182
+ # The available options are:
183
+ #
184
+ # port:: Port number (default value is 21)
185
+ # ssl:: If options[:ssl] is true, then an attempt will be made
186
+ # to use SSL (now TLS) to connect to the server. For this
187
+ # to work OpenSSL [OSSL] and the Ruby OpenSSL [RSSL]
188
+ # extensions need to be installed. If options[:ssl] is a
189
+ # hash, it's passed to OpenSSL::SSL::SSLContext#set_params
190
+ # as parameters.
191
+ # private_data_connection:: If true, TLS is used for data connections.
192
+ # Default: +true+ when options[:ssl] is true.
193
+ # username:: Username for login. If options[:username] is the string
194
+ # "anonymous" and the options[:password] is +nil+,
195
+ # "anonymous@" is used as a password.
196
+ # password:: Password for login.
197
+ # account:: Account information for ACCT.
198
+ # passive:: When +true+, the connection is in passive mode. Default:
199
+ # +true+.
200
+ # open_timeout:: Number of seconds to wait for the connection to open.
201
+ # See Net::FTP#open_timeout for details. Default: +nil+.
202
+ # read_timeout:: Number of seconds to wait for one block to be read.
203
+ # See Net::FTP#read_timeout for details. Default: +60+.
204
+ # ssl_handshake_timeout:: Number of seconds to wait for the TLS
205
+ # handshake.
206
+ # See Net::FTP#ssl_handshake_timeout for
207
+ # details. Default: +nil+.
208
+ # debug_mode:: When +true+, all traffic to and from the server is
209
+ # written to +$stdout+. Default: +false+.
210
+ #
211
+ def initialize(host = nil, user_or_options = {}, passwd = nil, acct = nil)
212
+ super()
213
+ begin
214
+ options = user_or_options.to_hash
215
+ rescue NoMethodError
216
+ # for backward compatibility
217
+ options = {}
218
+ options[:username] = user_or_options
219
+ options[:password] = passwd
220
+ options[:account] = acct
221
+ end
222
+ @host = nil
223
+ if options[:ssl]
224
+ unless defined?(OpenSSL::SSL)
225
+ raise "SSL extension not installed"
226
+ end
227
+ ssl_params = options[:ssl] == true ? {} : options[:ssl]
228
+ @ssl_context = SSLContext.new
229
+ @ssl_context.set_params(ssl_params)
230
+ if defined?(VerifyCallbackProc)
231
+ @ssl_context.verify_callback = VerifyCallbackProc
232
+ end
233
+ @ssl_context.session_cache_mode =
234
+ OpenSSL::SSL::SSLContext::SESSION_CACHE_CLIENT |
235
+ OpenSSL::SSL::SSLContext::SESSION_CACHE_NO_INTERNAL_STORE
236
+ @ssl_context.session_new_cb = proc {|sock, sess| @ssl_session = sess }
237
+ @ssl_session = nil
238
+ if options[:private_data_connection].nil?
239
+ @private_data_connection = true
240
+ else
241
+ @private_data_connection = options[:private_data_connection]
242
+ end
243
+ else
244
+ @ssl_context = nil
245
+ if options[:private_data_connection]
246
+ raise ArgumentError,
247
+ "private_data_connection can be set to true only when ssl is enabled"
248
+ end
249
+ @private_data_connection = false
250
+ end
251
+ @binary = true
252
+ if options[:passive].nil?
253
+ @passive = @@default_passive
254
+ else
255
+ @passive = options[:passive]
256
+ end
257
+ if options[:debug_mode].nil?
258
+ @debug_mode = false
259
+ else
260
+ @debug_mode = options[:debug_mode]
261
+ end
262
+ @resume = false
263
+ @bare_sock = @sock = NullSocket.new
264
+ @logged_in = false
265
+ @open_timeout = options[:open_timeout]
266
+ @ssl_handshake_timeout = options[:ssl_handshake_timeout]
267
+ @read_timeout = options[:read_timeout] || 60
268
+ if host
269
+ connect(host, options[:port] || FTP_PORT)
270
+ if options[:username]
271
+ login(options[:username], options[:password], options[:account])
272
+ end
273
+ end
274
+ end
275
+
276
+ # A setter to toggle transfers in binary mode.
277
+ # +newmode+ is either +true+ or +false+
278
+ def binary=(newmode)
279
+ if newmode != @binary
280
+ @binary = newmode
281
+ send_type_command if @logged_in
282
+ end
283
+ end
284
+
285
+ # Sends a command to destination host, with the current binary sendmode
286
+ # type.
287
+ #
288
+ # If binary mode is +true+, then "TYPE I" (image) is sent, otherwise "TYPE
289
+ # A" (ascii) is sent.
290
+ def send_type_command # :nodoc:
291
+ if @binary
292
+ voidcmd("TYPE I")
293
+ else
294
+ voidcmd("TYPE A")
295
+ end
296
+ end
297
+ private :send_type_command
298
+
299
+ # Toggles transfers in binary mode and yields to a block.
300
+ # This preserves your current binary send mode, but allows a temporary
301
+ # transaction with binary sendmode of +newmode+.
302
+ #
303
+ # +newmode+ is either +true+ or +false+
304
+ def with_binary(newmode) # :nodoc:
305
+ oldmode = binary
306
+ self.binary = newmode
307
+ begin
308
+ yield
309
+ ensure
310
+ self.binary = oldmode
311
+ end
312
+ end
313
+ private :with_binary
314
+
315
+ # Obsolete
316
+ def return_code # :nodoc:
317
+ warn("Net::FTP#return_code is obsolete and do nothing", uplevel: 1)
318
+ return "\n"
319
+ end
320
+
321
+ # Obsolete
322
+ def return_code=(s) # :nodoc:
323
+ warn("Net::FTP#return_code= is obsolete and do nothing", uplevel: 1)
324
+ end
325
+
326
+ # Constructs a socket with +host+ and +port+.
327
+ #
328
+ # If SOCKSSocket is defined and the environment (ENV) defines
329
+ # SOCKS_SERVER, then a SOCKSSocket is returned, else a Socket is
330
+ # returned.
331
+ def open_socket(host, port) # :nodoc:
332
+ return Timeout.timeout(@open_timeout, OpenTimeout) {
333
+ if defined? SOCKSSocket and ENV["SOCKS_SERVER"]
334
+ @passive = true
335
+ SOCKSSocket.open(host, port)
336
+ else
337
+ Socket.tcp(host, port)
338
+ end
339
+ }
340
+ end
341
+ private :open_socket
342
+
343
+ def start_tls_session(sock)
344
+ ssl_sock = SSLSocket.new(sock, @ssl_context)
345
+ ssl_sock.sync_close = true
346
+ ssl_sock.hostname = @host if ssl_sock.respond_to? :hostname=
347
+ if @ssl_session &&
348
+ Process.clock_gettime(Process::CLOCK_REALTIME) < @ssl_session.time.to_f + @ssl_session.timeout
349
+ # ProFTPD returns 425 for data connections if session is not reused.
350
+ ssl_sock.session = @ssl_session
351
+ end
352
+ ssl_socket_connect(ssl_sock, @ssl_handshake_timeout || @open_timeout)
353
+ if @ssl_context.verify_mode != VERIFY_NONE
354
+ ssl_sock.post_connection_check(@host)
355
+ end
356
+ return ssl_sock
357
+ end
358
+ private :start_tls_session
359
+
360
+ #
361
+ # Establishes an FTP connection to host, optionally overriding the default
362
+ # port. If the environment variable +SOCKS_SERVER+ is set, sets up the
363
+ # connection through a SOCKS proxy. Raises an exception (typically
364
+ # <tt>Errno::ECONNREFUSED</tt>) if the connection cannot be established.
365
+ #
366
+ def connect(host, port = FTP_PORT)
367
+ if @debug_mode
368
+ print "connect: ", host, ", ", port, "\n"
369
+ end
370
+ synchronize do
371
+ @host = host
372
+ @bare_sock = open_socket(host, port)
373
+ @sock = BufferedSocket.new(@bare_sock, read_timeout: @read_timeout)
374
+ voidresp
375
+ if @ssl_context
376
+ begin
377
+ voidcmd("AUTH TLS")
378
+ ssl_sock = start_tls_session(@bare_sock)
379
+ @sock = BufferedSSLSocket.new(ssl_sock, read_timeout: @read_timeout)
380
+ if @private_data_connection
381
+ voidcmd("PBSZ 0")
382
+ voidcmd("PROT P")
383
+ end
384
+ rescue OpenSSL::SSL::SSLError, OpenTimeout
385
+ @sock.close
386
+ raise
387
+ end
388
+ end
389
+ end
390
+ end
391
+
392
+ #
393
+ # Set the socket used to connect to the FTP server.
394
+ #
395
+ # May raise FTPReplyError if +get_greeting+ is false.
396
+ def set_socket(sock, get_greeting = true)
397
+ synchronize do
398
+ @sock = sock
399
+ if get_greeting
400
+ voidresp
401
+ end
402
+ end
403
+ end
404
+
405
+ # If string +s+ includes the PASS command (password), then the contents of
406
+ # the password are cleaned from the string using "*"
407
+ def sanitize(s) # :nodoc:
408
+ if s =~ /^PASS /i
409
+ return s[0, 5] + "*" * (s.length - 5)
410
+ else
411
+ return s
412
+ end
413
+ end
414
+ private :sanitize
415
+
416
+ # Ensures that +line+ has a control return / line feed (CRLF) and writes
417
+ # it to the socket.
418
+ def putline(line) # :nodoc:
419
+ if @debug_mode
420
+ print "put: ", sanitize(line), "\n"
421
+ end
422
+ if /[\r\n]/ =~ line
423
+ raise ArgumentError, "A line must not contain CR or LF"
424
+ end
425
+ line = line + CRLF
426
+ @sock.write(line)
427
+ end
428
+ private :putline
429
+
430
+ # Reads a line from the sock. If EOF, then it will raise EOFError
431
+ def getline # :nodoc:
432
+ line = @sock.readline # if get EOF, raise EOFError
433
+ line.sub!(/(\r\n|\n|\r)\z/n, "")
434
+ if @debug_mode
435
+ print "get: ", sanitize(line), "\n"
436
+ end
437
+ return line
438
+ end
439
+ private :getline
440
+
441
+ # Receive a section of lines until the response code's match.
442
+ def getmultiline # :nodoc:
443
+ lines = []
444
+ lines << getline
445
+ code = lines.last.slice(/\A([0-9a-zA-Z]{3})-/, 1)
446
+ if code
447
+ delimiter = code + " "
448
+ begin
449
+ lines << getline
450
+ end until lines.last.start_with?(delimiter)
451
+ end
452
+ return lines.join("\n") + "\n"
453
+ end
454
+ private :getmultiline
455
+
456
+ # Receives a response from the destination host.
457
+ #
458
+ # Returns the response code or raises FTPTempError, FTPPermError, or
459
+ # FTPProtoError
460
+ def getresp # :nodoc:
461
+ @last_response = getmultiline
462
+ @last_response_code = @last_response[0, 3]
463
+ case @last_response_code
464
+ when /\A[123]/
465
+ return @last_response
466
+ when /\A4/
467
+ raise FTPTempError, @last_response
468
+ when /\A5/
469
+ raise FTPPermError, @last_response
470
+ else
471
+ raise FTPProtoError, @last_response
472
+ end
473
+ end
474
+ private :getresp
475
+
476
+ # Receives a response.
477
+ #
478
+ # Raises FTPReplyError if the first position of the response code is not
479
+ # equal 2.
480
+ def voidresp # :nodoc:
481
+ resp = getresp
482
+ if !resp.start_with?("2")
483
+ raise FTPReplyError, resp
484
+ end
485
+ end
486
+ private :voidresp
487
+
488
+ #
489
+ # Sends a command and returns the response.
490
+ #
491
+ def sendcmd(cmd)
492
+ synchronize do
493
+ putline(cmd)
494
+ return getresp
495
+ end
496
+ end
497
+
498
+ #
499
+ # Sends a command and expect a response beginning with '2'.
500
+ #
501
+ def voidcmd(cmd)
502
+ synchronize do
503
+ putline(cmd)
504
+ voidresp
505
+ end
506
+ end
507
+
508
+ # Constructs and send the appropriate PORT (or EPRT) command
509
+ def sendport(host, port) # :nodoc:
510
+ remote_address = @bare_sock.remote_address
511
+ if remote_address.ipv4?
512
+ cmd = "PORT " + (host.split(".") + port.divmod(256)).join(",")
513
+ elsif remote_address.ipv6?
514
+ cmd = sprintf("EPRT |2|%s|%d|", host, port)
515
+ else
516
+ raise FTPProtoError, host
517
+ end
518
+ voidcmd(cmd)
519
+ end
520
+ private :sendport
521
+
522
+ # Constructs a TCPServer socket
523
+ def makeport # :nodoc:
524
+ Addrinfo.tcp(@bare_sock.local_address.ip_address, 0).listen
525
+ end
526
+ private :makeport
527
+
528
+ # sends the appropriate command to enable a passive connection
529
+ def makepasv # :nodoc:
530
+ if @bare_sock.remote_address.ipv4?
531
+ host, port = parse227(sendcmd("PASV"))
532
+ else
533
+ host, port = parse229(sendcmd("EPSV"))
534
+ # host, port = parse228(sendcmd("LPSV"))
535
+ end
536
+ return host, port
537
+ end
538
+ private :makepasv
539
+
540
+ # Constructs a connection for transferring data
541
+ def transfercmd(cmd, rest_offset = nil) # :nodoc:
542
+ if @passive
543
+ host, port = makepasv
544
+ conn = open_socket(host, port)
545
+ if @resume and rest_offset
546
+ resp = sendcmd("REST " + rest_offset.to_s)
547
+ if !resp.start_with?("3")
548
+ raise FTPReplyError, resp
549
+ end
550
+ end
551
+ resp = sendcmd(cmd)
552
+ # skip 2XX for some ftp servers
553
+ resp = getresp if resp.start_with?("2")
554
+ if !resp.start_with?("1")
555
+ raise FTPReplyError, resp
556
+ end
557
+ else
558
+ sock = makeport
559
+ begin
560
+ addr = sock.local_address
561
+ sendport(addr.ip_address, addr.ip_port)
562
+ if @resume and rest_offset
563
+ resp = sendcmd("REST " + rest_offset.to_s)
564
+ if !resp.start_with?("3")
565
+ raise FTPReplyError, resp
566
+ end
567
+ end
568
+ resp = sendcmd(cmd)
569
+ # skip 2XX for some ftp servers
570
+ resp = getresp if resp.start_with?("2")
571
+ if !resp.start_with?("1")
572
+ raise FTPReplyError, resp
573
+ end
574
+ conn, = sock.accept
575
+ sock.shutdown(Socket::SHUT_WR) rescue nil
576
+ sock.read rescue nil
577
+ ensure
578
+ sock.close
579
+ end
580
+ end
581
+ if @private_data_connection
582
+ return BufferedSSLSocket.new(start_tls_session(conn),
583
+ read_timeout: @read_timeout)
584
+ else
585
+ return BufferedSocket.new(conn, read_timeout: @read_timeout)
586
+ end
587
+ end
588
+ private :transfercmd
589
+
590
+ #
591
+ # Logs in to the remote host. The session must have been
592
+ # previously connected. If +user+ is the string "anonymous" and
593
+ # the +password+ is +nil+, "anonymous@" is used as a password. If
594
+ # the +acct+ parameter is not +nil+, an FTP ACCT command is sent
595
+ # following the successful login. Raises an exception on error
596
+ # (typically <tt>Net::FTPPermError</tt>).
597
+ #
598
+ def login(user = "anonymous", passwd = nil, acct = nil)
599
+ if user == "anonymous" and passwd == nil
600
+ passwd = "anonymous@"
601
+ end
602
+
603
+ resp = ""
604
+ synchronize do
605
+ resp = sendcmd('USER ' + user)
606
+ if resp.start_with?("3")
607
+ raise FTPReplyError, resp if passwd.nil?
608
+ resp = sendcmd('PASS ' + passwd)
609
+ end
610
+ if resp.start_with?("3")
611
+ raise FTPReplyError, resp if acct.nil?
612
+ resp = sendcmd('ACCT ' + acct)
613
+ end
614
+ end
615
+ if !resp.start_with?("2")
616
+ raise FTPReplyError, resp
617
+ end
618
+ @welcome = resp
619
+ send_type_command
620
+ @logged_in = true
621
+ end
622
+
623
+ #
624
+ # Puts the connection into binary (image) mode, issues the given command,
625
+ # and fetches the data returned, passing it to the associated block in
626
+ # chunks of +blocksize+ characters. Note that +cmd+ is a server command
627
+ # (such as "RETR myfile").
628
+ #
629
+ def retrbinary(cmd, blocksize, rest_offset = nil) # :yield: data
630
+ synchronize do
631
+ with_binary(true) do
632
+ begin
633
+ conn = transfercmd(cmd, rest_offset)
634
+ while data = conn.read(blocksize)
635
+ yield(data)
636
+ end
637
+ conn.shutdown(Socket::SHUT_WR)
638
+ conn.read_timeout = 1
639
+ conn.read
640
+ ensure
641
+ conn.close if conn
642
+ end
643
+ voidresp
644
+ end
645
+ end
646
+ end
647
+
648
+ #
649
+ # Puts the connection into ASCII (text) mode, issues the given command, and
650
+ # passes the resulting data, one line at a time, to the associated block. If
651
+ # no block is given, prints the lines. Note that +cmd+ is a server command
652
+ # (such as "RETR myfile").
653
+ #
654
+ def retrlines(cmd) # :yield: line
655
+ synchronize do
656
+ with_binary(false) do
657
+ begin
658
+ conn = transfercmd(cmd)
659
+ while line = conn.gets
660
+ yield(line.sub(/\r?\n\z/, ""), !line.match(/\n\z/).nil?)
661
+ end
662
+ conn.shutdown(Socket::SHUT_WR)
663
+ conn.read_timeout = 1
664
+ conn.read
665
+ ensure
666
+ conn.close if conn
667
+ end
668
+ voidresp
669
+ end
670
+ end
671
+ end
672
+
673
+ #
674
+ # Puts the connection into binary (image) mode, issues the given server-side
675
+ # command (such as "STOR myfile"), and sends the contents of the file named
676
+ # +file+ to the server. If the optional block is given, it also passes it
677
+ # the data, in chunks of +blocksize+ characters.
678
+ #
679
+ def storbinary(cmd, file, blocksize, rest_offset = nil) # :yield: data
680
+ if rest_offset
681
+ file.seek(rest_offset, IO::SEEK_SET)
682
+ end
683
+ synchronize do
684
+ with_binary(true) do
685
+ begin
686
+ conn = transfercmd(cmd)
687
+ while buf = file.read(blocksize)
688
+ conn.write(buf)
689
+ yield(buf) if block_given?
690
+ end
691
+ conn.shutdown(Socket::SHUT_WR)
692
+ conn.read_timeout = 1
693
+ conn.read
694
+ ensure
695
+ conn.close if conn
696
+ end
697
+ voidresp
698
+ end
699
+ end
700
+ rescue Errno::EPIPE
701
+ # EPIPE, in this case, means that the data connection was unexpectedly
702
+ # terminated. Rather than just raising EPIPE to the caller, check the
703
+ # response on the control connection. If getresp doesn't raise a more
704
+ # appropriate exception, re-raise the original exception.
705
+ getresp
706
+ raise
707
+ end
708
+
709
+ #
710
+ # Puts the connection into ASCII (text) mode, issues the given server-side
711
+ # command (such as "STOR myfile"), and sends the contents of the file
712
+ # named +file+ to the server, one line at a time. If the optional block is
713
+ # given, it also passes it the lines.
714
+ #
715
+ def storlines(cmd, file) # :yield: line
716
+ synchronize do
717
+ with_binary(false) do
718
+ begin
719
+ conn = transfercmd(cmd)
720
+ while buf = file.gets
721
+ if buf[-2, 2] != CRLF
722
+ buf = buf.chomp + CRLF
723
+ end
724
+ conn.write(buf)
725
+ yield(buf) if block_given?
726
+ end
727
+ conn.shutdown(Socket::SHUT_WR)
728
+ conn.read_timeout = 1
729
+ conn.read
730
+ ensure
731
+ conn.close if conn
732
+ end
733
+ voidresp
734
+ end
735
+ end
736
+ rescue Errno::EPIPE
737
+ # EPIPE, in this case, means that the data connection was unexpectedly
738
+ # terminated. Rather than just raising EPIPE to the caller, check the
739
+ # response on the control connection. If getresp doesn't raise a more
740
+ # appropriate exception, re-raise the original exception.
741
+ getresp
742
+ raise
743
+ end
744
+
745
+ #
746
+ # Retrieves +remotefile+ in binary mode, storing the result in +localfile+.
747
+ # If +localfile+ is nil, returns retrieved data.
748
+ # If a block is supplied, it is passed the retrieved data in +blocksize+
749
+ # chunks.
750
+ #
751
+ def getbinaryfile(remotefile, localfile = File.basename(remotefile),
752
+ blocksize = DEFAULT_BLOCKSIZE, &block) # :yield: data
753
+ f = nil
754
+ result = nil
755
+ if localfile
756
+ if @resume
757
+ rest_offset = File.size?(localfile)
758
+ f = File.open(localfile, "a")
759
+ else
760
+ rest_offset = nil
761
+ f = File.open(localfile, "w")
762
+ end
763
+ elsif !block_given?
764
+ result = String.new
765
+ end
766
+ begin
767
+ f&.binmode
768
+ retrbinary("RETR #{remotefile}", blocksize, rest_offset) do |data|
769
+ f&.write(data)
770
+ block&.(data)
771
+ result&.concat(data)
772
+ end
773
+ return result
774
+ ensure
775
+ f&.close
776
+ end
777
+ end
778
+
779
+ #
780
+ # Retrieves +remotefile+ in ASCII (text) mode, storing the result in
781
+ # +localfile+.
782
+ # If +localfile+ is nil, returns retrieved data.
783
+ # If a block is supplied, it is passed the retrieved data one
784
+ # line at a time.
785
+ #
786
+ def gettextfile(remotefile, localfile = File.basename(remotefile),
787
+ &block) # :yield: line
788
+ f = nil
789
+ result = nil
790
+ if localfile
791
+ f = File.open(localfile, "w")
792
+ elsif !block_given?
793
+ result = String.new
794
+ end
795
+ begin
796
+ retrlines("RETR #{remotefile}") do |line, newline|
797
+ l = newline ? line + "\n" : line
798
+ f&.print(l)
799
+ block&.(line, newline)
800
+ result&.concat(l)
801
+ end
802
+ return result
803
+ ensure
804
+ f&.close
805
+ end
806
+ end
807
+
808
+ #
809
+ # Retrieves +remotefile+ in whatever mode the session is set (text or
810
+ # binary). See #gettextfile and #getbinaryfile.
811
+ #
812
+ def get(remotefile, localfile = File.basename(remotefile),
813
+ blocksize = DEFAULT_BLOCKSIZE, &block) # :yield: data
814
+ if @binary
815
+ getbinaryfile(remotefile, localfile, blocksize, &block)
816
+ else
817
+ gettextfile(remotefile, localfile, &block)
818
+ end
819
+ end
820
+
821
+ #
822
+ # Transfers +localfile+ to the server in binary mode, storing the result in
823
+ # +remotefile+. If a block is supplied, calls it, passing in the transmitted
824
+ # data in +blocksize+ chunks.
825
+ #
826
+ def putbinaryfile(localfile, remotefile = File.basename(localfile),
827
+ blocksize = DEFAULT_BLOCKSIZE, &block) # :yield: data
828
+ if @resume
829
+ begin
830
+ rest_offset = size(remotefile)
831
+ rescue Net::FTPPermError
832
+ rest_offset = nil
833
+ end
834
+ else
835
+ rest_offset = nil
836
+ end
837
+ f = File.open(localfile)
838
+ begin
839
+ f.binmode
840
+ if rest_offset
841
+ storbinary("APPE #{remotefile}", f, blocksize, rest_offset, &block)
842
+ else
843
+ storbinary("STOR #{remotefile}", f, blocksize, rest_offset, &block)
844
+ end
845
+ ensure
846
+ f.close
847
+ end
848
+ end
849
+
850
+ #
851
+ # Transfers +localfile+ to the server in ASCII (text) mode, storing the result
852
+ # in +remotefile+. If callback or an associated block is supplied, calls it,
853
+ # passing in the transmitted data one line at a time.
854
+ #
855
+ def puttextfile(localfile, remotefile = File.basename(localfile), &block) # :yield: line
856
+ f = File.open(localfile)
857
+ begin
858
+ storlines("STOR #{remotefile}", f, &block)
859
+ ensure
860
+ f.close
861
+ end
862
+ end
863
+
864
+ #
865
+ # Transfers +localfile+ to the server in whatever mode the session is set
866
+ # (text or binary). See #puttextfile and #putbinaryfile.
867
+ #
868
+ def put(localfile, remotefile = File.basename(localfile),
869
+ blocksize = DEFAULT_BLOCKSIZE, &block)
870
+ if @binary
871
+ putbinaryfile(localfile, remotefile, blocksize, &block)
872
+ else
873
+ puttextfile(localfile, remotefile, &block)
874
+ end
875
+ end
876
+
877
+ #
878
+ # Sends the ACCT command.
879
+ #
880
+ # This is a less common FTP command, to send account
881
+ # information if the destination host requires it.
882
+ #
883
+ def acct(account)
884
+ cmd = "ACCT " + account
885
+ voidcmd(cmd)
886
+ end
887
+
888
+ #
889
+ # Returns an array of filenames in the remote directory.
890
+ #
891
+ def nlst(dir = nil)
892
+ cmd = "NLST"
893
+ if dir
894
+ cmd = "#{cmd} #{dir}"
895
+ end
896
+ files = []
897
+ retrlines(cmd) do |line|
898
+ files.push(line)
899
+ end
900
+ return files
901
+ end
902
+
903
+ #
904
+ # Returns an array of file information in the directory (the output is like
905
+ # `ls -l`). If a block is given, it iterates through the listing.
906
+ #
907
+ def list(*args, &block) # :yield: line
908
+ cmd = "LIST"
909
+ args.each do |arg|
910
+ cmd = "#{cmd} #{arg}"
911
+ end
912
+ lines = []
913
+ retrlines(cmd) do |line|
914
+ lines << line
915
+ end
916
+ if block
917
+ lines.each(&block)
918
+ end
919
+ return lines
920
+ end
921
+ alias ls list
922
+ alias dir list
923
+
924
+ #
925
+ # MLSxEntry represents an entry in responses of MLST/MLSD.
926
+ # Each entry has the facts (e.g., size, last modification time, etc.)
927
+ # and the pathname.
928
+ #
929
+ class MLSxEntry
930
+ attr_reader :facts, :pathname
931
+
932
+ def initialize(facts, pathname)
933
+ @facts = facts
934
+ @pathname = pathname
935
+ end
936
+
937
+ standard_facts = %w(size modify create type unique perm
938
+ lang media-type charset)
939
+ standard_facts.each do |factname|
940
+ define_method factname.gsub(/-/, "_") do
941
+ facts[factname]
942
+ end
943
+ end
944
+
945
+ #
946
+ # Returns +true+ if the entry is a file (i.e., the value of the type
947
+ # fact is file).
948
+ #
949
+ def file?
950
+ return facts["type"] == "file"
951
+ end
952
+
953
+ #
954
+ # Returns +true+ if the entry is a directory (i.e., the value of the
955
+ # type fact is dir, cdir, or pdir).
956
+ #
957
+ def directory?
958
+ if /\A[cp]?dir\z/.match(facts["type"])
959
+ return true
960
+ else
961
+ return false
962
+ end
963
+ end
964
+
965
+ #
966
+ # Returns +true+ if the APPE command may be applied to the file.
967
+ #
968
+ def appendable?
969
+ return facts["perm"].include?(?a)
970
+ end
971
+
972
+ #
973
+ # Returns +true+ if files may be created in the directory by STOU,
974
+ # STOR, APPE, and RNTO.
975
+ #
976
+ def creatable?
977
+ return facts["perm"].include?(?c)
978
+ end
979
+
980
+ #
981
+ # Returns +true+ if the file or directory may be deleted by DELE/RMD.
982
+ #
983
+ def deletable?
984
+ return facts["perm"].include?(?d)
985
+ end
986
+
987
+ #
988
+ # Returns +true+ if the directory may be entered by CWD/CDUP.
989
+ #
990
+ def enterable?
991
+ return facts["perm"].include?(?e)
992
+ end
993
+
994
+ #
995
+ # Returns +true+ if the file or directory may be renamed by RNFR.
996
+ #
997
+ def renamable?
998
+ return facts["perm"].include?(?f)
999
+ end
1000
+
1001
+ #
1002
+ # Returns +true+ if the listing commands, LIST, NLST, and MLSD are
1003
+ # applied to the directory.
1004
+ #
1005
+ def listable?
1006
+ return facts["perm"].include?(?l)
1007
+ end
1008
+
1009
+ #
1010
+ # Returns +true+ if the MKD command may be used to create a new
1011
+ # directory within the directory.
1012
+ #
1013
+ def directory_makable?
1014
+ return facts["perm"].include?(?m)
1015
+ end
1016
+
1017
+ #
1018
+ # Returns +true+ if the objects in the directory may be deleted, or
1019
+ # the directory may be purged.
1020
+ #
1021
+ def purgeable?
1022
+ return facts["perm"].include?(?p)
1023
+ end
1024
+
1025
+ #
1026
+ # Returns +true+ if the RETR command may be applied to the file.
1027
+ #
1028
+ def readable?
1029
+ return facts["perm"].include?(?r)
1030
+ end
1031
+
1032
+ #
1033
+ # Returns +true+ if the STOR command may be applied to the file.
1034
+ #
1035
+ def writable?
1036
+ return facts["perm"].include?(?w)
1037
+ end
1038
+ end
1039
+
1040
+ CASE_DEPENDENT_PARSER = ->(value) { value }
1041
+ CASE_INDEPENDENT_PARSER = ->(value) { value.downcase }
1042
+ DECIMAL_PARSER = ->(value) { value.to_i }
1043
+ OCTAL_PARSER = ->(value) { value.to_i(8) }
1044
+ TIME_PARSER = ->(value, local = false) {
1045
+ unless /\A(?<year>\d{4})(?<month>\d{2})(?<day>\d{2})
1046
+ (?<hour>\d{2})(?<min>\d{2})(?<sec>\d{2})
1047
+ (?:\.(?<fractions>\d+))?/x =~ value
1048
+ raise FTPProtoError, "invalid time-val: #{value}"
1049
+ end
1050
+ usec = fractions.to_i * 10 ** (6 - fractions.to_s.size)
1051
+ Time.send(local ? :local : :utc, year, month, day, hour, min, sec, usec)
1052
+ }
1053
+ FACT_PARSERS = Hash.new(CASE_DEPENDENT_PARSER)
1054
+ FACT_PARSERS["size"] = DECIMAL_PARSER
1055
+ FACT_PARSERS["modify"] = TIME_PARSER
1056
+ FACT_PARSERS["create"] = TIME_PARSER
1057
+ FACT_PARSERS["type"] = CASE_INDEPENDENT_PARSER
1058
+ FACT_PARSERS["unique"] = CASE_DEPENDENT_PARSER
1059
+ FACT_PARSERS["perm"] = CASE_INDEPENDENT_PARSER
1060
+ FACT_PARSERS["lang"] = CASE_INDEPENDENT_PARSER
1061
+ FACT_PARSERS["media-type"] = CASE_INDEPENDENT_PARSER
1062
+ FACT_PARSERS["charset"] = CASE_INDEPENDENT_PARSER
1063
+ FACT_PARSERS["unix.mode"] = OCTAL_PARSER
1064
+ FACT_PARSERS["unix.owner"] = DECIMAL_PARSER
1065
+ FACT_PARSERS["unix.group"] = DECIMAL_PARSER
1066
+ FACT_PARSERS["unix.ctime"] = TIME_PARSER
1067
+ FACT_PARSERS["unix.atime"] = TIME_PARSER
1068
+
1069
+ def parse_mlsx_entry(entry)
1070
+ facts, pathname = entry.chomp.split(/ /, 2)
1071
+ unless pathname
1072
+ raise FTPProtoError, entry
1073
+ end
1074
+ return MLSxEntry.new(
1075
+ facts.scan(/(.*?)=(.*?);/).each_with_object({}) {
1076
+ |(factname, value), h|
1077
+ name = factname.downcase
1078
+ h[name] = FACT_PARSERS[name].(value)
1079
+ },
1080
+ pathname)
1081
+ end
1082
+ private :parse_mlsx_entry
1083
+
1084
+ #
1085
+ # Returns data (e.g., size, last modification time, entry type, etc.)
1086
+ # about the file or directory specified by +pathname+.
1087
+ # If +pathname+ is omitted, the current directory is assumed.
1088
+ #
1089
+ def mlst(pathname = nil)
1090
+ cmd = pathname ? "MLST #{pathname}" : "MLST"
1091
+ resp = sendcmd(cmd)
1092
+ if !resp.start_with?("250")
1093
+ raise FTPReplyError, resp
1094
+ end
1095
+ line = resp.lines[1]
1096
+ unless line
1097
+ raise FTPProtoError, resp
1098
+ end
1099
+ entry = line.sub(/\A(250-| *)/, "")
1100
+ return parse_mlsx_entry(entry)
1101
+ end
1102
+
1103
+ #
1104
+ # Returns an array of the entries of the directory specified by
1105
+ # +pathname+.
1106
+ # Each entry has the facts (e.g., size, last modification time, etc.)
1107
+ # and the pathname.
1108
+ # If a block is given, it iterates through the listing.
1109
+ # If +pathname+ is omitted, the current directory is assumed.
1110
+ #
1111
+ def mlsd(pathname = nil, &block) # :yield: entry
1112
+ cmd = pathname ? "MLSD #{pathname}" : "MLSD"
1113
+ entries = []
1114
+ retrlines(cmd) do |line|
1115
+ entries << parse_mlsx_entry(line)
1116
+ end
1117
+ if block
1118
+ entries.each(&block)
1119
+ end
1120
+ return entries
1121
+ end
1122
+
1123
+ #
1124
+ # Renames a file on the server.
1125
+ #
1126
+ def rename(fromname, toname)
1127
+ resp = sendcmd("RNFR #{fromname}")
1128
+ if !resp.start_with?("3")
1129
+ raise FTPReplyError, resp
1130
+ end
1131
+ voidcmd("RNTO #{toname}")
1132
+ end
1133
+
1134
+ #
1135
+ # Deletes a file on the server.
1136
+ #
1137
+ def delete(filename)
1138
+ resp = sendcmd("DELE #{filename}")
1139
+ if resp.start_with?("250")
1140
+ return
1141
+ elsif resp.start_with?("5")
1142
+ raise FTPPermError, resp
1143
+ else
1144
+ raise FTPReplyError, resp
1145
+ end
1146
+ end
1147
+
1148
+ #
1149
+ # Changes the (remote) directory.
1150
+ #
1151
+ def chdir(dirname)
1152
+ if dirname == ".."
1153
+ begin
1154
+ voidcmd("CDUP")
1155
+ return
1156
+ rescue FTPPermError => e
1157
+ if e.message[0, 3] != "500"
1158
+ raise e
1159
+ end
1160
+ end
1161
+ end
1162
+ cmd = "CWD #{dirname}"
1163
+ voidcmd(cmd)
1164
+ end
1165
+
1166
+ def get_body(resp) # :nodoc:
1167
+ resp.slice(/\A[0-9a-zA-Z]{3} (.*)$/, 1)
1168
+ end
1169
+ private :get_body
1170
+
1171
+ #
1172
+ # Returns the size of the given (remote) filename.
1173
+ #
1174
+ def size(filename)
1175
+ with_binary(true) do
1176
+ resp = sendcmd("SIZE #{filename}")
1177
+ if !resp.start_with?("213")
1178
+ raise FTPReplyError, resp
1179
+ end
1180
+ return get_body(resp).to_i
1181
+ end
1182
+ end
1183
+
1184
+ #
1185
+ # Returns the last modification time of the (remote) file. If +local+ is
1186
+ # +true+, it is returned as a local time, otherwise it's a UTC time.
1187
+ #
1188
+ def mtime(filename, local = false)
1189
+ return TIME_PARSER.(mdtm(filename), local)
1190
+ end
1191
+
1192
+ #
1193
+ # Creates a remote directory.
1194
+ #
1195
+ def mkdir(dirname)
1196
+ resp = sendcmd("MKD #{dirname}")
1197
+ return parse257(resp)
1198
+ end
1199
+
1200
+ #
1201
+ # Removes a remote directory.
1202
+ #
1203
+ def rmdir(dirname)
1204
+ voidcmd("RMD #{dirname}")
1205
+ end
1206
+
1207
+ #
1208
+ # Returns the current remote directory.
1209
+ #
1210
+ def pwd
1211
+ resp = sendcmd("PWD")
1212
+ return parse257(resp)
1213
+ end
1214
+ alias getdir pwd
1215
+
1216
+ #
1217
+ # Returns system information.
1218
+ #
1219
+ def system
1220
+ resp = sendcmd("SYST")
1221
+ if !resp.start_with?("215")
1222
+ raise FTPReplyError, resp
1223
+ end
1224
+ return get_body(resp)
1225
+ end
1226
+
1227
+ #
1228
+ # Aborts the previous command (ABOR command).
1229
+ #
1230
+ def abort
1231
+ line = "ABOR" + CRLF
1232
+ print "put: ABOR\n" if @debug_mode
1233
+ @sock.send(line, Socket::MSG_OOB)
1234
+ resp = getmultiline
1235
+ unless ["426", "226", "225"].include?(resp[0, 3])
1236
+ raise FTPProtoError, resp
1237
+ end
1238
+ return resp
1239
+ end
1240
+
1241
+ #
1242
+ # Returns the status (STAT command).
1243
+ #
1244
+ # pathname:: when stat is invoked with pathname as a parameter it acts like
1245
+ # list but a lot faster and over the same tcp session.
1246
+ #
1247
+ def status(pathname = nil)
1248
+ line = pathname ? "STAT #{pathname}" : "STAT"
1249
+ if /[\r\n]/ =~ line
1250
+ raise ArgumentError, "A line must not contain CR or LF"
1251
+ end
1252
+ print "put: #{line}\n" if @debug_mode
1253
+ @sock.send(line + CRLF, Socket::MSG_OOB)
1254
+ return getresp
1255
+ end
1256
+
1257
+ #
1258
+ # Returns the raw last modification time of the (remote) file in the format
1259
+ # "YYYYMMDDhhmmss" (MDTM command).
1260
+ #
1261
+ # Use +mtime+ if you want a parsed Time instance.
1262
+ #
1263
+ def mdtm(filename)
1264
+ resp = sendcmd("MDTM #{filename}")
1265
+ if resp.start_with?("213")
1266
+ return get_body(resp)
1267
+ end
1268
+ end
1269
+
1270
+ #
1271
+ # Issues the HELP command.
1272
+ #
1273
+ def help(arg = nil)
1274
+ cmd = "HELP"
1275
+ if arg
1276
+ cmd = cmd + " " + arg
1277
+ end
1278
+ sendcmd(cmd)
1279
+ end
1280
+
1281
+ #
1282
+ # Exits the FTP session.
1283
+ #
1284
+ def quit
1285
+ voidcmd("QUIT")
1286
+ end
1287
+
1288
+ #
1289
+ # Issues a NOOP command.
1290
+ #
1291
+ # Does nothing except return a response.
1292
+ #
1293
+ def noop
1294
+ voidcmd("NOOP")
1295
+ end
1296
+
1297
+ #
1298
+ # Issues a SITE command.
1299
+ #
1300
+ def site(arg)
1301
+ cmd = "SITE " + arg
1302
+ voidcmd(cmd)
1303
+ end
1304
+
1305
+ #
1306
+ # Issues a FEAT command
1307
+ #
1308
+ # Returns an array of supported optional features
1309
+ #
1310
+ def features
1311
+ resp = sendcmd("FEAT")
1312
+ if !resp.start_with?("211")
1313
+ raise FTPReplyError, resp
1314
+ end
1315
+
1316
+ feats = []
1317
+ resp.split("\n").each do |line|
1318
+ next if !line.start_with?(' ') # skip status lines
1319
+
1320
+ feats << line.strip
1321
+ end
1322
+
1323
+ return feats
1324
+ end
1325
+
1326
+ #
1327
+ # Issues an OPTS command
1328
+ # - name Should be the name of the option to set
1329
+ # - params is any optional parameters to supply with the option
1330
+ #
1331
+ # example: option('UTF8', 'ON') => 'OPTS UTF8 ON'
1332
+ #
1333
+ def option(name, params = nil)
1334
+ cmd = "OPTS #{name}"
1335
+ cmd += " #{params}" if params
1336
+
1337
+ voidcmd(cmd)
1338
+ end
1339
+
1340
+ #
1341
+ # Closes the connection. Further operations are impossible until you open
1342
+ # a new connection with #connect.
1343
+ #
1344
+ def close
1345
+ if @sock and not @sock.closed?
1346
+ begin
1347
+ @sock.shutdown(Socket::SHUT_WR) rescue nil
1348
+ orig, self.read_timeout = self.read_timeout, 3
1349
+ @sock.read rescue nil
1350
+ ensure
1351
+ @sock.close
1352
+ self.read_timeout = orig
1353
+ end
1354
+ end
1355
+ end
1356
+
1357
+ #
1358
+ # Returns +true+ iff the connection is closed.
1359
+ #
1360
+ def closed?
1361
+ @sock == nil or @sock.closed?
1362
+ end
1363
+
1364
+ # handler for response code 227
1365
+ # (Entering Passive Mode (h1,h2,h3,h4,p1,p2))
1366
+ #
1367
+ # Returns host and port.
1368
+ def parse227(resp) # :nodoc:
1369
+ if !resp.start_with?("227")
1370
+ raise FTPReplyError, resp
1371
+ end
1372
+ if m = /\((?<host>\d+(?:,\d+){3}),(?<port>\d+,\d+)\)/.match(resp)
1373
+ return parse_pasv_ipv4_host(m["host"]), parse_pasv_port(m["port"])
1374
+ else
1375
+ raise FTPProtoError, resp
1376
+ end
1377
+ end
1378
+ private :parse227
1379
+
1380
+ # handler for response code 228
1381
+ # (Entering Long Passive Mode)
1382
+ #
1383
+ # Returns host and port.
1384
+ def parse228(resp) # :nodoc:
1385
+ if !resp.start_with?("228")
1386
+ raise FTPReplyError, resp
1387
+ end
1388
+ if m = /\(4,4,(?<host>\d+(?:,\d+){3}),2,(?<port>\d+,\d+)\)/.match(resp)
1389
+ return parse_pasv_ipv4_host(m["host"]), parse_pasv_port(m["port"])
1390
+ elsif m = /\(6,16,(?<host>\d+(?:,\d+){15}),2,(?<port>\d+,\d+)\)/.match(resp)
1391
+ return parse_pasv_ipv6_host(m["host"]), parse_pasv_port(m["port"])
1392
+ else
1393
+ raise FTPProtoError, resp
1394
+ end
1395
+ end
1396
+ private :parse228
1397
+
1398
+ def parse_pasv_ipv4_host(s)
1399
+ return s.tr(",", ".")
1400
+ end
1401
+ private :parse_pasv_ipv4_host
1402
+
1403
+ def parse_pasv_ipv6_host(s)
1404
+ return s.split(/,/).map { |i|
1405
+ "%02x" % i.to_i
1406
+ }.each_slice(2).map(&:join).join(":")
1407
+ end
1408
+ private :parse_pasv_ipv6_host
1409
+
1410
+ def parse_pasv_port(s)
1411
+ return s.split(/,/).map(&:to_i).inject { |x, y|
1412
+ (x << 8) + y
1413
+ }
1414
+ end
1415
+ private :parse_pasv_port
1416
+
1417
+ # handler for response code 229
1418
+ # (Extended Passive Mode Entered)
1419
+ #
1420
+ # Returns host and port.
1421
+ def parse229(resp) # :nodoc:
1422
+ if !resp.start_with?("229")
1423
+ raise FTPReplyError, resp
1424
+ end
1425
+ if m = /\((?<d>[!-~])\k<d>\k<d>(?<port>\d+)\k<d>\)/.match(resp)
1426
+ return @bare_sock.remote_address.ip_address, m["port"].to_i
1427
+ else
1428
+ raise FTPProtoError, resp
1429
+ end
1430
+ end
1431
+ private :parse229
1432
+
1433
+ # handler for response code 257
1434
+ # ("PATHNAME" created)
1435
+ #
1436
+ # Returns host and port.
1437
+ def parse257(resp) # :nodoc:
1438
+ if !resp.start_with?("257")
1439
+ raise FTPReplyError, resp
1440
+ end
1441
+ return resp.slice(/"(([^"]|"")*)"/, 1).to_s.gsub(/""/, '"')
1442
+ end
1443
+ private :parse257
1444
+
1445
+ # :stopdoc:
1446
+ class NullSocket
1447
+ def read_timeout=(sec)
1448
+ end
1449
+
1450
+ def closed?
1451
+ true
1452
+ end
1453
+
1454
+ def close
1455
+ end
1456
+
1457
+ def method_missing(mid, *args)
1458
+ raise FTPConnectionError, "not connected"
1459
+ end
1460
+ end
1461
+
1462
+ class BufferedSocket < BufferedIO
1463
+ [:local_address, :remote_address, :addr, :peeraddr, :send, :shutdown].each do |method|
1464
+ define_method(method) { |*args|
1465
+ @io.__send__(method, *args)
1466
+ }
1467
+ end
1468
+
1469
+ def read(len = nil)
1470
+ if len
1471
+ s = super(len, String.new, true)
1472
+ return s.empty? ? nil : s
1473
+ else
1474
+ result = String.new
1475
+ while s = super(DEFAULT_BLOCKSIZE, String.new, true)
1476
+ break if s.empty?
1477
+ result << s
1478
+ end
1479
+ return result
1480
+ end
1481
+ end
1482
+
1483
+ def gets
1484
+ line = readuntil("\n", true)
1485
+ return line.empty? ? nil : line
1486
+ end
1487
+
1488
+ def readline
1489
+ line = gets
1490
+ if line.nil?
1491
+ raise EOFError, "end of file reached"
1492
+ end
1493
+ return line
1494
+ end
1495
+ end
1496
+
1497
+ if defined?(OpenSSL::SSL::SSLSocket)
1498
+ class BufferedSSLSocket < BufferedSocket
1499
+ def initialize(*args, **options)
1500
+ super
1501
+ @is_shutdown = false
1502
+ end
1503
+
1504
+ def shutdown(*args)
1505
+ # SSL_shutdown() will be called from SSLSocket#close, and
1506
+ # SSL_shutdown() will send the "close notify" alert to the peer,
1507
+ # so shutdown(2) should not be called.
1508
+ @is_shutdown = true
1509
+ end
1510
+
1511
+ def send(mesg, flags, dest = nil)
1512
+ # Ignore flags and dest.
1513
+ @io.write(mesg)
1514
+ end
1515
+
1516
+ private
1517
+
1518
+ def rbuf_fill
1519
+ if @is_shutdown
1520
+ raise EOFError, "shutdown has been called"
1521
+ else
1522
+ super
1523
+ end
1524
+ end
1525
+ end
1526
+ end
1527
+ # :startdoc:
1528
+ end
1529
+ end
1530
+
1531
+
1532
+ # Documentation comments:
1533
+ # - sourced from pickaxe and nutshell, with improvements (hopefully)