blob: 9fa586b93d0e828d586b7917d869e8b80f27bb52 [file] [log] [blame]
// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
part of dart.io;
/// Whether insecure connections to [host] are allowed.
///
/// [host] must be a [String] or [InternetAddress].
///
/// If any of the domain policies match [host], the matching policy will make
/// the decision. If multiple policies apply, the top matching policy makes the
/// decision. If none of the domain policies match, the embedder default is
/// used.
///
/// Loopback addresses are always allowed.
bool isInsecureConnectionAllowed(dynamic host) {
String hostString;
if (host is String) {
try {
if ("localhost" == host || InternetAddress(host).isLoopback) return true;
} on ArgumentError {
// Assume not loopback.
}
hostString = host;
} else if (host is InternetAddress) {
if (host.isLoopback) return true;
hostString = host.host;
} else {
throw ArgumentError.value(
host, "host", "Must be a String or InternetAddress");
}
final topMatchedPolicy = _findBestDomainNetworkPolicy(hostString);
final envOverride = bool.fromEnvironment(
"dart.library.io.may_insecurely_connect_to_all_domains",
defaultValue: true);
return topMatchedPolicy?.allowInsecureConnections ??
(envOverride && _EmbedderConfig._mayInsecurelyConnectToAllDomains);
}
/// Policy for a specific domain.
///
/// [_DomainNetworkPolicy] can be used to create exceptions to the global
/// network policy.
class _DomainNetworkPolicy {
/// https://tools.ietf.org/html/rfc1034#:~:text=Name%20space%20specifications
///
/// We specifically do not allow IP addresses.
static final _domainMatcher = RegExp(
r"^(?:[a-z\d-]{1,63}\.)+[a-z][a-z\d-]{0,62}$",
caseSensitive: false);
/// The domain on which the policy is being set.
///
/// This cannot be a numeric IP address.
///
/// For example: `example.com`.
final String domain;
/// Whether to allow insecure socket connections for this domain.
final bool allowInsecureConnections;
/// Whether this domain policy covers sub-domains as well.
///
/// If this is true, all subdomains inherit the same policy. For instance,
/// a policy set on `example.com` would apply to `*.example.com` such as
/// `subdomain.example.com` or `www.example.com`.
final bool includesSubDomains;
/// Creates a new domain exception in the network policy.
///
/// [domain] is the domain on which the policy is being set.
///
/// [includesSubDomains] determines whether the policy applies to
/// all sub domains. If this is set to true, all subdomains inherit the
/// same policy. For instance, a policy set on `example.com` would apply to
/// `*.example.com` such as `subdomain.example.com` or `www.example.com`.
///
/// [allowInsecureConnections] determines whether to allow insecure socket
/// connections for this [domain].
_DomainNetworkPolicy(this.domain,
{this.includesSubDomains = false,
this.allowInsecureConnections = false}) {
if (domain.length > 255 || !_domainMatcher.hasMatch(domain)) {
throw ArgumentError.value(domain, "domain", "Invalid domain name");
}
}
/// Calculates how well the policy matches to a given host string.
///
/// A host matches a [policy] if it ends with its [domain].
///
/// A score is given to such a match depending on the specificity of the
/// [domain]:
///
/// * A longer domain receives a higher score.
/// * A domain that does not allow sub domains receives a higher score.
///
/// Returns -1 if the policy does not match.
int matchScore(String host) {
final domainLength = domain.length;
final hostLength = host.length;
final lengthDelta = hostLength - domainLength;
if (host.endsWith(domain) &&
(lengthDelta == 0 ||
includesSubDomains && host.codeUnitAt(lengthDelta - 1) == 0x2e)) {
return domainLength * 2 + (includesSubDomains ? 0 : 1);
}
return -1;
}
/// Checks whether the [policy] to be added conflicts with existing policies.
///
/// Returns [true] if policy is safe to add to existing policy set and [false]
/// if policy can safely be ignored.
///
/// Throws [ArgumentError] if a conflict is detected.
bool checkConflict(List<_DomainNetworkPolicy> existingPolicies) {
for (final existingPolicy in existingPolicies) {
if (includesSubDomains == existingPolicy.includesSubDomains &&
domain == existingPolicy.domain) {
if (allowInsecureConnections ==
existingPolicy.allowInsecureConnections) {
// This is a duplicate policy
return false;
}
throw StateError("Contradiction in the domain security policies: "
"'$this' contradicts '$existingPolicy'");
}
}
return true;
}
/// This is used for encoding information about the policy in user visible
/// errors.
@override
String toString() {
final subDomainPrefix = includesSubDomains ? '*.' : '';
final insecureConnectionPermission =
allowInsecureConnections ? 'Allows' : 'Disallows';
return "$subDomainPrefix$domain: "
"$insecureConnectionPermission insecure connections";
}
}
/// Finds the top [DomainNetworkPolicy] instance that match given a single
/// [domain].
///
/// We order the policies according to how specific they are. The final policy
/// for a given [domain] is determined by the top matching
/// [DomainNetworkPolicy].
///
/// Returns null if there's no matching policy.
_DomainNetworkPolicy? _findBestDomainNetworkPolicy(String domain) {
var topScore = 0;
_DomainNetworkPolicy? topPolicy;
for (final _DomainNetworkPolicy policy in _domainPolicies) {
final score = policy.matchScore(domain);
if (score > topScore) {
topScore = score;
topPolicy = policy;
}
}
return topPolicy;
}
/// Domain level policies that dart:io is enforcing.
late List<_DomainNetworkPolicy> _domainPolicies =
_constructDomainPolicies(null);
List<_DomainNetworkPolicy> _constructDomainPolicies(
String? domainPoliciesString) {
final domainPolicies = <_DomainNetworkPolicy>[];
domainPoliciesString ??= String.fromEnvironment(
"dart.library.io.domain_network_policies",
defaultValue: "");
if (domainPoliciesString.isNotEmpty) {
final List<dynamic> policiesJson = json.decode(domainPoliciesString);
for (final List<dynamic> policyJson in policiesJson) {
assert(policyJson.length == 3);
final policy = _DomainNetworkPolicy(
policyJson[0],
includesSubDomains: policyJson[1],
allowInsecureConnections: policyJson[2],
);
if (policy.checkConflict(domainPolicies)) {
domainPolicies.add(policy);
}
}
}
return domainPolicies;
}