package jnpf.bizcommon.onlyoffice.service.impl; import cn.hutool.json.JSONUtil; import cn.hutool.jwt.JWTUtil; import com.sun.net.httpserver.HttpServer; import jnpf.bizcommon.onlyoffice.entity.OnlyOfficeProperties; import jnpf.dmsEntity.preview.model.DmsFilePreviewTicket; import jnpf.dmsEntity.storage.model.DmsPersistentPdfSource; import jnpf.exception.DataException; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; import org.junit.After; import org.junit.Before; import org.junit.Test; import java.io.ByteArrayOutputStream; import java.net.InetSocketAddress; import java.nio.charset.StandardCharsets; import java.util.UUID; import java.util.concurrent.atomic.AtomicReference; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; public class OnlyOfficeDmsPrintRendererTest { private HttpServer server; private OnlyOfficeDmsPrintRenderer renderer; private OnlyOfficeProperties properties; private byte[] pdf; private final AtomicReference conversionRequest = new AtomicReference<>(); private final AtomicReference sourceRequest = new AtomicReference<>(); private final AtomicReference delegatedBaseTicket = new AtomicReference<>(); @Before public void setup() throws Exception { pdf = onePagePdf(); server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0); server.createContext("/ConvertService.ashx", exchange -> { conversionRequest.set(new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8)); String body = "{\"endConvert\":true,\"fileUrl\":\"http://untrusted-host/" + "cache/files/job/output/result.pdf?md5=signed\"}"; byte[] bytes = body.getBytes(StandardCharsets.UTF_8); exchange.sendResponseHeaders(200, bytes.length); exchange.getResponseBody().write(bytes); exchange.close(); }); server.createContext("/cache/files/job/output/result.pdf", exchange -> { exchange.sendResponseHeaders(200, pdf.length); exchange.getResponseBody().write(pdf); exchange.close(); }); server.createContext("/api/file/Dms/OnlyOffice/", exchange -> { sourceRequest.set(exchange.getRequestURI().getRawQuery()); exchange.sendResponseHeaders(200, pdf.length); exchange.getResponseBody().write(pdf); exchange.close(); }); server.start(); String baseUrl = "http://127.0.0.1:" + server.getAddress().getPort(); properties = new OnlyOfficeProperties(); properties.setDsInternalUrl(baseUrl); properties.setDsPublicUrl("http://browser-onlyoffice"); properties.setFileFetchBaseUrl(baseUrl); properties.setJwtSecret("test-print-secret"); renderer = new OnlyOfficeDmsPrintRenderer(properties, ticket -> { delegatedBaseTicket.set(ticket); return "internal-base-ticket"; }); } @After public void cleanup() { if (server != null) { server.stop(0); } } @Test public void convertsOfficeSourceToPdfWithSignedPrintTicketUrl() { byte[] result = renderer.render(ticket("quality manual.docx"), "print+/ticket="); assertArrayEquals(pdf, result); cn.hutool.json.JSONObject body = JSONUtil.parseObj(conversionRequest.get()); assertEquals("docx", body.getStr("filetype")); assertEquals("pdf", body.getStr("outputtype")); assertEquals(false, body.getBool("async")); assertTrue(body.getStr("url").contains("/api/file/Dms/OnlyOffice/")); assertTrue(body.getStr("url").endsWith("ticket=print%2B%2Fticket%3D")); assertTrue(JWTUtil.verify(body.getStr("token"), properties.getJwtSecret().getBytes(StandardCharsets.UTF_8))); } @Test public void validatesExistingPdfWithoutCallingConversionService() { assertArrayEquals(pdf, renderer.render(ticket("controlled.pdf"), "print-ticket")); assertEquals(null, conversionRequest.get()); } @Test public void renderBaseUsesPersistentPdfForWordWithoutCallingConversionService() { DmsFilePreviewTicket ticket = ticket("controlled.docx"); DmsPersistentPdfSource source = new DmsPersistentPdfSource(); source.setObjectKey("dms-derived/pdf/version/source.pdf"); source.setSizeBytes((long) pdf.length); ticket.setPdfSource(source); assertArrayEquals(pdf, renderer.renderBase(ticket, "print-ticket")); assertEquals(null, conversionRequest.get()); assertEquals(DmsFilePreviewTicket.PRINT_BASE_SCOPE, delegatedBaseTicket.get().getScope()); assertTrue(sourceRequest.get().contains("ticket=internal-base-ticket")); assertTrue(!sourceRequest.get().contains("print-ticket")); } @Test public void rejectsUnsafeConversionResultPaths() { for (String value : new String[]{ "http://host/private/result.pdf", "/cache/files/../result.pdf", "/cache/files/%2e%2e/result.pdf", "/cache/files/%252e%252e/result.pdf", "/cache/files/job/%5c..%5cresult.pdf", "/cache/files/job/result.docx" }) { try { OnlyOfficeDmsPrintRenderer.resultPath(value); fail(); } catch (DataException expected) { // expected } } } @Test public void acceptsEncodedUnicodeConversionResultPath() { String value = "http://untrusted-host/cache/files/job/output/" + "%E8%B4%A8%E9%87%8F%E6%89%8B%E5%86%8C.pdf?md5=signed"; assertEquals("/cache/files/job/output/" + "%E8%B4%A8%E9%87%8F%E6%89%8B%E5%86%8C.pdf?md5=signed", OnlyOfficeDmsPrintRenderer.resultPath(value)); } @Test(expected = DataException.class) public void rejectsSourceBeyondConfiguredLimitBeforeNetworkRequest() { properties.getDmsPrint().setMaxSourceBytes(1); DmsFilePreviewTicket ticket = ticket("quality.docx"); ticket.setSizeBytes(2L); renderer.render(ticket, "print-ticket"); } private DmsFilePreviewTicket ticket(String fileName) { DmsFilePreviewTicket ticket = new DmsFilePreviewTicket(); ticket.setScope(DmsFilePreviewTicket.PRINT_SCOPE); ticket.setTenantId("tenant-1"); ticket.setUserId("user-1"); ticket.setFileVersionId(UUID.fromString("018f0000-0000-7000-8000-000000000702")); ticket.setFileName(fileName); ticket.setSizeBytes((long) pdf.length); ticket.setStoragePlatform("seaweedfs-1"); ticket.setBucketName("jnpf-dms"); ticket.setObjectPath("2026/09/document/"); ticket.setObjectName(fileName); ticket.setExpiresAt(System.currentTimeMillis() + 60_000L); return ticket; } private byte[] onePagePdf() throws Exception { try (PDDocument document = new PDDocument(); ByteArrayOutputStream output = new ByteArrayOutputStream()) { document.addPage(new PDPage()); document.save(output); return output.toByteArray(); } } }