blob: 3efa5ef3d765488793a02b77e64c11929bdbecb6 [file] [log] [blame]
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:yaml/yaml.dart';
import '../base/logger.dart';
/// Represents a rule for proxying requests based on a specific pattern.
/// Subclasses must implement the [matches], [replace], and [getTargetUri] methods.
sealed class ProxyRule {
static const _kLogEntryPrefix = '[ProxyRule]';
static const _kTarget = 'target';
static const _kRegex = 'regex';
static const _kPrefix = 'prefix';
static const _kReplace = 'replace';
/// Checks if the given [path] matches the rule's pattern.
bool matches(String path);
/// Replaces the matched part of the [path] according to the rule's logic.
/// If no replacement is needed, it returns the original path.
String replace(String path);
/// Returns the target URI to which the request should be proxied.
Uri getTargetUri();
/// If both or neither 'prefix' and 'regex' are defined, it logs an error and returns null.
/// Otherwise, it tries to create a [PrefixProxyRule] or [RegexProxyRule] based on the [yaml] keys.
static ProxyRule? fromYaml(YamlMap yaml, Logger logger) {
if (PrefixProxyRule.canHandle(yaml) && RegexProxyRule.canHandle(yaml)) {
logger.printError(
'$_kLogEntryPrefix Both $_kPrefix and $_kRegex are defined in the proxy rule YAML.'
' Only one should be used.',
);
return null;
} else if (PrefixProxyRule.canHandle(yaml)) {
return PrefixProxyRule.fromYaml(yaml, logger);
} else if (RegexProxyRule.canHandle(yaml)) {
return RegexProxyRule.fromYaml(yaml, logger);
} else {
logger.printError('$_kLogEntryPrefix Invalid proxy rule in YAML: $yaml');
return null;
}
}
}
/// A [ProxyRule] implementation that uses regular expressions for matching and
/// replacement.
///
/// This rule matches paths against a provided regular expression [_pattern].
/// If a [_replacement] string is provided, it replaces parts of the matched
/// path based on regex group capturing.
class RegexProxyRule implements ProxyRule {
/// Creates a [RegexProxyRule] with the given regular expression [pattern],
/// [target] URI base, and optional [replacement] string.
RegexProxyRule({required RegExp pattern, required String target, String? replacement})
: _pattern = pattern,
_target = target,
_replacement = replacement;
final RegExp _pattern;
final String _target;
final String? _replacement;
@override
bool matches(String path) {
return _pattern.hasMatch(path);
}
@override
String replace(String path) {
if (_replacement == null) {
return path;
}
return path.replaceAllMapped(_pattern, (Match match) {
String result = _replacement;
for (var i = 0; i <= match.groupCount; i++) {
result = result.replaceAll('\$$i', match.group(i) ?? '');
}
return result;
});
}
@override
Uri getTargetUri() {
final Uri targetBaseUri = Uri.parse(_target);
return targetBaseUri;
}
@override
String toString() {
return '{${ProxyRule._kRegex}: ${_pattern.pattern}, ${ProxyRule._kTarget}: $_target, ${ProxyRule._kReplace}: ${_replacement ?? 'null'}}';
}
/// Checks if the given [yaml] can be handled by this rule.
/// It requires the 'regex' key to be present and non-empty.
static bool canHandle(YamlMap yaml) {
return yaml.containsKey(ProxyRule._kRegex) &&
yaml[ProxyRule._kRegex] is String &&
(yaml[ProxyRule._kRegex] as String).isNotEmpty;
}
/// Attempts to create a [RegexProxyRule] from the provided [yaml] map.
/// If the 'regex' or 'target' keys are missing or invalid, it logs an error
/// and returns null.
/// If the 'regex' is invalid, it logs a warning and treats it as a string.
static RegexProxyRule? fromYaml(YamlMap yaml, Logger effectiveLogger) {
final regex = yaml[ProxyRule._kRegex] as String?;
final target = yaml[ProxyRule._kTarget] as String?;
final replacement = yaml[ProxyRule._kReplace] as String?;
if (regex == null || regex.isEmpty) {
return null;
} else if (target == null || target.isEmpty) {
effectiveLogger.printError(
'${ProxyRule._kLogEntryPrefix} Invalid ${ProxyRule._kTarget} for '
'${ProxyRule._kRegex}: $regex. ${ProxyRule._kTarget} cannot be null',
);
return null;
}
RegExp? pattern;
try {
pattern = RegExp(regex.trim());
} on FormatException catch (e) {
pattern = RegExp(RegExp.escape(regex));
effectiveLogger.printWarning(
'${ProxyRule._kLogEntryPrefix} Invalid regex pattern in ${ProxyRule._kRegex}: $regex. '
'Treating $regex as string. Error: $e',
);
}
return RegexProxyRule(pattern: pattern, target: target, replacement: replacement?.trim());
}
}
/// A [ProxyRule] implementation that matches paths starting with a specific prefix.
///
/// If a [_replacement] string is provided, it replaces the prefix in the matched path.
class PrefixProxyRule extends RegexProxyRule {
/// Creates a [PrefixProxyRule] with the given [prefix] prefix, [target] URI base,
/// and optional [replacement] string.
PrefixProxyRule({required String prefix, required super.target, super.replacement})
: super(pattern: RegExp('^${RegExp.escape(prefix)}'));
@override
String toString() {
return '{${ProxyRule._kPrefix}: ${_pattern.pattern}, ${ProxyRule._kTarget}: $_target, ${ProxyRule._kReplace}: ${_replacement ?? 'null'}}';
}
/// Checks if the given [yaml] can be handled by this rule.
/// It requires the 'prefix' key to be present and non-empty.
static bool canHandle(YamlMap yaml) {
return yaml.containsKey(ProxyRule._kPrefix) &&
yaml[ProxyRule._kPrefix] is String &&
(yaml[ProxyRule._kPrefix] as String).isNotEmpty;
}
/// Attempts to create a [PrefixProxyRule] from the provided [yaml] map.
/// If the 'prefix' or 'target' keys are missing or invalid, it logs an error
/// and returns null.
static PrefixProxyRule? fromYaml(YamlMap yaml, Logger effectiveLogger) {
final prefix = yaml[ProxyRule._kPrefix] as String?;
final target = yaml[ProxyRule._kTarget] as String?;
final replacement = yaml[ProxyRule._kReplace] as String?;
if (prefix == null || prefix.isEmpty) {
return null;
} else if (target == null || target.isEmpty) {
effectiveLogger.printError(
'${ProxyRule._kLogEntryPrefix} Invalid ${ProxyRule._kTarget} for '
'${ProxyRule._kPrefix}: $prefix. ${ProxyRule._kTarget} cannot be null',
);
return null;
}
return PrefixProxyRule(prefix: prefix, target: target, replacement: replacement?.trim());
}
}