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}