├── .gitignore ├── .travis.yml ├── LICENSE.txt ├── README.md ├── ownercheck ├── __init__.py ├── apps.py ├── conf.py ├── db.py └── domains.py ├── requirements.txt ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── conf.py ├── data │ ├── google.web.html │ ├── index.html │ ├── snapchat.web.html │ ├── twitter.android.html │ └── twitter.ios.html ├── test_apps.py ├── test_db.py └── test_domains.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | .env* 2 | *.db 3 | *.pyc 4 | *.egg-info* 5 | *.cache/ 6 | __pycache__* 7 | build/* 8 | dist/* 9 | .tox* 10 | .venv3* 11 | .coverage* 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: python 3 | python: 4 | - "2.7" 5 | - "3.5" 6 | install: pip install tox-travis 7 | script: tox 8 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Checksum Labs, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Ownercheck 2 | 3 | [![Travis](https://img.shields.io/travis/FallibleInc/ownercheck.svg?maxAge=2592000)](https://github.com/FallibleInc/ownercheck) 4 | 5 | `ownercheck` can be used to verify ownership of domains and mobile apps hosted on Android Play store or iOS app store. Domains can be verified by either adding a DNS record (CNAME or a TXT record) or by adding content to the existing website(a meta tag or uploading an empty file with the specified name). Mobile apps can be verified by checking the corresponding app page on app store for mention of user's verified domain in developers section of the page. 6 | 7 | The library uses SQLite to store and match generated verification codes, which can be easily swapped with any database backend you use (check db.py, need to make it configurable later). 8 | 9 | #### Installation 10 | 11 | ``` bash 12 | pip install ownercheck 13 | ```` 14 | 15 | #### Running tests 16 | 17 | ```` 18 | pip install tox 19 | tox 20 | ```` 21 | 22 | 23 | #### How to use 24 | 25 | ##### Verify domain ownership 26 | 27 | ###### User has access to the domain DNS settings 28 | 29 | ``` python 30 | import ownercheck 31 | 32 | ownercheck.generate_code('example.com', 'CNAME') 33 | # Now proceed to add a DNS entry for CNAME 34 | ownercheck.verify_domain('example.com', 'CNAME') 35 | ``` 36 | 37 | ###### User has access to the content hosted on the domain 38 | 39 | ``` python 40 | import ownercheck 41 | 42 | ownercheck.generate_code('example.com', 'METATAG') 43 | # Now proceed to add meta tag in your index.html as directed 44 | ownercheck.verify_domain('example.com', 'METATAG') # returns a bool 45 | ``` 46 | 47 | 48 | ##### Fetch mobile app links from domain 49 | 50 | ``` python 51 | import ownercheck 52 | 53 | ownercheck.fetch_apps(domain) 54 | # Do not use this for mobile apps verification. A malacious user can link to apps they do not own. 55 | # Get all the link to apps on a page and then use ownercheck.verify_app instead. 56 | ``` 57 | 58 | 59 | ##### Verify ownership of mobile apps on Play/App Store 60 | 61 | ``` python 62 | import ownercheck 63 | 64 | app_url = '' # Your Play store or App store published app URL 65 | domain = '' # Your domain related to the app 66 | 67 | ownercheck.verify_app(app_url, domain) 68 | ``` 69 | 70 | -------------------------------------------------------------------------------- /ownercheck/__init__.py: -------------------------------------------------------------------------------- 1 | from .domains import verify_domain 2 | from .apps import verify_app, fetch_apps 3 | from .db import generate_code 4 | -------------------------------------------------------------------------------- /ownercheck/apps.py: -------------------------------------------------------------------------------- 1 | try: 2 | from HTMLParser import HTMLParser 3 | from urlparse import urlparse 4 | except ImportError: 5 | from html.parser import HTMLParser 6 | from urllib.parse import urlparse 7 | 8 | 9 | import requests 10 | 11 | 12 | class MobileAppLinksParser(HTMLParser): 13 | 14 | def __init__(self): 15 | HTMLParser.__init__(self) 16 | self.app_links = set([]) 17 | 18 | def handle_starttag(self, tag, attrs): 19 | attrs = dict(attrs) 20 | if tag == 'a' and 'href' in attrs: 21 | parsed_url = urlparse(attrs['href']) 22 | if parsed_url.netloc == 'play.google.com': 23 | self.app_links.add((attrs['href'], 'Android')) 24 | elif parsed_url.netloc == 'itunes.apple.com': 25 | self.app_links.add((attrs['href'], 'iOS')) 26 | 27 | 28 | class AllLinksParser(HTMLParser): 29 | 30 | def __init__(self): 31 | HTMLParser.__init__(self) 32 | self.links = set([]) 33 | 34 | def handle_starttag(self, tag, attrs): 35 | attrs = dict(attrs) 36 | if tag == 'a' and 'href' in attrs: 37 | self.links.add(attrs['href']) 38 | 39 | 40 | class InvalidAppStoreURL(Exception): 41 | pass 42 | 43 | 44 | def verify_app(appstore_url, domain): 45 | parsed_url = urlparse(appstore_url) 46 | app_store = None 47 | if parsed_url.netloc == 'play.google.com': 48 | app_store = 'Android' 49 | elif parsed_url.netloc == 'itunes.apple.com': 50 | app_store = 'iOS' 51 | else: 52 | raise InvalidAppStoreURL() 53 | 54 | parser = AllLinksParser() 55 | r = requests.get(appstore_url) 56 | parser.feed(r.text) 57 | 58 | for i in parser.links: 59 | if app_store == 'Android' and urlparse(i).netloc == 'www.google.com': 60 | try: 61 | # Google Play store has the developer website as a query param 62 | page_domain = urlparse( 63 | urlparse(i).query.split('&')[0].split('=')[1]).netloc 64 | if page_domain == domain: 65 | return True 66 | except: 67 | pass 68 | else: 69 | if urlparse(i).netloc == domain: 70 | return True 71 | return False 72 | 73 | 74 | def fetch_apps(url): 75 | r = requests.get(url) 76 | parser = MobileAppLinksParser() 77 | parser.feed(r.text) 78 | return list(parser.app_links) 79 | -------------------------------------------------------------------------------- /ownercheck/conf.py: -------------------------------------------------------------------------------- 1 | # Types of verification checks possible 2 | CHECK_TYPES = ['CNAME', 'TXT', 'METATAG', 'FILE'] 3 | 4 | # The CNAME value that the user has to set if using CNAME verification 5 | CNAME_VALUE = 'verify007dada.fallible.co' 6 | 7 | # The name of the meta tag the user has to create 8 | META_TAG_NAME = 'fallible' 9 | 10 | # Refer db.py 11 | SQLITE_TABLE = 'fa-ownercheck.db' 12 | 13 | # Some websites do not respond properly without appearing as browsers 14 | FAKE_USER_AGENT = 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:41.0) Gecko/20100101 Firefox/41.0' 15 | -------------------------------------------------------------------------------- /ownercheck/db.py: -------------------------------------------------------------------------------- 1 | """ 2 | If you are using some other database, the functions can be safely 3 | replaced with your own but having the same functionality and interface. 4 | """ 5 | 6 | import uuid 7 | import sqlite3 8 | from .conf import SQLITE_TABLE 9 | 10 | 11 | def setup_db(): 12 | conn = sqlite3.connect(SQLITE_TABLE) 13 | cursor = conn.cursor() 14 | try: 15 | cursor.execute(''' 16 | CREATE TABLE codes 17 | (domain text, checktype text, value text)''') 18 | conn.commit() 19 | except: 20 | pass 21 | finally: 22 | conn.close() 23 | 24 | 25 | def generate_code(domain, check_type): 26 | current_code = get_code(domain, check_type) 27 | if current_code is not None: 28 | return current_code 29 | uid = str(uuid.uuid4()) 30 | conn = sqlite3.connect(SQLITE_TABLE) 31 | c = conn.cursor() 32 | c.execute(''' 33 | INSERT INTO codes 34 | VALUES(?, ?, ?)''', 35 | (domain, check_type, uid)) 36 | conn.commit() 37 | conn.close() 38 | return uid 39 | 40 | 41 | def get_code(domain, check_type): 42 | conn = sqlite3.connect(SQLITE_TABLE) 43 | cursor = conn.cursor() 44 | cursor.execute(''' 45 | SELECT value FROM codes 46 | WHERE domain=? AND checktype=?''', 47 | (domain, check_type)) 48 | row = cursor.fetchone() 49 | if row is not None: 50 | return row[0] 51 | else: 52 | return None 53 | 54 | 55 | def remove_code(domain, check_type): 56 | conn = sqlite3.connect(SQLITE_TABLE) 57 | cursor = conn.cursor() 58 | cursor.execute(''' 59 | DELETE FROM codes WHERE domain=? AND checktype=?''', 60 | (domain, check_type)) 61 | conn.commit() 62 | conn.close() 63 | -------------------------------------------------------------------------------- /ownercheck/domains.py: -------------------------------------------------------------------------------- 1 | try: 2 | from HTMLParser import HTMLParser 3 | except ImportError: 4 | from html.parser import HTMLParser 5 | import requests 6 | import dns.resolver 7 | from .db import setup_db, get_code 8 | from .conf import CHECK_TYPES, CNAME_VALUE, META_TAG_NAME, FAKE_USER_AGENT 9 | 10 | 11 | # Create db tables if not already created 12 | setup_db() 13 | 14 | 15 | class InvalidVerificationType(Exception): 16 | pass 17 | 18 | 19 | class NoVerificationCodeExists(Exception): 20 | pass 21 | 22 | 23 | def _verify_cname(subdomain, expected_value=CNAME_VALUE): 24 | try: 25 | records = dns.resolver.query(subdomain, 'CNAME') 26 | record = records[0].to_text() 27 | if record.endswith('.'): 28 | record = record[:-1] 29 | if expected_value.endswith('.'): 30 | expected_value = expected_value[:-1] 31 | if record == expected_value: 32 | return True 33 | except dns.resolver.NoAnswer: 34 | return False 35 | except dns.resolver.Timeout: 36 | return False 37 | return False 38 | 39 | 40 | def _verify_txt_record(domain, expected_value): 41 | try: 42 | records = dns.resolver.query(domain, 'TXT') 43 | for r in records: 44 | record = r.to_text() 45 | if record.startswith('"') and record.endswith('"'): 46 | record = record[1:-1] 47 | if record == expected_value: 48 | return True 49 | except (dns.resolver.NoAnswer, dns.resolver.Timeout): 50 | return False 51 | return False 52 | 53 | 54 | class MetaTagParser(HTMLParser): 55 | """ 56 | Given a meta tag name, saves it's content in value 57 | """ 58 | 59 | def __init__(self, meta_tag_name): 60 | HTMLParser.__init__(self) 61 | self.value = None 62 | self.tag_name = meta_tag_name 63 | 64 | def handle_starttag(self, tag, attrs): 65 | attrs = dict(attrs) 66 | if tag.lower() == 'meta' and attrs.get('name') == self.tag_name: 67 | self.value = attrs['content'] 68 | 69 | 70 | def _verify_meta_tag(domain, expected_value, tag=META_TAG_NAME): 71 | r = requests.get( 72 | 'http://' + domain, 73 | headers={ 74 | 'User-Agent': FAKE_USER_AGENT}) 75 | text = r.text 76 | parser = MetaTagParser(tag) 77 | parser.feed(text) 78 | return bool(parser.value == expected_value) 79 | 80 | 81 | def _verify_file_exists(domain, filename): 82 | r = requests.get('/'.join(['http:/', domain, filename])) 83 | return bool(200 <= r.status_code < 300) 84 | 85 | 86 | def verify_domain(domain, check_type): 87 | code = get_code(domain, check_type) 88 | 89 | if check_type not in CHECK_TYPES: 90 | raise InvalidVerificationType( 91 | '%s not in %s' % (check_type, str(CHECK_TYPES))) 92 | 93 | if code is None: 94 | raise NoVerificationCodeExists( 95 | 'No verification code found for %s' 96 | % str((domain, check_type))) 97 | 98 | response = False 99 | 100 | if check_type == 'CNAME': 101 | response = _verify_cname(code) 102 | elif check_type == 'TXT': 103 | response = _verify_txt_record(domain, code) 104 | elif check_type == 'METATAG': 105 | response = _verify_meta_tag(domain, code) 106 | elif check_type == 'FILE': 107 | response = _verify_file_exists(domain, code) 108 | 109 | return response 110 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | click==6.6 2 | cookies==2.2.1 3 | dnspython==1.14.0 4 | funcsigs==1.0.2 5 | mock==2.0.0 6 | pbr==1.10.0 7 | py==1.10.0 8 | pytest==3.0.1 9 | requests==2.20.0 10 | responses==0.5.1 11 | six==1.10.0 12 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name='ownercheck', 5 | version='0.2.1', 6 | description='Verify ownership of domain and mobile apps using DNS and other methods', 7 | author='Fallible', 8 | author_email='hello@fallible.co', 9 | url='https://github.com/FallibleInc/ownercheck', 10 | download_url='https://github.com/FallibleInc/ownercheck/tarball/0.2', 11 | packages=['ownercheck'], 12 | install_requires=[ 13 | 'dnspython', 14 | 'requests', 15 | 'pytest', 16 | 'responses', 17 | ], 18 | ) 19 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | from sys import path 2 | from os.path import dirname as dir 3 | path.append(dir(path[0])) 4 | 5 | -------------------------------------------------------------------------------- /tests/conf.py: -------------------------------------------------------------------------------- 1 | TEST_DOMAIN = "google.com" 2 | 3 | IOS_URL = 'https://itunes.apple.com/in/app/twitter/id333903271?mt=8' 4 | ANDROID_URL = 'https://play.google.com/store/apps/details?id=com.twitter.android' 5 | 6 | WEB_URL = 'https://snapchat.com' -------------------------------------------------------------------------------- /tests/data/google.web.html: -------------------------------------------------------------------------------- 1 | Google 2 | -------------------------------------------------------------------------------- /tests/data/index.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FallibleInc/ownercheck/f3d85f5e889b3815887da082911e6cabea542bc1/tests/data/index.html -------------------------------------------------------------------------------- /tests/data/snapchat.web.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Snapchat 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | Want your own Snapchat Geofilter? 62 | 63 | 64 |
65 | 66 |
67 |

68 | Snapchat 69 |

70 | 71 |
72 |
73 |
74 | 75 | 76 |
164 | 165 | 166 | 167 | 168 | 169 | -------------------------------------------------------------------------------- /tests/data/twitter.ios.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Twitter on the App Store 12 | 13 | 14 | 15 | 16 | 17 | 18 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 85 | 86 | 87 | 88 | 89 | 153 |
154 |
155 | 156 |

iTunes

157 | 158 |
159 |
160 |
161 | 162 |
163 |
Opening the iTunes Store.If iTunes doesn’t open, click the iTunes icon in your Dock or on your Windows desktop.Progress Indicator
164 |
Opening the iBooks Store.If iBooks doesn't open, click the iBooks app in your Dock.Progress Indicator
165 |
166 |
167 | 168 |
169 |
170 |
iTunes
171 | 172 |

iTunes is the world's easiest way to organize and add to your digital media collection.

173 | 174 |

175 | We are unable to find iTunes on your computer. To download the free app Twitter by Twitter, Inc., get iTunes now. 176 |

177 | 178 |
179 |

Do you already have iTunes? Click I Have iTunes to open it now.

180 | 181 | I Have iTunes 182 | 183 | Free Download 184 |
185 | 186 | iTunes for Mac + PC 187 |
188 |
189 |
190 | 191 | 192 | 193 | 194 | 195 | 196 | 199 | 200 |
201 | 202 |
203 | 204 |
205 | 206 |
207 |
208 |

Twitter

209 |

By Twitter, Inc.

210 |
Essentials
211 |
212 |
213 | 214 | View More by This Developer 215 | 216 |
217 |

Open iTunes to buy and download apps.

218 |
219 | 220 |
221 | 222 |
223 | 224 |

225 | 226 | Description 227 |

228 | 229 | 230 | 231 | 232 |

See what’s happening in the world right now. From breaking news and entertainment, sports and politics, to big events and everyday interests. If it’s happening anywhere, it’s happening on Twitter. Get the full story as it unfolds, with all the live commentary.

Be part of what everyone is talking about and get videos, live footage and Moments, direct from the source.

Join in on all the action by sharing what’s happening in your world. On Twitter you can post photos with stickers, GIFs, videos, and even stream live video with the Periscope button. There is no better way to have your voice heard.

233 | 234 | 235 |
236 | 237 | 238 | 239 | 240 | 241 | 242 |
243 | 244 |

245 | 246 | What's New in Version 6.60 247 |

248 | 249 | 250 | 251 | 252 |

See your Tweets when the lights go out. Night Mode delivers a dark color palette that's made for reading in the dark.

Plus, we've released new settings that give you more control over what you see on Twitter.

253 | 254 | 255 |
256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 |
264 | 265 |
266 | 267 | 268 |

Screenshots

269 | 270 | 271 | 272 | 273 |
274 | 275 | iPhone 276 | 277 | iPad 278 | 279 |
280 | 281 | 282 | 283 | 284 |
285 | 286 |
287 | 288 |
iPhone Screenshot 1
iPhone Screenshot 2
iPhone Screenshot 3
iPhone Screenshot 4
iPhone Screenshot 5
iPad Screenshot 1
289 |
290 |
291 | 292 | 293 | 294 | 295 |
296 | 297 |
298 | 299 | 300 |

Apple Watch

301 | 302 | 303 | 304 | 305 | 306 |
307 | 308 | 309 | 310 |
iPhone Screenshot 1
iPhone Screenshot 2
iPhone Screenshot 3
iPhone Screenshot 4
iPhone Screenshot 5
311 | 312 |
313 | 314 | 315 | 316 |
317 |

Customer Reviews

318 | 319 | 320 | 321 |
322 | 323 | 324 | 325 | 326 | 327 |
328 | 329 | Good!! Need Improvements 330 | 331 | 332 |
333 | 334 | 340 | 341 | 342 | 343 | 344 | 345 |

346 | The app is good after the latest night mode. There is one thing that the night mode s not effective in the tweet activity screen. Would be good if it reflects there too. 347 |

348 | 349 | 350 |
351 | 352 | 353 | 354 |
355 | 356 | 357 | 358 | 359 | 360 |
361 | 362 | List Tab not visible 363 | 364 | 365 |
366 | 367 | 373 | 374 | 375 | 376 | 377 | 378 |

379 | The List tab available on twitter while we access it with web browsers , is absent on the Twitter app. This is a must needed thing . I have to login to twitter via web brower due to the same not availabe on app. 380 |

381 | 382 | 383 |
384 | 385 | 386 | 387 |
388 | 389 | 390 | 391 | 392 | 393 |
394 | 395 | Twitter is paying my bills. 396 | 397 | 398 |
399 | 400 | 406 | 407 | 408 | 409 | 410 | 411 |

412 | Love it. If you don't understand it, don't delete it.. Hang on, you'll get a hang of it. It's really nice after that. 413 |

414 | 415 | 416 |
417 | 418 | 419 | 420 |
421 | 422 | 423 | 424 |
425 | 426 |
427 | 428 |
429 |
Twitter
430 | View in iTunes 431 |
This app is designed for both iPhone and iPad
Offers Apple Watch App for iPhone
  • Free
  • Category: News
  • Updated:
  • Version: 6.60
  • Size: 139 MB
  • Apple Watch: Yes
  • Languages: English, Arabic, Catalan, Croatian, Czech, Danish, Dutch, Finnish, French, German, Greek, Hebrew, Hindi, Hungarian, Indonesian, Italian, Japanese, Korean, Malay, Norwegian Bokmål, Polish, Portuguese, Romanian, Russian, Simplified Chinese, Slovak, Spanish, Swedish, Thai, Traditional Chinese, Turkish, Ukrainian, Vietnamese
  • Developer:

Compatibility: Requires iOS 8.1 or later. Compatible with iPhone, iPad, and iPod touch.

432 |
433 | 434 | 435 | 436 | 437 | 438 | 439 |
440 |

Customer Ratings

441 | 442 |
Current Version:
443 | 445 | 446 | 447 | 448 | 449 | 450 | 451 |
All Versions:
452 | 454 | 455 | 456 | 457 | 458 |
459 | 460 | 461 | 462 | 463 | 464 | 465 |
466 | 467 |

468 | 469 | More Apps by Twitter, Inc. 470 |

471 | 472 | 473 | 474 | 475 | 536 | 537 | 538 |
539 | 540 |
541 |
542 |
543 |
544 | 545 | 546 |
547 | 548 | 549 | 550 | 551 | 552 | 553 | 554 | 555 | 556 |
557 | 558 |
559 | 560 | 561 | 665 | 761 | 767 | 768 | 769 | 770 | 771 | 772 | 773 | 774 | 775 | -------------------------------------------------------------------------------- /tests/test_apps.py: -------------------------------------------------------------------------------- 1 | import io 2 | import re 3 | import responses 4 | from ownercheck.apps import verify_app, fetch_apps 5 | from .conf import IOS_URL, ANDROID_URL, WEB_URL 6 | 7 | 8 | @responses.activate 9 | def test_fetch_apps(): 10 | 11 | responses.add(responses.GET, 12 | WEB_URL, 13 | body=io.open('tests/data/snapchat.web.html', encoding='utf-8').read(), 14 | status=200, 15 | content_type='text/html') 16 | assert len(fetch_apps(WEB_URL)) == 2 17 | app_types = [i[1] for i in fetch_apps(WEB_URL)] 18 | app_types.sort() 19 | assert app_types == ['Android', 'iOS'] 20 | 21 | 22 | @responses.activate 23 | def test_verify_app(): 24 | responses.add(responses.GET, 25 | ANDROID_URL, 26 | body=io.open('tests/data/twitter.android.html', encoding='utf-8').read(), 27 | status=200, 28 | content_type='text/html', 29 | match_querystring=True) 30 | responses.add(responses.GET, 31 | IOS_URL, 32 | body=io.open('tests/data/twitter.ios.html', encoding='utf-8').read(), 33 | status=200, 34 | content_type='text/html', 35 | match_querystring=True) 36 | assert verify_app(ANDROID_URL, 'twitter.com') == True 37 | assert verify_app(IOS_URL, 'twitter.com') == True 38 | 39 | -------------------------------------------------------------------------------- /tests/test_db.py: -------------------------------------------------------------------------------- 1 | from ownercheck.db import setup_db, generate_code, get_code, remove_code 2 | from .conf import TEST_DOMAIN 3 | 4 | 5 | def test_generate_code(): 6 | setup_db() 7 | assert generate_code(TEST_DOMAIN, 'TXT') != None 8 | assert generate_code(TEST_DOMAIN, 'TXT') != generate_code( 9 | TEST_DOMAIN, 'CNAME') 10 | assert generate_code(TEST_DOMAIN, 'TXT') == generate_code( 11 | TEST_DOMAIN, 'TXT') 12 | 13 | 14 | def test_get_code(): 15 | setup_db() 16 | code = generate_code(TEST_DOMAIN, 'FILE') 17 | assert get_code(TEST_DOMAIN, 'FILE') == code 18 | assert get_code(TEST_DOMAIN, 'METATAG') == None 19 | 20 | 21 | def test_remove_code(): 22 | setup_db() 23 | file_code = generate_code(TEST_DOMAIN, 'FILE') 24 | cname_code = generate_code(TEST_DOMAIN, 'CNAME') 25 | remove_code(TEST_DOMAIN, 'FILE') 26 | new_file_code = generate_code(TEST_DOMAIN, 'FILE') 27 | new_cname_code = generate_code(TEST_DOMAIN, 'CNAME') 28 | assert file_code != new_file_code 29 | assert cname_code == new_cname_code 30 | 31 | -------------------------------------------------------------------------------- /tests/test_domains.py: -------------------------------------------------------------------------------- 1 | import io 2 | import uuid 3 | import subprocess 4 | import responses 5 | from ownercheck.domains import (_verify_cname, 6 | _verify_txt_record, 7 | _verify_meta_tag, 8 | _verify_file_exists, 9 | verify_domain) 10 | from ownercheck.db import generate_code 11 | from .conf import TEST_DOMAIN 12 | 13 | 14 | def test_verify_cname(): 15 | expected = 'www3.l.google.com.' 16 | if expected.endswith("'"): 17 | expected = expected.rstrip("'") 18 | if expected.endswith("\\n"): 19 | expected = expected[:-2] 20 | print(expected) 21 | assert _verify_cname('developers.google.com', expected) == True 22 | assert _verify_cname('developers.google.com', str(uuid.uuid4())) == False 23 | 24 | 25 | def test_verify_txt_record(): 26 | expected = "v=spf1 include:_spf.google.com ~all" 27 | assert _verify_txt_record("google.com", expected) == True 28 | assert _verify_txt_record("google.com", str(uuid.uuid4())) == False 29 | 30 | 31 | @responses.activate 32 | def test_verify_meta_tag(): 33 | responses.add(responses.GET, 34 | 'http://' + TEST_DOMAIN, 35 | body=io.open('tests/data/google.web.html', encoding='utf-8').read(), 36 | status=200, 37 | content_type='text/html') 38 | assert _verify_meta_tag(TEST_DOMAIN, "origin", "referrer") == True 39 | assert _verify_meta_tag(TEST_DOMAIN, str(uuid.uuid4()), "fallible") == False 40 | 41 | 42 | @responses.activate 43 | def test_verify_file_exists(): 44 | responses.add(responses.GET, 45 | 'http://' + TEST_DOMAIN + '/robots.txt', 46 | body='', 47 | status=200, 48 | content_type='text/html') 49 | random_str = str(uuid.uuid4()) 50 | responses.add(responses.GET, 51 | 'http://' + TEST_DOMAIN + '/' + random_str, 52 | body='', 53 | status=404, 54 | content_type='text/html') 55 | assert _verify_file_exists(TEST_DOMAIN, "robots.txt") == True 56 | assert _verify_file_exists(TEST_DOMAIN, random_str) == False 57 | 58 | 59 | def test_verify_domain(): 60 | pass 61 | 62 | 63 | 64 | # def expected_cname(): 65 | # p = subprocess.Popen(['host', '-t', 'CNAME', 'developers.google.com'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) 66 | # out, err = p.communicate() 67 | # expected = str(out).split(' ')[-1].strip() 68 | # return expected 69 | 70 | # def expected_txt(): 71 | # p = subprocess.Popen(['host', '-t', 'TXT', 'google.com'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) 72 | # out, err = p.communicate() 73 | # expected = None 74 | # try: 75 | # expected = str(out).split('"')[-2].strip() 76 | # except: 77 | # pass 78 | # return expected 79 | 80 | 81 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | #skipsdist = true 3 | envlist = py27,py35 4 | 5 | [testenv] 6 | deps = -rrequirements.txt 7 | commands = py.test 8 | 9 | 10 | --------------------------------------------------------------------------------