diff --git a/.gitignore b/.gitignore index 8c07133..727319d 100644 --- a/.gitignore +++ b/.gitignore @@ -6,5 +6,7 @@ # https://dart.dev/guides/libraries/private-files#pubspeclock. pubspec.lock +.idea/ + build/ coverage/ \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 26d3352..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml diff --git a/.idea/libraries/Dart_Packages.xml b/.idea/libraries/Dart_Packages.xml deleted file mode 100644 index 8f6c046..0000000 --- a/.idea/libraries/Dart_Packages.xml +++ /dev/null @@ -1,508 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/libraries/Dart_SDK.xml b/.idea/libraries/Dart_SDK.xml deleted file mode 100644 index f4e39df..0000000 --- a/.idea/libraries/Dart_SDK.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 639900d..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 1cdcf8d..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 94a25f7..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.pubignore b/.pubignore new file mode 100644 index 0000000..69b2ab9 --- /dev/null +++ b/.pubignore @@ -0,0 +1,5 @@ +.github/ +helpers/ +coverage/ +build/ +*.tar.gz diff --git a/CHANGELOG.md b/CHANGELOG.md index a6f387a..a1a1b3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## 0.1.0 (2023-06-21) + +- Adding CurtHttpHeaders class. +- Adding CurtResponse class. +- Updating tests. +- Thanks [GThurler](https://github.com/GThurler) for the improvements. + ## 0.0.3 (2023-06-20) - Cleaning headers when follow redirect is active. diff --git a/README.md b/README.md index b5f9e43..a36af4f 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Simple wrapper for curl in dart. ## Motivation -Allow https connections with TLS lower than 1.2. +Allow https connections with TLS less than 1.2 through curl. ## Funding diff --git a/example/curt_example.dart b/example/curt_example.dart index 81da86d..47cb68b 100644 --- a/example/curt_example.dart +++ b/example/curt_example.dart @@ -1,14 +1,13 @@ // ignore_for_file: avoid_print import 'package:curt/curt.dart'; -import 'package:http/http.dart'; /// /// /// void main() async { final Curt curt = Curt(); - final Response response = await curt.get(Uri.parse('https://google.com')); + final CurtResponse response = await curt.get(Uri.parse('https://google.com')); print('Status Code: ${response.statusCode}'); print('Headers: ${response.headers}'); print('Body:\n${response.body}'); diff --git a/lib/curt.dart b/lib/curt.dart index fb0f091..82048a1 100644 --- a/lib/curt.dart +++ b/lib/curt.dart @@ -2,3 +2,4 @@ library curt; export 'src/curt.dart'; +export 'src/curt_response.dart'; diff --git a/lib/src/curt.dart b/lib/src/curt.dart index a2490e4..f0dc5b8 100644 --- a/lib/src/curt.dart +++ b/lib/src/curt.dart @@ -3,7 +3,8 @@ import 'dart:convert'; import 'dart:io'; -import 'package:http/http.dart'; +import 'package:curt/src/curt_http_headers.dart'; +import 'package:curt/src/curt_response.dart'; /// /// @@ -31,10 +32,11 @@ class Curt { /// /// /// - Future send( + Future send( Uri uri, { required String method, Map headers = const {}, + List cookies = const [], String? data, }) async { final List args = ['-v', '-X', method]; @@ -61,6 +63,13 @@ class Curt { ..add('${header.key}: ${header.value}'); } + /// Cookies + for (final Cookie cookie in cookies) { + args + ..add('--cookie') + ..add('${cookie.name}=${cookie.value}'); + } + /// Body data if (data != null) { args @@ -104,7 +113,7 @@ class Curt { int statusCode = -1; - final Map responseHeaders = {}; + final CurtHttpHeaders responseHeaders = CurtHttpHeaders(); for (final String verboseLine in verboseLines) { if (debug) { @@ -120,8 +129,10 @@ class Curt { RegExpMatch? match = headerRegExp.firstMatch(line); if (match != null) { - responseHeaders[match.namedGroup('key').toString()] = - match.namedGroup('value').toString(); + responseHeaders.add( + match.namedGroup('key').toString(), + match.namedGroup('value').toString(), + ); continue; } @@ -134,7 +145,7 @@ class Curt { } } - return Response( + return CurtResponse( run.stdout.toString(), statusCode, headers: responseHeaders, @@ -144,19 +155,22 @@ class Curt { /// /// /// - Future sendJson( + Future sendJson( Uri uri, { required String method, required Map body, Map headers = const {}, + List cookies = const [], + String contentType = 'application/json', }) { final Map newHeaders = Map.of(headers); - newHeaders['Content-Type'] = 'application/json'; + newHeaders['Content-Type'] = contentType; return send( uri, method: method, headers: newHeaders, + cookies: cookies, data: json.encode(body), ); } @@ -164,58 +178,80 @@ class Curt { /// /// /// - Future get( + Future get( Uri uri, { Map headers = const {}, + List cookies = const [], }) => - send(uri, method: 'GET', headers: headers); + send(uri, method: 'GET', headers: headers, cookies: cookies); /// /// /// - Future post( + Future post( Uri uri, { Map headers = const {}, + List cookies = const [], String? data, }) => - send(uri, method: 'POST', headers: headers, data: data); + send(uri, method: 'POST', headers: headers, data: data, cookies: cookies); /// /// /// - Future postJson( + Future postJson( Uri uri, { required Map body, Map headers = const {}, + List cookies = const [], + String contentType = 'application/json', }) => - sendJson(uri, method: 'POST', headers: headers, body: body); + sendJson( + uri, + method: 'POST', + headers: headers, + body: body, + cookies: cookies, + contentType: contentType, + ); /// /// /// - Future put( + Future put( Uri uri, { Map headers = const {}, + List cookies = const [], String? data, }) => - send(uri, method: 'PUT', headers: headers, data: data); + send(uri, method: 'PUT', headers: headers, data: data, cookies: cookies); /// /// /// - Future putJson( + Future putJson( Uri uri, { required Map body, Map headers = const {}, + List cookies = const [], + String contentType = 'application/json', }) => - sendJson(uri, method: 'PUT', headers: headers, body: body); + sendJson( + uri, + method: 'PUT', + headers: headers, + body: body, + cookies: cookies, + contentType: contentType, + ); /// /// /// - Future delete( + Future delete( Uri uri, { Map headers = const {}, + List cookies = const [], }) => - send(uri, method: 'DELETE', headers: headers); + send(uri, method: 'DELETE', headers: headers, cookies: cookies); } diff --git a/lib/src/curt_http_headers.dart b/lib/src/curt_http_headers.dart new file mode 100644 index 0000000..972d273 --- /dev/null +++ b/lib/src/curt_http_headers.dart @@ -0,0 +1,211 @@ +import 'dart:io'; + +import 'package:intl/intl.dart'; + +/// +/// +/// +class CurtHttpHeaders implements HttpHeaders { + final Map> _headers = >{}; + + // TODO(anyone): properly implement these stubbed properties and methods? + /// All of these go unused for our current use cases, so they're being stubbed + @override + bool chunkedTransferEncoding = true; + + @override + int contentLength = -1; + + @override + ContentType? contentType; + + @override + DateTime? date; + + @override + DateTime? expires; + + @override + String? host; + + @override + DateTime? ifModifiedSince; + + @override + bool persistentConnection = true; + + @override + int? port; + + /// + /// + /// + @override + void noFolding(String name) {} + + /// + /// + /// + void _add(String name, String value) { + final String lowerName = name.toLowerCase(); + + switch (lowerName) { + case HttpHeaders.contentLengthHeader: + contentLength = int.tryParse(value) ?? -1; + break; + case HttpHeaders.dateHeader: + try { + date = DateFormat('EEE, dd MMM yyyy HH:mm:ss zzz').parse(value); + } on Exception catch (_) {} + break; + } + + if (_headers.containsKey(lowerName)) { + if (!_headers[lowerName]!.contains(value)) { + _headers[lowerName]!.add(value); + } + } else { + _headers[lowerName] = [value]; + } + } + + /// + /// + /// + void _remove(String name, String value) { + final String lowerName = name.toLowerCase(); + if (_headers.containsKey(lowerName)) { + _headers[lowerName]!.remove(value); + if (_headers[lowerName]!.isEmpty) { + _headers.remove(lowerName); + } + } + } + + /// + /// + /// + void _addAll(String name, Iterable values) { + for (final String value in values) { + _add(name, value); + } + } + + /// + /// + /// + void _removeAll(String name, Iterable values) { + for (final String value in values) { + _remove(name, value); + } + } + + /// + /// + /// + @override + void set(String name, Object value, {bool preserveHeaderCase = false}) { + final String nameToUse = preserveHeaderCase ? name : name.toLowerCase(); + if (value is Iterable) { + _headers[nameToUse] = value.map((dynamic v) => v.toString()).toList(); + } else { + _headers[nameToUse] = [value.toString()]; + } + } + + /// + /// + /// + @override + void add(String name, Object value, {bool preserveHeaderCase = false}) { + final String nameToUse = preserveHeaderCase ? name : name.toLowerCase(); + if (value is Iterable) { + _addAll(nameToUse, value.map((dynamic v) => v.toString())); + } else { + _add(nameToUse, value.toString()); + } + } + + /// + /// + /// + @override + void clear() => _headers.clear(); + + /// + /// + /// + @override + void remove(String name, Object value) { + if (value is Iterable) { + _removeAll(name, value.map((dynamic v) => v.toString())); + } else { + _remove(name, value.toString()); + } + } + + /// + /// + /// + @override + void removeAll(String name) => _headers.remove(name.toLowerCase()); + + /// + /// + /// + @override + void forEach(void Function(String name, List values) action) => + _headers.forEach(action); + + /// + /// + /// + @override + String? value(String name) { + final String lowerName = name.toLowerCase(); + if (_headers.containsKey(lowerName) && _headers[lowerName]!.isNotEmpty) { + return _headers[lowerName]!.first; + } + return null; + } + + /// + /// + /// + @override + List? operator [](String name) => _headers[name.toLowerCase()]; + + /// + /// + /// + @override + String toString() => _headers.toString(); + + /// + /// + /// + bool get isEmpty => _headers.isEmpty; + + /// + /// + /// + bool get isNotEmpty => _headers.isNotEmpty; + + /// + /// + /// + List get cookies { + if (!_headers.containsKey(HttpHeaders.setCookieHeader)) { + return []; + } + final Map rawInfo = {}; + for (final String rawValue in _headers[HttpHeaders.setCookieHeader]!) { + final Cookie cookie = Cookie.fromSetCookieValue(rawValue); + rawInfo[cookie.name] = cookie.value; // This prevents duplicate cookies + } + return rawInfo.entries + .map((MapEntry entry) => Cookie(entry.key, entry.value)) + .toList(); + } + +} diff --git a/lib/src/curt_response.dart b/lib/src/curt_response.dart new file mode 100644 index 0000000..2f8a566 --- /dev/null +++ b/lib/src/curt_response.dart @@ -0,0 +1,21 @@ +import 'dart:io'; +import 'package:curt/src/curt_http_headers.dart'; + +/// +/// +/// +class CurtResponse { + final String body; + final int statusCode; + final CurtHttpHeaders headers; + final List cookies; + + /// + /// + /// + CurtResponse( + this.body, + this.statusCode, { + required this.headers, + }) : cookies = headers.cookies; +} diff --git a/pubspec.yaml b/pubspec.yaml index d737fb9..a5fe45f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: curt description: Simple wrapper for curl in dart. -version: 0.0.3 +version: 0.1.0 repository: https://github.com/edufolly/curt environment: @@ -9,6 +9,8 @@ environment: dependencies: http: '>=0.13.5 <1.1.0' + intl: '>=0.17.0 <1.0.0' + dev_dependencies: # https://pub.dev/packages/coverage coverage: ^1.6.3 diff --git a/test/curt_test.dart b/test/curt_test.dart index b7b14b5..197ed9c 100644 --- a/test/curt_test.dart +++ b/test/curt_test.dart @@ -1,10 +1,8 @@ -// ignore_for_file: format-comment - import 'dart:io'; import 'dart:math'; import 'package:curt/src/curt.dart'; -import 'package:http/http.dart'; +import 'package:curt/src/curt_response.dart'; import 'package:test/test.dart'; import '../helpers/basic_test_result.dart'; @@ -77,28 +75,28 @@ void main() { for (final MapEntry entry in tests.entries) { test('GET ${entry.key}', () async { - final Response response = await curt.get(Uri.parse(entry.key)); + final CurtResponse response = await curt.get(Uri.parse(entry.key)); expect(response.statusCode, entry.value.statusCode); expect(response.headers, entry.value.headersMatcher); expect(response.body, entry.value.bodyMatcher); }); test('POST ${entry.key}', () async { - final Response response = await curt.post(Uri.parse(entry.key)); + final CurtResponse response = await curt.post(Uri.parse(entry.key)); expect(response.statusCode, entry.value.statusCode); expect(response.headers, entry.value.headersMatcher); expect(response.body, entry.value.bodyMatcher); }); test('PUT ${entry.key}', () async { - final Response response = await curt.put(Uri.parse(entry.key)); + final CurtResponse response = await curt.put(Uri.parse(entry.key)); expect(response.statusCode, entry.value.statusCode); expect(response.headers, entry.value.headersMatcher); expect(response.body, entry.value.bodyMatcher); }); test('DELETE ${entry.key}', () async { - final Response response = await curt.delete(Uri.parse(entry.key)); + final CurtResponse response = await curt.delete(Uri.parse(entry.key)); expect(response.statusCode, entry.value.statusCode); expect(response.headers, entry.value.headersMatcher); expect(response.body, entry.value.bodyMatcher); @@ -108,17 +106,13 @@ void main() { for (int gen = 0; gen < 3; gen++) { final int bytes = Random().nextInt(1024); test('Body Length $bytes', () async { - final Response response = await curt.get( + final CurtResponse response = await curt.get( Uri.parse('$scheme://$server:$httpPort/range/$bytes'), ); expect(response.statusCode, 200); expect(response.headers, isNotEmpty); - expect(response.headers.containsKey('Content-Length'), isTrue); - expect( - int.tryParse(response.headers['Content-Length'].toString()) ?? -1, - bytes, - ); + expect(response.headers.contentLength, bytes); expect(response.body, isNotEmpty); expect(response.body.length, bytes); }); @@ -137,11 +131,11 @@ void main() { }, ); - final Response response = await curt.get(uri); + final CurtResponse response = await curt.get(uri); expect(response.statusCode, 200); expect(response.headers, isNotEmpty); - expect(response.headers.containsKey('location'), isFalse); + expect(response.headers.value(HttpHeaders.locationHeader), isNull); expect(response.body, isEmpty); }); @@ -205,7 +199,7 @@ void main() { /// test('Simple HTTP GET', () async { - final Response response = + final CurtResponse response = await curt.get(Uri.parse('http://$server:$httpPort/')); expect(response.statusCode, 200); expect(response.headers, isNotEmpty); @@ -214,7 +208,7 @@ void main() { /// test('Simple HTTP POST', () async { - final Response response = + final CurtResponse response = await curt.post(Uri.parse('http://$server:$httpPort/')); expect(response.statusCode, 200); expect(response.headers, isNotEmpty); @@ -223,7 +217,7 @@ void main() { /// test('Simple HTTP PUT', () async { - final Response response = + final CurtResponse response = await curt.put(Uri.parse('http://$server:$httpPort/')); expect(response.statusCode, 200); expect(response.headers, isNotEmpty); @@ -232,7 +226,7 @@ void main() { /// test('Simple HTTP DELETE', () async { - final Response response = + final CurtResponse response = await curt.delete(Uri.parse('http://$server:$httpPort/')); expect(response.statusCode, 200); expect(response.headers, isNotEmpty); @@ -241,7 +235,7 @@ void main() { /// test('Simple HTTPS GET', () async { - final Response response = + final CurtResponse response = await curt.get(Uri.parse('https://$server:$httpsPort/')); expect(response.statusCode, 200); expect(response.headers, isNotEmpty); @@ -250,7 +244,7 @@ void main() { /// test('Simple HTTPS POST', () async { - final Response response = + final CurtResponse response = await curt.post(Uri.parse('https://$server:$httpsPort/')); expect(response.statusCode, 200); expect(response.headers, isNotEmpty); @@ -259,7 +253,7 @@ void main() { /// test('Simple HTTPS PUT', () async { - final Response response = + final CurtResponse response = await curt.put(Uri.parse('https://$server:$httpsPort/')); expect(response.statusCode, 200); expect(response.headers, isNotEmpty); @@ -268,7 +262,7 @@ void main() { /// test('Simple HTTPS DELETE', () async { - final Response response = + final CurtResponse response = await curt.delete(Uri.parse('https://$server:$httpsPort/')); expect(response.statusCode, 200); expect(response.headers, isNotEmpty);