package jnpf.bizcommon.onlyoffice.service.impl; import com.fasterxml.jackson.databind.ObjectMapper; import com.sun.net.httpserver.HttpServer; import jnpf.bizcommon.onlyoffice.entity.OnlyOfficeProperties; import jnpf.exception.DataException; import jnpf.onlyoffice.PageWatermarkRequest; import org.junit.Test; import java.net.InetSocketAddress; import java.nio.charset.StandardCharsets; import java.util.Base64; import java.util.List; import java.util.Map; import java.util.UUID; import java.util.concurrent.atomic.AtomicReference; import static org.junit.Assert.*; public class OnlyOfficePageWatermarkRendererTest { @Test public void signsStructuredArgumentsFetchesOnlyControlledResultAndFiltersOutOfRangePages() throws Exception { ObjectMapper mapper = new ObjectMapper(); AtomicReference> submitted = new AtomicReference<>(); Map jobs = new java.util.concurrent.ConcurrentHashMap<>(); AtomicReference instance = new AtomicReference<>(); AtomicReference downloadedScript = new AtomicReference<>(); AtomicReference resultName = new AtomicReference<>("Watermark.docx"); HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0); server.createContext("/docbuilder", exchange -> { submitted.set(mapper.readValue(exchange.getRequestBody(), Map.class)); downloadedScript.set(instance.get().script("test-job")); byte[] body = mapper.writeValueAsBytes(Map.of("end", true, "urls", Map.of(resultName.get(), "http://untrusted-host/cache/files/job/" + resultName.get() + "?expires=123&md5=abc"))); exchange.sendResponseHeaders(200, body.length); exchange.getResponseBody().write(body); exchange.close(); }); server.createContext("/cache/files/job/", exchange -> { byte[] body = resultName.get().endsWith("txt") ? "DMS_WATERMARK_ERROR:PAGE_OUT_OF_RANGE|6|5".getBytes(StandardCharsets.UTF_8) : new byte[]{80, 75, 3, 4}; exchange.sendResponseHeaders(200, body.length); exchange.getResponseBody().write(body); exchange.close(); }); server.start(); try { OnlyOfficeProperties properties = new OnlyOfficeProperties(); properties.setDsInternalUrl("http://127.0.0.1:" + server.getAddress().getPort()); properties.setFileFetchBaseUrl("http://gateway"); properties.setJwtSecret("test-only-signature"); OnlyOfficePageWatermarkRenderer renderer = new OnlyOfficePageWatermarkRenderer(properties, mapper, 1, payload -> { jobs.put("test-job", payload); return "test-job"; }, jobs::get, jobs::remove); instance.set(renderer); assertArrayEquals(new byte[]{80, 75, 3, 4}, renderer.render(request())); Map body = submitted.get(); assertEquals("http://gateway/api/biz/onlyoffice/watermark/script.js?jobTicket=test-job", body.get("url")); assertTrue(cn.hutool.jwt.JWTUtil.verify((String) body.get("token"), "test-only-signature".getBytes(StandardCharsets.UTF_8))); assertTrue(((Map) body.get("argument")).isEmpty()); String firstLine = downloadedScript.get().split("\n", 2)[0]; String jsonLiteral = firstLine.substring("var WatermarkInput = JSON.parse(".length(), firstLine.length() - 2); Map argument = mapper.readValue(mapper.readValue(jsonLiteral, String.class), Map.class); assertTrue(argument.get("sourceUrl").toString().endsWith("?ticket=ticket%26value")); assertEquals(List.of(1, 3, 5), ((Map) ((List) argument.get("overlays")).get(0)).get("pages")); Map variant = (Map) ((List) ((Map) ((List) argument.get("overlays")).get(0)).get("variants")).get(0); assertEquals(80.0D, variant.get("drawingWidthMm")); assertEquals(100.0D, variant.get("offsetXMm")); assertTrue(jobs.isEmpty()); rejectsScript(renderer, "test-job"); resultName.set("WatermarkError.txt"); try { renderer.render(request()); fail("An out-of-range page must fail the entire generation"); } catch (DataException error) { assertEquals("水印页码 6 超过文档总页数 5", error.getMessage()); } assertTrue(jobs.isEmpty()); resultName.set("Watermark.docx"); assertEquals(4, renderer.render(request()).length); // A failed job released its concurrency slot. PageWatermarkRequest large = request(); byte[] image = new byte[150_000]; image[0] = (byte) 137; image[1] = 80; image[2] = 78; image[3] = 71; large.getOverlays().get(0).getVariants().get(0).setPngBase64(Base64.getEncoder().encodeToString(image)); renderer.render(large); assertTrue("Image data must not be sent through Document Builder process arguments", mapper.writeValueAsBytes(submitted.get()).length < 4096); assertTrue(downloadedScript.get().length() > 200_000); assertTrue(downloadedScript.get().contains("if (page > pageCount) return;")); assertFalse(downloadedScript.get().contains("throw new Error('PAGE_OUT_OF_RANGE|")); assertTrue(jobs.isEmpty()); } finally { server.stop(0); } } @Test public void scriptRejectsOtherTicketScopesAndExpiredJobs() throws Exception { ObjectMapper mapper = new ObjectMapper(); Map jobs = new java.util.HashMap<>(); OnlyOfficePageWatermarkRenderer renderer = new OnlyOfficePageWatermarkRenderer(new OnlyOfficeProperties(), mapper, 1, payload -> "unused", jobs::get, jobs::remove); jobs.put("other", mapper.writeValueAsString(Map.of("scope", "PREVIEW", "expiresAt", Long.MAX_VALUE, "argument", Map.of()))); jobs.put("expired", mapper.writeValueAsString(Map.of("scope", "onlyoffice:dms-page-watermark", "expiresAt", 1L, "argument", Map.of()))); rejectsScript(renderer, "other"); rejectsScript(renderer, "expired"); rejectsScript(renderer, "missing"); assertTrue(renderer.script().startsWith("var WatermarkInput = Argument;")); } private void rejectsScript(OnlyOfficePageWatermarkRenderer renderer, String ticket) { try { renderer.script(ticket); fail("An invalid job ticket must not disclose watermark data"); } catch (DataException expected) { } } @Test public void rejectsUncontrolledArtifactPathsAndMalformedPageArrays() { for (String url : List.of("http://host/private/Watermark.docx", "http://host/cache/files/../Watermark.docx", "http://host/cache/files/%2e%2e/Watermark.docx", "http://host/cache/files/job/Other.docx")) { try { OnlyOfficePageWatermarkRenderer.resultPath(url, "Watermark.docx"); fail(url); } catch (DataException expected) { } } PageWatermarkRequest input = request(); input.getOverlays().get(0).setPages(List.of(1, 1)); try { OnlyOfficePageWatermarkRenderer.validate(input); fail("Duplicate physical pages must not reach Document Builder"); } catch (DataException expected) { } input = request(); input.getOverlays().get(0).getVariants().get(0).setOffsetXMm(150D); try { OnlyOfficePageWatermarkRenderer.validate(input); fail("A drawing outside its paper bounds must be rejected"); } catch (DataException expected) { } } private PageWatermarkRequest request() { return new PageWatermarkRequest(UUID.fromString("018f0000-0000-7000-8000-000000000702"), "ticket&value", "DMS_CUSTOM_0123456789abcdef0123456789abcdef", List.of(new PageWatermarkRequest.Overlay(List.of(1, 3, 5), List.of(new PageWatermarkRequest.Variant( 210, 297, 80, 20, 100, 6, Base64.getEncoder().encodeToString(new byte[]{(byte) 137, 80, 78, 71, 13, 10, 26, 10})))))); } @Test public void acceptsSeparateOverlaysForTheSamePhysicalPage() { PageWatermarkRequest input = request(); PageWatermarkRequest.Overlay overlay = input.getOverlays().get(0); input.setOverlays(List.of(overlay, overlay)); OnlyOfficePageWatermarkRenderer.validate(input); assertEquals(2, input.getOverlays().size()); } }