Skip to content

Commit d99adfc

Browse files
committed
IP addresses and networks
1 parent 3be3533 commit d99adfc

File tree

6 files changed

+448
-0
lines changed

6 files changed

+448
-0
lines changed

src/IPAddress.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import {IPv4, IPv6} from "./index.js";
2+
3+
/**
4+
* An IP address
5+
*/
6+
export abstract class IPAddress {
7+
/**
8+
* The integer representation of the IP address
9+
*/
10+
public readonly value: bigint;
11+
12+
/**
13+
* Create new IP address instance
14+
*/
15+
protected constructor(value: bigint) {
16+
this.value = value;
17+
}
18+
19+
/**
20+
* Create IP address from string
21+
* @throws {@link !RangeError} If provided string is not a valid IPv4 or IPv6 address
22+
*/
23+
public static fromString(str: string): IPv4 | IPv6 {
24+
if (str.includes(":")) return IPv6.fromString(str);
25+
return IPv4.fromString(str);
26+
}
27+
28+
/**
29+
* Get IP address binary representation
30+
*/
31+
public abstract binary(): ArrayBuffer;
32+
33+
/**
34+
* Check if the given addresses are equal
35+
*/
36+
public equals(other: IPAddress): boolean {
37+
return other instanceof this.constructor && other.value === this.value;
38+
}
39+
40+
/**
41+
* Format IP address as string
42+
*/
43+
public abstract toString(): string;
44+
}

src/IPv4.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import {IPAddress} from "./index.js";
2+
3+
/**
4+
* An IPv4 address
5+
*/
6+
export class IPv4 extends IPAddress {
7+
public static regex = /^(?:(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)$/;
8+
public static bitLength = 32;
9+
10+
/**
11+
* Create new IPv4 address instance
12+
* @throws {@link !TypeError} If
13+
*/
14+
public constructor(value: bigint);
15+
16+
public constructor(value: number);
17+
18+
public constructor(value: number | bigint) {
19+
const int = BigInt(value);
20+
if (int < 0n || int > 0xFFFFFFFFn) throw new TypeError("Expected 32-bit unsigned integer, got " + int.constructor.name + " " + int.toString(10));
21+
super(int);
22+
}
23+
24+
/**
25+
* Create an IPv4 address instance from octets
26+
* @throws {@link !RangeError} If provided octets are not 4
27+
*/
28+
public static fromBinary(octets: Uint8Array): IPv4 {
29+
if (octets.length !== 4) throw new RangeError("Expected 4 octets, got " + octets.length);
30+
31+
return new IPv4(
32+
(
33+
octets[0]! << 24 |
34+
octets[1]! << 16 |
35+
octets[2]! << 8 |
36+
octets[3]!
37+
) >>> 0
38+
);
39+
}
40+
41+
/**
42+
* Create an IPv4 address instance from string
43+
* @throws {@link !RangeError} If provided string is not a valid IPv4 address
44+
*/
45+
public static override fromString(str: string): IPv4 {
46+
const octets = str.split(".", 4).map(octet => Number.parseInt(octet, 10));
47+
if (octets.some(octet => Number.isNaN(octet) || octet < 0 || octet > 255))
48+
throw new RangeError("Expected valid IPv4 address, got " + str);
49+
50+
return IPv4.fromBinary(new Uint8Array(octets));
51+
}
52+
53+
/**
54+
* Get the 4 octets of the IPv4 address
55+
*/
56+
public override binary(): Uint8Array {
57+
return new Uint8Array([
58+
(this.value >> 24n) & 0xFFn,
59+
(this.value >> 16n) & 0xFFn,
60+
(this.value >> 8n) & 0xFFn,
61+
this.value & 0xFFn,
62+
].map(Number));
63+
}
64+
65+
public override toString(): string {
66+
return Array.from(this.binary()).map(octet => octet.toString(10)).join(".");
67+
}
68+
}

src/IPv6.ts

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import {IPAddress, IPv4, Subnet} from "./index.js";
2+
3+
/**
4+
* An IPv6 address
5+
*/
6+
export class IPv6 extends IPAddress {
7+
public static bitLength = 128;
8+
9+
/**
10+
* Create new IPv6 address instance
11+
*/
12+
public constructor(value: bigint) {
13+
if (value < 0n || value > 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFn)
14+
throw new TypeError("Expected 128-bit unsigned integer, got " + value.constructor.name + " 0x" + value.toString(16));
15+
super(value);
16+
}
17+
18+
/**
19+
* Create an IPv6 address instance from hextets
20+
* @throws {@link !RangeError} If provided hextets are not 8
21+
*/
22+
public static fromBinary(hextets: Uint16Array): IPv6 {
23+
if (hextets.length !== 8) throw new RangeError("Expected 8 hextets, got " + hextets.length);
24+
25+
return new IPv6(
26+
BigInt(hextets[0]!) << 112n |
27+
BigInt(hextets[1]!) << 96n |
28+
BigInt(hextets[2]!) << 80n |
29+
BigInt(hextets[3]!) << 64n |
30+
BigInt(hextets[4]!) << 48n |
31+
BigInt(hextets[5]!) << 32n |
32+
BigInt(hextets[6]!) << 16n |
33+
BigInt(hextets[7]!)
34+
);
35+
}
36+
37+
/**
38+
* Create an IPv6 address instance from string
39+
* @throws {@link !RangeError} If provided string is not a valid IPv6 address
40+
*/
41+
public static override fromString(str: string): IPv6 {
42+
const parts = str.split("::", 2);
43+
const hextestStart = parts[0]! === ""
44+
? []
45+
: parts[0]!.split(":").flatMap(IPv6.parseHextet);
46+
const hextestEnd = parts[1] === undefined || parts[1] === ""
47+
? []
48+
: parts[1].split(":").flatMap(IPv6.parseHextet);
49+
if (
50+
hextestStart.some(hextet => Number.isNaN(hextet) || hextet < 0 || hextet > 0xFFFF) ||
51+
hextestEnd.some(hextet => Number.isNaN(hextet) || hextet < 0 || hextet > 0xFFFF) ||
52+
(parts.length === 2 && hextestStart.length + hextestEnd.length > 6) ||
53+
(parts.length < 2 && hextestStart.length + hextestEnd.length !== 8)
54+
) throw new RangeError("Expected valid IPv6 address, got " + str);
55+
56+
const hextets = new Uint16Array(8);
57+
hextets.set(hextestStart, 0);
58+
hextets.set(hextestEnd, 8 - hextestEnd.length);
59+
60+
return IPv6.fromBinary(hextets);
61+
}
62+
63+
/**
64+
* Parse string hextet into unsigned 16-bit integer
65+
* @internal
66+
*/
67+
private static parseHextet(hextet: string): number | number[] {
68+
return IPv4.regex.test(hextet)
69+
? Array.from(new Uint16Array(IPv4.fromString(hextet).binary().buffer))
70+
: Number.parseInt(hextet, 16);
71+
}
72+
73+
/**
74+
* Get the 8 hextets of the IPv6 address
75+
*/
76+
public binary(): Uint16Array {
77+
return new Uint16Array([
78+
(this.value >> 112n) & 0xFFFFn,
79+
(this.value >> 96n) & 0xFFFFn,
80+
(this.value >> 80n) & 0xFFFFn,
81+
(this.value >> 64n) & 0xFFFFn,
82+
(this.value >> 48n) & 0xFFFFn,
83+
(this.value >> 32n) & 0xFFFFn,
84+
(this.value >> 16n) & 0xFFFFn,
85+
this.value & 0xFFFFn
86+
].map(Number));
87+
}
88+
89+
/**
90+
* Check whether this is an IPv4-mapped IPv6 address.
91+
* Only works for `::ffff:0:0/96`
92+
*/
93+
public hasMappedIPv4(): boolean {
94+
return Subnet.IPV4_MAPPED_IPV6.has(this);
95+
}
96+
97+
/**
98+
* Get the IPv4-mapped IPv6 address.
99+
* Returns the last 32 bits as an IPv4 address.
100+
* @see {@link IPv6#hasMappedIPv4}
101+
*/
102+
public getMappedIPv4(): IPv4 {
103+
return IPv4.fromBinary(new Uint8Array(this.binary().buffer).slice(-4));
104+
}
105+
106+
public override toString(): string {
107+
const str = Array.from(this.binary()).map(octet => octet.toString(16)).join(":");
108+
const longest = str.match(/(?:^|:)0(?::0)+(?:$|:)/g)?.reduce((acc, cur) => cur.length > acc.length ? cur : acc, "") ?? null;
109+
if (longest === null) return str;
110+
return str.replace(longest, "::");
111+
}
112+
}

src/Network.ts

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import {IPv4, IPv6, Subnet} from "./index.js"
2+
3+
/**
4+
* A network that can contain multiple subnets
5+
*/
6+
export class Network {
7+
/**
8+
* Reserved subnets
9+
*/
10+
public static readonly BOGON = new Network([
11+
// IPv4
12+
Subnet.fromCIDR("0.0.0.0/8"),
13+
Subnet.fromCIDR("10.0.0.0/8"),
14+
Subnet.fromCIDR("100.64.0.0/10"),
15+
Subnet.fromCIDR("127.0.0.0/8"),
16+
Subnet.fromCIDR("169.254.0.0/16"),
17+
Subnet.fromCIDR("172.16.0.0/12"),
18+
Subnet.fromCIDR("192.0.0.0/24"),
19+
Subnet.fromCIDR("192.0.2.0/24"),
20+
Subnet.fromCIDR("192.88.99.0/24"),
21+
Subnet.fromCIDR("192.168.0.0/16"),
22+
Subnet.fromCIDR("198.18.0.0/15"),
23+
Subnet.fromCIDR("198.51.100.0/24"),
24+
Subnet.fromCIDR("203.0.113.0/24"),
25+
Subnet.fromCIDR("224.0.0.0/4"),
26+
Subnet.fromCIDR("233.252.0.0/24"),
27+
Subnet.fromCIDR("240.0.0.0/4"),
28+
Subnet.fromCIDR("255.255.255.255/32"),
29+
// IPv6
30+
Subnet.fromCIDR("::/128"),
31+
Subnet.fromCIDR("::1/128"),
32+
Subnet.fromCIDR("64:ff9b:1::/48"),
33+
Subnet.fromCIDR("100::/64"),
34+
Subnet.fromCIDR("2001:20::/28"),
35+
Subnet.fromCIDR("2001:db8::/32"),
36+
Subnet.fromCIDR("3fff::/20"),
37+
Subnet.fromCIDR("5f00::/16"),
38+
Subnet.fromCIDR("fc00::/7"),
39+
Subnet.fromCIDR("fe80::/10"),
40+
]);
41+
42+
readonly #subnets: Map<string, Subnet<IPv4 | IPv6>> = new Map();
43+
44+
/**
45+
* Create new network
46+
* @param [subnets] Initial subnets to add to this network
47+
*/
48+
public constructor(subnets?: Iterable<Subnet<IPv4 | IPv6>>) {
49+
if (subnets) for (const subnet of subnets) this.add(subnet);
50+
}
51+
52+
/**
53+
* Add a subnet to this network
54+
*/
55+
public add(subnet: Subnet<IPv4 | IPv6>): void {
56+
this.#subnets.set(subnet.toString(), subnet);
57+
}
58+
59+
/**
60+
* Remove a subnet from this network
61+
* @param cidr CIDR notation of the subnet to remove
62+
*/
63+
public remove(cidr: string): void {
64+
this.#subnets.delete(Subnet.fromCIDR(cidr).toString());
65+
}
66+
67+
/**
68+
* Check if subnet is in this network
69+
* @param cidr CIDR notation of the subnet to check
70+
*/
71+
public hasSubnet(cidr: string): boolean {
72+
return this.#subnets.has(Subnet.fromCIDR(cidr).toString());
73+
}
74+
75+
/**
76+
* Get all subnets in this network mapped to their CIDR notation
77+
*/
78+
public subnets(): ReadonlyMap<string, Subnet<IPv4 | IPv6>> {
79+
return this.#subnets;
80+
}
81+
82+
/**
83+
* Check if an IP address is in this network
84+
*/
85+
public has(address: IPv4 | IPv6): boolean {
86+
for (const subnet of this.#subnets.values()) if (subnet.has(address)) return true;
87+
return false;
88+
}
89+
90+
/**
91+
* Get the number of addresses in this network
92+
*/
93+
public size(): bigint {
94+
let size = 0n;
95+
for (const subnet of this.#subnets.values()) size += subnet.size();
96+
return size;
97+
}
98+
}

0 commit comments

Comments
 (0)