Skip to content
Closed
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ pom.xml.versionsBackup
**/.settings
**/*.class
.metadata/*
"Server/OPENAS2_LOG_DIR_IS_UNDEFINED/*.txt"
206 changes: 183 additions & 23 deletions Server/src/main/java/org/openas2/cmd/processor/restapi/ApiResource.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,48 +5,76 @@
*/
package org.openas2.cmd.processor.restapi;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import org.openas2.cert.AliasedCertificateFactory;
import org.openas2.cert.CertificateFactory;
import org.openas2.cmd.CommandResult;
import org.openas2.cmd.processor.RestCommandProcessor;

import jakarta.annotation.security.RolesAllowed;
import jakarta.ws.rs.Consumes;

import jakarta.ws.rs.DefaultValue;


import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.HEAD;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.HEAD;


import jakarta.ws.rs.PathParam;

import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.Context;

import jakarta.ws.rs.core.MultivaluedMap;
import jakarta.ws.rs.core.Request;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.UriInfo;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.MultivaluedMap;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.StringWriter;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;


import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;



import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathFactory;

import org.glassfish.grizzly.http.server.Request;

import org.openas2.cert.AliasedCertificateFactory;
import org.openas2.cert.CertificateFactory;
import org.openas2.cmd.CommandResult;
import org.openas2.cmd.processor.RestCommandProcessor;
import org.openas2.Session;
import org.openas2.util.Properties;

import org.slf4j.LoggerFactory;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;



/**
* @author javier
*/
Expand All @@ -70,12 +98,11 @@ public static void setProcessor(RestCommandProcessor aProcessor) {
private static RestCommandProcessor processor;
@Context
UriInfo ui;
@Context
Request request;
private final ObjectMapper mapper;

public ApiResource() {

mapper = new ObjectMapper();
// enable pretty printing
mapper.enable(SerializationFeature.INDENT_OUTPUT);
Expand Down Expand Up @@ -220,6 +247,139 @@ public Response headCommand(@PathParam("param") String command) {
return Response.status(200).build();
}

@GET
@RolesAllowed({"ADMIN"})
@Path("/getPropertyList")
@Produces(MediaType.APPLICATION_JSON)
public Response getPropertyList(@Context Request request) {
if (!request.isSecure() && !isLocalhost(request)) {
return Response.status(Response.Status.FORBIDDEN)
.entity("{\"error\":\"SSL/TLS required\"}")
.type(MediaType.APPLICATION_JSON)
.build();
}
Map<String, String> result = new HashMap<>();
try {
result = (Map<String, String>) Properties.getProperties();

@uhurusurfa uhurusurfa Aug 27, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Note that this returns the parsed properties not the properties as set in the config.xml proerties section or the properties file.
If any properties use property references or envoronment variable references within their value then those references will have been replaced with actual values.
Based off the comment in your previous PR, you are using this for administration and debugging.
Are you intending to use this in a UI that allows you to change properties?

There are extensive debug logging statements that support figuring out issuses with properties if you set TRACE level logging.

Note also that there are a number of passwords in the properties list and passwords should not be returned in the list as it is a security risk. You will have to filter out password properties.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes, intended usage is primarily for administration and debugging. Regarding the passwords, would redacting properties before returning the properties address the concern? I can implement that to ensure sensitive information is not exposed

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Yes - if you remove any property that has the word "password" in it's property name or property value that should work.
These are the currently known properties that have password information:

               as2_keystore_password="testas2"
               ssl_trust_keystore_password="testas2"
               ssl_keystore_password="testas2"
               restapi.command.processor.password="pWd"
               module.HealthCheckModule.keystore_password="$properties.ssl_keystore_password$"
               msg_tracking.tcp_server_password="openas2"

}catch(Exception ex) {
LoggerFactory.getLogger(ApiResource.class.getName()).error(ex.getMessage(), ex);
throw ex;
}
ObjectMapper om = new ObjectMapper();
try {
String js = om.writeValueAsString(result);
return Response.ok(js, MediaType.APPLICATION_JSON).build();
} catch (JsonProcessingException e) {
return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity("error").type(MediaType.APPLICATION_JSON).build();
}
}

@GET
@RolesAllowed({"ADMIN"})
@Path("/getXml")
@Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON})
public Response getXml(@Context Request request,
@QueryParam("filename") String filename,
@QueryParam("xpath") String xpathExpression) {

if (!request.isSecure() && !isLocalhost(request)) {
return Response.status(Response.Status.FORBIDDEN)
.entity("{\"error\":\"SSL/TLS required\"}")
.type(MediaType.APPLICATION_JSON)
.build();
}


Set<String> allowedXPaths = Set.of(
"/partnerships/partner",
"/partnerships/partner/name",
"/partnerships/*",
"*"
);

if (xpathExpression == null || !allowedXPaths.contains(xpathExpression)) {
return Response.status(Response.Status.BAD_REQUEST)
Comment thread Fixed
.entity("{\"error\":\"Invalid or disallowed XPath expression\"}")
.type(MediaType.APPLICATION_JSON)
.build();
}

Session session = getProcessor().getSession();
String filePath = session.getBaseDirectory() + "/" + filename;

@uhurusurfa uhurusurfa Aug 27, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Since there is no validation of the file path this could retrieve any XML file on the server and is a security risk.
Is there a reason why you need to pass a filename in at all since you are only interested in the partnerships file?

EDIT: I was too quick off the mark on this comment - I see it is "protected" by enforcing HTTPS and can therefore be retrieved from anywhere in the world but it will still return sensitive information such as passwords if they are hard coded into the config.xml file and since you do not restrict which files this API endpoint can acces it can return the config.xml file.


try {

DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); // prevent XXE
DocumentBuilder builder = factory.newDocumentBuilder();
Document document = builder.parse(new File(filePath));


XPath xpath = XPathFactory.newInstance().newXPath();
XPathExpression expr = xpath.compile(xpathExpression);
NodeList nodeList = (NodeList) expr.evaluate(document, XPathConstants.NODESET);


Document resultDocument = builder.newDocument();
Element root = resultDocument.createElement("results");
resultDocument.appendChild(root);

for (int i = 0; i < nodeList.getLength(); i++) {
Node importedNode = resultDocument.importNode(nodeList.item(i), true);
root.appendChild(importedNode);
}

// Convert to string
Transformer transformer = TransformerFactory.newInstance().newTransformer();
StringWriter stringWriter = new StringWriter();
transformer.transform(new DOMSource(resultDocument), new StreamResult(stringWriter));

return Response.ok(stringWriter.toString(), MediaType.APPLICATION_XML).build();

} catch (Exception exception) {
LoggerFactory.getLogger(ApiResource.class.getName()).error(
"Error processing XML file: " + filePath, exception);
return Response.serverError()
.entity("{\"error\":\"Internal server error\"}")
.type(MediaType.APPLICATION_JSON)
.build();
}
}



private static boolean isLocalhost(Request request) {
boolean isLocalhost = request.getRemoteAddr().equals("127.0.0.1") || request.getRemoteAddr().equals("::1");
return isLocalhost;
}

private NodeList getNodes(String xmlFileName, String xpathExpression) {
NodeList nodeList = null;
try {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();

// === XXE Protection ===
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
dbf.setXIncludeAware(false);
dbf.setExpandEntityReferences(false);

DocumentBuilder db = dbf.newDocumentBuilder();
File file = new File(xmlFileName);
Document document = db.parse(file);

XPathExpression xPathExpr = XPathFactory.newInstance().newXPath().compile(xpathExpression);
nodeList = (NodeList) xPathExpr.evaluate(document, XPathConstants.NODESET);

} catch (Exception ex) {
LoggerFactory.getLogger(ApiResource.class.getName()).error("Error parsing XML file: " + xmlFileName, ex);
// return null on error
}
return nodeList;
}
Comment on lines +356 to +381

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This appears to be a redundant method.
What is the reason this is here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The XML read here is used specifically to retrieve partnership endpoint information (polling location).
This allows the end-user UI to present available partnerships so that AS2 payloads can be placed in the appropriate directories.
Additionally, I intend to provide an API endpoint that can receive a partnership and an AS2 payload, and route the payload according to the partnership definitions.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This method is not called from anywhere so I am unclear why it is here unless you have other code that is not included in this PR.


private CommandResult importCertificateByStream(String itemId, MultivaluedMap<String, String> formParams) throws Exception {
try {
List<String> params = new ArrayList<String>();
Expand All @@ -245,7 +405,7 @@ private CommandResult importCertificateByStream(String itemId, MultivaluedMap<St
} catch (Exception ex) {
LoggerFactory.getLogger(ApiResource.class.getName()).error(ex.getMessage(), ex);
throw ex;
// return Response.status(506).entity( ex.getMessage()).build();

}
}

Expand Down
Loading