/****************************************************************************
* Copyright (C) 2012 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.ResponseType;
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.Socket;
import java.net.URISyntaxException;
import javax.annotation.Nonnull;
import javax.xml.namespace.QName;
import javax.xml.transform.TransformerException;
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.TlsAuthentication;
import org.openecard.bouncycastle.crypto.tls.TlsClientProtocol;
import org.openecard.common.DynamicContext;
import org.openecard.common.ECardConstants;
import org.openecard.control.module.tctoken.TR03112Keys;
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.crypto.tls.proxy.ProxySettings;
import org.openecard.common.util.FileUtils;
import org.openecard.control.module.tctoken.TlsConnectionHandler;
import org.openecard.crypto.tls.auth.DynamicAuthentication;
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;
/**
* 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 <johannes.schmoelz@ecsec.de>
* @author Tobias Wich <tobias.wich@ecsec.de>
* @author Dirk Petrautzki <petrautzki@hs-coburg.de>
*/
public class PAOS {
private static final Logger logger = LoggerFactory.getLogger(PAOS.class);
public static final String HEADER_KEY_PAOS = "PAOS";
// TODO: add service and actions as stated in https://dev.openecard.org/issues/191
public static final String HEADER_VALUE_PAOS = "ver=\"" + ECardConstants.PAOS_VERSION_20 + "\"";
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 MessageIdGenerator idGenerator;
private final WSMarshaller m;
private final Dispatcher dispatcher;
private final TlsConnectionHandler tlsHandler;
/**
* 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;
this.tlsHandler = tlsHandler;
try {
this.idGenerator = new MessageIdGenerator();
this.m = WSMarshallerFactory.createInstance();
} catch (WSMarshallerException e) {
logger.error(e.getMessage(), e);
throw new PAOSException(e);
}
}
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 MessageID in PAOS header.");
}
if (! idGenerator.setRemoteID(id)) {
// IDs don't match throw exception
throw new PAOSException("MessageID from result doesn't match.");
}
} 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);
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) {
logger.error(ex.getMessage(), ex);
throw new PAOSException(ex.getMessage(), ex);
} catch (IOException ex) {
logger.error(ex.getMessage(), ex);
throw new PAOSException(ex.getMessage(), ex);
} catch (SAXException ex) {
logger.error(ex.getMessage(), ex);
throw new PAOSException(ex.getMessage(), 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.
*/
public StartPAOSResponse sendStartPAOS(StartPAOS message) throws DispatcherException, PAOSException {
Object msg = message;
try {
// loop and send makes a computer happy
while (true) {
// set up connection to PAOS endpoint
StreamHttpClientConnection conn = openHttpStream();
HttpContext ctx = new BasicHttpContext();
HttpRequestExecutor httpexecutor = new HttpRequestExecutor();
DefaultConnectionReuseStrategy reuse = new DefaultConnectionReuseStrategy();
boolean isReusable;
// send as long as connection is valid
do {
// prepare request
String resource = tlsHandler.getResource();
BasicHttpEntityEnclosingRequest req = new BasicHttpEntityEnclosingRequest("POST", resource);
req.setParams(conn.getParams());
HttpRequestHelper.setDefaultHeader(req, tlsHandler.getServerAddress());
req.setHeader(HEADER_KEY_PAOS, HEADER_VALUE_PAOS);
// this is how it would be correct
//req.setHeader("Accept", "text/html;q=0.2, application/vnd.paos+xml");
// and this is how it works :-/
req.setHeader("Accept", "text/html; 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
HttpResponse response = httpexecutor.execute(req, conn, ctx);
int statusCode = response.getStatusLine().getStatusCode();
conn.receiveResponseEntity(response);
HttpEntity entity = response.getEntity();
byte[] entityData = FileUtils.toByteArray(entity.getContent());
HttpUtils.dumpHttpResponse(logger, response, entityData);
checkHTTPStatusCode(msg, statusCode);
// consume entity
Object requestObj = processPAOSRequest(new ByteArrayInputStream(entityData));
// break when message is startpaosresponse
if (requestObj instanceof StartPAOSResponse) {
StartPAOSResponse startPAOSResponse = (StartPAOSResponse) requestObj;
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);
} while (isReusable);
}
} catch (HttpException ex) {
throw new PAOSException("Failed to deliver or receive PAOS HTTP message.", ex);
} catch (IOException ex) {
throw new PAOSException(ex);
} catch (SOAPException ex) {
throw new PAOSException("Failed to create SOAP message instance from given JAXB message.", ex);
} catch (URISyntaxException ex) {
throw new PAOSException("Hostname or port of the remote server are invalid.", ex);
} catch (MarshallingTypeException ex) {
throw new DispatcherException("Failed to marshal JAXB object.", ex);
} catch (InvocationTargetException ex) {
throw new DispatcherException("The dispatched method threw an exception.", ex);
} catch (TransformerException ex) {
throw new DispatcherException(ex);
} catch (WSException ex) {
throw new PAOSException(ex);
}
}
private StreamHttpClientConnection openHttpStream()
throws IOException, URISyntaxException {
StreamHttpClientConnection conn;
if (tlsHandler.usesTls()) {
TlsClientProtocol handler = tlsHandler.createTlsConnection();
conn = new StreamHttpClientConnection(handler.getInputStream(), handler.getOutputStream());
saveServiceCertificate();
} else {
// TODO: remove some time
// no TLS
Socket socket = ProxySettings.getDefault().getSocket(tlsHandler.getHostname(), tlsHandler.getPort());
conn = new StreamHttpClientConnection(socket.getInputStream(), socket.getOutputStream());
}
return conn;
}
/**
* Stores the received eService certificate as {@link Certificate} in the dynamic context.
* This will only take place when {@link DynamicAuthentication} is used as {@link TlsAuthentication}.
*/
private void saveServiceCertificate() {
try {
DynamicContext dynCtx = DynamicContext.getInstance(TR03112Keys.INSTANCE_KEY);
TlsAuthentication authentication = tlsHandler.getTlsClient().getAuthentication();
if (authentication instanceof DynamicAuthentication) {
DynamicAuthentication noAuth = (DynamicAuthentication) authentication;
// server certificate is the first one in the chain
org.openecard.bouncycastle.crypto.tls.Certificate certificate = noAuth.getServerCertificate();
dynCtx.put(TR03112Keys.ESERVICE_CERTIFICATE, certificate);
} else {
String msg = "eService Certificate not saved in DynamicContext.";
logger.debug(msg);
}
} catch (IOException e) {
logger.error("Certificate couldn't be encoded.", e);
}
}
/**
* Check the status code returned from the server.
* If the status code indicates an error, a PAOSException will be thrown.
*
* @param msg The last message we sent to the server
* @param statusCode The status code we received from the server
* @throws PAOSException If the server returned a HTTP error code
*/
private void checkHTTPStatusCode(Object msg, int statusCode) throws PAOSException {
if (statusCode < 200 || statusCode > 299) {
if (msg instanceof ResponseType) {
ResponseType resp = (ResponseType) msg;
try {
WSHelper.checkResult(resp);
} catch (WSException ex) {
throw new PAOSException("Received HTML Error Code " + statusCode, ex);
}
}
throw new PAOSException("Received HTML Error Code " + statusCode);
}
}
}