Skip to content

Commit

Permalink
GH-148 Proper handling of JSON-encoded multi-part parameter values.
Browse files Browse the repository at this point in the history
  • Loading branch information
nmihajlovski committed Jan 25, 2018
1 parent 9fb5095 commit 6307981
Show file tree
Hide file tree
Showing 4 changed files with 132 additions and 35 deletions.
Expand Up @@ -149,31 +149,50 @@ public Map<String, String> toMap(Buf src, boolean urlDecodeKeys, boolean urlDeco
}

@SuppressWarnings("unchecked")
public void toUrlEncodedParams(Buf src, Map<String, Object> params) {
public void toUrlDecodedParams(Buf src, Map<String, Object> params, BufRanges contentTypes) {
for (int i = 0; i < count; i++) {
String key = keys[i].str(src.bytes());
String val = values[i].str(src.bytes());

boolean isJSON = isJSON(src, contentTypes, i);

key = Msc.urlDecodeOrKeepOriginal(key);
val = Msc.urlDecodeOrKeepOriginal(val);

if (key.endsWith("[]")) {
key = Str.sub(key, 0, -2);
List<String> list = (List<String>) params.get(key);
if (!isJSON) {
val = Msc.urlDecodeOrKeepOriginal(val);

if (list == null) {
list = U.list();
params.put(key, list);
}
if (key.endsWith("[]")) {
key = Str.sub(key, 0, -2);
List<String> list = (List<String>) params.get(key);

if (list == null) {
list = U.list();
params.put(key, list);
}

list.add(val);

list.add(val);
} else {
params.put(key, val);
}

} else {
params.put(key, val);
params.put(key, JSON.parse(val));
}
}
}

private boolean isJSON(Buf src, BufRanges contentTypes, int index) {
if (contentTypes.count > 0) {
U.must(contentTypes.count > index);
BufRange ct = contentTypes.get(index);
return !ct.isEmpty() && ct.str(src.bytes()).startsWith("application/json");

} else {
return false;
}
}

public Map<String, byte[]> toBinaryMap(Buf src, boolean urlDecodeKeys) {
Map<String, byte[]> map = U.map();

Expand Down
Expand Up @@ -248,8 +248,9 @@ public void parseHeadersIntoKV(Buf buf, BufRanges headers, KeyValueRanges header
/**
* @return <code>false</code> if the data wasn't parsed.
*/
public boolean parseBody(Buf src, KeyValueRanges headers, BufRange body, KeyValueRanges data,
Map<String, List<Upload>> files, RapidoidHelper helper) {
private boolean parseBody(Buf src, KeyValueRanges headers, BufRange body,
KeyValueRanges data, BufRanges dataContentTypes,
Map<String, List<Upload>> files, RapidoidHelper helper) {

if (body.isEmpty()) {
return true;
Expand All @@ -274,7 +275,7 @@ public boolean parseBody(Buf src, KeyValueRanges headers, BufRange body, KeyValu
Err.rteIf(multipartBoundary.isEmpty(), "Invalid multi-part HTTP request!");

Map<String, List<Upload>> autoFiles = Coll.mapOfLists();
parseMultiParts(src, body, data, autoFiles, multipartBoundary, helper);
parseMultiParts(src, body, data, dataContentTypes, autoFiles, multipartBoundary, helper);
files.putAll(autoFiles);

return true;
Expand Down Expand Up @@ -308,8 +309,8 @@ private void detectMultipartBoundary(Buf src, BufRange body, BufRange multipartB
}

/* http://www.w3.org/TR/html401/interact/forms.html#h-17.13.4.2 */
private void parseMultiParts(Buf src, BufRange body, KeyValueRanges data, Map<String, List<Upload>> files,
BufRange multipartBoundary, RapidoidHelper helper) {
private void parseMultiParts(Buf src, BufRange body, KeyValueRanges data, BufRanges dataContentTypes,
Map<String, List<Upload>> files, BufRange multipartBoundary, RapidoidHelper helper) {

int start = body.start;
int limit = body.limit();
Expand All @@ -323,7 +324,7 @@ private void parseMultiParts(Buf src, BufRange body, KeyValueRanges data, Map<St
if (pos1 >= 0 && pos2 >= 0) {
int from = pos1 + sepLen + 2;
int to = pos2 - 2;
parseMultiPart(src, body, data, files, multipartBoundary, helper, from, to);
parseMultiPart(src, data, dataContentTypes, files, helper, from, to);
}

pos1 = pos2;
Expand All @@ -336,8 +337,8 @@ private void parseMultiParts(Buf src, BufRange body, KeyValueRanges data, Map<St
}
}

private void parseMultiPart(Buf src, BufRange body, KeyValueRanges data, Map<String, List<Upload>> files,
BufRange multipartBoundary, RapidoidHelper helper, int from, int to) {
private void parseMultiPart(Buf src, KeyValueRanges data, BufRanges dataContentTypes,
Map<String, List<Upload>> files, RapidoidHelper helper, int from, int to) {

KeyValueRanges headers = helper.headersKV.reset();
BufRange partBody = helper.ranges4.ranges[0];
Expand Down Expand Up @@ -376,23 +377,11 @@ private void parseMultiPart(Buf src, BufRange body, KeyValueRanges data, Map<Str
// video/mp4; codecs="avc1.640028 | DEFAULT=text/plain
BufRange contentType = headers.get(src, CONTENT_TYPE, false);

charset.reset();
contType.reset();
contEnc.reset();

if (contentType != null) {
BytesUtil.split(src.bytes(), contentType, SEMI_COL, contType, contEnc, true);
if (BytesUtil.startsWith(src.bytes(), contEnc, CHARSET_EQ, false)) {
charset.assign(contEnc);
charset.strip(CHARSET_EQ.length, 0);
BytesUtil.trim(src.bytes(), charset);

if (!BytesUtil.matches(src.bytes(), charset, _UTF_8, false)
&& !BytesUtil.matches(src.bytes(), charset, _ISO_8859_1, false)) {
Log.warn("Tipically the UTF-8 and ISO-8859-1 charsets are expected, but received different!",
"charset", src.get(charset));
}
}
if (Log.isDebugEnabled()) {
checkCharset(src, contType, contEnc, charset, contentType);
}

// (OPTIONAL) e.g. 7bit | 8bit | binary | DEFAULT=7bit
Expand All @@ -409,6 +398,13 @@ private void parseMultiPart(Buf src, BufRange body, KeyValueRanges data, Map<Str
int ind = data.add();
data.keys[ind].assign(name);
data.values[ind].assign(partBody);

if (contentType != null) {
dataContentTypes.add(contentType.start, contentType.length);
} else {
dataContentTypes.add();
}

} else {
String uploadParamName = src.get(name);
String uploadFilename = src.get(filename);
Expand All @@ -417,6 +413,25 @@ private void parseMultiPart(Buf src, BufRange body, KeyValueRanges data, Map<Str
}
}

private void checkCharset(Buf src, BufRange contType, BufRange contEnc, BufRange charset, BufRange contentType) {
if (contentType != null) {
BytesUtil.split(src.bytes(), contentType, SEMI_COL, contType, contEnc, true);

if (BytesUtil.startsWith(src.bytes(), contEnc, CHARSET_EQ, false)) {
charset.assign(contEnc);
charset.strip(CHARSET_EQ.length, 0);
BytesUtil.trim(src.bytes(), charset);

if (!BytesUtil.matches(src.bytes(), charset, _UTF_8, false)
&& !BytesUtil.matches(src.bytes(), charset, _ISO_8859_1, false)) {

Log.warn("Tipically the UTF-8 and ISO-8859-1 charsets are expected, but received different!",
"charset", src.get(charset));
}
}
}
}

private boolean parseDisposition(Buf src, BufRange dispoA, BufRange dispoB, BufRange name, BufRange filename) {
if (BytesUtil.startsWith(src.bytes(), dispoA, NAME_EQ, false)) {

Expand Down Expand Up @@ -482,9 +497,11 @@ private HttpContentType getContentType(Buf buf, KeyValueRanges headers, BufRange
public boolean parsePosted(Buf input, KeyValueRanges headersKV, BufRange rBody, KeyValueRanges posted,
Map<String, List<Upload>> files, RapidoidHelper helper, Map<String, Object> dest) {

boolean completed = parseBody(input, headersKV, rBody, posted, files, helper);
BufRanges dataContentTypes = helper.ranges3.reset();

boolean completed = parseBody(input, headersKV, rBody, posted, dataContentTypes, files, helper);

posted.toUrlEncodedParams(input, dest);
posted.toUrlDecodedParams(input, dest, dataContentTypes);

return completed;
}
Expand Down
@@ -0,0 +1,53 @@
/*-
* #%L
* rapidoid-integration-tests
* %%
* Copyright (C) 2014 - 2018 Nikolche Mihajlovski and contributors
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/

package org.rapidoid.http;

import org.junit.Test;
import org.rapidoid.annotation.Authors;
import org.rapidoid.annotation.Since;
import org.rapidoid.setup.On;
import org.rapidoid.u.U;

import java.util.Map;

@Authors("Nikolche Mihajlovski")
@Since("5.5.4")
public class HttpClientJSONParamsTest extends IsolatedIntegrationTest {

@Test
public void testHttpReqWithJsonParams() {
On.post("/echo").json((Req req, ReqData data) -> data);

long[] arr = {-10, 0, 20, 9090909090909090909L};
Map<String, Object> map = U.map("x", 1, "t", true);
Map<String, Object> data = U.map("n", 1, "s", "abc", "arr", arr, "map", map);

onlyPost("/echo", data);
}

}

class ReqData {
public int n;
public String s;
public long[] arr;
public Map<String, Object> map;
}
@@ -0,0 +1,8 @@
HTTP/1.1 200 OK
Connection: keep-alive
Server: Rapidoid
Date: XXXXX GMT
Content-Type: application/json
Content-Length: 77

{"n":1,"s":"abc","arr":[-10,0,20,9090909090909090909],"map":{"x":1,"t":true}}

0 comments on commit 6307981

Please sign in to comment.