Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
265 changes: 144 additions & 121 deletions packages/flutter_tools/lib/src/base/net.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,152 +4,175 @@

import 'dart:async';

import '../base/context.dart';
import 'package:meta/meta.dart';
import 'package:platform/platform.dart';

import '../convert.dart';
import '../globals.dart' as globals;
import 'common.dart';
import 'file_system.dart';
import 'io.dart';
import 'logger.dart';

const int kNetworkProblemExitCode = 50;

typedef HttpClientFactory = HttpClient Function();

typedef UrlTunneller = Future<String> Function(String url);

/// Download a file from the given URL.
///
/// If a destination file is not provided, returns the bytes.
///
/// If a destination file is provided, streams the bytes to that file and
/// returns an empty list.
///
/// If [maxAttempts] is exceeded, returns null.
Future<List<int>> fetchUrl(Uri url, {
int maxAttempts,
File destFile,
}) async {
int attempts = 0;
int durationSeconds = 1;
while (true) {
attempts += 1;
_MemoryIOSink memorySink;
IOSink sink;
if (destFile == null) {
memorySink = _MemoryIOSink();
sink = memorySink;
} else {
sink = destFile.openWrite();
}
class Net {
Net({
HttpClientFactory httpClientFactory,
@required Logger logger,
@required Platform platform,
}) :
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

uber-nit: I don't love the formatting here. Of course, if you do it the other way then you end up odd indentation on the next lines.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup. I was trying to avoid the odd indentation for the ':'

_httpClientFactory = httpClientFactory,
_logger = logger,
_platform = platform;

final HttpClientFactory _httpClientFactory;

final Logger _logger;

final Platform _platform;

/// Download a file from the given URL.
///
/// If a destination file is not provided, returns the bytes.
///
/// If a destination file is provided, streams the bytes to that file and
/// returns an empty list.
///
/// If [maxAttempts] is exceeded, returns null.
Future<List<int>> fetchUrl(Uri url, {
int maxAttempts,
File destFile,
}) async {
int attempts = 0;
int durationSeconds = 1;
while (true) {
attempts += 1;
_MemoryIOSink memorySink;
IOSink sink;
if (destFile == null) {
memorySink = _MemoryIOSink();
sink = memorySink;
} else {
sink = destFile.openWrite();
}

final bool result = await _attempt(
url,
destSink: sink,
);
if (result) {
return memorySink?.writes?.takeBytes() ?? <int>[];
}
final bool result = await _attempt(
url,
destSink: sink,
);
if (result) {
return memorySink?.writes?.takeBytes() ?? <int>[];
}

if (maxAttempts != null && attempts >= maxAttempts) {
globals.printStatus('Download failed -- retry $attempts');
return null;
}
globals.printStatus('Download failed -- attempting retry $attempts in '
'$durationSeconds second${ durationSeconds == 1 ? "" : "s"}...');
await Future<void>.delayed(Duration(seconds: durationSeconds));
if (durationSeconds < 64) {
durationSeconds *= 2;
if (maxAttempts != null && attempts >= maxAttempts) {
_logger.printStatus('Download failed -- retry $attempts');
return null;
}
_logger.printStatus(
'Download failed -- attempting retry $attempts in '
'$durationSeconds second${ durationSeconds == 1 ? "" : "s"}...',
);
await Future<void>.delayed(Duration(seconds: durationSeconds));
if (durationSeconds < 64) {
durationSeconds *= 2;
}
}
}
}

/// Check if the given URL points to a valid endpoint.
Future<bool> doesRemoteFileExist(Uri url) async => await _attempt(url, onlyHeaders: true);

// Returns true on success and false on failure.
Future<bool> _attempt(Uri url, {
IOSink destSink,
bool onlyHeaders = false,
}) async {
assert(onlyHeaders || destSink != null);
globals.printTrace('Downloading: $url');
HttpClient httpClient;
if (context.get<HttpClientFactory>() != null) {
httpClient = context.get<HttpClientFactory>()();
} else {
httpClient = HttpClient();
}
HttpClientRequest request;
HttpClientResponse response;
try {
if (onlyHeaders) {
request = await httpClient.headUrl(url);
/// Check if the given URL points to a valid endpoint.
Future<bool> doesRemoteFileExist(Uri url) => _attempt(url, onlyHeaders: true);

// Returns true on success and false on failure.
Future<bool> _attempt(Uri url, {
IOSink destSink,
bool onlyHeaders = false,
}) async {
assert(onlyHeaders || destSink != null);
_logger.printTrace('Downloading: $url');
HttpClient httpClient;
if (_httpClientFactory != null) {
httpClient = _httpClientFactory();
} else {
request = await httpClient.getUrl(url);
}
response = await request.close();
} on ArgumentError catch (error) {
final String overrideUrl = globals.platform.environment['FLUTTER_STORAGE_BASE_URL'];
if (overrideUrl != null && url.toString().contains(overrideUrl)) {
globals.printError(error.toString());
throwToolExit(
'The value of FLUTTER_STORAGE_BASE_URL ($overrideUrl) could not be '
'parsed as a valid url. Please see https://flutter.dev/community/china '
'for an example of how to use it.\n'
'Full URL: $url',
exitCode: kNetworkProblemExitCode,);
httpClient = HttpClient();
}
globals.printError(error.toString());
rethrow;
} on HandshakeException catch (error) {
globals.printTrace(error.toString());
throwToolExit(
'Could not authenticate download server. You may be experiencing a man-in-the-middle attack,\n'
'your network may be compromised, or you may have malware installed on your computer.\n'
'URL: $url',
exitCode: kNetworkProblemExitCode,
);
} on SocketException catch (error) {
globals.printTrace('Download error: $error');
return false;
} on HttpException catch (error) {
globals.printTrace('Download error: $error');
return false;
}
assert(response != null);

// If we're making a HEAD request, we're only checking to see if the URL is
// valid.
if (onlyHeaders) {
return response.statusCode == HttpStatus.ok;
}
if (response.statusCode != HttpStatus.ok) {
if (response.statusCode > 0 && response.statusCode < 500) {
HttpClientRequest request;
HttpClientResponse response;
try {
if (onlyHeaders) {
request = await httpClient.headUrl(url);
} else {
request = await httpClient.getUrl(url);
}
response = await request.close();
} on ArgumentError catch (error) {
final String overrideUrl = _platform.environment['FLUTTER_STORAGE_BASE_URL'];
if (overrideUrl != null && url.toString().contains(overrideUrl)) {
_logger.printError(error.toString());
throwToolExit(
'The value of FLUTTER_STORAGE_BASE_URL ($overrideUrl) could not be '
'parsed as a valid url. Please see https://flutter.dev/community/china '
'for an example of how to use it.\n'
'Full URL: $url',
exitCode: kNetworkProblemExitCode,);
}
_logger.printError(error.toString());
rethrow;
} on HandshakeException catch (error) {
_logger.printTrace(error.toString());
throwToolExit(
'Download failed.\n'
'URL: $url\n'
'Error: ${response.statusCode} ${response.reasonPhrase}',
'Could not authenticate download server. You may be experiencing a man-in-the-middle attack,\n'
'your network may be compromised, or you may have malware installed on your computer.\n'
'URL: $url',
exitCode: kNetworkProblemExitCode,
);
} on SocketException catch (error) {
_logger.printTrace('Download error: $error');
return false;
} on HttpException catch (error) {
_logger.printTrace('Download error: $error');
return false;
}
assert(response != null);

// If we're making a HEAD request, we're only checking to see if the URL is
// valid.
if (onlyHeaders) {
return response.statusCode == HttpStatus.ok;
}
if (response.statusCode != HttpStatus.ok) {
if (response.statusCode > 0 && response.statusCode < 500) {
throwToolExit(
'Download failed.\n'
'URL: $url\n'
'Error: ${response.statusCode} ${response.reasonPhrase}',
exitCode: kNetworkProblemExitCode,
);
}
// 5xx errors are server errors and we can try again
_logger.printTrace('Download error: ${response.statusCode} ${response.reasonPhrase}');
return false;
}
_logger.printTrace('Received response from server, collecting bytes...');
try {
assert(destSink != null);
await response.forEach(destSink.add);
return true;
} on IOException catch (error) {
_logger.printTrace('Download error: $error');
return false;
} finally {
await destSink?.flush();
await destSink?.close();
}
// 5xx errors are server errors and we can try again
globals.printTrace('Download error: ${response.statusCode} ${response.reasonPhrase}');
return false;
}
globals.printTrace('Received response from server, collecting bytes...');
try {
assert(destSink != null);
await response.forEach(destSink.add);
return true;
} on IOException catch (error) {
globals.printTrace('Download error: $error');
return false;
} finally {
await destSink?.flush();
await destSink?.close();
}
}



/// An IOSink that collects whatever is written to it.
class _MemoryIOSink implements IOSink {
@override
Expand Down
46 changes: 29 additions & 17 deletions packages/flutter_tools/lib/src/cache.dart
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,9 @@ class Cache {
_fileSystem = fileSystem ?? globals.fs,
_platform = platform ?? globals.platform ,
_osUtils = osUtils ?? os {
// TODO(zra): Move to initializer list once logger and platform parameters
// are required.
_net = Net(logger: _logger, platform: _platform);
if (artifacts == null) {
_artifacts.add(MaterialFonts(this));

Expand Down Expand Up @@ -135,6 +138,8 @@ class Cache {
final FileSystem _fileSystem;
final OperatingSystemUtils _osUtils;

Net _net;

static const List<String> _hostsBlockedInChina = <String> [
'storage.googleapis.com',
];
Expand Down Expand Up @@ -386,7 +391,7 @@ class Cache {
final File cachedFile = _fileSystem.file(_fileSystem.path.join(serviceDir.path, url.pathSegments.last));
if (!cachedFile.existsSync()) {
try {
await _downloadFile(url, cachedFile);
await downloadFile(url, cachedFile);
} catch (e) {
throwToolExit('Failed to fetch third-party artifact $url: $e');
}
Expand Down Expand Up @@ -439,6 +444,26 @@ class Cache {
this.includeAllPlatforms = includeAllPlatformsState;
return allAvailible;
}

/// Download a file from the given [url] and write it to [location].
Future<void> downloadFile(Uri url, File location) async {
_ensureExists(location.parent);
await _net.fetchUrl(url, destFile: location);
}

Future<bool> doesRemoteExist(String message, Uri url) async {
final Status status = globals.logger.startProgress(
message,
timeout: timeoutConfiguration.slowOperation,
);
bool exists;
try {
exists = await _net.doesRemoteFileExist(url);
} finally {
status.stop();
}
return exists;
}
}

/// Representation of a set of artifacts used by the tool.
Expand Down Expand Up @@ -559,7 +584,7 @@ abstract class CachedArtifact extends ArtifactSet {
if (!verifier(tempFile)) {
final Status status = globals.logger.startProgress(message, timeout: timeoutConfiguration.slowOperation);
try {
await _downloadFile(url, tempFile);
await cache.downloadFile(url, tempFile);
status.stop();
} catch (exception) {
status.cancel();
Expand Down Expand Up @@ -741,7 +766,7 @@ abstract class EngineCachedArtifact extends CachedArtifact {

bool exists = false;
for (final String pkgName in getPackageDirs()) {
exists = await _doesRemoteExist('Checking package $pkgName is available...',
exists = await cache.doesRemoteExist('Checking package $pkgName is available...',
Uri.parse(url + pkgName + '.zip'));
if (!exists) {
return false;
Expand All @@ -751,7 +776,7 @@ abstract class EngineCachedArtifact extends CachedArtifact {
for (final List<String> toolsDir in getBinaryDirs()) {
final String cacheDir = toolsDir[0];
final String urlPath = toolsDir[1];
exists = await _doesRemoteExist('Checking $cacheDir tools are available...',
exists = await cache.doesRemoteExist('Checking $cacheDir tools are available...',
Uri.parse(url + urlPath));
if (!exists) {
return false;
Expand Down Expand Up @@ -1305,19 +1330,6 @@ String flattenNameSubdirs(Uri url) {
return globals.fs.path.joinAll(convertedPieces);
}

/// Download a file from the given [url] and write it to [location].
Future<void> _downloadFile(Uri url, File location) async {
_ensureExists(location.parent);
await fetchUrl(url, destFile: location);
}

Future<bool> _doesRemoteExist(String message, Uri url) async {
final Status status = globals.logger.startProgress(message, timeout: timeoutConfiguration.slowOperation);
final bool exists = await doesRemoteFileExist(url);
status.stop();
return exists;
}

/// Create the given [directory] and parents, as necessary.
void _ensureExists(Directory directory) {
if (!directory.existsSync()) {
Expand Down
Loading