/* * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. * Copyright (c) 2013, MPL CodeInside http://codeinside.ru */ package ru.codeinside.gws.core; import org.w3c.dom.Document; import org.w3c.dom.DocumentFragment; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import ru.codeinside.gws.api.CryptoProvider; import ru.codeinside.gws.api.Enclosure; import ru.codeinside.gws.api.InfoSystem; import ru.codeinside.gws.api.Packet; import ru.codeinside.gws.api.Revision; import ru.codeinside.gws.api.RouterPacket; import ru.codeinside.gws.core.enclosure.AppliedDocumentType; import ru.codeinside.gws.core.enclosure.AppliedDocumentsType; import ru.codeinside.gws.core.enclosure.ObjectFactory; import javax.xml.bind.DatatypeConverter; import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBElement; import javax.xml.bind.JAXBException; import javax.xml.bind.Marshaller; import javax.xml.bind.Unmarshaller; import javax.xml.datatype.DatatypeConfigurationException; import javax.xml.datatype.DatatypeFactory; import javax.xml.namespace.QName; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.soap.AttachmentPart; import javax.xml.soap.SOAPBodyElement; import javax.xml.soap.SOAPElement; import javax.xml.soap.SOAPException; import javax.xml.soap.SOAPHeader; import javax.xml.soap.SOAPMessage; import javax.xml.soap.SOAPPart; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.StringReader; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.GregorianCalendar; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; /** * Общие методы разбора. */ final public class Xml { final public static String REV111111 = "http://smev.gosuslugi.ru/rev111111"; final public static String REV120315 = "http://smev.gosuslugi.ru/rev120315"; public static DocumentFragment parseXml(Document doc, String xml) throws SAXException, IOException, ParserConfigurationException { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); factory.setNamespaceAware(true); Document d = factory.newDocumentBuilder().parse( new InputSource(new StringReader("<fragment>" + xml + "</fragment>"))); // Импорт узлов для совместимости с документом final Node node = doc.importNode(d.getDocumentElement(), true); //doc.adoptNode(d.getDocumentElement()) DocumentFragment docfrag = doc.createDocumentFragment(); while (node.hasChildNodes()) { docfrag.appendChild(node.removeChild(node.getFirstChild())); } return docfrag; } public static TextContent getTextContent(final Element element, final String ns, final String name) { NodeList nodes = element.getElementsByTagNameNS(ns, name); if (nodes.getLength() > 0) { final Node node = nodes.item(0); if (node instanceof Element) { final Element child = (Element) node; return new TextContent(child.getTextContent()); } } return null; } /** * Получить текст для "правильного" элемента. */ public static String getRequiredText(Element element, String ns, String name) { NodeList nodes = element.getElementsByTagNameNS(ns, name); final Element text = (Element) nodes.item(0); return text.getTextContent(); } public static String parseOptionalTextContent(Element element, String ns, String name) { TextContent textContent = getTextContent(element, ns, name); if (textContent != null) { return textContent.text; } return null; } public static byte[] parseOptionalBase64Content(Element element, String ns, String name) { final TextContent textContent = getTextContent(element, ns, name); if (textContent != null && textContent.text != null) { return DatatypeConverter.parseBase64Binary(textContent.text.trim()); } return null; } public static Element parseAction(Element body) { NodeList childs = body.getChildNodes(); if (childs.getLength() > 0) { for (int i = 0; i < childs.getLength(); i++) { final Node item = childs.item(i); if (item instanceof Element) { return (Element) item; } } } return null; } public static Element getFirstElement(Element parent, String ns, String name) { final NodeList nodes = parent.getElementsByTagNameNS(ns, name); if (nodes.getLength() != 1) { return null; } final Node node = nodes.item(0); if (node instanceof Element) { return (Element) node; } return null; } public static void addSystem(SOAPElement smevSender, String code, String name) throws SOAPException { smevSender.addChildElement("Code", "smev").setValue(code); smevSender.addChildElement("Name", "smev").setValue(name); } public static void fillSmevMessageByPacket(final SOAPElement action, Packet packet, Revision revision) throws SOAPException, DatatypeConfigurationException { final SOAPElement smevMessage = action.addChildElement("Message", "smev"); addSystem(smevMessage.addChildElement("Sender", "smev"), packet.sender.code, packet.sender.name); addSystem(smevMessage.addChildElement("Recipient", "smev"), packet.recipient.code, packet.recipient.name); if (packet.originator != null) { addSystem(smevMessage.addChildElement("Originator", "smev"), packet.originator.code, packet.originator.name); } if (revision == Revision.rev120315) { if (packet.serviceName != null) { smevMessage.addChildElement("ServiceName", "smev").setValue(packet.serviceName); } } smevMessage.addChildElement("TypeCode", "smev").setValue(packet.typeCode.getName()); smevMessage.addChildElement("Status", "smev").setValue(packet.status.name()); GregorianCalendar gc = new GregorianCalendar(); gc.setTime(packet.date); smevMessage.addChildElement("Date", "smev").setValue( DatatypeFactory.newInstance().newXMLGregorianCalendar(gc).toXMLFormat()); smevMessage.addChildElement("ExchangeType", "smev").setValue(packet.exchangeType); if (packet.requestIdRef != null) { smevMessage.addChildElement("RequestIdRef", "smev").setValue(packet.requestIdRef); } if (packet.originRequestIdRef != null) { smevMessage.addChildElement("OriginRequestIdRef", "smev").setValue(packet.originRequestIdRef); } if (packet.serviceCode != null) { smevMessage.addChildElement("ServiceCode", "smev").setValue(packet.serviceCode); } if (packet.caseNumber != null) { smevMessage.addChildElement("CaseNumber", "smev").setValue(packet.caseNumber); } // для ревизии rev120315 не используем SubMessages:smev if (packet.testMsg != null) { smevMessage.addChildElement("TestMsg", "smev").setValue(packet.testMsg); } if (packet.oktmo != null) { smevMessage.addChildElement("OKTMO", "smev").setValue(packet.oktmo); } } public static Packet parseSmevMessage(final Element action, final Revision revision) { final String smevNs = revisionToNs(revision); Packet packet = new Packet(); Element message = getFirstElement(action, smevNs, "Message"); if (message == null) { throw new IllegalStateException("Ошибка в <smev:Message>"); } Element sender = getFirstElement(message, smevNs, "Sender"); Element recipient = getFirstElement(message, smevNs, "Recipient"); if (sender == null) { throw new IllegalStateException("Ошибка в <smev:Sender>"); } if (recipient == null) { throw new IllegalStateException("Ошибка в <smev:Recipient>"); } packet.sender = parseInfoSystem(sender, smevNs); packet.recipient = parseInfoSystem(recipient, smevNs); { Element originator = getFirstElement(message, smevNs, "Originator"); if (originator != null) { packet.originator = parseInfoSystem(originator, smevNs); } } TextContent typeCode = getTextContent(message, smevNs, "TypeCode"); for (final Packet.Type type : Packet.Type.values()) { if (type.getName().equals(typeCode.text)) { packet.typeCode = type; break; } } TextContent status = getTextContent(message, smevNs, "Status"); packet.status = Packet.Status.valueOf(status.text); TextContent date = getTextContent(message, smevNs, "Date"); packet.date = DatatypeConverter.parseDateTime(date.text).getTime(); packet.exchangeType = getTextContent(message, smevNs, "ExchangeType").text; packet.requestIdRef = trimToNull(parseOptionalTextContent(message, smevNs, "RequestIdRef")); packet.originRequestIdRef = trimToNull(parseOptionalTextContent(message, smevNs, "OriginRequestIdRef")); if (revision == Revision.rev120315) { packet.serviceName = parseOptionalTextContent(message, smevNs, "ServiceName"); } packet.serviceCode = parseOptionalTextContent(message, smevNs, "ServiceCode"); packet.caseNumber = parseOptionalTextContent(message, smevNs, "CaseNumber"); packet.testMsg = parseOptionalTextContent(message, smevNs, "TestMsg"); packet.oktmo = parseOptionalTextContent(message, smevNs, "OKTMO"); return packet; } private static String revisionToNs(Revision revision) { final String smevNs; if (revision == Revision.rev111111) { smevNs = REV111111; } else { smevNs = REV120315; } return smevNs; } private static InfoSystem parseInfoSystem(final Element element, String ns) { final TextContent code = getTextContent(element, ns, "Code"); final TextContent name = getTextContent(element, ns, "Name"); if (code == null || name == null) { return null; } return new InfoSystem(code.text, name.text); } static public RouterPacket parseRouterPacket(final SOAPHeader soapHeader, final Revision revision) { if (soapHeader != null) { final String smevNs = revisionToNs(revision); final NodeList headers = soapHeader.getElementsByTagNameNS(smevNs, "Header"); if (headers.getLength() > 0) { final Element header = (Element) headers.item(0); final TextContent messageId = getTextContent(header, smevNs, "MessageId"); final TextContent timeStamp = getTextContent(header, smevNs, "TimeStamp"); final TextContent nodeId = getTextContent(header, smevNs, "NodeId"); final TextContent messageClass = getTextContent(header, smevNs, "MessageClass"); if (messageId == null || timeStamp == null || nodeId == null || messageClass == null) { throw new IllegalStateException("Ошибка роутера СМЭВ: пропущен блок"); } // TODO: проверить wsu:Id на достоверность подписи, нужа поддержка криптопровайдера RouterPacket routerPacket = new RouterPacket(); routerPacket.messageId = messageId.text; routerPacket.nodeId = nodeId.text; routerPacket.timeStamp = DatatypeConverter.parseDateTime(timeStamp.text).getTime(); routerPacket.direction = RouterPacket.parseDirection(messageClass.text); return routerPacket; } } return null; } public static Element getAppData(final Element messageData, final Revision revision) { final String smevNs = revisionToNs(revision); Element appData = getFirstElement(messageData, smevNs, "AppData"); if (appData != null && appData.getChildNodes().getLength() > 0) { try { final DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); dbf.setNamespaceAware(true); final Document doc = dbf.newDocumentBuilder().newDocument(); appData = (Element) doc.importNode(appData, true); //if (appData.getPrefix() != null) { // appData = (Element) doc.renameNode(appData, "", "AppData"); //} doc.appendChild(appData); return appData; } catch (Exception e) { throw new RuntimeException(e); } } return null; } public static MessageDataContent processMessageData(SOAPMessage message, Element action, Revision revision, CryptoProvider cryptoProvider) throws SOAPException { final String smevNs = revisionToNs(revision); final MessageDataContent mdc = new MessageDataContent(); final Element messageData = getFirstElement(action, smevNs, "MessageData"); if (messageData != null) { final Element appDocument = getFirstElement(messageData, smevNs, "AppDocument"); if (appDocument != null) { Map<String, Enclosure> attachments = Collections.emptyMap(); mdc.requestCode = parseOptionalTextContent(appDocument, smevNs, "RequestCode"); final byte[] binaryData = parseOptionalBase64Content(appDocument, smevNs, "BinaryData"); if (binaryData != null) { attachments = Zip.collectAttachments(new ByteArrayInputStream(binaryData)); } else { Element reference = getFirstElement(appDocument, smevNs, "Reference"); if (reference != null) { attachments = parseAttachments(message, smevNs, appDocument, reference); } } final Set<String> files = new LinkedHashSet<String>(); for (final String file : attachments.keySet()) { if (!file.endsWith(".sig")) { files.add(file); } } final ArrayList<Enclosure> enclosures = new ArrayList<Enclosure>(files.size()); // это можно вызывать уже ПОСЛЕ так как не зависит напрямую от протокола! if (mdc.requestCode != null) { final String metaName = mdc.requestCode + ".xml"; if (attachments.containsKey(metaName)) { final AppliedDocumentsType types = unmarshallMetadata(attachments.get(metaName).content); if (types != null) { for (final AppliedDocumentType doc : types.getAppliedDocument()) { final String zipName = doc.getURL(); final Enclosure enclosure; if (!attachments.containsKey(zipName)) { enclosure = new Enclosure(zipName, new byte[0]); attachments.put(zipName, enclosure); files.add(zipName); } else { enclosure = attachments.get(zipName); } enclosure.id = doc.getID(); enclosure.digest = doc.getDigestValue(); enclosure.mimeType = doc.getType(); enclosure.number = doc.getNumber(); enclosure.fileName = doc.getName(); enclosure.code = doc.getCodeDocument(); } } } } for (final String file : files) { final Enclosure enclosure = attachments.get(file); enclosures.add(enclosure); final String pkcs7File = file + ".sig"; if (attachments.containsKey(pkcs7File)) { Enclosure sig = attachments.get(pkcs7File); enclosure.signature = cryptoProvider.fromPkcs7(sig.content); if (!cryptoProvider.validate(enclosure.signature, enclosure.digest, enclosure.content)) { if (enclosure.signature != null) enclosure.signature = enclosure.signature.toInvalid(); } } } mdc.attachmens = enclosures; } mdc.appData = getAppData(messageData, revision); } return mdc; } private static Map<String, Enclosure> parseAttachments(SOAPMessage message, String smevNs, Element appDocument, Element reference) throws SOAPException { Map<String, Enclosure> attachments = Collections.emptyMap(); Element include = getFirstElement(reference, "http://www.w3.org/2004/08/xop/include", "Include"); byte[] digestValue = parseOptionalBase64Content(appDocument, smevNs, "DigestValue"); String href = trimToNull(include.getAttribute("href")); if (href != null && href.startsWith("cid:")) { String cid = href.substring(4); // skip "cid:" String quotedCid = "<" + cid + ">"; Iterator<AttachmentPart> soapAttachments = message.getAttachments(); while (soapAttachments.hasNext()) { AttachmentPart attachmentPart = soapAttachments.next(); String contentId = attachmentPart.getContentId(); if (cid.equals(contentId) || quotedCid.equals(contentId)) { String contentType = attachmentPart.getContentType(); if ("application/zip".equals(contentType)) { boolean digestValid = true; if (digestValue != null) { try { MessageDigest digest = MessageDigest.getInstance("GOST3411"); digest.update(DatatypeConverter.printBase64Binary(attachmentPart.getRawContentBytes()).getBytes()); digestValid = Arrays.equals(digestValue, digest.digest()); } catch (NoSuchAlgorithmException e) { Logger.getLogger(Enclosure.class.getName()).warning("Отсутствует GOST3411, нельзя проверить целостность" + cid); } } if (digestValid) { attachments = Zip.collectAttachments(attachmentPart.getRawContent()); } else { Logger.getLogger(Enclosure.class.getName()).warning("Нарушение целостности " + cid); } } else { Logger.getLogger(Enclosure.class.getName()).info("invalid contentType " + contentType + " for " + cid); } break; } } } else { Logger.getLogger(Enclosure.class.getName()).info("invalid href in Include: " + href); } return attachments; } public static void addMessageData( String appData, String enclosureDescriptor, Enclosure[] enclosures, SOAPBodyElement action, SOAPPart part, CryptoProvider cryptoProvider, Revision revision) throws SOAPException, ParserConfigurationException, SAXException, IOException { final boolean hasAppData = appData != null; final boolean hasEnclosures = enclosures != null && enclosures.length > 0; if (hasAppData || hasEnclosures) { final SOAPElement messageData = action.addChildElement("MessageData", "smev"); if (hasAppData) { if (appData.contains("AppData>")) { appData = appData.replaceAll("<AppData Id=\"AppData\">", "").replaceAll("</AppData>", ""); messageData .addChildElement("AppData", "smev").addAttribute(new QName("Id"), "AppData") .appendChild(Xml.parseXml(part, appData)); } else { messageData .addChildElement("AppData", "smev") .appendChild(Xml.parseXml(part, appData)); } } if (hasEnclosures) { final SOAPElement appDocument = messageData.addChildElement("AppDocument", "smev"); // RequestCode ввели после rev110801 final boolean requestCodeRequired = revision != Revision.rev110801; String requestCode = null; boolean needDescriptor = false; if (requestCodeRequired) { requestCode = enclosureDescriptor == null ? "metadata" : enclosureDescriptor; for (final Enclosure enclosure : enclosures) { if (enclosure.id != null || enclosure.code != null || enclosure.fileName != null || enclosure.digest != null || enclosure.number != null) { needDescriptor = true; } } if (needDescriptor) { final String zipPath = requestCode + ".xml"; for (final Enclosure enclosure : enclosures) { if (zipPath.equals(enclosure.zipPath)) { needDescriptor = false; break; } } } appDocument .addChildElement("RequestCode", "smev") .setValue(requestCode); } final Enclosure[] enclosures2; if (!needDescriptor) { enclosures2 = enclosures; } else { enclosures2 = new Enclosure[enclosures.length + 1]; for (int i = 0; i < enclosures.length; i++) { enclosures2[1 + i] = enclosures[i]; } final AppliedDocumentsType docs = new AppliedDocumentsType(); for (final Enclosure enclosure : enclosures) { final AppliedDocumentType doc = new AppliedDocumentType(); doc.setURL(enclosure.zipPath); doc.setID(enclosure.id); doc.setDigestValue(enclosure.digest); doc.setType(enclosure.mimeType); doc.setNumber(enclosure.number); doc.setName(enclosure.fileName); doc.setCodeDocument(enclosure.code); docs.getAppliedDocument().add(doc); } enclosures2[0] = new Enclosure(requestCode + ".xml", marshallMetadata(docs)); } appDocument .addChildElement("BinaryData", "smev") .setValue(Zip.toBinaryData(enclosures2, cryptoProvider)); } } } private static AppliedDocumentsType unmarshallMetadata(final byte[] content) { try { final JAXBContext ctx = JAXBContext.newInstance(ObjectFactory.class); Unmarshaller unmarshaller = ctx.createUnmarshaller(); Object object = unmarshaller.unmarshal(new ByteArrayInputStream(content)); if (object instanceof JAXBElement) { object = ((JAXBElement<?>) object).getValue(); } return (AppliedDocumentsType) object; } catch (JAXBException e) { // Logger.getLogger(Xml.class.getName()).log(Level.WARNING, "parse meta fail", e); System.err.println("Ошибка в процессе преобразования(unmarshalling) данных."); return null; } } private static byte[] marshallMetadata(final AppliedDocumentsType metadata) { try { final JAXBContext ctx = JAXBContext.newInstance(ObjectFactory.class); final Marshaller marshaller = ctx.createMarshaller(); final ByteArrayOutputStream baos = new ByteArrayOutputStream(); marshaller.marshal(metadata, baos); return baos.toByteArray(); } catch (JAXBException e) { Logger.getLogger(Xml.class.getName()).log(Level.WARNING, "marshall meta fail", e); return null; } } private static String trimToNull(final String text) { if (text == null) { return null; } final String trimmed = text.trim(); if (trimmed.isEmpty()) { return null; } return trimmed; } /** * "Носитель" текста */ final public static class TextContent { final public String text; TextContent(String text) { this.text = text; } @Override public String toString() { return text; } } /** * "Носитель" данных */ final public static class MessageDataContent { public String requestCode; public List<Enclosure> attachmens; public Element appData; } }