diff options
| author | Jakob Heher <jakob.heher@iaik.tugraz.at> | 2026-06-10 14:28:52 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-06-10 14:28:52 +0200 |
| commit | 7a204f2c2520a875fa9c4166c243775e52f03e77 (patch) | |
| tree | 99bc09ba6e79c84ee3eaeb181168291c730ff568 /pdf-as-web/src | |
| parent | afbe8f6aee8d7554b52aa4aa24561731715695da (diff) | |
| download | pdf-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')
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); + } } |
