001package ca.uhn.fhir.validation;
002
003/*
004 * #%L
005 * HAPI FHIR - Core Library
006 * %%
007 * Copyright (C) 2014 - 2019 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.IOException;
043import java.io.InputStream;
044import java.io.InputStreamReader;
045import java.io.StringReader;
046import java.nio.charset.StandardCharsets;
047import java.util.*;
048
049public class SchemaBaseValidator implements IValidatorModule {
050        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.";
051
052        private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SchemaBaseValidator.class);
053        private static final Set<String> SCHEMA_NAMES;
054
055        static {
056                HashSet<String> sn = new HashSet<>();
057                sn.add("xml.xsd");
058                sn.add("xhtml1-strict.xsd");
059                sn.add("fhir-single.xsd");
060                sn.add("fhir-xhtml.xsd");
061                sn.add("tombstone.xsd");
062                sn.add("opensearch.xsd");
063                sn.add("opensearchscore.xsd");
064                sn.add("xmldsig-core-schema.xsd");
065                SCHEMA_NAMES = Collections.unmodifiableSet(sn);
066        }
067
068        private final Map<String, Schema> myKeyToSchema = new HashMap<>();
069        private FhirContext myCtx;
070
071        public SchemaBaseValidator(FhirContext theContext) {
072                myCtx = theContext;
073        }
074
075        private void doValidate(IValidationContext<?> theContext) {
076                Schema schema = loadSchema();
077
078                try {
079                        Validator validator = schema.newValidator();
080                        MyErrorHandler handler = new MyErrorHandler(theContext);
081                        validator.setErrorHandler(handler);
082                        String encodedResource;
083                        if (theContext.getResourceAsStringEncoding() == EncodingEnum.XML) {
084                                encodedResource = theContext.getResourceAsString();
085                        } else {
086                                encodedResource = theContext.getFhirContext().newXmlParser().encodeResourceToString((IBaseResource) theContext.getResource());
087                        }
088
089                        try {
090                                /*
091                                 * See https://github.com/jamesagnew/hapi-fhir/issues/339
092                                 * https://www.owasp.org/index.php/XML_External_Entity_(XXE)_Processing
093                                 */
094                                validator.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, "");
095                                validator.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
096                        } catch (SAXNotRecognizedException ex) {
097                                ourLog.warn("Jaxp 1.5 Support not found.", ex);
098                        }
099
100                        validator.validate(new StreamSource(new StringReader(encodedResource)));
101                } catch (SAXParseException e) {
102                        SingleValidationMessage message = new SingleValidationMessage();
103                        message.setLocationLine(e.getLineNumber());
104                        message.setLocationCol(e.getColumnNumber());
105                        message.setMessage(e.getLocalizedMessage());
106                        message.setSeverity(ResultSeverityEnum.FATAL);
107                        theContext.addValidationMessage(message);
108                } catch (SAXException | IOException e) {
109                        // Catch all
110                        throw new ConfigurationException("Could not load/parse schema file", e);
111                }
112        }
113
114        private Schema loadSchema() {
115                String key = "fhir-single.xsd";
116
117                synchronized (myKeyToSchema) {
118                        Schema schema = myKeyToSchema.get(key);
119                        if (schema != null) {
120                                return schema;
121                        }
122
123                        Source baseSource = loadXml("fhir-single.xsd");
124
125                        SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
126                        schemaFactory.setResourceResolver(new MyResourceResolver());
127
128                        try {
129                                try {
130                                        /*
131                                         * See https://github.com/jamesagnew/hapi-fhir/issues/339
132                                         * https://www.owasp.org/index.php/XML_External_Entity_(XXE)_Processing
133                                         */
134                                        schemaFactory.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, "");
135                                } catch (SAXNotRecognizedException e) {
136                                        ourLog.warn("Jaxp 1.5 Support not found.", e);
137                                }
138                                schema = schemaFactory.newSchema(new Source[]{baseSource});
139                        } catch (SAXException e) {
140                                throw new ConfigurationException("Could not load/parse schema file: " + "fhir-single.xsd", e);
141                        }
142                        myKeyToSchema.put(key, schema);
143                        return schema;
144                }
145        }
146
147        Source loadXml(String theSchemaName) {
148                String pathToBase = myCtx.getVersion().getPathToSchemaDefinitions() + '/' + theSchemaName;
149                ourLog.debug("Going to load resource: {}", pathToBase);
150                try (InputStream baseIs = FhirValidator.class.getResourceAsStream(pathToBase)) {
151                        if (baseIs == null) {
152                                throw new InternalErrorException("Schema not found. " + RESOURCES_JAR_NOTE);
153                        }
154                        try (BOMInputStream bomInputStream = new BOMInputStream(baseIs, false)) {
155                                try (InputStreamReader baseReader = new InputStreamReader(bomInputStream, StandardCharsets.UTF_8)) {
156                                        // Buffer so that we can close the input stream
157                                        String contents = IOUtils.toString(baseReader);
158                                        return new StreamSource(new StringReader(contents), null);
159                                }
160                        }
161                } catch (IOException e) {
162                        throw new InternalErrorException(e);
163                }
164        }
165
166        @Override
167        public void validateResource(IValidationContext<IBaseResource> theContext) {
168                doValidate(theContext);
169        }
170
171        private final class MyResourceResolver implements LSResourceResolver {
172                private MyResourceResolver() {
173                }
174
175                @Override
176                public LSInput resolveResource(String theType, String theNamespaceURI, String thePublicId, String theSystemId, String theBaseURI) {
177                        if (theSystemId != null && SCHEMA_NAMES.contains(theSystemId)) {
178                                LSInputImpl input = new LSInputImpl();
179                                input.setPublicId(thePublicId);
180                                input.setSystemId(theSystemId);
181                                input.setBaseURI(theBaseURI);
182                                // String pathToBase = "ca/uhn/fhir/model/" + myVersion + "/schema/" + theSystemId;
183                                String pathToBase = myCtx.getVersion().getPathToSchemaDefinitions() + '/' + theSystemId;
184
185                                ourLog.debug("Loading referenced schema file: " + pathToBase);
186
187                                InputStream baseIs = FhirValidator.class.getResourceAsStream(pathToBase);
188                                if (baseIs == null) {
189                                        throw new InternalErrorException("Schema file not found: " + pathToBase);
190                                }
191
192                                input.setByteStream(baseIs);
193                                //FIXME resource leak
194                                return input;
195
196                        }
197
198                        throw new ConfigurationException("Unknown schema: " + theSystemId);
199                }
200        }
201
202        private static class MyErrorHandler implements org.xml.sax.ErrorHandler {
203
204                private IValidationContext<?> myContext;
205
206                MyErrorHandler(IValidationContext<?> theContext) {
207                        myContext = theContext;
208                }
209
210                private void addIssue(SAXParseException theException, ResultSeverityEnum theSeverity) {
211                        SingleValidationMessage message = new SingleValidationMessage();
212                        message.setLocationLine(theException.getLineNumber());
213                        message.setLocationCol(theException.getColumnNumber());
214                        message.setMessage(theException.getLocalizedMessage());
215                        message.setSeverity(theSeverity);
216                        myContext.addValidationMessage(message);
217                }
218
219                @Override
220                public void error(SAXParseException theException) {
221                        addIssue(theException, ResultSeverityEnum.ERROR);
222                }
223
224                @Override
225                public void fatalError(SAXParseException theException) {
226                        addIssue(theException, ResultSeverityEnum.FATAL);
227                }
228
229                @Override
230                public void warning(SAXParseException theException) {
231                        addIssue(theException, ResultSeverityEnum.WARNING);
232                }
233
234        }
235
236}