363 lines
12 KiB
JavaScript
363 lines
12 KiB
JavaScript
import { useState } from 'react'
|
||
import axios from 'axios'
|
||
import './App.css'
|
||
|
||
const API_URL = 'http://localhost:8000'
|
||
|
||
function App() {
|
||
const [ipAddress, setIpAddress] = useState('')
|
||
const [cidr, setCidr] = useState('24')
|
||
const [subnetMask, setSubnetMask] = useState('')
|
||
const [inputType, setInputType] = useState('cidr') // 'cidr' or 'mask'
|
||
const [result, setResult] = useState(null)
|
||
const [loading, setLoading] = useState(false)
|
||
const [error, setError] = useState('')
|
||
|
||
// Validate IP address format
|
||
const validateIpAddress = (ip) => {
|
||
if (!ip || !ip.trim()) {
|
||
return 'IP address is required'
|
||
}
|
||
|
||
const ipPattern = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/
|
||
if (!ipPattern.test(ip)) {
|
||
return 'Invalid IP format. Expected: xxx.xxx.xxx.xxx'
|
||
}
|
||
|
||
const octets = ip.split('.')
|
||
for (let i = 0; i < octets.length; i++) {
|
||
const num = parseInt(octets[i])
|
||
if (isNaN(num) || num < 0 || num > 255) {
|
||
return `Invalid octet ${i + 1}: must be between 0-255`
|
||
}
|
||
}
|
||
|
||
return null
|
||
}
|
||
|
||
// Validate subnet mask format
|
||
const validateSubnetMask = (mask) => {
|
||
if (!mask || !mask.trim()) {
|
||
return 'Subnet mask is required'
|
||
}
|
||
|
||
const parts = mask.split('.')
|
||
if (parts.length !== 4) {
|
||
return 'Subnet mask must have 4 octets'
|
||
}
|
||
|
||
// Check each octet is 0-255
|
||
for (let part of parts) {
|
||
const num = parseInt(part)
|
||
if (isNaN(num) || num < 0 || num > 255) {
|
||
return 'Each octet must be between 0-255'
|
||
}
|
||
}
|
||
|
||
// Common valid subnet masks
|
||
const validMasks = [
|
||
'255.255.255.255', '255.255.255.254', '255.255.255.252', '255.255.255.248',
|
||
'255.255.255.240', '255.255.255.224', '255.255.255.192', '255.255.255.128',
|
||
'255.255.255.0', '255.255.254.0', '255.255.252.0', '255.255.248.0',
|
||
'255.255.240.0', '255.255.224.0', '255.255.192.0', '255.255.128.0',
|
||
'255.255.0.0', '255.254.0.0', '255.252.0.0', '255.248.0.0',
|
||
'255.240.0.0', '255.224.0.0', '255.192.0.0', '255.128.0.0',
|
||
'255.0.0.0', '254.0.0.0', '252.0.0.0', '248.0.0.0',
|
||
'240.0.0.0', '224.0.0.0', '192.0.0.0', '128.0.0.0', '0.0.0.0'
|
||
]
|
||
|
||
if (!validMasks.includes(mask)) {
|
||
return 'Invalid subnet mask (bits must be contiguous)'
|
||
}
|
||
|
||
return null
|
||
}
|
||
|
||
const calculateSubnet = async (e) => {
|
||
e.preventDefault()
|
||
setError('')
|
||
setResult(null)
|
||
|
||
// Frontend validation
|
||
const ipError = validateIpAddress(ipAddress)
|
||
if (ipError) {
|
||
setError(ipError)
|
||
return
|
||
}
|
||
|
||
if (inputType === 'cidr') {
|
||
const cidrNum = parseInt(cidr)
|
||
if (isNaN(cidrNum) || cidrNum < 0 || cidrNum > 32) {
|
||
setError('CIDR must be between 0 and 32')
|
||
return
|
||
}
|
||
} else {
|
||
const maskError = validateSubnetMask(subnetMask)
|
||
if (maskError) {
|
||
setError(maskError)
|
||
return
|
||
}
|
||
}
|
||
|
||
setLoading(true)
|
||
|
||
try {
|
||
const payload = {
|
||
ip_address: ipAddress,
|
||
}
|
||
|
||
if (inputType === 'cidr') {
|
||
payload.cidr = parseInt(cidr)
|
||
} else {
|
||
payload.subnet_mask = subnetMask
|
||
}
|
||
|
||
const response = await axios.post(`${API_URL}/calculate`, payload)
|
||
setResult(response.data)
|
||
} catch (err) {
|
||
const errorMessage = err.response?.data?.detail || err.message || 'Failed to calculate subnet'
|
||
setError(errorMessage)
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}
|
||
|
||
const InfoCard = ({ label, value, highlight }) => (
|
||
<div className={`info-card ${highlight ? 'highlight' : ''}`}>
|
||
<div className="info-label">{label}</div>
|
||
<div className="info-value">{value}</div>
|
||
</div>
|
||
)
|
||
|
||
const BinaryCard = ({ label, decimal, binary }) => (
|
||
<div className="binary-card">
|
||
<div className="binary-label">{label}</div>
|
||
<div className="binary-decimal">{decimal}</div>
|
||
<div className="binary-value">{binary}</div>
|
||
</div>
|
||
)
|
||
|
||
// Calculate ALL next sequential networks based on the current network
|
||
const calculateNetworkRanges = (result) => {
|
||
const networkId = result.network_id
|
||
const cidr = result.cidr
|
||
const totalAddresses = result.total_hosts
|
||
|
||
// Parse network ID to get starting IP
|
||
const parts = networkId.split('.').map(Number)
|
||
let networkNum = (parts[0] << 24) + (parts[1] << 16) + (parts[2] << 8) + parts[3]
|
||
|
||
const networks = []
|
||
|
||
// Calculate all possible networks until end of IPv4 space
|
||
// For performance, limit to 10000 networks max
|
||
const maxNetworks = 10000
|
||
let count = 0
|
||
|
||
// Generate next sequential networks
|
||
while (count < maxNetworks) {
|
||
const currentNetworkNum = networkNum + (count * totalAddresses)
|
||
|
||
// Check if we're still within valid IPv4 range
|
||
const broadcastNum = currentNetworkNum + totalAddresses - 1
|
||
if (currentNetworkNum > 0xFFFFFFFF || broadcastNum > 0xFFFFFFFF) break
|
||
|
||
// Convert number back to IP address
|
||
const networkAddress = `${(currentNetworkNum >>> 24) & 0xFF}.${(currentNetworkNum >>> 16) & 0xFF}.${(currentNetworkNum >>> 8) & 0xFF}.${currentNetworkNum & 0xFF}`
|
||
const broadcastAddress = `${(broadcastNum >>> 24) & 0xFF}.${(broadcastNum >>> 16) & 0xFF}.${(broadcastNum >>> 8) & 0xFF}.${broadcastNum & 0xFF}`
|
||
|
||
// Calculate first and last usable IPs
|
||
let firstUsable, lastUsable
|
||
if (cidr === 32) {
|
||
firstUsable = networkAddress
|
||
lastUsable = networkAddress
|
||
} else if (cidr === 31) {
|
||
firstUsable = networkAddress
|
||
lastUsable = broadcastAddress
|
||
} else {
|
||
const firstUsableNum = currentNetworkNum + 1
|
||
const lastUsableNum = broadcastNum - 1
|
||
firstUsable = `${(firstUsableNum >>> 24) & 0xFF}.${(firstUsableNum >>> 16) & 0xFF}.${(firstUsableNum >>> 8) & 0xFF}.${firstUsableNum & 0xFF}`
|
||
lastUsable = `${(lastUsableNum >>> 24) & 0xFF}.${(lastUsableNum >>> 16) & 0xFF}.${(lastUsableNum >>> 8) & 0xFF}.${lastUsableNum & 0xFF}`
|
||
}
|
||
|
||
networks.push({
|
||
networkAddress: `${networkAddress}/${cidr}`,
|
||
firstUsable,
|
||
lastUsable,
|
||
broadcastAddress,
|
||
usableHosts: result.usable_hosts
|
||
})
|
||
|
||
count++
|
||
}
|
||
|
||
return networks
|
||
}
|
||
|
||
return (
|
||
<div className="app">
|
||
<div className="container">
|
||
<h1 className="title">🌐 IP Subnet Calculator</h1>
|
||
|
||
<form onSubmit={calculateSubnet} className="form">
|
||
<div className="input-group">
|
||
<label htmlFor="ipAddress">IP Address</label>
|
||
<input
|
||
id="ipAddress"
|
||
type="text"
|
||
value={ipAddress}
|
||
onChange={(e) => setIpAddress(e.target.value)}
|
||
placeholder="e.g., 192.168.1.1"
|
||
required
|
||
/>
|
||
</div>
|
||
|
||
<div className="input-type-selector">
|
||
<button
|
||
type="button"
|
||
className={`type-btn ${inputType === 'cidr' ? 'active' : ''}`}
|
||
onClick={() => setInputType('cidr')}
|
||
>
|
||
CIDR Notation
|
||
</button>
|
||
<button
|
||
type="button"
|
||
className={`type-btn ${inputType === 'mask' ? 'active' : ''}`}
|
||
onClick={() => setInputType('mask')}
|
||
>
|
||
Subnet Mask
|
||
</button>
|
||
</div>
|
||
|
||
{inputType === 'cidr' ? (
|
||
<div className="input-group">
|
||
<label htmlFor="cidr">CIDR (1-32)</label>
|
||
<input
|
||
id="cidr"
|
||
type="number"
|
||
min="1"
|
||
max="32"
|
||
value={cidr}
|
||
onChange={(e) => setCidr(e.target.value)}
|
||
required
|
||
/>
|
||
</div>
|
||
) : (
|
||
<div className="input-group">
|
||
<label htmlFor="subnetMask">Subnet Mask</label>
|
||
<input
|
||
id="subnetMask"
|
||
type="text"
|
||
value={subnetMask}
|
||
onChange={(e) => setSubnetMask(e.target.value)}
|
||
placeholder="e.g., 255.255.255.0"
|
||
required
|
||
/>
|
||
</div>
|
||
)}
|
||
|
||
<button type="submit" className="calculate-btn" disabled={loading}>
|
||
{loading ? 'Calculating...' : 'Calculate Subnet'}
|
||
</button>
|
||
</form>
|
||
|
||
{error && (
|
||
<div className="error-message">
|
||
⚠️ {error}
|
||
</div>
|
||
)}
|
||
|
||
{result && (
|
||
<div className="results">
|
||
<h2 className="results-title">Subnet Information</h2>
|
||
|
||
{/* Binary Representation Section */}
|
||
<div className="binary-section">
|
||
<h3 className="section-title">Binary Representation</h3>
|
||
<div className="binary-grid">
|
||
<BinaryCard
|
||
label="Address"
|
||
decimal={result.ip_address}
|
||
binary={result.ip_address_binary}
|
||
/>
|
||
<BinaryCard
|
||
label="Netmask"
|
||
decimal={`${result.subnet_mask} = /${result.cidr}`}
|
||
binary={result.subnet_mask_binary}
|
||
/>
|
||
<BinaryCard
|
||
label="Wildcard"
|
||
decimal={result.wildcard_mask}
|
||
binary={result.wildcard_binary}
|
||
/>
|
||
<BinaryCard
|
||
label="Network"
|
||
decimal={`${result.network_id}/${result.cidr}`}
|
||
binary={result.network_id_binary}
|
||
/>
|
||
<BinaryCard
|
||
label="Broadcast"
|
||
decimal={result.broadcast_address}
|
||
binary={result.broadcast_binary}
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Main Information Section */}
|
||
<h3 className="section-title">Network Details</h3>
|
||
<div className="results-grid">
|
||
<InfoCard label="Network ID" value={`${result.network_id}/${result.cidr}`} highlight />
|
||
<InfoCard label="Broadcast Address" value={result.broadcast_address} highlight />
|
||
|
||
<InfoCard label="First Usable IP" value={result.first_usable_ip} />
|
||
<InfoCard label="Last Usable IP" value={result.last_usable_ip} />
|
||
|
||
<InfoCard label="Total Addresses" value={result.total_hosts.toLocaleString()} highlight />
|
||
<InfoCard label="Usable Hosts" value={result.usable_hosts.toLocaleString()} highlight />
|
||
|
||
<InfoCard label="Subnet Mask" value={result.subnet_mask} />
|
||
<InfoCard label="Wildcard Mask" value={result.wildcard_mask} />
|
||
|
||
<InfoCard label="Network Class" value={result.network_class} />
|
||
<InfoCard label="IP Type" value={result.ip_type} />
|
||
|
||
<InfoCard
|
||
label="Private IP"
|
||
value={result.is_private ? '✅ Yes' : '❌ No'}
|
||
/>
|
||
</div>
|
||
|
||
{/* Next Networks Section */}
|
||
<h3 className="section-title">
|
||
📊 All Next Sequential Networks ({calculateNetworkRanges(result).length.toLocaleString()} networks)
|
||
</h3>
|
||
<div className="ranges-table">
|
||
<div className="table-header">
|
||
<div className="table-cell">#</div>
|
||
<div className="table-cell">Network</div>
|
||
<div className="table-cell">Usable Range</div>
|
||
<div className="table-cell">Broadcast</div>
|
||
<div className="table-cell">Hosts</div>
|
||
</div>
|
||
<div className="table-body">
|
||
{calculateNetworkRanges(result).map((network, index) => (
|
||
<div key={index} className="table-row">
|
||
<div className="table-cell range-id">{index + 1}</div>
|
||
<div className="table-cell network-address">{network.networkAddress}</div>
|
||
<div className="table-cell">{network.firstUsable} - {network.lastUsable}</div>
|
||
<div className="table-cell">{network.broadcastAddress}</div>
|
||
<div className="table-cell">{network.usableHosts.toLocaleString()}</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
export default App
|