from fastapi import FastAPI, HTTPException from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel, validator import ipaddress import re from typing import Optional app = FastAPI(title="IP Subnet Calculator API") # CORS configuration app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) class SubnetInput(BaseModel): ip_address: str cidr: Optional[int] = None subnet_mask: Optional[str] = None @validator('ip_address') def validate_ip_address(cls, v): if not v or not v.strip(): raise ValueError("IP address cannot be empty") # Check basic format ip_pattern = r'^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$' if not re.match(ip_pattern, v): raise ValueError("Invalid IP address format. Expected format: xxx.xxx.xxx.xxx") # Check each octet is valid (0-255) octets = v.split('.') for i, octet in enumerate(octets): try: num = int(octet) if num < 0 or num > 255: raise ValueError(f"Octet {i+1} must be between 0 and 255. Got: {num}") except ValueError: raise ValueError(f"Invalid octet value: {octet}") # Validate with ipaddress library try: ipaddress.IPv4Address(v) except Exception as e: raise ValueError(f"Invalid IP address: {str(e)}") return v @validator('cidr') def validate_cidr(cls, v): if v is not None: if not isinstance(v, int): raise ValueError("CIDR must be a number") if v < 0 or v > 32: raise ValueError("CIDR must be between 0 and 32") return v @validator('subnet_mask') def validate_subnet_mask(cls, v): if v is not None: if not v.strip(): raise ValueError("Subnet mask cannot be empty") # Check format parts = v.split('.') if len(parts) != 4: raise ValueError("Subnet mask must have 4 octets (e.g., 255.255.255.0)") # Validate with ipaddress try: mask = ipaddress.IPv4Address(v) # Check if it's a valid subnet mask (contiguous 1s followed by 0s) mask_int = int(mask) # Valid masks have all 1s on the left and all 0s on the right # Check by converting to binary and verifying pattern binary = bin(mask_int)[2:].zfill(32) if '01' in binary: # If there's a 0 followed by 1, it's invalid raise ValueError("Invalid subnet mask: bits must be contiguous (all 1s followed by all 0s)") except Exception as e: raise ValueError(f"Invalid subnet mask: {str(e)}") return v class SubnetInfo(BaseModel): ip_address: str ip_address_binary: str cidr: int subnet_mask: str subnet_mask_binary: str network_id: str network_id_binary: str broadcast_address: str broadcast_binary: str first_usable_ip: str last_usable_ip: str total_hosts: int usable_hosts: int wildcard_mask: str wildcard_binary: str network_class: str is_private: bool ip_type: str @app.get("/") def read_root(): return {"message": "IP Subnet Calculator API", "version": "1.0.0"} @app.post("/calculate", response_model=SubnetInfo) def calculate_subnet(subnet_input: SubnetInput): try: # Validate that at least one of CIDR or subnet mask is provided if subnet_input.cidr is None and subnet_input.subnet_mask is None: raise HTTPException( status_code=400, detail="You must provide either CIDR notation (e.g., 24) or subnet mask (e.g., 255.255.255.0)" ) # Determine CIDR notation if subnet_input.cidr is not None: cidr = subnet_input.cidr ip_with_cidr = f"{subnet_input.ip_address}/{cidr}" elif subnet_input.subnet_mask is not None: # Convert subnet mask to CIDR try: subnet_mask = ipaddress.IPv4Address(subnet_input.subnet_mask) cidr = sum(bin(int(x)).count('1') for x in str(subnet_mask).split('.')) # Validate CIDR is reasonable if cidr == 0: raise HTTPException( status_code=400, detail="Invalid subnet mask: 0.0.0.0 is not a valid subnet mask" ) ip_with_cidr = f"{subnet_input.ip_address}/{cidr}" except Exception as e: raise HTTPException( status_code=400, detail=f"Invalid subnet mask format: {str(e)}" ) # Validate the IP address try: ip_obj = ipaddress.IPv4Address(subnet_input.ip_address) except Exception as e: raise HTTPException( status_code=400, detail=f"Invalid IP address format: {str(e)}" ) # Create network object try: network = ipaddress.IPv4Network(ip_with_cidr, strict=False) except Exception as e: raise HTTPException( status_code=400, detail=f"Cannot create network from IP {subnet_input.ip_address} with CIDR /{cidr}: {str(e)}" ) # Calculate network class first_octet = int(subnet_input.ip_address.split('.')[0]) if first_octet < 128: network_class = "A" elif first_octet < 192: network_class = "B" elif first_octet < 224: network_class = "C" elif first_octet < 240: network_class = "D (Multicast)" else: network_class = "E (Reserved)" # Calculate wildcard mask subnet_mask_int = int(network.netmask) wildcard_int = (2**32 - 1) ^ subnet_mask_int wildcard_mask = str(ipaddress.IPv4Address(wildcard_int)) # Get first and last usable IPs - Calculate mathematically to avoid memory issues total_hosts = network.num_addresses if cidr == 32: # Single host first_usable = str(network.network_address) last_usable = str(network.network_address) usable_hosts = 1 elif cidr == 31: # Point-to-point network (RFC 3021) first_usable = str(network.network_address) last_usable = str(network.broadcast_address) usable_hosts = 2 else: # Calculate first and last usable IPs mathematically # First usable = network address + 1 # Last usable = broadcast address - 1 network_int = int(network.network_address) broadcast_int = int(network.broadcast_address) first_usable = str(ipaddress.IPv4Address(network_int + 1)) last_usable = str(ipaddress.IPv4Address(broadcast_int - 1)) usable_hosts = total_hosts - 2 # Exclude network and broadcast addresses # Helper function to convert IP to binary representation def ip_to_binary(ip_str): ip = ipaddress.IPv4Address(ip_str) # Convert to 32-bit binary and format with dots every 8 bits binary = bin(int(ip))[2:].zfill(32) return f"{binary[0:8]}.{binary[8:16]}.{binary[16:24]}.{binary[24:32]}" # Determine IP type if ip_obj.is_loopback: ip_type = "Loopback" elif ip_obj.is_multicast: ip_type = "Multicast" elif ip_obj.is_link_local: ip_type = "Link-Local" elif ip_obj.is_reserved: ip_type = "Reserved" else: ip_type = "Unicast" return SubnetInfo( ip_address=subnet_input.ip_address, ip_address_binary=ip_to_binary(subnet_input.ip_address), cidr=cidr, subnet_mask=str(network.netmask), subnet_mask_binary=ip_to_binary(str(network.netmask)), network_id=str(network.network_address), network_id_binary=ip_to_binary(str(network.network_address)), broadcast_address=str(network.broadcast_address), broadcast_binary=ip_to_binary(str(network.broadcast_address)), first_usable_ip=first_usable, last_usable_ip=last_usable, total_hosts=network.num_addresses, usable_hosts=usable_hosts, wildcard_mask=wildcard_mask, wildcard_binary=ip_to_binary(wildcard_mask), network_class=network_class, is_private=ip_obj.is_private, ip_type=ip_type ) except HTTPException: # Re-raise HTTP exceptions as-is raise except ValueError as e: # Pydantic validation errors raise HTTPException(status_code=400, detail=str(e)) except Exception as e: # Unexpected errors raise HTTPException( status_code=500, detail=f"Unexpected error during calculation: {str(e)}" ) @app.get("/health") def health_check(): return {"status": "healthy"} if __name__ == "__main__": import uvicorn uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)