aboutsummaryrefslogtreecommitdiff
path: root/pdf-as-web/src
diff options
context:
space:
mode:
authorJakob Heher <jakob.heher@iaik.tugraz.at>2026-06-10 14:28:52 +0200
committerGitHub <noreply@github.com>2026-06-10 14:28:52 +0200
commit7a204f2c2520a875fa9c4166c243775e52f03e77 (patch)
tree99bc09ba6e79c84ee3eaeb181168291c730ff568 /pdf-as-web/src
parentafbe8f6aee8d7554b52aa4aa24561731715695da (diff)
downloadpdf-as-4-7a204f2c2520a875fa9c4166c243775e52f03e77.tar.gz
pdf-as-4-7a204f2c2520a875fa9c4166c243775e52f03e77.tar.bz2
pdf-as-4-7a204f2c2520a875fa9c4166c243775e52f03e77.zip
Connector url cleanup (#94)
* cleanup the URL in the BKUSLConnector/SL20Connector for pdf-as-web connector bkus (even though they are never used, as best as i can tell) * add a test case for redirection destination on formpost
Diffstat (limited to 'pdf-as-web/src')
-rw-r--r--pdf-as-web/src/main/java/at/gv/egiz/pdfas/web/helper/PdfAsHelper.java8
-rw-r--r--pdf-as-web/src/test/java/at/gv/egiz/pdfas/web/test/MockMoaSigningTest.java8
-rw-r--r--pdf-as-web/src/test/java/at/gv/egiz/pdfas/web/test/RealTomcatTests.java44
-rw-r--r--pdf-as-web/src/test/java/at/gv/egiz/pdfas/web/test/TestUtils.java146
4 files changed, 180 insertions, 26 deletions
diff --git a/pdf-as-web/src/main/java/at/gv/egiz/pdfas/web/helper/PdfAsHelper.java b/pdf-as-web/src/main/java/at/gv/egiz/pdfas/web/helper/PdfAsHelper.java
index 22178921..554d79d1 100644
--- a/pdf-as-web/src/main/java/at/gv/egiz/pdfas/web/helper/PdfAsHelper.java
+++ b/pdf-as-web/src/main/java/at/gv/egiz/pdfas/web/helper/PdfAsHelper.java
@@ -541,7 +541,7 @@ public class PdfAsHelper {
session.setAttribute(PDF_SL_INTERACTIVE, connector);
// prepare first document
- IPlainSigner signer = getSignerFromConnector(connector, config, session);
+ IPlainSigner signer = getSignerFromConnector(connector, session);
session.setAttribute(PDF_SIGNER, signer);
String qrCodeContent = PdfAsHelper.getQRCodeContent(request);
@@ -606,10 +606,10 @@ public class PdfAsHelper {
}
- private static IPlainSigner getSignerFromConnector(Connector connector, Configuration config, HttpSession session) throws PdfAsWebException {
+ private static IPlainSigner getSignerFromConnector(Connector connector, HttpSession session) throws PdfAsWebException {
val slConnector = switch(connector) {
- case BKU, ONLINEBKU, MOBILEBKU -> new BKUSLConnector(config);
- case SECLAYER20 -> new SL20Connector(config);
+ case BKU, ONLINEBKU, MOBILEBKU -> new BKUSLConnector(generateBKUURL(connector));
+ case SECLAYER20 -> new SL20Connector(WebConfiguration.getSecurityLayer20URL());
default -> throw new PdfAsWebException("Invalid connector (bku | onlinebku | mobilebku | sl20)");
};
session.setAttribute(PDF_SL_CONNECTOR, slConnector);
diff --git a/pdf-as-web/src/test/java/at/gv/egiz/pdfas/web/test/MockMoaSigningTest.java b/pdf-as-web/src/test/java/at/gv/egiz/pdfas/web/test/MockMoaSigningTest.java
index a94899e2..38adc050 100644
--- a/pdf-as-web/src/test/java/at/gv/egiz/pdfas/web/test/MockMoaSigningTest.java
+++ b/pdf-as-web/src/test/java/at/gv/egiz/pdfas/web/test/MockMoaSigningTest.java
@@ -71,17 +71,11 @@ public class MockMoaSigningTest extends TestUtils.CanWatchOperationCount {
return socket.getLocalPort();
}
}
- private static String azstring(int length) {
- return
- new Random().ints(97,123).limit(length)
- .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append)
- .toString();
- }
public final int port = freePort();
public final String endpointURL = "http://127.0.0.1:"+port+"/moa-spss/services/SignatureCreation";
public final Endpoint endpoint =
Endpoint.publish(endpointURL, this);
- public final String keyIdentifier = azstring(16);
+ public final String keyIdentifier = TestUtils.azstring(16);
public final IPlainSigner signer;
diff --git a/pdf-as-web/src/test/java/at/gv/egiz/pdfas/web/test/RealTomcatTests.java b/pdf-as-web/src/test/java/at/gv/egiz/pdfas/web/test/RealTomcatTests.java
index 534df72c..c6d4f0e7 100644
--- a/pdf-as-web/src/test/java/at/gv/egiz/pdfas/web/test/RealTomcatTests.java
+++ b/pdf-as-web/src/test/java/at/gv/egiz/pdfas/web/test/RealTomcatTests.java
@@ -38,26 +38,44 @@ public class RealTomcatTests {
@SneakyThrows
public void fileErrorOnNoDocument() {
byte[] pdf = IOUtils.toByteArray(RealTomcatTests.class.getResourceAsStream("/data/enc_own.pdf"));
- val boundary = "----TEST";
- val prefix = (
- "--"+boundary+"\r\nContent-Disposition: form-data; name=\"source\"\r\n\r\ninternal\r\n"+
- "--"+boundary+"\r\nContent-Disposition: form-data; name=\"connector\"\r\n\r\nmobilebku\r\n"+
- "--"+boundary+"\r\nContent-Disposition: form-data; name=\"pdf-file\"; filename=\"\"\r\nContent-Type: application/pdf\r\n\r\n"
- ).getBytes(StandardCharsets.UTF_8);
- val suffix = (
- "\r\n--"+boundary+"--\r\n"
- ).getBytes(StandardCharsets.UTF_8);
- val multipartBody = List.of(prefix, pdf, suffix);
+ val multipart = TestUtils.Multipart.builder()
+ .Value("source", "internal")
+ .Value("connector", "mobilebku")
+ .File("pdf-file", "", "application/pdf", pdf)
+ .build();
val client = HttpClient.newBuilder().followRedirects(HttpClient.Redirect.NEVER).build();
val request = HttpRequest.newBuilder()
.uri(URI.create("http://localhost:"+port+"/Sign"))
- .header("Content-Type", "multipart/form-data; boundary="+boundary)
- .POST(HttpRequest.BodyPublishers.ofByteArrays(multipartBody))
+ .header("Content-Type", multipart.getContentType())
+ .POST(HttpRequest.BodyPublishers.ofByteArrays(multipart.getBody()))
+ .build();
+
+ val response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
+ assertEquals(200, response.statusCode());
+ assertTrue("Should contain redirect to a-trust", response.body().contains("https://service.a-trust.at/mobile/https-security-layer-request"));
+ }
+
+ @Test
+ @SneakyThrows
+ public void externSignServletTest() {
+ byte[] pdf = IOUtils.toByteArray(RealTomcatTests.class.getResourceAsStream("/data/enc_own.pdf"));
+ val multipart = TestUtils.Multipart.builder()
+ .Value("connector", "mobilebku")
+ .Value("invoke-app-url", "http://foo.bar/success")
+ .Value("invoke-app-error-url", "http://foo.bar/error")
+ .File("pdf-file", "", "application/pdf", pdf)
+ .build();
+
+ val client = HttpClient.newBuilder().followRedirects(HttpClient.Redirect.ALWAYS).build();
+ val request = HttpRequest.newBuilder()
+ .uri(URI.create("http://localhost:"+port+"/Sign"))
+ .header("Content-Type", multipart.getContentType())
+ .POST(HttpRequest.BodyPublishers.ofByteArrays(multipart.getBody()))
.build();
val response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
assertEquals(200, response.statusCode());
- assertTrue("Should contain redirect to a-trust", response.body().contains("https-security-layer-request"));
+ assertTrue("Should contain redirect to a-trust", response.body().contains("https://service.a-trust.at/mobile/https-security-layer-request"));
}
}
diff --git a/pdf-as-web/src/test/java/at/gv/egiz/pdfas/web/test/TestUtils.java b/pdf-as-web/src/test/java/at/gv/egiz/pdfas/web/test/TestUtils.java
index 900b1f82..afab8fcf 100644
--- a/pdf-as-web/src/test/java/at/gv/egiz/pdfas/web/test/TestUtils.java
+++ b/pdf-as-web/src/test/java/at/gv/egiz/pdfas/web/test/TestUtils.java
@@ -3,17 +3,19 @@ package at.gv.egiz.pdfas.web.test;
import at.gv.egiz.pdfas.web.stats.impl.StatisticMicrometerBackend;
import com.jayway.jsonpath.JsonPath;
import io.micrometer.core.instrument.MeterRegistry;
+import lombok.SneakyThrows;
import lombok.val;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
-import java.util.Arrays;
-import java.util.List;
+import java.nio.charset.StandardCharsets;
+import java.util.*;
public class TestUtils {
private static double getOperationCount(MockMvc mvc, String... tags) throws Exception {
@@ -43,4 +45,144 @@ public class TestUtils {
return () -> Assertions.assertEquals(initialCount+1.0, TestUtils.getOperationCount(mvc, tags), 0.0001);
}
}
+
+ public static String azstring(int length) {
+ return
+ new Random().ints(97,123).limit(length)
+ .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append)
+ .toString();
+ }
+
+ // how is this not in lang.commons? anyway it's boyer-moore
+ private static boolean arrayContainsArray(byte[] haystack, byte[] needle) {
+ val n = haystack.length;
+ val m = needle.length;
+ val badCharTable = new int[256];
+ Arrays.fill(badCharTable, -1);
+ for (int i=0; i<needle.length; ++i) { badCharTable[needle[i] & 0xff] = i; }
+
+ int skip;
+ for (int i = 0; i <= n-m; i += skip) {
+ skip = 0;
+ for (int j = m-1; j >= 0; j--) {
+ if (needle[j] != haystack[i+j]) {
+ int badCharShift = j-badCharTable[haystack[i+j] & 0xff];
+ skip = Math.max(1, badCharShift);
+ break;
+ }
+ }
+ if (skip == 0) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Test
+ @SneakyThrows
+ public void arrayContainsArrayTest() {
+ Assertions.assertTrue(arrayContainsArray(new byte[] { 1, 3, 5, 4, 2 }, new byte[] { 5, 4 }));
+ Assertions.assertTrue(arrayContainsArray(new byte[] { 9, -5, 13, 42, 5 }, new byte[] { 42, 5 }));
+ Assertions.assertTrue(arrayContainsArray(new byte[] { 13, 86, 63, 51, -5 }, new byte[] { 13, 86 }));
+ Assertions.assertTrue(arrayContainsArray(new byte[] { 9, -5, 21, 42, 3 }, new byte[] { 9, -5, 21, 42, 3 }));
+ Assertions.assertFalse(arrayContainsArray(new byte[] { 1, 3, 5, 4, 2 }, new byte[] { 2, 1 }));
+ Assertions.assertFalse(arrayContainsArray(new byte[] { 9, 8, 4, 6, 2 }, new byte[] { 5 }));
+ Assertions.assertFalse(arrayContainsArray(new byte[] { 42, 41, 40 }, new byte[] { 40, 41 }));
+ Assertions.assertFalse(arrayContainsArray(new byte[] { 40, 41 }, new byte[] { 41, 40 }));
+ }
+
+ public sealed interface Multipart permits Multipart.Value, Multipart.File {
+ String getKey();
+ @lombok.Value
+ class Value implements Multipart {
+ String key;
+ String value;
+ }
+
+ @lombok.Value
+ class File implements Multipart {
+ String key;
+ String filename;
+ String contentType;
+ byte[] contents;
+ }
+
+ public static class Builder {
+ private Builder() {}
+ private final ArrayList<Multipart> parts = new ArrayList<>();
+
+ public Builder Value(String key, String value) {
+ parts.add(new Value(key, value));
+ return this;
+ }
+
+ public Builder File(String key, String filename, String contentType, byte[] contents) {
+ parts.add(new File(key, filename, contentType, contents));
+ return this;
+ }
+
+ public Multipart.Body build() {
+ return buildMultipartBody(parts.toArray(new Multipart[0]));
+ }
+ }
+
+ static Builder builder() { return new Builder(); }
+
+ @lombok.Value
+ class Body {
+ String contentType;
+ Iterable<byte[]> body;
+ }
+ }
+
+ private static String findMultipartBoundary(Multipart[] parts) {
+ while (true) {
+ val boundaryCandidate = "----"+azstring(32);
+ val candidateBytes = boundaryCandidate.getBytes(StandardCharsets.UTF_8);
+ if (Arrays.stream(parts).allMatch(part -> {
+ if (part.getKey().contains(boundaryCandidate)) return false;
+ if (part instanceof Multipart.Value v) {
+ if (v.getValue().contains(boundaryCandidate)) return false;
+ } else if (part instanceof Multipart.File f) {
+ if (f.filename.contains(boundaryCandidate)) return false;
+ if (f.contentType.contains(boundaryCandidate)) return false;
+ if (arrayContainsArray(f.contents, candidateBytes)) return false;
+ }
+ return true;
+ })) {
+ return boundaryCandidate;
+ }
+ }
+ }
+
+ public static Multipart.Body buildMultipartBody(Multipart... parts) {
+ val boundary = findMultipartBoundary(parts);
+ val preName = ("--"+boundary+"\r\nContent-Disposition: form-data; name=\"").getBytes(StandardCharsets.UTF_8);
+ val postNameKV = "\"\r\n\r\n".getBytes(StandardCharsets.UTF_8);
+ val postNamePreFilename = "\"; filename=\"".getBytes(StandardCharsets.UTF_8);
+ val postFilenamePreContentType = "\"\r\nContent-Type: ".getBytes(StandardCharsets.UTF_8);
+ val postContentTypePreFile = "\r\n\r\n".getBytes(StandardCharsets.UTF_8);
+ val terminator = "\r\n".getBytes(StandardCharsets.UTF_8);
+ val finalTerminator = ("--"+boundary+"--\r\n").getBytes(StandardCharsets.UTF_8);
+ val result = new LinkedList<byte[]>();
+ for (val part : parts) {
+ result.add(preName);
+ result.add(part.getKey().getBytes(StandardCharsets.UTF_8));
+ if (part instanceof Multipart.Value v) {
+ result.add(postNameKV);
+ result.add(v.getValue().getBytes(StandardCharsets.UTF_8));
+ result.add(terminator);
+ } else if (part instanceof Multipart.File f) {
+ result.add(postNamePreFilename);
+ result.add(f.getFilename().getBytes(StandardCharsets.UTF_8));
+ result.add(postFilenamePreContentType);
+ result.add(f.getContentType().getBytes(StandardCharsets.UTF_8));
+ result.add(postContentTypePreFile);
+ result.add(f.getContents());
+ result.add(terminator);
+ } else { throw new IllegalStateException(); }
+ }
+ result.add(finalTerminator);
+ return new Multipart.Body("multipart/form-data; boundary="+boundary, result);
+ }
}