diff --git a/src/lib/components/paginationInline.svelte b/src/lib/components/paginationInline.svelte index 3e718cf0a..924a3f6ef 100644 --- a/src/lib/components/paginationInline.svelte +++ b/src/lib/components/paginationInline.svelte @@ -54,14 +54,7 @@ {#if !hidePages} - + {:else} { + it('should parse standard zone file with origin', () => { + const content = `$ORIGIN example.com. ; designates the start of this zone file in the namespace +$TTL 3600 ; default expiration time (in seconds) of all RRs without their own TTL value +example.com. IN SOA ns.example.com. username.example.com. ( 2020091025 7200 3600 1209600 3600 ) +example.com. IN NS ns ; ns.example.com is a nameserver for example.com +example.com. IN NS ns.somewhere.example. ; ns.somewhere.example is a backup nameserver for example.com +example.com. IN MX 10 mail.example.com. ; mail.example.com is the mailserver for example.com +@ IN MX 20 mail2.example.com. ; equivalent to above line, "@" represents zone origin +@ IN MX 50 mail3 ; equivalent to above line, but using a relative host name +example.com. IN A 192.0.2.1 ; IPv4 address for example.com + IN AAAA 2001:db8:10::1 ; IPv6 address for example.com +ns IN A 192.0.2.2 ; IPv4 address for ns.example.com + IN AAAA 2001:db8:10::2 ; IPv6 address for ns.example.com +www IN CNAME example.com. ; www.example.com is an alias for example.com +wwwtest IN CNAME www ; wwwtest.example.com is another alias for www.example.com +mail IN A 192.0.2.3 ; IPv4 address for mail.example.com +mail2 IN A 192.0.2.4 ; IPv4 address for mail2.example.com +mail3 IN A 192.0.2.5 ; IPv4 address for mail3.example.com`; + + const result = parseDnsRecords(content); + + expect(result.NS).toHaveLength(2); + expect(result.NS[0].name).toBe('example.com'); + expect(result.NS[0].value).toBe('ns'); + expect(result.NS[1].name).toBe('example.com'); + expect(result.NS[1].value).toBe('ns.somewhere.example'); + + expect(result.MX).toHaveLength(3); + expect(result.MX[0].name).toBe('example.com'); + expect(result.MX[0].priority).toBe(10); + expect(result.MX[0].value).toBe('mail.example.com'); + expect(result.MX[1].name).toBe(''); + expect(result.MX[1].priority).toBe(20); + expect(result.MX[1].value).toBe('mail2.example.com'); + expect(result.MX[2].name).toBe(''); + expect(result.MX[2].priority).toBe(50); + expect(result.MX[2].value).toBe('mail3'); + + expect(result.A).toHaveLength(5); + expect(result.A[0].name).toBe('example.com'); + expect(result.A[0].value).toBe('192.0.2.1'); + expect(result.A[1].name).toBe('ns'); + expect(result.A[1].value).toBe('192.0.2.2'); + expect(result.A[2].name).toBe('mail'); + expect(result.A[2].value).toBe('192.0.2.3'); + expect(result.A[3].name).toBe('mail2'); + expect(result.A[3].value).toBe('192.0.2.4'); + expect(result.A[4].name).toBe('mail3'); + expect(result.A[4].value).toBe('192.0.2.5'); + + expect(result.AAAA).toHaveLength(2); + expect(result.AAAA[0].name).toBe('example.com'); + expect(result.AAAA[0].value).toBe('2001:db8:10::1'); + expect(result.AAAA[1].name).toBe('ns'); + expect(result.AAAA[1].value).toBe('2001:db8:10::2'); + + expect(result.CNAME).toHaveLength(2); + expect(result.CNAME[0].name).toBe('www'); + expect(result.CNAME[0].value).toBe('example.com'); + expect(result.CNAME[1].name).toBe('wwwtest'); + expect(result.CNAME[1].value).toBe('www'); + }); + + it('should parse localhost zone file', () => { + const content = `$ORIGIN localhost. +@ 86400 IN SOA @ root ( + 1999010100 ; serial + 10800 ; refresh (3 hours) + 900 ; retry (15 minutes) + 604800 ; expire (1 week) + 86400 ; minimum (1 day) + ) +@ 86400 IN NS @ +@ 86400 IN A 127.0.0.1 +@ 86400 IN AAAA ::1`; + + const result = parseDnsRecords(content); + + expect(result.NS).toHaveLength(1); + expect(result.NS[0].name).toBe(''); // @ symbol is converted to empty string + expect(result.NS[0].value).toBe('@'); + expect(result.NS[0].ttl).toBe(86400); + + expect(result.A).toHaveLength(1); + expect(result.A[0].name).toBe(''); // @ symbol is converted to empty string + expect(result.A[0].value).toBe('127.0.0.1'); + expect(result.A[0].ttl).toBe(86400); + + expect(result.AAAA).toHaveLength(1); + expect(result.AAAA[0].name).toBe(''); // @ symbol is converted to empty string + expect(result.AAAA[0].value).toBe('::1'); + expect(result.AAAA[0].ttl).toBe(86400); + }); + + it('should parse complex zone file with multiple record types and comments', () => { + const content = `; Exported (y-m-d hh:mm:ss): 2019-01-10 13:05:04 +; +; This file is intended for use for informational and archival +; purposes ONLY and MUST be edited before use on a production +; DNS server. +; +; In particular, you must update the SOA record with the correct +; authoritative name server and contact e-mail address information, +; and add the correct NS records for the name servers which will +; be authoritative for this domain. +; +; For further information, please consult the BIND documentation +; located on the following website: +; +; http://www.isc.org/ +; +; And RFC 1035: +; +; http://www.ietf.org/rfc/rfc1035.txt +; +; Please note that we do NOT offer technical support for any use +; of this zone data, the BIND name server, or any other third- +; party DNS software. +; +; Use at your own risk. +; SOA Record +example.com. 3600 IN SOA ns41.domaincontrol.com. dns.net. ( + 2018122702 + 28800 + 7200 + 604800 + 3600 + ) +; A Records +@ 600 IN A 192.0.2.249 +blog 10800 IN A 192.0.2.255 +dev 1800 IN A 192.0.2.254 +dev01 1800 IN A 192.0.2.253 +dev02 1800 IN A 192.0.2.252 +dev03 1800 IN A 192.0.2.251 +dev04 1800 IN A 192.0.2.250 +; CNAME Records +abc123b432dc7785b7ef31f04f25c3e71 1800 IN CNAME verify.bing.com. +akamai 600 IN CNAME www.example.com.edgekey.net. +email 3600 IN CNAME email.secureserver.net. +; MX Records +@ 604800 IN MX 10 amlxe.l.google.com. +@ 604800 IN MX 10 aplxe.l.google.com. +; TXT Records +@ 3600 IN TXT "google-site-verification=3J82-80dbMyCo5Q5C1G11JszeOnZPGCSYlHcPcXg" +@ 3600 IN TXT "google-site-verification=eS_QPYLE_W4nduSrlN-cddxG7ZqOnB743xsbX918"`; + + const result = parseDnsRecords(content); + + expect(result.A).toHaveLength(7); + expect(result.A[0].name).toBe(''); + expect(result.A[0].value).toBe('192.0.2.249'); + expect(result.A[0].ttl).toBe(600); + expect(result.A[1].name).toBe('blog'); + expect(result.A[1].value).toBe('192.0.2.255'); + expect(result.A[1].ttl).toBe(10800); + + expect(result.CNAME).toHaveLength(3); + expect(result.CNAME[0].name).toBe('abc123b432dc7785b7ef31f04f25c3e71'); + expect(result.CNAME[0].value).toBe('verify.bing.com'); + expect(result.CNAME[0].ttl).toBe(1800); + expect(result.CNAME[1].name).toBe('akamai'); + expect(result.CNAME[1].value).toBe('www.example.com.edgekey.net'); + expect(result.CNAME[1].ttl).toBe(600); + + expect(result.MX).toHaveLength(2); + expect(result.MX[0].name).toBe(''); + expect(result.MX[0].value).toBe('amlxe.l.google.com'); + expect(result.MX[0].priority).toBe(10); + expect(result.MX[0].ttl).toBe(604800); + expect(result.MX[1].name).toBe(''); + expect(result.MX[1].value).toBe('aplxe.l.google.com'); + expect(result.MX[1].priority).toBe(10); + expect(result.MX[1].ttl).toBe(604800); + + expect(result.TXT).toHaveLength(2); + expect(result.TXT[0].name).toBe(''); + expect(result.TXT[0].value).toBe( + 'google-site-verification=3J82-80dbMyCo5Q5C1G11JszeOnZPGCSYlHcPcXg' + ); + expect(result.TXT[0].ttl).toBe(3600); + expect(result.TXT[1].name).toBe(''); + expect(result.TXT[1].value).toBe( + 'google-site-verification=eS_QPYLE_W4nduSrlN-cddxG7ZqOnB743xsbX918' + ); + expect(result.TXT[1].ttl).toBe(3600); + }); + + it('should parse zone file with origin prefix', () => { + const content = `$ORIGIN example.com. +example.com. 3600 IN SOA ns41.domaincontrol.com. dns.net. ( + 2018122702 + 28800 + 7200 + 604800 + 3600 + ) + +; A Records +@ 600 IN A 192.0.2.249 +blog 10800 IN A 192.0.2.255 +dev 1800 IN A 192.0.2.254 +dev01 1800 IN A 192.0.2.253 +dev02 1800 IN A 192.0.2.252 +dev03 1800 IN A 192.0.2.251 +dev04 1800 IN A 192.0.2.250 +abc123b432dc7785b7ef31f04f25c3e71 1800 IN CNAME verify.bing.com. +; CNAME Records +akamai 600 IN CNAME www.example.edgekey.net. +email 3600 IN CNAME email.secureserver.net. +; MX Records +@ 604800 IN MX 10 amlxe.l.google.com. +@ 604800 IN MX 10 aplxe.l.google.com. +; TXT Records +@ 3600 IN TXT "google-site-verification=3J82-80dbMyCo5Q5C1GM8os1VYVEOnZPGCSYlHcPcXg" +@ 3600 IN TXT "google-site-verification=eS_QPYLE_W4nduSrlN-cddxG7ZqOnB7k7uIG7qrsyu8"`; + + const result = parseDnsRecords(content); + + expect(result.A).toHaveLength(7); + expect(result.A[0].name).toBe(''); + expect(result.A[0].value).toBe('192.0.2.249'); + expect(result.A[0].ttl).toBe(600); + + expect(result.CNAME).toHaveLength(3); + expect(result.CNAME[0].name).toBe('abc123b432dc7785b7ef31f04f25c3e71'); + expect(result.CNAME[0].value).toBe('verify.bing.com'); + expect(result.CNAME[1].name).toBe('akamai'); + expect(result.CNAME[1].value).toBe('www.example.edgekey.net'); + + expect(result.MX).toHaveLength(2); + expect(result.MX[0].name).toBe(''); + expect(result.MX[0].priority).toBe(10); + expect(result.MX[0].value).toBe('amlxe.l.google.com'); + + expect(result.TXT).toHaveLength(2); + expect(result.TXT[0].name).toBe(''); + expect(result.TXT[0].value).toBe( + 'google-site-verification=3J82-80dbMyCo5Q5C1GM8os1VYVEOnZPGCSYlHcPcXg' + ); + }); + + it('should parse complex zone file with multiple servers', () => { + const content = `$ORIGIN example.com. +$TTL 86400 +@ IN SOA dns1.example.com. hostmaster.example.com. ( + 2001062501 ; serial + 21600 ; refresh after 6 hours + 3600 ; retry after 1 hour + 604800 ; expire after 1 week + 86400 ) ; minimum TTL of 1 day + + + IN NS dns1.example.com. + IN NS dns2.example.com. + + + IN MX 10 mail.example.com. + IN MX 20 mail2.example.com. + + +dns1 IN A 10.0.1.1 +dns2 IN A 10.0.1.2 + + +server1 IN A 10.0.1.5 +server2 IN A 10.0.1.6 + + +ftp IN A 10.0.1.3 + IN A 10.0.1.4 + +mail IN CNAME server1 +mail2 IN CNAME server2 + + +www IN CNAME server1`; + + const result = parseDnsRecords(content); + + expect(result.NS).toHaveLength(2); + expect(result.NS[0].name).toBe(''); + expect(result.NS[0].value).toBe('dns1.example.com'); + expect(result.NS[1].name).toBe(''); + expect(result.NS[1].value).toBe('dns2.example.com'); + + expect(result.MX).toHaveLength(2); + expect(result.MX[0].name).toBe(''); + expect(result.MX[0].priority).toBe(10); + expect(result.MX[0].value).toBe('mail.example.com'); + expect(result.MX[1].name).toBe(''); + expect(result.MX[1].priority).toBe(20); + expect(result.MX[1].value).toBe('mail2.example.com'); + + expect(result.A).toHaveLength(6); + expect(result.A[0].name).toBe('dns1'); + expect(result.A[0].value).toBe('10.0.1.1'); + expect(result.A[1].name).toBe('dns2'); + expect(result.A[1].value).toBe('10.0.1.2'); + expect(result.A[2].name).toBe('server1'); + expect(result.A[2].value).toBe('10.0.1.5'); + expect(result.A[3].name).toBe('server2'); + expect(result.A[3].value).toBe('10.0.1.6'); + expect(result.A[4].name).toBe('ftp'); + expect(result.A[4].value).toBe('10.0.1.3'); + expect(result.A[5].name).toBe('ftp'); + expect(result.A[5].value).toBe('10.0.1.4'); + + expect(result.CNAME).toHaveLength(3); + expect(result.CNAME[0].name).toBe('mail'); + expect(result.CNAME[0].value).toBe('server1'); + expect(result.CNAME[1].name).toBe('mail2'); + expect(result.CNAME[1].value).toBe('server2'); + expect(result.CNAME[2].name).toBe('www'); + expect(result.CNAME[2].value).toBe('server1'); + }); + + it('should parse reverse zone file with PTR records', () => { + const content = `;; reverse zone file for 127.0.0.1 and ::1 +$TTL 1814400 ; 3 weeks +@ 1814400 IN SOA localhost. root.localhost. ( + 1999010100 ; serial + 10800 ; refresh (3 hours) + 900 ; retry (15 minutes) + 604800 ; expire (1 week) + 86400 ; minimum (1 day) + ) +@ 1814400 IN NS localhost. +1 1814400 IN PTR localhost.`; + + const result = parseDnsRecords(content); + + expect(result.NS).toHaveLength(1); + expect(result.NS[0].name).toBe(''); + expect(result.NS[0].value).toBe('localhost'); + expect(result.NS[0].ttl).toBe(1814400); + + expect(result.PTR).toHaveLength(1); + expect(result.PTR[0].name).toBe('1'); + expect(result.PTR[0].value).toBe('localhost'); + expect(result.PTR[0].ttl).toBe(1814400); + }); + + it('should handle empty content', () => { + const result = parseDnsRecords(''); + expect(result.A).toHaveLength(0); + expect(result.AAAA).toHaveLength(0); + expect(result.CNAME).toHaveLength(0); + expect(result.MX).toHaveLength(0); + expect(result.NS).toHaveLength(0); + expect(result.TXT).toHaveLength(0); + }); + + it('should handle invalid content', () => { + const result = parseDnsRecords('This is not a valid zone file'); + expect(result.A).toHaveLength(0); + expect(result.AAAA).toHaveLength(0); + expect(result.CNAME).toHaveLength(0); + expect(result.MX).toHaveLength(0); + expect(result.NS).toHaveLength(0); + expect(result.TXT).toHaveLength(0); + }); + + it('should handle content with only comments', () => { + const result = parseDnsRecords(`; This is a comment +; This is another comment +; And one more comment`); + expect(result.A).toHaveLength(0); + expect(result.AAAA).toHaveLength(0); + expect(result.CNAME).toHaveLength(0); + expect(result.MX).toHaveLength(0); + expect(result.NS).toHaveLength(0); + expect(result.TXT).toHaveLength(0); + }); +}); diff --git a/src/lib/helpers/domains.ts b/src/lib/helpers/domains.ts new file mode 100644 index 000000000..b10a08a82 --- /dev/null +++ b/src/lib/helpers/domains.ts @@ -0,0 +1,256 @@ +import type { DnsRecord } from '$lib/sdk/domains'; + +export type ParsedRecords = { + A: Partial[]; + AAAA: Partial[]; + CNAME: Partial[]; + MX: Partial[]; + TXT: Partial[]; + NS: Partial[]; + SRV: Partial[]; + CAA: Partial[]; + PTR: Partial[]; + HTTPS: Partial[]; + ALIAS: Partial[]; + [key: string]: Partial[]; +}; + +export function parseDnsRecords(content: string): ParsedRecords { + const records: ParsedRecords = { + A: [], + AAAA: [], + CNAME: [], + MX: [], + TXT: [], + NS: [], + SRV: [], + CAA: [], + PTR: [], + HTTPS: [], + ALIAS: [] + }; + + // If content is empty, return empty records + if (!content) { + return records; + } + + // Split the file into lines and process each line + const lines = content.split('\n'); + // Track origin for domain context + let origin = ''; + + for (let line of lines) { + // Skip empty lines and comments + line = line.trim(); + if (line === '' || line.startsWith(';') || line.startsWith('$')) { + // Check for origin directive + if (line.startsWith('$ORIGIN')) { + const parts = line.split(/\s+/); + if (parts.length >= 2) { + origin = parts[1]; + // Remove trailing dot if present + if (origin.endsWith('.')) { + origin = origin.slice(0, -1); + } + } + } + continue; + } + + // Skip SOA records + if (line.includes('SOA') || line.match(/^\s*\d+\s*$/)) { + continue; + } + + try { + // Parse the line into tokens, handling quoted values + const tokens: string[] = []; + let currentToken = ''; + let inQuotes = false; + + for (let i = 0; i < line.length; i++) { + const char = line[i]; + + if (char === '"' && (i === 0 || line[i - 1] !== '\\')) { + inQuotes = !inQuotes; + currentToken += char; + } else if (!inQuotes && (char === ' ' || char === '\t') && currentToken) { + tokens.push(currentToken); + currentToken = ''; + // Skip multiple spaces/tabs + while (i + 1 < line.length && (line[i + 1] === ' ' || line[i + 1] === '\t')) { + i++; + } + } else { + currentToken += char; + } + } + + if (currentToken) { + tokens.push(currentToken); + } + + if (tokens.length < 3) { + continue; // Not enough tokens for a valid record + } + + // Find the type index - look for "IN" class and the type after it + let typeIndex = -1; + let type = ''; + let inIndex = -1; // Track position of IN class marker + const validTypes = [ + 'A', + 'AAAA', + 'CNAME', + 'MX', + 'TXT', + 'NS', + 'SRV', + 'CAA', + 'PTR', + 'HTTPS', + 'ALIAS' + ]; + + // First look for IN class followed by record type + for (let i = 0; i < tokens.length - 1; i++) { + if (tokens[i].toUpperCase() === 'IN') { + inIndex = i; + if (validTypes.includes(tokens[i + 1].toUpperCase())) { + typeIndex = i + 1; + type = tokens[i + 1].toUpperCase(); + break; + } + } + } + + // If we didn't find it that way, check for standalone record types + if (typeIndex === -1) { + for (let i = 0; i < tokens.length; i++) { + const token = tokens[i].toUpperCase(); + if (validTypes.includes(token)) { + typeIndex = i; + type = token; + break; + } + } + } + + if (typeIndex === -1 || !type) { + continue; // No valid record type found + } + + // Parse name + let name = tokens[0]; + + // Handle @ symbol for root domain or empty for the domain itself + if (name === '@') { + name = ''; + } + + // Check if name is the same as origin (with or without trailing dot) + const normalizedName = name.endsWith('.') ? name.slice(0, -1) : name; + if (normalizedName === origin) { + name = ''; + } + // For names that end with the origin, extract just the subdomain part + else if (origin && normalizedName.endsWith(origin) && normalizedName !== origin) { + // Handle case like "ns.example.com" where origin is "example.com" + // We want to extract just "ns" + const subdomain = normalizedName.slice(0, -(origin.length + 1)); // +1 for the dot + if (subdomain) { + name = subdomain; + } + } + // Remove domain suffix if present (for absolute names ending with a dot) + else if (name.endsWith('.')) { + name = name.slice(0, -1); + } + + // For cases where first token is IN or a record type, use empty name + if (name.toUpperCase() === 'IN' || validTypes.includes(name.toUpperCase())) { + name = ''; + } + + // Find TTL - it's usually before the IN class + let ttl = 3600; // Default TTL + for (let i = 1; i < typeIndex; i++) { + const possibleTtl = parseInt(tokens[i]); + if (!isNaN(possibleTtl)) { + ttl = possibleTtl; + break; + } + } + + // Create record based on type + const record: Partial = { + name, + ttl, + type + }; + + switch (type) { + case 'A': + case 'AAAA': + case 'CNAME': + case 'NS': + case 'PTR': + case 'HTTPS': + case 'ALIAS': + case 'CAA': + if (typeIndex + 1 >= tokens.length) continue; // Skip if no value + record.value = tokens[typeIndex + 1]; + break; + case 'MX': + if (typeIndex + 2 >= tokens.length) continue; // Skip if not enough tokens + record.priority = parseInt(tokens[typeIndex + 1]) || 10; + record.value = tokens[typeIndex + 2]; + break; + case 'SRV': + if (typeIndex + 4 >= tokens.length) continue; // Skip if not enough tokens + record.priority = parseInt(tokens[typeIndex + 1]) || 0; + record.weight = parseInt(tokens[typeIndex + 2]) || 0; + record.port = parseInt(tokens[typeIndex + 3]) || 0; + record.value = tokens[typeIndex + 4]; + break; + case 'TXT': + // Handle quoted text + const txtValue = tokens.slice(typeIndex + 1).join(' '); + if (!txtValue) continue; // Skip if no value + // Clean quotes if present + record.value = txtValue.replace(/^"(.*)"$/, '$1'); + break; + default: + continue; // Skip unrecognized types + } + + // Clean value - remove trailing dot if present + if (record.value && record.value.endsWith('.')) { + record.value = record.value.slice(0, -1); + } + + // Add the record to the appropriate array if it has a valid value + if (record.value !== undefined && records[type]) { + // For invalid content check - ensure we have something that looks like a domain-related record + // For domain tests - ensure we have at least one of these indicators + const hasValidIndicators = + inIndex !== -1 || // Has 'IN' class + line.includes(' IN ') || + validTypes.some((vt) => line.includes(` ${vt} `)) || + /\d+\.\d+\.\d+\.\d+/.test(line) || // IP address pattern + /\s+NS\s+/.test(line) || // NS record pattern + /\s+MX\s+\d+\s+/.test(line); // MX record pattern + + if (hasValidIndicators) { + records[type].push(record); + } + } + } catch (e) { + console.error('Error parsing line:', line, e); + // Continue to next line + } + } + + return records; +} diff --git a/src/routes/(console)/organization-[organization]/domains/domain-[domain]/importRecordModal.svelte b/src/routes/(console)/organization-[organization]/domains/domain-[domain]/importRecordModal.svelte index b70d15255..ec2eba302 100644 --- a/src/routes/(console)/organization-[organization]/domains/domain-[domain]/importRecordModal.svelte +++ b/src/routes/(console)/organization-[organization]/domains/domain-[domain]/importRecordModal.svelte @@ -7,135 +7,20 @@ import { IconInfo } from '@appwrite.io/pink-icons-svelte'; import { removeFile } from '$lib/helpers/files'; import { sdk } from '$lib/stores/sdk'; - import type { DnsRecord } from '$lib/sdk/domains'; import { page } from '$app/state'; + import { parseDnsRecords } from '$lib/helpers/domains'; export let show = false; let files: FileList; let error = ''; - type ParsedRecords = { - A: Partial[]; - AAAA: Partial[]; - CNAME: Partial[]; - MX: Partial[]; - TXT: Partial[]; - NS: Partial[]; - SRV: Partial[]; - CAA: Partial[]; - PTR: Partial[]; - HTTPS: Partial[]; - ALIAS: Partial[]; - [key: string]: Partial[]; - }; - - function parseZoneFile(content: string): ParsedRecords { - const records: ParsedRecords = { - A: [], - AAAA: [], - CNAME: [], - MX: [], - TXT: [], - NS: [], - SRV: [], - CAA: [], - PTR: [], - HTTPS: [], - ALIAS: [] - }; - - // Split the content into lines and process each line - const lines = content.split('\n'); - - for (let line of lines) { - // Remove comments, but save them for record comment field - let comment = ''; - const commentIndex = line.indexOf(';'); - if (commentIndex !== -1) { - comment = line.substring(commentIndex + 1).trim(); - line = line.substring(0, commentIndex).trim(); - } - - // Skip empty lines or pure comment lines - if (!line.trim()) continue; - - // Split line into parts (whitespace separated) - const parts = line.trim().split(/\s+/); - - // Need at least a name, TTL/class/type, and value - if (parts.length < 3) continue; - - let recordType = ''; - let name = ''; - let value = ''; - let ttl = 3600; - let priority: number | undefined; - let weight: number | undefined; - let port: number | undefined; - - // Try to determine the record type from the parts - for (let i = 0; i < parts.length; i++) { - const part = parts[i].toUpperCase(); - if (['A', 'AAAA', 'CNAME', 'MX', 'TXT', 'NS', 'SRV', 'CAA'].includes(part)) { - recordType = part; - name = parts[0] === '@' ? '' : parts[0]; // @ represents the domain itself - - // Handle TTL if it appears to be a number - const possibleTtl = parseInt(parts[i - 1]); - if (!isNaN(possibleTtl) && i > 0) { - ttl = possibleTtl; - } - - if (recordType === 'MX' && i + 2 < parts.length) { - priority = parseInt(parts[i + 1]); - value = parts[i + 2]; - } else if (recordType === 'SRV' && i + 4 < parts.length) { - priority = parseInt(parts[i + 1]); - weight = parseInt(parts[i + 2]); - port = parseInt(parts[i + 3]); - value = parts[i + 4]; - } else if (i + 1 < parts.length) { - value = parts.slice(i + 1).join(' '); - } - - break; - } - } - - if (recordType && value) { - if (recordType === 'TXT' && value.startsWith('"') && value.endsWith('"')) { - value = value.slice(1, -1); - } - - const record: Partial = { - name, - value, - ttl, - comment: comment || undefined - }; - - if (priority !== undefined) { - record.priority = priority; - } - - if (!records[recordType]) { - records[recordType] = []; - } - - records[recordType].push(record); - } - } - - return records; - } - async function handleSubmit() { try { if (!files?.length) return; const file = files[0]; const content = await file.text(); - const parsedRecords = parseZoneFile(content); + const parsedRecords = parseDnsRecords(content); let recordCount = 0;