net.dart 6.57 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 8
import 'package:meta/meta.dart';

9
import '../convert.dart';
10
import 'common.dart';
11
import 'file_system.dart';
12
import 'io.dart';
13
import 'logger.dart';
14
import 'platform.dart';
15 16

const int kNetworkProblemExitCode = 50;
17

18
typedef HttpClientFactory = HttpClient Function();
19

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

22 23 24 25 26 27 28 29 30
class Net {
  Net({
    HttpClientFactory httpClientFactory,
    @required Logger logger,
    @required Platform platform,
  }) :
    _httpClientFactory = httpClientFactory,
    _logger = logger,
    _platform = platform;
31

32
  final HttpClientFactory _httpClientFactory;
33

34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82
  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>[];
      }

      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;
      }
83
    }
84
  }
85

86 87 88 89 90 91 92 93 94 95 96 97 98
  /// 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();
99
    } else {
100
      httpClient = HttpClient();
101
    }
102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125
    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());
126
      throwToolExit(
127 128 129
        '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',
130 131
        exitCode: kNetworkProblemExitCode,
      );
132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169
    } 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();
170
    }
171 172 173
  }
}

174 175


176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202
/// 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'));
203
  }
204 205 206 207 208 209 210 211 212

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

  @override
  void writeAll(Iterable<dynamic> objects, [ String separator = '' ]) {
    bool addSeparator = false;
213
    for (final dynamic object in objects) {
214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234
      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 { }
235
}
236 237 238 239 240 241 242 243 244 245

/// Returns [true] if [address] is an IPv6 address.
bool isIPv6Address(String address) {
  try {
    Uri.parseIPv6Address(address);
    return true;
  } on FormatException {
    return false;
  }
}