001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *   http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.apache.commons.io.serialization;
020
021import java.io.IOException;
022import java.io.InputStream;
023import java.io.InvalidClassException;
024import java.io.ObjectInputStream;
025import java.io.ObjectStreamClass;
026import java.util.regex.Pattern;
027
028import org.apache.commons.io.build.AbstractStreamBuilder;
029
030/**
031 * An {@link ObjectInputStream} that's restricted to deserialize a limited set of classes.
032 *
033 * <p>
034 * Various accept/reject methods allow for specifying which classes can be deserialized.
035 * </p>
036 * <h2>Reading safely</h2>
037 * <p>
038 * Here is the only way to safely read a HashMap of String keys and Integer values:
039 * </p>
040 *
041 * <pre>{@code
042 * // Defining Object fixture
043 * final HashMap<String, Integer> map1 = new HashMap<>();
044 * map1.put("1", 1);
045 * // Writing serialized fixture
046 * final byte[] byteArray;
047 * try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
048 *         final ObjectOutputStream oos = new ObjectOutputStream(baos)) {
049 *     oos.writeObject(map1);
050 *     oos.flush();
051 *     byteArray = baos.toByteArray();
052 * }
053 * // Reading
054 * try (ByteArrayInputStream bais = new ByteArrayInputStream(byteArray);
055 *         ValidatingObjectInputStream vois = ValidatingObjectInputStream.builder()
056 *             .accept(HashMap.class, Number.class, Integer.class)
057 *             .setInputStream(bais)
058 *             .get()) {
059 *     // String.class is automatically accepted
060 *     final HashMap<String, Integer> map2 = (HashMap<String, Integer>) vois.readObject();
061 *     assertEquals(map1, map2);
062 * }
063 * // Reusing a configuration
064 * final ObjectStreamClassPredicate predicate = new ObjectStreamClassPredicate()
065 *     .accept(HashMap.class, Number.class, Integer.class);
066 * try (ByteArrayInputStream bais = new ByteArrayInputStream(byteArray);
067 *         ValidatingObjectInputStream vois = ValidatingObjectInputStream.builder()
068 *             .setPredicate(predicate)
069 *             .setInputStream(bais)
070 *             .get()) {
071 *     // String.class is automatically accepted
072 *     final HashMap<String, Integer> map2 = (HashMap<String, Integer>) vois.readObject();
073 *     assertEquals(map1, map2);
074 * }
075 * }</pre>
076 * <p>
077 * Design inspired by a <a href="http://www.ibm.com/developerworks/library/se-lookahead/">IBM DeveloperWorks Article</a>.
078 * </p>
079 *
080 * @since 2.5
081 */
082public class ValidatingObjectInputStream extends ObjectInputStream {
083
084    // @formatter:off
085    /**
086     * Builds a new {@link ValidatingObjectInputStream}.
087     *
088     * <h2>Using NIO</h2>
089     * <pre>{@code
090     * ValidatingObjectInputStream s = ValidatingObjectInputStream.builder()
091     *   .setPath(Paths.get("MyFile.ser"))
092     *   .get();}
093     * </pre>
094     * <h2>Using IO</h2>
095     * <pre>{@code
096     * ValidatingObjectInputStream s = ValidatingObjectInputStream.builder()
097     *   .setFile(new File("MyFile.ser"))
098     *   .get();}
099     * </pre>
100     *
101     * @see #get()
102     * @since 2.18.0
103     */
104    // @formatter:on
105    public static class Builder extends AbstractStreamBuilder<ValidatingObjectInputStream, Builder> {
106
107        private ObjectStreamClassPredicate predicate = new ObjectStreamClassPredicate();
108
109        /**
110         * Constructs a new builder of {@link ValidatingObjectInputStream}.
111         *
112         * @deprecated Use {@link #builder()}.
113         */
114        @Deprecated
115        public Builder() {
116            // empty
117        }
118
119        /**
120         * Accepts the specified classes for deserialization, unless they are otherwise rejected.
121         *
122         * @param classes Classes to accept
123         * @return this object
124         * @since 2.18.0
125         */
126        public Builder accept(final Class<?>... classes) {
127            predicate.accept(classes);
128            return this;
129        }
130
131        /**
132         * Accepts class names where the supplied ClassNameMatcher matches for deserialization, unless they are otherwise rejected.
133         *
134         * @param matcher a class name matcher to <em>accept</em> objects.
135         * @return this instance.
136         * @since 2.18.0
137         */
138        public Builder accept(final ClassNameMatcher matcher) {
139            predicate.accept(matcher);
140            return this;
141        }
142
143        /**
144         * Accepts class names that match the supplied pattern for deserialization, unless they are otherwise rejected.
145         *
146         * @param pattern a Pattern for compiled regular expression.
147         * @return this instance.
148         * @since 2.18.0
149         */
150        public Builder accept(final Pattern pattern) {
151            predicate.accept(pattern);
152            return this;
153        }
154
155        /**
156         * Accepts the wildcard specified classes for deserialization, unless they are otherwise rejected.
157         *
158         * @param patterns Wildcard file name patterns as defined by {@link org.apache.commons.io.FilenameUtils#wildcardMatch(String, String)
159         *                 FilenameUtils.wildcardMatch}
160         * @return this instance.
161         * @since 2.18.0
162         */
163        public Builder accept(final String... patterns) {
164            predicate.accept(patterns);
165            return this;
166        }
167
168        /**
169         * Builds a new {@link ValidatingObjectInputStream}.
170         * <p>
171         * You must set an aspect that supports {@link #getInputStream()} on this builder, otherwise, this method throws an exception.
172         * </p>
173         * <p>
174         * This builder uses the following aspects:
175         * </p>
176         * <ul>
177         * <li>{@link #getInputStream()} gets the target aspect.</li>
178         * <li>predicate</li>
179         * <li>charsetDecoder</li>
180         * <li>writeImmediately</li>
181         * </ul>
182         *
183         * @return a new instance.
184         * @throws UnsupportedOperationException if the origin cannot provide a {@link InputStream}.
185         * @throws IOException                   if an I/O error occurs converting to an {@link InputStream} using {@link #getInputStream()}.
186         * @see #getWriter()
187         * @see #getUnchecked()
188         */
189        @Override
190        public ValidatingObjectInputStream get() throws IOException {
191            return new ValidatingObjectInputStream(getInputStream(), predicate);
192        }
193
194        /**
195         * Gets the predicate.
196         *
197         * @return the predicate.
198         * @since 2.18.0
199         */
200        public ObjectStreamClassPredicate getPredicate() {
201            return predicate;
202        }
203
204        /**
205         * Rejects the specified classes for deserialization, even if they are otherwise accepted.
206         *
207         * @param classes Classes to reject
208         * @return this instance.
209         * @since 2.18.0
210         */
211        public Builder reject(final Class<?>... classes) {
212            predicate.reject(classes);
213            return this;
214        }
215
216        /**
217         * Rejects class names where the supplied ClassNameMatcher matches for deserialization, even if they are otherwise accepted.
218         *
219         * @param matcher the matcher to use
220         * @return this instance.
221         * @since 2.18.0
222         */
223        public Builder reject(final ClassNameMatcher matcher) {
224            predicate.reject(matcher);
225            return this;
226        }
227
228        /**
229         * Rejects class names that match the supplied pattern for deserialization, even if they are otherwise accepted.
230         *
231         * @param pattern standard Java regexp
232         * @return this instance.
233         * @since 2.18.0
234         */
235        public Builder reject(final Pattern pattern) {
236            predicate.reject(pattern);
237            return this;
238        }
239
240        /**
241         * Rejects the wildcard specified classes for deserialization, even if they are otherwise accepted.
242         *
243         * @param patterns Wildcard file name patterns as defined by {@link org.apache.commons.io.FilenameUtils#wildcardMatch(String, String)
244         *                 FilenameUtils.wildcardMatch}
245         * @return this instance.
246         * @since 2.18.0
247         */
248        public Builder reject(final String... patterns) {
249            predicate.reject(patterns);
250            return this;
251        }
252
253        /**
254         * Sets the predicate, null resets to an empty new ObjectStreamClassPredicate.
255         *
256         * @param predicate the predicate.
257         * @return this instance.
258         * @since 2.18.0
259         */
260        public Builder setPredicate(final ObjectStreamClassPredicate predicate) {
261            this.predicate = predicate != null ? predicate : new ObjectStreamClassPredicate();
262            return this;
263        }
264
265    }
266
267    /**
268     * Constructs a new {@link Builder}.
269     *
270     * @return a new {@link Builder}.
271     * @since 2.18.0
272     */
273    public static Builder builder() {
274        return new Builder();
275    }
276
277    private final ObjectStreamClassPredicate predicate;
278
279    /**
280     * Constructs an instance to deserialize the specified input stream. At least one accept method needs to be called to specify which classes can be
281     * deserialized, as by default no classes are accepted.
282     *
283     * @param input an input stream
284     * @throws IOException if an I/O error occurs while reading stream header
285     * @deprecated Use {@link #builder()}.
286     */
287    @Deprecated
288    public ValidatingObjectInputStream(final InputStream input) throws IOException {
289        this(input, new ObjectStreamClassPredicate());
290    }
291
292    /**
293     * Constructs an instance to deserialize the specified input stream. At least one accept method needs to be called to specify which classes can be
294     * deserialized, as by default no classes are accepted.
295     *
296     * @param input     an input stream.
297     * @param predicate how to accept and reject classes.
298     * @throws IOException if an I/O error occurs while reading stream header.
299     */
300    private ValidatingObjectInputStream(final InputStream input, final ObjectStreamClassPredicate predicate) throws IOException {
301        super(input);
302        this.predicate = predicate;
303    }
304
305    /**
306     * Accepts the specified classes for deserialization, unless they are otherwise rejected.
307     * <p>
308     * The reject list takes precedence over the accept list.
309     * </p>
310     *
311     * @param classes Classes to accept
312     * @return this instance.
313     */
314    public ValidatingObjectInputStream accept(final Class<?>... classes) {
315        predicate.accept(classes);
316        return this;
317    }
318
319    /**
320     * Accepts class names where the supplied ClassNameMatcher matches for deserialization, unless they are otherwise rejected.
321     * <p>
322     * The reject list takes precedence over the accept list.
323     * </p>
324     *
325     * @param matcher a class name matcher to <em>accept</em> objects.
326     * @return this instance.
327     */
328    public ValidatingObjectInputStream accept(final ClassNameMatcher matcher) {
329        predicate.accept(matcher);
330        return this;
331    }
332
333    /**
334     * Accepts class names that match the supplied pattern for deserialization, unless they are otherwise rejected.
335     * <p>
336     * The reject list takes precedence over the accept list.
337     * </p>
338     *
339     * @param pattern a Pattern for compiled regular expression.
340     * @return this instance.
341     */
342    public ValidatingObjectInputStream accept(final Pattern pattern) {
343        predicate.accept(pattern);
344        return this;
345    }
346
347    /**
348     * Accepts the wildcard specified classes for deserialization, unless they are otherwise rejected.
349     * <p>
350     * The reject list takes precedence over the accept list.
351     * </p>
352     *
353     * @param patterns Wildcard file name patterns as defined by {@link org.apache.commons.io.FilenameUtils#wildcardMatch(String, String)
354     *                 FilenameUtils.wildcardMatch}.
355     * @return this instance.
356     */
357    public ValidatingObjectInputStream accept(final String... patterns) {
358        predicate.accept(patterns);
359        return this;
360    }
361
362    /**
363     * Checks that the class name conforms to requirements.
364     * <p>
365     * The reject list takes precedence over the accept list.
366     * </p>
367     *
368     * @param name The class name to test.
369     * @throws InvalidClassException Thrown when a rejected or non-accepted class is found.
370     */
371    private void checkClassName(final String name) throws InvalidClassException {
372        if (!predicate.test(name)) {
373            invalidClassNameFound(name);
374        }
375    }
376
377    /**
378     * Called to throw {@link InvalidClassException} if an invalid class name is found during deserialization. Can be overridden, for example to log those class
379     * names.
380     *
381     * @param className name of the invalid class.
382     * @throws InvalidClassException Thrown with a message containing the class name.
383     */
384    protected void invalidClassNameFound(final String className) throws InvalidClassException {
385        throw new InvalidClassException("Class name not accepted: " + className);
386    }
387
388    /**
389     * Delegates to {@link #readObject()} and casts to the generic {@code T}.
390     *
391     * @param <T> The return type.
392     * @return Result from {@link #readObject()}.
393     * @throws ClassNotFoundException Thrown by {@link #readObject()}.
394     * @throws IOException            Thrown by {@link #readObject()}.
395     * @throws ClassCastException     Thrown when {@link #readObject()} does not match {@code T}.
396     * @since 2.18.0
397     */
398    @SuppressWarnings("unchecked")
399    public <T> T readObjectCast() throws ClassNotFoundException, IOException {
400        return (T) super.readObject();
401    }
402
403    /**
404     * Rejects the specified classes for deserialization, even if they are otherwise accepted.
405     * <p>
406     * The reject list takes precedence over the accept list.
407     * </p>
408     *
409     * @param classes Classes to reject.
410     * @return this instance.
411     */
412    public ValidatingObjectInputStream reject(final Class<?>... classes) {
413        predicate.reject(classes);
414        return this;
415    }
416
417    /**
418     * Rejects class names where the supplied ClassNameMatcher matches for deserialization, even if they are otherwise accepted.
419     * <p>
420     * The reject list takes precedence over the accept list.
421     * </p>
422     *
423     * @param matcher a class name matcher to <em>reject</em> objects.
424     * @return this instance.
425     */
426    public ValidatingObjectInputStream reject(final ClassNameMatcher matcher) {
427        predicate.reject(matcher);
428        return this;
429    }
430
431    /**
432     * Rejects class names that match the supplied pattern for deserialization, even if they are otherwise accepted.
433     * <p>
434     * The reject list takes precedence over the accept list.
435     * </p>
436     *
437     * @param pattern a Pattern for compiled regular expression.
438     * @return this instance.
439     */
440    public ValidatingObjectInputStream reject(final Pattern pattern) {
441        predicate.reject(pattern);
442        return this;
443    }
444
445    /**
446     * Rejects the wildcard specified classes for deserialization, even if they are otherwise accepted.
447     * <p>
448     * The reject list takes precedence over the accept list.
449     * </p>
450     *
451     * @param patterns An array of wildcard file name patterns as defined by {@link org.apache.commons.io.FilenameUtils#wildcardMatch(String, String)
452     *                 FilenameUtils.wildcardMatch}
453     * @return this instance.
454     */
455    public ValidatingObjectInputStream reject(final String... patterns) {
456        predicate.reject(patterns);
457        return this;
458    }
459
460    @Override
461    protected Class<?> resolveClass(final ObjectStreamClass osc) throws IOException, ClassNotFoundException {
462        checkClassName(osc.getName());
463        return super.resolveClass(osc);
464    }
465}