Line data Source code
1 : // ignore_for_file: avoid_print
2 :
3 : import 'dart:convert';
4 : import 'dart:io';
5 :
6 : import 'package:curt/src/curt_http_headers.dart';
7 : import 'package:curt/src/curt_response.dart';
8 :
9 : ///
10 : ///
11 : ///
12 : class Curt {
13 : static const String opensslConfigOverridePath = '/tmp/curt-openssl.cnf';
14 :
15 : final Map<String, String> environment = <String, String>{};
16 : final String executable;
17 : final bool debug;
18 : final bool insecure;
19 : final bool silent;
20 : final bool followRedirects;
21 : final bool linuxOpensslTLSOverride;
22 : final int timeout;
23 :
24 : ///
25 : ///
26 : ///
27 1 : Curt({
28 : this.executable = 'curl',
29 : this.debug = false,
30 : this.insecure = false,
31 : this.silent = true,
32 : this.followRedirects = false,
33 : this.linuxOpensslTLSOverride = false,
34 : this.timeout = 10000,
35 : }) {
36 : /// https://askubuntu.com/questions/1250787/when-i-try-to-curl-a-website-i-get-ssl-error
37 : /// This openssl problem can happen on any distro with curl linking
38 : /// dynamically to openssl, even though the link is specific to ubuntu.
39 : /// The workaround here is to create a config override for openssl, and run
40 : /// curl with that config override - this will allow TLS v1.0 and v1.1
41 : /// requests to work, which are blocked by openssl, NOT curl
42 2 : if (Platform.isLinux && linuxOpensslTLSOverride) {
43 0 : final StringBuffer buffer = StringBuffer()
44 0 : ..writeln('openssl_conf = openssl_init')
45 0 : ..writeln('[openssl_init]')
46 0 : ..writeln('ssl_conf = ssl_sect')
47 0 : ..writeln('[ssl_sect]')
48 0 : ..writeln('system_default = system_default_sect')
49 0 : ..writeln('[system_default_sect]')
50 0 : ..writeln('CipherString = DEFAULT@SECLEVEL=1');
51 :
52 0 : File(opensslConfigOverridePath)
53 0 : ..createSync(recursive: true)
54 0 : ..writeAsStringSync(buffer.toString());
55 :
56 0 : environment['OPENSSL_CONF'] = opensslConfigOverridePath;
57 : }
58 : }
59 :
60 : ///
61 : ///
62 : ///
63 1 : Future<CurtResponse> send(
64 : Uri uri, {
65 : required String method,
66 : Map<String, String> headers = const <String, String>{},
67 : List<Cookie> cookies = const <Cookie>[],
68 : String? data,
69 : }) async {
70 1 : final List<String> args = <String>['-v', '-X', method];
71 :
72 : /// Insecure
73 1 : if (insecure) {
74 1 : args.add('-k');
75 : }
76 :
77 : /// Silent
78 1 : if (silent) {
79 1 : args.add('-s');
80 : }
81 :
82 : /// Follow Redirects
83 1 : if (followRedirects) {
84 1 : args.add('-L');
85 : }
86 :
87 : /// Headers
88 2 : for (final MapEntry<String, String> header in headers.entries) {
89 : args
90 1 : ..add('-H')
91 4 : ..add('${header.key}: ${header.value}');
92 : }
93 :
94 : /// Cookies
95 1 : for (final Cookie cookie in cookies) {
96 : args
97 0 : ..add('--cookie')
98 0 : ..add('${cookie.name}=${cookie.value}');
99 : }
100 :
101 : /// Body data
102 : if (data != null) {
103 : args
104 0 : ..add('-d')
105 0 : ..add(data);
106 : }
107 :
108 : /// URL
109 2 : args.add(uri.toString());
110 :
111 1 : if (debug) {
112 0 : print('$executable ${args.join(' ')}');
113 : }
114 :
115 : ///
116 : /// Run
117 : ///
118 1 : final ProcessResult run = await Process.run(
119 1 : executable,
120 : args,
121 1 : environment: environment,
122 1 : ).timeout(
123 1 : Duration(
124 1 : milliseconds: timeout,
125 : ),
126 : );
127 :
128 2 : if (run.exitCode != 0) {
129 0 : if (debug) {
130 0 : print('Exit Code: ${run.exitCode}');
131 0 : print(run.stdout);
132 0 : print(run.stderr);
133 : }
134 0 : throw Exception('Error: ${run.exitCode} - ${run.stderr}');
135 : }
136 :
137 : ///
138 : /// Parse
139 : ///
140 3 : final List<String> verboseLines = run.stderr.toString().split('\n');
141 :
142 1 : final RegExp headerRegExp = RegExp('(?<key>.*?): (?<value>.*)');
143 :
144 1 : final RegExp protocolRegExp = RegExp(r'HTTP(.*?) (?<statusCode>\d*)');
145 :
146 1 : int statusCode = -1;
147 :
148 1 : final CurtHttpHeaders responseHeaders = CurtHttpHeaders();
149 :
150 2 : for (final String verboseLine in verboseLines) {
151 1 : if (debug) {
152 0 : print(verboseLine);
153 : }
154 :
155 1 : if (verboseLine.isEmpty) {
156 : continue;
157 : }
158 :
159 2 : if (verboseLine.substring(0, 1) == '<') {
160 1 : final String line = verboseLine.substring(2);
161 :
162 1 : RegExpMatch? match = headerRegExp.firstMatch(line);
163 : if (match != null) {
164 1 : responseHeaders.add(
165 2 : match.namedGroup('key').toString(),
166 2 : match.namedGroup('value').toString(),
167 : );
168 : continue;
169 : }
170 :
171 1 : match = protocolRegExp.firstMatch(line);
172 : if (match != null) {
173 : statusCode =
174 3 : int.tryParse(match.namedGroup('statusCode').toString()) ?? -1;
175 1 : responseHeaders.clear();
176 : }
177 : }
178 : }
179 :
180 1 : return CurtResponse(
181 2 : run.stdout.toString(),
182 : statusCode,
183 : headers: responseHeaders,
184 : );
185 : }
186 :
187 : ///
188 : ///
189 : ///
190 0 : Future<CurtResponse> sendJson(
191 : Uri uri, {
192 : required String method,
193 : required Map<String, dynamic> body,
194 : Map<String, String> headers = const <String, String>{},
195 : List<Cookie> cookies = const <Cookie>[],
196 : String contentType = 'application/json',
197 : }) {
198 0 : final Map<String, String> newHeaders = Map<String, String>.of(headers);
199 0 : newHeaders['Content-Type'] = contentType;
200 :
201 0 : return send(
202 : uri,
203 : method: method,
204 : headers: newHeaders,
205 : cookies: cookies,
206 0 : data: json.encode(body),
207 : );
208 : }
209 :
210 : ///
211 : ///
212 : ///
213 1 : Future<CurtResponse> get(
214 : Uri uri, {
215 : Map<String, String> headers = const <String, String>{},
216 : List<Cookie> cookies = const <Cookie>[],
217 : }) =>
218 1 : send(uri, method: 'GET', headers: headers, cookies: cookies);
219 :
220 : ///
221 : ///
222 : ///
223 1 : Future<CurtResponse> post(
224 : Uri uri, {
225 : Map<String, String> headers = const <String, String>{},
226 : List<Cookie> cookies = const <Cookie>[],
227 : String? data,
228 : }) =>
229 1 : send(uri, method: 'POST', headers: headers, data: data, cookies: cookies);
230 :
231 : ///
232 : ///
233 : ///
234 0 : Future<CurtResponse> postJson(
235 : Uri uri, {
236 : required Map<String, dynamic> body,
237 : Map<String, String> headers = const <String, String>{},
238 : List<Cookie> cookies = const <Cookie>[],
239 : String contentType = 'application/json',
240 : }) =>
241 0 : sendJson(
242 : uri,
243 : method: 'POST',
244 : headers: headers,
245 : body: body,
246 : cookies: cookies,
247 : contentType: contentType,
248 : );
249 :
250 : ///
251 : ///
252 : ///
253 1 : Future<CurtResponse> put(
254 : Uri uri, {
255 : Map<String, String> headers = const <String, String>{},
256 : List<Cookie> cookies = const <Cookie>[],
257 : String? data,
258 : }) =>
259 1 : send(uri, method: 'PUT', headers: headers, data: data, cookies: cookies);
260 :
261 : ///
262 : ///
263 : ///
264 0 : Future<CurtResponse> putJson(
265 : Uri uri, {
266 : required Map<String, dynamic> body,
267 : Map<String, String> headers = const <String, String>{},
268 : List<Cookie> cookies = const <Cookie>[],
269 : String contentType = 'application/json',
270 : }) =>
271 0 : sendJson(
272 : uri,
273 : method: 'PUT',
274 : headers: headers,
275 : body: body,
276 : cookies: cookies,
277 : contentType: contentType,
278 : );
279 :
280 : ///
281 : ///
282 : ///
283 1 : Future<CurtResponse> delete(
284 : Uri uri, {
285 : Map<String, String> headers = const <String, String>{},
286 : List<Cookie> cookies = const <Cookie>[],
287 : }) =>
288 1 : send(uri, method: 'DELETE', headers: headers, cookies: cookies);
289 : }
|