dnsomatic 0.4.0 → 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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