Skip to content

Commit

Permalink
refactor: implement IP address conversion and parsing for IPv4 and IPv6
Browse files Browse the repository at this point in the history
  • Loading branch information
microshine committed Dec 23, 2024
1 parent 97747ed commit b486a40
Show file tree
Hide file tree
Showing 3 changed files with 328 additions and 9 deletions.
3 changes: 1 addition & 2 deletions packages/x509/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,7 @@
"dependencies": {
"@peculiar/asn1-schema": "^2.3.13",
"asn1js": "^3.0.5",
"ipaddr.js": "^2.1.0",
"pvtsutils": "^1.3.6",
"tslib": "^2.8.1"
}
}
}
188 changes: 181 additions & 7 deletions packages/x509/src/ip_converter.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,129 @@
import * as ip from "ipaddr.js";
import { Convert } from "pvtsutils";

export class IpConverter {
private static isIPv4(ip: string): boolean {
return /^(\d{1,3}\.){3}\d{1,3}$/.test(ip);
}

private static parseIPv4(ip: string): number[] {
const parts = ip.split(".");
if (parts.length !== 4) {
throw new Error("Invalid IPv4 address");
}

return parts.map((part) => {
const num = parseInt(part, 10);
if (isNaN(num) || num < 0 || num > 255) {
throw new Error("Invalid IPv4 address part");
}
return num;
});
}

private static parseIPv6(ip: string): number[] {
// Handle compressed notation
const expandedIP = this.expandIPv6(ip);
const parts = expandedIP.split(":");

if (parts.length !== 8) {
throw new Error("Invalid IPv6 address");
}

return parts.reduce((bytes: number[], part) => {
const num = parseInt(part, 16);
if (isNaN(num) || num < 0 || num > 0xffff) {
throw new Error("Invalid IPv6 address part");
}
bytes.push((num >> 8) & 0xff);
bytes.push(num & 0xff);
return bytes;
}, []);
}

private static expandIPv6(ip: string): string {
if (!ip.includes("::")) {
return ip;
}

const parts = ip.split("::");
if (parts.length > 2) {
throw new Error("Invalid IPv6 address");
}

const left = parts[0] ? parts[0].split(":") : [];
const right = parts[1] ? parts[1].split(":") : [];
const missing = 8 - (left.length + right.length);

if (missing < 0) {
throw new Error("Invalid IPv6 address");
}

return [...left, ...Array(missing).fill("0"), ...right].join(":");
}

private static formatIPv6(bytes: Uint8Array): string {
const parts: string[] = [];
for (let i = 0; i < 16; i += 2) {
parts.push(((bytes[i] << 8) | bytes[i + 1]).toString(16));
}
return this.compressIPv6(parts.join(":"));
}

private static compressIPv6(ip: string): string {
// Find longest sequence of zeros
const parts = ip.split(":");
let longestZeroStart = -1;
let longestZeroLength = 0;
let currentZeroStart = -1;
let currentZeroLength = 0;

for (let i = 0; i < parts.length; i++) {
if (parts[i] === "0") {
if (currentZeroStart === -1) {
currentZeroStart = i;
}
currentZeroLength++;
} else {
if (currentZeroLength > longestZeroLength) {
longestZeroStart = currentZeroStart;
longestZeroLength = currentZeroLength;
}
currentZeroStart = -1;
currentZeroLength = 0;
}
}

if (currentZeroLength > longestZeroLength) {
longestZeroStart = currentZeroStart;
longestZeroLength = currentZeroLength;
}

if (longestZeroLength > 1) {
const before = parts.slice(0, longestZeroStart).join(":");
const after = parts.slice(longestZeroStart + longestZeroLength).join(":");
return `${before}::${after}`;
}

return ip;
}

private static parseCIDR(text: string): [number[], number] {
const [addr, prefixStr] = text.split("/");
const prefix = parseInt(prefixStr, 10);

if (this.isIPv4(addr)) {
if (prefix < 0 || prefix > 32) {
throw new Error("Invalid IPv4 prefix length");
}
return [this.parseIPv4(addr), prefix];
} else {
if (prefix < 0 || prefix > 128) {
throw new Error("Invalid IPv6 prefix length");
}
return [this.parseIPv6(addr), prefix];
}
}

private static decodeIP(value: string): string {
if (value.length === 64 && parseInt(value, 16) === 0) {
return "::/0";
Expand All @@ -23,16 +145,68 @@ export class IpConverter {
}

public static toString(buf: ArrayBuffer): string {
if (buf.byteLength === 4 || buf.byteLength === 16) {
const uint8 = new Uint8Array(buf);
const addr = ip.fromByteArray(Array.from(uint8));
return addr.toString();
const uint8 = new Uint8Array(buf);

// Handle plain IPv4 or IPv6
if (uint8.length === 4) {
return Array.from(uint8).join(".");
}
if (uint8.length === 16) {
return this.formatIPv6(uint8);
}

// Handle IP + mask (NameConstraints)
if (uint8.length === 8 || uint8.length === 32) {
const half = uint8.length / 2;
const addrBytes = uint8.slice(0, half);
const maskBytes = uint8.slice(half);

const isAllZeros = uint8.every((byte) => byte === 0);
if (isAllZeros) {
return uint8.length === 8 ? "0.0.0.0/0" : "::/0";
}

const prefixLen = maskBytes.reduce((a, b) => a + (b.toString(2).match(/1/g) || []).length, 0);

if (uint8.length === 8) {
const addrStr = Array.from(addrBytes).join(".");
const maskStr = Array.from(maskBytes).join(".");
return `${addrStr}/${prefixLen} (mask ${maskStr})`;
} else {
const addrStr = this.formatIPv6(addrBytes);
return `${addrStr}/${prefixLen}`;
}
}

return this.decodeIP(Convert.ToHex(buf));
}

public static fromString(text: string): ArrayBuffer {
const addr = ip.parse(text);
return new Uint8Array(addr.toByteArray()).buffer;
const ipText = text.split(" (mask ")[0];

if (ipText.includes("/")) {
const [addr, prefix] = this.parseCIDR(ipText);
const maskBytes = new Uint8Array(addr.length);

let bitsLeft = prefix;
for (let i = 0; i < maskBytes.length; i++) {
if (bitsLeft >= 8) {
maskBytes[i] = 0xff;
bitsLeft -= 8;
} else if (bitsLeft > 0) {
maskBytes[i] = 0xff << (8 - bitsLeft);
bitsLeft = 0;
}
}

const out = new Uint8Array(addr.length * 2);
out.set(addr, 0);
out.set(maskBytes, addr.length);
return out.buffer;
}

// Parse single IP address
const bytes = this.isIPv4(ipText) ? this.parseIPv4(ipText) : this.parseIPv6(ipText);
return new Uint8Array(bytes).buffer;
}
}
146 changes: 146 additions & 0 deletions packages/x509/test/ip_converter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import { IpConverter } from "../src/ip_converter";
import * as assert from "assert";

describe("IpConverter", () => {
describe("toString", () => {
it("converts IPv4 address", () => {
const input = new Uint8Array([192, 168, 0, 1]).buffer;
assert.strictEqual(IpConverter.toString(input), "192.168.0.1");
});

it("converts IPv6 address", () => {
const input = new Uint8Array([
0x20, 0x01, 0x0d, 0xb8, 0x85, 0xa3, 0x00, 0x00, 0x00, 0x00, 0x8a, 0x2e, 0x03, 0x70, 0x73,
0x34,
]).buffer;
assert.strictEqual(IpConverter.toString(input), "2001:db8:85a3::8a2e:370:7334");
});

it("converts IPv4 with netmask", () => {
const input = new Uint8Array([
192,
168,
0,
0, // IP
255,
255,
255,
0, // Mask
]).buffer;
assert.strictEqual(IpConverter.toString(input), "192.168.0.0/24 (mask 255.255.255.0)");
});

it("converts IPv4 with different netmask", () => {
const input = new Uint8Array([
10,
24,
0,
0, // IP
255,
255,
248,
0, // Mask
]).buffer;
assert.strictEqual(IpConverter.toString(input), "10.24.0.0/21 (mask 255.255.248.0)");
});

it("converts IPv6 with netmask", () => {
const input = new Uint8Array([
0x20, 0x01, 0x0d, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
]).buffer;
assert.strictEqual(IpConverter.toString(input), "2001:db8::/64");
});

it("handles special case IPv4 0.0.0.0/0", () => {
const input = new Uint8Array(8).buffer; // All zeros, 8 bytes for IPv4
assert.strictEqual(IpConverter.toString(input), "0.0.0.0/0");
});

it("handles special case IPv6 ::/0", () => {
const input = new Uint8Array(32).buffer; // All zeros, 32 bytes for IPv6
assert.strictEqual(IpConverter.toString(input), "::/0");
});
});

describe("fromString", () => {
it("parses IPv4 address", () => {
const result = IpConverter.fromString("192.168.0.1");
assert.deepStrictEqual(Array.from(new Uint8Array(result)), [192, 168, 0, 1]);
});

it("parses IPv6 address", () => {
const result = IpConverter.fromString("2001:db8:85a3::8a2e:370:7334");
assert.deepStrictEqual(
Array.from(new Uint8Array(result)),
[
0x20, 0x01, 0x0d, 0xb8, 0x85, 0xa3, 0x00, 0x00, 0x00, 0x00, 0x8a, 0x2e, 0x03, 0x70, 0x73,
0x34,
],
);
});

it("parses IPv4 CIDR notation", () => {
const result = IpConverter.fromString("192.168.0.0/24");
assert.deepStrictEqual(Array.from(new Uint8Array(result)), [
192,
168,
0,
0, // IP
255,
255,
255,
0, // Mask
]);
});

it("parses IPv6 CIDR notation", () => {
const result = IpConverter.fromString("2001:db8::/64");
const expected = new Uint8Array(32);
expected.set([0x20, 0x01, 0x0d, 0xb8], 0);
expected.set([0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff], 16);
assert.deepStrictEqual(Array.from(new Uint8Array(result)), Array.from(expected));
});

it("parses IPv4 with mask string notation", () => {
const result = IpConverter.fromString("192.168.0.0/24 (mask 255.255.255.0)");
assert.deepStrictEqual(Array.from(new Uint8Array(result)), [
192,
168,
0,
0, // IP
255,
255,
255,
0, // Mask
]);
});

it("parses complex IPv4 with mask string notation", () => {
const result = IpConverter.fromString("10.24.0.0/21 (mask 255.255.248.0)");
assert.deepStrictEqual(Array.from(new Uint8Array(result)), [
10,
24,
0,
0, // IP
255,
255,
248,
0, // Mask
]);
});

it("throws on invalid IP", () => {
assert.throws(() => {
IpConverter.fromString("invalid");
});
});

it("throws on invalid CIDR", () => {
assert.throws(() => {
IpConverter.fromString("192.168.0.0/33");
});
});
});
});

0 comments on commit b486a40

Please sign in to comment.