/tools/Ruby/lib/ruby/1.8/open-uri.rb

http://github.com/agross/netopenspace · Ruby · 678 lines · 417 code · 44 blank · 217 comment · 80 complexity · 6fa3faa13fbfc6ea54afde7d32ed8e5c MD5 · raw file

  1. require 'uri'
  2. require 'stringio'
  3. require 'time'
  4. module Kernel
  5. private
  6. alias open_uri_original_open open # :nodoc:
  7. # makes possible to open various resources including URIs.
  8. # If the first argument respond to `open' method,
  9. # the method is called with the rest arguments.
  10. #
  11. # If the first argument is a string which begins with xxx://,
  12. # it is parsed by URI.parse. If the parsed object respond to `open' method,
  13. # the method is called with the rest arguments.
  14. #
  15. # Otherwise original open is called.
  16. #
  17. # Since open-uri.rb provides URI::HTTP#open, URI::HTTPS#open and
  18. # URI::FTP#open,
  19. # Kernel[#.]open can accepts such URIs and strings which begins with
  20. # http://, https:// and ftp://.
  21. # In these case, the opened file object is extended by OpenURI::Meta.
  22. def open(name, *rest, &block) # :doc:
  23. if name.respond_to?(:open)
  24. name.open(*rest, &block)
  25. elsif name.respond_to?(:to_str) &&
  26. %r{\A[A-Za-z][A-Za-z0-9+\-\.]*://} =~ name &&
  27. (uri = URI.parse(name)).respond_to?(:open)
  28. uri.open(*rest, &block)
  29. else
  30. open_uri_original_open(name, *rest, &block)
  31. end
  32. end
  33. module_function :open
  34. end
  35. # OpenURI is an easy-to-use wrapper for net/http, net/https and net/ftp.
  36. #
  37. #== Example
  38. #
  39. # It is possible to open http/https/ftp URL as usual like opening a file:
  40. #
  41. # open("http://www.ruby-lang.org/") {|f|
  42. # f.each_line {|line| p line}
  43. # }
  44. #
  45. # The opened file has several methods for meta information as follows since
  46. # it is extended by OpenURI::Meta.
  47. #
  48. # open("http://www.ruby-lang.org/en") {|f|
  49. # f.each_line {|line| p line}
  50. # p f.base_uri # <URI::HTTP:0x40e6ef2 URL:http://www.ruby-lang.org/en/>
  51. # p f.content_type # "text/html"
  52. # p f.charset # "iso-8859-1"
  53. # p f.content_encoding # []
  54. # p f.last_modified # Thu Dec 05 02:45:02 UTC 2002
  55. # }
  56. #
  57. # Additional header fields can be specified by an optional hash argument.
  58. #
  59. # open("http://www.ruby-lang.org/en/",
  60. # "User-Agent" => "Ruby/#{RUBY_VERSION}",
  61. # "From" => "foo@bar.invalid",
  62. # "Referer" => "http://www.ruby-lang.org/") {|f|
  63. # # ...
  64. # }
  65. #
  66. # The environment variables such as http_proxy, https_proxy and ftp_proxy
  67. # are in effect by default. :proxy => nil disables proxy.
  68. #
  69. # open("http://www.ruby-lang.org/en/raa.html", :proxy => nil) {|f|
  70. # # ...
  71. # }
  72. #
  73. # URI objects can be opened in a similar way.
  74. #
  75. # uri = URI.parse("http://www.ruby-lang.org/en/")
  76. # uri.open {|f|
  77. # # ...
  78. # }
  79. #
  80. # URI objects can be read directly. The returned string is also extended by
  81. # OpenURI::Meta.
  82. #
  83. # str = uri.read
  84. # p str.base_uri
  85. #
  86. # Author:: Tanaka Akira <akr@m17n.org>
  87. module OpenURI
  88. Options = {
  89. :proxy => true,
  90. :progress_proc => true,
  91. :content_length_proc => true,
  92. :http_basic_authentication => true,
  93. }
  94. def OpenURI.check_options(options) # :nodoc:
  95. options.each {|k, v|
  96. next unless Symbol === k
  97. unless Options.include? k
  98. raise ArgumentError, "unrecognized option: #{k}"
  99. end
  100. }
  101. end
  102. def OpenURI.scan_open_optional_arguments(*rest) # :nodoc:
  103. if !rest.empty? && (String === rest.first || Integer === rest.first)
  104. mode = rest.shift
  105. if !rest.empty? && Integer === rest.first
  106. perm = rest.shift
  107. end
  108. end
  109. return mode, perm, rest
  110. end
  111. def OpenURI.open_uri(name, *rest) # :nodoc:
  112. uri = URI::Generic === name ? name : URI.parse(name)
  113. mode, perm, rest = OpenURI.scan_open_optional_arguments(*rest)
  114. options = rest.shift if !rest.empty? && Hash === rest.first
  115. raise ArgumentError.new("extra arguments") if !rest.empty?
  116. options ||= {}
  117. OpenURI.check_options(options)
  118. unless mode == nil ||
  119. mode == 'r' || mode == 'rb' ||
  120. mode == File::RDONLY
  121. raise ArgumentError.new("invalid access mode #{mode} (#{uri.class} resource is read only.)")
  122. end
  123. io = open_loop(uri, options)
  124. if block_given?
  125. begin
  126. yield io
  127. ensure
  128. io.close
  129. end
  130. else
  131. io
  132. end
  133. end
  134. def OpenURI.open_loop(uri, options) # :nodoc:
  135. case opt_proxy = options.fetch(:proxy, true)
  136. when true
  137. find_proxy = lambda {|u| u.find_proxy}
  138. when nil, false
  139. find_proxy = lambda {|u| nil}
  140. when String
  141. opt_proxy = URI.parse(opt_proxy)
  142. find_proxy = lambda {|u| opt_proxy}
  143. when URI::Generic
  144. find_proxy = lambda {|u| opt_proxy}
  145. else
  146. raise ArgumentError.new("Invalid proxy option: #{opt_proxy}")
  147. end
  148. uri_set = {}
  149. buf = nil
  150. while true
  151. redirect = catch(:open_uri_redirect) {
  152. buf = Buffer.new
  153. uri.buffer_open(buf, find_proxy.call(uri), options)
  154. nil
  155. }
  156. if redirect
  157. if redirect.relative?
  158. # Although it violates RFC2616, Location: field may have relative
  159. # URI. It is converted to absolute URI using uri as a base URI.
  160. redirect = uri + redirect
  161. end
  162. unless OpenURI.redirectable?(uri, redirect)
  163. raise "redirection forbidden: #{uri} -> #{redirect}"
  164. end
  165. if options.include? :http_basic_authentication
  166. # send authentication only for the URI directly specified.
  167. options = options.dup
  168. options.delete :http_basic_authentication
  169. end
  170. uri = redirect
  171. raise "HTTP redirection loop: #{uri}" if uri_set.include? uri.to_s
  172. uri_set[uri.to_s] = true
  173. else
  174. break
  175. end
  176. end
  177. io = buf.io
  178. io.base_uri = uri
  179. io
  180. end
  181. def OpenURI.redirectable?(uri1, uri2) # :nodoc:
  182. # This test is intended to forbid a redirection from http://... to
  183. # file:///etc/passwd.
  184. # However this is ad hoc. It should be extensible/configurable.
  185. uri1.scheme.downcase == uri2.scheme.downcase ||
  186. (/\A(?:http|ftp)\z/i =~ uri1.scheme && /\A(?:http|ftp)\z/i =~ uri2.scheme)
  187. end
  188. def OpenURI.open_http(buf, target, proxy, options) # :nodoc:
  189. if proxy
  190. raise "Non-HTTP proxy URI: #{proxy}" if proxy.class != URI::HTTP
  191. end
  192. if target.userinfo && "1.9.0" <= RUBY_VERSION
  193. # don't raise for 1.8 because compatibility.
  194. raise ArgumentError, "userinfo not supported. [RFC3986]"
  195. end
  196. require 'net/http'
  197. klass = Net::HTTP
  198. if URI::HTTP === target
  199. # HTTP or HTTPS
  200. if proxy
  201. klass = Net::HTTP::Proxy(proxy.host, proxy.port)
  202. end
  203. target_host = target.host
  204. target_port = target.port
  205. request_uri = target.request_uri
  206. else
  207. # FTP over HTTP proxy
  208. target_host = proxy.host
  209. target_port = proxy.port
  210. request_uri = target.to_s
  211. end
  212. http = klass.new(target_host, target_port)
  213. if target.class == URI::HTTPS
  214. require 'net/https'
  215. http.use_ssl = true
  216. http.verify_mode = OpenSSL::SSL::VERIFY_PEER
  217. store = OpenSSL::X509::Store.new
  218. store.set_default_paths
  219. http.cert_store = store
  220. end
  221. header = {}
  222. options.each {|k, v| header[k] = v if String === k }
  223. resp = nil
  224. http.start {
  225. req = Net::HTTP::Get.new(request_uri, header)
  226. if options.include? :http_basic_authentication
  227. user, pass = options[:http_basic_authentication]
  228. req.basic_auth user, pass
  229. end
  230. http.request(req) {|response|
  231. resp = response
  232. if options[:content_length_proc] && Net::HTTPSuccess === resp
  233. if resp.key?('Content-Length')
  234. options[:content_length_proc].call(resp['Content-Length'].to_i)
  235. else
  236. options[:content_length_proc].call(nil)
  237. end
  238. end
  239. resp.read_body {|str|
  240. buf << str
  241. if options[:progress_proc] && Net::HTTPSuccess === resp
  242. options[:progress_proc].call(buf.size)
  243. end
  244. }
  245. }
  246. }
  247. io = buf.io
  248. io.rewind
  249. io.status = [resp.code, resp.message]
  250. resp.each {|name,value| buf.io.meta_add_field name, value }
  251. case resp
  252. when Net::HTTPSuccess
  253. when Net::HTTPMovedPermanently, # 301
  254. Net::HTTPFound, # 302
  255. Net::HTTPSeeOther, # 303
  256. Net::HTTPTemporaryRedirect # 307
  257. throw :open_uri_redirect, URI.parse(resp['location'])
  258. else
  259. raise OpenURI::HTTPError.new(io.status.join(' '), io)
  260. end
  261. end
  262. class HTTPError < StandardError
  263. def initialize(message, io)
  264. super(message)
  265. @io = io
  266. end
  267. attr_reader :io
  268. end
  269. class Buffer # :nodoc:
  270. def initialize
  271. @io = StringIO.new
  272. @size = 0
  273. end
  274. attr_reader :size
  275. StringMax = 10240
  276. def <<(str)
  277. @io << str
  278. @size += str.length
  279. if StringIO === @io && StringMax < @size
  280. require 'tempfile'
  281. io = Tempfile.new('open-uri')
  282. io.binmode
  283. Meta.init io, @io if @io.respond_to? :meta
  284. io << @io.string
  285. @io = io
  286. end
  287. end
  288. def io
  289. Meta.init @io unless @io.respond_to? :meta
  290. @io
  291. end
  292. end
  293. # Mixin for holding meta-information.
  294. module Meta
  295. def Meta.init(obj, src=nil) # :nodoc:
  296. obj.extend Meta
  297. obj.instance_eval {
  298. @base_uri = nil
  299. @meta = {}
  300. }
  301. if src
  302. obj.status = src.status
  303. obj.base_uri = src.base_uri
  304. src.meta.each {|name, value|
  305. obj.meta_add_field(name, value)
  306. }
  307. end
  308. end
  309. # returns an Array which consists status code and message.
  310. attr_accessor :status
  311. # returns a URI which is base of relative URIs in the data.
  312. # It may differ from the URI supplied by a user because redirection.
  313. attr_accessor :base_uri
  314. # returns a Hash which represents header fields.
  315. # The Hash keys are downcased for canonicalization.
  316. attr_reader :meta
  317. def meta_add_field(name, value) # :nodoc:
  318. @meta[name.downcase] = value
  319. end
  320. # returns a Time which represents Last-Modified field.
  321. def last_modified
  322. if v = @meta['last-modified']
  323. Time.httpdate(v)
  324. else
  325. nil
  326. end
  327. end
  328. RE_LWS = /[\r\n\t ]+/n
  329. RE_TOKEN = %r{[^\x00- ()<>@,;:\\"/\[\]?={}\x7f]+}n
  330. RE_QUOTED_STRING = %r{"(?:[\r\n\t !#-\[\]-~\x80-\xff]|\\[\x00-\x7f])*"}n
  331. RE_PARAMETERS = %r{(?:;#{RE_LWS}?#{RE_TOKEN}#{RE_LWS}?=#{RE_LWS}?(?:#{RE_TOKEN}|#{RE_QUOTED_STRING})#{RE_LWS}?)*}n
  332. def content_type_parse # :nodoc:
  333. v = @meta['content-type']
  334. # The last (?:;#{RE_LWS}?)? matches extra ";" which violates RFC2045.
  335. if v && %r{\A#{RE_LWS}?(#{RE_TOKEN})#{RE_LWS}?/(#{RE_TOKEN})#{RE_LWS}?(#{RE_PARAMETERS})(?:;#{RE_LWS}?)?\z}no =~ v
  336. type = $1.downcase
  337. subtype = $2.downcase
  338. parameters = []
  339. $3.scan(/;#{RE_LWS}?(#{RE_TOKEN})#{RE_LWS}?=#{RE_LWS}?(?:(#{RE_TOKEN})|(#{RE_QUOTED_STRING}))/no) {|att, val, qval|
  340. val = qval.gsub(/[\r\n\t !#-\[\]-~\x80-\xff]+|(\\[\x00-\x7f])/) { $1 ? $1[1,1] : $& } if qval
  341. parameters << [att.downcase, val]
  342. }
  343. ["#{type}/#{subtype}", *parameters]
  344. else
  345. nil
  346. end
  347. end
  348. # returns "type/subtype" which is MIME Content-Type.
  349. # It is downcased for canonicalization.
  350. # Content-Type parameters are stripped.
  351. def content_type
  352. type, *parameters = content_type_parse
  353. type || 'application/octet-stream'
  354. end
  355. # returns a charset parameter in Content-Type field.
  356. # It is downcased for canonicalization.
  357. #
  358. # If charset parameter is not given but a block is given,
  359. # the block is called and its result is returned.
  360. # It can be used to guess charset.
  361. #
  362. # If charset parameter and block is not given,
  363. # nil is returned except text type in HTTP.
  364. # In that case, "iso-8859-1" is returned as defined by RFC2616 3.7.1.
  365. def charset
  366. type, *parameters = content_type_parse
  367. if pair = parameters.assoc('charset')
  368. pair.last.downcase
  369. elsif block_given?
  370. yield
  371. elsif type && %r{\Atext/} =~ type &&
  372. @base_uri && /\Ahttp\z/i =~ @base_uri.scheme
  373. "iso-8859-1" # RFC2616 3.7.1
  374. else
  375. nil
  376. end
  377. end
  378. # returns a list of encodings in Content-Encoding field
  379. # as an Array of String.
  380. # The encodings are downcased for canonicalization.
  381. def content_encoding
  382. v = @meta['content-encoding']
  383. if v && %r{\A#{RE_LWS}?#{RE_TOKEN}#{RE_LWS}?(?:,#{RE_LWS}?#{RE_TOKEN}#{RE_LWS}?)*}o =~ v
  384. v.scan(RE_TOKEN).map {|content_coding| content_coding.downcase}
  385. else
  386. []
  387. end
  388. end
  389. end
  390. # Mixin for HTTP and FTP URIs.
  391. module OpenRead
  392. # OpenURI::OpenRead#open provides `open' for URI::HTTP and URI::FTP.
  393. #
  394. # OpenURI::OpenRead#open takes optional 3 arguments as:
  395. # OpenURI::OpenRead#open([mode [, perm]] [, options]) [{|io| ... }]
  396. #
  397. # `mode', `perm' is same as Kernel#open.
  398. #
  399. # However, `mode' must be read mode because OpenURI::OpenRead#open doesn't
  400. # support write mode (yet).
  401. # Also `perm' is just ignored because it is meaningful only for file
  402. # creation.
  403. #
  404. # `options' must be a hash.
  405. #
  406. # Each pairs which key is a string in the hash specify a extra header
  407. # field for HTTP.
  408. # I.e. it is ignored for FTP without HTTP proxy.
  409. #
  410. # The hash may include other options which key is a symbol:
  411. #
  412. # [:proxy]
  413. # Synopsis:
  414. # :proxy => "http://proxy.foo.com:8000/"
  415. # :proxy => URI.parse("http://proxy.foo.com:8000/")
  416. # :proxy => true
  417. # :proxy => false
  418. # :proxy => nil
  419. #
  420. # If :proxy option is specified, the value should be String, URI,
  421. # boolean or nil.
  422. # When String or URI is given, it is treated as proxy URI.
  423. # When true is given or the option itself is not specified,
  424. # environment variable `scheme_proxy' is examined.
  425. # `scheme' is replaced by `http', `https' or `ftp'.
  426. # When false or nil is given, the environment variables are ignored and
  427. # connection will be made to a server directly.
  428. #
  429. # [:http_basic_authentication]
  430. # Synopsis:
  431. # :http_basic_authentication=>[user, password]
  432. #
  433. # If :http_basic_authentication is specified,
  434. # the value should be an array which contains 2 strings:
  435. # username and password.
  436. # It is used for HTTP Basic authentication defined by RFC 2617.
  437. #
  438. # [:content_length_proc]
  439. # Synopsis:
  440. # :content_length_proc => lambda {|content_length| ... }
  441. #
  442. # If :content_length_proc option is specified, the option value procedure
  443. # is called before actual transfer is started.
  444. # It takes one argument which is expected content length in bytes.
  445. #
  446. # If two or more transfer is done by HTTP redirection, the procedure
  447. # is called only one for a last transfer.
  448. #
  449. # When expected content length is unknown, the procedure is called with
  450. # nil.
  451. # It is happen when HTTP response has no Content-Length header.
  452. #
  453. # [:progress_proc]
  454. # Synopsis:
  455. # :progress_proc => lambda {|size| ...}
  456. #
  457. # If :progress_proc option is specified, the proc is called with one
  458. # argument each time when `open' gets content fragment from network.
  459. # The argument `size' `size' is a accumulated transfered size in bytes.
  460. #
  461. # If two or more transfer is done by HTTP redirection, the procedure
  462. # is called only one for a last transfer.
  463. #
  464. # :progress_proc and :content_length_proc are intended to be used for
  465. # progress bar.
  466. # For example, it can be implemented as follows using Ruby/ProgressBar.
  467. #
  468. # pbar = nil
  469. # open("http://...",
  470. # :content_length_proc => lambda {|t|
  471. # if t && 0 < t
  472. # pbar = ProgressBar.new("...", t)
  473. # pbar.file_transfer_mode
  474. # end
  475. # },
  476. # :progress_proc => lambda {|s|
  477. # pbar.set s if pbar
  478. # }) {|f| ... }
  479. #
  480. # OpenURI::OpenRead#open returns an IO like object if block is not given.
  481. # Otherwise it yields the IO object and return the value of the block.
  482. # The IO object is extended with OpenURI::Meta.
  483. def open(*rest, &block)
  484. OpenURI.open_uri(self, *rest, &block)
  485. end
  486. # OpenURI::OpenRead#read([options]) reads a content referenced by self and
  487. # returns the content as string.
  488. # The string is extended with OpenURI::Meta.
  489. # The argument `options' is same as OpenURI::OpenRead#open.
  490. def read(options={})
  491. self.open(options) {|f|
  492. str = f.read
  493. Meta.init str, f
  494. str
  495. }
  496. end
  497. end
  498. end
  499. module URI
  500. class Generic
  501. # returns a proxy URI.
  502. # The proxy URI is obtained from environment variables such as http_proxy,
  503. # ftp_proxy, no_proxy, etc.
  504. # If there is no proper proxy, nil is returned.
  505. #
  506. # Note that capitalized variables (HTTP_PROXY, FTP_PROXY, NO_PROXY, etc.)
  507. # are examined too.
  508. #
  509. # But http_proxy and HTTP_PROXY is treated specially under CGI environment.
  510. # It's because HTTP_PROXY may be set by Proxy: header.
  511. # So HTTP_PROXY is not used.
  512. # http_proxy is not used too if the variable is case insensitive.
  513. # CGI_HTTP_PROXY can be used instead.
  514. def find_proxy
  515. name = self.scheme.downcase + '_proxy'
  516. proxy_uri = nil
  517. if name == 'http_proxy' && ENV.include?('REQUEST_METHOD') # CGI?
  518. # HTTP_PROXY conflicts with *_proxy for proxy settings and
  519. # HTTP_* for header information in CGI.
  520. # So it should be careful to use it.
  521. pairs = ENV.reject {|k, v| /\Ahttp_proxy\z/i !~ k }
  522. case pairs.length
  523. when 0 # no proxy setting anyway.
  524. proxy_uri = nil
  525. when 1
  526. k, v = pairs.shift
  527. if k == 'http_proxy' && ENV[k.upcase] == nil
  528. # http_proxy is safe to use because ENV is case sensitive.
  529. proxy_uri = ENV[name]
  530. else
  531. proxy_uri = nil
  532. end
  533. else # http_proxy is safe to use because ENV is case sensitive.
  534. proxy_uri = ENV.to_hash[name]
  535. end
  536. if !proxy_uri
  537. # Use CGI_HTTP_PROXY. cf. libwww-perl.
  538. proxy_uri = ENV["CGI_#{name.upcase}"]
  539. end
  540. elsif name == 'http_proxy'
  541. unless proxy_uri = ENV[name]
  542. if proxy_uri = ENV[name.upcase]
  543. warn 'The environment variable HTTP_PROXY is discouraged. Use http_proxy.'
  544. end
  545. end
  546. else
  547. proxy_uri = ENV[name] || ENV[name.upcase]
  548. end
  549. if proxy_uri && self.host
  550. require 'socket'
  551. begin
  552. addr = IPSocket.getaddress(self.host)
  553. proxy_uri = nil if /\A127\.|\A::1\z/ =~ addr
  554. rescue SocketError
  555. end
  556. end
  557. if proxy_uri
  558. proxy_uri = URI.parse(proxy_uri)
  559. name = 'no_proxy'
  560. if no_proxy = ENV[name] || ENV[name.upcase]
  561. no_proxy.scan(/([^:,]*)(?::(\d+))?/) {|host, port|
  562. if /(\A|\.)#{Regexp.quote host}\z/i =~ self.host &&
  563. (!port || self.port == port.to_i)
  564. proxy_uri = nil
  565. break
  566. end
  567. }
  568. end
  569. proxy_uri
  570. else
  571. nil
  572. end
  573. end
  574. end
  575. class HTTP
  576. def buffer_open(buf, proxy, options) # :nodoc:
  577. OpenURI.open_http(buf, self, proxy, options)
  578. end
  579. include OpenURI::OpenRead
  580. end
  581. class FTP
  582. def buffer_open(buf, proxy, options) # :nodoc:
  583. if proxy
  584. OpenURI.open_http(buf, self, proxy, options)
  585. return
  586. end
  587. require 'net/ftp'
  588. directories = self.path.split(%r{/}, -1)
  589. directories.shift if directories[0] == '' # strip a field before leading slash
  590. directories.each {|d|
  591. d.gsub!(/%([0-9A-Fa-f][0-9A-Fa-f])/) { [$1].pack("H2") }
  592. }
  593. unless filename = directories.pop
  594. raise ArgumentError, "no filename: #{self.inspect}"
  595. end
  596. directories.each {|d|
  597. if /[\r\n]/ =~ d
  598. raise ArgumentError, "invalid directory: #{d.inspect}"
  599. end
  600. }
  601. if /[\r\n]/ =~ filename
  602. raise ArgumentError, "invalid filename: #{filename.inspect}"
  603. end
  604. typecode = self.typecode
  605. if typecode && /\A[aid]\z/ !~ typecode
  606. raise ArgumentError, "invalid typecode: #{typecode.inspect}"
  607. end
  608. # The access sequence is defined by RFC 1738
  609. ftp = Net::FTP.open(self.host)
  610. # todo: extract user/passwd from .netrc.
  611. user = 'anonymous'
  612. passwd = nil
  613. user, passwd = self.userinfo.split(/:/) if self.userinfo
  614. ftp.login(user, passwd)
  615. directories.each {|cwd|
  616. ftp.voidcmd("CWD #{cwd}")
  617. }
  618. if typecode
  619. # xxx: typecode D is not handled.
  620. ftp.voidcmd("TYPE #{typecode.upcase}")
  621. end
  622. if options[:content_length_proc]
  623. options[:content_length_proc].call(ftp.size(filename))
  624. end
  625. ftp.retrbinary("RETR #{filename}", 4096) { |str|
  626. buf << str
  627. options[:progress_proc].call(buf.size) if options[:progress_proc]
  628. }
  629. ftp.close
  630. buf.io.rewind
  631. end
  632. include OpenURI::OpenRead
  633. end
  634. end