Skip to content

Commit e8c1677

Browse files
authored
πŸ”’οΈ Enhance XML security (#15)
- πŸ”’οΈ Enhance XML security - ✨ Refactor HTTP response handling - πŸ“„ Add detailed HTTP endpoints documentation to README
1 parent ba8cecc commit e8c1677

File tree

13 files changed

+359
-39
lines changed

13 files changed

+359
-39
lines changed

β€Ž.dockerignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
**/.DS_Store
2+
**/LICENSE
3+
**/*.md

β€Ž.github/workflows/publish.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,4 @@ jobs:
3535
run: gradle shadowJar
3636

3737
- name: Build images
38-
run: docker buildx build --platform=linux/amd64 --platform=linux/arm64 -t easybill/e-invoice-visualizer:${{github.ref_name}} -t easybill/e-invoice-visualizer:latest . --push
38+
run: docker buildx build --platform=linux/amd64 --platform=linux/arm64 -t easybill/e-invoice-visualizer:${{github.ref_name}} -t easybill/e-invoice-visualizer:latest . --push

β€ŽDockerfile

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
FROM eclipse-temurin:21-alpine
22

3+
RUN adduser -H -D xrviz xrviz
4+
USER xrviz
5+
36
WORKDIR /app
47

5-
COPY build/libs/xrviz.jar /app/xrviz.jar
6-
COPY data /app/data
8+
COPY --chown=xrviz:xrviz build/libs/xrviz.jar /app/xrviz.jar
9+
COPY --chown=xrviz:xrviz data /app/data
710

811
EXPOSE 4000
912

10-
CMD ["java", "-jar", "/app/xrviz.jar"]
13+
CMD ["java", "-jar", "xrviz.jar"]

β€ŽREADME.md

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,79 @@
1+
12
# e-invoice-visualizer
23

34
![](version-badge.svg)
45

5-
CII-/XR-Visualizer. Outputs HTML and PDF.
6+
This microservice visualizes CII/UBL e-invoices by converting them into HTML or PDF formats. It provides three main HTTP endpoints to interact with the service.
7+
8+
## HTTP Endpoints
9+
10+
### 1. Health Check
11+
12+
Checks the health status of the service.
13+
14+
**Endpoint:**
15+
```
16+
GET /health
17+
```
18+
19+
**Example:**
20+
```sh
21+
curl -X GET http://localhost:8080/health
22+
```
23+
24+
### 2. Convert XML to HTML
25+
26+
Converts a given XML e-invoice to an HTML file.
27+
28+
**Endpoint:**
29+
```
30+
POST /convert.html
31+
```
32+
**Example response:**
33+
```json
34+
{
35+
"version": "0.1.10 (ba8cecca)",
36+
"freeMemory": 254288368,
37+
"totalMemory": 268435456,
38+
"uptime": 94859746,
39+
"uptimeString": "1d 2h 20m 59s"
40+
}
41+
```
42+
43+
**Headers:**
44+
- `Content-Type: application/xml`
45+
- `Accept-Language: <language_code>` (optional, e.g., `de` for German)
46+
Valid: `de` or `en`, default: `de`
47+
48+
**Example:**
49+
```sh
50+
curl -X POST http://localhost:8080/convert.html \
51+
-H "Content-Type: application/xml" \
52+
-H "Accept-Language: en" \
53+
--data-binary @path/to/your/e-invoice.xml
54+
```
55+
56+
### 3. Convert XML to PDF
57+
58+
Converts a given XML e-invoice to a PDF file.
59+
60+
**Endpoint:**
61+
```
62+
POST /convert.pdf
63+
```
64+
65+
**Headers:**
66+
- `Content-Type: application/xml`
67+
- `Accept-Language: <language_code>` (optional, e.g., `de` for German)
68+
Valid: `de` or `en`, default: `de`
69+
70+
**Example:**
71+
```sh
72+
curl -X POST http://localhost:8080/convert.pdf \
73+
-H "Content-Type: application/xml" \
74+
-H "Accept-Language: en" \
75+
--data-binary @path/to/your/e-invoice.xml
76+
```
677

778
## Build a new version
879

@@ -13,4 +84,4 @@ To build a new version follow these steps:
1384
3. In necessary, `make` to update external dependencies.
1485
4. Push changes to the repository.
1586
5. Create, review and merge a pull request.
16-
6. Create a new release in GitHub with the new version number as tag.
87+
6. Create a new release in GitHub with the new version number as tag.

β€Žgradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
currentVersion=0.1.10
1+
currentVersion=0.1.11
22
mainClassName=io.github.easybill.xrviz.App

β€Žsrc/main/java/io/github/easybill/xrviz/XslTransformer.java

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
import org.apache.fop.apps.*;
44
import org.xml.sax.InputSource;
55
import org.xml.sax.SAXException;
6+
import org.xml.sax.XMLReader;
67

7-
import javax.xml.transform.Source;
8+
import javax.xml.parsers.SAXParser;
9+
import javax.xml.parsers.SAXParserFactory;
810
import javax.xml.transform.Transformer;
911
import javax.xml.transform.TransformerException;
1012
import javax.xml.transform.TransformerFactory;
@@ -30,6 +32,22 @@ public class XslTransformer {
3032
public static final String UBL_I_VALIDATION_STRING = "Invoice";
3133
public static final String UBL_C_VALIDATION_STRING = "CreditNote";
3234
public static final Pattern REGEX = Pattern.compile("[<:](CrossIndustryInvoice|Invoice|CreditNote)");
35+
private static final XMLReader xmlReader;
36+
37+
static {
38+
try {
39+
SAXParserFactory factory = SAXParserFactory.newInstance();
40+
SAXParser saxParser = factory.newSAXParser();
41+
xmlReader = saxParser.getXMLReader();
42+
xmlReader.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
43+
xmlReader.setFeature("http://xml.org/sax/features/external-general-entities", false);
44+
xmlReader.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
45+
} catch (Exception e) {
46+
logger.log(Level.SEVERE, "Error initializing XMLReader", e);
47+
throw new RuntimeException(e);
48+
}
49+
}
50+
3351

3452
enum DocumentType {
3553
CII("cii-xr.xsl"),
@@ -91,9 +109,9 @@ private static DOMSource transformXmlToXr(String inputXml, DocumentType type) th
91109
TransformerFactory factory = TransformerFactory.newInstance();
92110
StreamSource source = new StreamSource("data/xsl/" + type.getXslName());
93111
Transformer transformer = factory.newTransformer(source);
94-
Source xml = new StreamSource(new StringReader(inputXml));
112+
SAXSource saxSource = new SAXSource(xmlReader, new InputSource(new StringReader(inputXml)));
95113
DOMResult domResult = new DOMResult();
96-
transformer.transform(xml, domResult);
114+
transformer.transform(saxSource, domResult);
97115
return new DOMSource(domResult.getNode());
98116
}
99117

β€Žsrc/main/java/io/github/easybill/xrviz/handler/HtmlHandler.java

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import javax.xml.transform.TransformerException;
88
import java.io.IOException;
9+
import java.net.HttpURLConnection;
910
import java.nio.charset.StandardCharsets;
1011

1112
public class HtmlHandler extends XmlRequestExtractor implements HttpHandler {
@@ -19,16 +20,12 @@ public void handle(HttpExchange exchange) throws IOException {
1920
return;
2021
}
2122

22-
byte[] response = XslTransformer.transformToHtml(xml.get(), getLanguage(exchange)).getBytes(StandardCharsets.UTF_8);
23-
24-
exchange.getResponseHeaders().set("Content-Type", "text/html");
25-
exchange.sendResponseHeaders(200, response.length);
26-
exchange.getResponseBody().write(response);
27-
exchange.getResponseBody().close();
23+
sendResponse(exchange,
24+
XslTransformer.transformToHtml(xml.get(), getLanguage(exchange)).getBytes(StandardCharsets.UTF_8),
25+
HttpURLConnection.HTTP_OK, "text/html");
2826

2927
} catch (TransformerException e) {
30-
exchange.sendResponseHeaders(500, -1);
31-
28+
sendResponse(exchange, e.getMessage().getBytes(), HttpURLConnection.HTTP_BAD_REQUEST, "text/plain");
3229
logger.severe("Error while transforming XML to HTML: " + e.getMessage());
3330
}
3431
}

β€Žsrc/main/java/io/github/easybill/xrviz/handler/PdfHandler.java

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import javax.xml.transform.TransformerException;
99
import java.io.IOException;
10+
import java.net.HttpURLConnection;
1011

1112
public class PdfHandler extends XmlRequestExtractor implements HttpHandler {
1213
@Override
@@ -19,17 +20,13 @@ public void handle(HttpExchange exchange) throws IOException {
1920
return;
2021
}
2122

22-
byte[] response = XslTransformer.transformToPdf(xml.get(), getLanguage(exchange));
23-
24-
exchange.getResponseHeaders().set("Content-Type", "application/pdf");
25-
exchange.sendResponseHeaders(200, response.length);
26-
exchange.getResponseBody().write(response);
27-
exchange.getResponseBody().close();
23+
sendResponse(exchange,
24+
XslTransformer.transformToPdf(xml.get(), getLanguage(exchange)),
25+
HttpURLConnection.HTTP_OK, "application/pdf");
2826

2927
} catch (TransformerException | SAXException e) {
30-
exchange.sendResponseHeaders(500, -1);
31-
3228
logger.severe("Error while transforming XML to PDF: " + e.getMessage());
29+
sendResponse(exchange, e.getMessage().getBytes(), HttpURLConnection.HTTP_BAD_REQUEST, "text/plain");
3330
}
3431
}
3532
}

β€Žsrc/main/java/io/github/easybill/xrviz/handler/StatusHandler.java

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,10 @@
55
import io.github.easybill.xrviz.Config;
66

77
import java.io.IOException;
8+
import java.net.HttpURLConnection;
89
import java.nio.charset.StandardCharsets;
910

10-
import static io.github.easybill.xrviz.handler.XmlRequestExtractor.logger;
11-
12-
public class StatusHandler implements HttpHandler {
11+
public class StatusHandler extends XmlRequestExtractor implements HttpHandler {
1312

1413
public static final String JSON_RESPONSE = """
1514
{"version":"%s","freeMemory":%d,"totalMemory":%d,"uptime":%d,"uptimeString":"%s"}""";
@@ -29,13 +28,10 @@ public void handle(HttpExchange exchange) throws IOException {
2928
uptime.upStr()
3029
).getBytes(StandardCharsets.UTF_8);
3130

32-
exchange.getResponseHeaders().set("Content-Type", "application/json");
33-
exchange.sendResponseHeaders(200, response.length);
34-
exchange.getResponseBody().write(response);
35-
exchange.getResponseBody().close();
31+
sendResponse(exchange, response, HttpURLConnection.HTTP_OK, "application/json");
3632
} else {
3733
logger.warning("Wrong method request for health check: " + exchange.getRequestMethod());
38-
exchange.sendResponseHeaders(405, -1); // 405 Method Not Allowed
34+
exchange.sendResponseHeaders(HttpURLConnection.HTTP_BAD_METHOD, -1);
3935
}
4036

4137
}

β€Žsrc/main/java/io/github/easybill/xrviz/handler/XmlRequestExtractor.java

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ Optional<String> validate(HttpExchange exchange) throws IOException {
3232
String encoding = detector.getDetectedCharset();
3333
detector.reset();
3434

35-
Charset charset = null;
35+
Charset charset;
3636
try {
3737
charset = (encoding != null) ? Charset.forName(encoding) : StandardCharsets.UTF_8;
3838
} catch (IllegalArgumentException e) {
@@ -60,7 +60,18 @@ Optional<String> validate(HttpExchange exchange) throws IOException {
6060

6161
String getLanguage(HttpExchange exchange) {
6262
String acceptLanguage = exchange.getRequestHeaders().getFirst("Accept-Language");
63-
return acceptLanguage != null && acceptLanguage.toLowerCase().contains("en") ? "en" : "de";
63+
return acceptLanguage != null && acceptLanguage.toLowerCase().startsWith("en") ? "en" : "de";
64+
}
65+
66+
void sendResponse(HttpExchange exchange, byte[] response, int responseCode, String contentType) {
67+
try {
68+
exchange.getResponseHeaders().set("Content-Type", contentType);
69+
exchange.sendResponseHeaders(responseCode, response.length);
70+
exchange.getResponseBody().write(response);
71+
exchange.getResponseBody().close();
72+
} catch (IOException e) {
73+
logger.severe("Error while sending response: " + e.getMessage());
74+
}
6475
}
6576

6677
private boolean isXMLValid(String xml) {

β€Žsrc/test/http/api-test.http

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,3 +94,16 @@ Content-Type: application/xml
9494
Accept-Language: de
9595

9696
< ./broken-encoding.xml
97+
98+
### Generate a HTML file from a XML with XEE
99+
POST {{baseUrl}}/convert.html
100+
Content-Type: application/xml
101+
Accept-Language: de
102+
103+
< ./base-example-utf8-xee.xml
104+
> {%
105+
client.test("Request executed with 400", function () {
106+
client.assert(response.status === 400, "Response status is not 400");
107+
client.assert(response.body.toString().includes('SXXP0003'), "Response body does not contain SXXP0003");
108+
});
109+
%}

0 commit comments

Comments
Β (0)