001package ca.uhn.fhir.validation;
002
003/*
004 * #%L
005 * HAPI FHIR - Core Library
006 * %%
007 * Copyright (C) 2014 - 2020 University Health Network
008 * %%
009 * Licensed under the Apache License, Version 2.0 (the "License");
010 * you may not use this file except in compliance with the License.
011 * You may obtain a copy of the License at
012 *
013 *      http://www.apache.org/licenses/LICENSE-2.0
014 *
015 * Unless required by applicable law or agreed to in writing, software
016 * distributed under the License is distributed on an "AS IS" BASIS,
017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
018 * See the License for the specific language governing permissions and
019 * limitations under the License.
020 * #L%
021 */
022
023import ca.uhn.fhir.context.ConfigurationException;
024import ca.uhn.fhir.context.FhirContext;
025import ca.uhn.fhir.rest.api.EncodingEnum;
026import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
027import org.apache.commons.io.IOUtils;
028import org.apache.commons.io.input.BOMInputStream;
029import org.hl7.fhir.instance.model.api.IBaseResource;
030import org.w3c.dom.ls.LSInput;
031import org.w3c.dom.ls.LSResourceResolver;
032import org.xml.sax.SAXException;
033import org.xml.sax.SAXNotRecognizedException;
034import org.xml.sax.SAXParseException;
035
036import javax.xml.XMLConstants;
037import javax.xml.transform.Source;
038import javax.xml.transform.stream.StreamSource;
039import javax.xml.validation.Schema;
040import javax.xml.validation.SchemaFactory;
041import javax.xml.validation.Validator;
042import java.io.ByteArrayInputStream;
043import java.io.IOException;
044import java.io.InputStream;
045import java.io.InputStreamReader;
046import java.io.StringReader;
047import java.nio.charset.StandardCharsets;
048import java.util.Collections;
049import java.util.HashMap;
050import java.util.HashSet;
051import java.util.Map;
052import java.util.Set;
053
054public class SchemaBaseValidator implements IValidatorModule {
055        public static final String RESOURCES_JAR_NOTE = "Note that as of HAPI FHIR 1.2, DSTU2 validation files are kept in a separate JAR (hapi-fhir-validation-resources-XXX.jar) which must be added to your classpath. See the HAPI FHIR download page for more information.";
056
057        private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SchemaBaseValidator.class);
058        private static final Set<String> SCHEMA_NAMES;
059
060        static {
061                HashSet<String> sn = new HashSet<>();
062                sn.add("xml.xsd");
063                sn.add("xhtml1-strict.xsd");
064                sn.add("fhir-single.xsd");
065                sn.add("fhir-xhtml.xsd");
066                sn.add("tombstone.xsd");
067                sn.add("opensearch.xsd");
068                sn.add("opensearchscore.xsd");
069                sn.add("xmldsig-core-schema.xsd");
070                SCHEMA_NAMES = Collections.unmodifiableSet(sn);
071        }
072
073        private final Map<String, Schema> myKeyToSchema = new HashMap<>();
074        private FhirContext myCtx;
075
076        public SchemaBaseValidator(FhirContext theContext) {
077                myCtx = theContext;
078        }
079
080        private void doValidate(IValidationContext<?> theContext) {
081                Schema schema = loadSchema();
082
083                try {
084                        Validator validator = schema.newValidator();
085                        MyErrorHandler handler = new MyErrorHandler(theContext);
086                        validator.setErrorHandler(handler);
087                        String encodedResource;
088                        if (theContext.getResourceAsStringEncoding() == EncodingEnum.XML) {
089                                encodedResource = theContext.getResourceAsString();
090                        } else {
091                                encodedResource = theContext.getFhirContext().newXmlParser().encodeResourceToString((IBaseResource) theContext.getResource());
092                        }
093
094                        try {
095                                /*
096                                 * See https://github.com/jamesagnew/hapi-fhir/issues/339
097                                 * https://www.owasp.org/index.php/XML_External_Entity_(XXE)_Processing
098                                 */
099                                validator.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, "");
100                                validator.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
101                        } catch (SAXNotRecognizedException ex) {
102                                ourLog.warn("Jaxp 1.5 Support not found.", ex);
103                        }
104
105                        validator.validate(new StreamSource(new StringReader(encodedResource)));
106                } catch (SAXParseException e) {
107                        SingleValidationMessage message = new SingleValidationMessage();
108                        message.setLocationLine(e.getLineNumber());
109                        message.setLocationCol(e.getColumnNumber());
110                        message.setMessage(e.getLocalizedMessage());
111                        message.setSeverity(ResultSeverityEnum.FATAL);
112                        theContext.addValidationMessage(message);
113                } catch (SAXException | IOException e) {
114                        // Catch all
115                        throw new ConfigurationException("Could not load/parse schema file", e);
116                }
117        }
118
119        private Schema loadSchema() {
120                String key = "fhir-single.xsd";
121
122                synchronized (myKeyToSchema) {
123                        Schema schema = myKeyToSchema.get(key);
124                        if (schema != null) {
125                                return schema;
126                        }
127
128                        Source baseSource = loadXml("fhir-single.xsd");
129
130                        SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
131                        schemaFactory.setResourceResolver(new MyResourceResolver());
132
133                        try {
134                                try {
135                                        /*
136                                         * See https://github.com/jamesagnew/hapi-fhir/issues/339
137                                         * https://www.owasp.org/index.php/XML_External_Entity_(XXE)_Processing
138                                         */
139                                        schemaFactory.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, "");
140                                } catch (SAXNotRecognizedException e) {
141                                        ourLog.warn("Jaxp 1.5 Support not found.", e);
142                                }
143                                schema = schemaFactory.newSchema(new Source[]{baseSource});
144                        } catch (SAXException e) {
145                                throw new ConfigurationException("Could not load/parse schema file: " + "fhir-single.xsd", e);
146                        }
147                        myKeyToSchema.put(key, schema);
148                        return schema;
149                }
150        }
151
152        Source loadXml(String theSchemaName) {
153                String pathToBase = myCtx.getVersion().getPathToSchemaDefinitions() + '/' + theSchemaName;
154                ourLog.debug("Going to load resource: {}", pathToBase);
155                try (InputStream baseIs = FhirValidator.class.getResourceAsStream(pathToBase)) {
156                        if (baseIs == null) {
157                                throw new InternalErrorException("Schema not found. " + RESOURCES_JAR_NOTE);
158                        }
159                        try (BOMInputStream bomInputStream = new BOMInputStream(baseIs, false)) {
160                                try (InputStreamReader baseReader = new InputStreamReader(bomInputStream, StandardCharsets.UTF_8)) {
161                                        // Buffer so that we can close the input stream
162                                        String contents = IOUtils.toString(baseReader);
163                                        return new StreamSource(new StringReader(contents), null);
164                                }
165                        }
166                } catch (IOException e) {
167                        throw new InternalErrorException(e);
168                }
169        }
170
171        @Override
172        public void validateResource(IValidationContext<IBaseResource> theContext) {
173                doValidate(theContext);
174        }
175
176        private final class MyResourceResolver implements LSResourceResolver {
177                private MyResourceResolver() {
178                }
179
180                @Override
181                public LSInput resolveResource(String theType, String theNamespaceURI, String thePublicId, String theSystemId, String theBaseURI) {
182                        if (theSystemId != null && SCHEMA_NAMES.contains(theSystemId)) {
183                                LSInputImpl input = new LSInputImpl();
184                                input.setPublicId(thePublicId);
185                                input.setSystemId(theSystemId);
186                                input.setBaseURI(theBaseURI);
187                                String pathToBase = myCtx.getVersion().getPathToSchemaDefinitions() + '/' + theSystemId;
188
189                                ourLog.debug("Loading referenced schema file: " + pathToBase);
190
191                                try (InputStream baseIs = FhirValidator.class.getResourceAsStream(pathToBase)) {
192                                        if (baseIs == null) {
193                                                throw new InternalErrorException("Schema file not found: " + pathToBase);
194                                        }
195
196                                        byte[] bytes = IOUtils.toByteArray(baseIs);
197                                        input.setByteStream(new ByteArrayInputStream(bytes));
198                                } catch (IOException e) {
199                                        throw new InternalErrorException(e);
200                                }
201                                return input;
202
203                        }
204
205                        throw new ConfigurationException("Unknown schema: " + theSystemId);
206                }
207        }
208
209        private static class MyErrorHandler implements org.xml.sax.ErrorHandler {
210
211                private IValidationContext<?> myContext;
212
213                MyErrorHandler(IValidationContext<?> theContext) {
214                        myContext = theContext;
215                }
216
217                private void addIssue(SAXParseException theException, ResultSeverityEnum theSeverity) {
218                        SingleValidationMessage message = new SingleValidationMessage();
219                        message.setLocationLine(theException.getLineNumber());
220                        message.setLocationCol(theException.getColumnNumber());
221                        message.setMessage(theException.getLocalizedMessage());
222                        message.setSeverity(theSeverity);
223                        myContext.addValidationMessage(message);
224                }
225
226                @Override
227                public void error(SAXParseException theException) {
228                        addIssue(theException, ResultSeverityEnum.ERROR);
229                }
230
231                @Override
232                public void fatalError(SAXParseException theException) {
233                        addIssue(theException, ResultSeverityEnum.FATAL);
234                }
235
236                @Override
237                public void warning(SAXParseException theException) {
238                        addIssue(theException, ResultSeverityEnum.WARNING);
239                }
240
241        }
242
243}