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
6 changes: 3 additions & 3 deletions server/src/com/mirth/connect/client/core/Client.java
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ public Connector getConnector(javax.ws.rs.client.Client client, Configuration ru
try {
config.register(Class.forName(apiProviderClass));
} catch (Throwable t) {
logger.error("Error registering API provider class: " + apiProviderClass);
logger.error("Error registering API provider class: {}", apiProviderClass, t);
}
}
}
Expand All @@ -219,7 +219,7 @@ public void registerApiProviders(Set<String> packageNames, Set<String> classes)
client.register(clazz);
}
} catch (Throwable t) {
logger.error("Error registering API provider package: " + packageName);
logger.error("Error registering API provider package: {}", packageName, t);
}
}
}
Expand All @@ -229,7 +229,7 @@ public void registerApiProviders(Set<String> packageNames, Set<String> classes)
try {
client.register(Class.forName(clazz));
} catch (Throwable t) {
logger.error("Error registering API provider class: " + clazz);
logger.error("Error registering API provider class: {}", clazz, t);
}
}
}
Expand Down
4 changes: 3 additions & 1 deletion server/src/com/mirth/connect/server/MirthWebServer.java
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@
import com.mirth.connect.client.core.api.Replaces;
import com.mirth.connect.model.ApiProvider;
import com.mirth.connect.model.MetaData;
import com.mirth.connect.server.api.DontRequireRequestedWith;
import com.mirth.connect.server.api.MirthServlet;
import com.mirth.connect.server.api.providers.ApiOriginFilter;
import com.mirth.connect.server.api.providers.ClickjackingFilter;
Expand Down Expand Up @@ -454,7 +455,7 @@ private ServletContextHandler createApiServletContextHandler(String contextPath,
apiServletContextHandler.setContextPath(contextPath + baseAPI + apiPath);
apiServletContextHandler.addFilter(new FilterHolder(new ApiOriginFilter(mirthProperties)), "/*", EnumSet.of(DispatcherType.REQUEST));
apiServletContextHandler.addFilter(new FilterHolder(new ClickjackingFilter(mirthProperties)), "/*", EnumSet.of(DispatcherType.REQUEST));
apiServletContextHandler.addFilter(new FilterHolder(new RequestedWithFilter(mirthProperties)), "/*", EnumSet.of(DispatcherType.REQUEST));
RequestedWithFilter.configure(mirthProperties);
apiServletContextHandler.addFilter(new FilterHolder(new MethodFilter()), "/*", EnumSet.of(DispatcherType.REQUEST));
apiServletContextHandler.addFilter(new FilterHolder(new StrictTransportSecurityFilter(mirthProperties)), "/*", EnumSet.of(DispatcherType.REQUEST));
setConnectorNames(apiServletContextHandler, apiAllowHTTP);
Expand Down Expand Up @@ -608,6 +609,7 @@ private ApiProviders getApiProviders(Version version) {
providerClasses.addAll(serverProviderClasses);
providerClasses.add(OpenApiResource.class);
providerClasses.add(AcceptHeaderOpenApiResource.class);
providerClasses.add(DontRequireRequestedWith.class);
Copy link

Copilot AI Dec 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DontRequireRequestedWith is an annotation, not a provider class. It should not be added to providerClasses. Instead, RequestedWithFilter.class should be added here. The annotation itself doesn't need to be registered - it's just a marker used by the filter.

Suggested change
providerClasses.add(DontRequireRequestedWith.class);
providerClasses.add(RequestedWithFilter.class);

Copilot uses AI. Check for mistakes.

return new ApiProviders(servletInterfacePackages, servletInterfaces, providerPackages, providerClasses);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// SPDX-License-Identifier: MPL-2.0
// SPDX-FileCopyrightText: 2025 Mitch Gaffigan <mitch.gaffigan@comcast.net>

package com.mirth.connect.server.api;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* If this annotation is present on a method or class, the X-Requested-With header
* requirement will not be enforced for that resource.
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface DontRequireRequestedWith {
}
3 changes: 3 additions & 0 deletions server/src/com/mirth/connect/server/api/MirthServlet.java
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,9 @@ private void setContext() {
}

public void setOperation(Operation operation) {
if (operation == null) {
throw new MirthApiException("Method operation cannot be null.");
}
if (extensionName != null) {
operation = new ExtensionOperation(extensionName, operation);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// SPDX-License-Identifier: MPL-2.0
// SPDX-FileCopyrightText: Mirth Corporation
// SPDX-FileCopyrightText: 2025 Mitch Gaffigan <mitch.gaffigan@comcast.net>
/*
* Copyright (c) Mirth Corporation. All rights reserved.
*
Expand All @@ -10,55 +13,65 @@
package com.mirth.connect.server.api.providers;

import java.io.IOException;
import java.lang.reflect.Method;
import java.util.List;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.ext.Provider;
import javax.annotation.Priority;
import javax.ws.rs.Priorities;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerRequestFilter;
import javax.ws.rs.container.ResourceInfo;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Response;

import org.apache.commons.configuration2.PropertiesConfiguration;
import org.apache.commons.lang3.StringUtils;

@Provider
public class RequestedWithFilter implements Filter {
import com.mirth.connect.server.api.DontRequireRequestedWith;

private boolean isRequestedWithHeaderRequired = true;
@Priority(Priorities.AUTHENTICATION + 100)
public class RequestedWithFilter implements ContainerRequestFilter {
Comment on lines +32 to +33
Copy link

Copilot AI Dec 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The RequestedWithFilter class is missing the @Provider annotation. This annotation is required for JAX-RS to automatically discover and register this filter. Other similar filters in the codebase (like ApiOriginFilter, ClickjackingFilter, etc.) use this annotation. Without it, the filter may not be properly registered with the Jersey servlet container.

Copilot uses AI. Check for mistakes.

@Context
private ResourceInfo resourceInfo;

public RequestedWithFilter(PropertiesConfiguration mirthProperties) {

private static boolean isRequestedWithHeaderRequired = true;

// Jax requires a no-arg constructor to instantiate providers via classpath scanning.
public RequestedWithFilter() {
}

public static void configure(PropertiesConfiguration mirthProperties) {
isRequestedWithHeaderRequired = mirthProperties.getBoolean("server.api.require-requested-with", true);
}

@Override
public void init(FilterConfig filterConfig) throws ServletException {}
public static boolean isRequestedWithHeaderRequired() {
return isRequestedWithHeaderRequired;
}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletResponse res = (HttpServletResponse) response;
public void filter(ContainerRequestContext requestContext) throws IOException {
if (!isRequestedWithHeaderRequired) {
return;
}

// If the resource method or class is annotated with DontRequireRequestedWith, skip the check
if (resourceInfo != null) {
Method method = resourceInfo.getResourceMethod();
if (method != null && method.getAnnotation(DontRequireRequestedWith.class) != null) {
return;
}
Class<?> resourceClass = resourceInfo.getResourceClass();
if (resourceClass != null && resourceClass.getAnnotation(DontRequireRequestedWith.class) != null) {
return;
}
}

HttpServletRequest servletRequest = (HttpServletRequest)request;
String requestedWithHeader = (String) servletRequest.getHeader("X-Requested-With");
List<String> header = requestContext.getHeaders().get("X-Requested-With");

//if header is required and not present, send an error
Copy link

Copilot AI Dec 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment should have a space after // to match the project's code style convention. Should be // if header is required... instead of //if header is required...

Suggested change
//if header is required and not present, send an error
// if header is required and not present, send an error

Copilot uses AI. Check for mistakes.
if(isRequestedWithHeaderRequired && StringUtils.isBlank(requestedWithHeader)) {
res.sendError(400, "All requests must have 'X-Requested-With' header");
if (header == null || header.isEmpty() || StringUtils.isBlank(header.get(0))) {
requestContext.abortWith(Response.status(400).entity("All requests must have 'X-Requested-With' header").build());
}
else {
chain.doFilter(request, response);
}

}

public boolean isRequestedWithHeaderRequired() {
return isRequestedWithHeaderRequired;
}

@Override
public void destroy() {}
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
// SPDX-License-Identifier: MPL-2.0
// SPDX-FileCopyrightText: Mirth Corporation
// SPDX-FileCopyrightText: 2025 Mitch Gaffigan <mitch.gaffigan@comcast.net>
package com.mirth.connect.server.api.providers;

import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import javax.servlet.FilterChain;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.core.Response;

import org.apache.commons.configuration2.PropertiesConfiguration;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.ArgumentMatchers;
import org.mockito.Mockito;

import com.mirth.connect.client.core.PropertiesConfigurationUtil;
Expand All @@ -22,26 +27,34 @@ public class RequestedWithFilterTest extends TestCase {
@Test
//assert that if property is set to false, isRequestedWithHeaderRequired = false
public void testConstructor() {

mirthProperties.clearProperty("server.api.require-requested-with");
RequestedWithFilter.configure(mirthProperties);
assertEquals(true, RequestedWithFilter.isRequestedWithHeaderRequired());

mirthProperties.setProperty("server.api.require-requested-with", "false");
RequestedWithFilter requestedWithFilter = new RequestedWithFilter(mirthProperties);
assertEquals(requestedWithFilter.isRequestedWithHeaderRequired(), false);
RequestedWithFilter.configure(mirthProperties);
assertEquals(false, RequestedWithFilter.isRequestedWithHeaderRequired());
}

@Test
//assert that HttpServletResponse.sendError() is called when X-Requested-With is required but not present
public void testDoFilterRequestedWithTrue() {

mirthProperties.setProperty("server.api.require-requested-with", "true");
RequestedWithFilter testFilter = new RequestedWithFilter(mirthProperties);

HttpServletRequest mockReq = Mockito.mock(HttpServletRequest.class);
HttpServletResponse mockResp = Mockito.mock(HttpServletResponse.class);
FilterChain mockFilterChain = Mockito.mock(FilterChain.class);

RequestedWithFilter.configure(mirthProperties);

ContainerRequestContext mockCtx = Mockito.mock(ContainerRequestContext.class);
when(mockCtx.getHeaders()).thenReturn(new javax.ws.rs.core.MultivaluedHashMap<String, String>());

try {
testFilter.doFilter(mockReq, mockResp, mockFilterChain);
verify(mockResp).sendError(HttpServletResponse.SC_BAD_REQUEST, "All requests must have 'X-Requested-With' header");
RequestedWithFilter filter = new RequestedWithFilter();
filter.filter(mockCtx);
ArgumentCaptor<Response> responseCaptor =
ArgumentCaptor.forClass(Response.class);
verify(mockCtx).abortWith(responseCaptor.capture());
Response response = responseCaptor.getValue();
assertEquals(400, response.getStatus());
assertEquals("All requests must have 'X-Requested-With' header", response.getEntity());
} catch (Exception e) {
e.printStackTrace();
}
Expand All @@ -52,15 +65,15 @@ public void testDoFilterRequestedWithTrue() {
public void testDoFilterRequestedWithFalse() {

mirthProperties.setProperty("server.api.require-requested-with", "false");
RequestedWithFilter testFilter = new RequestedWithFilter(mirthProperties);

HttpServletRequest mockReq = Mockito.mock(HttpServletRequest.class);
HttpServletResponse mockResp = Mockito.mock(HttpServletResponse.class);
FilterChain mockFilterChain = Mockito.mock(FilterChain.class);

RequestedWithFilter.configure(mirthProperties);

ContainerRequestContext mockCtx = Mockito.mock(ContainerRequestContext.class);
when(mockCtx.getHeaders()).thenReturn(new javax.ws.rs.core.MultivaluedHashMap<String, String>());

try {
testFilter.doFilter(mockReq, mockResp, mockFilterChain);
verify(mockResp, never()).sendError(HttpServletResponse.SC_BAD_REQUEST, "All requests must have 'X-Requested-With' header");
RequestedWithFilter filter = new RequestedWithFilter();
filter.filter(mockCtx);
verify(mockCtx, never()).abortWith(ArgumentMatchers.any(javax.ws.rs.core.Response.class));
} catch (Exception e) {
e.printStackTrace();
}
Expand Down