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}