# port_scan.py - quét port TCP của MỘT thiết bị trong mạng của BẠN.
# Chỉ dùng thư viện có sẵn của Python 3.
# Chỉ chạy trên thiết bị của chính bạn hoặc nơi được cho phép.
# Chỉ nhận IP riêng (192.168.x.x, 10.x.x.x, 127.0.0.1...).
# Ví dụ: python3 port_scan.py 127.0.0.1            (quét các port phổ biến)
#        python3 port_scan.py 192.168.1.1 -p 1-1024
#        python3 port_scan.py 192.168.1.1 -p 22,80,443
import argparse
import ipaddress
import socket
import time
from concurrent.futures import ThreadPoolExecutor

COMMON = [21, 22, 23, 25, 53, 80, 110, 135, 139, 143, 443, 445, 554, 631, 993, 995, 1723, 3306, 3389,
          5000, 5900, 7000, 8000, 8008, 8080, 8443, 9100, 62078]
MAX_PORTS = 2000   # giới hạn số port mỗi lần quét, tránh gõ nhầm 1-65535 làm nghẽn máy


def parse_ports(text):
    # Đổi chuỗi người dùng gõ thành danh sách số port. Chấp nhận "22,80,443" hoặc
    # "1-1024" hoặc kết hợp "22,80,8000-8100"
    ports = set()
    for part in text.split(","):
        part = part.strip()
        if "-" in part:
            a, b = part.split("-", 1)
            ports.update(range(int(a), int(b) + 1))
        elif part:
            ports.add(int(part))
    if not ports or min(ports) < 1 or max(ports) > 65535:
        raise ValueError("port phai nam trong khoang 1-65535")
    return sorted(ports)


def probe(ip, port, timeout):
    # Thử "gõ cửa" một port bằng một kết nối TCP.
    # Ba kết quả có thể: open / closed / filtered.
    # [1] Tạo socket TCP (SOCK_STREAM: có bắt tay ba bước)
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        # [2] Chỉ chờ tối đa `timeout` giây; im lặng quá lâu thì ngắt, không để
        #     chương trình treo
        s.settimeout(timeout)
        try:
            # [3] Kết nối thành công = bắt tay xong = có dịch vụ đang nghe -> "open"
            s.connect((ip, port))
            return port, "open"
        except ConnectionRefusedError:
            return port, "closed"      # có người trả lời "không có dịch vụ ở đây" (gói RST) -> "closed"
        except (socket.timeout, OSError):
            return port, "filtered"    # im lặng hoặc không tới được: thường là tường lửa chặn -> "filtered"


def service_name(port):
    try:
        return socket.getservbyport(port, "tcp")
    except OSError:
        return "?"


def main():
    ap = argparse.ArgumentParser(description="Quet port TCP cua mot thiet bi trong mang cua ban")
    ap.add_argument("ip", help="IP rieng can quet, vi du 192.168.1.1 hoac 127.0.0.1")
    ap.add_argument("-p", "--ports", help="danh sach port, vi du 22,80,443 hoac 1-1024 (mac dinh: ~30 port pho bien)")
    ap.add_argument("--timeout", type=float, default=2.0, help="so giay cho moi port (mac dinh 2; router cham tra loi can timeout dai)")
    args = ap.parse_args()

    # [1] Phân tích chuỗi IP người dùng gõ (sai định dạng thì tự báo lỗi, dừng ngay)
    ip = ipaddress.ip_address(args.ip)
    # [2] Chốt chặn: chỉ nhận IP riêng hoặc 127.0.0.1; IP Internet bị từ chối ngay
    if not (ip.is_private or ip.is_loopback):
        raise SystemExit("Chi quet IP rieng (192.168.x.x, 10.x.x.x, 127.0.0.1...). Khong quet Internet.")
    # [3] Lấy danh sách port: người dùng gõ -p thì phân tích, không thì dùng bảng
    #     COMMON; gõ sai thì thoát kèm lý do
    try:
        ports = parse_ports(args.ports) if args.ports else COMMON
    except ValueError as e:
        raise SystemExit("Loi port: " + str(e))
    # [4] Giới hạn số port mỗi lần quét
    if len(ports) > MAX_PORTS:
        raise SystemExit(f"Toi da {MAX_PORTS} port moi lan. Ban nhap {len(ports)}.")

    print(f"Dang quet {ip} ({len(ports)} port), timeout {args.timeout}s moi port...")
    # [1] Ghi lại giờ bắt đầu để lát nữa tính xem quét mất bao lâu
    start = time.time()
    # [2] Tạo 100 luồng làm việc cùng lúc; mỗi luồng thử một port
    with ThreadPoolExecutor(max_workers=100) as pool:
        # [3] pool.map giao từng port cho một luồng đang rảnh, đợi tất cả xong rồi
        #     trả danh sách (port, trạng thái)
        results = list(pool.map(lambda p: probe(str(ip), p, args.timeout), ports))
    count = {"open": 0, "closed": 0, "filtered": 0}
    print("PORT    TRANG THAI  DICH VU")
    for port, state in results:
        count[state] += 1
        if state == "open":
            print(f"{port:<7} {state:<11} {service_name(port)}")
    print(f"\nmo: {count['open']}  dong: {count['closed']}  im lang (filtered): {count['filtered']}  "
          f"(xong sau {time.time() - start:.1f} giay)")


if __name__ == "__main__":
    main()
