Merge "BUG-2511 Fix XXE vulnerability in initial config loaders"
[controller.git] / opendaylight / config / config-persister-directory-xml-adapter / src / main / java / org / opendaylight / controller / config / persist / storage / directory / xml / XmlDirectoryPersister.java
1 /*
2  * Copyright (c) 2013 Cisco Systems, Inc. and others.  All rights reserved.
3  *
4  * This program and the accompanying materials are made available under the
5  * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6  * and is available at http://www.eclipse.org/legal/epl-v10.html
7  */
8 package org.opendaylight.controller.config.persist.storage.directory.xml;
9
10 import static com.google.common.base.Preconditions.checkArgument;
11
12 import com.google.common.base.Optional;
13 import com.google.common.io.Files;
14 import java.io.File;
15 import java.io.FilenameFilter;
16 import java.io.IOException;
17 import java.util.ArrayList;
18 import java.util.Arrays;
19 import java.util.Collections;
20 import java.util.List;
21 import java.util.Set;
22 import java.util.SortedSet;
23 import javax.xml.bind.JAXBContext;
24 import javax.xml.bind.JAXBException;
25 import javax.xml.bind.Unmarshaller;
26 import javax.xml.stream.XMLInputFactory;
27 import javax.xml.stream.XMLStreamException;
28 import javax.xml.stream.XMLStreamReader;
29 import javax.xml.transform.stream.StreamSource;
30 import org.opendaylight.controller.config.persist.api.ConfigSnapshotHolder;
31 import org.opendaylight.controller.config.persist.api.Persister;
32 import org.opendaylight.controller.config.persist.storage.file.xml.model.ConfigSnapshot;
33 import org.slf4j.Logger;
34 import org.slf4j.LoggerFactory;
35
36 public class XmlDirectoryPersister implements Persister {
37     private static final Logger LOG = LoggerFactory.getLogger(XmlDirectoryPersister.class);
38
39     private final File storage;
40     private final Optional<FilenameFilter> extensionsFilter;
41
42     /**
43      * Creates XmlDirectoryPersister that picks up all files in specified folder
44      */
45     public XmlDirectoryPersister(final File storage) {
46         this(storage, Optional.<FilenameFilter>absent());
47     }
48
49     /**
50      * Creates XmlDirectoryPersister that picks up files only with specified file extension
51      */
52     public XmlDirectoryPersister(final File storage, final Set<String> fileExtensions) {
53         this(storage, Optional.of(getFilter(fileExtensions)));
54     }
55
56     private XmlDirectoryPersister(final File storage, final Optional<FilenameFilter> extensionsFilter) {
57         checkArgument(storage.exists() && storage.isDirectory(), "Storage directory does not exist: " + storage);
58         this.storage = storage;
59         this.extensionsFilter = extensionsFilter;
60     }
61
62     @Override
63     public void persistConfig(final ConfigSnapshotHolder holder) throws IOException {
64         throw new UnsupportedOperationException("This adapter is read only. Please set readonly=true on " + getClass());
65     }
66
67     @Override
68     public List<ConfigSnapshotHolder> loadLastConfigs() throws IOException {
69         File[] filesArray = extensionsFilter.isPresent() ? storage.listFiles(extensionsFilter.get()) : storage.listFiles();
70         if (filesArray == null || filesArray.length == 0) {
71             return Collections.emptyList();
72         }
73         List<File> sortedFiles = new ArrayList<>(Arrays.asList(filesArray));
74         Collections.sort(sortedFiles);
75         // combine all found files
76         LOG.debug("Reading files in following order: {}", sortedFiles);
77
78         List<ConfigSnapshotHolder> result = new ArrayList<>();
79         for (File file : sortedFiles) {
80             LOG.trace("Adding file '{}' to combined result", file);
81             Optional<ConfigSnapshotHolder> h = fromXmlSnapshot(file);
82             // Ignore non valid snapshot
83             if(h.isPresent() == false) {
84                 continue;
85             }
86
87             result.add(h.get());
88         }
89         return result;
90     }
91
92     private Optional<ConfigSnapshotHolder> fromXmlSnapshot(final File file) {
93         try {
94             return Optional.of(loadLastConfig(file));
95         } catch (JAXBException e) {
96             // In case of parse error, issue a warning, ignore and continue
97             LOG.warn(
98                     "Unable to parse configuration snapshot from {}. Initial config from {} will be IGNORED in this run. ",
99                     file, file);
100             LOG.warn(
101                     "Note that subsequent config files may fail due to this problem. ",
102                     "Xml markup in this file needs to be fixed, for detailed information see enclosed exception.",
103                     e);
104         }
105
106         return Optional.absent();
107     }
108
109     public static ConfigSnapshotHolder loadLastConfig(final File file) throws JAXBException {
110         JAXBContext jaxbContext = JAXBContext.newInstance(ConfigSnapshot.class);
111         Unmarshaller um = jaxbContext.createUnmarshaller();
112         XMLInputFactory xif = XMLInputFactory.newFactory();
113         xif.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false);
114         xif.setProperty(XMLInputFactory.SUPPORT_DTD, false);
115         try {
116             XMLStreamReader xsr = xif.createXMLStreamReader(new StreamSource(file));
117             return asHolder((ConfigSnapshot) um.unmarshal(xsr));
118         } catch (final XMLStreamException e) {
119             throw new JAXBException(e);
120         }
121     }
122
123     private static ConfigSnapshotHolder asHolder(final ConfigSnapshot unmarshalled) {
124         return new ConfigSnapshotHolder() {
125             @Override
126             public String getConfigSnapshot() {
127                 return unmarshalled.getConfigSnapshot();
128             }
129
130             @Override
131             public SortedSet<String> getCapabilities() {
132                 return unmarshalled.getCapabilities();
133             }
134
135             @Override
136             public String toString() {
137                 return unmarshalled.toString();
138             }
139         };
140     }
141
142     private static FilenameFilter getFilter(final Set<String>fileExtensions) {
143         checkArgument(fileExtensions.isEmpty() == false, "No file extension provided", fileExtensions);
144
145         return new FilenameFilter() {
146             @Override
147             public boolean accept(final File dir, final String name) {
148                 String ext = Files.getFileExtension(name);
149                 return fileExtensions.contains(ext);
150             }
151         };
152     }
153
154     @Override
155     public void close() {
156
157     }
158
159     @Override
160     public String toString() {
161         final StringBuffer sb = new StringBuffer("XmlDirectoryPersister{");
162         sb.append("storage=").append(storage);
163         sb.append('}');
164         return sb.toString();
165     }
166 }