1 /*
2 * Copyright 2012-2025 CodeLibs Project and the Others.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
13 * either express or implied. See the License for the specific language
14 * governing permissions and limitations under the License.
15 */
16 package org.codelibs.fess.crawler.serializer;
17
18 import java.io.ByteArrayInputStream;
19 import java.io.ByteArrayOutputStream;
20 import java.io.IOException;
21 import java.math.BigDecimal;
22 import java.math.BigInteger;
23 import java.sql.Timestamp;
24 import java.util.ArrayList;
25 import java.util.Arrays;
26 import java.util.Collections;
27 import java.util.Date;
28 import java.util.HashMap;
29 import java.util.HashSet;
30 import java.util.LinkedHashMap;
31 import java.util.LinkedHashSet;
32 import java.util.LinkedList;
33 import java.util.TreeMap;
34 import java.util.TreeSet;
35
36 import org.apache.logging.log4j.LogManager;
37 import org.apache.logging.log4j.Logger;
38 import org.codelibs.core.exception.IORuntimeException;
39 import org.codelibs.core.io.SerializeUtil;
40 import org.codelibs.fess.util.ComponentUtil;
41
42 import com.esotericsoftware.kryo.Kryo;
43 import com.esotericsoftware.kryo.io.Input;
44 import com.esotericsoftware.kryo.io.Output;
45 import com.esotericsoftware.kryo.serializers.CollectionSerializer;
46 import com.esotericsoftware.kryo.serializers.MapSerializer;
47
48 /**
49 * A serializer class for handling object serialization and deserialization.
50 * <p>
51 * This class provides serialization capabilities using different serializers,
52 * currently supporting Kryo and JavaBin serialization formats. The serializer
53 * type is determined by the crawler data serializer configuration.
54 * </p>
55 * <p>
56 * The class is thread-safe and uses ThreadLocal to maintain Kryo instances
57 * per thread to avoid synchronization overhead.
58 * </p>
59 *
60 */
61 public class DataSerializer {
62
63 /** Logger for this class. */
64 private static final Logger logger = LogManager.getLogger(DataSerializer.class);
65
66 /** Constant for JavaBin serializer type. */
67 protected static final String JAVABIN = "javabin";
68
69 /** Constant for Kryo serializer type. */
70 protected static final String KRYO = "kryo";
71
72 /** ThreadLocal container for Kryo instances to ensure thread safety. */
73 protected final ThreadLocal<Kryo> kryoThreadLocal;
74
75 /**
76 * Constructs a new DataSerializer.
77 * <p>
78 * Initializes the ThreadLocal Kryo instances with appropriate configuration.
79 * The Kryo instances are configured to require class registration for security,
80 * preventing deserialization of arbitrary classes that could lead to RCE vulnerabilities.
81 * Only explicitly registered classes can be serialized/deserialized.
82 * </p>
83 */
84 public DataSerializer() {
85 kryoThreadLocal = ThreadLocal.withInitial(() -> {
86 final Kryo kryo = new Kryo();
87 // Enable registration requirement for security - only registered classes can be deserialized
88 kryo.setRegistrationRequired(true);
89 if (logger.isDebugEnabled()) {
90 kryo.setWarnUnregisteredClasses(true);
91 }
92 // Register allowed classes for serialization/deserialization
93 registerClasses(kryo);
94 return kryo;
95 });
96 }
97
98 /**
99 * Registers all classes that are allowed for Kryo serialization/deserialization.
100 * <p>
101 * This method registers only the classes that are needed by the crawler data serialization.
102 * By explicitly registering classes, we prevent deserialization of arbitrary classes
103 * which could lead to remote code execution vulnerabilities through gadget chains.
104 * </p>
105 *
106 * @param kryo the Kryo instance to register classes with
107 */
108 protected void registerClasses(final Kryo kryo) {
109 // Primitive types and wrappers
110 kryo.register(String.class);
111 kryo.register(String[].class);
112 kryo.register(Integer.class);
113 kryo.register(int[].class);
114 kryo.register(Long.class);
115 kryo.register(long[].class);
116 kryo.register(Double.class);
117 kryo.register(double[].class);
118 kryo.register(Float.class);
119 kryo.register(float[].class);
120 kryo.register(Boolean.class);
121 kryo.register(boolean[].class);
122 kryo.register(Byte.class);
123 kryo.register(byte[].class);
124 kryo.register(Short.class);
125 kryo.register(short[].class);
126 kryo.register(Character.class);
127 kryo.register(char[].class);
128
129 // Common object types
130 kryo.register(Object.class);
131 kryo.register(Object[].class);
132 kryo.register(Class.class);
133
134 // Date and time types
135 kryo.register(Date.class);
136 kryo.register(Timestamp.class);
137
138 // Numeric types
139 kryo.register(BigDecimal.class);
140 kryo.register(BigInteger.class);
141
142 // Collections - with explicit serializers for safety
143 kryo.register(ArrayList.class, new CollectionSerializer<>());
144 kryo.register(LinkedList.class, new CollectionSerializer<>());
145 kryo.register(HashSet.class, new CollectionSerializer<>());
146 kryo.register(LinkedHashSet.class, new CollectionSerializer<>());
147 kryo.register(TreeSet.class, new CollectionSerializer<>());
148
149 // Maps - with explicit serializers for safety
150 kryo.register(HashMap.class, new MapSerializer<>());
151 kryo.register(LinkedHashMap.class, new MapSerializer<>());
152 kryo.register(TreeMap.class, new MapSerializer<>());
153
154 // Immutable collections (from Collections utility)
155 // Register each class individually to ensure partial failures don't skip all registrations
156 registerClassSafely(kryo, Collections.emptyList().getClass());
157 registerClassSafely(kryo, Collections.emptySet().getClass());
158 registerClassSafely(kryo, Collections.emptyMap().getClass());
159 registerClassSafely(kryo, Collections.singletonList(null).getClass());
160 registerClassSafely(kryo, Collections.singleton(null).getClass());
161 registerClassSafely(kryo, Collections.singletonMap(null, null).getClass());
162 registerClassSafely(kryo, Arrays.asList().getClass());
163 }
164
165 /**
166 * Safely registers a class with Kryo, logging any registration failures at WARN level.
167 * <p>
168 * This method catches exceptions for individual class registrations to ensure
169 * that a failure to register one class doesn't prevent other classes from being registered.
170 * Registration failures are logged at WARN level since they may cause serialization errors later.
171 * </p>
172 *
173 * @param kryo the Kryo instance to register the class with
174 * @param clazz the class to register
175 */
176 private void registerClassSafely(final Kryo kryo, final Class<?> clazz) {
177 try {
178 kryo.register(clazz);
179 } catch (final Exception e) {
180 logger.warn("Failed to register class for Kryo serialization: {}", clazz.getName(), e);
181 }
182 }
183
184 /**
185 * Gets the configured serializer type from the Fess configuration.
186 *
187 * @return the serializer type (either "kryo" or "javabin")
188 */
189 protected String getSerializerType() {
190 return ComponentUtil.getFessConfig().getCrawlerDataSerializer();
191 }
192
193 /**
194 * Serializes an object to a byte array.
195 * <p>
196 * The serialization method used depends on the configured serializer type.
197 * Supported types are Kryo and JavaBin serialization.
198 * </p>
199 *
200 * @param obj the object to serialize
201 * @return the serialized object as a byte array
202 * @throws IllegalArgumentException if an unsupported serializer type is configured
203 * @throws IORuntimeException if an I/O error occurs during serialization
204 */
205 public byte[] fromObjectToBinary(final Object obj) {
206 final String serializer = getSerializerType();
207 return switch (serializer) {
208 case KRYO -> serializeWithKryo(obj);
209 case JAVABIN -> SerializeUtil.fromObjectToBinary(obj);
210 default -> throw new IllegalArgumentException("Unexpected value: " + serializer);
211 };
212 }
213
214 /**
215 * Deserializes a byte array back to an object.
216 * <p>
217 * The deserialization method used depends on the configured serializer type.
218 * Supported types are Kryo and JavaBin deserialization.
219 * </p>
220 *
221 * @param bytes the byte array to deserialize
222 * @return the deserialized object
223 * @throws IllegalArgumentException if an unsupported serializer type is configured
224 * @throws IORuntimeException if an I/O error occurs during deserialization
225 */
226 public Object fromBinaryToObject(final byte[] bytes) {
227 final String serializer = getSerializerType();
228 return switch (serializer) {
229 case KRYO -> deserializeWithKryo(bytes);
230 case JAVABIN -> SerializeUtil.fromBinaryToObject(bytes);
231 default -> throw new IllegalArgumentException("Unexpected value: " + serializer);
232 };
233 }
234
235 /**
236 * Serializes an object using Kryo serialization.
237 * <p>
238 * Uses the thread-local Kryo instance to serialize the object along with
239 * its class information. The serialized data is written to a byte array
240 * output stream.
241 * </p>
242 *
243 * @param obj the object to serialize
244 * @return the serialized object as a byte array
245 * @throws IORuntimeException if an I/O error occurs during serialization
246 */
247 protected byte[] serializeWithKryo(final Object obj) {
248 final Kryo kryo = kryoThreadLocal.get();
249 try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); final Output output = new Output(baos)) {
250 kryo.writeClassAndObject(output, obj);
251 output.flush();
252 return baos.toByteArray();
253 } catch (final IOException e) {
254 throw new IORuntimeException(e);
255 }
256 }
257
258 /**
259 * Deserializes a byte array using Kryo deserialization.
260 * <p>
261 * Uses the thread-local Kryo instance to read both the class information
262 * and object data from the byte array input stream.
263 * </p>
264 *
265 * @param bytes the byte array to deserialize
266 * @return the deserialized object
267 */
268 protected Object deserializeWithKryo(final byte[] bytes) {
269 final Kryo kryo = kryoThreadLocal.get();
270 try (final Input input = new Input(new ByteArrayInputStream(bytes))) {
271 return kryo.readClassAndObject(input);
272 }
273 }
274 }