| OLD | NEW |
| (Empty) |
| 1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file | |
| 2 // for details. All rights reserved. Use of this source code is governed by a | |
| 3 // BSD-style license that can be found in the LICENSE file. | |
| 4 | |
| 5 library curl_client; | |
| 6 | |
| 7 import 'dart:async'; | |
| 8 import 'dart:io'; | |
| 9 | |
| 10 import '../../pkg/http/lib/http.dart' as http; | |
| 11 import 'io.dart'; | |
| 12 import 'log.dart' as log; | |
| 13 import 'utils.dart'; | |
| 14 | |
| 15 /// A drop-in replacement for [http.Client] that uses the `curl` command-line | |
| 16 /// utility rather than [dart:io] to make requests. This class will only exist | |
| 17 /// temporarily until [dart:io] natively supports requests over HTTPS. | |
| 18 class CurlClient extends http.BaseClient { | |
| 19 /// The path to the `curl` executable to run. | |
| 20 /// | |
| 21 /// By default on Unix-like operating systems, this will look up `curl` on the | |
| 22 /// system path. On Windows, it will use the bundled `curl.exe`. | |
| 23 final String executable; | |
| 24 | |
| 25 /// Creates a new [CurlClient] with [executable] as the path to the `curl` | |
| 26 /// executable. | |
| 27 /// | |
| 28 /// By default on Unix-like operating systems, this will look up `curl` on the | |
| 29 /// system path. On Windows, it will use the bundled `curl.exe`. | |
| 30 CurlClient([String executable]) | |
| 31 : executable = executable == null ? _defaultExecutable : executable; | |
| 32 | |
| 33 /// Sends a request via `curl` and returns the response. | |
| 34 Future<http.StreamedResponse> send(http.BaseRequest request) { | |
| 35 log.fine("Sending Curl request $request"); | |
| 36 | |
| 37 var requestStream = request.finalize(); | |
| 38 return withTempDir((tempDir) { | |
| 39 var headerFile = join(tempDir, "curl-headers"); | |
| 40 var arguments = _argumentsForRequest(request, headerFile); | |
| 41 var process; | |
| 42 return startProcess(executable, arguments).then((process_) { | |
| 43 process = process_; | |
| 44 return requestStream.pipe(wrapOutputStream(process.stdin)); | |
| 45 }).then((_) { | |
| 46 return _waitForHeaders(process, expectBody: request.method != "HEAD"); | |
| 47 }).then((_) => new File(headerFile).readAsLines()) | |
| 48 .then((lines) => _buildResponse(request, process, lines)); | |
| 49 }); | |
| 50 } | |
| 51 | |
| 52 /// Returns the list of arguments to `curl` necessary for performing | |
| 53 /// [request]. [headerFile] is the path to the file where the response headers | |
| 54 /// should be stored. | |
| 55 List<String> _argumentsForRequest( | |
| 56 http.BaseRequest request, String headerFile) { | |
| 57 // Note: This line of code gets munged by create_sdk.py to be the correct | |
| 58 // relative path to the certificate file in the SDK. | |
| 59 var pathToCertificates = "../../third_party/curl/ca-certificates.crt"; | |
| 60 | |
| 61 var arguments = [ | |
| 62 "--dump-header", headerFile, | |
| 63 "--cacert", relativeToPub(pathToCertificates) | |
| 64 ]; | |
| 65 if (request.method == 'HEAD') { | |
| 66 arguments.add("--head"); | |
| 67 } else { | |
| 68 arguments.add("--request"); | |
| 69 arguments.add(request.method); | |
| 70 } | |
| 71 if (request.followRedirects) { | |
| 72 arguments.add("--location"); | |
| 73 arguments.add("--max-redirs"); | |
| 74 arguments.add(request.maxRedirects.toString()); | |
| 75 } | |
| 76 if (request.contentLength != 0) { | |
| 77 arguments.add("--data-binary"); | |
| 78 arguments.add("@-"); | |
| 79 } | |
| 80 | |
| 81 // Override the headers automatically added by curl. We want to make it | |
| 82 // behave as much like the dart:io client as possible. | |
| 83 var headers = { | |
| 84 'accept': '', | |
| 85 'user-agent': '' | |
| 86 }; | |
| 87 request.headers.forEach((name, value) => headers[name] = value); | |
| 88 if (request.contentLength < 0) { | |
| 89 headers['content-length'] = ''; | |
| 90 headers['transfer-encoding'] = 'chunked'; | |
| 91 } else if (request.contentLength > 0) { | |
| 92 headers['content-length'] = request.contentLength.toString(); | |
| 93 } | |
| 94 | |
| 95 headers.forEach((name, value) { | |
| 96 arguments.add("--header"); | |
| 97 arguments.add("$name: $value"); | |
| 98 }); | |
| 99 arguments.add(request.url.toString()); | |
| 100 | |
| 101 return arguments; | |
| 102 } | |
| 103 | |
| 104 /// Returns a [Future] that completes once the `curl` [process] has finished | |
| 105 /// receiving the response headers. [expectBody] indicates that the server is | |
| 106 /// expected to send a response body (which is not the case for HEAD | |
| 107 /// requests). | |
| 108 /// | |
| 109 /// Curl prints the headers to a file and then prints the body to stdout. So, | |
| 110 /// in theory, we could read the headers as soon as we see anything appear | |
| 111 /// in stdout. However, that seems to be too early to successfully read the | |
| 112 /// file (at least on Mac). Instead, this just waits until the entire process | |
| 113 /// has completed. | |
| 114 Future _waitForHeaders(Process process, {bool expectBody}) { | |
| 115 var completer = new Completer(); | |
| 116 process.onExit = (exitCode) { | |
| 117 log.io("Curl process exited with code $exitCode."); | |
| 118 | |
| 119 if (exitCode == 0) { | |
| 120 completer.complete(null); | |
| 121 return; | |
| 122 } | |
| 123 | |
| 124 chainToCompleter(consumeInputStream(process.stderr).then((stderrBytes) { | |
| 125 var message = new String.fromCharCodes(stderrBytes); | |
| 126 log.fine('Got error reading headers from curl: $message'); | |
| 127 if (exitCode == 47) { | |
| 128 throw new RedirectLimitExceededException([]); | |
| 129 } else { | |
| 130 throw new HttpException(message); | |
| 131 } | |
| 132 }), completer); | |
| 133 }; | |
| 134 | |
| 135 // If there's not going to be a response body (e.g. for HEAD requests), curl | |
| 136 // prints the headers to stdout instead of the body. We want to wait until | |
| 137 // all the headers are received to read them from the header file. | |
| 138 if (!expectBody) { | |
| 139 return Future.wait([ | |
| 140 consumeInputStream(process.stdout), | |
| 141 completer.future | |
| 142 ]); | |
| 143 } | |
| 144 | |
| 145 return completer.future; | |
| 146 } | |
| 147 | |
| 148 /// Returns a [http.StreamedResponse] from the response data printed by the | |
| 149 /// `curl` [process]. [lines] are the headers that `curl` wrote to a file. | |
| 150 http.StreamedResponse _buildResponse( | |
| 151 http.BaseRequest request, Process process, List<String> lines) { | |
| 152 // When curl follows redirects, it prints the redirect headers as well as | |
| 153 // the headers of the final request. Each block is separated by a blank | |
| 154 // line. We just care about the last block. There is one trailing empty | |
| 155 // line, though, which we don't want to consider a separator. | |
| 156 var lastBlank = lines.lastIndexOf("", lines.length - 2); | |
| 157 if (lastBlank != -1) lines.removeRange(0, lastBlank + 1); | |
| 158 | |
| 159 var statusParts = lines.removeAt(0).split(" "); | |
| 160 var status = int.parse(statusParts[1]); | |
| 161 var isRedirect = status >= 300 && status < 400; | |
| 162 var reasonPhrase = | |
| 163 Strings.join(statusParts.getRange(2, statusParts.length - 2), " "); | |
| 164 var headers = {}; | |
| 165 for (var line in lines) { | |
| 166 if (line.isEmpty) continue; | |
| 167 var split = split1(line, ":"); | |
| 168 headers[split[0].toLowerCase()] = split[1].trim(); | |
| 169 } | |
| 170 var responseStream = process.stdout; | |
| 171 if (responseStream.closed) { | |
| 172 responseStream = new ListInputStream(); | |
| 173 responseStream.markEndOfStream(); | |
| 174 } | |
| 175 var contentLength = -1; | |
| 176 if (headers.containsKey('content-length')) { | |
| 177 contentLength = int.parse(headers['content-length']); | |
| 178 } | |
| 179 | |
| 180 return new http.StreamedResponse( | |
| 181 wrapInputStream(responseStream), status, contentLength, | |
| 182 request: request, | |
| 183 headers: headers, | |
| 184 isRedirect: isRedirect, | |
| 185 reasonPhrase: reasonPhrase); | |
| 186 } | |
| 187 | |
| 188 /// The default executable to use for running curl. On Windows, this is the | |
| 189 /// path to the bundled `curl.exe`; elsewhere, this is just "curl", and we | |
| 190 /// assume it to be installed and on the user's PATH. | |
| 191 static String get _defaultExecutable { | |
| 192 if (Platform.operatingSystem != 'windows') return 'curl'; | |
| 193 // Note: This line of code gets munged by create_sdk.py to be the correct | |
| 194 // relative path to curl in the SDK. | |
| 195 var pathToCurl = "../../third_party/curl/curl.exe"; | |
| 196 return relativeToPub(pathToCurl); | |
| 197 } | |
| 198 } | |
| OLD | NEW |