]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - lib/rbot/httputil.rb
Add debug backtrace info for HttpUtil failures
[user/henk/code/ruby/rbot.git] / lib / rbot / httputil.rb
1 module Irc
2 module Utils
3
4 require 'resolv'
5 require 'net/http'
6 require 'net/https'
7 Net::HTTP.version_1_2
8
9 # class for making http requests easier (mainly for plugins to use)
10 # this class can check the bot proxy configuration to determine if a proxy
11 # needs to be used, which includes support for per-url proxy configuration.
12 class HttpUtil
13     BotConfig.register BotConfigBooleanValue.new('http.use_proxy',
14       :default => false, :desc => "should a proxy be used for HTTP requests?")
15     BotConfig.register BotConfigStringValue.new('http.proxy_uri', :default => false,
16       :desc => "Proxy server to use for HTTP requests (URI, e.g http://proxy.host:port)")
17     BotConfig.register BotConfigStringValue.new('http.proxy_user',
18       :default => nil,
19       :desc => "User for authenticating with the http proxy (if required)")
20     BotConfig.register BotConfigStringValue.new('http.proxy_pass',
21       :default => nil,
22       :desc => "Password for authenticating with the http proxy (if required)")
23     BotConfig.register BotConfigArrayValue.new('http.proxy_include',
24       :default => [],
25       :desc => "List of regexps to check against a URI's hostname/ip to see if we should use the proxy to access this URI. All URIs are proxied by default if the proxy is set, so this is only required to re-include URIs that might have been excluded by the exclude list. e.g. exclude /.*\.foo\.com/, include bar\.foo\.com")
26     BotConfig.register BotConfigArrayValue.new('http.proxy_exclude',
27       :default => [],
28       :desc => "List of regexps to check against a URI's hostname/ip to see if we should use avoid the proxy to access this URI and access it directly")
29     BotConfig.register BotConfigIntegerValue.new('http.max_redir',
30       :default => 5,
31       :desc => "Maximum number of redirections to be used when getting a document")
32     BotConfig.register BotConfigIntegerValue.new('http.expire_time',
33       :default => 60,
34       :desc => "After how many minutes since last use a cached document is considered to be expired")
35     BotConfig.register BotConfigIntegerValue.new('http.max_cache_time',
36       :default => 60*24,
37       :desc => "After how many minutes since first use a cached document is considered to be expired")
38     BotConfig.register BotConfigIntegerValue.new('http.no_expire_cache',
39       :default => false,
40       :desc => "Set this to true if you want the bot to never expire the cached pages")
41
42   def initialize(bot)
43     @bot = bot
44     @cache = Hash.new
45     @headers = {
46       'User-Agent' => "rbot http util #{$version} (http://linuxbrit.co.uk/rbot/)",
47     }
48   end
49
50   # if http_proxy_include or http_proxy_exclude are set, then examine the
51   # uri to see if this is a proxied uri
52   # the in/excludes are a list of regexps, and each regexp is checked against
53   # the server name, and its IP addresses
54   def proxy_required(uri)
55     use_proxy = true
56     if @bot.config["http.proxy_exclude"].empty? && @bot.config["http.proxy_include"].empty?
57       return use_proxy
58     end
59
60     list = [uri.host]
61     begin
62       list.concat Resolv.getaddresses(uri.host)
63     rescue StandardError => err
64       warning "couldn't resolve host uri.host"
65     end
66
67     unless @bot.config["http.proxy_exclude"].empty?
68       re = @bot.config["http.proxy_exclude"].collect{|r| Regexp.new(r)}
69       re.each do |r|
70         list.each do |item|
71           if r.match(item)
72             use_proxy = false
73             break
74           end
75         end
76       end
77     end
78     unless @bot.config["http.proxy_include"].empty?
79       re = @bot.config["http.proxy_include"].collect{|r| Regexp.new(r)}
80       re.each do |r|
81         list.each do |item|
82           if r.match(item)
83             use_proxy = true
84             break
85           end
86         end
87       end
88     end
89     debug "using proxy for uri #{uri}?: #{use_proxy}"
90     return use_proxy
91   end
92
93   # uri:: Uri to create a proxy for
94   #
95   # return a net/http Proxy object, which is configured correctly for
96   # proxying based on the bot's proxy configuration.
97   # This will include per-url proxy configuration based on the bot config
98   # +http_proxy_include/exclude+ options.
99   def get_proxy(uri)
100     proxy = nil
101     proxy_host = nil
102     proxy_port = nil
103     proxy_user = nil
104     proxy_pass = nil
105
106     if @bot.config["http.use_proxy"]
107       if (ENV['http_proxy'])
108         proxy = URI.parse ENV['http_proxy'] rescue nil
109       end
110       if (@bot.config["http.proxy_uri"])
111         proxy = URI.parse @bot.config["http.proxy_uri"] rescue nil
112       end
113       if proxy
114         debug "proxy is set to #{proxy.host} port #{proxy.port}"
115         if proxy_required(uri)
116           proxy_host = proxy.host
117           proxy_port = proxy.port
118           proxy_user = @bot.config["http.proxy_user"]
119           proxy_pass = @bot.config["http.proxy_pass"]
120         end
121       end
122     end
123
124     h = Net::HTTP.new(uri.host, uri.port, proxy_host, proxy_port, proxy_user, proxy_port)
125     h.use_ssl = true if uri.scheme == "https"
126     return h
127   end
128
129   # uri::         uri to query (Uri object)
130   # readtimeout:: timeout for reading the response
131   # opentimeout:: timeout for opening the connection
132   #
133   # simple get request, returns (if possible) response body following redirs
134   # and caching if requested
135   # it yields the urls it gets redirected to, for future uses
136   def get(uri, readtimeout=10, opentimeout=5, max_redir=@bot.config["http.max_redir"], cache=false)
137     proxy = get_proxy(uri)
138     proxy.open_timeout = opentimeout
139     proxy.read_timeout = readtimeout
140
141     begin
142       proxy.start() {|http|
143         resp = http.get(uri.request_uri(), @headers)
144         case resp
145         when Net::HTTPSuccess
146           if cache
147             k = uri.to_s
148             @cache[k] = Hash.new
149             @cache[k][:body] = resp.body
150             @cache[k][:last_mod] = Time.httpdate(resp['last-modified']) if resp.key?('last-modified')
151             if resp.key?('date')
152               @cache[k][:first_use] = Time.httpdate(resp['date'])
153               @cache[k][:last_use] = Time.httpdate(resp['date'])
154             else
155               now = Time.new
156               @cache[k][:first_use] = now
157               @cache[k][:last_use] = now
158             end
159             @cache[k][:count] = 1
160           end
161           return resp.body
162         when Net::HTTPRedirection
163           debug "Redirecting #{uri} to #{resp['location']}"
164           yield resp['location']
165           if max_redir > 0
166             return get( URI.parse(resp['location']), readtimeout, opentimeout, max_redir-1, cache)
167           else
168             warning "Max redirection reached, not going to #{resp['location']}"
169           end
170         else
171           debug "HttpUtil.get return code #{resp.code} #{resp.body}"
172         end
173         return nil
174       }
175     rescue StandardError, Timeout::Error => e
176       error "HttpUtil.get exception: #{e.inspect}, while trying to get #{uri}"
177       debug e.backtrace.join("\n")
178     end
179     return nil
180   end
181
182   # just like the above, but only gets the head
183   def head(uri, readtimeout=10, opentimeout=5, max_redir=@bot.config["http.max_redir"])
184     proxy = get_proxy(uri)
185     proxy.open_timeout = opentimeout
186     proxy.read_timeout = readtimeout
187
188     begin
189       proxy.start() {|http|
190         resp = http.head(uri.request_uri(), @headers)
191         case resp
192         when Net::HTTPSuccess
193           return resp
194         when Net::HTTPRedirection
195           debug "Redirecting #{uri} to #{resp['location']}"
196           yield resp['location']
197           if max_redir > 0
198             return head( URI.parse(resp['location']), readtimeout, opentimeout, max_redir-1)
199           else
200             warning "Max redirection reached, not going to #{resp['location']}"
201           end
202         else
203           debug "HttpUtil.head return code #{resp.code}"
204         end
205         return nil
206       }
207     rescue StandardError, Timeout::Error => e
208       error "HttpUtil.head exception: #{e.inspect}, while trying to get #{uri}"
209       debug e.backtrace.join("\n")
210     end
211     return nil
212   end
213
214   # gets a page from the cache if it's still (assumed to be) valid
215   # TODO remove stale cached pages, except when called with noexpire=true
216   def get_cached(uri, readtimeout=10, opentimeout=5,
217                  max_redir=@bot.config['http.max_redir'],
218                  noexpire=@bot.config['http.no_expire_cache'])
219     k = uri.to_s
220     if !@cache.key?(k)
221       remove_stale_cache unless noexpire
222       return get(uri, readtimeout, opentimeout, max_redir, true)
223     end
224     now = Time.new
225     begin
226       # See if the last-modified header can be used
227       # Assumption: the page was not modified if both the header
228       # and the cached copy have the last-modified value, and it's the same time
229       # If only one of the cached copy and the header have the value, or if the
230       # value is different, we assume that the cached copyis invalid and therefore
231       # get a new one.
232       # On our first try, we tested for last-modified in the webpage first,
233       # and then on the local cache. however, this is stupid (in general),
234       # so we only test for the remote page if the local copy had the header
235       # in the first place.
236       if @cache[k].key?(:last_mod)
237         h = head(uri, readtimeout, opentimeout, max_redir)
238         if h.key?('last-modified')
239           if Time.httpdate(h['last-modified']) == @cache[k][:last_mod]
240             if resp.key?('date')
241               @cache[k][:last_use] = Time.httpdate(resp['date'])
242             else
243               @cache[k][:last_use] = now
244             end
245             @cache[k][:count] += 1
246             return @cache[k][:body]
247           end
248           remove_stale_cache unless noexpire
249           return get(uri, readtimeout, opentimeout, max_redir, true)
250         end
251         remove_stale_cache unless noexpire
252         return get(uri, readtimeout, opentimeout, max_redir, true)
253       end
254     rescue => e
255       warning "Error #{e.inspect} getting the page #{uri}, using cache"
256       debug e.backtrace.join("\n")
257       return @cache[k][:body]
258     end
259     # If we still haven't returned, we are dealing with a non-redirected document
260     # that doesn't have the last-modified attribute
261     debug "Could not use last-modified attribute for URL #{uri}, guessing cache validity"
262     if noexpire or !expired?(@cache[k], now)
263       @cache[k][:count] += 1
264       @cache[k][:last_use] = now
265       debug "Using cache"
266       return @cache[k][:body]
267     end
268     debug "Cache expired, getting anew"
269     @cache.delete(k)
270     remove_stale_cache unless noexpire
271     return get(uri, readtimeout, opentimeout, max_redir, true)
272   end
273
274   def expired?(hash, time)
275     (time - hash[:last_use] > @bot.config['http.expire_time']*60) or
276     (time - hash[:first_use] > @bot.config['http.max_cache_time']*60)
277   end
278
279   def remove_stale_cache
280     now = Time.new
281     @cache.reject! { |k, val|
282       !val.key?[:last_modified] && expired?(val, now)
283     }
284   end
285
286 end
287 end
288 end