View Javadoc
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 }