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<String> conversionRequest = new AtomicReference<>();
|
private final AtomicReference<String> sourceRequest = new AtomicReference<>();
|
private final AtomicReference<DmsFilePreviewTicket> 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();
|
}
|
}
|
}
|