3. Refactoring e Scelte di Design

In questa lezione decideremo quali sono le migliori scelte di design per proseguire la scrittura del nostro port scanner, ovvero uno strumento che ci permetta di verificare quali porte siano aperte e in ascolto sui nostri server. A questo scopo abbiamo iniziato a scrivere il codice e abbiamo suddiviso il problema in vari “sottoproblemi” organizzando lo script tra diverse funzioni.

Le funzioni stanno crescendo di numero e abbiamo quindi la necessità di raggruppare ulteriormente il nostro codice: alcune di queste funzioni utilizzano variabili comuni come OPEN_PORTS, che viene utilizzata sia nella funzione scan_port() che in if __name__ == "__main__". Adesso che dobbiamo modificare ulteriormente il codice, esso potrebbe diventare difficile da mantenere e da leggere.

È proprio in contesti come questi che ci viene in aiuto la programmazione a oggetti: creiamo una classe per il nostro scanner e definiamo i metodi propri dello scanner in sé lasciando all’esterno funzionalità di supporto come extract_json_data() e definendo le variabili come OPEN_PORTS all’interno della classe stessa.

Per prima cosa spostiamo la funzione extract_json_data() in un altro file in modo da ottenere un modulo dedicato: questo non è strettamente necessario ma ci aiuta a mantenere il codice ordinato. Creiamo quindi il file utils.py:

import json

def extract_json_data(filename):
    with open(filename, "r") as file:
        data = json.load(file)
    return data

Importiamola quindi nel nostro file p_scan.py:

from utils import extract_json_data


Inseriamo le nostre funzioni all'interno di una classe per rendere il nostro codice più pulito

Adesso possiamo iniziare a scrivere la nostra classe principale. Facciamo in modo che open_ports diventi una variabile di istanza e PORTS_DATA_FILE una variabile di classe. Indentiamo di un ulteriore livello le funzioni che abbiamo scritto nelle lezioni precedenti:

import socket
from utils import extract_json_data

class PScan:
    PORTS_DATA_FILE = "./common_ports.json"
    
    def __init__(self):
        self.open_ports = []

    def get_ports_info():
        data = extract_json_data(PORTS_DATA_FILE)
        ports_info = {int(k): v for (k, v) in data.items()}
        return ports_info

    def get_host_ip_addr(target):
        try:
            ip_addr = socket.gethostbyname(target)
        except socket.gaierror as e:
            print(f"C'è stato un errore... {e}")
        else:
            return ip_addr

    def scan_port(ip, port):
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.settimeout(1.0)
        conn_status = sock.connect_ex((ip, port))
        if conn_status == 0:
            OPEN_PORTS.append(port)
        sock.close()

if __name__ == "__main__":
    print("Programma scritto per solo scopo educativo!!!")
    target = input("Inserire Target: ")
    ip_addr = get_host_ip_addr(target)
    ports_info = get_ports_info()

    for port in ports_info.keys():
        try:
            print(f"Scanning: {ip_addr}:{port}")
            scan_port(ip_addr, port)
        except KeyboardInterrupt:
            print("\nExiting...")
            break

    print("Open Ports:")
    for port in OPEN_PORTS:
        print(str(port), ports_info[port])

Iniziamo a convertire le funzioni in metodi: get_ports_info() deve accedere alla variabile di classe PORTS_DATA_FILE, mentre ports_info viene utilizzata su più punti, quindi la definiamo tra le variabili di classe come un dizionario vuoto:

class PScan:
    PORTS_DATA_FILE = "./common_ports.json"

    def __init__(self):
        self.open_ports = []
        self.ports_info = {}

    def get_ports_info(self):
        data = extract_json_data(PScan.PORTS_DATA_FILE)
        self.ports_info = {int(k): v for (k, v) in data.items()}

Il metodo get_ports info() è pronto: estrae i dati relativi al file definito in PORTS_DATA_FILE utilizzando extract_json_data() che abbiamo importato da utils.py.

La funzione get_host_ip_addr() accetta un parametro target e ci restituisce l’indirizzo IP ad esso associato qualora non ci siano errori: in questo contesto è meglio che rimanga all’interno della classe PScan. Utilizziamo il decoratore @staticmethod per definire quei metodi che hanno una correlazione con la classe ma che non vi sono legati direttamente:

@staticmethod
def get_host_ip_addr(target):
    try:
        ip_addr = socket.gethostbyname(target)
    except socket.gaierror as e:
        print(f"C'è stato un errore... {e}")
    else:
        return ip_addr

Infatti senza il decoratore non possiamo passare target come argomento perché dovremmo passargli self. Passiamo a scan_port() e passiamogli come argomento proprio self. Questo metodo serve per aggiungere le porte alla lista open_ports, modifichiamo per questo scopo OPEN_PORTS:

def scan_port(self, ip, port):
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.settimeout(1.0)
    conn_status = sock.connect_ex((ip, port))
    if conn_status == 0:
        self.open_ports.append(port)
    sock.close()

Se guardiamo adesso il codice sotto if __name__ == "__main__", lo vedremo pieno di errori perché i metodi definiti in una classe non sono accessibili dall’esterno: aggiungiamo al costruttore __init__ una nuova variabile remote_host che inizializziamo come stringa vuota:

class PScan:

    PORTS_DATA_FILE = "./common_ports.json"

    def __init__(self):
        self.ports_info = {}
        self.open_ports = []
        self.remote_host = ""

Creiamo un nuovo metodo run() e inziamo a spostarci parte del codice di if __name__ == "__main__" e facciamo in modo che remote_host sia modificato da qui tramite get_host_ip_addr():

def run(self):
	print("Programma scritto per solo scopo educativo!!!")
    target = input("Inserire Target: ")
	self.remote_host = self.get_host_ip_addr(target)

Adesso scan_port() può utilizzare remote_host, non avrà più bisogno dell’indirizzo IP passato come parametro perché potrà reperirlo direttamente dai valori associati all’istanza:

def scan_port(self, port):
    # ...
    conn_status = sock.connect_ex((self.remote_host, port))
    # ...

Chiamiamo gets_ports_info() nel metodo run() in modo che popoli il dizionario e aggiungiamoci anche i blocchi di codice in if __name__ == "__main__" relativi ai ciclo for aggiornandoli secondo i cambiamenti che abbiamo fatto:

def run(self):
	print("Programma scritto per solo scopo educativo!!!")
    target = input("Inserire Target: ")
    self.remote_host = self.get_host_ip_addr(target)
    self.get_ports_info()

    for port in self.ports_info.keys():
        try:
            print(f"Scanning: {self.remote_host}:{port}")
            self.scan_port(port)
        except KeyboardInterrupt:
            print("\nExiting...")
            break

    print("Open Ports:")
    for port in self.open_ports:
        print(str(port), self.ports_info[port])

Ora che tutte le funzionalità del nostro scanner sono racchiuse all’interno della classe PScan, per inizializzarle modifichiamo l’istruzione if __name__ == "__main__":

if __name__ == "__main__":
    pscan = PScan()
    pscan.run()

Ecco il nostro codice completo:

import socket
from utils import extract_json_data


class PScan:

    PORTS_DATA_FILE = "./common_ports.json"

    def __init__(self):
        self.ports_info = {}
        self.open_ports = []
        self.remote_host = ""

    def get_ports_info(self):
        data = extract_json_data(PScan.PORTS_DATA_FILE)
        self.ports_info = {int(k): v for (k, v) in data.items()}

    @staticmethod
    def get_host_ip_addr(target):
        try:
            ip_addr = socket.gethostbyname(target)
        except socket.gaierror as e:
            print(f"C'è stato un errore... {e}")
        else:
            return ip_addr

    def scan_port(self, ip, port):
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.settimeout(1.0)
        conn_status = sock.connect_ex((self.remote_host, port))
        if conn_status == 0:
            self.open_ports.append(port)
        sock.close()

    def run(self):
        print("Programma scritto per solo scopo educativo!!!")
        target = input("Inserire Target: ")
        self.remote_host = self.get_host_ip_addr(target)
        self.get_ports_info()
    
        for port in self.ports_info.keys():
            try:
                print(f"Scanning: {self.remote_host}:{port}")
                self.scan_port(port)
            except KeyboardInterrupt:
                print("\nExiting...")
                break
    
        print("Open Ports:")
        for port in self.open_ports:
            print(str(port), self.ports_info[port])

if __name__ == "__main__":
    pscan = PScan()
    pscan.run()