Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
12278c4
feat(version-history): add HTTPS/PAT authentication support
thaitraninnovar Apr 25, 2026
e57b4dc
feat(version-history): add Pull/Push/Reload actions to GitStatusTabPanel
thaitraninnovar Apr 25, 2026
aa8a1a2
Adds an optional useStorageStats boolean parameter (default false)
thaitraninnovar Apr 28, 2026
2df960d
chore(release): bump version to 26.3.1
Innovarzweng Apr 28, 2026
d9b34bd
fix(security): suppress privilege-check log when running as normal user
Innovarzweng Apr 28, 2026
a2024b6
fix(custom-extensions): add 26.3.0 back to mirthVersion compatibility…
Innovarzweng Apr 28, 2026
579b613
fix(security): suppress privilege-check log when no_new_privs is active
Innovarzweng Apr 29, 2026
3d4c9ae
fix(http): prevent static resource requests from creating channel mes…
Innovarzweng May 4, 2026
4720940
Merge remote-tracking branch 'origin/update-version-history' into fea…
Innovarzweng May 4, 2026
56b1264
test(IRT-577): add KeystorePasswordRegenerationTest, fix KeystoreWarn…
Innovarzweng May 8, 2026
e109647
Merge remote-tracking branch 'origin/bridgelink_development' into fea…
Innovarzweng May 8, 2026
4387e01
fix(IRT-577): align checkbox with message text in KeystoreWarningDial…
Innovarzweng May 8, 2026
ad37cfb
test(IRT-577): add regenerateKeystorePassword controller tests to Def…
Innovarzweng May 8, 2026
e046bc4
fix(IRT-776): set Derby upgrade=false in mirth.properties
Innovarzweng May 8, 2026
29910fc
chore: remove Windows installer firstLogin.password comment from mirt…
Innovarzweng May 11, 2026
0b72a86
fix(IRT-828): set Content-Length on static resources before writing
Innovarzweng May 12, 2026
5309ed8
fix(IRT-831): enforce contextPath routing in HttpReceiver via Context…
Innovarzweng May 13, 2026
ae83b2a
fix(IRT-832): set Content-Length in HttpReceiver sendResponse and sen…
Innovarzweng May 13, 2026
edaa0b8
fix(IRT-833): set Content-Length on InstallerFileHandler downloads
Innovarzweng May 13, 2026
714b990
fix(IRT-834): set Content-Length on Swagger UI static file responses
Innovarzweng May 13, 2026
d37db71
fix(IRT-835): set Content-Length on WebStart JNLP responses
Innovarzweng May 13, 2026
bf77a3b
fix(IRT-831): fall through to channel handler when static resource fi…
Innovarzweng May 13, 2026
4536db6
fix(IRT-831): embed DIRECTORY handlers in channel context for fallthr…
Innovarzweng May 13, 2026
a200466
fix(IRT-831): extend resource fallthrough to CUSTOM and FILE types
Innovarzweng May 14, 2026
bf386f8
test(IRT-831): add GROUP E fallthrough tests for CUSTOM and FILE reso…
Innovarzweng May 14, 2026
4554d7f
test(IRT-834): add SwaggerUiFilter unit tests for Content-Length on s…
Innovarzweng May 14, 2026
5a97395
chore: remove OIE workflow file from BridgeLink repo
Innovarzweng May 14, 2026
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
42 changes: 0 additions & 42 deletions .github/workflows/build.yaml

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -35,19 +35,18 @@ public KeystoreWarningDialog(Window owner) {
private void initComponents() {
setDefaultCloseOperation(javax.swing.WindowConstants.DISPOSE_ON_CLOSE);

JLabel iconLabel = new JLabel(UIManager.getIcon("OptionPane.warningIcon"));
javax.swing.Icon warningIcon = UIManager.getIcon("OptionPane.warningIcon");
JLabel iconLabel = new JLabel(warningIcon);
int iconWidth = (warningIcon != null) ? warningIcon.getIconWidth() : 32;

JLabel messageLabel = new JLabel(
"<html><b>The keystore passwords for this BridgeLink instance are still set to "
+ "default values.</b><br><br>"
+ "The keystore stores the SSL/TLS certificate for the BridgeLink API and<br>"
+ "Administrator (port 8443).<br><br>"
+ "<b>Note:</b> If any channels have message-level encryption enabled<br>"
+ "(encryptData=true), those stored messages will become unreadable after<br>"
+ "regeneration. Channels will continue to process new messages normally.<br><br>"
+ "<b>Note:</b> If encrypt.properties=true in mirth.properties, the encrypted<br>"
+ "database.password will also become unreadable after regeneration.<br>"
+ "This setting defaults to false in most installations.<br><br>"
+ "<b>Note:</b> Channel message encryption (encryptData=true) and encrypted<br>"
+ "database passwords (encrypt.properties=true) are NOT affected &mdash; they<br>"
+ "use a separate AES key that is preserved during regeneration.<br><br>"
+ "<b>Note:</b> BridgeLink must be restarted after regenerating the keystore<br>"
+ "for the new SSL certificate to take effect.</html>");

Expand Down Expand Up @@ -87,7 +86,9 @@ public void actionPerformed(ActionEvent evt) {
.addGroup(layout.createSequentialGroup()
.addComponent(iconLabel)
.addComponent(messageLabel))
.addComponent(regenerateCheckBox)
.addGroup(layout.createSequentialGroup()
.addGap(iconWidth + 6)
.addComponent(regenerateCheckBox))
.addComponent(buttonPanel, javax.swing.GroupLayout.DEFAULT_SIZE,
javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
);
Expand Down
5 changes: 1 addition & 4 deletions server/conf/mirth.properties
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,6 @@ dir.tempdata = ${dir.appdata}/temp
http.port = 8080
https.port = 8443

# Windows installer sets this for fresh installs; leave commented for upgrades.
# default.firstLogin.password =

# password requirements
password.minlength = 8
password.minupper = 1
Expand Down Expand Up @@ -89,7 +86,7 @@ database = derby
# SQL Server/Sybase (jTDS) jdbc:jtds:sqlserver://localhost:1433/mirthdb
# Microsoft SQL Server jdbc:sqlserver://localhost:1433;databaseName=mirthdb
# If you are using the Microsoft SQL Server driver, please also specify database.driver below
database.url = jdbc:derby:${dir.appdata}/mirthdb;create=true;upgrade=true
database.url = jdbc:derby:${dir.appdata}/mirthdb;create=true;upgrade=false

# If using a custom or non-default driver, specify it here.
# example:
Expand Down
124 changes: 97 additions & 27 deletions server/src/com/mirth/connect/connectors/http/HttpReceiver.java
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,14 @@ public void onStart() throws ConnectorTaskException {
server = new Server();
configuration.configureReceiver(this);

// All static resource handlers are embedded inside the channel ContextHandler rather
// than given their own separate ContextHandlers. CoreContextHandler.handle() returns
// true once the request path matches the context prefix — even when the inner EE8
// handler never sets isHandled(). A separate ContextHandler per resource therefore
// blocks fallthrough to the channel for any sub-path the resource cannot serve
// (e.g. GET /test/data/wrong when the resource is the CUSTOM type at /test/data).
HandlerCollection handlers = new HandlerCollection();
List<org.eclipse.jetty.ee8.nested.Handler> resourceHandlers = new ArrayList<>();

// Add handlers for each static resource
if (getConnectorProperties().getStaticResources() != null) {
Expand Down Expand Up @@ -278,33 +285,67 @@ public void onStart() throws ConnectorTaskException {
for (List<HttpStaticResource> staticResourcesList : staticResourcesMap.descendingMap().values()) {
for (HttpStaticResource staticResource : staticResourcesList) {
logger.debug("Adding static resource handler for context path: " + staticResource.getContextPath());
ContextHandler resourceContextHandler = new ContextHandler();
resourceContextHandler.setContextPath(staticResource.getContextPath());
// This allows resources to be requested without a relative context path (e.g. "/")
resourceContextHandler.setAllowNullPathInfo(true);
resourceContextHandler.setHandler(new StaticResourceHandler(staticResource));
handlers.addHandler(resourceContextHandler);
org.eclipse.jetty.ee8.nested.Handler resourceInner = new StaticResourceHandler(staticResource);
if (authenticatorProvider != null) {
resourceInner = createSecurityHandler(resourceInner);
}
// Collected here; embedded inside the channel context below.
resourceHandlers.add(resourceInner);
}
}
}

// Add the main request handler
// Build the channel context handler. All static resource handlers are placed
// before RequestHandler inside a stop-on-first-handled HandlerCollection so that
// any request the resource handler cannot serve falls through to the channel.
// Using HandlerCollection (not an anonymous AbstractHandler) ensures Jetty's
// lifecycle methods (setServer, start) propagate to all inner handlers via addHandler().
ContextHandler contextHandler = new ContextHandler();
contextHandler.setContextPath(contextPath);
contextHandler.setHandler(new RequestHandler());
contextHandler.setAllowNullPathInfo(true);
org.eclipse.jetty.ee8.nested.Handler channelBase = new RequestHandler();
if (authenticatorProvider != null) {
channelBase = createSecurityHandler(channelBase);
}
if (resourceHandlers.isEmpty()) {
contextHandler.setHandler(channelBase);
} else {
final org.eclipse.jetty.ee8.nested.Handler finalChannelBase = channelBase;
HandlerCollection resourceChain = new HandlerCollection() {
@Override
public void handle(String target, Request baseRequest,
HttpServletRequest servletRequest, HttpServletResponse servletResponse)
throws IOException, ServletException {
for (org.eclipse.jetty.ee8.nested.Handler h : getHandlers()) {
h.handle(target, baseRequest, servletRequest, servletResponse);
if (baseRequest.isHandled()) {
return;
}
}
}
};
for (org.eclipse.jetty.ee8.nested.Handler h : resourceHandlers) {
resourceChain.addHandler(h);
}
resourceChain.addHandler(finalChannelBase);
contextHandler.setHandler(resourceChain);
}
handlers.addHandler(contextHandler);

// Wrap the handler collection in a security handler if needed
org.eclipse.jetty.ee8.nested.Handler serverHandler = handlers;
if (authenticatorProvider != null) {
serverHandler = createSecurityHandler(handlers);
org.eclipse.jetty.server.Handler.Sequence handlerSequence =
new org.eclipse.jetty.server.Handler.Sequence();
for (org.eclipse.jetty.ee8.nested.Handler h : handlers.getHandlers()) {
if (h instanceof ContextHandler ch) {
ch.setServer(server);
handlerSequence.addHandler(ch.getCoreContextHandler());
}
}

// In Jetty 12, we need to wrap the EE8 handler in a ContextHandler and get the core handler
ContextHandler rootContextHandler = new ContextHandler();
rootContextHandler.setContextPath("/");
rootContextHandler.setHandler(serverHandler);
server.setHandler(rootContextHandler.getCoreContextHandler());
org.eclipse.jetty.server.handler.DefaultHandler defaultHandler =
new org.eclipse.jetty.server.handler.DefaultHandler();
defaultHandler.setServeFavIcon(false);
defaultHandler.setShowContexts(false);
handlerSequence.addHandler(defaultHandler);
server.setHandler(handlerSequence);

logger.debug("starting HTTP server with address: " + host + ":" + port);
server.start();
Expand Down Expand Up @@ -346,6 +387,10 @@ protected String getConfigurationClass() {
private class RequestHandler extends AbstractHandler {
@Override
public void handle(String target, Request baseRequest, HttpServletRequest servletRequest, HttpServletResponse servletResponse) throws IOException, ServletException {
// Skip if a static resource handler already served this request (Jetty 12 EE8 ContextHandler no longer guards on isHandled())
if (baseRequest.isHandled()) {
return;
}
logger.debug("received HTTP request");
eventController.dispatchEvent(new ConnectionStatusEvent(getChannelId(), getMetaDataId(), getSourceName(), ConnectionStatusEventType.CONNECTED));
DispatchResult dispatchResult = null;
Expand Down Expand Up @@ -514,6 +559,7 @@ protected void sendResponse(Request baseRequest, HttpServletResponse servletResp
gzipOutputStream.write(responseBytes);
gzipOutputStream.finish();
} else {
servletResponse.setContentLength(responseBytes.length);
responseOutputStream.write(responseBytes);
}

Expand Down Expand Up @@ -546,9 +592,11 @@ protected void sendErrorResponse(Request baseRequest, HttpServletResponse servle
}
}

byte[] errBytes = responseError.getBytes();
servletResponse.setContentType("text/plain");
servletResponse.setStatus(HttpStatus.SC_INTERNAL_SERVER_ERROR);
servletResponse.getOutputStream().write(responseError.getBytes());
servletResponse.setContentLength(errBytes.length);
servletResponse.getOutputStream().write(errBytes);
}

protected Object getMessage(Request request, Map<String, Object> sourceMap, List<Attachment> attachments) throws IOException, ChannelException, MessagingException, DonkeyElementException, ParserConfigurationException {
Expand Down Expand Up @@ -679,6 +727,7 @@ public void handle(String target, Request baseRequest, HttpServletRequest servle

String originalThreadName = Thread.currentThread().getName();

boolean responded = false;
try {
Thread.currentThread().setName("HTTP Receiver Thread on " + getChannel().getName() + " (" + getChannelId() + ") < " + originalThreadName);
HttpRequestMessage requestMessage = createRequestMessage(baseRequest, true);
Expand Down Expand Up @@ -724,12 +773,14 @@ public void handle(String target, Request baseRequest, HttpServletRequest servle
OutputStream responseOutputStream = servletResponse.getOutputStream();

// If the client accepts GZIP compression, compress the content
boolean gzipOutput = false;
List<String> acceptEncodingList = requestMessage.getCaseInsensitiveHeaders().get("Accept-Encoding");
if (CollectionUtils.isNotEmpty(acceptEncodingList)) {
for (String acceptEncoding : acceptEncodingList) {
if (acceptEncoding != null && acceptEncoding.contains("gzip")) {
servletResponse.setHeader(HTTP.CONTENT_ENCODING, "gzip");
responseOutputStream = new GZIPOutputStream(responseOutputStream);
gzipOutput = true;
break;
}
}
Expand All @@ -739,7 +790,11 @@ public void handle(String target, Request baseRequest, HttpServletRequest servle
// Just stream the file itself back to the client
InputStream is = null;
try {
is = new FileInputStream(value);
File f = new File(value);
if (!gzipOutput) {
servletResponse.setContentLengthLong(f.length());
}
is = new FileInputStream(f);
IOUtils.copy(is, responseOutputStream);
} finally {
ResourceUtil.closeResourceQuietly(is);
Expand Down Expand Up @@ -787,6 +842,9 @@ public void handle(String target, Request baseRequest, HttpServletRequest servle
// A valid file was found; stream it back to the client
InputStream is = null;
try {
if (!gzipOutput) {
servletResponse.setContentLengthLong(file.length());
}
is = new FileInputStream(file);
IOUtils.copy(is, responseOutputStream);
} finally {
Expand All @@ -799,26 +857,38 @@ public void handle(String target, Request baseRequest, HttpServletRequest servle
}
} else {
// Stream the value string back to the client
IOUtils.write(value, responseOutputStream, charset);
byte[] valueBytes = value.getBytes(charset);
if (!gzipOutput) {
servletResponse.setContentLength(valueBytes.length);
}
responseOutputStream.write(valueBytes);
}

// If we gzipped, we need to finish the stream now
if (responseOutputStream instanceof GZIPOutputStream) {
if (gzipOutput) {
((GZIPOutputStream) responseOutputStream).finish();
}
responded = true;
} catch (Throwable t) {
logger.error("Error handling static HTTP resource request (" + getConnectorProperties().getName() + " \"Source\" on channel " + getChannelId() + ").", t);
eventController.dispatchEvent(new ErrorEvent(getChannelId(), getMetaDataId(), null, ErrorEventType.SOURCE_CONNECTOR, getSourceName(), getConnectorProperties().getName(), "Error handling static HTTP resource request", t));

servletResponse.reset();
servletResponse.setContentType(ContentType.TEXT_PLAIN.toString());
servletResponse.setStatus(HttpStatus.SC_INTERNAL_SERVER_ERROR);
servletResponse.getOutputStream().write(ExceptionUtils.getStackTrace(t).getBytes());
try {
servletResponse.reset();
servletResponse.setContentType(ContentType.TEXT_PLAIN.toString());
servletResponse.setStatus(HttpStatus.SC_INTERNAL_SERVER_ERROR);
servletResponse.getOutputStream().write(ExceptionUtils.getStackTrace(t).getBytes());
} catch (IllegalStateException ise) {
logger.debug("Could not reset already-committed response after static resource error", ise);
}
responded = true;
} finally {
Thread.currentThread().setName(originalThreadName);
}

baseRequest.setHandled(true);
if (responded) {
baseRequest.setHandled(true);
}
}
}

Expand Down
Loading