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 |