Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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`:
Expand Down
2 changes: 1 addition & 1 deletion lib/src/bones_api_base.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
174 changes: 159 additions & 15 deletions lib/src/bones_api_server.dart
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,23 @@
/// 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;

Expand Down Expand Up @@ -164,6 +181,8 @@
bool? cacheStaticFilesResponses,
int? staticFilesCacheMaxMemorySize,
int? staticFilesCacheMaxContentLength,
this.maxPayloadLength,
this.decompressPayload = false,
this.apiConfig,
bool? logToConsole,
bool? logQueue,
Expand Down Expand Up @@ -260,6 +279,9 @@
var staticFilesCacheMaxContentLength =
a.optionAsInt('static-files-cache-max-content-length');

var maxPayloadLength = a.optionAsInt('max-payload-length');
var decompressPayload = a.flag('decompress-payload');

Check warning on line 283 in lib/src/bones_api_server.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/bones_api_server.dart#L282-L283

Added lines #L282 - L283 were not covered by tests

var logToConsole = a.flagOr('log-toConsole', null);

var logQueue = a.flagOr('log-queue', null);
Expand Down Expand Up @@ -308,6 +330,8 @@
cacheStaticFilesResponses: cacheStaticFilesResponses,
staticFilesCacheMaxMemorySize: staticFilesCacheMaxMemorySize,
staticFilesCacheMaxContentLength: staticFilesCacheMaxContentLength,
maxPayloadLength: maxPayloadLength,
decompressPayload: decompressPayload,
serverResponseDelay: serverResponseDelay,
apiConfig: apiConfig,
logToConsole: logToConsole,
Expand Down Expand Up @@ -697,6 +721,8 @@
'cacheStaticFilesResponses': cacheStaticFilesResponses,
'staticFilesCacheMaxMemorySize': staticFilesCacheMaxMemorySize,
'staticFilesCacheMaxContentLength': staticFilesCacheMaxContentLength,
'maxPayloadLength': maxPayloadLength,
'decompressPayload': decompressPayload,

Check warning on line 725 in lib/src/bones_api_server.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/bones_api_server.dart#L724-L725

Added lines #L724 - L725 were not covered by tests
'logToConsole': logToConsole,
'logQueue': logQueue,
if (args.isNotEmpty) 'args': args.toList(),
Expand All @@ -720,6 +746,8 @@
apiCacheControl: json['apiCacheControl'],
staticFilesCacheControl: json['staticFilesCacheControl'],
cacheStaticFilesResponses: json['cacheStaticFilesResponses'],
maxPayloadLength: json['maxPayloadLength'],
decompressPayload: json['decompressPayload'],

Check warning on line 750 in lib/src/bones_api_server.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/bones_api_server.dart#L749-L750

Added lines #L749 - L750 were not covered by tests
logToConsole: json['logToConsole'],
logQueue: json['logQueue'],
args: json['args'],
Expand Down Expand Up @@ -767,6 +795,8 @@
super.cacheStaticFilesResponses,
super.staticFilesCacheMaxMemorySize,
super.staticFilesCacheMaxContentLength,
super.maxPayloadLength,
super.decompressPayload,
super.serverResponseDelay,
super.logToConsole,
super.logQueue,
Expand Down Expand Up @@ -796,6 +826,8 @@
apiServerConfig.staticFilesCacheMaxMemorySize,
staticFilesCacheMaxContentLength:
apiServerConfig.staticFilesCacheMaxContentLength,
maxPayloadLength: apiServerConfig.maxPayloadLength,
decompressPayload: apiServerConfig.decompressPayload,

Check warning on line 830 in lib/src/bones_api_server.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/bones_api_server.dart#L829-L830

Added lines #L829 - L830 were not covered by tests
serverResponseDelay: apiServerConfig.serverResponseDelay,
logToConsole: apiServerConfig.logToConsole,
logQueue: apiServerConfig.logQueue,
Expand Down Expand Up @@ -912,6 +944,8 @@
super.apiCacheControl,
super.staticFilesCacheControl,
super.cacheStaticFilesResponses,
super.maxPayloadLength,
super.decompressPayload,
super.serverResponseDelay,
super.logToConsole,
super.logQueue,
Expand Down Expand Up @@ -1003,6 +1037,8 @@
cacheStaticFilesResponses: cacheStaticFilesResponses,
staticFilesCacheMaxMemorySize: staticFilesCacheMaxMemorySize,
staticFilesCacheMaxContentLength: staticFilesCacheMaxContentLength,
maxPayloadLength: maxPayloadLength,
decompressPayload: decompressPayload,
letsEncryptDirectory: letsEncryptDirectory,
securePort: securePort,
useSessionID: useSessionID,
Expand Down Expand Up @@ -1032,7 +1068,10 @@

/// Converts a [request] to an [APIRequest].
static FutureOr<APIRequest> 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;
Expand Down Expand Up @@ -1094,7 +1133,10 @@

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;

Expand Down Expand Up @@ -1158,9 +1200,18 @@
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).");

Check warning on line 1212 in lib/src/bones_api_server.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/bones_api_server.dart#L1211-L1212

Added lines #L1211 - L1212 were not covered by tests
}

var contentMimeType = _resolveContentMimeType(request);
if (contentLength == null && contentMimeType == null) {
return Future.value(null);
Expand All @@ -1169,9 +1220,21 @@
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);
}
}

Expand Down Expand Up @@ -1204,8 +1267,12 @@
}

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;
Expand All @@ -1218,10 +1285,12 @@
return (mimeType, payload);
});

static Future<String?> _loadPayloadString(
MimeType mimeType, Request request) =>
static Future<String?> _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;

Expand All @@ -1233,13 +1302,21 @@
});

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<List<int>> payloadBlocks) {
static Uint8List _loadPayloadBytes(
Request request, List<List<int>> payloadBlocks,
{int? maxPayloadLength, bool? decompressPayload}) {
maxPayloadLength ??= -1;
decompressPayload ??= false;

Uint8List bytes;

if (payloadBlocks.length == 1) {
Expand All @@ -1251,9 +1328,19 @@
}

assert(bytes.length == bs0.length);

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 >= 0 && allBytesSz > maxPayloadLength) {
throw StateError(
"Payload size ($allBytesSz) exceeds `maxPayloadLength` ($maxPayloadLength).");

Check warning on line 1341 in lib/src/bones_api_server.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/bones_api_server.dart#L1340-L1341

Added lines #L1340 - L1341 were not covered by tests
}

bytes = Uint8List(allBytesSz);
var bytesOffset = 0;

Expand All @@ -1268,9 +1355,59 @@
assert(bytesOffset == allBytesSz);
}

if (decompressPayload && bytes.isNotEmpty) {
var contentEncoding = request.headers[HttpHeaders.contentEncodingHeader];

switch (contentEncoding) {
case null:
case '':
case 'identity':
{
break;
}
case 'gzip':
{
bytes = _decodePayloadGzip(bytes, maxPayloadLength);
}
case 'deflate':
{
bytes = _decodePayloadZlib(bytes);
}
default:
throw UnsupportedError(
"Unknown `Content-Encoding`: $contentEncoding");

Check warning on line 1378 in lib/src/bones_api_server.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/bones_api_server.dart#L1377-L1378

Added lines #L1377 - L1378 were not covered by tests
}

if (maxPayloadLength >= 0 && bytes.length > maxPayloadLength) {
throw StateError(
"Decompressed `$contentEncoding` payload size (${bytes.length}) exceeds `maxPayloadLength` ($maxPayloadLength).");
}
}

return bytes;
}

static Uint8List _decodePayloadGzip(Uint8List bytes, int maxPayloadLength) {
if (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) {
Expand Down Expand Up @@ -1700,6 +1837,8 @@
super.cacheStaticFilesResponses,
super.staticFilesCacheMaxMemorySize,
super.staticFilesCacheMaxContentLength,
super.maxPayloadLength,
super.decompressPayload,
super.serverResponseDelay,
super.logToConsole,
super.logQueue,
Expand Down Expand Up @@ -2144,10 +2283,15 @@
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);
Expand Down
4 changes: 2 additions & 2 deletions pubspec.yaml
Original file line number Diff line number Diff line change
@@ -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:
Expand Down Expand Up @@ -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
Expand Down
Loading