dnsomatic 0.4.0 → 0.4.1

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