cleanRelayUrl function
Normalizes a relay URL according to RFC 3986.
This function performs the following normalizations (RFC 3986 Section 6):
- Trims whitespace
- Fixes malformed protocols (e.g., wss:/// -> wss://)
- Case normalization: lowercases scheme and host (Section 6.2.2.1)
- Percent-encoding normalization (Section 6.2.2.2):
- Decodes percent-encoded unreserved characters
- Uppercases hex digits in percent-encodings
- Path segment normalization: removes dot segments (Section 6.2.2.3)
- Scheme-based normalization: removes default ports (Section 6.2.3)
- Removes trailing slashes
- Validates the URL format
Returns null if the URL is invalid.
Implementation
String? cleanRelayUrl(String adr) {
adr = adr.trim();
if (adr.isEmpty) {
return null;
}
// Remove extra slashes after protocol (e.g., wss:/// -> wss://)
adr = adr.replaceFirstMapped(
_extraSlashesRegex, (match) => '${match.group(1)}//');
// Parse using Dart's Uri class for RFC 3986 compliance
Uri uri;
try {
uri = Uri.parse(adr);
} catch (_) {
return null;
}
// Validate scheme (only ws and wss allowed)
// RFC 3986 Section 6.2.2.1: Case normalization - scheme to lowercase
final scheme = uri.scheme.toLowerCase();
if (scheme != 'ws' && scheme != 'wss') {
return null;
}
// Validate host
// RFC 3986 Section 6.2.2.1: Case normalization - host to lowercase
final host = uri.host.toLowerCase();
if (host.isEmpty) {
return null;
}
// Check for invalid host patterns (starting/ending with hyphen)
if (host.startsWith('-') || host.endsWith('-')) {
return null;
}
// RFC 3986 Section 6.2.3: Scheme-based normalization
// Remove default ports (443 for wss, 80 for ws)
int? port = uri.hasPort ? uri.port : null;
if ((scheme == 'wss' && port == 443) || (scheme == 'ws' && port == 80)) {
port = null;
}
// RFC 3986 Section 6.2.2.3: Path segment normalization
// Remove dot segments using the algorithm from Section 5.2.4
String path = removeDotSegments(uri.path);
// Remove trailing slash from path
if (path.length > 1 && path.endsWith('/')) {
path = path.substring(0, path.length - 1);
}
// Build normalized URL
final buffer = StringBuffer('$scheme://$host');
if (port != null) {
buffer.write(':$port');
}
// Add normalized path
if (path.isNotEmpty && path != '/') {
buffer.write(normalizePercentEncoding(path));
}
// Preserve query string if present (with normalized percent-encoding)
if (uri.hasQuery) {
buffer.write('?${normalizePercentEncoding(uri.query)}');
}
// Preserve fragment if present (with normalized percent-encoding)
if (uri.hasFragment) {
buffer.write('#${normalizePercentEncoding(uri.fragment)}');
}
final normalizedUrl = buffer.toString();
// Final validation with regex
if (!normalizedUrl.contains(relayUrlRegex)) {
return null;
}
return normalizedUrl;
}