├── setup.cfg ├── .travis.yml ├── .gitignore ├── LICENSE ├── domcheck ├── __init__.py └── strategies.py ├── test.py ├── setup.py └── README.rst /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal=1 3 | [flake8] 4 | max-line-length = 160 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - '2.7' 4 | - '3.3' 5 | - pypy 6 | script: python setup.py test 7 | after_script: flake8 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | *,cover 45 | 46 | # Translations 47 | *.mo 48 | *.pot 49 | 50 | # Django stuff: 51 | *.log 52 | 53 | # Sphinx documentation 54 | docs/_build/ 55 | 56 | # PyBuilder 57 | target/ 58 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Olivier Poitrey 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /domcheck/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from .strategies import check_dns_txt, check_dns_cname, check_meta_tag, check_html_file 4 | 5 | 6 | def check(domain, prefix, code, strategies='*'): 7 | """ 8 | Check the ownership of a domain by going thru a serie of strategies. 9 | If at least one strategy succeed, the domain is considered verified, 10 | and this methods returns true. 11 | 12 | The prefix is a fixed DNS safe string like "yourservice-domain-verification" 13 | and the code is a random value associated to this domain. It is advised to 14 | prefix the code by a fixed value that is unique to your service like 15 | "yourservice2k3dWdk9dwz". 16 | 17 | By default all available strategies are tested. You can limit the check 18 | to a limited set of strategies by passing a comma separated list of 19 | strategy names like "nds_txt,dns_cname". See the "strategies" module 20 | for a full list of avaialble strategies. 21 | """ 22 | if strategies == '*' or 'dns_txt' in strategies: 23 | if check_dns_txt(domain, prefix, code): 24 | return True 25 | if strategies == '*' or 'dns_cname' in strategies: 26 | if check_dns_cname(domain, prefix, code): 27 | return True 28 | if strategies == '*' or 'meta_tag' in strategies: 29 | if check_meta_tag(domain, prefix, code): 30 | return True 31 | if strategies == '*' or 'html_file' in strategies: 32 | if check_html_file(domain, prefix, code): 33 | return True 34 | return False 35 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import unittest 4 | from domcheck.strategies import search_meta_tag 5 | 6 | 7 | class TestMetaSearch(unittest.TestCase): 8 | def test_basic(self): 9 | self.assertTrue(search_meta_tag('', 'foo', 'bar')) 10 | 11 | def test_xhtml(self): 12 | self.assertTrue(search_meta_tag('', 'foo', 'bar')) 13 | 14 | def test_multispaces(self): 15 | self.assertTrue(search_meta_tag('', 'foo', 'bar')) 16 | 17 | def test_singlequote(self): 18 | self.assertTrue(search_meta_tag('', 'foo', 'bar')) 19 | 20 | def test_inverted_attrs(self): 21 | self.assertTrue(search_meta_tag('', 'foo', 'bar')) 22 | self.assertTrue(search_meta_tag('', 'foo', 'bar')) 23 | 24 | def test_out_of_head(self): 25 | self.assertFalse(search_meta_tag('', 'foo', 'bar')) 26 | self.assertFalse(search_meta_tag('', 'foo', 'bar')) 27 | 28 | def test_non_matching_quotes(self): 29 | self.assertFalse(search_meta_tag('', 'foo', 'bar')) 30 | self.assertFalse(search_meta_tag('', 'foo', 'bar')) 31 | self.assertFalse(search_meta_tag('', 'foo', 'bar')) 32 | self.assertFalse(search_meta_tag('', 'foo', 'bar')) 33 | 34 | if __name__ == '__main__': 35 | unittest.main() 36 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import sys 4 | 5 | try: 6 | from setuptools import setup 7 | except ImportError: 8 | from distutils.core import setup 9 | 10 | with open('README.rst', 'r') as f: 11 | readme = f.read() 12 | 13 | if sys.version_info >= (3, 2): 14 | install_requires = ["dnspython3 >= 1.12.0"] 15 | else: 16 | install_requires = ["dnspython >= 1.12.0"] 17 | 18 | setup( 19 | name='domcheck', 20 | version='1.1.4', 21 | description='Domain Ownership Checker', 22 | long_description=readme, 23 | author='Olivier Poitrey', 24 | author_email='rs@dailymotion.com', 25 | url='https://github.com/rs/domcheck', 26 | keywords=["domain", "validation", "verification", "check", "ownership", "dns", "txt", "cname", "meta"], 27 | packages=['domcheck'], 28 | package_dir={'domcheck': 'domcheck'}, 29 | install_requires=install_requires, 30 | test_suite='test', 31 | tests_require=['flake8', 'nose'], 32 | license="MIT", 33 | classifiers=[ 34 | 'Development Status :: 4 - Beta', 35 | 'Environment :: Web Environment', 36 | 'Environment :: No Input/Output (Daemon)', 37 | 'Environment :: Console', 38 | 'Intended Audience :: Developers', 39 | 'Intended Audience :: System Administrators', 40 | 'Intended Audience :: Telecommunications Industry', 41 | 'License :: OSI Approved :: MIT License', 42 | 'Operating System :: Unix', 43 | 'Programming Language :: Python', 44 | 'Programming Language :: Python :: 2.7', 45 | 'Programming Language :: Python :: 3.4', 46 | 'Topic :: Internet :: WWW/HTTP', 47 | 'Topic :: Internet :: WWW/HTTP :: HTTP Servers', 48 | 'Topic :: Internet :: WWW/HTTP :: Site Management', 49 | 'Topic :: Security', 50 | ] 51 | ) 52 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Domcheck: Domain Ownership Validation 2 | ===================================== 3 | 4 | .. image:: https://img.shields.io/pypi/v/domcheck.svg 5 | :target: https://pypi.python.org/pypi/domcheck 6 | 7 | .. image:: https://travis-ci.org/rs/domcheck.svg?branch=master 8 | :target: https://travis-ci.org/rs/domcheck 9 | 10 | This Python library implements different strategies to validate the ownership of a domain name. 11 | 12 | Available strategies 13 | -------------------- 14 | 15 | All strategies takes 3 arguments: the domain to check, a static DNS safe ``prefix`` like "yourservice-domain-verification" and a randomly generated ``code``. 16 | 17 | - **DNS TXT record**: checks for the ``{prefix}-{code}`` string present in one of the ``TXT`` records on the domain name. 18 | - **DNS CNAME record**: checks for the existence of ``CNAME` record composed on the static ``{prefix}-{code}`` on the domain pointing to domain (usually yours) which the host is {prefix} (i.e.: {prefix}.yourdomain.com). NOTE: you may want to make sure that {prefix}.yourdomain.com resolves to something as some zone editors may check that. 19 | - **Meta Tag**: checks for the presence of a ```` tag in the ```` part of the domain's home page using either HTTP or HTTPs protocols. 20 | - **HTML File**: checks for the presence of a file named ``{code}.html`` at the root of the domain's website containing the string ``{prefix}={code}`` using either HTTP or HTTPs protocols. 21 | 22 | Usage Example 23 | ------------- 24 | 25 | Simple usage will check the domain with all available strategies and return ``True`` if the validation passed: 26 | 27 | .. code-block:: python 28 | 29 | import domcheck 30 | 31 | domain = "example.com" 32 | prefix = "myservice-domain-verification" 33 | code = "myserviceK2d8a0xdhh" 34 | 35 | if domcheck.check(domain, prefix, code): 36 | print("This domain is verified") 37 | 38 | 39 | You may filter strategies by passing a coma separated list of strategies: 40 | 41 | .. code-block:: python 42 | 43 | domcheck.check(domain, prefix, code, strategies="dns_txt,meta_tag") 44 | 45 | Installation 46 | ------------ 47 | 48 | To install domcheck, simply: 49 | 50 | $ pip install domcheck 51 | 52 | Licenses 53 | -------- 54 | 55 | All source code is licensed under the `MIT License `_. 56 | -------------------------------------------------------------------------------- /domcheck/strategies.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import dns.resolver 4 | import re 5 | import logging 6 | logger = logging.getLogger(__name__) 7 | 8 | try: 9 | # For Python 3.0 and later 10 | from urllib.request import urlopen, Request 11 | except ImportError: 12 | # Fall back to Python 2's urllib2 13 | from urllib2 import urlopen, Request 14 | 15 | 16 | def check_dns_txt(domain, prefix, code): 17 | """ 18 | Validates a domain by checking that {prefix}={code} is present in the TXT DNS record 19 | of the domain to check. 20 | 21 | Returns true if verification suceeded. 22 | """ 23 | token = '{}={}'.format(prefix, code) 24 | try: 25 | for rr in dns.resolver.query(domain, 'TXT'): 26 | if token in rr.to_text(): 27 | return True 28 | except: 29 | logger.debug('', exc_info=True) 30 | return False 31 | 32 | 33 | def check_dns_cname(domain, prefix, code): 34 | """ 35 | Validates a domain by checking the existance of the CNAME record on the domain as: 36 | {prefix}-{code}.{domain} pointing to a domain (usually yours) which the host is {prefix} 37 | (i.e.: {prefix}.yourdomain.com). 38 | 39 | Returns true if verification suceeded. 40 | """ 41 | fqdn = '{}-{}.{}'.format(prefix, code, domain) 42 | try: 43 | for rr in dns.resolver.query(fqdn, 'CNAME'): 44 | if rr.to_text().startswith(prefix + '.'): 45 | return True 46 | except: 47 | logger.debug('', exc_info=True) 48 | return False 49 | 50 | 51 | def search_meta_tag(html_doc, prefix, code): 52 | """ 53 | Checks whether the html_doc contains a meta matching the prefix & code 54 | """ 55 | regex = ''.format(prefix, code) 56 | meta = re.compile(regex, flags=re.MULTILINE | re.IGNORECASE) 57 | m = meta.search(html_doc) 58 | if m: 59 | head = re.search(r'', html_doc, flags=re.IGNORECASE) 60 | if head and m.start() < head.start(): 61 | return True 62 | return False 63 | 64 | 65 | def check_meta_tag(domain, prefix, code): 66 | """ 67 | Validates a domain by checking the existance of a 68 | tag in the of the home page of the domain using either HTTP or HTTPs protocols. 69 | 70 | Returns true if verification suceeded. 71 | """ 72 | url = '://{}'.format(domain) 73 | for proto in ('http', 'https'): 74 | try: 75 | req = Request(proto + url, headers={'User-Agent': 'Mozilla/5.0; Domcheck/1.0'}) 76 | res = urlopen(req, timeout=2) 77 | if res.code == 200: 78 | # Expect the to be found in the first 100k of the page 79 | content = str(res.read(100000)) 80 | res.close() 81 | return search_meta_tag(content, prefix, code) 82 | else: 83 | res.close() 84 | except: 85 | logger.debug('', exc_info=True) 86 | return False 87 | 88 | 89 | def check_html_file(domain, prefix, code): 90 | """ 91 | Validates a domain by checking the existance of a file named {code}.html at the root of the 92 | website using either HTTP or HTTPS protocols. The file must contain {prefix}={code} in the 93 | body of the file to ensure the host isn't responding 200 to any requests. 94 | 95 | Returns true if verification suceeded. 96 | """ 97 | url = '://{}/{}.html'.format(domain, code) 98 | token = '{}={}'.format(prefix, code) 99 | for proto in ('http', 'https'): 100 | try: 101 | req = Request(proto + url, headers={'User-Agent': 'Mozilla/5.0; Domcheck/1.0'}) 102 | res = urlopen(req, timeout=2) 103 | if res.code == 200: 104 | # Read 10k max 105 | content = str(res.read(10000)) 106 | res.close() 107 | if token in content: 108 | return True 109 | else: 110 | res.close() 111 | except: 112 | logger.debug('', exc_info=True) 113 | return False 114 | --------------------------------------------------------------------------------