Chromium Code Reviews| 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:io'; | |
| 8 | |
| 9 import 'base_client.dart'; | |
| 10 import 'base_request.dart'; | |
| 11 import 'streamed_response.dart'; | |
| 12 import 'utils.dart'; | |
| 13 | |
| 14 /// A drop-in replacement for [Client] that uses the `curl` command-line utility | |
| 15 /// rather than [dart:io] to make requests. This class will only exist | |
| 16 /// temporarily until [dart:io] natively supports requests over HTTPS. | |
|
Bob Nystrom
2012/11/07 23:15:15
Given this, maybe it should be put in a sublibrary
nweiz
2012/11/08 00:33:37
I don't think that adds any utility. It just makes
| |
| 17 class CurlClient extends BaseClient { | |
| 18 /// The path to the `curl` executable to run. By default, this will look up | |
| 19 /// `curl` on the system path. | |
| 20 final String executable; | |
| 21 | |
| 22 CurlClient([String executable]) | |
|
Bob Nystrom
2012/11/07 23:15:15
It's seems a bit redundant, but document the const
nweiz
2012/11/08 00:33:37
Done.
| |
| 23 : executable = executable == null ? "curl" : executable; | |
| 24 | |
| 25 /// Sends a request via `curl` and returns the response. | |
| 26 Future<StreamedResponse> send(BaseRequest request) { | |
| 27 var requestStream = request.finalize(); | |
| 28 return withTempDir((tempDir) { | |
| 29 var headerFile = new Path(tempDir).append("curl-headers").toNativePath(); | |
| 30 var arguments = _argumentsForRequest(request, headerFile); | |
| 31 var process; | |
| 32 return Process.start("curl", arguments).chain((process_) { | |
| 33 process = process_; | |
| 34 if (requestStream.closed) { | |
| 35 process.stdin.close(); | |
| 36 } else { | |
| 37 requestStream.pipe(process.stdin); | |
| 38 } | |
| 39 | |
| 40 return _waitForHeaders(process, request.method == "HEAD"); | |
| 41 }).chain((_) => new File(headerFile).readAsLines()) | |
| 42 .transform((lines) => _buildResponse(process, lines)); | |
| 43 }); | |
| 44 } | |
| 45 | |
| 46 /// Returns the list of arguments to `curl` necessary for performing | |
| 47 /// [request]. [headerFile] is the path to the file where the response headers | |
| 48 /// should be stored. | |
| 49 List<String> _argumentsForRequest(BaseRequest request, String headerFile) { | |
| 50 var arguments = ["--dump-header", headerFile]; | |
| 51 if (request.method == 'HEAD') { | |
| 52 arguments.add("--head"); | |
| 53 } else { | |
| 54 arguments.add("--request"); | |
| 55 arguments.add(request.method); | |
| 56 } | |
| 57 if (request.followRedirects) { | |
| 58 arguments.add("--location"); | |
| 59 arguments.add("--max-redirs"); | |
| 60 arguments.add(request.maxRedirects.toString()); | |
| 61 } | |
| 62 if (request.contentLength != 0) { | |
| 63 arguments.add("--data-binary"); | |
| 64 arguments.add("@-"); | |
| 65 } | |
| 66 | |
| 67 // Override the headers automatically added by curl. We want to make it | |
| 68 // behave as much like the dart:io client as possible. | |
| 69 var headers = { | |
| 70 'accept': '', | |
| 71 'user-agent': '' | |
| 72 }; | |
| 73 mapAddAll(headers, request.headers); | |
| 74 if (request.contentLength < 0) { | |
| 75 headers['content-length'] = ''; | |
| 76 headers['transfer-encoding'] = 'chunked'; | |
| 77 } else if (request.contentLength > 0) { | |
| 78 headers['content-length'] = request.contentLength.toString(); | |
| 79 } | |
| 80 | |
| 81 headers.forEach((name, value) { | |
| 82 arguments.add("--header"); | |
| 83 arguments.add("$name: $value"); | |
| 84 }); | |
| 85 arguments.add(request.url.toString()); | |
| 86 | |
| 87 return arguments; | |
| 88 } | |
| 89 | |
| 90 /// Returns a [Future] that completes once the `curl` [process] has finished | |
| 91 /// receiving the response headers. [noResponseBody] indicates that the server | |
| 92 /// is only expected to send headers, not a body (e.g. for HEAD requests). | |
| 93 Future _waitForHeaders(Process process, bool noResponseBody) { | |
|
Bob Nystrom
2012/11/07 23:15:15
How about "expectBody" instead of "noResponseBody"
nweiz
2012/11/08 00:33:37
Done.
| |
| 94 var exitCompleter = new Completer<int>(); | |
| 95 var exitFuture = exitCompleter.future; | |
| 96 process.onExit = (exitCode) { | |
| 97 if (exitCode == 0) { | |
| 98 exitCompleter.complete(0); | |
| 99 return; | |
| 100 } | |
| 101 | |
| 102 chainToCompleter(consumeInputStream(process.stderr) | |
| 103 .transform((stderrBytes) { | |
| 104 var message = new String.fromCharCodes(stderrBytes); | |
| 105 if (exitCode == 47) { | |
| 106 throw new RedirectLimitExceededException(message); | |
| 107 } else { | |
| 108 throw new HttpException(message); | |
| 109 } | |
| 110 }), exitCompleter); | |
| 111 }; | |
| 112 | |
| 113 // If there's not going to be a response body (e.g. for HEAD requests), curl | |
| 114 // prints the headers to stdout instead of the body. We want to wait until | |
| 115 // all the headers are received to read them from the header file. | |
| 116 if (noResponseBody) { | |
| 117 return Futures.wait([ | |
| 118 consumeInputStream(process.stdout), | |
| 119 exitFuture | |
| 120 ]); | |
| 121 } | |
| 122 | |
| 123 var completer = new Completer(); | |
| 124 var resetCallbacks = () { | |
|
Bob Nystrom
2012/11/07 23:15:15
resetCallbacks() {
...
}
Dart has nested functi
nweiz
2012/11/08 00:33:37
Done.
| |
| 125 process.stdout.onData = null; | |
| 126 process.stdout.onError = null; | |
| 127 process.stdout.onClosed = null; | |
| 128 }; | |
| 129 process.stdout.onData = () { | |
| 130 // TODO(nweiz): If an error happens after the body data starts being | |
| 131 // received, it should be piped through Response.stream once issue | |
| 132 // 3657 is fixed. | |
| 133 exitFuture.handleException((e) => true); | |
| 134 resetCallbacks(); | |
| 135 completer.complete(null); | |
| 136 }; | |
| 137 process.stdout.onError = (e) { | |
| 138 resetCallbacks(); | |
| 139 completer.completeException(e); | |
| 140 }; | |
| 141 process.stdout.onClosed = () { | |
| 142 resetCallbacks(); | |
| 143 chainToCompleter(exitFuture, completer); | |
| 144 }; | |
| 145 return completer.future; | |
| 146 } | |
| 147 | |
| 148 /// Returns a [StreamedResponse] from the response data printed by the `curl` | |
| 149 /// [process]. [lines] are the headers that `curl` wrote to a file. | |
| 150 StreamedResponse _buildResponse(Process process, List<String> lines) { | |
| 151 // When curl follows redirects, it prints the redirect headers as well as | |
| 152 // the headers of the final request. Each block is separated by a blank | |
| 153 // line. We just care about the last block. There is one trailing empty | |
| 154 // line, though, which we don't want to consider a separator. | |
| 155 var lastBlank = lines.lastIndexOf("", lines.length - 2); | |
| 156 if (lastBlank != -1) lines.removeRange(0, lastBlank + 1); | |
| 157 | |
| 158 var statusParts = lines.removeAt(0).split(" "); | |
| 159 var status = int.parse(statusParts[1]); | |
| 160 var isRedirect = status >= 300 && status < 400; | |
| 161 var reasonPhrase = | |
| 162 Strings.join(" ", statusParts.getRange(2, statusParts.length - 2)); | |
| 163 var headers = <String>{}; | |
| 164 for (var line in lines) { | |
| 165 if (line.isEmpty) continue; | |
| 166 var split = split1(line, ":"); | |
| 167 headers[split[0].toLowerCase()] = split[1].trim(); | |
|
Bob Nystrom
2012/11/07 23:15:15
What's the toLowercase here for?
nweiz
2012/11/08 00:33:37
Matching dart:io's convention of lower-casing head
Bob Nystrom
2012/11/08 00:49:13
Works for me. A comment to that effect would help
| |
| 168 } | |
| 169 var responseStream = process.stdout; | |
| 170 if (responseStream.closed) { | |
| 171 responseStream = new ListInputStream(); | |
| 172 responseStream.markEndOfStream(); | |
| 173 } | |
| 174 var contentLength = -1; | |
| 175 if (headers.containsKey('content-length')) { | |
| 176 contentLength = int.parse(headers['content-length']); | |
| 177 } | |
| 178 | |
| 179 return new StreamedResponse(responseStream, status, contentLength, | |
| 180 headers: headers, | |
| 181 isRedirect: isRedirect, | |
| 182 reasonPhrase: reasonPhrase); | |
| 183 } | |
| 184 } | |
| OLD | NEW |