Skip to content

Commit 83a3df0

Browse files
committed
Initial commit
1 parent f34937e commit 83a3df0

20 files changed

Lines changed: 1507 additions & 1 deletion

.gitignore

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
# Test generated files
2+
3+
dns.pickle
4+
dns-lru.pickle
5+
jsonpickle.stash
6+
pickle.stash
7+
dns.sqlite
8+
disk-cache-dir
9+
diskdict-cache-dir
10+
111
# Byte-compiled / optimized / DLL files
212
__pycache__/
313
*.py[cod]

README.md

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,81 @@
1-
# dns-cache
1+
# dns-cache
2+
3+
`dns-cache` is a Python client side DNS caching framework utilising
4+
[`dnspython`](https://github.com/rthalley/dnspython) v1.15+ for DNS
5+
and supports various local key stores, and provides caching of lookup failures,
6+
and configurable expiration of cached responses.
7+
8+
Some reasons to use a client side cache include:
9+
- processing data containing many repeated invalid domains,
10+
- running a local DNS caching service is not practical or appropriate,
11+
- adding reporting of DNS activity performed within a job.
12+
13+
## Installation
14+
15+
The recommended way to install `dns-cache` is by using pip as follows:
16+
17+
`pip install dns-cache`
18+
19+
## Getting started
20+
21+
To quickly benefit from client side dns-caching in your existing application, install the system resolver.
22+
23+
```python
24+
import dns_cache
25+
import requests
26+
27+
dns_cache.override_system_resolver()
28+
29+
for i in range(10):
30+
requests.get('http://www.coala.io/')
31+
```
32+
33+
If you have a fast dns proxy, 10 requests will possibly show no performance improvement.
34+
Even 100 may not perform better in this contrived example.
35+
36+
However when many parts of a system are performing lookups on the same DNS records, or where
37+
sessions are being closed and new ones created and need to access the same DNS records,
38+
the difference becomes more noticable, especially in jobs which takes hours to run.
39+
40+
For long running jobs, use the `min_ttl` argument to increase the default if 5 minutes isnt sufficient.
41+
It can be set to `dns_cache.NO_EXPIRY` for a ttl of one week, which is not recommended except when
42+
accompanied with custom cache expiration logic.
43+
44+
## Key stores
45+
46+
Multiple key stores are supported, and their dependencies need to added separately as required.
47+
48+
1. `pickle` and [`pickle4`](https://github.com/moreati/pickle4) backport: `dns_cache.pickle.PickableCache`
49+
2. [`diskcache`](https://github.com/grantjenks/python-diskcache): `dns_cache.diskcache.DiskCache`
50+
3. [`stash.py`](https://github.com/fuzeman/stash.py/): `dns_cache.stash.StashCache`
51+
4. [`sqlitedict`](https://github.com/RaRe-Technologies/sqlitedict): `dns_cache.sqlitedict.SqliteDictCache`
52+
5. [`disk_dict`](https://github.com/AWNystrom/DiskDict): `dns_cache.disk_dict.DiskDictCache` (Python 2.7 only)
53+
54+
`stash.py` support uses `pickle` or `jsonpickle` on Python 3, however only `jsonpickle` works on Python 2.7.
55+
56+
## Caching additions
57+
58+
The following classes can be used separately or together.
59+
60+
1. `dns_cache.resolver.AggressiveCachingResolver`: indexes all qnames in the response, increasing the number of keys,
61+
but reducing the number of requests and cached responses when several related records are requested, such as a HTTP redirect
62+
from www.foo.com to foo.com (or vis versa) where one is a CNAME point to the other.
63+
2. `dns_cache.resolver.ExceptionCachingResolver`: caches lookup failures.
64+
65+
**Note:** `dns_cache.override_system_resolver()` can be used to install a custom `resolver` or `cache`, which may
66+
be derived from the above classes or your own implementation from scratch.
67+
68+
## TODO
69+
70+
1. Support [`python-benedict`](https://github.com/fabiocaccamo/python-benedict)
71+
2. Use [`dnsbin`](https://github.com/ettic-team/dnsbin) for testing
72+
3. Add redis, memcached and cloud caching backends
73+
74+
## Similar projects
75+
76+
Python:
77+
1. [`velocity`](https://github.com/s0md3v/velocity) is a lighter weight approach, with a [`serious bug`](https://github.com/s0md3v/velocity/issues/2)
78+
2. [`dnsplug`](https://github.com/nresare/dnsplug), unfortunately not available on PyPI.
79+
80+
Go:
81+
1. [`dnscache`](https://github.com/rs/dnscache)

dns_cache/__init__.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import atexit
2+
import os
3+
import os.path
4+
import sys
5+
6+
from dns.resolver import override_system_resolver as upstream_override_system_resolver
7+
8+
from .expiration import _NO_EXPIRY as NO_EXPIRY
9+
from .expiration import FIVE_MINS, MinExpirationCache, NoExpirationCache
10+
from .pickle import PickableCache
11+
from .resolver import AggressiveCachingResolver, ExceptionCachingResolver
12+
13+
__version__ = "0.1.0"
14+
15+
16+
class Resolver(AggressiveCachingResolver, ExceptionCachingResolver):
17+
pass
18+
19+
20+
class MinExpirationPickableCache(MinExpirationCache, PickableCache):
21+
pass
22+
23+
24+
class NoExpirationPickableCache(NoExpirationCache, PickableCache):
25+
pass
26+
27+
28+
def override_system_resolver(
29+
resolver=None, cache=None, directory=None, min_ttl=FIVE_MINS
30+
): # pragma: no cover
31+
if not cache:
32+
if directory:
33+
try:
34+
os.makedirs(directory, exist_ok=True)
35+
except TypeError:
36+
try:
37+
os.makedirs(directory)
38+
except OSError:
39+
pass
40+
41+
filename = os.path.join(directory, "dns.pickle")
42+
if min_ttl == NO_EXPIRY:
43+
cache = MinExpirationPickableCache(filename=filename, min_ttl=min_ttl)
44+
else:
45+
cache = MinExpirationPickableCache(filename=filename, min_ttl=min_ttl)
46+
else:
47+
if min_ttl == NO_EXPIRY:
48+
cache = NoExpirationCache(min_ttl=min_ttl)
49+
else:
50+
cache = MinExpirationCache(min_ttl=min_ttl)
51+
52+
if not resolver:
53+
resolver = Resolver(configure=False)
54+
try:
55+
if sys.platform == "win32":
56+
resolver.read_registry()
57+
else:
58+
resolver.read_resolv_conf("/etc/resolv.conf")
59+
except Exception:
60+
resolver.nameservers = ["8.8.8.8"]
61+
62+
resolver.cache = cache
63+
64+
upstream_override_system_resolver(resolver)
65+
66+
if hasattr(cache, "__del__"):
67+
atexit.register(cache.__del__)
68+
69+
return resolver

dns_cache/disk_dict.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
from __future__ import absolute_import
2+
3+
import jsonpickle
4+
5+
from dns.resolver import Cache
6+
7+
from .key_transform import StringKeyDictBase
8+
9+
from disk_dict import DiskDict
10+
11+
12+
class DiskDict(StringKeyDictBase, DiskDict): # pragma: no cover
13+
def __len__(self):
14+
return len(list(self.keys()))
15+
try:
16+
return len(self.keys())
17+
except ValueError:
18+
return 0
19+
20+
21+
class DiskDictCacheBase(object): # pragma: no cover
22+
def __init__(
23+
self,
24+
directory,
25+
serializer=jsonpickle.dumps,
26+
deserializer=jsonpickle.loads,
27+
*args,
28+
**kwargs
29+
): # pragma: no cover
30+
super(DiskDictCacheBase, self).__init__(*args, **kwargs)
31+
self.data = DiskDict(
32+
location=directory, serializer=serializer, deserializer=deserializer
33+
)
34+
35+
36+
class DiskDictCache(DiskDictCacheBase, Cache): # pragma: no cover
37+
def __init__(self, *args, **kwargs):
38+
super(DiskDictCache, self).__init__(*args, **kwargs)

dns_cache/diskcache.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from __future__ import absolute_import
2+
3+
import diskcache as dc
4+
5+
from dns.resolver import Cache, LRUCache
6+
7+
8+
class DiskCacheBase(object):
9+
def __init__(self, directory, *args, **kwargs):
10+
super(DiskCacheBase, self).__init__(*args, **kwargs)
11+
self.data = dc.Cache(directory)
12+
13+
14+
class DiskCache(DiskCacheBase, Cache):
15+
def __init__(self, *args, **kwargs):
16+
super(DiskCache, self).__init__(*args, **kwargs)
17+
18+
19+
class DiskLRUCache(DiskCacheBase, LRUCache):
20+
def __init__(self, *args, **kwargs):
21+
super(DiskLRUCache, self).__init__(*args, **kwargs)

dns_cache/expiration.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import time
2+
3+
from dns.resolver import Cache, LRUCache
4+
5+
FIVE_MINS = 60 * 5
6+
TEN_MINS = 60 * 10
7+
SECONDS_PER_DAY = 60 * 60 * 24
8+
SECONDS_PER_WEEK = SECONDS_PER_DAY * 7
9+
10+
_NO_EXPIRY = SECONDS_PER_WEEK
11+
12+
MIN_TTL = FIVE_MINS
13+
14+
15+
class MinExpirationCacheBase(object):
16+
def __init__(self, min_ttl=None, *args, **kwargs):
17+
if not min_ttl:
18+
min_ttl = MIN_TTL
19+
super(MinExpirationCacheBase, self).__init__(*args, **kwargs)
20+
self.min_ttl = min_ttl
21+
22+
def put(self, key, value):
23+
now = time.time()
24+
min_expiration = now + self.min_ttl
25+
if min_expiration > value.expiration:
26+
value.expiration = min_expiration
27+
super(MinExpirationCacheBase, self).put(key, value)
28+
29+
30+
class NoExpirationCacheBase(MinExpirationCacheBase):
31+
def __init__(self, min_ttl=_NO_EXPIRY):
32+
super(NoExpirationCacheBase, self).__init__(min_ttl)
33+
34+
def _maybe_clean(self):
35+
"""Avoid the _maybe_clean phase of dns.resolver.Cache."""
36+
pass
37+
38+
39+
class MinExpirationCache(MinExpirationCacheBase, Cache):
40+
def __init__(self, cleaning_interval=None, min_ttl=None, *args, **kwargs):
41+
if not min_ttl:
42+
min_ttl = MIN_TTL
43+
if not cleaning_interval:
44+
cleaning_interval = max(MIN_TTL, min_ttl)
45+
super(MinExpirationCache, self).__init__(
46+
cleaning_interval=cleaning_interval, min_ttl=min_ttl, *args, **kwargs
47+
)
48+
49+
50+
class NoExpirationCache(NoExpirationCacheBase, Cache):
51+
pass
52+
53+
54+
class MinExpirationLRUCache(MinExpirationCacheBase, LRUCache):
55+
pass
56+
57+
58+
class NoExpirationLRUCache(NoExpirationCacheBase, LRUCache):
59+
pass

dns_cache/key_transform.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
from dns.name import from_text
2+
3+
4+
def key_encode(key):
5+
name, rdtype, rdclass = key
6+
return "{}!{}!{}".format(name, rdtype, rdclass)
7+
8+
9+
def key_decode(key):
10+
name, rdtype, rdclass = key.rsplit("!", 2)
11+
return (from_text(name, None), int(rdtype), int(rdclass))
12+
13+
14+
class KeyTransformDictBase(object):
15+
def __contains__(self, key):
16+
if isinstance(key, tuple):
17+
key = self.key_encode(key)
18+
return super(KeyTransformDictBase, self).__contains__(key)
19+
20+
def __setitem__(self, key, value):
21+
if isinstance(key, tuple):
22+
key = self.key_encode(key)
23+
super(KeyTransformDictBase, self).__setitem__(key, value)
24+
25+
def get(self, key, default=None):
26+
if isinstance(key, tuple):
27+
key = self.key_encode(key)
28+
return super(KeyTransformDictBase, self).get(key, default)
29+
30+
def put(self, key, value):
31+
if isinstance(key, tuple):
32+
key = self.key_encode(key)
33+
try:
34+
return super(KeyTransformDictBase, self).put(key, value)
35+
except AttributeError:
36+
return super(KeyTransformDictBase, self).__setitem__(key, value)
37+
38+
def __getitem__(self, key):
39+
if isinstance(key, tuple):
40+
key = self.key_encode(key)
41+
return super(KeyTransformDictBase, self).__getitem__(key)
42+
43+
def __delitem__(self, key):
44+
if isinstance(key, tuple):
45+
key = self.key_encode(key)
46+
super(KeyTransformDictBase, self).__delitem__(key)
47+
48+
def keys(self):
49+
return (key_decode(key) for key in super(KeyTransformDictBase, self).keys())
50+
51+
def items(self):
52+
for key, value in super(KeyTransformDictBase, self).items():
53+
yield key_decode(key), value
54+
55+
56+
class StringKeyDictBase(KeyTransformDictBase):
57+
key_encode = staticmethod(key_encode)
58+
key_decode = staticmethod(key_decode)
59+
60+
61+
class StringKeyDict(StringKeyDictBase, dict):
62+
pass

0 commit comments

Comments
 (0)