net.dart 5.83 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5 6
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';

7
import '../base/context.dart';
8
import '../convert.dart';
9
import '../globals.dart' as globals;
10
import 'common.dart';
11
import 'file_system.dart';
12
import 'io.dart';
13 14

const int kNetworkProblemExitCode = 50;
15

16
typedef HttpClientFactory = HttpClient Function();
17

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

20 21 22 23 24 25 26 27 28 29 30 31
/// 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 {
32
  int attempts = 0;
33
  int durationSeconds = 1;
34 35
  while (true) {
    attempts += 1;
36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
    _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>[];
51
    }
52

53
    if (maxAttempts != null && attempts >= maxAttempts) {
54
      globals.printStatus('Download failed -- retry $attempts');
55 56
      return null;
    }
57
    globals.printStatus('Download failed -- attempting retry $attempts in '
58 59
        '$durationSeconds second${ durationSeconds == 1 ? "" : "s"}...');
    await Future<void>.delayed(Duration(seconds: durationSeconds));
60
    if (durationSeconds < 64) {
61
      durationSeconds *= 2;
62
    }
63 64
  }
}
65

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

69 70 71 72 73 74
// Returns true on success and false on failure.
Future<bool> _attempt(Uri url, {
  IOSink destSink,
  bool onlyHeaders = false,
}) async {
  assert(onlyHeaders || destSink != null);
75
  globals.printTrace('Downloading: $url');
76
  HttpClient httpClient;
77 78
  if (context.get<HttpClientFactory>() != null) {
    httpClient = context.get<HttpClientFactory>()();
79
  } else {
80
    httpClient = HttpClient();
81
  }
82
  HttpClientRequest request;
83
  HttpClientResponse response;
84
  try {
85 86 87 88 89
    if (onlyHeaders) {
      request = await httpClient.headUrl(url);
    } else {
      request = await httpClient.getUrl(url);
    }
90
    response = await request.close();
91
  } on ArgumentError catch (error) {
92
    final String overrideUrl = globals.platform.environment['FLUTTER_STORAGE_BASE_URL'];
93
    if (overrideUrl != null && url.toString().contains(overrideUrl)) {
94
      globals.printError(error.toString());
95 96 97 98 99 100 101
      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,);
    }
102
    globals.printError(error.toString());
103
    rethrow;
104
  } on HandshakeException catch (error) {
105
    globals.printTrace(error.toString());
106 107 108 109 110 111
    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,
    );
112
  } on SocketException catch (error) {
113
    globals.printTrace('Download error: $error');
114
    return false;
115
  } on HttpException catch (error) {
116
    globals.printTrace('Download error: $error');
117
    return false;
118
  }
119 120
  assert(response != null);

121 122 123
  // If we're making a HEAD request, we're only checking to see if the URL is
  // valid.
  if (onlyHeaders) {
124
    return response.statusCode == HttpStatus.ok;
125
  }
126
  if (response.statusCode != HttpStatus.ok) {
127 128 129 130 131 132 133 134 135
    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
136
    globals.printTrace('Download error: ${response.statusCode} ${response.reasonPhrase}');
137
    return false;
138
  }
139
  globals.printTrace('Received response from server, collecting bytes...');
140
  try {
141 142 143
    assert(destSink != null);
    await response.forEach(destSink.add);
    return true;
144
  } on IOException catch (error) {
145
    globals.printTrace('Download error: $error');
146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179
    return false;
  } finally {
    await destSink?.flush();
    await destSink?.close();
  }
}

/// An IOSink that collects whatever is written to it.
class _MemoryIOSink implements IOSink {
  @override
  Encoding encoding = utf8;

  final BytesBuilder writes = BytesBuilder(copy: false);

  @override
  void add(List<int> data) {
    writes.add(data);
  }

  @override
  Future<void> addStream(Stream<List<int>> stream) {
    final Completer<void> completer = Completer<void>();
    stream.listen(add).onDone(completer.complete);
    return completer.future;
  }

  @override
  void writeCharCode(int charCode) {
    add(<int>[charCode]);
  }

  @override
  void write(Object obj) {
    add(encoding.encode('$obj'));
180
  }
181 182 183 184 185 186 187 188 189

  @override
  void writeln([ Object obj = '' ]) {
    add(encoding.encode('$obj\n'));
  }

  @override
  void writeAll(Iterable<dynamic> objects, [ String separator = '' ]) {
    bool addSeparator = false;
190
    for (final dynamic object in objects) {
191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211
      if (addSeparator) {
        write(separator);
      }
      write(object);
      addSeparator = true;
    }
  }

  @override
  void addError(dynamic error, [ StackTrace stackTrace ]) {
    throw UnimplementedError();
  }

  @override
  Future<void> get done => close();

  @override
  Future<void> close() async { }

  @override
  Future<void> flush() async { }
212
}