提交 4127c9ac 编写于 作者: V Vlad Ilyushchenko

fix(http): NPE when content-disposition is missing or malformed. Added more tests.

上级 c85d4afc
...@@ -146,6 +146,9 @@ public class HttpConnectionContext implements IOContext, Locality, Mutable { ...@@ -146,6 +146,9 @@ public class HttpConnectionContext implements IOContext, Locality, Mutable {
} catch (PeerIsSlowToReadException ignore) { } catch (PeerIsSlowToReadException ignore) {
LOG.debug().$("peer is slow writer").$(); LOG.debug().$("peer is slow writer").$();
dispatcher.registerChannel(this, IOOperation.READ); dispatcher.registerChannel(this, IOOperation.READ);
} catch (ServerDisconnectException ignore) {
LOG.info().$("kicked out [fd=").$(fd).$(']');
dispatcher.disconnect(this);
} }
break; break;
case IOOperation.WRITE: case IOOperation.WRITE:
...@@ -159,6 +162,9 @@ public class HttpConnectionContext implements IOContext, Locality, Mutable { ...@@ -159,6 +162,9 @@ public class HttpConnectionContext implements IOContext, Locality, Mutable {
dispatcher.registerChannel(this, IOOperation.WRITE); dispatcher.registerChannel(this, IOOperation.WRITE);
} catch (PeerDisconnectedException ignore) { } catch (PeerDisconnectedException ignore) {
dispatcher.disconnect(this); dispatcher.disconnect(this);
} catch (ServerDisconnectException ignore) {
LOG.info().$("kicked out [fd=").$(fd).$(']');
dispatcher.disconnect(this);
} }
} else { } else {
assert false; assert false;
...@@ -192,7 +198,8 @@ public class HttpConnectionContext implements IOContext, Locality, Mutable { ...@@ -192,7 +198,8 @@ public class HttpConnectionContext implements IOContext, Locality, Mutable {
} }
} }
private void handleClientRecv(HttpRequestProcessorSelector selector) throws PeerDisconnectedException, PeerIsSlowToReadException { private void handleClientRecv(HttpRequestProcessorSelector selector)
throws PeerDisconnectedException, PeerIsSlowToReadException, ServerDisconnectException {
try { try {
final long fd = this.fd; final long fd = this.fd;
// this is address of where header ended in our receive buffer // this is address of where header ended in our receive buffer
......
...@@ -26,11 +26,12 @@ package io.questdb.cutlass.http; ...@@ -26,11 +26,12 @@ package io.questdb.cutlass.http;
import io.questdb.network.PeerDisconnectedException; import io.questdb.network.PeerDisconnectedException;
import io.questdb.network.PeerIsSlowToReadException; import io.questdb.network.PeerIsSlowToReadException;
import io.questdb.network.ServerDisconnectException;
public interface HttpMultipartContentListener { public interface HttpMultipartContentListener {
void onChunk(long lo, long hi) throws PeerDisconnectedException, PeerIsSlowToReadException; void onChunk(long lo, long hi) throws PeerDisconnectedException, PeerIsSlowToReadException;
void onPartBegin(HttpRequestHeader partHeader) throws PeerDisconnectedException, PeerIsSlowToReadException; void onPartBegin(HttpRequestHeader partHeader) throws PeerDisconnectedException, PeerIsSlowToReadException, ServerDisconnectException;
void onPartEnd() throws PeerDisconnectedException, PeerIsSlowToReadException; void onPartEnd() throws PeerDisconnectedException, PeerIsSlowToReadException, ServerDisconnectException;
} }
...@@ -26,6 +26,7 @@ package io.questdb.cutlass.http; ...@@ -26,6 +26,7 @@ package io.questdb.cutlass.http;
import io.questdb.network.PeerDisconnectedException; import io.questdb.network.PeerDisconnectedException;
import io.questdb.network.PeerIsSlowToReadException; import io.questdb.network.PeerIsSlowToReadException;
import io.questdb.network.ServerDisconnectException;
import io.questdb.std.Mutable; import io.questdb.std.Mutable;
import io.questdb.std.Unsafe; import io.questdb.std.Unsafe;
import io.questdb.std.str.DirectByteCharSequence; import io.questdb.std.str.DirectByteCharSequence;
...@@ -89,7 +90,8 @@ public class HttpMultipartContentParser implements Closeable, Mutable { ...@@ -89,7 +90,8 @@ public class HttpMultipartContentParser implements Closeable, Mutable {
return this; return this;
} }
public boolean parse(long lo, long hi, HttpMultipartContentListener listener) throws PeerDisconnectedException, PeerIsSlowToReadException { public boolean parse(long lo, long hi, HttpMultipartContentListener listener)
throws PeerDisconnectedException, PeerIsSlowToReadException, ServerDisconnectException {
long _lo = Long.MAX_VALUE; long _lo = Long.MAX_VALUE;
long ptr = lo; long ptr = lo;
while (ptr < hi) { while (ptr < hi) {
......
...@@ -26,6 +26,7 @@ package io.questdb.cutlass.http; ...@@ -26,6 +26,7 @@ package io.questdb.cutlass.http;
import io.questdb.network.PeerDisconnectedException; import io.questdb.network.PeerDisconnectedException;
import io.questdb.network.PeerIsSlowToReadException; import io.questdb.network.PeerIsSlowToReadException;
import io.questdb.network.ServerDisconnectException;
public interface HttpRequestProcessor { public interface HttpRequestProcessor {
void onHeadersReady(HttpConnectionContext context); void onHeadersReady(HttpConnectionContext context);
...@@ -35,6 +36,6 @@ public interface HttpRequestProcessor { ...@@ -35,6 +36,6 @@ public interface HttpRequestProcessor {
default void resumeRecv(HttpConnectionContext context) { default void resumeRecv(HttpConnectionContext context) {
} }
default void resumeSend(HttpConnectionContext context) throws PeerDisconnectedException, PeerIsSlowToReadException { default void resumeSend(HttpConnectionContext context) throws PeerDisconnectedException, PeerIsSlowToReadException, ServerDisconnectException {
} }
} }
...@@ -34,10 +34,7 @@ import io.questdb.cutlass.text.TextException; ...@@ -34,10 +34,7 @@ import io.questdb.cutlass.text.TextException;
import io.questdb.cutlass.text.TextLoader; import io.questdb.cutlass.text.TextLoader;
import io.questdb.log.Log; import io.questdb.log.Log;
import io.questdb.log.LogFactory; import io.questdb.log.LogFactory;
import io.questdb.network.IOOperation; import io.questdb.network.*;
import io.questdb.network.NoSpaceLeftInResponseBufferException;
import io.questdb.network.PeerDisconnectedException;
import io.questdb.network.PeerIsSlowToReadException;
import io.questdb.std.*; import io.questdb.std.*;
import io.questdb.std.str.CharSink; import io.questdb.std.str.CharSink;
...@@ -275,9 +272,10 @@ public class TextImportProcessor implements HttpRequestProcessor, HttpMultipartC ...@@ -275,9 +272,10 @@ public class TextImportProcessor implements HttpRequestProcessor, HttpMultipartC
} }
@Override @Override
public void onPartBegin(HttpRequestHeader partHeader) throws PeerDisconnectedException, PeerIsSlowToReadException { public void onPartBegin(HttpRequestHeader partHeader) throws PeerDisconnectedException, PeerIsSlowToReadException, ServerDisconnectException {
LOG.debug().$("part begin [name=").$(partHeader.getContentDispositionName()).$(']').$(); final CharSequence contentDisposition = partHeader.getContentDispositionName();
if (Chars.equals("data", partHeader.getContentDispositionName())) { LOG.debug().$("part begin [name=").$(contentDisposition).$(']').$();
if (Chars.equalsNc("data", contentDisposition)) {
final HttpRequestHeader rh = transientContext.getRequestHeader(); final HttpRequestHeader rh = transientContext.getRequestHeader();
CharSequence name = rh.getUrlParam("name"); CharSequence name = rh.getUrlParam("name");
...@@ -285,10 +283,8 @@ public class TextImportProcessor implements HttpRequestProcessor, HttpMultipartC ...@@ -285,10 +283,8 @@ public class TextImportProcessor implements HttpRequestProcessor, HttpMultipartC
name = partHeader.getContentDispositionFilename(); name = partHeader.getContentDispositionFilename();
} }
if (name == null) { if (name == null) {
transientContext.simpleResponse().sendStatus(400, "no name given"); transientContext.simpleResponse().sendStatus(400, "no file name given");
// we have to disconnect to interrupt potentially large upload throw ServerDisconnectException.INSTANCE;
transientContext.getDispatcher().disconnect(transientContext);
return;
} }
transientState.analysed = false; transientState.analysed = false;
...@@ -304,17 +300,21 @@ public class TextImportProcessor implements HttpRequestProcessor, HttpMultipartC ...@@ -304,17 +300,21 @@ public class TextImportProcessor implements HttpRequestProcessor, HttpMultipartC
transientState.forceHeader = Chars.equalsNc("true", rh.getUrlParam("forceHeader")); transientState.forceHeader = Chars.equalsNc("true", rh.getUrlParam("forceHeader"));
transientState.messagePart = MESSAGE_DATA; transientState.messagePart = MESSAGE_DATA;
} else if (Chars.equals("schema", partHeader.getContentDispositionName())) { } else if (Chars.equalsNc("schema", contentDisposition)) {
transientState.textLoader.setState(TextLoader.LOAD_JSON_METADATA); transientState.textLoader.setState(TextLoader.LOAD_JSON_METADATA);
transientState.messagePart = MESSAGE_SCHEMA; transientState.messagePart = MESSAGE_SCHEMA;
} else { } else {
// todo: disconnect if (partHeader.getContentDisposition() == null) {
transientState.messagePart = MESSAGE_UNKNOWN; transientContext.simpleResponse().sendStatus(400, "'Content-Disposition' multipart header missing'");
} else {
transientContext.simpleResponse().sendStatus(400, "invalid value in 'Content-Disposition' multipart header");
}
throw ServerDisconnectException.INSTANCE;
} }
} }
@Override @Override
public void onPartEnd() throws PeerDisconnectedException, PeerIsSlowToReadException { public void onPartEnd() throws PeerDisconnectedException, PeerIsSlowToReadException, ServerDisconnectException {
try { try {
LOG.debug().$("part end").$(); LOG.debug().$("part end").$();
transientState.textLoader.wrapUp(); transientState.textLoader.wrapUp();
...@@ -351,14 +351,14 @@ public class TextImportProcessor implements HttpRequestProcessor, HttpMultipartC ...@@ -351,14 +351,14 @@ public class TextImportProcessor implements HttpRequestProcessor, HttpMultipartC
@Override @Override
public void resumeSend( public void resumeSend(
HttpConnectionContext context HttpConnectionContext context
) throws PeerDisconnectedException, PeerIsSlowToReadException { ) throws PeerDisconnectedException, PeerIsSlowToReadException, ServerDisconnectException {
doResumeSend(LV.get(context), context.getChunkedResponseSocket()); doResumeSend(LV.get(context), context.getChunkedResponseSocket());
} }
private void doResumeSend( private void doResumeSend(
TextImportProcessorState state, TextImportProcessorState state,
HttpChunkedResponseSocket socket HttpChunkedResponseSocket socket
) throws PeerDisconnectedException, PeerIsSlowToReadException { ) throws PeerDisconnectedException, PeerIsSlowToReadException, ServerDisconnectException {
try { try {
if (state.json) { if (state.json) {
...@@ -374,8 +374,7 @@ public class TextImportProcessor implements HttpRequestProcessor, HttpMultipartC ...@@ -374,8 +374,7 @@ public class TextImportProcessor implements HttpRequestProcessor, HttpMultipartC
// is larger that response content buffer // is larger that response content buffer
// all we can do in this scenario is to log appropriately // all we can do in this scenario is to log appropriately
// and disconnect socket // and disconnect socket
// todo: this is a force disconnect throw ServerDisconnectException.INSTANCE;
throw PeerDisconnectedException.INSTANCE;
} }
} }
...@@ -410,7 +409,8 @@ public class TextImportProcessor implements HttpRequestProcessor, HttpMultipartC ...@@ -410,7 +409,8 @@ public class TextImportProcessor implements HttpRequestProcessor, HttpMultipartC
socket.done(); socket.done();
} }
private void sendResponse(HttpConnectionContext context) throws PeerDisconnectedException, PeerIsSlowToReadException { private void sendResponse(HttpConnectionContext context)
throws PeerDisconnectedException, PeerIsSlowToReadException, ServerDisconnectException {
TextImportProcessorState state = LV.get(context); TextImportProcessorState state = LV.get(context);
// todo: may be set this up when headers are ready? // todo: may be set this up when headers are ready?
state.json = Chars.equalsNc("json", context.getRequestHeader().getUrlParam("fmt")); state.json = Chars.equalsNc("json", context.getRequestHeader().getUrlParam("fmt"));
......
/*******************************************************************************
* ___ _ ____ ____
* / _ \ _ _ ___ ___| |_| _ \| __ )
* | | | | | | |/ _ \/ __| __| | | | _ \
* | |_| | |_| | __/\__ \ |_| |_| | |_) |
* \__\_\\__,_|\___||___/\__|____/|____/
*
* Copyright (c) 2014-2019 Appsicle
* Copyright (c) 2019-2020 QuestDB
*
* 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.
*
******************************************************************************/
package io.questdb.network;
public class ServerDisconnectException extends Exception {
public static final ServerDisconnectException INSTANCE = new ServerDisconnectException();
}
...@@ -409,6 +409,110 @@ public class IODispatcherTest { ...@@ -409,6 +409,110 @@ public class IODispatcherTest {
"\r\n" + "\r\n" +
"--------------------------27d997ca93d2689d\r\n" + "--------------------------27d997ca93d2689d\r\n" +
"content-disposition: form-data; name=\"data\"; filename=\"fhv_tripdata_2017-02.csv\"\r\n" + "content-disposition: form-data; name=\"data\"; filename=\"fhv_tripdata_2017-02.csv\"\r\n" +
"content-type: application/octet-stream\r\n" +
"\r\n" +
"9988" +
"\r\n" +
"--------------------------27d997ca93d2689d--",
NetworkFacadeImpl.INSTANCE,
true,
1
);
}
@Test
public void testMissingContentDisposition() throws Exception {
testImport(
"HTTP/1.1 400 Bad request\r\n" +
"Server: questDB/1.0\r\n" +
"Date: Thu, 1 Jan 1970 00:00:00 GMT\r\n" +
"Transfer-Encoding: chunked\r\n" +
"Content-Type: text/html; charset=utf-8\r\n" +
"\r\n" +
"31\r\n" +
"'Content-Disposition' multipart header missing'\r\n" +
"\r\n" +
"00\r\n" +
"\r\n",
"POST /upload HTTP/1.1\r\n" +
"host: localhost:9001\r\n" +
"User-Agent: curl/7.64.0\r\n" +
"Accept: */*\r\n" +
"Content-Length: 437760673\r\n" +
"Content-Type: multipart/form-data; boundary=------------------------27d997ca93d2689d\r\n" +
"Expect: 100-continue\r\n" +
"\r\n" +
"--------------------------27d997ca93d2689d\r\n" +
"Content-Type: application/octet-stream\r\n" +
"\r\n" +
"9988" +
"\r\n" +
"--------------------------27d997ca93d2689d--",
NetworkFacadeImpl.INSTANCE,
true,
1
);
}
@Test
public void testMissingContentDispositionName() throws Exception {
testImport(
"HTTP/1.1 400 Bad request\r\n" +
"Server: questDB/1.0\r\n" +
"Date: Thu, 1 Jan 1970 00:00:00 GMT\r\n" +
"Transfer-Encoding: chunked\r\n" +
"Content-Type: text/html; charset=utf-8\r\n" +
"\r\n" +
"39\r\n" +
"invalid value in 'Content-Disposition' multipart header\r\n" +
"\r\n" +
"00\r\n" +
"\r\n",
"POST /upload HTTP/1.1\r\n" +
"host: localhost:9001\r\n" +
"User-Agent: curl/7.64.0\r\n" +
"Accept: */*\r\n" +
"Content-Length: 437760673\r\n" +
"Content-Type: multipart/form-data; boundary=------------------------27d997ca93d2689d\r\n" +
"Expect: 100-continue\r\n" +
"\r\n" +
"--------------------------27d997ca93d2689d\r\n" +
"content-disposition: ; filename=\"fhv_tripdata_2017-02.csv\"\r\n" +
"Content-Type: application/octet-stream\r\n" +
"\r\n" +
"9988" +
"\r\n" +
"--------------------------27d997ca93d2689d--",
NetworkFacadeImpl.INSTANCE,
true,
1
);
}
@Test
public void testMissingContentDispositionFileName() throws Exception {
testImport(
"HTTP/1.1 400 Bad request\r\n" +
"Server: questDB/1.0\r\n" +
"Date: Thu, 1 Jan 1970 00:00:00 GMT\r\n" +
"Transfer-Encoding: chunked\r\n" +
"Content-Type: text/html; charset=utf-8\r\n" +
"\r\n" +
"14\r\n" +
"no file name given\r\n" +
"\r\n" +
"00\r\n" +
"\r\n",
"POST /upload HTTP/1.1\r\n" +
"host: localhost:9001\r\n" +
"User-Agent: curl/7.64.0\r\n" +
"Accept: */*\r\n" +
"Content-Length: 437760673\r\n" +
"Content-Type: multipart/form-data; boundary=------------------------27d997ca93d2689d\r\n" +
"Expect: 100-continue\r\n" +
"\r\n" +
"--------------------------27d997ca93d2689d\r\n" +
"content-disposition: form-data; name=\"data\"\r\n" +
"Content-Type: application/octet-stream\r\n" + "Content-Type: application/octet-stream\r\n" +
"\r\n" + "\r\n" +
"9988" + "9988" +
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册