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<Map<String, Object>> submitted = new AtomicReference<>();
|
Map<String, Object> jobs = new java.util.concurrent.ConcurrentHashMap<>();
|
AtomicReference<OnlyOfficePageWatermarkRenderer> instance = new AtomicReference<>();
|
AtomicReference<String> downloadedScript = new AtomicReference<>();
|
AtomicReference<String> 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<String, Object> 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<String, Object> 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());
|
}
|
}
|