diff --git a/pom.xml b/pom.xml index bf618c8db0..e4a892d3fa 100644 --- a/pom.xml +++ b/pom.xml @@ -157,10 +157,10 @@ 3.0.0 - 4.1.56.Final - 2.0.35.Final + 4.1.58.Final + 2.0.36.Final 2.14.0 - 2.12.0 + 2.12.1 3.4.2 2.12.1 42.2.16 diff --git a/server/http-core/src/main/java/cc/blynk/core/http/handlers/BlynkHttpPostMultipartRequestDecoder.java b/server/http-core/src/main/java/cc/blynk/core/http/handlers/BlynkHttpPostMultipartRequestDecoder.java new file mode 100644 index 0000000000..dc6915ea28 --- /dev/null +++ b/server/http-core/src/main/java/cc/blynk/core/http/handlers/BlynkHttpPostMultipartRequestDecoder.java @@ -0,0 +1,34 @@ +package cc.blynk.core.http.handlers; + +import io.netty.handler.codec.http.HttpContent; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.multipart.HttpDataFactory; +import io.netty.handler.codec.http.multipart.HttpPostMultipartRequestDecoder; + +import java.nio.charset.Charset; + +/** + * Full copy of HttpPostRequestDecoder to fix + * https://github.com/netty/netty/issues/10281 + */ +public class BlynkHttpPostMultipartRequestDecoder extends HttpPostMultipartRequestDecoder { + + private final boolean state; + + public BlynkHttpPostMultipartRequestDecoder(HttpDataFactory factory, HttpRequest request, Charset charset) { + super(factory, request, charset); + this.state = true; + } + + @Override + public HttpPostMultipartRequestDecoder offer(HttpContent content) { + //this is very dirty hack + //we skip the first invocation of the offer + //it wont work for every case, but we have Aggregate handler in the pipeline + //so we are covered here :) + if (state) { + return super.offer(content); + } + return null; + } +} diff --git a/server/http-core/src/main/java/cc/blynk/core/http/handlers/BlynkHttpPostRequestDecoder.java b/server/http-core/src/main/java/cc/blynk/core/http/handlers/BlynkHttpPostRequestDecoder.java new file mode 100644 index 0000000000..9c00365252 --- /dev/null +++ b/server/http-core/src/main/java/cc/blynk/core/http/handlers/BlynkHttpPostRequestDecoder.java @@ -0,0 +1,241 @@ +package cc.blynk.core.http.handlers; + +import io.netty.handler.codec.http.HttpConstants; +import io.netty.handler.codec.http.HttpContent; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpHeaderValues; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.multipart.DefaultHttpDataFactory; +import io.netty.handler.codec.http.multipart.HttpDataFactory; +import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder; +import io.netty.handler.codec.http.multipart.HttpPostStandardRequestDecoder; +import io.netty.handler.codec.http.multipart.InterfaceHttpData; +import io.netty.handler.codec.http.multipart.InterfaceHttpPostRequestDecoder; +import io.netty.util.internal.ObjectUtil; +import io.netty.util.internal.StringUtil; + +import java.nio.charset.Charset; +import java.util.List; + +/** + * Full copy of HttpPostRequestDecoder to fix + * https://github.com/netty/netty/issues/10281 + */ +public class BlynkHttpPostRequestDecoder implements InterfaceHttpPostRequestDecoder { + + private final InterfaceHttpPostRequestDecoder decoder; + + /** + * + * @param request + * the request to decode + * @throws NullPointerException + * for request + * @throws HttpPostRequestDecoder.ErrorDataDecoderException + * if the default charset was wrong when decoding or other + * errors + */ + public BlynkHttpPostRequestDecoder(HttpRequest request) { + this(new DefaultHttpDataFactory(DefaultHttpDataFactory.MINSIZE), request, HttpConstants.DEFAULT_CHARSET); + } + + /** + * + * @param factory + * the factory used to create InterfaceHttpData + * @param request + * the request to decode + * @throws NullPointerException + * for request or factory + * @throws HttpPostRequestDecoder.ErrorDataDecoderException + * if the default charset was wrong when decoding or other + * errors + */ + public BlynkHttpPostRequestDecoder(HttpDataFactory factory, HttpRequest request) { + this(factory, request, HttpConstants.DEFAULT_CHARSET); + } + + /** + * + * @param factory + * the factory used to create InterfaceHttpData + * @param request + * the request to decode + * @param charset + * the charset to use as default + * @throws NullPointerException + * for request or charset or factory + * @throws HttpPostRequestDecoder.ErrorDataDecoderException + * if the default charset was wrong when decoding or other + * errors + */ + public BlynkHttpPostRequestDecoder(HttpDataFactory factory, HttpRequest request, Charset charset) { + ObjectUtil.checkNotNull(factory, "factory"); + ObjectUtil.checkNotNull(request, "request"); + ObjectUtil.checkNotNull(charset, "charset"); + + // Fill default values + if (isMultipart(request)) { + decoder = new BlynkHttpPostMultipartRequestDecoder(factory, request, charset); + } else { + decoder = new HttpPostStandardRequestDecoder(factory, request, charset); + } + } + + /** + * Check if the given request is a multipart request + * @return True if the request is a Multipart request + */ + public static boolean isMultipart(HttpRequest request) { + String mimeType = request.headers().get(HttpHeaderNames.CONTENT_TYPE); + if (mimeType != null && mimeType.startsWith(HttpHeaderValues.MULTIPART_FORM_DATA.toString())) { + return getMultipartDataBoundary(mimeType) != null; + } + return false; + } + + /** + * Check from the request ContentType if this request is a Multipart request. + * @return an array of String if multipartDataBoundary exists with the multipartDataBoundary + * as first element, charset if any as second (missing if not set), else null + */ + protected static String[] getMultipartDataBoundary(String contentType) { + // Check if Post using "multipart/form-data; boundary=--89421926422648 [; charset=xxx]" + String[] headerContentType = splitHeaderContentType(contentType); + final String multiPartHeader = HttpHeaderValues.MULTIPART_FORM_DATA.toString(); + if (headerContentType[0].regionMatches(true, 0, multiPartHeader, 0, multiPartHeader.length())) { + int mrank; + int crank; + final String boundaryHeader = HttpHeaderValues.BOUNDARY.toString(); + if (headerContentType[1].regionMatches(true, 0, boundaryHeader, 0, boundaryHeader.length())) { + mrank = 1; + crank = 2; + } else if (headerContentType[2].regionMatches(true, 0, boundaryHeader, 0, boundaryHeader.length())) { + mrank = 2; + crank = 1; + } else { + return null; + } + String boundary = StringUtil.substringAfter(headerContentType[mrank], '='); + if (boundary == null) { + throw new HttpPostRequestDecoder.ErrorDataDecoderException("Needs a boundary value"); + } + if (boundary.charAt(0) == '"') { + String bound = boundary.trim(); + int index = bound.length() - 1; + if (bound.charAt(index) == '"') { + boundary = bound.substring(1, index); + } + } + final String charsetHeader = HttpHeaderValues.CHARSET.toString(); + if (headerContentType[crank].regionMatches(true, 0, charsetHeader, 0, charsetHeader.length())) { + String charset = StringUtil.substringAfter(headerContentType[crank], '='); + if (charset != null) { + return new String[] {"--" + boundary, charset}; + } + } + return new String[] {"--" + boundary}; + } + return null; + } + + @Override + public boolean isMultipart() { + return decoder.isMultipart(); + } + + @Override + public void setDiscardThreshold(int discardThreshold) { + decoder.setDiscardThreshold(discardThreshold); + } + + @Override + public int getDiscardThreshold() { + return decoder.getDiscardThreshold(); + } + + @Override + public List getBodyHttpDatas() { + return decoder.getBodyHttpDatas(); + } + + @Override + public List getBodyHttpDatas(String name) { + return decoder.getBodyHttpDatas(name); + } + + @Override + public InterfaceHttpData getBodyHttpData(String name) { + return decoder.getBodyHttpData(name); + } + + @Override + public InterfaceHttpPostRequestDecoder offer(HttpContent content) { + return decoder.offer(content); + } + + @Override + public boolean hasNext() { + return decoder.hasNext(); + } + + @Override + public InterfaceHttpData next() { + return decoder.next(); + } + + @Override + public InterfaceHttpData currentPartialHttpData() { + return decoder.currentPartialHttpData(); + } + + @Override + public void destroy() { + decoder.destroy(); + } + + @Override + public void cleanFiles() { + decoder.cleanFiles(); + } + + @Override + public void removeHttpDataFromClean(InterfaceHttpData data) { + decoder.removeHttpDataFromClean(data); + } + + /** + * Split the very first line (Content-Type value) in 3 Strings + * + * @return the array of 3 Strings + */ + private static String[] splitHeaderContentType(String sb) { + int aStart; + int aEnd; + int bStart; + int bEnd; + int cStart; + int cEnd; + aStart = HttpPostBodyUtil.findNonWhitespace(sb, 0); + aEnd = sb.indexOf(';'); + if (aEnd == -1) { + return new String[] {sb, "", ""}; + } + bStart = HttpPostBodyUtil.findNonWhitespace(sb, aEnd + 1); + if (sb.charAt(aEnd - 1) == ' ') { + aEnd--; + } + bEnd = sb.indexOf(';', bStart); + if (bEnd == -1) { + bEnd = HttpPostBodyUtil.findEndOfString(sb); + return new String[] {sb.substring(aStart, aEnd), sb.substring(bStart, bEnd), ""}; + } + cStart = HttpPostBodyUtil.findNonWhitespace(sb, bEnd + 1); + if (sb.charAt(bEnd - 1) == ' ') { + bEnd--; + } + cEnd = HttpPostBodyUtil.findEndOfString(sb); + return new String[] {sb.substring(aStart, aEnd), sb.substring(bStart, bEnd), sb.substring(cStart, cEnd)}; + } + +} diff --git a/server/http-core/src/main/java/cc/blynk/core/http/handlers/HttpPostBodyUtil.java b/server/http-core/src/main/java/cc/blynk/core/http/handlers/HttpPostBodyUtil.java new file mode 100644 index 0000000000..899569b12a --- /dev/null +++ b/server/http-core/src/main/java/cc/blynk/core/http/handlers/HttpPostBodyUtil.java @@ -0,0 +1,94 @@ +/* + * Copyright 2012 The Netty Project + * + * The Netty Project licenses this file to you 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 cc.blynk.core.http.handlers; + +/** + * Shared Static object between HttpMessageDecoder, HttpPostRequestDecoder and HttpPostRequestEncoder + */ +final class HttpPostBodyUtil { + + public static final int chunkSize = 8096; + + /** + * Allowed mechanism for multipart + * mechanism := "7bit" + / "8bit" + / "binary" + Not allowed: "quoted-printable" + / "base64" + */ + public enum TransferEncodingMechanism { + /** + * Default encoding + */ + BIT7("7bit"), + /** + * Short lines but not in ASCII - no encoding + */ + BIT8("8bit"), + /** + * Could be long text not in ASCII - no encoding + */ + BINARY("binary"); + + private final String value; + + TransferEncodingMechanism(String value) { + this.value = value; + } + + public String value() { + return value; + } + + @Override + public String toString() { + return value; + } + } + + private HttpPostBodyUtil() { + } + + /** + * Find the first non whitespace + * @return the rank of the first non whitespace + */ + static int findNonWhitespace(String sb, int offset) { + int result; + for (result = offset; result < sb.length(); result++) { + if (!Character.isWhitespace(sb.charAt(result))) { + break; + } + } + return result; + } + + /** + * Find the end of String + * @return the rank of the end of string + */ + static int findEndOfString(String sb) { + int result; + for (result = sb.length(); result > 0; result--) { + if (!Character.isWhitespace(sb.charAt(result - 1))) { + break; + } + } + return result; + } + +} diff --git a/server/http-core/src/main/java/cc/blynk/core/http/handlers/UploadHandler.java b/server/http-core/src/main/java/cc/blynk/core/http/handlers/UploadHandler.java index 23cfc9f682..5d33be86f5 100644 --- a/server/http-core/src/main/java/cc/blynk/core/http/handlers/UploadHandler.java +++ b/server/http-core/src/main/java/cc/blynk/core/http/handlers/UploadHandler.java @@ -26,10 +26,10 @@ import io.netty.handler.codec.http.multipart.DefaultHttpDataFactory; import io.netty.handler.codec.http.multipart.DiskFileUpload; import io.netty.handler.codec.http.multipart.HttpDataFactory; -import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder; import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder.EndOfDataDecoderException; import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder.ErrorDataDecoderException; import io.netty.handler.codec.http.multipart.InterfaceHttpData; +import io.netty.handler.codec.http.multipart.InterfaceHttpPostRequestDecoder; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -51,7 +51,7 @@ public class UploadHandler extends SimpleChannelInboundHandler { private static final HttpDataFactory factory = new DefaultHttpDataFactory(true); final String handlerUri; - private HttpPostRequestDecoder decoder; + private InterfaceHttpPostRequestDecoder decoder; private final String staticFolderPath; private final String uploadFolder; @@ -85,7 +85,7 @@ public void channelRead0(ChannelHandlerContext ctx, HttpObject msg) { try { log.debug("Incoming {} {}", req.method(), req.uri()); - decoder = new HttpPostRequestDecoder(factory, req); + decoder = new BlynkHttpPostRequestDecoder(factory, req); } catch (ErrorDataDecoderException e) { log.error("Error creating http post request decoder.", e); ctx.writeAndFlush(badRequest(e.getMessage()));