mirror of
https://github.com/apache/nifi.git
synced 2025-02-08 19:14:57 +00:00
NIFI-7394: Add support for sending Multipart/FORM data to InvokeHTTP.
By using dynamic properties with a prefix naming scheme, allow definition of the parts, including the name to give the Flowfile content part, and optionally it's file name. After review: - change so that we can send just the form content or just form data without the flowfile - change the content name and content file name from dynamic properties to properties - change the dynamic name to be an invalid http header "post:form:xxxx" - add validation and more tests This closes #4234. Signed-off-by: Mark Payne <markap14@hotmail.com>
This commit is contained in:
parent
1259bd5bd1
commit
659a383723
@ -18,12 +18,6 @@ package org.apache.nifi.processors.standard;
|
|||||||
|
|
||||||
import static org.apache.commons.lang3.StringUtils.trimToEmpty;
|
import static org.apache.commons.lang3.StringUtils.trimToEmpty;
|
||||||
|
|
||||||
import com.burgstaller.okhttp.AuthenticationCacheInterceptor;
|
|
||||||
import com.burgstaller.okhttp.CachingAuthenticatorDecorator;
|
|
||||||
import com.burgstaller.okhttp.digest.CachingAuthenticator;
|
|
||||||
import com.burgstaller.okhttp.digest.DigestAuthenticator;
|
|
||||||
import com.google.common.io.Files;
|
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@ -57,6 +51,7 @@ import java.util.concurrent.atomic.AtomicReference;
|
|||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
import javax.net.ssl.HostnameVerifier;
|
import javax.net.ssl.HostnameVerifier;
|
||||||
import javax.net.ssl.KeyManager;
|
import javax.net.ssl.KeyManager;
|
||||||
import javax.net.ssl.KeyManagerFactory;
|
import javax.net.ssl.KeyManagerFactory;
|
||||||
@ -66,9 +61,17 @@ import javax.net.ssl.SSLSocketFactory;
|
|||||||
import javax.net.ssl.TrustManager;
|
import javax.net.ssl.TrustManager;
|
||||||
import javax.net.ssl.TrustManagerFactory;
|
import javax.net.ssl.TrustManagerFactory;
|
||||||
import javax.net.ssl.X509TrustManager;
|
import javax.net.ssl.X509TrustManager;
|
||||||
|
|
||||||
|
import com.burgstaller.okhttp.AuthenticationCacheInterceptor;
|
||||||
|
import com.burgstaller.okhttp.CachingAuthenticatorDecorator;
|
||||||
|
import com.burgstaller.okhttp.digest.CachingAuthenticator;
|
||||||
|
import com.burgstaller.okhttp.digest.DigestAuthenticator;
|
||||||
|
import com.google.common.io.Files;
|
||||||
import okhttp3.Cache;
|
import okhttp3.Cache;
|
||||||
import okhttp3.Credentials;
|
import okhttp3.Credentials;
|
||||||
import okhttp3.MediaType;
|
import okhttp3.MediaType;
|
||||||
|
import okhttp3.MultipartBody;
|
||||||
|
import okhttp3.MultipartBody.Builder;
|
||||||
import okhttp3.OkHttpClient;
|
import okhttp3.OkHttpClient;
|
||||||
import okhttp3.Request;
|
import okhttp3.Request;
|
||||||
import okhttp3.RequestBody;
|
import okhttp3.RequestBody;
|
||||||
@ -77,6 +80,7 @@ import okhttp3.ResponseBody;
|
|||||||
import okio.BufferedSink;
|
import okio.BufferedSink;
|
||||||
import org.apache.commons.io.input.TeeInputStream;
|
import org.apache.commons.io.input.TeeInputStream;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.apache.nifi.annotation.behavior.DynamicProperties;
|
||||||
import org.apache.nifi.annotation.behavior.DynamicProperty;
|
import org.apache.nifi.annotation.behavior.DynamicProperty;
|
||||||
import org.apache.nifi.annotation.behavior.InputRequirement;
|
import org.apache.nifi.annotation.behavior.InputRequirement;
|
||||||
import org.apache.nifi.annotation.behavior.InputRequirement.Requirement;
|
import org.apache.nifi.annotation.behavior.InputRequirement.Requirement;
|
||||||
@ -131,9 +135,17 @@ import org.joda.time.format.DateTimeFormatter;
|
|||||||
@WritesAttribute(attribute = "invokehttp.java.exception.message", description = "The Java exception message raised when the processor fails"),
|
@WritesAttribute(attribute = "invokehttp.java.exception.message", description = "The Java exception message raised when the processor fails"),
|
||||||
@WritesAttribute(attribute = "user-defined", description = "If the 'Put Response Body In Attribute' property is set then whatever it is set to "
|
@WritesAttribute(attribute = "user-defined", description = "If the 'Put Response Body In Attribute' property is set then whatever it is set to "
|
||||||
+ "will become the attribute key and the value would be the body of the HTTP response.")})
|
+ "will become the attribute key and the value would be the body of the HTTP response.")})
|
||||||
@DynamicProperty(name = "Header Name", value = "Attribute Expression Language", expressionLanguageScope = ExpressionLanguageScope.FLOWFILE_ATTRIBUTES,
|
@DynamicProperties ({
|
||||||
description = "Send request header with a key matching the Dynamic Property Key and a value created by evaluating "
|
@DynamicProperty(name = "Header Name", value = "Attribute Expression Language", expressionLanguageScope = ExpressionLanguageScope.FLOWFILE_ATTRIBUTES,
|
||||||
+ "the Attribute Expression Language set in the value of the Dynamic Property.")
|
description =
|
||||||
|
"Send request header with a key matching the Dynamic Property Key and a value created by evaluating "
|
||||||
|
+ "the Attribute Expression Language set in the value of the Dynamic Property."),
|
||||||
|
@DynamicProperty(name = "post:form:<NAME>", value = "Attribute Expression Language", expressionLanguageScope = ExpressionLanguageScope.FLOWFILE_ATTRIBUTES,
|
||||||
|
description =
|
||||||
|
"When the HTTP Method is POST, dynamic properties with the property name in the form of post:form:<NAME>,"
|
||||||
|
+ " where the <NAME> will be the form data name, will be used to fill out the multipart form parts."
|
||||||
|
+ " If send message body is false, the flowfile will not be sent, but any other form data will be.")
|
||||||
|
})
|
||||||
public final class InvokeHTTP extends AbstractProcessor {
|
public final class InvokeHTTP extends AbstractProcessor {
|
||||||
// flowfile attribute keys returned after reading the response
|
// flowfile attribute keys returned after reading the response
|
||||||
public final static String STATUS_CODE = "invokehttp.status.code";
|
public final static String STATUS_CODE = "invokehttp.status.code";
|
||||||
@ -148,6 +160,8 @@ public final class InvokeHTTP extends AbstractProcessor {
|
|||||||
|
|
||||||
public static final String DEFAULT_CONTENT_TYPE = "application/octet-stream";
|
public static final String DEFAULT_CONTENT_TYPE = "application/octet-stream";
|
||||||
|
|
||||||
|
public static final String FORM_BASE= "post:form";
|
||||||
|
|
||||||
// Set of flowfile attributes which we generally always ignore during
|
// Set of flowfile attributes which we generally always ignore during
|
||||||
// processing, including when converting http headers, copying attributes, etc.
|
// processing, including when converting http headers, copying attributes, etc.
|
||||||
// This set includes our strings defined above as well as some standard flowfile
|
// This set includes our strings defined above as well as some standard flowfile
|
||||||
@ -163,6 +177,8 @@ public final class InvokeHTTP extends AbstractProcessor {
|
|||||||
public static final String HTTP = "http";
|
public static final String HTTP = "http";
|
||||||
public static final String HTTPS = "https";
|
public static final String HTTPS = "https";
|
||||||
|
|
||||||
|
private static final Pattern DYNAMIC_FORM_PARAMETER_NAME = Pattern.compile("post:form:(?<formDataName>.*)$");
|
||||||
|
|
||||||
// properties
|
// properties
|
||||||
public static final PropertyDescriptor PROP_METHOD = new PropertyDescriptor.Builder()
|
public static final PropertyDescriptor PROP_METHOD = new PropertyDescriptor.Builder()
|
||||||
.name("HTTP Method")
|
.name("HTTP Method")
|
||||||
@ -297,6 +313,30 @@ public final class InvokeHTTP extends AbstractProcessor {
|
|||||||
.required(false)
|
.required(false)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
public static final PropertyDescriptor PROP_FORM_BODY_FORM_NAME = new PropertyDescriptor.Builder()
|
||||||
|
.name("form-body-form-name")
|
||||||
|
.displayName("Flowfile Form Data Name")
|
||||||
|
.description("When Send Message Body is true, and Flowfile Form Data Name is set, "
|
||||||
|
+ " the Flowfile will be sent as the message body in multipart/form format with this value "
|
||||||
|
+ "as the form data name.")
|
||||||
|
.required(false)
|
||||||
|
.addValidator(
|
||||||
|
StandardValidators.createAttributeExpressionLanguageValidator(AttributeExpression.ResultType.STRING, true))
|
||||||
|
.expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
public static final PropertyDescriptor PROP_SET_FORM_FILE_NAME = new PropertyDescriptor.Builder()
|
||||||
|
.name("set-form-filename")
|
||||||
|
.displayName("Set Flowfile Form Data File Name")
|
||||||
|
.description(
|
||||||
|
"When Send Message Body is true, Flowfile Form Data Name is set, "
|
||||||
|
+ "and Set Flowfile Form Data File Name is true, the Flowfile's fileName value "
|
||||||
|
+ "will be set as the filename property of the form data.")
|
||||||
|
.required(false)
|
||||||
|
.defaultValue("true")
|
||||||
|
.allowableValues("true","false")
|
||||||
|
.build();
|
||||||
|
|
||||||
// Per RFC 7235, 2617, and 2616.
|
// Per RFC 7235, 2617, and 2616.
|
||||||
// basic-credentials = base64-user-pass
|
// basic-credentials = base64-user-pass
|
||||||
// base64-user-pass = userid ":" password
|
// base64-user-pass = userid ":" password
|
||||||
@ -450,7 +490,9 @@ public final class InvokeHTTP extends AbstractProcessor {
|
|||||||
PROP_PENALIZE_NO_RETRY,
|
PROP_PENALIZE_NO_RETRY,
|
||||||
PROP_USE_ETAG,
|
PROP_USE_ETAG,
|
||||||
PROP_ETAG_MAX_CACHE_SIZE,
|
PROP_ETAG_MAX_CACHE_SIZE,
|
||||||
IGNORE_RESPONSE_CONTENT));
|
IGNORE_RESPONSE_CONTENT,
|
||||||
|
PROP_FORM_BODY_FORM_NAME,
|
||||||
|
PROP_SET_FORM_FILE_NAME));
|
||||||
|
|
||||||
// relationships
|
// relationships
|
||||||
public static final Relationship REL_SUCCESS_REQ = new Relationship.Builder()
|
public static final Relationship REL_SUCCESS_REQ = new Relationship.Builder()
|
||||||
@ -512,6 +554,22 @@ public final class InvokeHTTP extends AbstractProcessor {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected PropertyDescriptor getSupportedDynamicPropertyDescriptor(String propertyDescriptorName) {
|
protected PropertyDescriptor getSupportedDynamicPropertyDescriptor(String propertyDescriptorName) {
|
||||||
|
if (propertyDescriptorName.startsWith(FORM_BASE)) {
|
||||||
|
|
||||||
|
Matcher matcher = DYNAMIC_FORM_PARAMETER_NAME.matcher(propertyDescriptorName);
|
||||||
|
if (matcher.matches()) {
|
||||||
|
return new PropertyDescriptor.Builder()
|
||||||
|
.required(false)
|
||||||
|
.name(propertyDescriptorName)
|
||||||
|
.description("Form Data " + matcher.group("formDataName"))
|
||||||
|
.addValidator(StandardValidators.createAttributeExpressionLanguageValidator(AttributeExpression.ResultType.STRING, true))
|
||||||
|
.dynamic(true)
|
||||||
|
.expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return new PropertyDescriptor.Builder()
|
return new PropertyDescriptor.Builder()
|
||||||
.required(false)
|
.required(false)
|
||||||
.name(propertyDescriptorName)
|
.name(propertyDescriptorName)
|
||||||
@ -594,6 +652,35 @@ public final class InvokeHTTP extends AbstractProcessor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for dynamic properties for form components.
|
||||||
|
// Even if the flowfile is not sent, we may still send form parameters.
|
||||||
|
boolean hasFormData = false;
|
||||||
|
Map<String, PropertyDescriptor> propertyDescriptors = new HashMap<>();
|
||||||
|
for (final Map.Entry<PropertyDescriptor, String> entry : validationContext.getProperties().entrySet()) {
|
||||||
|
Matcher matcher = DYNAMIC_FORM_PARAMETER_NAME.matcher(entry.getKey().getName());
|
||||||
|
if (matcher.matches()) {
|
||||||
|
hasFormData = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if form data exists, and send body is true, Flowfile Form Data Name must be set.
|
||||||
|
final boolean sendBody = validationContext.getProperty(PROP_SEND_BODY).asBoolean();
|
||||||
|
final boolean contentNameSet = validationContext.getProperty(PROP_FORM_BODY_FORM_NAME).isSet();
|
||||||
|
if (hasFormData) {
|
||||||
|
if (sendBody && !contentNameSet) {
|
||||||
|
results.add(new ValidationResult.Builder().subject(PROP_FORM_BODY_FORM_NAME.getDisplayName())
|
||||||
|
.valid(false).explanation(
|
||||||
|
"If dynamic form data properties are set, and send body is true, Flowfile Form Data Name must be configured.")
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!sendBody && contentNameSet) {
|
||||||
|
results.add(new ValidationResult.Builder().subject(PROP_FORM_BODY_FORM_NAME.getDisplayName())
|
||||||
|
.valid(false).explanation("If Flowfile Form Data Name is configured, Send Message Body must be true.")
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1023,29 +1110,66 @@ public final class InvokeHTTP extends AbstractProcessor {
|
|||||||
return requestBuilder.build();
|
return requestBuilder.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
private RequestBody getRequestBodyToSend(final ProcessSession session, final ProcessContext context, final FlowFile requestFlowFile) {
|
private RequestBody getRequestBodyToSend(final ProcessSession session, final ProcessContext context,
|
||||||
if(context.getProperty(PROP_SEND_BODY).asBoolean()) {
|
final FlowFile requestFlowFile) {
|
||||||
return new RequestBody() {
|
|
||||||
@Override
|
|
||||||
public MediaType contentType() {
|
|
||||||
String contentType = context.getProperty(PROP_CONTENT_TYPE).evaluateAttributeExpressions(requestFlowFile).getValue();
|
|
||||||
contentType = StringUtils.isBlank(contentType) ? DEFAULT_CONTENT_TYPE : contentType;
|
|
||||||
return MediaType.parse(contentType);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
boolean sendBody = context.getProperty(PROP_SEND_BODY).asBoolean();
|
||||||
public void writeTo(BufferedSink sink) throws IOException {
|
|
||||||
session.exportTo(requestFlowFile, sink.outputStream());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
String evalContentType = context.getProperty(PROP_CONTENT_TYPE)
|
||||||
public long contentLength(){
|
.evaluateAttributeExpressions(requestFlowFile).getValue();
|
||||||
return useChunked ? -1 : requestFlowFile.getSize();
|
final String contentType = StringUtils.isBlank(evalContentType) ? DEFAULT_CONTENT_TYPE : evalContentType;
|
||||||
}
|
String contentKey = context.getProperty(PROP_FORM_BODY_FORM_NAME).evaluateAttributeExpressions(requestFlowFile).getValue();
|
||||||
};
|
|
||||||
} else {
|
// Check for dynamic properties for form components.
|
||||||
return RequestBody.create(null, new byte[0]);
|
// Even if the flowfile is not sent, we may still send form parameters.
|
||||||
|
Map<String, PropertyDescriptor> propertyDescriptors = new HashMap<>();
|
||||||
|
for (final Map.Entry<PropertyDescriptor, String> entry : context.getProperties().entrySet()) {
|
||||||
|
Matcher matcher = DYNAMIC_FORM_PARAMETER_NAME.matcher(entry.getKey().getName());
|
||||||
|
if (matcher.matches()) {
|
||||||
|
propertyDescriptors.put(matcher.group("formDataName"), entry.getKey());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RequestBody requestBody = new RequestBody() {
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public MediaType contentType() {
|
||||||
|
return MediaType.parse(contentType);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeTo(BufferedSink sink) throws IOException {
|
||||||
|
session.exportTo(requestFlowFile, sink.outputStream());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long contentLength() {
|
||||||
|
return useChunked ? -1 : requestFlowFile.getSize();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (propertyDescriptors.size() > 0 || StringUtils.isNotEmpty(contentKey)) {
|
||||||
|
// we have form data
|
||||||
|
MultipartBody.Builder builder = new Builder().setType(MultipartBody.FORM);
|
||||||
|
boolean useFileName = context.getProperty(PROP_SET_FORM_FILE_NAME).asBoolean();
|
||||||
|
String contentFileName = null;
|
||||||
|
if (useFileName) {
|
||||||
|
contentFileName = requestFlowFile.getAttribute(CoreAttributes.FILENAME.key());
|
||||||
|
}
|
||||||
|
// loop through the dynamic form parameters
|
||||||
|
for (final Map.Entry<String, PropertyDescriptor> entry : propertyDescriptors.entrySet()) {
|
||||||
|
final String propValue = context.getProperty(entry.getValue().getName())
|
||||||
|
.evaluateAttributeExpressions(requestFlowFile).getValue();
|
||||||
|
builder.addFormDataPart(entry.getKey(), propValue);
|
||||||
|
}
|
||||||
|
if (sendBody) {
|
||||||
|
builder.addFormDataPart(contentKey, contentFileName, requestBody);
|
||||||
|
}
|
||||||
|
return builder.build();
|
||||||
|
} else if (sendBody) {
|
||||||
|
return requestBody;
|
||||||
|
}
|
||||||
|
return RequestBody.create(null, new byte[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Request.Builder setHeaderProperties(final ProcessContext context, Request.Builder requestBuilder, final FlowFile requestFlowFile) {
|
private Request.Builder setHeaderProperties(final ProcessContext context, Request.Builder requestBuilder, final FlowFile requestFlowFile) {
|
||||||
@ -1063,6 +1187,12 @@ public final class InvokeHTTP extends AbstractProcessor {
|
|||||||
logger.warn(excludedHeaders.get(headerKey), new Object[]{headerKey});
|
logger.warn(excludedHeaders.get(headerKey), new Object[]{headerKey});
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// don't include dynamic form data properties
|
||||||
|
if ( DYNAMIC_FORM_PARAMETER_NAME.matcher(headerKey).matches()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
requestBuilder = requestBuilder.addHeader(headerKey, headerValue);
|
requestBuilder = requestBuilder.addHeader(headerKey, headerValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,28 +17,12 @@
|
|||||||
|
|
||||||
package org.apache.nifi.processors.standard.util;
|
package org.apache.nifi.processors.standard.util;
|
||||||
|
|
||||||
import org.apache.nifi.flowfile.attributes.CoreAttributes;
|
import static org.apache.commons.codec.binary.Base64.encodeBase64;
|
||||||
import org.apache.nifi.processors.standard.InvokeHTTP;
|
import static org.junit.Assert.assertEquals;
|
||||||
import org.apache.nifi.provenance.ProvenanceEventRecord;
|
import static org.junit.Assert.assertNotNull;
|
||||||
import org.apache.nifi.provenance.ProvenanceEventType;
|
import static org.junit.Assert.assertTrue;
|
||||||
import org.apache.nifi.util.MockFlowFile;
|
import static org.junit.Assert.fail;
|
||||||
import org.apache.nifi.util.TestRunner;
|
|
||||||
import org.apache.nifi.web.util.TestServer;
|
|
||||||
import org.eclipse.jetty.security.ConstraintSecurityHandler;
|
|
||||||
import org.eclipse.jetty.security.DefaultIdentityService;
|
|
||||||
import org.eclipse.jetty.security.HashLoginService;
|
|
||||||
import org.eclipse.jetty.security.ServerAuthException;
|
|
||||||
import org.eclipse.jetty.security.authentication.DigestAuthenticator;
|
|
||||||
import org.eclipse.jetty.server.Authentication;
|
|
||||||
import org.eclipse.jetty.server.Handler;
|
|
||||||
import org.eclipse.jetty.server.Request;
|
|
||||||
import org.eclipse.jetty.server.handler.AbstractHandler;
|
|
||||||
import org.junit.Assert;
|
|
||||||
import org.junit.Test;
|
|
||||||
|
|
||||||
import javax.servlet.ServletException;
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
|
||||||
import javax.servlet.http.HttpServletResponse;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.PrintWriter;
|
import java.io.PrintWriter;
|
||||||
import java.io.UnsupportedEncodingException;
|
import java.io.UnsupportedEncodingException;
|
||||||
@ -49,11 +33,35 @@ import java.util.HashMap;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import javax.servlet.MultipartConfigElement;
|
||||||
|
import javax.servlet.ServletException;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
import javax.servlet.http.Part;
|
||||||
|
|
||||||
import static org.apache.commons.codec.binary.Base64.encodeBase64;
|
import org.apache.nifi.components.PropertyDescriptor;
|
||||||
import static org.junit.Assert.assertEquals;
|
import org.apache.nifi.expression.ExpressionLanguageScope;
|
||||||
import static org.junit.Assert.assertTrue;
|
import org.apache.nifi.flowfile.attributes.CoreAttributes;
|
||||||
import static org.junit.Assert.fail;
|
import org.apache.nifi.processors.standard.InvokeHTTP;
|
||||||
|
import org.apache.nifi.provenance.ProvenanceEventRecord;
|
||||||
|
import org.apache.nifi.provenance.ProvenanceEventType;
|
||||||
|
import org.apache.nifi.util.MockFlowFile;
|
||||||
|
import org.apache.nifi.util.StringUtils;
|
||||||
|
import org.apache.nifi.util.TestRunner;
|
||||||
|
import org.apache.nifi.web.util.TestServer;
|
||||||
|
import org.eclipse.jetty.http.MultiPartFormInputStream;
|
||||||
|
import org.eclipse.jetty.security.ConstraintSecurityHandler;
|
||||||
|
import org.eclipse.jetty.security.DefaultIdentityService;
|
||||||
|
import org.eclipse.jetty.security.HashLoginService;
|
||||||
|
import org.eclipse.jetty.security.ServerAuthException;
|
||||||
|
import org.eclipse.jetty.security.authentication.DigestAuthenticator;
|
||||||
|
import org.eclipse.jetty.server.Authentication;
|
||||||
|
import org.eclipse.jetty.server.Handler;
|
||||||
|
import org.eclipse.jetty.server.Request;
|
||||||
|
import org.eclipse.jetty.server.handler.AbstractHandler;
|
||||||
|
import org.eclipse.jetty.util.MultiPartInputStreamParser;
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
public abstract class TestInvokeHttpCommon {
|
public abstract class TestInvokeHttpCommon {
|
||||||
|
|
||||||
@ -1011,6 +1019,195 @@ public abstract class TestInvokeHttpCommon {
|
|||||||
runner.assertTransferCount(InvokeHTTP.REL_RESPONSE, 1);
|
runner.assertTransferCount(InvokeHTTP.REL_RESPONSE, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPostWithFormDataWithFileName() throws Exception {
|
||||||
|
final String suppliedMimeType = "text/plain";
|
||||||
|
MultipartFormHandler handler = new MultipartFormHandler();
|
||||||
|
handler.addExpectedPart("name1", "form data 1");
|
||||||
|
handler.addExpectedPart("name2", "form data 2");
|
||||||
|
handler.addExpectedPart("content", "Hello");
|
||||||
|
handler.addFileName("content", "file_name");
|
||||||
|
addHandler(handler);
|
||||||
|
|
||||||
|
runner.setProperty(InvokeHTTP.PROP_METHOD, "POST");
|
||||||
|
runner.setProperty(InvokeHTTP.PROP_URL, url + "/post");
|
||||||
|
runner.setProperty(InvokeHTTP.PROP_CONTENT_TYPE, suppliedMimeType);
|
||||||
|
|
||||||
|
// dynamic form properties
|
||||||
|
PropertyDescriptor dynamicProp1 = new PropertyDescriptor.Builder()
|
||||||
|
.dynamic(true)
|
||||||
|
.name(InvokeHTTP.FORM_BASE + ":name1")
|
||||||
|
.expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES)
|
||||||
|
.build();
|
||||||
|
runner.setProperty(dynamicProp1, "form data 1");
|
||||||
|
|
||||||
|
PropertyDescriptor dynamicProp2 = new PropertyDescriptor.Builder()
|
||||||
|
.dynamic(true)
|
||||||
|
.name(InvokeHTTP.FORM_BASE + ":name2")
|
||||||
|
.expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES)
|
||||||
|
.build();
|
||||||
|
runner.setProperty(dynamicProp2, "form data 2");
|
||||||
|
|
||||||
|
runner.setProperty(InvokeHTTP.PROP_FORM_BODY_FORM_NAME ,"content");
|
||||||
|
runner.setProperty(InvokeHTTP.PROP_SET_FORM_FILE_NAME, "true");
|
||||||
|
|
||||||
|
final Map<String, String> attrs = new HashMap<>();
|
||||||
|
attrs.put(CoreAttributes.MIME_TYPE.key(), "text/csv");
|
||||||
|
attrs.put(CoreAttributes.FILENAME.key(), "file_name");
|
||||||
|
runner.enqueue("Hello".getBytes(), attrs);
|
||||||
|
|
||||||
|
runner.run(1);
|
||||||
|
runner.assertTransferCount(InvokeHTTP.REL_SUCCESS_REQ, 1);
|
||||||
|
runner.assertTransferCount(InvokeHTTP.REL_RESPONSE, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPostFormContentOnly() throws Exception {
|
||||||
|
final String suppliedMimeType = "text/plain";
|
||||||
|
MultipartFormHandler handler = new MultipartFormHandler();
|
||||||
|
handler.addExpectedPart("content", "Hello");
|
||||||
|
handler.addFileName("content", "file_name");
|
||||||
|
addHandler(handler);
|
||||||
|
|
||||||
|
runner.setProperty(InvokeHTTP.PROP_METHOD, "POST");
|
||||||
|
runner.setProperty(InvokeHTTP.PROP_URL, url + "/post");
|
||||||
|
runner.setProperty(InvokeHTTP.PROP_CONTENT_TYPE, suppliedMimeType);
|
||||||
|
|
||||||
|
runner.setProperty(InvokeHTTP.PROP_FORM_BODY_FORM_NAME ,"content");
|
||||||
|
runner.setProperty(InvokeHTTP.PROP_SET_FORM_FILE_NAME, "true");
|
||||||
|
|
||||||
|
final Map<String, String> attrs = new HashMap<>();
|
||||||
|
attrs.put(CoreAttributes.MIME_TYPE.key(), "text/csv");
|
||||||
|
attrs.put(CoreAttributes.FILENAME.key(), "file_name");
|
||||||
|
runner.enqueue("Hello".getBytes(), attrs);
|
||||||
|
|
||||||
|
runner.run(1);
|
||||||
|
runner.assertTransferCount(InvokeHTTP.REL_SUCCESS_REQ, 1);
|
||||||
|
runner.assertTransferCount(InvokeHTTP.REL_RESPONSE, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPostWithFormDataNoFileName() throws Exception {
|
||||||
|
final String suppliedMimeType = "text/plain";
|
||||||
|
MultipartFormHandler handler = new MultipartFormHandler();
|
||||||
|
handler.addExpectedPart("name1", "form data 1");
|
||||||
|
handler.addExpectedPart("name2", "form data 2");
|
||||||
|
handler.addExpectedPart("content", "Hello");
|
||||||
|
addHandler(handler);
|
||||||
|
|
||||||
|
runner.setProperty(InvokeHTTP.PROP_METHOD, "POST");
|
||||||
|
runner.setProperty(InvokeHTTP.PROP_URL, url + "/post");
|
||||||
|
runner.setProperty(InvokeHTTP.PROP_CONTENT_TYPE, suppliedMimeType);
|
||||||
|
|
||||||
|
// dynamic form properties
|
||||||
|
PropertyDescriptor dynamicProp1 = new PropertyDescriptor.Builder()
|
||||||
|
.dynamic(true)
|
||||||
|
.name(InvokeHTTP.FORM_BASE + ":name1")
|
||||||
|
.expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES)
|
||||||
|
.build();
|
||||||
|
runner.setProperty(dynamicProp1, "form data 1");
|
||||||
|
|
||||||
|
PropertyDescriptor dynamicProp2 = new PropertyDescriptor.Builder()
|
||||||
|
.dynamic(true)
|
||||||
|
.name(InvokeHTTP.FORM_BASE + ":name2")
|
||||||
|
.expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES)
|
||||||
|
.build();
|
||||||
|
runner.setProperty(dynamicProp2, "form data 2");
|
||||||
|
|
||||||
|
runner.setProperty(InvokeHTTP.PROP_FORM_BODY_FORM_NAME ,"content");
|
||||||
|
|
||||||
|
final Map<String, String> attrs = new HashMap<>();
|
||||||
|
attrs.put(CoreAttributes.MIME_TYPE.key(), "text/csv");
|
||||||
|
runner.enqueue("Hello".getBytes(), attrs);
|
||||||
|
|
||||||
|
runner.run(1);
|
||||||
|
runner.assertTransferCount(InvokeHTTP.REL_SUCCESS_REQ, 1);
|
||||||
|
runner.assertTransferCount(InvokeHTTP.REL_RESPONSE, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPostWithFormDataNoFile() throws Exception {
|
||||||
|
final String suppliedMimeType = "text/plain";
|
||||||
|
MultipartFormHandler handler = new MultipartFormHandler();
|
||||||
|
handler.addExpectedPart("name1", "form data 1");
|
||||||
|
handler.addExpectedPart("name2", "form data 2");
|
||||||
|
addHandler(handler);
|
||||||
|
|
||||||
|
runner.setProperty(InvokeHTTP.PROP_METHOD, "POST");
|
||||||
|
runner.setProperty(InvokeHTTP.PROP_URL, url + "/post");
|
||||||
|
runner.setProperty(InvokeHTTP.PROP_CONTENT_TYPE, suppliedMimeType);
|
||||||
|
runner.setProperty(InvokeHTTP.PROP_SEND_BODY, "false");
|
||||||
|
|
||||||
|
// dynamic form properties
|
||||||
|
PropertyDescriptor dynamicProp1 = new PropertyDescriptor.Builder()
|
||||||
|
.dynamic(true)
|
||||||
|
.name(InvokeHTTP.FORM_BASE + ":name1")
|
||||||
|
.expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES)
|
||||||
|
.build();
|
||||||
|
runner.setProperty(dynamicProp1, "form data 1");
|
||||||
|
|
||||||
|
PropertyDescriptor dynamicProp2 = new PropertyDescriptor.Builder()
|
||||||
|
.dynamic(true)
|
||||||
|
.name(InvokeHTTP.FORM_BASE + ":name2")
|
||||||
|
.expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES)
|
||||||
|
.build();
|
||||||
|
runner.setProperty(dynamicProp2, "form data 2");
|
||||||
|
|
||||||
|
|
||||||
|
final Map<String, String> attrs = new HashMap<>();
|
||||||
|
attrs.put(CoreAttributes.MIME_TYPE.key(), "text/csv");
|
||||||
|
runner.enqueue("Hello".getBytes(), attrs);
|
||||||
|
|
||||||
|
runner.run(1);
|
||||||
|
runner.assertTransferCount(InvokeHTTP.REL_SUCCESS_REQ, 1);
|
||||||
|
runner.assertTransferCount(InvokeHTTP.REL_RESPONSE, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPostNoSendBodyWithContentFails() throws Exception {
|
||||||
|
final String suppliedMimeType = "text/plain";
|
||||||
|
|
||||||
|
runner.setProperty(InvokeHTTP.PROP_METHOD, "POST");
|
||||||
|
runner.setProperty(InvokeHTTP.PROP_URL, url + "/post");
|
||||||
|
runner.setProperty(InvokeHTTP.PROP_CONTENT_TYPE, suppliedMimeType);
|
||||||
|
runner.setProperty(InvokeHTTP.PROP_SEND_BODY, "false");
|
||||||
|
|
||||||
|
runner.setProperty(InvokeHTTP.PROP_FORM_BODY_FORM_NAME ,"content");
|
||||||
|
runner.assertNotValid();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPostNoFormContentWithFileNameFails() throws Exception {
|
||||||
|
final String suppliedMimeType = "text/plain";
|
||||||
|
runner.setProperty(InvokeHTTP.PROP_METHOD, "POST");
|
||||||
|
runner.setProperty(InvokeHTTP.PROP_URL, url + "/post");
|
||||||
|
runner.setProperty(InvokeHTTP.PROP_CONTENT_TYPE, suppliedMimeType);
|
||||||
|
|
||||||
|
// dynamic form properties
|
||||||
|
PropertyDescriptor dynamicProp1 = new PropertyDescriptor.Builder()
|
||||||
|
.dynamic(true)
|
||||||
|
.name(InvokeHTTP.FORM_BASE + ":name1")
|
||||||
|
.expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES)
|
||||||
|
.build();
|
||||||
|
runner.setProperty(dynamicProp1, "form data 1");
|
||||||
|
|
||||||
|
PropertyDescriptor dynamicProp2 = new PropertyDescriptor.Builder()
|
||||||
|
.dynamic(true)
|
||||||
|
.name(InvokeHTTP.FORM_BASE + ":name2")
|
||||||
|
.expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES)
|
||||||
|
.build();
|
||||||
|
runner.setProperty(dynamicProp2, "form data 2");
|
||||||
|
|
||||||
|
runner.setProperty(InvokeHTTP.PROP_SET_FORM_FILE_NAME, "true");
|
||||||
|
|
||||||
|
final Map<String, String> attrs = new HashMap<>();
|
||||||
|
attrs.put(CoreAttributes.MIME_TYPE.key(), "text/csv");
|
||||||
|
attrs.put(CoreAttributes.FILENAME.key(), "file_name");
|
||||||
|
runner.enqueue("Hello".getBytes(), attrs);
|
||||||
|
|
||||||
|
runner.assertNotValid();
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testPutWithMimeType() throws Exception {
|
public void testPutWithMimeType() throws Exception {
|
||||||
final String suppliedMimeType = "text/plain";
|
final String suppliedMimeType = "text/plain";
|
||||||
@ -1555,6 +1752,91 @@ public abstract class TestInvokeHttpCommon {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class MultipartFormHandler extends AbstractHandler {
|
||||||
|
private static final String MULTIPART_FORMDATA_TYPE = "multipart/form-data";
|
||||||
|
|
||||||
|
private String headerToTrack;
|
||||||
|
private String trackedHeaderValue;
|
||||||
|
private final HashMap<String,String> expectedParts = new HashMap<>();
|
||||||
|
private String fileNamePartName = null;
|
||||||
|
private String fileName = null;
|
||||||
|
|
||||||
|
public MultipartFormHandler() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addExpectedPart(String name, String value) {
|
||||||
|
expectedParts.put(name,value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addFileName(String partName, String fileName) {
|
||||||
|
fileNamePartName = partName;
|
||||||
|
this.fileName = fileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setHeaderToTrack(String headerToTrack) {
|
||||||
|
this.headerToTrack = headerToTrack;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTrackedHeaderValue() {
|
||||||
|
return trackedHeaderValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isMultipartRequest(HttpServletRequest request) {
|
||||||
|
return request.getContentType() != null
|
||||||
|
&& request.getContentType().startsWith(MULTIPART_FORMDATA_TYPE);
|
||||||
|
}
|
||||||
|
private static final MultipartConfigElement MULTI_PART_CONFIG = new MultipartConfigElement(
|
||||||
|
System.getProperty("java.io.tmpdir"));
|
||||||
|
@Override
|
||||||
|
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response)
|
||||||
|
throws IOException, ServletException {
|
||||||
|
|
||||||
|
baseRequest.setHandled(true);
|
||||||
|
|
||||||
|
assertTrue(request.getHeader("Content-Type").startsWith("multipart/form-data"));
|
||||||
|
|
||||||
|
this.trackedHeaderValue = baseRequest.getHttpFields().get(headerToTrack);
|
||||||
|
boolean multipartRequest = isMultipartRequest(request);
|
||||||
|
if (multipartRequest) {
|
||||||
|
baseRequest.setAttribute(Request.MULTIPART_CONFIG_ELEMENT, MULTI_PART_CONFIG);
|
||||||
|
if (expectedParts.size() > 0) {
|
||||||
|
assertEquals(expectedParts.size(), request.getParts().size());
|
||||||
|
}
|
||||||
|
for (Part part : request.getParts()) {
|
||||||
|
String name;
|
||||||
|
String val;
|
||||||
|
String partFileName;
|
||||||
|
if (part instanceof MultiPartInputStreamParser.MultiPart) {
|
||||||
|
MultiPartInputStreamParser.MultiPart multiPart = ((MultiPartInputStreamParser.MultiPart) part);
|
||||||
|
val = new String(multiPart.getBytes());
|
||||||
|
name = part.getName();
|
||||||
|
partFileName = part.getSubmittedFileName();
|
||||||
|
} else if (part instanceof MultiPartFormInputStream.MultiPart) {
|
||||||
|
MultiPartFormInputStream.MultiPart multiPart = ((MultiPartFormInputStream.MultiPart) part);
|
||||||
|
val = new String(multiPart.getBytes());
|
||||||
|
name = part.getName();
|
||||||
|
partFileName = part.getSubmittedFileName();
|
||||||
|
} else {
|
||||||
|
name = "NO";
|
||||||
|
val = "NO";
|
||||||
|
partFileName = "NO";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expectedParts.size() > 0) {
|
||||||
|
assertNotNull(expectedParts.get(name));
|
||||||
|
assertEquals(expectedParts.get(name), val);
|
||||||
|
}
|
||||||
|
if (!StringUtils.isBlank(fileNamePartName)) {
|
||||||
|
if (name.equals(fileNamePartName)) {
|
||||||
|
assertEquals(fileName, partFileName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
public static class GetOrHeadHandler extends AbstractHandler {
|
public static class GetOrHeadHandler extends AbstractHandler {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
Loading…
x
Reference in New Issue
Block a user