├── __init__.py ├── tests ├── __init__.py ├── test_helper.py └── test_pyscanner.py ├── pyportscanner ├── __init__.py ├── etc │ ├── __init__.py │ ├── helper.pyc │ ├── __init__.pyc │ ├── service_port.py │ └── helper.py └── pyscanner.py ├── _config.yml ├── ExampleGraph └── portscanner_output_new.png ├── .travis.yml ├── CHANGELOG.md ├── setup.py ├── .gitignore ├── README.md ├── examples └── PortScanExample.py └── LICENSE /__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pyportscanner/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pyportscanner/etc/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-slate -------------------------------------------------------------------------------- /pyportscanner/etc/helper.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YaokaiYang-assaultmaster/py3PortScanner/HEAD/pyportscanner/etc/helper.pyc -------------------------------------------------------------------------------- /pyportscanner/etc/__init__.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YaokaiYang-assaultmaster/py3PortScanner/HEAD/pyportscanner/etc/__init__.pyc -------------------------------------------------------------------------------- /ExampleGraph/portscanner_output_new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YaokaiYang-assaultmaster/py3PortScanner/HEAD/ExampleGraph/portscanner_output_new.png -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - '3.6' 4 | - '3.5' 5 | - '3.4' 6 | 7 | matrix: 8 | fast_finish: true 9 | 10 | install: true 11 | 12 | script: pytest 13 | -------------------------------------------------------------------------------- /tests/test_helper.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest.mock import Mock, patch 3 | 4 | from os import sys, path 5 | sys.path.append(path.dirname(path.dirname(path.abspath(__file__)))) 6 | 7 | from pyportscanner.etc import helper 8 | 9 | class HelperTest(unittest.TestCase): 10 | def test_get_domain(self): 11 | test_url = 'https://docs.python.org/3/library/unittest.mock.html#unittest.mock.Mock' 12 | result = helper.get_domain(test_url) 13 | self.assertEqual(result, 'docs.python.org') 14 | 15 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | Versions are monotonically increased based on Semantic Versioning. 4 | 5 | *** 6 | 7 | ## V0.3 (10/09/2018) 8 | 9 | ### Backward incompatible changes: 10 | No longer compatible with python 2.7. 11 | For a version compatible with python 2.7, refer to [v0.2](https://github.com/YaokaiYang-assaultmaster/PythonPortScanner). 12 | 13 | ### Deprecations 14 | None 15 | 16 | ### Changes 17 | Migrate to python version >= 3.0. 18 | Integrate the whole [nmap](https://github.com/nmap/nmap) port list into the project. 19 | 20 | ## V0.2 (09/12/2017) 21 | 22 | ### Backward incompatible changes 23 | None 24 | 25 | ### Deprecations 26 | None 27 | 28 | ### Changes 29 | Fixed [issue #1](https://github.com/YaokaiYang-assaultmaster/PythonPortScanner/issues/1). 30 | Specifically, fixed Windows has no `SO_REUSEPORT` for TCP socket error and change message 31 | encoding to `utf-8` before sending out. 32 | 33 | *** 34 | 35 | ## V0.1 (05/30/2017) 36 | 37 | ### Backward incompatible changes 38 | None 39 | 40 | ### Deprecations 41 | None 42 | 43 | ### Changes 44 | Initialized the whole project. First version finished. 45 | -------------------------------------------------------------------------------- /pyportscanner/etc/service_port.py: -------------------------------------------------------------------------------- 1 | from functools import total_ordering 2 | 3 | 4 | @total_ordering 5 | class ServicePort(object): 6 | """ 7 | A class wrapper for storing the information 8 | parsed out from nmap-services.txt file, 9 | including the service name, port number, protocol and open frequency. 10 | 11 | Sorting of the ServicePort object is done in the following ways: 12 | 1. Those with higher frequency are considered as greater. 13 | 2. If two objects have the same frequency, the one with a larger port number 14 | is considered as greater. 15 | """ 16 | def __init__(self, service_name, port_num, proto, freq): 17 | self.service_name = service_name 18 | self.port_num = port_num 19 | self.proto = proto 20 | self.freq = freq 21 | 22 | def __eq__(self, other): 23 | return self.freq == other.freq and self.port_num == other.port_num 24 | 25 | def __ne__(self, other): 26 | return self.freq != other.freq or self.port_num != other.port_num 27 | 28 | def __lt__(self, other): 29 | if self.freq == other.freq: 30 | return self.port_num < other.port_num 31 | else: 32 | return self.freq > other.freq 33 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from setuptools import setup, find_packages 4 | 5 | here = os.path.abspath(os.path.dirname(__file__)) 6 | with open(os.path.join(here, 'README.md')) as f: 7 | README = f.read() 8 | with open(os.path.join(here, 'CHANGELOG.md')) as f: 9 | CHANGES = f.read() 10 | 11 | dev_requires = [ 12 | 'pytest', 13 | 'pytest-cov', 14 | ] 15 | 16 | 17 | setup( 18 | author='Yaokai Yang', 19 | name='pyportscanner', 20 | version='0.3.2', 21 | description='Port Scanner for Python3+', 22 | long_description=README, 23 | long_description_content_type='text/markdown', 24 | classifiers=[ 25 | "Development Status :: 4 - Beta", 26 | "Intended Audience :: Developers", 27 | "Intended Audience :: System Administrators", 28 | "License :: OSI Approved :: Apache Software License", 29 | "Operating System :: Microsoft :: Windows", 30 | "Operating System :: POSIX", 31 | "Programming Language :: Python", 32 | "Topic :: System :: Networking :: Monitoring", 33 | "Topic :: Software Development :: Libraries :: Python Modules", 34 | "Programming Language :: Python :: 3", 35 | "Programming Language :: Python :: 3.4", 36 | "Programming Language :: Python :: 3.5", 37 | "Programming Language :: Python :: 3.6", 38 | "Programming Language :: Python :: 3.7", 39 | ], 40 | url='https://github.com/YaokaiYang-assaultmaster/py3PortScanner', 41 | packages=find_packages(), 42 | package_data={'pyportscanner': ['etc/*.dat']}, 43 | include_package_data=True, 44 | zip_safe=False, 45 | extras_require={ 46 | 'dev': dev_requires, 47 | }, 48 | ) 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | .hypothesis/ 50 | .pytest_cache/ 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django stuff: 57 | *.log 58 | local_settings.py 59 | db.sqlite3 60 | 61 | # Flask stuff: 62 | instance/ 63 | .webassets-cache 64 | 65 | # Scrapy stuff: 66 | .scrapy 67 | 68 | # Sphinx documentation 69 | docs/_build/ 70 | 71 | # PyBuilder 72 | target/ 73 | 74 | # Jupyter Notebook 75 | .ipynb_checkpoints 76 | 77 | # IPython 78 | profile_default/ 79 | ipython_config.py 80 | 81 | # pyenv 82 | .python-version 83 | 84 | # celery beat schedule file 85 | celerybeat-schedule 86 | 87 | # SageMath parsed files 88 | *.sage.py 89 | 90 | # Environments 91 | .env 92 | .venv 93 | env/ 94 | venv/ 95 | ENV/ 96 | env.bak/ 97 | venv.bak/ 98 | 99 | # Spyder project settings 100 | .spyderproject 101 | .spyproject 102 | 103 | # Rope project settings 104 | .ropeproject 105 | 106 | # mkdocs documentation 107 | /site 108 | 109 | # mypy 110 | .mypy_cache/ 111 | .dmypy.json 112 | dmypy.json 113 | 114 | # Pyre type checker 115 | .pyre/ 116 | 117 | # IDEA index file 118 | .idea 119 | -------------------------------------------------------------------------------- /pyportscanner/etc/helper.py: -------------------------------------------------------------------------------- 1 | import re 2 | from urllib.parse import urlparse 3 | import pkg_resources 4 | 5 | from pyportscanner.etc.service_port import ServicePort 6 | 7 | 8 | def read_input(): 9 | """ 10 | Read the 'nmap-services.txt' file and store all the information into 11 | a dict() of {port, ServicePort} pairs for reference later. 12 | """ 13 | resource_package = __name__ 14 | resource_path = 'nmap-services.dat' 15 | resource = pkg_resources.resource_stream(resource_package, resource_path) 16 | 17 | port_map = dict() 18 | line_regex = '([a-zA-Z0-9-]+)\s+(\d+)/(\w+)\s+(\d+\.\d+)\s+(\#.*)' 19 | pattern = re.compile(line_regex) 20 | for line in resource: 21 | line = line.decode('utf-8') 22 | # skip comments 23 | if line.startswith('#'): 24 | continue 25 | result = pattern.match(line) 26 | if result: 27 | service_name = result.group(1) 28 | port_num = int(result.group(2)) 29 | proto = result.group(3) 30 | freq = float(result.group(4)) 31 | service_port = ServicePort(service_name, port_num, proto, freq) 32 | if port_num not in port_map: 33 | port_map[port_num] = service_port 34 | elif port_map[port_num].freq < freq: 35 | # only keeps the port and protocol with highest usage frequency 36 | port_map[port_num] = service_port 37 | 38 | return port_map 39 | 40 | 41 | def get_domain(url): 42 | """ 43 | Return the hostname, or domain name, of a url. 44 | e.g. for 'http://google.com/path', it will return google.com 45 | :param url: String url 46 | :return: hostname of the url (note the hostname does not include the protocol part) 47 | """ 48 | if not url: 49 | return u"" 50 | full_url = u"http://{}" 51 | if not url.startswith(u"http://") and not url.startswith(u"https://"): 52 | # otherwise the urlparse will return empty values 53 | full_url = full_url.format(url) 54 | else: 55 | full_url = url 56 | parse_result = urlparse(full_url) 57 | return parse_result.hostname 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python Port Scanner v0.3 2 | 3 | [![Build Status](https://travis-ci.com/YaokaiYang-assaultmaster/py3PortScanner.svg?branch=master)](https://travis-ci.com/YaokaiYang-assaultmaster/py3PortScanner) 4 | 5 | An easy to use Python3 package that could perform port scanning conveniently. 6 | 7 | An output example is showed as following: 8 | ![Output Example](https://github.com/YaokaiYang-assaultmaster/py3PortScanner/blob/master/ExampleGraph/portscanner_output_new.png) 9 | 10 | ## Installation 11 | ### Install with pip 12 | ``` 13 | pip install pyportscanner 14 | ``` 15 | 16 | ### Install with setup scripts 17 | 1. Clone or download this repository. 18 | 2. Install the package using `python setup.py install`. 19 | 3. Voilà! You are ready to go! 20 | 21 | ## QuickStart 22 | 1. Add `from pyportscanner import pyscanner` to the beginning of your code. 23 | 2. Initialize a new PortScanner object using `scanner = pyscanner.PortScanner(target_ports=100, timeout=10, verbose=True)`. 24 | 3. Then call `scanner.scan(objective)` to perform a port scan to a specific target. 25 | The target could either be an IPv4 address or a host name. 26 | 4. __Note that the total scan time for a target website is highly related to the timeout value set for the Scanner object. Thus for the seek of efficiency, the timeout should not be too long.__ 27 | 28 | ## Documentation 29 | ### _class pyportscanner.pyscanner.PortScanner(target_ports=None, thread_limit=100, timeout=10, verbose=False)_ 30 | PortScanner is the class provides methods to execute the port scan request. A PortScanner object is needed for performing 31 | the port scan request. 32 | 33 | - _target_ports_ can be a list or int. If this args is a list, then the list of ports specified by it is going to be scanned, 34 | default to all ports we have in file. If this args is an int, then it specifies the top X number of ports to be scanned based on usage 35 | frequency rank. 36 | - _thread_limit_ is the number of thread being used for scan. 37 | - _timeout_ is the timeout for the socket to wait for a response. 38 | - _verbose_ specifies whether the results would be print out or not. If `True`, results will be print out. 39 | 40 | ### _Functions_ 41 | __PortScanner.scan(objective, message = '')__ 42 | 43 | Scan an objective with the given message included in the packets sent out. 44 | 45 | - _objective_ is the target that is going to be scanned. It could be an IPv4 address or a hostname. 46 | - _message_ is the message that is going to be included in the scanning packets sent out. If not provided, no message will be included in the packets. 47 | 48 | An example usage case is showed in [_examples/PortScanExample.py_](https://github.com/YaokaiYang-assaultmaster/py3PortScanner/blob/master/examples/PortScanExample.py). 49 | 50 | ### Unit Test 51 | 52 | In order to run unit test, execute the following command under the root directory. 53 | 54 | ```Python 55 | pytest --cov pyportscanner/ tests/ 56 | ``` 57 | 58 | ## [Change logs](https://github.com/YaokaiYang-assaultmaster/py3PortScanner/blob/master/CHANGELOG.md) 59 | -------------------------------------------------------------------------------- /examples/PortScanExample.py: -------------------------------------------------------------------------------- 1 | from pyportscanner import pyscanner 2 | 3 | 4 | def main(): 5 | # Initialize a Scanner object that will scan top 50 commonly used ports. 6 | scanner = pyscanner.PortScanner(target_ports=100, verbose=True) 7 | 8 | host_name = 'google.com' 9 | 10 | message = 'put whatever message you want here' 11 | 12 | ''' 13 | output contains a dictionary of {port:status} pairs 14 | in which port is the list of ports we scanned 15 | and status is either 'OPEN' or 'CLOSE' 16 | ''' 17 | 18 | res = scanner.scan(host_name, message) 19 | ''' 20 | ************************************************************ 21 | 22 | Start scanning website: google.com 23 | Server ip is: 172.217.6.110 24 | 80/TCP : OPEN 25 | 26 | 2000/UDP : OPEN 27 | 28 | 5060/UDP : OPEN 29 | 30 | 8008/TCP : OPEN 31 | 32 | Host google.com scanned in 10.033627033233643 seconds 33 | Scan completed! 34 | ''' 35 | 36 | ''' 37 | > res 38 | {9: 'CLOSE', 39 | 17: 'CLOSE', 40 | 19: 'CLOSE', 41 | 21: 'CLOSE', 42 | 22: 'CLOSE', 43 | 25: 'CLOSE', 44 | 26: 'CLOSE', 45 | 49: 'CLOSE', 46 | 53: 'CLOSE', 47 | 67: 'CLOSE', 48 | 68: 'CLOSE', 49 | 69: 'CLOSE', 50 | 80: 'OPEN', 51 | 81: 'CLOSE', 52 | 88: 'CLOSE', 53 | 110: 'CLOSE', 54 | 111: 'CLOSE', 55 | 113: 'CLOSE', 56 | 123: 'CLOSE', 57 | 135: 'CLOSE', 58 | 136: 'CLOSE', 59 | 137: 'CLOSE', 60 | 138: 'CLOSE', 61 | 139: 'CLOSE', 62 | 143: 'CLOSE', 63 | 158: 'CLOSE', 64 | 161: 'CLOSE', 65 | 162: 'CLOSE', 66 | 177: 'CLOSE', 67 | 179: 'CLOSE', 68 | 199: 'CLOSE', 69 | 427: 'CLOSE', 70 | 443: 'CLOSE', 71 | 445: 'CLOSE', 72 | 465: 'CLOSE', 73 | 514: 'CLOSE', 74 | 515: 'CLOSE', 75 | 518: 'CLOSE', 76 | 520: 'CLOSE', 77 | 548: 'CLOSE', 78 | 554: 'CLOSE', 79 | 587: 'CLOSE', 80 | 593: 'CLOSE', 81 | 623: 'CLOSE', 82 | 626: 'CLOSE', 83 | 631: 'CLOSE', 84 | 646: 'CLOSE', 85 | 993: 'CLOSE', 86 | 995: 'CLOSE', 87 | 999: 'CLOSE', 88 | 1022: 'CLOSE', 89 | 1025: 'CLOSE', 90 | 1026: 'CLOSE', 91 | 1027: 'CLOSE', 92 | 1029: 'CLOSE', 93 | 1030: 'CLOSE', 94 | 1031: 'CLOSE', 95 | 1032: 'CLOSE', 96 | 1433: 'CLOSE', 97 | 1434: 'CLOSE', 98 | 1645: 'CLOSE', 99 | 1646: 'CLOSE', 100 | 1718: 'CLOSE', 101 | 1719: 'CLOSE', 102 | 1720: 'CLOSE', 103 | 1723: 'CLOSE', 104 | 1812: 'CLOSE', 105 | 1813: 'CLOSE', 106 | 1900: 'CLOSE', 107 | 2000: 'OPEN', 108 | 2001: 'CLOSE', 109 | 2049: 'CLOSE', 110 | 2222: 'CLOSE', 111 | 2223: 'CLOSE', 112 | 3283: 'CLOSE', 113 | 3389: 'CLOSE', 114 | 3456: 'CLOSE', 115 | 3703: 'CLOSE', 116 | 4045: 'CLOSE', 117 | 4500: 'CLOSE', 118 | 5000: 'CLOSE', 119 | 5060: 'OPEN', 120 | 5353: 'CLOSE', 121 | 5355: 'CLOSE', 122 | 5666: 'CLOSE', 123 | 5900: 'CLOSE', 124 | 8000: 'CLOSE', 125 | 8008: 'OPEN', 126 | 8080: 'CLOSE', 127 | 8443: 'CLOSE', 128 | 8888: 'CLOSE', 129 | 9200: 'CLOSE', 130 | 10000: 'CLOSE', 131 | 17185: 'CLOSE', 132 | 20031: 'CLOSE', 133 | 31337: 'CLOSE', 134 | 32768: 'CLOSE', 135 | 32769: 'CLOSE', 136 | 32770: 'CLOSE', 137 | 32771: 'CLOSE'} 138 | ''' 139 | 140 | 141 | if __name__ == "__main__": 142 | main() 143 | -------------------------------------------------------------------------------- /pyportscanner/pyscanner.py: -------------------------------------------------------------------------------- 1 | import concurrent.futures 2 | import platform 3 | import socket 4 | import time 5 | from collections import deque 6 | from socket import error as socket_error 7 | 8 | from pyportscanner.etc.helper import read_input, get_domain 9 | 10 | 11 | class PortScanner: 12 | @classmethod 13 | def __usage(cls): 14 | """ 15 | Return the usage information for invalid input host name. 16 | """ 17 | print('Python Port Scanner') 18 | print('Please make sure the input host name is in the form of "foo.com" or "http://foo.com!"\n') 19 | 20 | @property 21 | def timeout_val(self): 22 | return self.__timeout 23 | 24 | @timeout_val.setter 25 | def timeout_val(self, timeout): 26 | if timeout != int(timeout): 27 | raise TypeError('Timeout must be an integer') 28 | elif timeout <= 0: 29 | raise ValueError( 30 | 'Invalid timeout value: {}.' 31 | 'Timeout must be greater than 0'.format(timeout) 32 | ) 33 | else: 34 | self.__timeout = timeout 35 | 36 | @timeout_val.getter 37 | def timeout_val(self): 38 | return self.__timeout 39 | 40 | @property 41 | def thread_limit(self): 42 | return self.__thread_limit 43 | 44 | @thread_limit.setter 45 | def thread_limit(self, thread_limit): 46 | if thread_limit != int(thread_limit): 47 | raise TypeError('thread limit must be an integer') 48 | elif thread_limit <= 0 or thread_limit > 50000: 49 | self.__thread_limit = 100 50 | raise ValueError( 51 | 'Invalid thread limit {}.' 52 | 'Thread limit must be within 0 to 50000 '.format(thread_limit) 53 | ) 54 | else: 55 | self.__thread_limit = thread_limit 56 | 57 | @thread_limit.getter 58 | def thread_limit(self): 59 | return self.__thread_limit 60 | 61 | def __init__(self, target_ports=None, thread_limit=100, timeout=10, verbose=False): 62 | """ 63 | Constructor of a PortScanner object. If target_ports is a list, this list of ports will be used as 64 | the port list to be scanned. If the target_ports is a int, it should be 50, 100 or 1000, indicating 65 | which default list will be used. 66 | 67 | :param target_ports: If this args is a list, then this list of ports is going to be scanned, 68 | default to all ports we have in file. 69 | If this args is an int, then it specifies the top X number of ports to be scanned based on usage 70 | frequency rank. 71 | :type target_ports: list or int 72 | :param verbose: If True, the scanner will print out scanning result. If False, the scanner 73 | will scan silently. 74 | :type verbose boolean 75 | """ 76 | # default ports to be scanned are all ports in file 77 | self.__port_map = read_input() 78 | 79 | # default thread number limit 80 | self.__thread_limit = thread_limit 81 | 82 | # default connection timeout time in seconds 83 | self.__timeout = timeout 84 | 85 | self.__verbose = verbose 86 | 87 | if target_ports is None: 88 | self.targets = self.__port_map.keys() 89 | elif type(target_ports) == list: 90 | self.targets = target_ports 91 | elif type(target_ports) == int: 92 | self.targets = self.extract_list(target_ports) 93 | 94 | def extract_list(self, target_port_rank): 95 | """ 96 | Extract the top X ranked ports based usage frequency. 97 | If a number greater than the total number of ports we have is specified, scan all ports. 98 | 99 | :param target_port_rank: top X commonly used port list to be returned. 100 | :return: top X commonly used port list. 101 | """ 102 | if target_port_rank <= 0: 103 | raise ValueError( 104 | 'Invalid input {}. No ports can be selected'.format(target_port_rank) 105 | ) 106 | 107 | service_port_list = sorted(self.__port_map.values()) 108 | port_list = list(ele.port_num for ele in service_port_list) 109 | return sorted(port_list[:target_port_rank]) 110 | 111 | def get_target_ports(self): 112 | """ 113 | Return the list of ports being scanned. 114 | 115 | :return: list of ports scanned by current Scanner object. 116 | :rtype: list 117 | """ 118 | return self.targets 119 | 120 | def get_top_k_ports(self, k): 121 | """ 122 | Return top K commonly used ports. 123 | 124 | :param k: top K ports to be returned. 125 | :type k: int 126 | :return: top K commonly used ports. 127 | :rtype: list 128 | """ 129 | port_list = self.extract_list(k) 130 | return port_list 131 | 132 | def scan(self, objective, message=''): 133 | """ 134 | This is the function need to be called to perform port scanning. 135 | 136 | :param objective: the objective that is going to be scanned. Could be a host name or an IPv4 address. 137 | :param message: the message that is going to be included in the scanning packets 138 | in order to prevent ethical problem (default: ''). 139 | :return: a dict object containing the scan results for a given host in the form of 140 | {port_number: status} 141 | :rtype: dict 142 | """ 143 | try: 144 | socket.inet_aton(objective) 145 | host_name = objective 146 | except OSError or socket_error: 147 | # this is not an valid IPv4 address 148 | host_name = get_domain(objective) 149 | 150 | if self.__verbose: 151 | print('\n') 152 | print('*' * 60 + '\n') 153 | print('Start scanning target: {}'.format(host_name)) 154 | 155 | try: 156 | server_ip = socket.gethostbyname(host_name) 157 | if self.__verbose: 158 | print('Target IP is: {}'.format(str(server_ip))) 159 | 160 | except Exception: 161 | # If the DNS resolution of a website cannot be finished, abort the host. 162 | if self.__verbose: 163 | print('Target {} unknown! Scan failed.'.format(host_name)) 164 | self.__usage() 165 | return {} 166 | 167 | start_time = time.time() 168 | output = self.__scan_ports(server_ip, message) 169 | stop_time = time.time() 170 | 171 | if self.__verbose: 172 | print('Target {} scanned in {} seconds'.format(host_name, stop_time - start_time)) 173 | print('Scan completed!\n') 174 | 175 | return output 176 | 177 | def __scan_ports(self, ip, message): 178 | """ 179 | Controller of the __scan_ports_helper() function 180 | 181 | :param ip: the ip address that is being scanned 182 | :type ip: str 183 | :param delay: the time in seconds that a TCP socket waits until timeout 184 | :type delay: int 185 | :param message: the message that is going to be included in the scanning packets, 186 | in order to prevent ethical problem, default to ''. 187 | :type message: str 188 | :return: a dict that stores result in {port, status} style pairs. 189 | status can be 'OPEN' or 'CLOSE'. 190 | """ 191 | output = dict() 192 | for port in self.targets: 193 | output[port] = 'CLOSE' 194 | 195 | futures = deque() 196 | with concurrent.futures.ThreadPoolExecutor(max_workers=self.__thread_limit) as executor: 197 | for port in self.targets: 198 | future = executor.submit(self.__TCP_connect, ip, port, message) 199 | futures.append(future) 200 | while len(futures) >= self.__thread_limit: 201 | self.__check_futures(output, futures) 202 | time.sleep(0.01) 203 | 204 | # make sure all thread outputs are stored. 205 | while futures: 206 | self.__check_futures(output, futures) 207 | time.sleep(0.01) 208 | 209 | # Print opening ports from small to large 210 | if self.__verbose: 211 | for port in self.targets: 212 | if output[port] == 'OPEN': 213 | service = self.__port_map.get(port, None) 214 | if service: 215 | port_proto = '{}/{}'.format(port, service.proto.upper()) 216 | else: 217 | port_proto = '{}/{}'.format(port, 'UNKNOWN') 218 | print('{:10}: {:>10}\n'.format(port_proto, output[port])) 219 | 220 | return output 221 | 222 | @classmethod 223 | def __check_futures(cls, output, futures): 224 | """ 225 | Check the executing status of Futures and retrieve the results from them. 226 | :param output: dict for storing the results 227 | :param futures: list of concurrent.futures.Future object 228 | """ 229 | for _ in range(len(futures)): 230 | future = futures.popleft() 231 | if future.done(): 232 | try: 233 | port, status = future.result() 234 | output[port] = status 235 | except socket_error: 236 | pass 237 | else: 238 | futures.append(future) 239 | 240 | def __TCP_connect(self, ip, port_number, message): 241 | """ 242 | Perform status checking for a given port on a given ip address using TCP handshake 243 | 244 | :param ip: the ip address that is being scanned 245 | :type ip: str 246 | :param port_number: the port that is going to be checked 247 | :type port_number: int 248 | :param message: the message that is going to be included in the scanning packets, 249 | in order to prevent ethical problem, default to ''. 250 | :type message: str 251 | """ 252 | # Initialize the TCP socket object based on different operating systems. 253 | # All systems except for 'Windows' will be treated equally. 254 | curr_os = platform.system() 255 | if curr_os == 'Windows': 256 | TCP_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 257 | TCP_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 258 | TCP_sock.settimeout(self.__timeout) 259 | else: 260 | TCP_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 261 | TCP_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) 262 | TCP_sock.settimeout(self.__timeout) 263 | 264 | b_message = message.encode('utf-8', errors='replace') 265 | 266 | # Initialize a UDP socket to send scanning alert message if there exists an non-empty message 267 | UDP_sock = None 268 | try: 269 | if message: 270 | UDP_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 271 | UDP_sock.sendto(b_message, (ip, int(port_number))) 272 | 273 | result = TCP_sock.connect_ex((ip, int(port_number))) 274 | if message and result == 0: 275 | TCP_sock.sendall(b_message) 276 | 277 | # If the TCP handshake is successful, the port is OPEN. Otherwise it is CLOSE 278 | if result == 0: 279 | return port_number, 'OPEN' 280 | else: 281 | return port_number, 'CLOSE' 282 | 283 | except socket_error as e: 284 | # Failed to perform a TCP handshake means the port is probably close. 285 | return port_number, 'CLOSE' 286 | finally: 287 | if UDP_sock: 288 | UDP_sock.close() 289 | TCP_sock.close() 290 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /tests/test_pyscanner.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import collections 3 | from unittest.mock import Mock, patch 4 | import socket 5 | from socket import error as socket_error 6 | 7 | from concurrent import futures 8 | 9 | from os import sys, path 10 | sys.path.append(path.dirname(path.dirname(path.abspath(__file__)))) 11 | 12 | from pyportscanner import pyscanner 13 | from pyportscanner.etc.service_port import ServicePort 14 | 15 | 16 | @patch('pyportscanner.pyscanner.socket', autospec=True) 17 | @patch('pyportscanner.pyscanner.read_input', autospec=True) 18 | class PortScannerTest(unittest.TestCase): 19 | def setUp(self): 20 | self.target_ports = [80, 443] 21 | self.thread_limit = 100 22 | self.timeout = 10 23 | port_80 = ServicePort('HTTP', 80, 'TCP', 0.1) 24 | port_443 = ServicePort('TLS', 443, 'TCP', 0.09) 25 | self.mock_port_list = { 26 | 80: port_80, 27 | 443: port_443, 28 | } 29 | self.test_ip = 'test_ip_address' 30 | self.test_host = 'http://test_domain.com' 31 | self.test_domain = 'test_domain.com' 32 | 33 | def test_init_func(self, mock_read_input, mock_socket): 34 | scanner = pyscanner.PortScanner(self.target_ports, self.thread_limit, self.timeout) 35 | self.assertIsNotNone(scanner) 36 | self.assertEqual(scanner.get_target_ports(), self.target_ports) 37 | self.assertEqual(scanner.thread_limit, self.thread_limit) 38 | self.assertEqual(scanner.timeout_val, self.timeout) 39 | 40 | def test_extract_list_success(self, mock_read_input, mock_socket): 41 | mock_read_input.return_value = self.mock_port_list 42 | scanner = pyscanner.PortScanner(self.target_ports, self.thread_limit, self.timeout) 43 | result = scanner.extract_list(2) 44 | self.assertEqual(result, [80, 443]) 45 | result = scanner.extract_list(1) 46 | self.assertEqual(result, [80]) 47 | 48 | def text_extract_list_error(self, mock_read_input, mock_socket): 49 | mock_read_input.return_value = self.mock_port_list 50 | scanner = pyscanner.PortScanner(self.target_ports, self.thread_limit, self.timeout) 51 | self.assertRaises(scanner.extract_list(-1), ValueError) 52 | 53 | def test_get_target_ports(self, mock_read_input, mock_socket): 54 | mock_read_input.return_value = self.mock_port_list 55 | scanner = pyscanner.PortScanner(self.target_ports, self.thread_limit, self.timeout) 56 | result = scanner.get_target_ports() 57 | self.assertIsNotNone(result) 58 | self.assertEqual(result, self.target_ports) 59 | 60 | def test_get_top_k_ports(self, mock_read_input, mock_socket): 61 | mock_read_input.return_value = self.mock_port_list 62 | scanner = pyscanner.PortScanner(self.target_ports, self.thread_limit, self.timeout) 63 | result = scanner.get_top_k_ports(2) 64 | self.assertIsNotNone(result) 65 | self.assertEqual(result, self.target_ports) 66 | 67 | def test_scan_input_ip_success(self, mock_read_input, mock_socket): 68 | mock_read_input.return_value = self.mock_port_list 69 | mock_socket.inet_aton.return_value = None 70 | mock_socket.gethostbyname.return_value = self.test_ip 71 | mock_scan_results = { 72 | 80: 'OPEN', 73 | 443: 'CLOSE', 74 | } 75 | scanner = pyscanner.PortScanner(self.target_ports, self.thread_limit, self.timeout) 76 | # private instance methods in python are name mangled 77 | # see https://docs.python.org/3.5/tutorial/classes.html#private-variables 78 | scanner._PortScanner__scan_ports = Mock(return_value=mock_scan_results) 79 | result = scanner.scan(self.test_ip) 80 | self.assertEqual(result, mock_scan_results) 81 | mock_socket.gethostbyname.assert_called_once_with(self.test_ip) 82 | 83 | def test_scan_os_error_success(self, mock_read_input, mock_socket): 84 | mock_read_input.return_value = self.mock_port_list 85 | mock_socket.inet_aton.side_effect = OSError 86 | mock_socket.gethostbyname.return_value = self.test_ip 87 | mock_scan_results = { 88 | 80: 'OPEN', 89 | 443: 'CLOSE', 90 | } 91 | scanner = pyscanner.PortScanner(self.target_ports, self.thread_limit, self.timeout) 92 | # private instance methods in python are name mangled 93 | # see https://docs.python.org/3.5/tutorial/classes.html#private-variables 94 | scanner._PortScanner__scan_ports = Mock(return_value=mock_scan_results) 95 | result = scanner.scan(self.test_host) 96 | self.assertEqual(result, mock_scan_results) 97 | scanner._PortScanner__scan_ports.assert_called_once_with(self.test_ip, '') 98 | mock_socket.gethostbyname.assert_called_once_with(self.test_domain) 99 | 100 | def test_scan_socket_error_success(self, mock_read_input, mock_socket): 101 | mock_read_input.return_value = self.mock_port_list 102 | mock_socket.inet_aton.side_effect = socket_error 103 | mock_socket.gethostbyname.return_value = self.test_ip 104 | mock_scan_results = { 105 | 80: 'OPEN', 106 | 443: 'CLOSE', 107 | } 108 | scanner = pyscanner.PortScanner(self.target_ports, self.thread_limit, self.timeout) 109 | # private instance methods in python are name mangled 110 | # see https://docs.python.org/3.5/tutorial/classes.html#private-variables 111 | scanner._PortScanner__scan_ports = Mock(return_value=mock_scan_results) 112 | result = scanner.scan(self.test_host) 113 | self.assertEqual(result, mock_scan_results) 114 | scanner._PortScanner__scan_ports.assert_called_once_with(self.test_ip, '') 115 | mock_socket.gethostbyname.assert_called_once_with(self.test_domain) 116 | 117 | def test_scan_server_unknown(self, mock_read_input, mock_socket): 118 | mock_read_input.return_value = self.mock_port_list 119 | mock_socket.gethostbyname.side_effect = socket_error 120 | mock_socket.inet_aton.side_effect = socket_error 121 | mock_scan_results = { 122 | 80: 'OPEN', 123 | 443: 'CLOSE', 124 | } 125 | scanner = pyscanner.PortScanner(self.target_ports, self.thread_limit, self.timeout) 126 | # private instance methods in python are name mangled 127 | # see https://docs.python.org/3.5/tutorial/classes.html#private-variables 128 | scanner._PortScanner__scan_ports = Mock(return_value=mock_scan_results) 129 | result = scanner.scan(self.test_host) 130 | self.assertEqual(result, {}) 131 | scanner._PortScanner__scan_ports.assert_not_called() 132 | mock_socket.gethostbyname.assert_called_once_with(self.test_domain) 133 | 134 | @patch('pyportscanner.pyscanner.concurrent.futures.ThreadPoolExecutor', autospec=True) 135 | def test_scan_ports_success(self, mock_executor, mock_read_input, mock_socket): 136 | mock_read_input.return_value = self.mock_port_list 137 | mock_future1 = Mock(spec=futures.Future) 138 | mock_future1.done.return_value = True 139 | mock_future1.result.return_value = (80, 'OPEN') 140 | mock_future2 = Mock(spec=futures.Future) 141 | mock_future2.done.side_effect = [False, True] 142 | mock_future2.result.return_value = (443, 'OPEN') 143 | mock_executor.return_value.__enter__.return_value.submit.side_effect = [mock_future1, mock_future2] 144 | scanner = pyscanner.PortScanner(self.target_ports, self.thread_limit, self.timeout) 145 | result = scanner._PortScanner__scan_ports(self.test_ip, '') 146 | self.assertEqual(result, {80: 'OPEN', 443: 'OPEN'}) 147 | 148 | @patch('pyportscanner.pyscanner.concurrent.futures.ThreadPoolExecutor', autospec=True) 149 | def test_scan_ports_exception(self, mock_executor, mock_read_input, mock_socket): 150 | mock_read_input.return_value = self.mock_port_list 151 | mock_future1 = Mock(spec=futures.Future) 152 | mock_future1.done.return_value = True 153 | mock_future1.result.return_value = (80, 'OPEN') 154 | mock_future2 = Mock(spec=futures.Future) 155 | mock_future2.done.side_effect = [False, True] 156 | mock_future2.result.side_effect = socket_error 157 | mock_executor.return_value.__enter__.return_value.submit.side_effect = [mock_future1, mock_future2] 158 | scanner = pyscanner.PortScanner(self.target_ports, self.thread_limit, self.timeout) 159 | result = scanner._PortScanner__scan_ports(self.test_ip, '') 160 | self.assertEqual(result, {80: 'OPEN', 443: 'CLOSE'}) 161 | 162 | @patch('pyportscanner.pyscanner.concurrent.futures.ThreadPoolExecutor', autospec=True) 163 | def test_scan_ports_thread_limit(self, mock_executor, mock_read_input, mock_socket): 164 | mock_read_input.return_value = self.mock_port_list 165 | mock_future1 = Mock(spec=futures.Future) 166 | mock_future1.done.return_value = True 167 | mock_future1.result.return_value = (80, 'OPEN') 168 | mock_future2 = Mock(spec=futures.Future) 169 | mock_future2.done.side_effect = [False, True] 170 | mock_future2.result.side_effect = socket_error 171 | mock_executor.return_value.__enter__.return_value.submit.side_effect = [mock_future1, mock_future2] 172 | scanner = pyscanner.PortScanner(self.target_ports, 1, self.timeout) 173 | result = scanner._PortScanner__scan_ports(self.test_ip, '') 174 | self.assertEqual(result, {80: 'OPEN', 443: 'CLOSE'}) 175 | 176 | def test_check_futures_succes(self, mock_read_input, mock_socket): 177 | mock_read_input.return_value = self.mock_port_list 178 | mock_future1 = Mock(spec=futures.Future) 179 | mock_future1.done.return_value = True 180 | mock_future1.result.return_value = (80, 'OPEN') 181 | mock_future2 = Mock(spec=futures.Future) 182 | mock_future2.done.return_value = False 183 | test_futures = collections.deque() 184 | test_futures.append(mock_future1) 185 | test_futures.append(mock_future2) 186 | test_output = { 187 | 80: 'CLOSE', 188 | 443: 'CLOSE', 189 | } 190 | scanner = pyscanner.PortScanner(self.target_ports, self.thread_limit, self.timeout) 191 | scanner._PortScanner__check_futures(test_output, test_futures) 192 | self.assertEqual(len(test_futures), 1) 193 | self.assertEqual(test_output, {80: 'OPEN', 443: 'CLOSE'}) 194 | 195 | @patch('pyportscanner.pyscanner.platform', autospec=True) 196 | def test_TCP_connect_open(self, mock_platform, mock_read_input, mock_socket): 197 | test_message = 'test_message_djiqojiocn' 198 | mock_platform.system.return_value = 'Linux' 199 | mock_tcp_socket = Mock(spec=socket.socket) 200 | mock_tcp_socket.setsockopt.return_value = None 201 | mock_tcp_socket.settimeout.return_value = None 202 | # assume the port is open 203 | mock_tcp_socket.connect_ex.return_value = 0 204 | mock_tcp_socket.sendall.return_value = None 205 | mock_tcp_socket.close.return_value = None 206 | mock_udp_socket = Mock(spec=socket.socket) 207 | mock_udp_socket.sendto.return_value = None 208 | mock_socket.socket.side_effect = [mock_tcp_socket, mock_udp_socket] 209 | scanner = pyscanner.PortScanner(self.target_ports, self.thread_limit, self.timeout) 210 | result = scanner._PortScanner__TCP_connect(self.test_ip, 80, test_message) 211 | self.assertEqual(result, (80, 'OPEN')) 212 | mock_tcp_socket.connect_ex.assert_called_once_with((self.test_ip, 80)) 213 | mock_tcp_socket.sendall.assert_called_once_with(test_message.encode('utf8')) 214 | mock_tcp_socket.close.assert_called_once_with() 215 | mock_tcp_socket.settimeout.assert_called_once_with(self.timeout) 216 | mock_udp_socket.sendto.assert_called_once_with(test_message.encode('utf8'), (self.test_ip, 80)) 217 | mock_udp_socket.close.assert_called_once_with() 218 | 219 | @patch('pyportscanner.pyscanner.platform', autospec=True) 220 | def test_TCP_connect_close(self, mock_platform, mock_read_input, mock_socket): 221 | test_message = 'test_message_djiqojiocn' 222 | mock_platform.system.return_value = 'Linux' 223 | mock_tcp_socket = Mock(spec=socket.socket) 224 | mock_tcp_socket.setsockopt.return_value = None 225 | mock_tcp_socket.settimeout.return_value = None 226 | # assume the port is close 227 | mock_tcp_socket.connect_ex.return_value = 1 228 | mock_tcp_socket.sendall.return_value = None 229 | mock_tcp_socket.close.return_value = None 230 | mock_udp_socket = Mock(spec=socket.socket) 231 | mock_udp_socket.sendto.return_value = None 232 | mock_socket.socket.side_effect = [mock_tcp_socket, mock_udp_socket] 233 | scanner = pyscanner.PortScanner(self.target_ports, self.thread_limit, self.timeout) 234 | result = scanner._PortScanner__TCP_connect(self.test_ip, 80, test_message) 235 | self.assertEqual(result, (80, 'CLOSE')) 236 | mock_tcp_socket.connect_ex.assert_called_once_with((self.test_ip, 80)) 237 | mock_tcp_socket.sendall.assert_not_called() 238 | mock_tcp_socket.close.assert_called_once_with() 239 | mock_tcp_socket.settimeout.assert_called_once_with(self.timeout) 240 | mock_udp_socket.sendto.assert_called_once_with(test_message.encode('utf8'), (self.test_ip, 80)) 241 | mock_udp_socket.close.assert_called_once_with() 242 | 243 | @patch('pyportscanner.pyscanner.platform', autospec=True) 244 | def test_TCP_connect_socket_error(self, mock_platform, mock_read_input, mock_socket): 245 | test_message = 'test_message_djiqojiocn' 246 | mock_platform.system.return_value = 'Linux' 247 | mock_tcp_socket = Mock(spec=socket.socket) 248 | mock_tcp_socket.setsockopt.return_value = None 249 | mock_tcp_socket.settimeout.return_value = None 250 | # assume the port is close 251 | mock_tcp_socket.connect_ex.side_effect = socket_error 252 | mock_tcp_socket.sendall.return_value = None 253 | mock_tcp_socket.close.return_value = None 254 | mock_udp_socket = Mock(spec=socket.socket) 255 | mock_udp_socket.sendto.return_value = None 256 | mock_socket.socket.side_effect = [mock_tcp_socket, mock_udp_socket] 257 | scanner = pyscanner.PortScanner(self.target_ports, self.thread_limit, self.timeout) 258 | result = scanner._PortScanner__TCP_connect(self.test_ip, 80, test_message) 259 | self.assertEqual(result, (80, 'CLOSE')) 260 | mock_tcp_socket.connect_ex.assert_called_once_with((self.test_ip, 80)) 261 | mock_tcp_socket.close.assert_called_once_with() 262 | mock_tcp_socket.settimeout.assert_called_once_with(self.timeout) 263 | mock_udp_socket.sendto.assert_called_once_with(test_message.encode('utf8'), (self.test_ip, 80)) 264 | mock_udp_socket.close.assert_called_once_with() 265 | --------------------------------------------------------------------------------