Commit ded1d833 authored by mhellka's avatar mhellka
Browse files

Clarified form parser API and added FormHelper.

parent 7f4f4582
......@@ -121,10 +121,36 @@ public interface RestContext extends AutoCloseable {
int read(byte[] buffer, int off, int len) throws IOException;
/**
* Read bytes from the request body to a byte buffer. It is an error to call
* this method if {@link #hasEntity()} returns false.
*
* @param buffer Byte buffer to copy bytes to.
* @return Actual number of bytes copied to the buffer.
* @throws IOException if an IO error occurred.
*/
default int read(byte[] buffer) throws IOException {
return read(buffer, 0, buffer.length);
}
/**
* Read bytes from the request body to a byte buffer. It is an error to call
* this method if {@link #hasEntity()} returns false.
*
* @param buffer Byte buffer to copy bytes to.
* @return Actual number of bytes copied to the buffer.
* @throws IOException if an IO error occurred.
*/
default int read(ByteBuffer buffer) throws IOException {
if (buffer.hasArray())
return read(buffer.array(), buffer.arrayOffset() + buffer.position(), buffer.remaining());
final byte[] buff = new byte[buffer.remaining()];
final int i = read(buff);
buffer.put(buff, 0, i);
return i;
}
/**
* Change the response status code.
*
......
package de.gwdg.cdstar.rest.utils;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import de.gwdg.cdstar.Utils;
import de.gwdg.cdstar.rest.api.RestContext;
import de.gwdg.cdstar.rest.utils.form.FormParser;
import de.gwdg.cdstar.rest.utils.form.FormParserException;
import de.gwdg.cdstar.rest.utils.form.FormPart;
import de.gwdg.cdstar.rest.utils.form.MultipartParser;
import de.gwdg.cdstar.rest.utils.form.UrlEncodedParser;
public class FormHelper {
private final Map<String, String> parts;
private FormHelper(Map<String, String> parts) {
this.parts = parts;
}
public Optional<String> get(String name) {
return Optional.ofNullable(parts.get(name));
}
/**
* Parse the entire request in a blocking fashion.
*
* @param ctx request context
* @param maxElementSize throw an error if a single form element is larger than
* this.
* @param maxRead throw an error if the entire form is larger than this.
* @return a readily parsed {@link FormHelper}
* @throws EntityTooLarge
* @throws UnsupportedContentType
* @throws FormFieldTooLarge
* @throws FormParserException
* @throws IOException
*/
public static FormHelper parse(RestContext ctx, int maxElementSize, int maxRead)
throws EntityTooLarge, UnsupportedContentType, FormFieldTooLarge, FormParserException, IOException {
if (!ctx.hasEntity())
return new FormHelper(Collections.emptyMap());
final long clen = ctx.getContentLength();
if (clen > 0) {
if (clen > maxRead)
throw new EntityTooLarge();
maxRead = (int) Math.min(maxRead, clen);
}
FormParser form;
if (ctx.isMultipart()) {
form = new MultipartParser(ctx.getHeader("Content-Type"), "UTF-8", maxElementSize);
} else if (ctx.isForm()) {
form = new UrlEncodedParser(maxElementSize, true, StandardCharsets.UTF_8);
} else {
throw new UnsupportedContentType();
}
final Map<String, String> result = new HashMap<>();
final int bufferSize = Utils.gate(maxElementSize, 1024 * 64, maxRead);
final ByteBuffer buffer = ByteBuffer.allocate(bufferSize);
int total = 0;
boolean eof = false;
do {
final int chunk = ctx.read(buffer);
if ((total += chunk) > maxRead)
throw new EntityTooLarge();
buffer.flip();
final List<FormPart> parts;
if (buffer.hasRemaining()) {
parts = form.parse(buffer);
buffer.clear();
} else {
parts = form.finish();
eof = true;
}
for (final FormPart part : parts) {
if (part.isComplete())
result.put(part.getName(), part.drainToString(StandardCharsets.UTF_8));
else if (part.getBuffered() >= maxElementSize)
throw new FormFieldTooLarge(part.getName());
}
} while (!eof);
return new FormHelper(result);
}
public static class EntityTooLarge extends IOException {
}
public static class UnsupportedContentType extends IOException {
}
public static class FormFieldTooLarge extends IOException {
private final String fieldName;
public FormFieldTooLarge(String name) {
super(name);
fieldName = name;
}
public String getFieldName() {
return fieldName;
}
}
}
......@@ -11,16 +11,17 @@ public interface FormParser {
ByteBuffer EOF = ByteBuffer.allocate(0);
/**
* Parse some bytes and return a list of zero or more {@link FormPart}
* instances. The last element in a list may be incomplete, so always check
* {@link FormPart#isComplete()}.
* Parse all bytes from the given buffer and return a list of zero or more
* {@link FormPart} instances. The last element in a list may be incomplete, so
* always check {@link FormPart#isComplete()}. Incomplete parts are returned
* repeatedly until they are complete.
*/
List<FormPart> parse(ByteBuffer bytes) throws Exception;
List<FormPart> parse(ByteBuffer bytes) throws FormParserException;
/**
/**
* Return any buffered {@link FormPart}s and close the parser.
*/
List<FormPart> finish() throws Exception;
List<FormPart> finish() throws FormParserException;
}
\ No newline at end of file
package de.gwdg.cdstar.rest.utils.form;
import java.io.IOException;
public class FormParserException extends IOException {
private static final long serialVersionUID = -3006800522476991884L;
public FormParserException(String message) {
super(message);
}
public FormParserException(String message, Throwable cause) {
super(message, cause);
}
}
......@@ -94,4 +94,9 @@ public interface FormPart {
*/
String drainToString(Charset charset);
/**
* Throw away the current part content.
*/
void clear();
}
\ No newline at end of file
......@@ -25,7 +25,7 @@ public class MultipartParser implements FormParser {
volatile MultipartPart currentPart;
private boolean multipartMessageComplete;
private Exception error;
private FormParserException error;
private final NioMultipartParser multipartParser;
private final MultipartContext multipartContext;
......@@ -63,9 +63,12 @@ public class MultipartParser implements FormParser {
}
@Override
public List<FormPart> parse(ByteBuffer bytes) throws Exception {
public List<FormPart> parse(ByteBuffer bytes) throws FormParserException {
if (bytes.hasArray()) {
return parse(bytes.array(), bytes.arrayOffset() + bytes.position(), bytes.remaining());
final List<FormPart> result = parse(bytes.array(), bytes.arrayOffset() + bytes.position(),
bytes.remaining());
bytes.position(bytes.limit());
return result;
} else {
final byte[] copy = new byte[bytes.remaining()];
bytes.get(copy);
......@@ -73,7 +76,7 @@ public class MultipartParser implements FormParser {
}
}
private synchronized List<FormPart> parse(byte[] bytes, int offset, int len) throws Exception {
private synchronized List<FormPart> parse(byte[] bytes, int offset, int len) throws FormParserException {
multipartParser.write(bytes, offset, len);
if (error != null)
......@@ -93,9 +96,9 @@ public class MultipartParser implements FormParser {
}
@Override
public List<FormPart> finish() throws Exception {
public List<FormPart> finish() throws FormParserException {
if (!multipartMessageComplete)
throw new IOException("Unexpected end of stream.");
throw new FormParserException("Unexpected end of stream.");
return Collections.emptyList();
}
......@@ -133,7 +136,7 @@ public class MultipartParser implements FormParser {
@Override
public void onError(String message, Throwable cause) {
error = new IllegalStateException(message, cause);
error = new FormParserException(message, cause);
}
}
......
......@@ -103,4 +103,11 @@ public class MultipartPart implements FormPart {
return new String(drain().array(), charset);
}
@Override
public void clear() {
byte[] chunk;
while ((chunk = chunks.pollFirst()) != null)
drained += chunk.length;
}
}
\ No newline at end of file
......@@ -63,13 +63,12 @@ public class UrlEncodedParser implements FormParser {
/**
* Parse a chunk of input and return a list of {@link UrlFormPart}s.
*
* @throws IllegalArgumentException in protocol errors (e.g. invalid characters
* after '%' escape sequence or if a part (key
* + value) is larger than
* {@link #getMaxElementSize()})
* @throws FormParserException on protocol errors (e.g. invalid characters after
* '%' escape sequence or if a part (key + value) is
* larger than {@link #getMaxElementSize()})
*/
@Override
public List<FormPart> parse(ByteBuffer bytes) {
public List<FormPart> parse(ByteBuffer bytes) throws FormParserException {
while (bytes.hasRemaining()) {
offset++;
......@@ -84,8 +83,8 @@ public class UrlEncodedParser implements FormParser {
else if (b >= 'a' && b <= 'f')
v = 10 + b - 'a';
else
throw new IllegalArgumentException(
"Invalid byte in hex escape sequence: offset=" + offset + ", byte=" + (b & 0xFF));
throw new FormParserException(
"Invalid byte in hex escape sequence: offset=" + offset + ", byte=" + (b & 0xFF));
if (escapedByte == -1) {
escapedByte = v;
......@@ -108,11 +107,11 @@ public class UrlEncodedParser implements FormParser {
return flush();
}
private void put(byte b) {
private void put(byte b) throws FormParserException {
try {
buffer.put(b);
} catch (final BufferOverflowException e) {
throw new IllegalArgumentException("Maximum element size for url-encoded form data exceeded.", e);
throw new FormParserException("Maximum element size for url-encoded form data exceeded.", e);
}
}
......@@ -208,6 +207,11 @@ public class UrlEncodedParser implements FormParser {
return new String(value, charset);
}
@Override
public void clear() {
drained = true;
}
}
public static boolean isForm(String contentType) {
......
......@@ -30,6 +30,7 @@ import de.gwdg.cdstar.rest.utils.QueryHelper;
import de.gwdg.cdstar.rest.utils.RequestThrottle;
import de.gwdg.cdstar.rest.utils.RestUtils;
import de.gwdg.cdstar.rest.utils.SessionHelper;
import de.gwdg.cdstar.rest.utils.form.FormParserException;
import de.gwdg.cdstar.rest.utils.form.MultipartParser;
import de.gwdg.cdstar.rest.utils.form.UrlEncodedParser;
import de.gwdg.cdstar.rest.v3.async.ArchiveImporter;
......
......@@ -23,6 +23,7 @@ import de.gwdg.cdstar.Utils;
import de.gwdg.cdstar.rest.api.AsyncContext;
import de.gwdg.cdstar.rest.api.RestContext;
import de.gwdg.cdstar.rest.utils.form.FormParser;
import de.gwdg.cdstar.rest.utils.form.FormParserException;
import de.gwdg.cdstar.rest.utils.form.FormPart;
import de.gwdg.cdstar.rest.v3.errors.ApiErrors;
import de.gwdg.cdstar.runtime.RuntimeContext;
......@@ -91,16 +92,16 @@ public class ArchiveUpdater {
*
* @throws Exception
*/
private void onRead(ByteBuffer buffer) throws Exception {
private void onRead(ByteBuffer buffer) {
try {
if (buffer.hasRemaining())
parts.addAll(parser.parse(buffer));
if (ac.endOfStream())
parts.addAll(parser.finish());
process();
} catch (final Exception e) {
} catch (final FormParserException e) {
log.warn("Error while parsing update request.", e);
final ErrorResponse error = new ErrorResponse(400, "Invalid request", "Error while parsing request.")
final ErrorResponse error = new ErrorResponse(400, "BadRequest", "Error while parsing request.")
.detail("content_type", ctx.getContentType())
.detail("message", e.getMessage());
onError(error);
......
......@@ -26,7 +26,7 @@ public class UrlEncodedParserTest {
}
}
List<FormPart> parseChunks(String... chunks) {
List<FormPart> parseChunks(String... chunks) throws FormParserException {
final UrlEncodedParser parser = new UrlEncodedParser(1024, true, StandardCharsets.UTF_8);
final List<FormPart> parts = new ArrayList<>();
for (final String chunk : chunks)
......@@ -36,7 +36,7 @@ public class UrlEncodedParserTest {
}
@Test
public void testParseChunked() {
public void testParseChunked() throws FormParserException {
assertParsed(parseChunks("foo&bar=xxx"), "foo", "", "bar", "xxx");
assertParsed(parseChunks("foo", "&bar=xxx"), "foo", "", "bar", "xxx");
assertParsed(parseChunks("foo&", "bar=xxx"), "foo", "", "bar", "xxx");
......@@ -44,18 +44,18 @@ public class UrlEncodedParserTest {
}
@Test
public void testEmptyParts() {
public void testEmptyParts() throws FormParserException {
assertParsed(parseChunks("foo&&bar=xxx"), "foo", "", "bar", "xxx");
assertParsed(parseChunks("foo&", "&bar=xxx"), "foo", "", "bar", "xxx");
}
@Test
public void testEmptyKeyParameters() {
public void testEmptyKeyParameters() throws FormParserException {
assertParsed(parseChunks("=foo&="), "", "foo", "", "");
}
@Test
public void testParseEscaped() {
public void testParseEscaped() throws FormParserException {
assertParsed(parseChunks("foo%20"), "foo ", "");
assertParsed(parseChunks("foo%20+"), "foo ", "");
}
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment