ipify/backend/main.py
2025-12-23 21:05:31 +02:00

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)