Browse Source
servers and i18n support - Move connection tabs from Dialog to PageComponents/Connect/Tabs for better organization - Add persistent HTTP server storage with status tracking in appStore - Implement hostname parsing and validation utilities - Default new HTTP connections to HTTPS for better security - Add comprehensive i18n translation support for all connection tab strings - Remove package-lock.json in favor of deno.lock dependency management - Update connection tab UI with improved server management (add/edit/remove) - Remove preliminary status checking logic (to be reimplemented with toradio API) - Maintain fixed window constraints for large monitor compatibilitypull/645/head
12 changed files with 533 additions and 14731 deletions
File diff suppressed because it is too large
@ -0,0 +1,230 @@ |
|||||
|
/** |
||||
|
* Utility functions for parsing and validating hostnames and network addresses |
||||
|
*/ |
||||
|
|
||||
|
export interface ParsedHostname { |
||||
|
hostname: string; |
||||
|
port?: number; |
||||
|
secure: boolean; |
||||
|
hasExplicitProtocol: boolean; |
||||
|
isValid: boolean; |
||||
|
error?: string; |
||||
|
} |
||||
|
|
||||
|
const DEFAULT_HTTP_PORT = 80; // Standard HTTP port
|
||||
|
const DEFAULT_HTTPS_PORT = 443; // Standard HTTPS port
|
||||
|
|
||||
|
/** |
||||
|
* Parses and validates a hostname input, handling various formats: |
||||
|
* - hostname.local |
||||
|
* - 192.168.1.100 |
||||
|
* - hostname:8080 |
||||
|
* - http://hostname
|
||||
|
* - https://hostname:8080
|
||||
|
*/ |
||||
|
export function parseHostname(input: string): ParsedHostname { |
||||
|
if (!input || typeof input !== "string") { |
||||
|
return { |
||||
|
hostname: "", |
||||
|
secure: false, |
||||
|
hasExplicitProtocol: false, |
||||
|
isValid: false, |
||||
|
error: "Invalid input: hostname is required", |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
// Trim whitespace and remove dangerous characters
|
||||
|
const sanitized = input.trim().replace(/[<>&"']/g, ""); |
||||
|
|
||||
|
if (!sanitized) { |
||||
|
return { |
||||
|
hostname: "", |
||||
|
secure: false, |
||||
|
hasExplicitProtocol: false, |
||||
|
isValid: false, |
||||
|
error: "Invalid input: hostname cannot be empty", |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
let hostname = sanitized; |
||||
|
let secure = false; |
||||
|
let hasExplicitProtocol = false; |
||||
|
let port: number | undefined; |
||||
|
|
||||
|
// Check for explicit protocol
|
||||
|
if (hostname.startsWith("https://")) { |
||||
|
secure = true; |
||||
|
hasExplicitProtocol = true; |
||||
|
hostname = hostname.slice(8); // Remove 'https://'
|
||||
|
} else if (hostname.startsWith("http://")) { |
||||
|
secure = false; |
||||
|
hasExplicitProtocol = true; |
||||
|
hostname = hostname.slice(7); // Remove 'http://'
|
||||
|
} |
||||
|
|
||||
|
// Remove trailing slash if present
|
||||
|
hostname = hostname.replace(/\/$/, ""); |
||||
|
|
||||
|
// Extract port if specified
|
||||
|
const portMatch = hostname.match(/^(.+):(\d+)$/); |
||||
|
if (portMatch) { |
||||
|
hostname = portMatch[1]; |
||||
|
const parsedPort = parseInt(portMatch[2], 10); |
||||
|
|
||||
|
if (parsedPort < 1 || parsedPort > 65535) { |
||||
|
return { |
||||
|
hostname: hostname, |
||||
|
secure, |
||||
|
hasExplicitProtocol, |
||||
|
isValid: false, |
||||
|
error: "Invalid port: must be between 1 and 65535", |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
port = parsedPort; |
||||
|
} |
||||
|
|
||||
|
// Validate hostname format
|
||||
|
if (!isValidHostname(hostname)) { |
||||
|
return { |
||||
|
hostname: hostname, |
||||
|
port, |
||||
|
secure, |
||||
|
hasExplicitProtocol, |
||||
|
isValid: false, |
||||
|
error: "Invalid hostname format", |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
return { |
||||
|
hostname: hostname, |
||||
|
port, |
||||
|
secure, |
||||
|
hasExplicitProtocol, |
||||
|
isValid: true, |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Validates hostname format (domain names and IP addresses) |
||||
|
*/ |
||||
|
function isValidHostname(hostname: string): boolean { |
||||
|
if (!hostname || hostname.length === 0) { |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
// Check for invalid characters
|
||||
|
if (!/^[a-zA-Z0-9.-]+$/.test(hostname)) { |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
// Check if it's an IPv4 address
|
||||
|
if (isValidIPv4(hostname)) { |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
// Check if it's a valid domain name
|
||||
|
if (isValidDomain(hostname)) { |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Validates IPv4 address format |
||||
|
*/ |
||||
|
function isValidIPv4(ip: string): boolean { |
||||
|
const ipv4Regex = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/; |
||||
|
const match = ip.match(ipv4Regex); |
||||
|
|
||||
|
if (!match) { |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
// Check that each octet is between 0-255
|
||||
|
for (let i = 1; i <= 4; i++) { |
||||
|
const octet = parseInt(match[i], 10); |
||||
|
if (octet < 0 || octet > 255) { |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Validates domain name format |
||||
|
*/ |
||||
|
function isValidDomain(domain: string): boolean { |
||||
|
// Basic domain validation
|
||||
|
if (domain.length > 253) { |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
// Cannot start or end with hyphen or dot
|
||||
|
if ( |
||||
|
domain.startsWith("-") || domain.endsWith("-") || |
||||
|
domain.startsWith(".") || domain.endsWith(".") |
||||
|
) { |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
// Split into labels and validate each
|
||||
|
const labels = domain.split("."); |
||||
|
for (const label of labels) { |
||||
|
if (label.length === 0 || label.length > 63) { |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
// Label cannot start or end with hyphen
|
||||
|
if (label.startsWith("-") || label.endsWith("-")) { |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
// Label must contain only alphanumeric and hyphens
|
||||
|
if (!/^[a-zA-Z0-9-]+$/.test(label)) { |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Formats hostname with appropriate default port for connection |
||||
|
*/ |
||||
|
export function formatHostnameForConnection(parsed: ParsedHostname): string { |
||||
|
if (!parsed.isValid) { |
||||
|
throw new Error(parsed.error || "Invalid hostname"); |
||||
|
} |
||||
|
|
||||
|
const port = parsed.port || |
||||
|
(parsed.secure ? DEFAULT_HTTPS_PORT : DEFAULT_HTTP_PORT); |
||||
|
|
||||
|
// Only include port if it was explicitly provided or if it's not the default
|
||||
|
const shouldIncludePort = parsed.port !== undefined; |
||||
|
|
||||
|
return shouldIncludePort ? `${parsed.hostname}:${port}` : parsed.hostname; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Formats hostname for TransportHTTP.create() - always includes port |
||||
|
*/ |
||||
|
export function formatHostnameForTransport(parsed: ParsedHostname): string { |
||||
|
if (!parsed.isValid) { |
||||
|
throw new Error(parsed.error || "Invalid hostname"); |
||||
|
} |
||||
|
|
||||
|
const port = parsed.port || |
||||
|
(parsed.secure ? DEFAULT_HTTPS_PORT : DEFAULT_HTTP_PORT); |
||||
|
|
||||
|
return `${parsed.hostname}:${port}`; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Gets the appropriate protocol string based on parsed hostname |
||||
|
*/ |
||||
|
export function getProtocol(parsed: ParsedHostname): "http" | "https" { |
||||
|
return parsed.secure ? "https" : "http"; |
||||
|
} |
||||
Loading…
Reference in new issue