From b486a40598219ca33f875f67ea8c3cf84150729e Mon Sep 17 00:00:00 2001 From: microshine Date: Mon, 23 Dec 2024 13:39:28 +0100 Subject: [PATCH] refactor: implement IP address conversion and parsing for IPv4 and IPv6 --- packages/x509/package.json | 3 +- packages/x509/src/ip_converter.ts | 188 +++++++++++++++++++++++++++-- packages/x509/test/ip_converter.ts | 146 ++++++++++++++++++++++ 3 files changed, 328 insertions(+), 9 deletions(-) create mode 100644 packages/x509/test/ip_converter.ts diff --git a/packages/x509/package.json b/packages/x509/package.json index 511611b..5494c77 100644 --- a/packages/x509/package.json +++ b/packages/x509/package.json @@ -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" } -} +} \ No newline at end of file diff --git a/packages/x509/src/ip_converter.ts b/packages/x509/src/ip_converter.ts index 221f8b9..30f07ff 100644 --- a/packages/x509/src/ip_converter.ts +++ b/packages/x509/src/ip_converter.ts @@ -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"; @@ -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; } } diff --git a/packages/x509/test/ip_converter.ts b/packages/x509/test/ip_converter.ts new file mode 100644 index 0000000..21a47f3 --- /dev/null +++ b/packages/x509/test/ip_converter.ts @@ -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"); + }); + }); + }); +});