刘光辉
3 小时以前 06f314545ac3514d7ce28939920866ae8188d31c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
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());
    }
}