mirror of
https://github.com/we-promise/sure.git
synced 2026-05-24 21:14:56 +00:00
* Add mobile custom proxy headers * Clear login placeholders on focus Email/password fields ship with example values pre-filled. Tapping the field now clears the placeholder so users don't have to delete it manually. Skips clearing if the user has already edited the value. * Push Configuration as a route from Sign in Opening Configuration from the Sign in screen now uses Navigator.push instead of toggling a state flag, so Android back returns to Sign in instead of quitting the app. Saving the URL auto-pops the route. * Address PR review on custom proxy headers - Test Connection no longer leaves global ApiConfig headers mutated; unsaved edits are restored in a finally block after the probe. - _loadSavedUrl / _loadCustomHeaders wrap storage reads in try/catch and always finish initialization with sensible defaults. - Sanitization is now a single CustomProxyHeader.sanitize() reused by ApiConfig.setCustomProxyHeaders and CustomProxyHeadersService. - Brief comment on redactedValue explaining the length-obscuring design. * Harden custom proxy header validation and load path - validateValue now rejects ASCII control characters (CR/LF/tab/etc.) to prevent header-injection via crafted values. - loadHeaders moves the secure-storage read inside the try block so platform exceptions are caught the same way JSON parse errors are.
91 lines
2.8 KiB
Dart
91 lines
2.8 KiB
Dart
class CustomProxyHeader {
|
|
static final RegExp _headerNamePattern = RegExp(r"^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$");
|
|
// Reject ASCII control bytes in values to block CR/LF header injection.
|
|
static final RegExp _headerValueControlChars = RegExp(r'[\x00-\x1F\x7F]');
|
|
static const Set<String> _reservedNames = {
|
|
'accept',
|
|
'authorization',
|
|
'content-type',
|
|
'x-api-key',
|
|
};
|
|
|
|
final String name;
|
|
final String value;
|
|
|
|
CustomProxyHeader({
|
|
required String name,
|
|
required String value,
|
|
}) : name = name.trim(),
|
|
value = value.trim();
|
|
|
|
factory CustomProxyHeader.fromJson(Map<String, dynamic> json) {
|
|
return CustomProxyHeader(
|
|
name: json['name'] as String? ?? '',
|
|
value: json['value'] as String? ?? '',
|
|
);
|
|
}
|
|
|
|
Map<String, dynamic> toJson() => {
|
|
'name': name,
|
|
'value': value,
|
|
};
|
|
|
|
String get normalizedName => name.toLowerCase();
|
|
|
|
// Length is intentionally obscured: short values get a fixed 4-bullet mask
|
|
// and longer values get a fixed 6-bullet prefix + last 4 chars. Keeping the
|
|
// last 4 lets users sanity-check what they entered without leaking length.
|
|
String get redactedValue {
|
|
if (value.isEmpty) return '';
|
|
if (value.length <= 4) return '••••';
|
|
return '••••••${value.substring(value.length - 4)}';
|
|
}
|
|
|
|
/// Drops headers with empty/invalid name or value, then dedupes by
|
|
/// case-insensitive name (last write wins). Single source of truth used by
|
|
/// both `ApiConfig.setCustomProxyHeaders` and the persistence service.
|
|
static List<CustomProxyHeader> sanitize(List<CustomProxyHeader> headers) {
|
|
final byName = <String, CustomProxyHeader>{};
|
|
for (final header in headers) {
|
|
if (!header.isComplete) continue;
|
|
if (validateName(header.name) != null) continue;
|
|
if (validateValue(header.value) != null) continue;
|
|
byName[header.normalizedName] = header;
|
|
}
|
|
return byName.values.toList(growable: false);
|
|
}
|
|
|
|
bool get isComplete => name.isNotEmpty && value.isNotEmpty;
|
|
|
|
static String? validateName(String value) {
|
|
final trimmed = value.trim();
|
|
if (trimmed.isEmpty) return 'Header name is required';
|
|
if (!_headerNamePattern.hasMatch(trimmed)) {
|
|
return 'Use a valid HTTP header name';
|
|
}
|
|
if (_reservedNames.contains(trimmed.toLowerCase())) {
|
|
return 'This header is managed by the app';
|
|
}
|
|
return null;
|
|
}
|
|
|
|
static String? validateValue(String value) {
|
|
if (value.trim().isEmpty) return 'Header value is required';
|
|
if (_headerValueControlChars.hasMatch(value)) {
|
|
return 'Header value contains control characters';
|
|
}
|
|
return null;
|
|
}
|
|
|
|
@override
|
|
bool operator ==(Object other) {
|
|
return identical(this, other) ||
|
|
other is CustomProxyHeader &&
|
|
name == other.name &&
|
|
value == other.value;
|
|
}
|
|
|
|
@override
|
|
int get hashCode => Object.hash(name, value);
|
|
}
|