/****************************************************************************
* Copyright (C) 2012-2015 ecsec GmbH.
* All rights reserved.
* Contact: ecsec GmbH (info@ecsec.de)
*
* This file is part of the Open eCard App.
*
* GNU General Public License Usage
* This file may be used under the terms of the GNU General Public
* License version 3.0 as published by the Free Software Foundation
* and appearing in the file LICENSE.GPL included in the packaging of
* this file. Please review the following information to ensure the
* GNU General Public License version 3.0 requirements will be met:
* http://www.gnu.org/copyleft/gpl.html.
*
* Other Usage
* Alternatively, this file may be used in accordance with the terms
* and conditions contained in a signed written agreement between
* you and ecsec GmbH.
*
***************************************************************************/
package org.openecard.transport.paos;
import iso.std.iso_iec._24727.tech.schema.StartPAOS;
import iso.std.iso_iec._24727.tech.schema.StartPAOSResponse;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.net.URISyntaxException;
import javax.annotation.Nonnull;
import javax.xml.namespace.QName;
import javax.xml.transform.TransformerException;
import oasis.names.tc.dss._1_0.core.schema.ResponseBaseType;
import org.openecard.apache.http.HttpEntity;
import org.openecard.apache.http.HttpException;
import org.openecard.apache.http.HttpResponse;
import org.openecard.apache.http.entity.ContentType;
import org.openecard.apache.http.entity.StringEntity;
import org.openecard.apache.http.impl.DefaultConnectionReuseStrategy;
import org.openecard.apache.http.message.BasicHttpEntityEnclosingRequest;
import org.openecard.apache.http.protocol.BasicHttpContext;
import org.openecard.apache.http.protocol.HttpContext;
import org.openecard.apache.http.protocol.HttpRequestExecutor;
import org.openecard.bouncycastle.crypto.tls.TlsClientProtocol;
import org.openecard.common.ECardConstants;
import org.openecard.common.WSHelper;
import org.openecard.common.WSHelper.WSException;
import org.openecard.common.interfaces.Dispatcher;
import org.openecard.common.interfaces.DispatcherException;
import org.openecard.common.util.FileUtils;
import org.openecard.binding.tctoken.TlsConnectionHandler;
import org.openecard.transport.httpcore.HttpRequestHelper;
import org.openecard.transport.httpcore.HttpUtils;
import org.openecard.transport.httpcore.StreamHttpClientConnection;
import org.openecard.ws.marshal.MarshallingTypeException;
import org.openecard.ws.marshal.WSMarshaller;
import org.openecard.ws.marshal.WSMarshallerException;
import org.openecard.ws.marshal.WSMarshallerFactory;
import org.openecard.ws.soap.SOAPException;
import org.openecard.ws.soap.SOAPHeader;
import org.openecard.ws.soap.SOAPMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.xml.sax.SAXException;
import static org.openecard.binding.tctoken.ex.ErrorTranslations.*;
/**
* PAOS implementation for JAXB types.
* This implementation can be configured to speak TLS by creating the instance with a TlsClient. The dispatcher instance
* is used to deliver the messages to the instances implementing the webservice interfaces.
*
* @author Johannes Schmoelz
* @author Tobias Wich
* @author Dirk Petrautzki
* @author Hans-Martin Haase
*/
public class PAOS {
private static final Logger logger = LoggerFactory.getLogger(PAOS.class);
public static final String HEADER_KEY_PAOS = "PAOS";
public static final QName RELATES_TO = new QName(ECardConstants.WS_ADDRESSING, "RelatesTo");
public static final QName REPLY_TO = new QName(ECardConstants.WS_ADDRESSING, "ReplyTo");
public static final QName MESSAGE_ID = new QName(ECardConstants.WS_ADDRESSING, "MessageID");
public static final QName ADDRESS = new QName(ECardConstants.WS_ADDRESSING, "Address");
public static final QName PAOS_PAOS = new QName(ECardConstants.PAOS_VERSION_20, "PAOS");
public static final QName PAOS_VERSION = new QName(ECardConstants.PAOS_VERSION_20, "Version");
public static final QName PAOS_ENDPOINTREF = new QName(ECardConstants.PAOS_VERSION_20, "EndpointReference");
public static final QName PAOS_ADDRESS = new QName(ECardConstants.PAOS_VERSION_20, "Address");
public static final QName PAOS_METADATA = new QName(ECardConstants.PAOS_VERSION_20, "MetaData");
public static final QName PAOS_SERVICETYPE = new QName(ECardConstants.PAOS_VERSION_20, "ServiceType");
private final String headerValuePaos;
private final MessageIdGenerator idGenerator;
private final WSMarshaller m;
private final Dispatcher dispatcher;
private final TlsConnectionHandler tlsHandler;
private final String serviceString;
/**
* Creates a PAOS instance and configures it for a given endpoint.
* If tlsClient is not null the connection must be HTTPs, else HTTP.
*
* @param dispatcher The dispatcher instance capable of dispatching the received messages.
* @param tlsHandler The TlsClient containing the configuration of the yet to be established TLS channel, or
* {@code null} if TLS should not be used.
* @throws PAOSException In case the PAOS module could not be initialized.
*/
public PAOS(@Nonnull Dispatcher dispatcher, @Nonnull TlsConnectionHandler tlsHandler) throws PAOSException {
this.dispatcher = dispatcher.getFilter();
this.tlsHandler = tlsHandler;
serviceString = buildServiceString();
headerValuePaos = String.format("ver=\"%s\" %s", ECardConstants.PAOS_VERSION_20, serviceString);
try {
this.idGenerator = new MessageIdGenerator();
this.m = WSMarshallerFactory.createInstance();
} catch (WSMarshallerException ex) {
logger.error(ex.getMessage(), ex);
throw new PAOSException(ex);
}
}
private String getRelatesTo(SOAPMessage msg) throws SOAPException {
return getHeaderElement(msg, RELATES_TO);
}
private void setRelatesTo(SOAPMessage msg, String value) throws SOAPException {
Element elem = getHeaderElement(msg, RELATES_TO, true);
elem.setTextContent(value);
}
private String getHeaderElement(SOAPMessage msg, QName elem) throws SOAPException {
Element headerElem = getHeaderElement(msg, elem, false);
return (headerElem == null) ? null : headerElem.getTextContent().trim();
}
private Element getHeaderElement(SOAPMessage msg, QName elem, boolean create) throws SOAPException {
Element result = null;
SOAPHeader h = msg.getSOAPHeader();
// try to find a header
for (Element e : h.getChildElements()) {
if (e.getLocalName().equals(elem.getLocalPart()) && e.getNamespaceURI().equals(elem.getNamespaceURI())) {
result = e;
break;
}
}
// if no such element in header, create new
if (result == null && create) {
result = h.addHeaderElement(elem);
}
return result;
}
private void addMessageIDs(SOAPMessage msg) throws SOAPException {
String otherID = idGenerator.getRemoteID();
String newID = idGenerator.createNewID(); // also swaps messages in
// MessageIdGenerator
if (otherID != null) {
// add relatesTo element
setRelatesTo(msg, otherID);
}
// add MessageID element
setMessageID(msg, newID);
}
private void updateMessageID(SOAPMessage msg) throws PAOSException {
try {
String id = getMessageID(msg);
if (id == null) {
throw new PAOSException(NO_MESSAGE_ID);
}
if (! idGenerator.setRemoteID(id)) {
// IDs don't match throw exception
throw new PAOSException(MESSAGE_ID_MISSMATCH);
}
} catch (SOAPException e) {
logger.error(e.getMessage(), e);
throw new PAOSException(e.getMessage(), e);
}
}
private String getMessageID(SOAPMessage msg) throws SOAPException {
return getHeaderElement(msg, MESSAGE_ID);
}
private void setMessageID(SOAPMessage msg, String value) throws SOAPException {
Element elem = getHeaderElement(msg, MESSAGE_ID, true);
elem.setTextContent(value);
}
private Object processPAOSRequest(InputStream content) throws PAOSException {
try {
Document doc = m.str2doc(content);
SOAPMessage msg = m.doc2soap(doc);
// msg.getSOAPHeader().
updateMessageID(msg);
if (logger.isDebugEnabled()) {
try {
logger.debug("Message received:\n{}", m.doc2str(doc));
} catch (TransformerException ex) {
logger.warn("Failed to log PAOS request message.", ex);
}
}
return m.unmarshal(msg.getSOAPBody().getChildElements().get(0));
} catch (MarshallingTypeException ex) {
logger.error(ex.getMessage(), ex);
throw new PAOSException(ex.getMessage(), ex);
} catch (WSMarshallerException ex) {
String msg = "Failed to read/process message from PAOS server.";
logger.error(msg, ex);
throw new PAOSException(MARSHALLING_ERROR, ex);
} catch (IOException | SAXException ex) {
String msg = "Failed to read/process message from PAOS server.";
logger.error(msg, ex);
throw new PAOSException(SOAP_MESSAGE_FAILURE, ex);
}
}
private String createPAOSResponse(Object obj) throws MarshallingTypeException, SOAPException, TransformerException {
SOAPMessage msg = createSOAPMessage(obj);
String result = m.doc2str(msg.getDocument());
logger.debug("Message sent:\n{}", result);
return result;
}
private SOAPMessage createSOAPMessage(Object content) throws MarshallingTypeException, SOAPException {
Document contentDoc = m.marshal(content);
SOAPMessage msg = m.add2soap(contentDoc);
SOAPHeader header = msg.getSOAPHeader();
// fill header with paos stuff
Element paos = header.addHeaderElement(PAOS_PAOS);
paos.setAttributeNS(ECardConstants.SOAP_ENVELOPE, "actor", ECardConstants.ACTOR_NEXT);
paos.setAttributeNS(ECardConstants.SOAP_ENVELOPE, "mustUnderstand", "1");
Element version = header.addChildElement(paos, PAOS_VERSION);
version.setTextContent(ECardConstants.PAOS_VERSION_20);
Element endpointReference = header.addChildElement(paos, PAOS_ENDPOINTREF);
Element address = header.addChildElement(endpointReference, PAOS_ADDRESS);
address.setTextContent("http://www.projectliberty.org/2006/01/role/paos");
Element metaData = header.addChildElement(endpointReference, PAOS_METADATA);
Element serviceType = header.addChildElement(metaData, PAOS_SERVICETYPE);
serviceType.setTextContent(ECardConstants.PAOS_NEXT);
Element replyTo = header.addHeaderElement(REPLY_TO);
address = header.addChildElement(replyTo, ADDRESS);
address.setTextContent("http://www.projectliberty.org/2006/02/role/paos");
// add message IDs
addMessageIDs(msg);
return msg;
}
/**
* Sends start PAOS and answers all successor messages to the server associated with this instance.
* Messages are exchanged until the server replies with a {@code StartPAOSResponse} message.
*
* @param message The StartPAOS message which is sent in the first message.
* @return The {@code StartPAOSResponse} message from the server.
* @throws DispatcherException In case there errors with the message conversion or the dispatcher.
* @throws PAOSException In case there were errors in the transport layer.
* @throws PAOSConnectionException
*/
public StartPAOSResponse sendStartPAOS(StartPAOS message) throws DispatcherException, PAOSException,
PAOSConnectionException {
Object msg = message;
StreamHttpClientConnection conn = null;
HttpContext ctx = new BasicHttpContext();
HttpRequestExecutor httpexecutor = new HttpRequestExecutor();
DefaultConnectionReuseStrategy reuse = new DefaultConnectionReuseStrategy();
boolean firstLoop = true;
boolean connectionDropped = false;
ResponseBaseType lastResponse = null;
try {
// loop and send makes a computer happy
while (true) {
if (! firstLoop && tlsHandler.isSameChannel()) {
throw new PAOSException(CONNECTION_CLOSED);
}
firstLoop = false;
// set up connection to PAOS endpoint
conn = openHttpStream();
boolean isReusable;
// send as long as connection is valid
try {
do {
// save the last message we sent to the eID-Server.
if (msg instanceof ResponseBaseType) {
lastResponse = (ResponseBaseType) msg;
}
// prepare request
String resource = tlsHandler.getResource();
BasicHttpEntityEnclosingRequest req = new BasicHttpEntityEnclosingRequest("POST", resource);
HttpRequestHelper.setDefaultHeader(req, tlsHandler.getServerAddress());
req.setHeader(HEADER_KEY_PAOS, headerValuePaos);
req.setHeader("Accept", "text/xml, application/xml, application/vnd.paos+xml");
ContentType reqContentType = ContentType.create("application/vnd.paos+xml", "UTF-8");
HttpUtils.dumpHttpRequest(logger, "before adding content", req);
String reqMsgStr = createPAOSResponse(msg);
StringEntity reqMsg = new StringEntity(reqMsgStr, reqContentType);
req.setEntity(reqMsg);
req.setHeader(reqMsg.getContentType());
req.setHeader("Content-Length", Long.toString(reqMsg.getContentLength()));
// send request and receive response
logger.debug("Sending HTTP request.");
HttpResponse response = httpexecutor.execute(req, conn, ctx);
logger.debug("HTTP response received.");
int statusCode = response.getStatusLine().getStatusCode();
try {
checkHTTPStatusCode(statusCode);
} catch (PAOSConnectionException ex) {
// The eID-Server or at least the test suite may have aborted the communication after an
// response with error. So check the status of our last response to the eID-Server
if (lastResponse != null) {
WSHelper.checkResult(lastResponse);
}
throw ex;
}
conn.receiveResponseEntity(response);
HttpEntity entity = response.getEntity();
byte[] entityData = FileUtils.toByteArray(entity.getContent());
HttpUtils.dumpHttpResponse(logger, response, entityData);
// consume entity
Object requestObj = processPAOSRequest(new ByteArrayInputStream(entityData));
// break when message is startpaosresponse
if (requestObj instanceof StartPAOSResponse) {
StartPAOSResponse startPAOSResponse = (StartPAOSResponse) requestObj;
// Some eID-Servers ignore error from previous steps so check whether our last message was ok.
// This does not in case we sent a correct message with wrong content and the eID-Server returns
// an ok.
if (lastResponse != null) {
WSHelper.checkResult(lastResponse);
}
WSHelper.checkResult(startPAOSResponse);
return startPAOSResponse;
}
// send via dispatcher
msg = dispatcher.deliver(requestObj);
// check if connection can be used one more time
isReusable = reuse.keepAlive(response, ctx);
connectionDropped = false;
} while (isReusable);
} catch (IOException ex) {
if (! connectionDropped) {
connectionDropped = true;
logger.warn("PAOS server closed the connection. Trying to connect again. (Try {})");
} else {
String errMsg = "Error in the link to the PAOS server.";
logger.error(errMsg);
throw new PAOSException(DELIVERY_FAILED, ex);
}
}
}
} catch (HttpException ex) {
throw new PAOSException(DELIVERY_FAILED, ex);
} catch (SOAPException ex) {
throw new PAOSException(SOAP_MESSAGE_FAILURE, ex);
} catch (MarshallingTypeException ex) {
throw new PAOSDispatcherException(MARSHALLING_ERROR, ex);
} catch (InvocationTargetException ex) {
throw new PAOSDispatcherException(DISPATCHER_ERROR, ex);
} catch (TransformerException ex) {
throw new DispatcherException(ex);
} catch (WSException ex) {
throw new PAOSException(ex);
} finally {
try {
if (conn != null) {
conn.close();
}
} catch (IOException ex) {
// throw new PAOSException(ex);
}
}
}
private StreamHttpClientConnection openHttpStream() throws PAOSConnectionException {
StreamHttpClientConnection conn;
try {
logger.debug("Opening connection to PAOS server.");
TlsClientProtocol handler = tlsHandler.createTlsConnection();
conn = new StreamHttpClientConnection(handler.getInputStream(), handler.getOutputStream());
logger.debug("Connection to PAOS server established.");
return conn;
} catch (IOException | URISyntaxException ex) {
throw new PAOSConnectionException(ex);
}
}
/**
* Check the status code returned from the server.
* If the status code indicates an error, a PAOSException will be thrown.
*
* @param statusCode The status code we received from the server
* @throws PAOSException If the server returned a HTTP error code
*/
private void checkHTTPStatusCode(int statusCode) throws PAOSConnectionException {
// Check the result code. According to the PAOS Spec section 9.4 the server has to send 202
// All tested test servers return 200 so accept both but generate a warning message in case of 200
if (statusCode != 200 && statusCode != 202) {
throw new PAOSConnectionException(INVALID_HTTP_STATUS, statusCode);
} else if (statusCode == 200) {
String msg2 = "The PAOS endpoint sent the http status code 200 which does not conform to the "
+ "PAOS specification. (See section 9.4 Processing Rules of the PAOS Specification)";
logger.warn(msg2);
}
}
/**
* Creates a String with all available services.
*
* @return A String containing all available services.
*/
private String buildServiceString() {
StringBuilder builder = new StringBuilder();
for (String service : dispatcher.getServiceList()) {
builder.append(";");
builder.append('"');
builder.append(service);
builder.append('"');
}
return builder.toString();
}
}