net.dart 5.68 KB
Newer Older
1 2 3 4 5 6
// Copyright 2015 The Chromium 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 'dart:async';

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

const int kNetworkProblemExitCode = 50;
16

17
typedef HttpClientFactory = HttpClient Function();
18

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

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

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

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

120 121 122
  // If we're making a HEAD request, we're only checking to see if the URL is
  // valid.
  if (onlyHeaders) {
123
    return response.statusCode == HttpStatus.ok;
124
  }
125
  if (response.statusCode != HttpStatus.ok) {
126 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
    printTrace('Download error: ${response.statusCode} ${response.reasonPhrase}');
136
    return false;
137
  }
138
  printTrace('Received response from server, collecting bytes...');
139
  try {
140 141 142
    assert(destSink != null);
    await response.forEach(destSink.add);
    return true;
143 144
  } on IOException catch (error) {
    printTrace('Download error: $error');
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 170 171 172 173 174 175 176 177 178
    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'));
179
  }
180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210

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

  @override
  void writeAll(Iterable<dynamic> objects, [ String separator = '' ]) {
    bool addSeparator = false;
    for (dynamic object in objects) {
      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 { }
211
}