265 lines
9.4 KiB
Python
265 lines
9.4 KiB
Python
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)
|