├── LICENSE ├── LICENSE_NOMINUM └── LICENSE_RACKSPACE ├── MANIFEST.in ├── README.rst ├── dnsq.py ├── dnsq_test.py └── setup.py /LICENSE/LICENSE_NOMINUM: -------------------------------------------------------------------------------- 1 | Copyright (C) 2001-2003 Nominum, Inc. 2 | 3 | Permission to use, copy, modify, and distribute this software and its 4 | documentation for any purpose with or without fee is hereby granted, 5 | provided that the above copyright notice and this permission notice 6 | appear in all copies. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES 9 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR 11 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT 14 | OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | -------------------------------------------------------------------------------- /LICENSE/LICENSE_RACKSPACE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE/LICENSE_NOMINUM 2 | include LICENSE/LICENSE_RACKSPACE 3 | include README.rst 4 | include MANIFEST.in 5 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | dnsq 2 | ==== 3 | 4 | DNS Query Tool 5 | 6 | Usage 7 | ----- 8 | 9 | .. code-block:: py 10 | 11 | >>> import dnsq 12 | >>> dnsq.query_dns('www.example.com', 'a') 13 | ['93.184.216.119'] 14 | 15 | .. code-block:: py 16 | 17 | >>> import dnsq 18 | >>> dnsq.mx_hosts_for('example.com') 19 | ['example.com'] 20 | -------------------------------------------------------------------------------- /dnsq.py: -------------------------------------------------------------------------------- 1 | """ 2 | High-level DNS library built on top of dnspython. 3 | Only two functions matter here: 4 | 5 | - query_dns() : runs an arbitrary DNS query 6 | - mx_hosts_for() : returns a list of MX hosts for a given domain 7 | """ 8 | import logging 9 | import socket 10 | import threading 11 | from collections import deque 12 | from itertools import groupby 13 | from random import shuffle 14 | 15 | import dns 16 | import dns.exception 17 | import dns.resolver 18 | import dns.reversename 19 | from expiringdict import ExpiringDict 20 | 21 | _log = logging.getLogger(__name__) 22 | 23 | _DNS_CACHE_LIFE_SECONDS = 240.0 24 | _DNS_TIMEOUT_SECONDS = 3.0 # timeout per DNS server 25 | _DNS_LIFETIME_TIMEOUT_SECONDS = 5.2 # total timeout per DNS request 26 | _PTR_CACHE_LEN = 512 27 | 28 | # This cache is used to store PTR records for IP addresses 29 | _ptr_cache = ExpiringDict(max_len=_PTR_CACHE_LEN, 30 | max_age_seconds=_DNS_CACHE_LIFE_SECONDS) 31 | 32 | _thread_local = threading.local() 33 | _thread_local.resolver = None 34 | 35 | 36 | def get_primary_nameserver(hostname): 37 | """ 38 | Query DNS for the primary nameserver (SOA) for the given hostname. 39 | """ 40 | dq = deque(hostname.split('.')) 41 | while len(dq) > 1: 42 | soa = query_dns('.'.join(dq), 'SOA') 43 | if soa: 44 | return soa[0].split(" ")[0].strip(".") 45 | dq.popleft() 46 | 47 | 48 | def mx_hosts_for(hostname): 49 | """ 50 | Returns a list of MX hostnames for a given domain name, sorted by their priority + randomization 51 | Note that if no MX records are found it falls back to default 52 | 53 | >>> mx_hosts_for('gmail.com') 54 | ['alt1.gmail-smtp-in.l.google.com', 'alt2.gmail-smtp-in.l.google.com'] 55 | 56 | Raises ecxeptions for network errors. 57 | """ 58 | retval = [] 59 | try: 60 | answers = sorted(_exec_query(hostname, 'MX')) 61 | for mx_pref, grouper in groupby(answers, 62 | lambda entry: entry.preference): 63 | group = [entry.exchange.to_text() for entry in grouper] 64 | shuffle(group) 65 | retval += group 66 | 67 | # timeout, raise an exception - let them retry 68 | except dns.exception.Timeout: 69 | raise Exception("DNS failure for " + str(hostname)) 70 | 71 | # no MX record: 72 | except dns.resolver.NoAnswer: 73 | retval = [hostname] 74 | 75 | # invalid domain 76 | except dns.resolver.NXDOMAIN: 77 | retval = [] 78 | 79 | # empty label (ex: domain..com) 80 | except dns.name.EmptyLabel: 81 | retval = [] 82 | 83 | # filter out invalid queries (tld does not exist) 84 | dns_attention_string = ''.join( 85 | ['your-dns-needs-immediate-attention.', hostname.rstrip('.'), '.']) 86 | retval = [s for s in retval if s != dns_attention_string] 87 | 88 | # strip ending . and filter None 89 | retval = [h.strip('.') for h in retval] 90 | return filter(lambda x: x, retval) 91 | 92 | 93 | def ptr_record_for(ipaddress): 94 | """ 95 | Performs reverse DNS lookup on a given IP address. 96 | This is a replacement for socket.gethostbyaddr(), but with the following 97 | differences: 98 | - Returns None instead of throwing exceptions 99 | - It is fast: it will not block for 5+ seconds for IPs without PTRs 100 | - It is caching: it will be instant nearly all the time 101 | 102 | >>> ptr_record_for('127.0.0.1') 103 | "localhost" 104 | >>> ptr_record_for('74.125.224.123') 105 | "nuq04s08-in-f27.1e100.net" 106 | >>> ptr_record_for('74.125.224.1') 107 | None 108 | """ 109 | if ipaddress == '127.0.0.1': 110 | return 'localhost' 111 | 112 | MISSING = "unknown" 113 | retval = None 114 | 115 | # see if we have it cached: 116 | cached_value = _ptr_cache.get(ipaddress) 117 | if cached_value: 118 | return cached_value if cached_value != MISSING else None 119 | 120 | try: 121 | # get the in_addr.arpa name, like 142.224.125.74.in-addr.arpa. 122 | inaddr_arpa_name = dns.reversename.from_address(ipaddress).to_text() 123 | 124 | # now use ARPA name to query for PTR: 125 | hosts = query_dns(inaddr_arpa_name, "PTR") 126 | if hosts: 127 | retval = hosts[0].strip('.') 128 | _ptr_cache[ipaddress] = retval 129 | # success: found the PTR record: 130 | return retval 131 | except: 132 | pass 133 | 134 | # no suitable PTR: 135 | _ptr_cache[ipaddress] = MISSING 136 | return None 137 | 138 | 139 | def spf_record_for(hostname, bypass_cache=True): 140 | """Retrieves SPF record for a given hostname. 141 | 142 | According to the standard, domain must not have multiple SPF records, so 143 | if it's the case then an empty string is returned. 144 | """ 145 | try: 146 | primary_ns = None 147 | if bypass_cache: 148 | primary_ns = get_primary_nameserver(hostname) 149 | 150 | txt_records = query_dns(hostname, 'txt', primary_ns) 151 | spf_records = [r for r in txt_records if r.strip().startswith('v=spf')] 152 | 153 | if len(spf_records) == 1: 154 | return spf_records[0] 155 | 156 | except Exception as e: 157 | _log.exception(e) 158 | 159 | return '' 160 | 161 | 162 | def query_dns(hostname, record_type, name_srv=None): 163 | """ 164 | Runs simple DNS queries, like: 165 | >>> query_dns('mailgun.net', 'txt') 166 | ['v=spf1 include:_spf.mailgun.org ~all'] 167 | """ 168 | try: 169 | # if nameserver was specified, convert it into IP: 170 | name_srv_ip = None 171 | if name_srv: 172 | ips = query_dns(name_srv, 'A') 173 | if ips: 174 | name_srv_ip = ips[0] 175 | 176 | records = _exec_query(hostname, record_type, name_srv_ip) 177 | if record_type.lower() == 'txt': 178 | return [record.to_text().strip("\"").replace('" "', '') for record 179 | in records] 180 | else: 181 | return [record.to_text() for record in records] 182 | 183 | # no entry? 184 | except ( 185 | dns.resolver.NXDOMAIN, dns.resolver.NoAnswer, dns.resolver.NoNameservers): 186 | return [] 187 | 188 | 189 | def _exec_query(hostname, record_type, name_srv_ip=None): 190 | """ 191 | Execute a DNS query against a given name source. 192 | """ 193 | try: 194 | # if nameserver specified then try it first. 195 | if name_srv_ip: 196 | resolver = _new_resolver(name_srv_ip) 197 | try: 198 | return resolver.query(hostname, record_type, tcp=True) 199 | except dns.exception.Timeout: 200 | pass 201 | 202 | # if it's not specified or timed out then use default nameserver 203 | return _get_default_resolver().query(hostname, record_type, tcp=True) 204 | 205 | except (dns.exception.Timeout, 206 | dns.resolver.NoNameservers, 207 | socket.error): 208 | return [] 209 | 210 | 211 | def _get_default_resolver(): 212 | """ 213 | Returns DNS resolver instance for the calling thread. 214 | """ 215 | resolver = getattr(_thread_local, 'resolver', None) 216 | if resolver: 217 | return resolver 218 | 219 | _thread_local.resolver = _new_resolver() 220 | return _thread_local.resolver 221 | 222 | 223 | def _new_resolver(name_srv_ip=None): 224 | resolver = dns.resolver.Resolver() 225 | resolver.timeout = _DNS_TIMEOUT_SECONDS 226 | resolver.lifetime = _DNS_LIFETIME_TIMEOUT_SECONDS 227 | if name_srv_ip: 228 | resolver.nameservers = [name_srv_ip] 229 | return resolver 230 | -------------------------------------------------------------------------------- /dnsq_test.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import time 3 | 4 | import dns 5 | import dns.resolver 6 | import dnsq 7 | from mock import Mock, patch 8 | from nose.tools import eq_, ok_, assert_raises 9 | 10 | 11 | def test_dns_query(): 12 | # note: we're not mocking DNS, i.e. you need to have internet connection to run this 13 | # test. perhaps we should change it? 14 | answer = dnsq.query_dns('mailgun.net', 'mx') 15 | eq_(2, len(answer)) 16 | ok_("10 mxa.mailgun.org." in answer) 17 | ok_("10 mxb.mailgun.org." in answer) 18 | 19 | # test TXT concatenation: 20 | with patch.object(dns.resolver.Resolver, 'query') as query: 21 | query.reset_mock() 22 | query.return_value = [Mock()] 23 | query.return_value[0].to_text = Mock(return_value="\"Hello\" \"world\"") 24 | eq_(['Helloworld'], dnsq.query_dns('mailgun.us', 'txt')) 25 | 26 | # specify the name server 27 | eq_(['Helloworld'], dnsq.query_dns('mailgun.us', 'txt', 28 | name_srv='ns1.com')) 29 | # socket error 30 | query.side_effect = socket.error 31 | eq_([], dnsq.query_dns('mailgun.us', 'txt')) 32 | 33 | # test fallback to default nameserver 34 | query.side_effect = dns.exception.Timeout 35 | eq_([], dnsq.query_dns('mailgun.us', 'txt', name_srv='ns1.com')) 36 | 37 | # test errors: 38 | with patch.object(dns.resolver.Resolver, 'query') as query: 39 | query.side_effect = dns.resolver.NoNameservers() 40 | eq_([], dnsq.query_dns('mailgun.net', 'mx')) 41 | query.side_effect = dns.resolver.NXDOMAIN() 42 | eq_([], dnsq.query_dns('mailgun.net', 'mx')) 43 | query.side_effect = dns.resolver.NoAnswer() 44 | eq_([], dnsq.query_dns('mailgun.net', 'mx')) 45 | 46 | 47 | def test_mx_lookup(): 48 | # query against live DNS server: 49 | answer = dnsq.mx_hosts_for('gmail.com')[0] 50 | ok_('google.com' in answer) 51 | 52 | with patch.object(dnsq, '_get_default_resolver') as get_resolver: 53 | r = Mock() 54 | get_resolver.return_value = r 55 | 56 | # makes a fake MX reply 57 | def fake_mx(name): 58 | class FakeEntry(object): 59 | @property 60 | def preference(self): return 1 61 | 62 | @property 63 | def exchange(self): 64 | class Value(object): 65 | def to_text(self): return name 66 | 67 | return Value() 68 | 69 | return FakeEntry() 70 | 71 | # test MX timeout failure: 72 | with patch.object(r, 'query', Mock(side_effect=dns.exception.Timeout)): 73 | eq_([], dnsq.mx_hosts_for('host.com')) 74 | 75 | # test querying an invalid domain: 76 | with patch.object(r, 'query', 77 | Mock(side_effect=dns.resolver.NXDOMAIN())): 78 | eq_([], dnsq.mx_hosts_for('invalid-siteeeee.com')) 79 | 80 | # test querying a domain without MX: 81 | with patch.object(r, 'query', 82 | Mock(side_effect=dns.resolver.NoAnswer())): 83 | eq_(['host.com'], dnsq.mx_hosts_for('host.com')) 84 | 85 | # test querying a domain with MX: 86 | with patch.object(r, 'query') as query_mock: 87 | query_mock.return_value = [fake_mx('mx.host.com.'), 88 | fake_mx('mx2.host.com.')] 89 | eq_(set(['mx.host.com', 'mx2.host.com']), 90 | set(dnsq.mx_hosts_for('host.com'))) 91 | 92 | # test failure: 93 | with patch.object(r, 'query', Mock(side_effect=Exception('bam!'))): 94 | assert_raises(Exception, dnsq.mx_hosts_for, 'host.com') 95 | 96 | # test dns failure 97 | with patch.object(dnsq, '_exec_query') as exec_query: 98 | exec_query.side_effect = dns.exception.Timeout 99 | assert_raises(Exception, dnsq.mx_hosts_for, 'host.com') 100 | 101 | 102 | @patch.object(dnsq, 'get_primary_nameserver', Mock(return_value='ns.com')) 103 | @patch.object(dnsq, 'query_dns') 104 | def test_spf_record_for(dns): 105 | # No SPF records 106 | dns.return_value = ["blah"] 107 | eq_('', dnsq.spf_record_for('host.com')) 108 | 109 | # Multiple SPF records 110 | dns.return_value = ["v=spf1 +all", "blah", "v=spf1 -all"] 111 | eq_('', dnsq.spf_record_for('host.com')) 112 | 113 | # OK - one SPF record 114 | dns.return_value = ["blah", "v=spf1 +all", "blahblah"] 115 | eq_("v=spf1 +all", dnsq.spf_record_for('host.com')) 116 | 117 | 118 | def test_ptr_record_for(): 119 | eq_(dnsq.ptr_record_for('127.0.0.1'), 'localhost') 120 | 121 | # Ev: this one actually "calls the internet". Which is pretty bad from the maintenance 122 | # perspective, but I am not sure how else to reliably cover it: 123 | eq_(dnsq.ptr_record_for('50.56.21.178'), 124 | socket.gethostbyaddr('50.56.21.178')[0]) 125 | 126 | # lets test caching: 127 | with patch.object(dnsq, 'query_dns') as query_dns: 128 | query_dns.return_value = [] 129 | eq_(None, dnsq.ptr_record_for('1.1.1.1')) 130 | then = time.time() # measure time 131 | eq_(None, dnsq.ptr_record_for('1.1.1.1')) 132 | eq_(1, query_dns.call_count, "query_dns() should be called only once!" \ 133 | "... otherwise the PTR cache is not working!") 134 | 135 | time_elapsed = (time.time() - then) 136 | ok_(time_elapsed < 0.001, 137 | "PTR lookup was too slow. The cache is not working?") 138 | 139 | query_dns.side_effect = Exception('Bam!') 140 | eq_(None, dnsq.ptr_record_for('1.1.1.2')) 141 | 142 | 143 | @patch.object(dnsq, 'query_dns') 144 | def test_get_primary_nameserver(query_dns): 145 | query_dns.side_effect = [[], ['srv1.com.', 'srv2.com.']] 146 | eq_('srv1.com', dnsq.get_primary_nameserver('tratata.ololo.com')) 147 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup(name='dnsq', 4 | version='1.3.0', 5 | description='DNS Query Tool', 6 | long_description=open("README.rst").read(), 7 | author='Rackspace', 8 | author_email='admin@mailgunhq.com', 9 | license='Apache 2', 10 | url='http://www.mailgun.com', 11 | py_modules=['dnsq'], 12 | zip_safe=True, 13 | install_requires=[ 14 | 'dnspython>=1.11.1', 15 | 'expiringdict>=1.1']) 16 | --------------------------------------------------------------------------------