31451fa647b909a6bed9b0ef7bedee908b112123
[apachelog.git] / apachelog / resolve.py
1 import os.path as _os_path
2 import pickle as _pickle
3 import re as _re
4 import socket as _socket
5
6
7 class Resolver (object):
8     """A simple reverse-DNS resolver.
9
10     Maintains a class-level cache of resolved IPs to avoid repeated
11     lookups on the same IP address.
12
13     Avoid hanging if we can't resolve a name.
14
15     >>> import socket
16     >>> if hasattr(_socket, 'setdefaulttimeout'):
17     ...     socket.setdefaulttimeout(5)  # set 5 second timeout
18
19     >>> r = Resolver()
20     >>> r.IP = {}  # clear cache of date from previous tests
21     >>> r.resolve('198.41.0.4')
22     'a.root-servers.net'
23     >>> r.IP
24     {'198.41.0.4': ('a.root-servers.net', [], ['198.41.0.4'])}
25
26     If you want to give shorter names to various DNS names, you can
27     add an entry to the class-level ``REGEXPS``.  The entry should use
28     your name as the key, and a list of matching regexps as the value.
29     You need to enable this enhanced resolution using the ``smart``
30     argument.
31
32     >>> r.resolve('66.249.68.33')
33     'crawl-66-249-68-33.googlebot.com'
34     >>> r = Resolver(smart=True)
35     >>> r.resolve('66.249.68.34')
36     'googlebot'
37     """
38     IP = {}
39
40     REGEXPS = {
41         'feedburner': [_re.compile('.*rate-limited-proxy-.*.google.com.*')],
42         }
43     for bot in [
44         'baiduspider',
45         'googlebot',
46         'msnbot',  # a.k.a: bingbot
47         'yandex',
48         ]:
49         REGEXPS[bot] = [_re.compile('.*{}.*'.format(bot))]
50
51     _cache_file = _os_path.expanduser(
52         _os_path.join('~', '.apachelog-resolver.cache'))
53     _cache_loaded = False
54
55     def __init__(self, smart=False):
56         self._smart = smart
57         self.load_cache()
58
59     @classmethod
60     def load_cache(self):
61         if not self._cache_loaded:
62             self._cache_loaded = True
63             try:
64                 with open(self._cache_file, 'rb') as f:
65                     self.IP = _pickle.load(f)
66             except IOError:
67                 pass
68             if self.IP is None:
69                 self.IP = {}
70
71     @classmethod
72     def save_cache(self):
73         self.load_cache()  # avoid clobbering unloaded content
74         with open(self._cache_file, 'wb') as f:
75             _pickle.dump(self.IP, f)
76
77     def resolve(self, ip):
78         if ip not in self.IP:
79             try:
80                 self.IP[ip] = _socket.gethostbyaddr(ip)
81             except _socket.herror as e:
82                 self.IP[ip] = (ip, [], [ip])
83             except _socket.gaierror as e:
84                 self.IP[ip] = (ip, [], [ip])
85             else:
86                 if self._smart:
87                     self._smart_resolve(ip)
88         return self.IP[ip][0]
89
90     def _smart_resolve(self, ip):
91         x = self.IP[ip]
92         if self._smart:
93             for name,regexps in self.REGEXPS.items():
94                 for regexp in regexps:
95                     if regexp.match(self.IP[ip][0]):
96                         self.IP[ip] = (name, x[1], x[2])
97
98     def ips(self, name):
99         "Return a set of IP addresses used by a smart-resolved name."
100         ips = set()
101         for ip,values in self.IP.items():
102             if values[0] == name:
103                 for x in values[2]:
104                     ips.add(x)
105         return ips