From 010467bc01a668baf55da25bb17b5d25feab69f5 Mon Sep 17 00:00:00 2001 From: gmpassos Date: Mon, 16 Jun 2025 05:14:36 -0300 Subject: [PATCH 1/4] v1.9.3-beta.8 - `APIServerConfig`, `APIServerWorker`, `APIServer`: - Add `maxPayloadLength` and `decompressPayload` options for request handling. - `APIServer`: - `_loadPayloadBytes`: - Added support for compressed payload in gzip and deflate. - Added `_decodePayloadGzip` to handled GZip decompression and check the decompressed size in header before decompression. --- CHANGELOG.md | 10 ++ lib/src/bones_api_base.dart | 2 +- lib/src/bones_api_server.dart | 173 +++++++++++++++++++++++++++++++--- pubspec.yaml | 4 +- test/bones_api_test.dart | 95 ++++++++++++++++++- 5 files changed, 265 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9295a6ae..8b402900 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +## 1.9.3-beta.8 + +- `APIServerConfig`, `APIServerWorker`, `APIServer`: + - Add `maxPayloadLength` and `decompressPayload` options for request handling. + +- `APIServer`: + - `_loadPayloadBytes`: + - Added support for compressed payload in gzip and deflate. + - Added `_decodePayloadGzip` to handled GZip decompression and check the decompressed size in header before decompression. + ## 1.9.3-beta.7 - `GenericEntityHandler`, `ClassReflectionEntityHandler`: diff --git a/lib/src/bones_api_base.dart b/lib/src/bones_api_base.dart index 0da37629..e182e582 100644 --- a/lib/src/bones_api_base.dart +++ b/lib/src/bones_api_base.dart @@ -42,7 +42,7 @@ typedef APILogger = void Function(APIRoot apiRoot, String type, String? message, /// Bones API Library class. class BonesAPI { // ignore: constant_identifier_names - static const String VERSION = '1.9.3-beta.7'; + static const String VERSION = '1.9.3-beta.8'; static bool _boot = false; diff --git a/lib/src/bones_api_server.dart b/lib/src/bones_api_server.dart index 9e42798c..8868b786 100644 --- a/lib/src/bones_api_server.dart +++ b/lib/src/bones_api_server.dart @@ -124,6 +124,23 @@ class APIServerConfig { /// The maximum `Content-Length` (in bytes) allowed for a cached [Response]. Default: 10M final int staticFilesCacheMaxContentLength; + /// The maximum allowed request payload size in bytes. + /// + /// If set, any request with a payload larger than this value will be + /// rejected with a 500 Bad Request response. + /// + /// Default: null (no size limit). + final int? maxPayloadLength; + + /// Whether to decompress the request payload if it is compressed using + /// a supported `Content-Encoding` (e.g., gzip or deflate). + /// + /// If `true`, the payload will be automatically decompressed before processing. + /// If `false`, compressed payloads will be left as-is. + /// + /// Default: false. + final bool decompressPayload; + /// If `true` log messages to [stdout] (console). late final bool logToConsole; @@ -164,6 +181,8 @@ class APIServerConfig { bool? cacheStaticFilesResponses, int? staticFilesCacheMaxMemorySize, int? staticFilesCacheMaxContentLength, + this.maxPayloadLength, + this.decompressPayload = false, this.apiConfig, bool? logToConsole, bool? logQueue, @@ -260,6 +279,9 @@ class APIServerConfig { var staticFilesCacheMaxContentLength = a.optionAsInt('static-files-cache-max-content-length'); + var maxPayloadLength = a.optionAsInt('max-payload-length'); + var decompressPayload = a.flag('decompress-payload'); + var logToConsole = a.flagOr('log-toConsole', null); var logQueue = a.flagOr('log-queue', null); @@ -308,6 +330,8 @@ class APIServerConfig { cacheStaticFilesResponses: cacheStaticFilesResponses, staticFilesCacheMaxMemorySize: staticFilesCacheMaxMemorySize, staticFilesCacheMaxContentLength: staticFilesCacheMaxContentLength, + maxPayloadLength: maxPayloadLength, + decompressPayload: decompressPayload, serverResponseDelay: serverResponseDelay, apiConfig: apiConfig, logToConsole: logToConsole, @@ -697,6 +721,8 @@ class APIServerConfig { 'cacheStaticFilesResponses': cacheStaticFilesResponses, 'staticFilesCacheMaxMemorySize': staticFilesCacheMaxMemorySize, 'staticFilesCacheMaxContentLength': staticFilesCacheMaxContentLength, + 'maxPayloadLength': maxPayloadLength, + 'decompressPayload': decompressPayload, 'logToConsole': logToConsole, 'logQueue': logQueue, if (args.isNotEmpty) 'args': args.toList(), @@ -720,6 +746,8 @@ class APIServerConfig { apiCacheControl: json['apiCacheControl'], staticFilesCacheControl: json['staticFilesCacheControl'], cacheStaticFilesResponses: json['cacheStaticFilesResponses'], + maxPayloadLength: json['maxPayloadLength'], + decompressPayload: json['decompressPayload'], logToConsole: json['logToConsole'], logQueue: json['logQueue'], args: json['args'], @@ -767,6 +795,8 @@ abstract class _APIServerBase extends APIServerConfig { super.cacheStaticFilesResponses, super.staticFilesCacheMaxMemorySize, super.staticFilesCacheMaxContentLength, + super.maxPayloadLength, + super.decompressPayload, super.serverResponseDelay, super.logToConsole, super.logQueue, @@ -796,6 +826,8 @@ abstract class _APIServerBase extends APIServerConfig { apiServerConfig.staticFilesCacheMaxMemorySize, staticFilesCacheMaxContentLength: apiServerConfig.staticFilesCacheMaxContentLength, + maxPayloadLength: apiServerConfig.maxPayloadLength, + decompressPayload: apiServerConfig.decompressPayload, serverResponseDelay: apiServerConfig.serverResponseDelay, logToConsole: apiServerConfig.logToConsole, logQueue: apiServerConfig.logQueue, @@ -912,6 +944,8 @@ class APIServer extends _APIServerBase { super.apiCacheControl, super.staticFilesCacheControl, super.cacheStaticFilesResponses, + super.maxPayloadLength, + super.decompressPayload, super.serverResponseDelay, super.logToConsole, super.logQueue, @@ -1003,6 +1037,8 @@ class APIServer extends _APIServerBase { cacheStaticFilesResponses: cacheStaticFilesResponses, staticFilesCacheMaxMemorySize: staticFilesCacheMaxMemorySize, staticFilesCacheMaxContentLength: staticFilesCacheMaxContentLength, + maxPayloadLength: maxPayloadLength, + decompressPayload: decompressPayload, letsEncryptDirectory: letsEncryptDirectory, securePort: securePort, useSessionID: useSessionID, @@ -1032,7 +1068,10 @@ class APIServer extends _APIServerBase { /// Converts a [request] to an [APIRequest]. static FutureOr toAPIRequest(Request request, - {required bool cookieless, required bool useSessionID}) { + {required bool cookieless, + required bool useSessionID, + int? maxPayloadLength, + bool? decompressPayload}) { var requestTime = DateTime.now(); var method = parseAPIRequestMethod(request.method) ?? APIRequestMethod.GET; @@ -1094,7 +1133,10 @@ class APIServer extends _APIServerBase { var parsingDuration = DateTime.now().difference(requestTime); - return _resolvePayload(request).resolveMapped((payloadResolved) { + return _resolvePayload(request, + maxPayloadLength: maxPayloadLength, + decompressPayload: decompressPayload) + .resolveMapped((payloadResolved) { var mimeType = payloadResolved?.$1; var payload = payloadResolved?.$2; @@ -1158,9 +1200,18 @@ class APIServer extends _APIServerBase { static final MimeType _mimeTypeTextPlain = MimeType.parse(MimeType.textPlain)!; - static Future<(MimeType, Object)?> _resolvePayload(Request request) { + static FutureOr<(MimeType, Object)?> _resolvePayload(Request request, + {int? maxPayloadLength, bool? decompressPayload}) { var contentLength = request.contentLength; + if (maxPayloadLength != null && + maxPayloadLength >= 0 && + contentLength != null && + contentLength > maxPayloadLength) { + throw StateError( + "Can't read payload. `Content-Length` ($contentLength) exceeds `maxPayloadLength` ($maxPayloadLength)."); + } + var contentMimeType = _resolveContentMimeType(request); if (contentLength == null && contentMimeType == null) { return Future.value(null); @@ -1169,9 +1220,21 @@ class APIServer extends _APIServerBase { var mimeType = contentMimeType ?? _mimeTypeTextPlain; if (mimeType.isStringType) { - return _resolvePayloadFromString(mimeType, request); + if (contentLength == 0) { + return (mimeType, ''); + } + + return _resolvePayloadFromString(mimeType, request, + maxPayloadLength: maxPayloadLength, + decompressPayload: decompressPayload); } else { - return _resolvePayloadBytes(mimeType, request); + if (contentLength == 0) { + return (mimeType, Uint8List(0)); + } + + return _resolvePayloadBytes(mimeType, request, + maxPayloadLength: maxPayloadLength, + decompressPayload: decompressPayload); } } @@ -1204,8 +1267,12 @@ class APIServer extends _APIServerBase { } static Future<(MimeType, Object)?> _resolvePayloadFromString( - MimeType mimeType, Request request) => - _loadPayloadString(mimeType, request).then((s) { + MimeType mimeType, Request request, + {int? maxPayloadLength, bool? decompressPayload}) => + _loadPayloadString(mimeType, request, + maxPayloadLength: maxPayloadLength, + decompressPayload: decompressPayload) + .then((s) { if (s == null) return null; Object payload = s; @@ -1218,10 +1285,12 @@ class APIServer extends _APIServerBase { return (mimeType, payload); }); - static Future _loadPayloadString( - MimeType mimeType, Request request) => + static Future _loadPayloadString(MimeType mimeType, Request request, + {int? maxPayloadLength, bool? decompressPayload}) => request.read().toList().then((bs) { - var allBytes = _loadPayloadBytes(bs); + var allBytes = _loadPayloadBytes(request, bs, + maxPayloadLength: maxPayloadLength, + decompressPayload: decompressPayload); var encoding = mimeType.charsetEncoding ?? utf8; @@ -1233,13 +1302,18 @@ class APIServer extends _APIServerBase { }); static Future<(MimeType, Uint8List)?> _resolvePayloadBytes( - MimeType mimeType, Request request) => + MimeType mimeType, Request request, + {int? maxPayloadLength, bool? decompressPayload}) => request.read().toList().then((bs) { - var allBytes = _loadPayloadBytes(bs); + var allBytes = _loadPayloadBytes(request, bs, + maxPayloadLength: maxPayloadLength, + decompressPayload: decompressPayload); return (mimeType, allBytes); }); - static Uint8List _loadPayloadBytes(List> payloadBlocks) { + static Uint8List _loadPayloadBytes( + Request request, List> payloadBlocks, + {int? maxPayloadLength, bool? decompressPayload}) { Uint8List bytes; if (payloadBlocks.length == 1) { @@ -1251,9 +1325,23 @@ class APIServer extends _APIServerBase { } assert(bytes.length == bs0.length); + + if (maxPayloadLength != null && + maxPayloadLength >= 0 && + bytes.length > maxPayloadLength) { + throw StateError( + "Payload size (${bytes.length}) exceeds `maxPayloadLength` ($maxPayloadLength)."); + } } else { var allBytesSz = payloadBlocks.map((e) => e.length).sum; + if (maxPayloadLength != null && + maxPayloadLength >= 0 && + allBytesSz > maxPayloadLength) { + throw StateError( + "Payload size ($allBytesSz) exceeds `maxPayloadLength` ($maxPayloadLength)."); + } + bytes = Uint8List(allBytesSz); var bytesOffset = 0; @@ -1268,9 +1356,57 @@ class APIServer extends _APIServerBase { assert(bytesOffset == allBytesSz); } + var contentEncoding = request.headers[HttpHeaders.contentEncodingHeader]; + + if (contentEncoding != null && + (decompressPayload ?? false) && + bytes.isNotEmpty) { + switch (contentEncoding) { + case '': + case 'identity': + { + break; + } + case 'gzip': + { + bytes = + _decodePayloadGzip(bytes, maxPayloadLength: maxPayloadLength); + } + case 'deflate': + { + bytes = _decodePayloadZlib(bytes); + } + default: + throw UnsupportedError( + "Unknown `Content-Encoding`: $contentEncoding"); + } + } + return bytes; } + static Uint8List _decodePayloadGzip(Uint8List bytes, + {int? maxPayloadLength}) { + if (maxPayloadLength != null && maxPayloadLength >= 0) { + final byteData = ByteData.sublistView(bytes); + var gzipUncompressedSize = + byteData.getUint32(bytes.length - 4, Endian.little); + + if (gzipUncompressedSize < 0 || gzipUncompressedSize > maxPayloadLength) { + throw StateError( + "Can't decompress payload of size ${bytes.length}: GZip payload uncompressed size ($gzipUncompressedSize) exceeds `maxPayloadLength` ($maxPayloadLength)."); + } + } + + var decoded = gzip.decode(bytes); + return decoded is Uint8List ? decoded : Uint8List.fromList(decoded); + } + + static Uint8List _decodePayloadZlib(Uint8List bytes) { + var decoded = zlib.decode(bytes); + return decoded is Uint8List ? decoded : Uint8List.fromList(decoded); + } + static final RegExp _regExpSpace = RegExp(r'\s+'); static APICredential? _resolveCredential(Request request) { @@ -1700,6 +1836,8 @@ final class APIServerWorker extends _APIServerBase { super.cacheStaticFilesResponses, super.staticFilesCacheMaxMemorySize, super.staticFilesCacheMaxContentLength, + super.maxPayloadLength, + super.decompressPayload, super.serverResponseDelay, super.logToConsole, super.logQueue, @@ -2144,10 +2282,15 @@ final class APIServerWorker extends _APIServerBase { APIRequest? apiRequest; try { return APIServer.toAPIRequest(request, - cookieless: cookieless, useSessionID: useSessionID) - .resolveMapped((apiReq) { + cookieless: cookieless, + useSessionID: useSessionID, + maxPayloadLength: maxPayloadLength, + decompressPayload: decompressPayload) + .then((apiReq) { apiRequest = apiReq; return _processAPIRequest(request, apiReq); + }, onError: (e, s) { + return _errorProcessing(request, apiRequest, e, s); }); } catch (e, s) { return _errorProcessing(request, apiRequest, e, s); diff --git a/pubspec.yaml b/pubspec.yaml index ea6e80ea..dba013ad 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: bones_api description: Bones_API - A powerful API backend framework for Dart. It comes with a built-in HTTP Server, route handler, entity handler, SQL translator, and DB adapters. -version: 1.9.3-beta.7 +version: 1.9.3-beta.8 homepage: https://github.com/Colossus-Services/bones_api environment: @@ -54,7 +54,7 @@ dependencies: dev_dependencies: lints: ^5.1.1 build_runner: ^2.4.15 - build_verify: ^3.1.0 + build_verify: ^3.1.1 test: ^1.26.2 pubspec: ^2.3.0 dependency_validator: ^4.1.3 diff --git a/test/bones_api_test.dart b/test/bones_api_test.dart index 522909a5..ee053949 100644 --- a/test/bones_api_test.dart +++ b/test/bones_api_test.dart @@ -3,6 +3,7 @@ import 'dart:convert' as convert; import 'dart:io'; import 'dart:typed_data'; +import 'package:archive/archive_io.dart'; import 'package:bones_api/bones_api_console.dart'; import 'package:bones_api/bones_api_server.dart'; import 'package:mercury_client/mercury_client.dart' as mercury_client; @@ -277,7 +278,8 @@ void main() { group('APIServer', () { final api = MyAPI(); - final apiServer = APIServer(api, 'localhost', 5544); + final apiServer = APIServer(api, 'localhost', 5544, + decompressPayload: true, maxPayloadLength: 100); setUp(() async { await apiServer.start(); @@ -445,6 +447,84 @@ void main() { '(200, Payload> mimeType: text/plain; charset=latin1 ; length: 10<>)')); }); + test('payload(Hello World!) /base', () async { + var res = await _getURL('${apiServer.url}base/payload', + method: APIRequestMethod.POST, + payload: convert.utf8.encode('Hello World!'), + payloadType: 'text/plain'); + expect(res.toString(), equals('(200, PAYLOAD: 12 <>)')); + }); + + test('payload(empty) /base', () async { + var res = await _getURL('${apiServer.url}base/payload', + method: APIRequestMethod.POST, + payload: Uint8List(0), + payloadType: 'application/octet-stream'); + expect(res.toString(), equals('(200, PAYLOAD: empty)')); + }); + + test('payload+gzip(Hello World With GZip!) /base', () async { + var payload = convert.utf8.encode('Hello World With GZip!'); + var payloadGZip = GZipEncoder().encode(payload); + + var res = await _getURL('${apiServer.url}base/payload', + method: APIRequestMethod.POST, + headers: {'Content-Encoding': 'gzip'}, + payload: payloadGZip, + payloadType: 'text/plain'); + expect(res.toString(), + equals('(200, PAYLOAD: 22 <>)')); + }); + + test('payload(large content) /base', () async { + const largePayload = + 'This is a large content, over maxPayloadLength: 100> ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz'; + var res = await _getURL('${apiServer.url}base/payload', + method: APIRequestMethod.POST, + payload: convert.utf8.encode(largePayload), + payloadType: 'text/plain'); + expect( + res.toString(), + allOf(startsWith('(500, ERROR processing request:'), + contains('Payload size (115) exceeds `maxPayloadLength` (100)'))); + }); + + test('payload+gzip /base', () async { + const content = + 'This is a normal content: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'; + var payload = convert.utf8.encode(content); + var payloadGZip = GZipEncoder().encode(payload); + + var res = await _getURL('${apiServer.url}base/payload', + method: APIRequestMethod.POST, + headers: {'Content-Encoding': 'gzip'}, + payload: payloadGZip, + payloadType: 'text/plain'); + expect( + res.toString(), + equals( + '(200, PAYLOAD: 90 <>)')); + }); + + test('payload+gzip(large content) /base', () async { + const largePayload = + 'This is a large content: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'; + var payload = convert.utf8.encode(largePayload); + var payloadGZip = GZipEncoder().encode(payload); + + var res = await _getURL('${apiServer.url}base/payload', + method: APIRequestMethod.POST, + headers: {'Content-Encoding': 'gzip'}, + payload: payloadGZip, + payloadType: 'text/plain'); + expect( + res.toString(), + allOf( + startsWith('(500, ERROR processing request:'), + contains( + "Can't decompress payload of size 48: GZip payload uncompressed size (117) exceeds `maxPayloadLength` (100)."))); + }); + test('get /info', () async { var res = await _getURL('${apiServer.url}info/echo', method: APIRequestMethod.GET, parameters: {'msg': 'Hello!'}); @@ -514,6 +594,7 @@ void main() { '{"name":"foo","method":"GET","uri":"http://localhost:0/base/foo"},' '{"name":"foo","method":"POST","uri":"http://localhost:0/base/foo"},' '{"name":"upload","method":"POST","uri":"http://localhost:5544/base/upload"},' + '{"name":"payload","method":"POST","uri":"http://localhost:5544/base/payload"},' '{"name":"patch","method":"PATCH","uri":"http://localhost:0/base/patch"},' '{"name":"put","method":"PUT","uri":"http://localhost:0/base/put"},' '{"name":"delete","method":"DELETE","uri":"http://localhost:0/base/delete"}' @@ -797,6 +878,16 @@ class MyBaseModule extends APIModule { 'mimeType: ${request.payloadMimeType} ; ' 'length: ${request.payloadAsBytes?.length}' '${(request.payloadMimeType?.isStringType ?? false) ? '<<${request.payload}>>' : ''}')); + + routes.post('payload', (request) { + var payloadBytes = request.payloadAsBytes; + if (payloadBytes == null || payloadBytes.isEmpty) { + return APIResponse.ok('PAYLOAD: empty'); + } + + var payloadStr = convert.utf8.decode(payloadBytes); + return APIResponse.ok('PAYLOAD: ${payloadStr.length} <<$payloadStr>>'); + }); } } @@ -843,12 +934,14 @@ class MyInfoModule extends APIModule { Future<(int, String)> _getURL(String url, {APIRequestMethod? method, Map? parameters, + Map? headers, List? payload, String? payloadType, String? expectedContentType}) async { var (status, content, _) = await _getUrlAndHeaders(url, method: method, parameters: parameters, + headers: headers, payload: payload, payloadType: payloadType, expectedContentType: expectedContentType); From 7eb0dc5228239fc0c364799845c145498c4699ba Mon Sep 17 00:00:00 2001 From: gmpassos Date: Mon, 16 Jun 2025 05:24:30 -0300 Subject: [PATCH 2/4] clean code --- lib/src/bones_api_server.dart | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/lib/src/bones_api_server.dart b/lib/src/bones_api_server.dart index 8868b786..4e4c86bb 100644 --- a/lib/src/bones_api_server.dart +++ b/lib/src/bones_api_server.dart @@ -1314,6 +1314,9 @@ class APIServer extends _APIServerBase { static Uint8List _loadPayloadBytes( Request request, List> payloadBlocks, {int? maxPayloadLength, bool? decompressPayload}) { + maxPayloadLength ??= -1; + decompressPayload ??= false; + Uint8List bytes; if (payloadBlocks.length == 1) { @@ -1326,18 +1329,14 @@ class APIServer extends _APIServerBase { assert(bytes.length == bs0.length); - if (maxPayloadLength != null && - maxPayloadLength >= 0 && - bytes.length > maxPayloadLength) { + if (maxPayloadLength >= 0 && bytes.length > maxPayloadLength) { throw StateError( "Payload size (${bytes.length}) exceeds `maxPayloadLength` ($maxPayloadLength)."); } } else { var allBytesSz = payloadBlocks.map((e) => e.length).sum; - if (maxPayloadLength != null && - maxPayloadLength >= 0 && - allBytesSz > maxPayloadLength) { + if (maxPayloadLength >= 0 && allBytesSz > maxPayloadLength) { throw StateError( "Payload size ($allBytesSz) exceeds `maxPayloadLength` ($maxPayloadLength)."); } @@ -1356,12 +1355,11 @@ class APIServer extends _APIServerBase { assert(bytesOffset == allBytesSz); } - var contentEncoding = request.headers[HttpHeaders.contentEncodingHeader]; + if (decompressPayload && bytes.isNotEmpty) { + var contentEncoding = request.headers[HttpHeaders.contentEncodingHeader]; - if (contentEncoding != null && - (decompressPayload ?? false) && - bytes.isNotEmpty) { switch (contentEncoding) { + case null: case '': case 'identity': { @@ -1369,8 +1367,7 @@ class APIServer extends _APIServerBase { } case 'gzip': { - bytes = - _decodePayloadGzip(bytes, maxPayloadLength: maxPayloadLength); + bytes = _decodePayloadGzip(bytes, maxPayloadLength); } case 'deflate': { @@ -1385,9 +1382,8 @@ class APIServer extends _APIServerBase { return bytes; } - static Uint8List _decodePayloadGzip(Uint8List bytes, - {int? maxPayloadLength}) { - if (maxPayloadLength != null && maxPayloadLength >= 0) { + static Uint8List _decodePayloadGzip(Uint8List bytes, int maxPayloadLength) { + if (maxPayloadLength >= 0) { final byteData = ByteData.sublistView(bytes); var gzipUncompressedSize = byteData.getUint32(bytes.length - 4, Endian.little); @@ -1399,6 +1395,14 @@ class APIServer extends _APIServerBase { } var decoded = gzip.decode(bytes); + + if (maxPayloadLength >= 0) { + if (decoded.length > maxPayloadLength) { + throw StateError( + "Decompressed GZip payload size (${decoded.length}) exceeds `maxPayloadLength` ($maxPayloadLength)."); + } + } + return decoded is Uint8List ? decoded : Uint8List.fromList(decoded); } From a007788d0d77ad3b5adaa6acb348780ce05e9eb1 Mon Sep 17 00:00:00 2001 From: gmpassos Date: Mon, 16 Jun 2025 05:31:26 -0300 Subject: [PATCH 3/4] clean code --- lib/src/bones_api_server.dart | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/lib/src/bones_api_server.dart b/lib/src/bones_api_server.dart index 4e4c86bb..fbfc432e 100644 --- a/lib/src/bones_api_server.dart +++ b/lib/src/bones_api_server.dart @@ -1377,6 +1377,11 @@ class APIServer extends _APIServerBase { throw UnsupportedError( "Unknown `Content-Encoding`: $contentEncoding"); } + + if (maxPayloadLength >= 0 && bytes.length > maxPayloadLength) { + throw StateError( + "Decompressed GZip payload size (${bytes.length}) exceeds `maxPayloadLength` ($maxPayloadLength)."); + } } return bytes; @@ -1395,14 +1400,6 @@ class APIServer extends _APIServerBase { } var decoded = gzip.decode(bytes); - - if (maxPayloadLength >= 0) { - if (decoded.length > maxPayloadLength) { - throw StateError( - "Decompressed GZip payload size (${decoded.length}) exceeds `maxPayloadLength` ($maxPayloadLength)."); - } - } - return decoded is Uint8List ? decoded : Uint8List.fromList(decoded); } From 52195698e66092dc7ff5d950e1434547855c7c8b Mon Sep 17 00:00:00 2001 From: gmpassos Date: Mon, 16 Jun 2025 06:05:08 -0300 Subject: [PATCH 4/4] clean code ; zlib payload test --- lib/src/bones_api_server.dart | 2 +- test/bones_api_test.dart | 36 +++++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/lib/src/bones_api_server.dart b/lib/src/bones_api_server.dart index fbfc432e..509c4eff 100644 --- a/lib/src/bones_api_server.dart +++ b/lib/src/bones_api_server.dart @@ -1380,7 +1380,7 @@ class APIServer extends _APIServerBase { if (maxPayloadLength >= 0 && bytes.length > maxPayloadLength) { throw StateError( - "Decompressed GZip payload size (${bytes.length}) exceeds `maxPayloadLength` ($maxPayloadLength)."); + "Decompressed `$contentEncoding` payload size (${bytes.length}) exceeds `maxPayloadLength` ($maxPayloadLength)."); } } diff --git a/test/bones_api_test.dart b/test/bones_api_test.dart index ee053949..34f600de 100644 --- a/test/bones_api_test.dart +++ b/test/bones_api_test.dart @@ -525,6 +525,42 @@ void main() { "Can't decompress payload of size 48: GZip payload uncompressed size (117) exceeds `maxPayloadLength` (100)."))); }); + test('payload+zlib /base', () async { + const largePayload = + 'This is a large content: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'; + var payload = convert.utf8.encode(largePayload); + var payloadZlib = ZLibEncoder().encode(payload); + + var res = await _getURL('${apiServer.url}base/payload', + method: APIRequestMethod.POST, + headers: {'Content-Encoding': 'deflate'}, + payload: payloadZlib, + payloadType: 'text/plain'); + expect( + res.toString(), + equals( + '(200, PAYLOAD: 89 <>)')); + }); + + test('payload+zlib(large content) /base', () async { + const largePayload = + 'This is a large content: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'; + var payload = convert.utf8.encode(largePayload); + var payloadZlib = ZLibEncoder().encode(payload); + + var res = await _getURL('${apiServer.url}base/payload', + method: APIRequestMethod.POST, + headers: {'Content-Encoding': 'deflate'}, + payload: payloadZlib, + payloadType: 'text/plain'); + expect( + res.toString(), + allOf( + startsWith('(500, ERROR processing request:'), + contains( + "Decompressed `deflate` payload size (117) exceeds `maxPayloadLength` (100)"))); + }); + test('get /info', () async { var res = await _getURL('${apiServer.url}info/echo', method: APIRequestMethod.GET, parameters: {'msg': 'Hello!'});