Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@

import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.apache.http.entity.ContentType;
import org.jdesktop.swingx.decorator.Highlighter;
import org.jdesktop.swingx.decorator.HighlighterFactory;
Expand Down Expand Up @@ -142,6 +143,7 @@ public ConnectorProperties getProperties() {
HttpReceiverProperties properties = (HttpReceiverProperties) getDefaults();
properties.setContextPath(contextPathField.getText());
properties.setTimeout(receiveTimeoutField.getText());
properties.setRequestHeaderSize(requestHeaderSizeField.getText());
properties.setXmlBody(messageContentXmlBodyRadio.isSelected());
properties.setParseMultipart(parseMultipartYesRadio.isSelected());
properties.setIncludeMetadata(includeMetadataYesRadio.isSelected());
Expand All @@ -168,6 +170,7 @@ public void setProperties(ConnectorProperties properties) {

contextPathField.setText(props.getContextPath());
receiveTimeoutField.setText(props.getTimeout());
requestHeaderSizeField.setText(props.getRequestHeaderSize());

updateHttpUrl();

Expand Down Expand Up @@ -247,6 +250,19 @@ public boolean checkProperties(ConnectorProperties properties, boolean highlight
}
}

/*
* A template is resolved when the channel starts, so only a plain value can be checked here.
* A non-positive size is worth catching because Jetty treats it as no limit at all, which
* silently removes the header cap rather than failing visibly.
*/
String requestHeaderSize = props.getRequestHeaderSize();
if (!requestHeaderSize.contains("$") && NumberUtils.toInt(requestHeaderSize, 0) <= 0) {
valid = false;
if (highlight) {
requestHeaderSizeField.setBackground(UIConstants.INVALID_COLOR);
}
}

if (!props.getSourceConnectorProperties().getResponseVariable().equalsIgnoreCase("None")) {
if (props.getResponseContentType().length() == 0) {
valid = false;
Expand All @@ -269,6 +285,7 @@ public boolean checkProperties(ConnectorProperties properties, boolean highlight
@Override
public void resetInvalidProperties() {
receiveTimeoutField.setBackground(null);
requestHeaderSizeField.setBackground(null);
responseContentTypeField.setBackground(null);
responseHeadersVariableField.setBackground(null);
}
Expand Down Expand Up @@ -841,6 +858,8 @@ protected void initComponents() {
contextPathField = new MirthTextField();
receiveTimeoutLabel = new JLabel();
receiveTimeoutField = new MirthTextField();
requestHeaderSizeLabel = new JLabel();
requestHeaderSizeField = new MirthTextField();
httpUrlField = new JTextField();
httpUrlLabel = new JLabel();
headersLabel = new JLabel();
Expand Down Expand Up @@ -901,6 +920,8 @@ public void keyReleased(java.awt.event.KeyEvent evt) {

receiveTimeoutLabel.setText("Receive Timeout (ms):");

requestHeaderSizeLabel.setText("Request Header Size (bytes):");

httpUrlLabel.setText("HTTP URL:");

headersLabel.setText("Response Headers:");
Expand Down Expand Up @@ -1042,6 +1063,7 @@ protected void initToolTips() {
charsetEncodingCombobox.setToolTipText(String.format("<html>Select the character set encoding to be used for the response to the sending system.<br>Set to Default to assume the default character set encoding for the JVM running %s.</html>", BrandingConstants.PRODUCT_NAME));
contextPathField.setToolTipText("The context path for the HTTP Listener URL.");
receiveTimeoutField.setToolTipText("Enter the maximum idle time in milliseconds for a connection.");
requestHeaderSizeField.setToolTipText("<html>The maximum combined size in bytes of all request headers.<br/>Requests larger than this are rejected with 431 Request Header Fields<br/>Too Large before the channel sees them. The default is 8192.<br/>The value may include template substitutions, and must resolve to a number.</html>");
httpUrlField.setToolTipText("<html>Displays the generated HTTP URL for the HTTP Listener.</html>");
responseHeadersTable.setToolTipText("Response header parameters are encoded as HTTP headers in the response sent to the client.");
responseStatusCodeField.setToolTipText("<html>Enter the status code for the HTTP response. If this field is left blank a <br>default status code of 200 will be returned for a successful message, <br>and 500 will be returned for an errored message. If a \"Respond from\" <br>value is chosen, that response will be used to determine a successful <br>or errored response.<html>");
Expand All @@ -1061,12 +1083,14 @@ protected void initToolTips() {
}

protected void initLayout() {
setLayout(new MigLayout("insets 0 8 0 8, novisualpadding, hidemode 3, gap 12 6", "[][]6[]", "[][][][][][][][][][][][][grow][grow]"));
setLayout(new MigLayout("insets 0 8 0 8, novisualpadding, hidemode 3, gap 12 6", "[][]6[]", "[][][][][][][][][][][][][][grow][grow]"));

add(contextPathLabel, "right");
add(contextPathField, "w 150!, sx");
add(receiveTimeoutLabel, "newline, right");
add(receiveTimeoutField, "w 100!, sx");
add(requestHeaderSizeLabel, "newline, right");
add(requestHeaderSizeField, "w 100!, sx");
add(messageContentLabel, "newline, right");
add(messageContentPlainBodyRadio, "split 2");
add(messageContentXmlBodyRadio);
Expand Down Expand Up @@ -1229,6 +1253,8 @@ private void useResponseHeadersVariableFieldsEnabled(boolean useTemplate) {
private MirthRadioButton parseMultipartYesRadio;
protected MirthTextField receiveTimeoutField;
protected JLabel receiveTimeoutLabel;
protected MirthTextField requestHeaderSizeField;
protected JLabel requestHeaderSizeLabel;
protected JLabel responseStatusCodeLabel;
private MirthTextField responseContentTypeField;
private JLabel responseContentTypeLabel;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,17 +44,26 @@ public void configureConnectorUndeploy(Connector connector) {}

@Override
public void configureReceiver(HttpReceiver connector) throws Exception {
org.eclipse.jetty.server.HttpConfiguration httpConfig = new org.eclipse.jetty.server.HttpConfiguration();
httpConfig.setSendServerVersion(false);
httpConfig.setSendXPoweredBy(false);

ServerConnector listener = new ServerConnector(connector.getServer(), new HttpConnectionFactory(httpConfig));
ServerConnector listener = new ServerConnector(connector.getServer(), new HttpConnectionFactory(createHttpConfig(connector)));
listener.setHost(connector.getHost());
listener.setPort(connector.getPort());
listener.setIdleTimeout(connector.getTimeout());
connector.getServer().addConnector(listener);
}

/**
* Builds the Jetty configuration the listener is created with. An implementation that overrides
* {@link #configureReceiver(HttpReceiver)} should build its own listener from this rather than
* repeating the settings, so that it keeps up with changes to the defaults.
*/
protected org.eclipse.jetty.server.HttpConfiguration createHttpConfig(HttpReceiver connector) {
org.eclipse.jetty.server.HttpConfiguration httpConfig = new org.eclipse.jetty.server.HttpConfiguration();
httpConfig.setSendServerVersion(false);
httpConfig.setSendXPoweredBy(false);
httpConfig.setRequestHeaderSize(connector.getRequestHeaderSize());
return httpConfig;
}

@Override
public void configureDispatcher(HttpDispatcher connector, HttpDispatcherProperties connectorProperties) throws Exception {}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ public class HttpReceiver extends SourceConnector implements BinaryContentTypeRe
private String host;
private int port;
private int timeout;
private int requestHeaderSize;
private String[] binaryMimeTypesArray;
private Pattern binaryMimeTypesRegex;
private HttpAuthConnectorPluginProperties authProps;
Expand Down Expand Up @@ -202,6 +203,20 @@ public void onStart() throws ConnectorTaskException {
port = NumberUtils.toInt(replacer.replaceValues(getConnectorProperties().getListenerConnectorProperties().getPort(), channelId, channelName));
timeout = NumberUtils.toInt(replacer.replaceValues(getConnectorProperties().getTimeout(), channelId, channelName), 0);

/*
* A request header size that does not resolve to a positive number fails the connector
* rather than falling back to the default. Jetty treats a non-positive size as no limit at
* all, and a value that cannot be parsed would quietly restore a cap the user was trying to
* lower, so both cases are a silent loss of the limit. The connector panel rejects them, but
* a channel imported or pushed through the API never runs that check.
*/
String requestHeaderSizeValue = replacer.replaceValues(getConnectorProperties().getRequestHeaderSize(), channelId, channelName);
requestHeaderSize = NumberUtils.toInt(requestHeaderSizeValue, 0);

if (requestHeaderSize <= 0) {
throw new ConnectorTaskException("Invalid request header size: " + requestHeaderSizeValue);
}
Comment thread
pacmano1 marked this conversation as resolved.

// Initialize contextPath to "" or its value after replacements
String contextPath = (getConnectorProperties().getContextPath() == null ? "" : replacer.replaceValues(getConnectorProperties().getContextPath(), channelId, channelName)).trim();

Expand Down Expand Up @@ -839,6 +854,10 @@ public int getTimeout() {
return timeout;
}

public int getRequestHeaderSize() {
return requestHeaderSize;
}

protected Map<String, List<String>> extractParameters(Request request) {
Map<String, List<String>> parameterMap = new HashMap<String, List<String>>();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@
import com.mirth.connect.donkey.util.purge.PurgeUtil;

public class HttpReceiverProperties extends ConnectorProperties implements ListenerConnectorPropertiesInterface, SourceConnectorPropertiesInterface {
/**
* Jetty's own default for HttpConfiguration.setRequestHeaderSize. Channels saved before this
* property existed have no requestHeaderSize element, so the getter falls back to this and
* their behavior is unchanged.
*/
public static final int DEFAULT_REQUEST_HEADER_SIZE = 8192;

private ListenerConnectorProperties listenerConnectorProperties;
private SourceConnectorProperties sourceConnectorProperties;

Expand All @@ -42,6 +49,7 @@ public class HttpReceiverProperties extends ConnectorProperties implements Liste
private String charset;
private String contextPath;
private String timeout;
private String requestHeaderSize;
private List<HttpStaticResource> staticResources;

public HttpReceiverProperties() {
Expand All @@ -60,6 +68,7 @@ public HttpReceiverProperties() {
this.charset = "UTF-8";
this.contextPath = "";
this.timeout = "30000";
this.requestHeaderSize = String.valueOf(DEFAULT_REQUEST_HEADER_SIZE);
this.staticResources = new ArrayList<HttpStaticResource>();
this.responseHeadersVariable = "";
this.useResponseHeadersVariable = false;
Expand Down Expand Up @@ -177,6 +186,14 @@ public void setTimeout(String timeout) {
this.timeout = timeout;
}

public String getRequestHeaderSize() {
return requestHeaderSize == null ? String.valueOf(DEFAULT_REQUEST_HEADER_SIZE) : requestHeaderSize;
}

public void setRequestHeaderSize(String requestHeaderSize) {
this.requestHeaderSize = requestHeaderSize;
}

public List<HttpStaticResource> getStaticResources() {
return staticResources;
}
Expand Down Expand Up @@ -287,6 +304,7 @@ public Map<String, Object> getPurgedProperties() {
purgedProperties.put("responseHeaderChars", responseHeaders.size());
purgedProperties.put("charset", charset);
purgedProperties.put("timeout", PurgeUtil.getNumericValue(timeout));
purgedProperties.put("requestHeaderSize", PurgeUtil.getNumericValue(getRequestHeaderSize()));
return purgedProperties;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// SPDX-License-Identifier: MPL-2.0
// SPDX-FileCopyrightText: Open Integration Engine

package com.mirth.connect.connectors.http;

import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang3.StringUtils;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.handler.AbstractHandler;
import org.junit.After;
import org.junit.BeforeClass;
import org.junit.Test;

import com.google.inject.AbstractModule;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.mirth.connect.server.controllers.ConfigurationController;
import com.mirth.connect.server.controllers.ControllerFactory;

/**
* Exercises the request header size setting against a real Jetty connector, since the whole point
* of the property is behavior Jetty enforces before any channel code runs.
*/
public class DefaultHttpConfigurationTest {

private static final int LARGE_HEADER_BYTES = 16384;

private Server server;

@BeforeClass
public static void setupBeforeClass() {
ControllerFactory controllerFactory = mock(ControllerFactory.class);
when(controllerFactory.createConfigurationController()).thenReturn(mock(ConfigurationController.class));

Injector injector = Guice.createInjector(new AbstractModule() {
@Override
protected void configure() {
requestStaticInjection(ControllerFactory.class);
bind(ControllerFactory.class).toInstance(controllerFactory);
}
});
injector.getInstance(ControllerFactory.class);
}

@After
public void tearDown() throws Exception {
if (server != null) {
server.stop();
server = null;
}
}

@Test
public void testJettyDefaultRejectsOversizedHeader() throws Exception {
assertEquals(HttpStatus.REQUEST_HEADER_FIELDS_TOO_LARGE_431, sendLargeHeaderRequest(HttpReceiverProperties.DEFAULT_REQUEST_HEADER_SIZE));
}

@Test
public void testRaisedRequestHeaderSizeAcceptsOversizedHeader() throws Exception {
assertEquals(HttpStatus.OK_200, sendLargeHeaderRequest(65536));
}

private int sendLargeHeaderRequest(int requestHeaderSize) throws Exception {
int port = startReceiver(requestHeaderSize);

try (CloseableHttpClient client = HttpClients.createDefault()) {
HttpGet get = new HttpGet("http://127.0.0.1:" + port + "/");
get.addHeader("X-Large-Header", StringUtils.repeat('a', LARGE_HEADER_BYTES));

try (CloseableHttpResponse response = client.execute(get)) {
return response.getStatusLine().getStatusCode();
}
}
}

private int startReceiver(int requestHeaderSize) throws Exception {
server = new Server();

HttpReceiver receiver = mock(HttpReceiver.class);
when(receiver.getServer()).thenReturn(server);
when(receiver.getHost()).thenReturn("127.0.0.1");
when(receiver.getPort()).thenReturn(0);
when(receiver.getTimeout()).thenReturn(30000);
when(receiver.getRequestHeaderSize()).thenReturn(requestHeaderSize);

new DefaultHttpConfiguration().configureReceiver(receiver);

server.setHandler(new AbstractHandler() {
@Override
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
baseRequest.setHandled(true);
response.setStatus(HttpServletResponse.SC_OK);
}
});

server.start();
return ((ServerConnector) server.getConnectors()[0]).getLocalPort();
}
}
Loading
Loading