| OLD | NEW |
| 1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file | 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 | 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. | 3 // BSD-style license that can be found in the LICENSE file. |
| 4 | 4 |
| 5 library curl_client; | 5 library curl_client; |
| 6 | 6 |
| 7 import 'dart:io'; | 7 import 'dart:io'; |
| 8 | 8 |
| 9 import 'base_client.dart'; | 9 import '../../pkg/http/lib/http.dart' as http; |
| 10 import 'base_request.dart'; | 10 import 'io.dart'; |
| 11 import 'streamed_response.dart'; | |
| 12 import 'utils.dart'; | 11 import 'utils.dart'; |
| 13 | 12 |
| 14 /// A drop-in replacement for [Client] that uses the `curl` command-line utility | 13 /// A drop-in replacement for [http.Client] that uses the `curl` command-line |
| 15 /// rather than [dart:io] to make requests. This class will only exist | 14 /// utility rather than [dart:io] to make requests. This class will only exist |
| 16 /// temporarily until [dart:io] natively supports requests over HTTPS. | 15 /// temporarily until [dart:io] natively supports requests over HTTPS. |
| 17 class CurlClient extends BaseClient { | 16 class CurlClient extends http.BaseClient { |
| 18 /// The path to the `curl` executable to run. By default, this will look up | 17 /// The path to the `curl` executable to run. By default, this will look up |
| 19 /// `curl` on the system path. | 18 /// `curl` on the system path. |
| 20 final String executable; | 19 final String executable; |
| 21 | 20 |
| 22 /// Creates a new [CurlClient] with [executable] as the path to the `curl` | 21 /// Creates a new [CurlClient] with [executable] as the path to the `curl` |
| 23 /// executable. By default, this will look up `curl` on the system path. | 22 /// executable. By default, this will look up `curl` on the system path. |
| 24 CurlClient([String executable]) | 23 CurlClient([String executable]) |
| 25 : executable = executable == null ? "curl" : executable; | 24 : executable = executable == null ? "curl" : executable; |
| 26 | 25 |
| 27 /// Sends a request via `curl` and returns the response. | 26 /// Sends a request via `curl` and returns the response. |
| 28 Future<StreamedResponse> send(BaseRequest request) { | 27 Future<http.StreamedResponse> send(http.BaseRequest request) { |
| 29 var requestStream = request.finalize(); | 28 var requestStream = request.finalize(); |
| 30 return withTempDir((tempDir) { | 29 return withTempDir((tempDir) { |
| 31 var headerFile = new Path(tempDir).append("curl-headers").toNativePath(); | 30 var headerFile = new Path(tempDir).append("curl-headers").toNativePath(); |
| 32 var arguments = _argumentsForRequest(request, headerFile); | 31 var arguments = _argumentsForRequest(request, headerFile); |
| 33 var process; | 32 var process; |
| 34 return Process.start("curl", arguments).chain((process_) { | 33 return Process.start("curl", arguments).chain((process_) { |
| 35 process = process_; | 34 process = process_; |
| 36 if (requestStream.closed) { | 35 if (requestStream.closed) { |
| 37 process.stdin.close(); | 36 process.stdin.close(); |
| 38 } else { | 37 } else { |
| 39 requestStream.pipe(process.stdin); | 38 requestStream.pipe(process.stdin); |
| 40 } | 39 } |
| 41 | 40 |
| 42 return _waitForHeaders(process, expectBody: request.method != "HEAD"); | 41 return _waitForHeaders(process, expectBody: request.method != "HEAD"); |
| 43 }).chain((_) => new File(headerFile).readAsLines()) | 42 }).chain((_) => new File(headerFile).readAsLines()) |
| 44 .transform((lines) => _buildResponse(process, lines)); | 43 .transform((lines) => _buildResponse(process, lines)); |
| 45 }); | 44 }); |
| 46 } | 45 } |
| 47 | 46 |
| 48 /// Returns the list of arguments to `curl` necessary for performing | 47 /// Returns the list of arguments to `curl` necessary for performing |
| 49 /// [request]. [headerFile] is the path to the file where the response headers | 48 /// [request]. [headerFile] is the path to the file where the response headers |
| 50 /// should be stored. | 49 /// should be stored. |
| 51 List<String> _argumentsForRequest(BaseRequest request, String headerFile) { | 50 List<String> _argumentsForRequest( |
| 51 http.BaseRequest request, String headerFile) { |
| 52 var arguments = ["--dump-header", headerFile]; | 52 var arguments = ["--dump-header", headerFile]; |
| 53 if (request.method == 'HEAD') { | 53 if (request.method == 'HEAD') { |
| 54 arguments.add("--head"); | 54 arguments.add("--head"); |
| 55 } else { | 55 } else { |
| 56 arguments.add("--request"); | 56 arguments.add("--request"); |
| 57 arguments.add(request.method); | 57 arguments.add(request.method); |
| 58 } | 58 } |
| 59 if (request.followRedirects) { | 59 if (request.followRedirects) { |
| 60 arguments.add("--location"); | 60 arguments.add("--location"); |
| 61 arguments.add("--max-redirs"); | 61 arguments.add("--max-redirs"); |
| 62 arguments.add(request.maxRedirects.toString()); | 62 arguments.add(request.maxRedirects.toString()); |
| 63 } | 63 } |
| 64 if (request.contentLength != 0) { | 64 if (request.contentLength != 0) { |
| 65 arguments.add("--data-binary"); | 65 arguments.add("--data-binary"); |
| 66 arguments.add("@-"); | 66 arguments.add("@-"); |
| 67 } | 67 } |
| 68 | 68 |
| 69 // Override the headers automatically added by curl. We want to make it | 69 // Override the headers automatically added by curl. We want to make it |
| 70 // behave as much like the dart:io client as possible. | 70 // behave as much like the dart:io client as possible. |
| 71 var headers = { | 71 var headers = { |
| 72 'accept': '', | 72 'accept': '', |
| 73 'user-agent': '' | 73 'user-agent': '' |
| 74 }; | 74 }; |
| 75 mapAddAll(headers, request.headers); | 75 request.headers.forEach((name, value) => headers[name] = value); |
| 76 if (request.contentLength < 0) { | 76 if (request.contentLength < 0) { |
| 77 headers['content-length'] = ''; | 77 headers['content-length'] = ''; |
| 78 headers['transfer-encoding'] = 'chunked'; | 78 headers['transfer-encoding'] = 'chunked'; |
| 79 } else if (request.contentLength > 0) { | 79 } else if (request.contentLength > 0) { |
| 80 headers['content-length'] = request.contentLength.toString(); | 80 headers['content-length'] = request.contentLength.toString(); |
| 81 } | 81 } |
| 82 | 82 |
| 83 headers.forEach((name, value) { | 83 headers.forEach((name, value) { |
| 84 arguments.add("--header"); | 84 arguments.add("--header"); |
| 85 arguments.add("$name: $value"); | 85 arguments.add("$name: $value"); |
| (...skipping 13 matching lines...) Expand all Loading... |
| 99 process.onExit = (exitCode) { | 99 process.onExit = (exitCode) { |
| 100 if (exitCode == 0) { | 100 if (exitCode == 0) { |
| 101 exitCompleter.complete(0); | 101 exitCompleter.complete(0); |
| 102 return; | 102 return; |
| 103 } | 103 } |
| 104 | 104 |
| 105 chainToCompleter(consumeInputStream(process.stderr) | 105 chainToCompleter(consumeInputStream(process.stderr) |
| 106 .transform((stderrBytes) { | 106 .transform((stderrBytes) { |
| 107 var message = new String.fromCharCodes(stderrBytes); | 107 var message = new String.fromCharCodes(stderrBytes); |
| 108 if (exitCode == 47) { | 108 if (exitCode == 47) { |
| 109 throw new RedirectLimitExceededException(message); | 109 throw new RedirectLimitExceededException([]); |
| 110 } else { | 110 } else { |
| 111 throw new HttpException(message); | 111 throw new HttpException(message); |
| 112 } | 112 } |
| 113 }), exitCompleter); | 113 }), exitCompleter); |
| 114 }; | 114 }; |
| 115 | 115 |
| 116 // If there's not going to be a response body (e.g. for HEAD requests), curl | 116 // If there's not going to be a response body (e.g. for HEAD requests), curl |
| 117 // prints the headers to stdout instead of the body. We want to wait until | 117 // prints the headers to stdout instead of the body. We want to wait until |
| 118 // all the headers are received to read them from the header file. | 118 // all the headers are received to read them from the header file. |
| 119 if (!expectBody) { | 119 if (!expectBody) { |
| (...skipping 21 matching lines...) Expand all Loading... |
| 141 resetCallbacks(); | 141 resetCallbacks(); |
| 142 completer.completeException(e); | 142 completer.completeException(e); |
| 143 }; | 143 }; |
| 144 process.stdout.onClosed = () { | 144 process.stdout.onClosed = () { |
| 145 resetCallbacks(); | 145 resetCallbacks(); |
| 146 chainToCompleter(exitFuture, completer); | 146 chainToCompleter(exitFuture, completer); |
| 147 }; | 147 }; |
| 148 return completer.future; | 148 return completer.future; |
| 149 } | 149 } |
| 150 | 150 |
| 151 /// Returns a [StreamedResponse] from the response data printed by the `curl` | 151 /// Returns a [http.StreamedResponse] from the response data printed by the |
| 152 /// [process]. [lines] are the headers that `curl` wrote to a file. | 152 /// `curl` [process]. [lines] are the headers that `curl` wrote to a file. |
| 153 StreamedResponse _buildResponse(Process process, List<String> lines) { | 153 http.StreamedResponse _buildResponse(Process process, List<String> lines) { |
| 154 // When curl follows redirects, it prints the redirect headers as well as | 154 // When curl follows redirects, it prints the redirect headers as well as |
| 155 // the headers of the final request. Each block is separated by a blank | 155 // the headers of the final request. Each block is separated by a blank |
| 156 // line. We just care about the last block. There is one trailing empty | 156 // line. We just care about the last block. There is one trailing empty |
| 157 // line, though, which we don't want to consider a separator. | 157 // line, though, which we don't want to consider a separator. |
| 158 var lastBlank = lines.lastIndexOf("", lines.length - 2); | 158 var lastBlank = lines.lastIndexOf("", lines.length - 2); |
| 159 if (lastBlank != -1) lines.removeRange(0, lastBlank + 1); | 159 if (lastBlank != -1) lines.removeRange(0, lastBlank + 1); |
| 160 | 160 |
| 161 var statusParts = lines.removeAt(0).split(" "); | 161 var statusParts = lines.removeAt(0).split(" "); |
| 162 var status = int.parse(statusParts[1]); | 162 var status = int.parse(statusParts[1]); |
| 163 var isRedirect = status >= 300 && status < 400; | 163 var isRedirect = status >= 300 && status < 400; |
| 164 var reasonPhrase = | 164 var reasonPhrase = |
| 165 Strings.join(" ", statusParts.getRange(2, statusParts.length - 2)); | 165 Strings.join(statusParts.getRange(2, statusParts.length - 2), " "); |
| 166 var headers = <String>{}; | 166 var headers = <String>{}; |
| 167 for (var line in lines) { | 167 for (var line in lines) { |
| 168 if (line.isEmpty) continue; | 168 if (line.isEmpty) continue; |
| 169 var split = split1(line, ":"); | 169 var split = split1(line, ":"); |
| 170 headers[split[0].toLowerCase()] = split[1].trim(); | 170 headers[split[0].toLowerCase()] = split[1].trim(); |
| 171 } | 171 } |
| 172 var responseStream = process.stdout; | 172 var responseStream = process.stdout; |
| 173 if (responseStream.closed) { | 173 if (responseStream.closed) { |
| 174 responseStream = new ListInputStream(); | 174 responseStream = new ListInputStream(); |
| 175 responseStream.markEndOfStream(); | 175 responseStream.markEndOfStream(); |
| 176 } | 176 } |
| 177 var contentLength = -1; | 177 var contentLength = -1; |
| 178 if (headers.containsKey('content-length')) { | 178 if (headers.containsKey('content-length')) { |
| 179 contentLength = int.parse(headers['content-length']); | 179 contentLength = int.parse(headers['content-length']); |
| 180 } | 180 } |
| 181 | 181 |
| 182 return new StreamedResponse(responseStream, status, contentLength, | 182 return new http.StreamedResponse(responseStream, status, contentLength, |
| 183 headers: headers, | 183 headers: headers, |
| 184 isRedirect: isRedirect, | 184 isRedirect: isRedirect, |
| 185 reasonPhrase: reasonPhrase); | 185 reasonPhrase: reasonPhrase); |
| 186 } | 186 } |
| 187 } | 187 } |
| OLD | NEW |