-Test server listening on http://localhost:45244 GET http://localhost:45244/?hello=world Response: {"body":{},"query":{"hello":"world"},"files":[]}
-
-
-
-
-
-
121 ms
-passedGET Complex
-
-
-Test server listening on http://localhost:54627 Body: hello=world&nums%5B%5D=1&nums%5B%5D=2.0&nums%5B%5D=2&map.foo.bar=baz Response: {"body":{},"query":{"hello":"world","nums":[1,2.0,2],"map":{"foo":{"bar":"baz"}}},"files":[]}
-
-
-
-
-
-
-
-
454 ms
-urlencoded
-
-
-
-
334 ms
-passedPOST Simple
-
-
-Test server listening on http://localhost:38381 Body: hello=world Response: {"body":{"hello":"world"},"query":{},"files":[]}
-
-
-
-
-
-
120 ms
-passedPost Complex
-
-
-Test server listening on http://localhost:52437
-
-
-
-
-
-
-
-
195 ms
-JSON
-
-
-
-
83 ms
-passedPost Simple
-
-
-Test server listening on http://localhost:59756 Body: {"hello":"world"} Response: {"body":{"hello":"world"},"query":{},"files":[]}
-
-
-
-
-
-
112 ms
-passedPost Complex
-
-
-Test server listening on http://localhost:39215 Body: {"hello":"world","nums":[1,2.0,2],"map":{"foo":{"bar":"baz"}}} Response: {"body":{"hello":"world","nums":[1,2.0,2],"map":{"foo":{"bar":"baz"}}},"query":{},"files":[]}
-
-
-
-
-
-
-
-
181 ms
-File
-
-
-
-
181 ms
-passedSingle upload
-
-
-Test server listening on http://localhost:55740 Form Data:
----myBoundary
Content-Disposition: form-data; name="hello"
world
----myBoundary
Content-Disposition: file; name="file"; filename="app.dart"
Content-Type: text/plain
Hello world
----myBoundary-- Response: {"body":{"hello":"world"},"query":{},"files":[{"mimeType":"text/plain","name":"file","filename":"app.dart","data":[72,101,108,108,111,32,119,111,114,108,100]}]}
-
-
-
-
-ignoredMultiple upload
-
-
-
-
-
-
-
-
-
-
-
diff --git a/lib/body_parser.dart b/lib/body_parser.dart
index 1f1b22ba..9bb69e35 100644
--- a/lib/body_parser.dart
+++ b/lib/body_parser.dart
@@ -4,161 +4,7 @@ library body_parser;
import 'dart:async';
import 'dart:convert';
import 'dart:io';
-
-part 'file_upload_info.dart';
-
-/// A representation of data from an incoming request.
-class BodyParseResult {
- /// The parsed body.
- Map body = {};
-
- /// The parsed query string.
- Map query = {};
-
- /// All files uploaded within this request.
- List files = [];
-}
-
-/// Grabs data from an incoming request.
-///
-/// Supports urlencoded and JSON, as well as multipart/form-data uploads.
-/// On a file upload request, only fields with the name **'file'** are processed
-/// as files. Anything else is put in the body. You can change the upload file name
-/// via the *fileUploadName* parameter. :)
-Future parseBody(HttpRequest request,
- {String fileUploadName: 'file'}) async {
- BodyParseResult result = new BodyParseResult();
- ContentType contentType = request.headers.contentType;
-
- // Parse body
- if (contentType != null) {
- if (contentType.mimeType == 'application/json')
- result.body = JSON.decode(await request.transform(UTF8.decoder).join());
- else if (contentType.mimeType == 'application/x-www-form-urlencoded') {
- String body = await request.transform(UTF8.decoder).join();
- buildMapFromUri(result.body, body);
- }
- }
-
- // Parse query
- RegExp queryRgx = new RegExp(r'\?(.+)$');
- String uriString = request.requestedUri.toString();
- if (queryRgx.hasMatch(uriString)) {
- Match queryMatch = queryRgx.firstMatch(uriString);
- buildMapFromUri(result.query, queryMatch.group(1));
- }
-
- // Accept file
- if (contentType != null && request.method == 'POST') {
- RegExp parseBoundaryRgx = new RegExp(
- r'multipart\/form-data;\s*boundary=([^\s;]+)');
- if (parseBoundaryRgx.hasMatch(contentType.toString())) {
- Match boundaryMatch = parseBoundaryRgx.firstMatch(contentType.toString());
- String boundary = boundaryMatch.group(1);
- String body = await request.transform(UTF8.decoder).join();
- for (String chunk in body.split(boundary)) {
- var fileData = getFileDataFromChunk(
- chunk, boundary, fileUploadName, result.body);
- if (fileData != null)
- fileData.forEach((x) => result.files.add(x));
- }
- }
- }
-
- return result;
-}
-
-/// Parses file data from a multipart/form-data chunk.
-List getFileDataFromChunk(String chunk, String boundary, String fileUploadName,
- Map body) {
- FileUploadInfo result = new FileUploadInfo();
- RegExp isFormDataRgx = new RegExp(
- r'Content-Disposition:\s*([^;]+);\s*name="([^"]+)"');
-
- if (isFormDataRgx.hasMatch(chunk)) {
- Match formDataMatch = isFormDataRgx.firstMatch(chunk);
- String disposition = formDataMatch.group(1);
- String name = formDataMatch.group(2);
- String restOfChunk = chunk.substring(formDataMatch.end);
-
- RegExp parseFilenameRgx = new RegExp(r'filename="([^"]+)"');
- if (parseFilenameRgx.hasMatch(chunk)) {
- result.filename = parseFilenameRgx.firstMatch(chunk).group(1);
- }
-
- RegExp contentTypeRgx = new RegExp(r'Content-Type:\s*([^\r\n]+)\r\n');
- if (contentTypeRgx.hasMatch(restOfChunk)) {
- Match contentTypeMatch = contentTypeRgx.firstMatch(restOfChunk);
- restOfChunk = restOfChunk.substring(contentTypeMatch.end);
- result.mimeType = contentTypeMatch.group(1);
- } else restOfChunk = restOfChunk.replaceAll(new RegExp(r'^(\r\n)+'), "");
-
- restOfChunk = restOfChunk
- .replaceAll(boundary, "")
- .replaceFirst(new RegExp(r'\r\n$'), "");
-
- if (disposition == 'file' && name == fileUploadName) {
- result.name = name;
- result.data = UTF8.encode(restOfChunk);
- return [result];
- } else {
- buildMapFromUri(body, "$name=$restOfChunk");
- return null;
- }
- }
-
- return null;
-}
-
-/// Parses a URI-encoded string into real data! **Wow!**
-///
-/// Whichever map you provide will be automatically populated from the urlencoded body string you provide.
-buildMapFromUri(Map map, String body) {
- RegExp parseArrayRgx = new RegExp(r'^(.+)\[\]$');
-
- for (String keyValuePair in body.split('&')) {
- if (keyValuePair.contains('=')) {
- List split = keyValuePair.split('=');
- String key = Uri.decodeQueryComponent(split[0]);
- String value = Uri.decodeQueryComponent(split[1]);
-
- if (parseArrayRgx.hasMatch(key)) {
- Match queryMatch = parseArrayRgx.firstMatch(key);
- key = queryMatch.group(1);
- if (!(map[key] is List)) {
- map[key] = [];
- }
-
- map[key].add(getValue(value));
- } else if (key.contains('.')) {
- // i.e. map.foo.bar => [map, foo, bar]
- List keys = key.split('.');
-
- Map targetMap = map[keys[0]] ?? {};
- map[keys[0]] = targetMap;
- for (int i = 1; i < keys.length; i++) {
- if (i < keys.length - 1) {
- targetMap[keys[i]] = targetMap[keys[i]] ?? {};
- targetMap = targetMap[keys[i]];
- } else {
- targetMap[keys[i]] = getValue(value);
- }
- }
- }
- else map[key] = getValue(value);
- } else map[Uri.decodeQueryComponent(keyValuePair)] = true;
- }
-}
-
-getValue(String value) {
- num numValue = num.parse(value, (_) => double.NAN);
- if (!numValue.isNaN)
- return numValue;
- else if (value.startsWith('[') && value.endsWith(']'))
- return JSON.decode(value);
- else if (value.startsWith('{') && value.endsWith('}'))
- return JSON.decode(value);
- else if (value.trim().toLowerCase() == 'null')
- return null;
- else return value;
-}
\ No newline at end of file
+export 'src/body_parser.dart';
+export 'src/body_parse_result.dart';
+export 'src/file_upload_info.dart';
+export 'src/parse_body.dart';
diff --git a/lib/src/body_parse_result.dart b/lib/src/body_parse_result.dart
new file mode 100644
index 00000000..98131fa9
--- /dev/null
+++ b/lib/src/body_parse_result.dart
@@ -0,0 +1,13 @@
+import 'file_upload_info.dart';
+
+/// A representation of data from an incoming request.
+class BodyParseResult {
+ /// The parsed body.
+ Map body = {};
+
+ /// The parsed query string.
+ Map query = {};
+
+ /// All files uploaded within this request.
+ List files = [];
+}
diff --git a/lib/src/body_parser.dart b/lib/src/body_parser.dart
new file mode 100644
index 00000000..ad199e7b
--- /dev/null
+++ b/lib/src/body_parser.dart
@@ -0,0 +1,21 @@
+import 'dart:async';
+import 'dart:io';
+import 'body_parse_result.dart';
+
+class BodyParser implements StreamTransformer, BodyParseResult> {
+
+ @override
+ Stream bind(HttpRequest stream) {
+ var _stream = new StreamController();
+
+ stream.toList().then((lists) {
+ var ints = [];
+ lists.forEach(ints.addAll);
+ _stream.close();
+
+
+ });
+
+ return _stream.stream;
+ }
+}
\ No newline at end of file
diff --git a/lib/src/chunk.dart b/lib/src/chunk.dart
new file mode 100644
index 00000000..2dd36d0b
--- /dev/null
+++ b/lib/src/chunk.dart
@@ -0,0 +1,8 @@
+import 'file_upload_info.dart';
+
+List getFileDataFromChunk(String chunk, String boundary,
+ String fileUploadName,
+ Map body) {
+ List result = [];
+ return result;
+}
\ No newline at end of file
diff --git a/lib/file_upload_info.dart b/lib/src/file_upload_info.dart
similarity index 94%
rename from lib/file_upload_info.dart
rename to lib/src/file_upload_info.dart
index d0556f39..85be74dd 100644
--- a/lib/file_upload_info.dart
+++ b/lib/src/file_upload_info.dart
@@ -1,5 +1,3 @@
-part of body_parser;
-
/// Represents a file uploaded to the server.
class FileUploadInfo {
/// The MIME type of the uploaded file.
diff --git a/lib/src/get_value.dart b/lib/src/get_value.dart
new file mode 100644
index 00000000..73e01507
--- /dev/null
+++ b/lib/src/get_value.dart
@@ -0,0 +1,14 @@
+import 'dart:convert';
+
+getValue(String value) {
+ num numValue = num.parse(value, (_) => double.NAN);
+ if (!numValue.isNaN)
+ return numValue;
+ else if (value.startsWith('[') && value.endsWith(']'))
+ return JSON.decode(value);
+ else if (value.startsWith('{') && value.endsWith('}'))
+ return JSON.decode(value);
+ else if (value.trim().toLowerCase() == 'null')
+ return null;
+ else return value;
+}
\ No newline at end of file
diff --git a/lib/src/map_from_uri.dart b/lib/src/map_from_uri.dart
new file mode 100644
index 00000000..22b5603b
--- /dev/null
+++ b/lib/src/map_from_uri.dart
@@ -0,0 +1,41 @@
+import 'get_value.dart';
+
+/// Parses a URI-encoded string into real data! **Wow!**
+///
+/// Whichever map you provide will be automatically populated from the urlencoded body string you provide.
+buildMapFromUri(Map map, String body) {
+ RegExp parseArrayRgx = new RegExp(r'^(.+)\[\]$');
+
+ for (String keyValuePair in body.split('&')) {
+ if (keyValuePair.contains('=')) {
+ List split = keyValuePair.split('=');
+ String key = Uri.decodeQueryComponent(split[0]);
+ String value = Uri.decodeQueryComponent(split[1]);
+
+ if (parseArrayRgx.hasMatch(key)) {
+ Match queryMatch = parseArrayRgx.firstMatch(key);
+ key = queryMatch.group(1);
+ if (!(map[key] is List)) {
+ map[key] = [];
+ }
+
+ map[key].add(getValue(value));
+ } else if (key.contains('.')) {
+ // i.e. map.foo.bar => [map, foo, bar]
+ List keys = key.split('.');
+
+ Map targetMap = map[keys[0]] ?? {};
+ map[keys[0]] = targetMap;
+ for (int i = 1; i < keys.length; i++) {
+ if (i < keys.length - 1) {
+ targetMap[keys[i]] = targetMap[keys[i]] ?? {};
+ targetMap = targetMap[keys[i]];
+ } else {
+ targetMap[keys[i]] = getValue(value);
+ }
+ }
+ }
+ else map[key] = getValue(value);
+ } else map[Uri.decodeQueryComponent(keyValuePair)] = true;
+ }
+}
\ No newline at end of file
diff --git a/lib/src/parse_body.dart b/lib/src/parse_body.dart
new file mode 100644
index 00000000..e9c953ca
--- /dev/null
+++ b/lib/src/parse_body.dart
@@ -0,0 +1,59 @@
+import 'dart:async';
+import 'dart:convert';
+import 'dart:io';
+import 'package:http_server/http_server.dart';
+import 'package:mime/mime.dart';
+import 'body_parse_result.dart';
+import 'chunk.dart';
+import 'file_upload_info.dart';
+import 'map_from_uri.dart';
+
+/// Grabs data from an incoming request.
+///
+/// Supports URL-encoded and JSON, as well as multipart/* forms.
+/// On a file upload request, only fields with the name **'file'** are processed
+/// as files. Anything else is put in the body. You can change the upload file name
+/// via the *fileUploadName* parameter. :)
+Future parseBody(HttpRequest request) async {
+ var result = new BodyParseResult();
+
+ if (request.headers.contentType != null) {
+ if (request.headers.contentType.primaryType == 'multipart' &&
+ request.headers.contentType.parameters.containsKey('boundary')) {
+ var parts = request
+ .transform(new MimeMultipartTransformer(
+ request.headers.contentType.parameters['boundary']))
+ .map((part) =>
+ HttpMultipartFormData.parse(part, defaultEncoding: UTF8));
+
+ await for (HttpMultipartFormData part in parts) {
+ if (part.isBinary || part.contentDisposition.parameters.containsKey("filename")) {
+ BytesBuilder builder = await part.fold(new BytesBuilder(), (BytesBuilder b, d) => b..add(d is! String ? d : d.codeUnits));
+ var upload = new FileUploadInfo(
+ mimeType: part.contentType.mimeType,
+ name: part.contentDisposition.parameters['name'],
+ filename: part.contentDisposition.parameters['filename'] ?? 'file',
+ data: builder.takeBytes());
+ result.files.add(upload);
+ } else if (part.isText) {
+ var text = await part.join();
+ buildMapFromUri(result.body, '${part.contentDisposition.parameters["name"]}=$text');
+ } else {
+ print("Found something else : ${part.contentDisposition}");
+ }
+ }
+ } else if (request.headers.contentType.mimeType ==
+ ContentType.JSON.mimeType) {
+ result.body
+ .addAll(JSON.decode(await request.transform(UTF8.decoder).join()));
+ } else if (request.headers.contentType.mimeType ==
+ 'application/x-www-form-urlencoded') {
+ String body = await request.transform(UTF8.decoder).join();
+ buildMapFromUri(result.body, body);
+ }
+ } else if (request.uri.hasQuery) {
+ buildMapFromUri(result.query, request.uri.query);
+ }
+
+ return result;
+}
diff --git a/pubspec.yaml b/pubspec.yaml
index 6a99e196..93b5f323 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,9 +1,11 @@
name: body_parser
author: Tobe O
-version: 1.0.0-dev
+version: 1.0.0-dev+1
description: Parse request bodies and query strings in Dart.
homepage: https://github.com/thosakwe/body_parser
+dependencies:
+ http_server: ">=0.9.6 <1.0.0"
dev_dependencies:
- http: any
- json_god: any
- test: any
\ No newline at end of file
+ http: ">=0.11.3 <0.12.0"
+ json_god: ">=2.0.0-beta <3.0.0"
+ test: ">=0.12.15 <0.13.0"
\ No newline at end of file
diff --git a/test/all_tests.dart b/test/all_tests.dart
index 7adf25a4..0686d5f0 100644
--- a/test/all_tests.dart
+++ b/test/all_tests.dart
@@ -1,168 +1,9 @@
-import 'dart:io';
-
-import 'package:body_parser/body_parser.dart';
-import 'package:http/http.dart' as http;
-import 'package:json_god/json_god.dart';
import 'package:test/test.dart';
+import 'server.dart' as server;
+import 'uploads.dart' as uploads;
+
main() {
- group('Test server support', () {
- HttpServer server;
- String url;
- http.Client client;
- God god;
-
- setUp(() async {
- server = await HttpServer.bind(InternetAddress.LOOPBACK_IP_V4, 0);
- server.listen((HttpRequest request) async {
- //Server will simply return a JSON representation of the parsed body
- request.response.write(god.serialize(await parseBody(request)));
- await request.response.close();
- });
- url = 'http://localhost:${server.port}';
- print('Test server listening on $url');
- client = new http.Client();
- god = new God();
- });
- tearDown(() async {
- await server.close(force: true);
- client.close();
- server = null;
- url = null;
- client = null;
- god = null;
- });
-
- group('query string', () {
- test('GET Simple', () async {
- print('GET $url/?hello=world');
- var response = await client.get('$url/?hello=world');
- print('Response: ${response.body}');
- expect(response.body,
- equals('{"body":{},"query":{"hello":"world"},"files":[]}'));
- });
-
- test('GET Complex', () async {
- var postData = 'hello=world&nums%5B%5D=1&nums%5B%5D=2.0&nums%5B%5D=${3 -
- 1}&map.foo.bar=baz';
- print('Body: $postData');
- var response = await client.get('$url/?$postData');
- print('Response: ${response.body}');
- var query = god.deserialize(response.body)['query'];
- expect(query['hello'], equals('world'));
- expect(query['nums'][2], equals(2));
- expect(query['map'] is Map, equals(true));
- expect(query['map']['foo'], equals({'bar': 'baz'}));
- });
- });
-
- group('urlencoded', () {
- Map headers = {
- HttpHeaders.CONTENT_TYPE: 'application/x-www-form-urlencoded'
- };
- test('POST Simple', () async {
- print('Body: hello=world');
- var response = await client.post(
- url, headers: headers, body: 'hello=world');
- print('Response: ${response.body}');
- expect(response.body,
- equals('{"body":{"hello":"world"},"query":{},"files":[]}'));
- });
-
- test('Post Complex', () async {
- var postData = 'hello=world&nums%5B%5D=1&nums%5B%5D=2.0&nums%5B%5D=${3 -
- 1}&map.foo.bar=baz';
- var response = await client.post(url, headers: headers, body: postData);
- var body = god.deserialize(response.body)['body'];
- expect(body['hello'], equals('world'));
- expect(body['nums'][2], equals(2));
- expect(body['map'] is Map, equals(true));
- expect(body['map']['foo'], equals({'bar': 'baz'}));
- });
- });
-
- group('JSON', () {
- Map headers = {
- HttpHeaders.CONTENT_TYPE: ContentType.JSON.toString()
- };
- test('Post Simple', () async {
- var postData = god.serialize({
- 'hello': 'world'
- });
- print('Body: $postData');
- var response = await client.post(
- url, headers: headers, body: postData);
- print('Response: ${response.body}');
- expect(response.body,
- equals('{"body":{"hello":"world"},"query":{},"files":[]}'));
- });
-
- test('Post Complex', () async {
- var postData = god.serialize({
- 'hello': 'world',
- 'nums': [1, 2.0, 3 - 1],
- 'map': {
- 'foo': {
- 'bar': 'baz'
- }
- }
- });
- print('Body: $postData');
- var response = await client.post(url, headers: headers, body: postData);
- print('Response: ${response.body}');
- var body = god.deserialize(response.body)['body'];
- expect(body['hello'], equals('world'));
- expect(body['nums'][2], equals(2));
- expect(body['map'] is Map, equals(true));
- expect(body['map']['foo'], equals({'bar': 'baz'}));
- });
- });
-
- group('File', () {
- test('Single upload', () async {
- String boundary = '----myBoundary';
- Map headers = {
- HttpHeaders.CONTENT_TYPE: 'multipart/form-data; boundary=$boundary'
- };
- String postData = '\r\n$boundary\r\n' +
- 'Content-Disposition: form-data; name="hello"\r\nworld\r\n$boundary\r\n' +
- 'Content-Disposition: file; name="file"; filename="app.dart"\r\n' +
- 'Content-Type: text/plain\r\nHello world\r\n$boundary--';
-
- print('Form Data: \n$postData');
- var response = await client.post(url, headers: headers, body: postData);
- print('Response: ${response.body}');
- Map json = god.deserialize(response.body);
- List