net-ftp 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,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)