From a0f97683b6a892db7cd1904159f3600c75b664e2 Mon Sep 17 00:00:00 2001 From: Gabriel Landais <glandais@kereval.com> Date: Tue, 14 Feb 2012 14:39:34 +0000 Subject: [PATCH] git-svn-id: https://scm.gforge.inria.fr/authscm/ycadoret/svn/gazelle/Maven/gazelle-proxy/trunk@26328 356b4b1a-1d2b-0410-8bf1-ffa24008f01e --- .../ihe/gazelle/proxy/gui/MessageBean.java | 9 +- .../proxy/listeners/HL7EventListener.java | 2 +- .../proxy/listeners/MessageListener.java | 2 +- .../MessageListenerSaveAbstract.java | 32 +- .../proxy/listeners/RawEventListener.java | 2 +- .../proxy/listeners/SyslogEventListener.java | 28 +- .../proxy/model/message/SyslogMessage.java | 95 +++- gazelle-proxy-netty/pom.xml | 14 +- .../protocols/raw/RawEventListenerSimple.java | 23 + .../netty/protocols/syslog/SyslogData.java | 127 ++++++ .../protocols/syslog/SyslogFrameDecoder.java | 57 +++ .../netty/protocols/syslog/SyslogProxy.java | 33 +- .../tools/SyslogEventListenerSimple.java | 11 +- .../java/net/ihe/gazelle/proxy/netty/App.java | 137 +++++- .../netty/syslog/AuthSSLSocketFactory.java | 257 +++++++++++ .../netty/syslog/AuthSSLX509TrustManager.java | 140 ++++++ .../proxy/netty/syslog/KeystoreDetails.java | 138 ++++++ .../proxy/netty/syslog/KeystoreManager.java | 430 ++++++++++++++++++ .../src/test/resources/keys/clientKeyStore | Bin 0 -> 1402 bytes .../src/test/resources/keys/serverKeyStore | Bin 0 -> 1367 bytes 20 files changed, 1459 insertions(+), 78 deletions(-) create mode 100644 gazelle-proxy-netty/src/main/java/net/ihe/gazelle/proxy/netty/protocols/raw/RawEventListenerSimple.java create mode 100644 gazelle-proxy-netty/src/main/java/net/ihe/gazelle/proxy/netty/protocols/syslog/SyslogData.java create mode 100644 gazelle-proxy-netty/src/main/java/net/ihe/gazelle/proxy/netty/protocols/syslog/SyslogFrameDecoder.java create mode 100644 gazelle-proxy-netty/src/test/java/net/ihe/gazelle/proxy/netty/syslog/AuthSSLSocketFactory.java create mode 100644 gazelle-proxy-netty/src/test/java/net/ihe/gazelle/proxy/netty/syslog/AuthSSLX509TrustManager.java create mode 100644 gazelle-proxy-netty/src/test/java/net/ihe/gazelle/proxy/netty/syslog/KeystoreDetails.java create mode 100644 gazelle-proxy-netty/src/test/java/net/ihe/gazelle/proxy/netty/syslog/KeystoreManager.java create mode 100644 gazelle-proxy-netty/src/test/resources/keys/clientKeyStore create mode 100644 gazelle-proxy-netty/src/test/resources/keys/serverKeyStore diff --git a/gazelle-proxy-jar/src/main/java/net/ihe/gazelle/proxy/gui/MessageBean.java b/gazelle-proxy-jar/src/main/java/net/ihe/gazelle/proxy/gui/MessageBean.java index 053bed57..85604732 100644 --- a/gazelle-proxy-jar/src/main/java/net/ihe/gazelle/proxy/gui/MessageBean.java +++ b/gazelle-proxy-jar/src/main/java/net/ihe/gazelle/proxy/gui/MessageBean.java @@ -30,6 +30,7 @@ import org.apache.commons.httpclient.methods.multipart.MultipartRequestEntity; import org.apache.commons.httpclient.methods.multipart.Part; import org.apache.commons.httpclient.methods.multipart.PartSource; import org.apache.commons.lang.StringEscapeUtils; +import org.apache.log4j.Logger; import org.jboss.seam.ScopeType; import org.jboss.seam.annotations.In; import org.jboss.seam.annotations.Name; @@ -42,6 +43,8 @@ import org.jboss.seam.international.StatusMessage; @Scope(ScopeType.PAGE) public class MessageBean { + private static final Logger log = Logger.getLogger(MessageBean.class); + @In private EntityManager entityManager; @@ -227,7 +230,6 @@ public class MessageBean { e.printStackTrace(); } // redirect the user to the good page - boolean fail = false; if (status == HttpStatus.SC_OK) { try { String key = filePost.getResponseBodyAsString(); @@ -238,11 +240,8 @@ public class MessageBean { response.sendRedirect(url); } catch (IOException e) { - fail = true; - e.printStackTrace(); + log.error(e); } - } else { - fail = true; } } } diff --git a/gazelle-proxy-jar/src/main/java/net/ihe/gazelle/proxy/listeners/HL7EventListener.java b/gazelle-proxy-jar/src/main/java/net/ihe/gazelle/proxy/listeners/HL7EventListener.java index 30581299..4e2a1f29 100644 --- a/gazelle-proxy-jar/src/main/java/net/ihe/gazelle/proxy/listeners/HL7EventListener.java +++ b/gazelle-proxy-jar/src/main/java/net/ihe/gazelle/proxy/listeners/HL7EventListener.java @@ -17,7 +17,7 @@ public class HL7EventListener extends SameEventListener<String> { requestChannelId, responseChannelId, side); hl7message.setMessageReceivedAsString(message); - messageListener.processMessageSimple(hl7message); + messageListener.processMessage(hl7message); } } } diff --git a/gazelle-proxy-jar/src/main/java/net/ihe/gazelle/proxy/listeners/MessageListener.java b/gazelle-proxy-jar/src/main/java/net/ihe/gazelle/proxy/listeners/MessageListener.java index 9ddb8162..328ccafa 100644 --- a/gazelle-proxy-jar/src/main/java/net/ihe/gazelle/proxy/listeners/MessageListener.java +++ b/gazelle-proxy-jar/src/main/java/net/ihe/gazelle/proxy/listeners/MessageListener.java @@ -6,7 +6,7 @@ import net.ihe.gazelle.proxy.model.message.AbstractMessage; public interface MessageListener { - void processMessageSimple(AbstractMessage abstractMessage); + void processMessage(AbstractMessage abstractMessage); EntityManager getEntityManager(); diff --git a/gazelle-proxy-jar/src/main/java/net/ihe/gazelle/proxy/listeners/MessageListenerSaveAbstract.java b/gazelle-proxy-jar/src/main/java/net/ihe/gazelle/proxy/listeners/MessageListenerSaveAbstract.java index 42d4ad1e..88f2f494 100644 --- a/gazelle-proxy-jar/src/main/java/net/ihe/gazelle/proxy/listeners/MessageListenerSaveAbstract.java +++ b/gazelle-proxy-jar/src/main/java/net/ihe/gazelle/proxy/listeners/MessageListenerSaveAbstract.java @@ -6,8 +6,6 @@ import net.ihe.gazelle.proxy.model.message.AbstractMessage; public abstract class MessageListenerSaveAbstract implements MessageListener { - private static org.apache.log4j.Logger log = org.apache.log4j.Logger.getLogger(MessageListenerSaveAbstract.class); - @Override public void processMessage(EntityManager em, AbstractMessage abstractMessage) { if (abstractMessage.getId() == null) { @@ -18,38 +16,10 @@ public abstract class MessageListenerSaveAbstract implements MessageListener { } @Override - public void processMessageSimple(AbstractMessage abstractMessage) { + public void processMessage(AbstractMessage abstractMessage) { EntityManager em = getEntityManager(); processMessage(em, abstractMessage); releaseEntityManager(em); } - /* - public void processMessage(EntityManager em, AbstractMessage abstractMessage) { - if (em == null || abstractMessage == null) { - log.fatal("entityManager null or abstractMessage null"); - return null; - } - AbstractMessage messageToReturn = null; - synchronized (em) { - - boolean transactionWasActive = false; - - EntityTransaction transaction = em.getTransaction(); - synchronized (transaction) { - if (!transaction.isActive()) { - transaction.begin(); - } else - transactionWasActive = true; - em.persist(abstractMessage); - messageToReturn = em.merge(abstractMessage); - - if (!transactionWasActive) { - transaction.commit(); - } - } - } - return messageToReturn; - } - */ } diff --git a/gazelle-proxy-jar/src/main/java/net/ihe/gazelle/proxy/listeners/RawEventListener.java b/gazelle-proxy-jar/src/main/java/net/ihe/gazelle/proxy/listeners/RawEventListener.java index 8cf79ee7..16aaad56 100644 --- a/gazelle-proxy-jar/src/main/java/net/ihe/gazelle/proxy/listeners/RawEventListener.java +++ b/gazelle-proxy-jar/src/main/java/net/ihe/gazelle/proxy/listeners/RawEventListener.java @@ -17,7 +17,7 @@ public class RawEventListener extends SameEventListener<byte[]> { requestChannelId, responseChannelId, side); rawmessage.setMessageReceived(message); - messageListener.processMessageSimple(rawmessage); + messageListener.processMessage(rawmessage); } } diff --git a/gazelle-proxy-jar/src/main/java/net/ihe/gazelle/proxy/listeners/SyslogEventListener.java b/gazelle-proxy-jar/src/main/java/net/ihe/gazelle/proxy/listeners/SyslogEventListener.java index 6ec18454..8953d855 100644 --- a/gazelle-proxy-jar/src/main/java/net/ihe/gazelle/proxy/listeners/SyslogEventListener.java +++ b/gazelle-proxy-jar/src/main/java/net/ihe/gazelle/proxy/listeners/SyslogEventListener.java @@ -1,23 +1,35 @@ package net.ihe.gazelle.proxy.listeners; +import net.ihe.gazelle.proxy.model.message.RawMessage; import net.ihe.gazelle.proxy.model.message.SyslogMessage; import net.ihe.gazelle.proxy.netty.channel.ProxySide; +import net.ihe.gazelle.proxy.netty.protocols.syslog.SyslogData; -public class SyslogEventListener extends SameEventListener<String> { +public class SyslogEventListener extends GazelleProxyEventListener<SyslogData, byte[]> { public SyslogEventListener(MessageListener messageListener) { super(messageListener); } - protected void saveMessage(String message, String requesterIp, int requesterPort, int proxyPort, - String responderIp, int responderPort, int requestChannelId, int responseChannelId, ProxySide side) { - // Messages are coming from different threads! + @Override + public void onRequest(SyslogData request, String requesterIp, int requesterPort, int proxyProviderPort, + String responderIp, int responderPort, int providerChannelId, int consumerChannelId) { synchronized (this) { - SyslogMessage syslogMessage = new SyslogMessage(requesterIp, requesterPort, proxyPort, responderIp, - responderPort, requestChannelId, responseChannelId, side); - syslogMessage.setMessageReceivedAsString(message); - messageListener.processMessageSimple(syslogMessage); + SyslogMessage syslogMessage = new SyslogMessage(requesterIp, requesterPort, proxyProviderPort, responderIp, + responderPort, providerChannelId, consumerChannelId, request); + messageListener.processMessage(syslogMessage); } } + @Override + public void onResponse(byte[] response, String requesterIp, int requesterPort, int proxyProviderPort, + String responderIp, int responderPort, int providerChannelId, int consumerChannelId) { + // Messages are coming from different threads! + synchronized (this) { + RawMessage rawmessage = new RawMessage(requesterIp, requesterPort, proxyProviderPort, responderIp, + responderPort, providerChannelId, consumerChannelId, ProxySide.RESPONSE); + rawmessage.setMessageReceived(response); + messageListener.processMessage(rawmessage); + } + } } diff --git a/gazelle-proxy-jar/src/main/java/net/ihe/gazelle/proxy/model/message/SyslogMessage.java b/gazelle-proxy-jar/src/main/java/net/ihe/gazelle/proxy/model/message/SyslogMessage.java index d2766ff2..069b5bbe 100644 --- a/gazelle-proxy-jar/src/main/java/net/ihe/gazelle/proxy/model/message/SyslogMessage.java +++ b/gazelle-proxy-jar/src/main/java/net/ihe/gazelle/proxy/model/message/SyslogMessage.java @@ -19,6 +19,7 @@ import javax.persistence.Entity; import net.ihe.gazelle.proxy.netty.ChannelType; import net.ihe.gazelle.proxy.netty.channel.ProxySide; +import net.ihe.gazelle.proxy.netty.protocols.syslog.SyslogData; import org.jboss.seam.annotations.Name; @@ -28,17 +29,107 @@ public class SyslogMessage extends AbstractMessage implements java.io.Serializab private static final long serialVersionUID = -2767782636271713844L; + private Integer facility; + + private String hostName; + + private Integer severity; + + private String timestamp; + + private String tag; + + private String appName; + + private String messageId; + + private String procId; + public SyslogMessage() { super(); + setProxySide(ProxySide.REQUEST); } public SyslogMessage(String fromIP, Integer localPort, Integer proxyPort, String toIP, Integer remotePort, - Integer requestChannelId, Integer responseChannelId, ProxySide proxySide) { - super(fromIP, localPort, proxyPort, toIP, remotePort, requestChannelId, responseChannelId, proxySide); + Integer requestChannelId, Integer responseChannelId, SyslogData request) { + super(fromIP, localPort, proxyPort, toIP, remotePort, requestChannelId, responseChannelId, ProxySide.REQUEST); + this.facility = request.getFacility(); + this.hostName = request.getHostName(); + this.severity = request.getSeverity(); + this.timestamp = request.getTimestamp(); + this.tag = request.getTag(); + this.appName = request.getAppName(); + this.messageId = request.getMessageId(); + this.procId = request.getProcId(); + setMessageReceivedAsString(request.getMessage()); } public ChannelType getChannelType() { return ChannelType.SYSLOG; } + public Integer getFacility() { + return facility; + } + + public void setFacility(Integer facility) { + this.facility = facility; + } + + public String getHostName() { + return hostName; + } + + public void setHostName(String hostName) { + this.hostName = hostName; + } + + public Integer getSeverity() { + return severity; + } + + public void setSeverity(Integer severity) { + this.severity = severity; + } + + public String getTimestamp() { + return timestamp; + } + + public void setTimestamp(String timestamp) { + this.timestamp = timestamp; + } + + public String getTag() { + return tag; + } + + public void setTag(String tag) { + this.tag = tag; + } + + public String getAppName() { + return appName; + } + + public void setAppName(String appName) { + this.appName = appName; + } + + public String getMessageId() { + return messageId; + } + + public void setMessageId(String messageId) { + this.messageId = messageId; + } + + public String getProcId() { + return procId; + } + + public void setProcId(String procId) { + this.procId = procId; + } + } diff --git a/gazelle-proxy-netty/pom.xml b/gazelle-proxy-netty/pom.xml index 11fabe65..d3fbc91e 100644 --- a/gazelle-proxy-netty/pom.xml +++ b/gazelle-proxy-netty/pom.xml @@ -1,4 +1,5 @@ -<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <groupId>net.ihe.gazelle.proxy</groupId> @@ -37,6 +38,17 @@ <artifactId>ihej-dicom</artifactId> <version>1.0</version> </dependency> + <dependency> + <groupId>org.openhealthtools.openatna</groupId> + <artifactId>syslog-core</artifactId> + <version>1.2</version> + </dependency> + <dependency> + <groupId>org.openhealthtools.openatna</groupId> + <artifactId>syslog-mina</artifactId> + <version>1.2</version> + <scope>test</scope> + </dependency> <dependency> <groupId>apache-log4j</groupId> <artifactId>log4j</artifactId> diff --git a/gazelle-proxy-netty/src/main/java/net/ihe/gazelle/proxy/netty/protocols/raw/RawEventListenerSimple.java b/gazelle-proxy-netty/src/main/java/net/ihe/gazelle/proxy/netty/protocols/raw/RawEventListenerSimple.java new file mode 100644 index 00000000..773cf539 --- /dev/null +++ b/gazelle-proxy-netty/src/main/java/net/ihe/gazelle/proxy/netty/protocols/raw/RawEventListenerSimple.java @@ -0,0 +1,23 @@ +package net.ihe.gazelle.proxy.netty.protocols.raw; + +import java.io.PrintStream; + +import net.ihe.gazelle.proxy.netty.ProxyEventListenerToStream; + +public class RawEventListenerSimple extends ProxyEventListenerToStream<byte[], byte[]> { + + public RawEventListenerSimple(PrintStream printStream) { + super(printStream); + } + + @Override + protected String decodeResponse(byte[] response) { + return new String(response); + } + + @Override + protected String decodeRequest(byte[] request) { + return new String(request); + } + +} diff --git a/gazelle-proxy-netty/src/main/java/net/ihe/gazelle/proxy/netty/protocols/syslog/SyslogData.java b/gazelle-proxy-netty/src/main/java/net/ihe/gazelle/proxy/netty/protocols/syslog/SyslogData.java new file mode 100644 index 00000000..c6392f39 --- /dev/null +++ b/gazelle-proxy-netty/src/main/java/net/ihe/gazelle/proxy/netty/protocols/syslog/SyslogData.java @@ -0,0 +1,127 @@ +package net.ihe.gazelle.proxy.netty.protocols.syslog; + +import java.io.ByteArrayOutputStream; + +import org.openhealthtools.openatna.syslog.LogMessage; +import org.openhealthtools.openatna.syslog.SyslogException; +import org.openhealthtools.openatna.syslog.SyslogMessage; +import org.openhealthtools.openatna.syslog.bsd.BsdMessage; +import org.openhealthtools.openatna.syslog.protocol.ProtocolMessage; + +public class SyslogData { + + private int facility; + private String hostName; + private int severity; + private String timestamp; + private String message; + private String tag; + private String appName; + private String messageId; + private String procId; + + public SyslogData(SyslogMessage<?> syslogMessage) { + super(); + + facility = syslogMessage.getFacility(); + hostName = syslogMessage.getHostName(); + severity = syslogMessage.getSeverity(); + timestamp = syslogMessage.getTimestamp(); + + LogMessage<?> logMessage = syslogMessage.getMessage(); + message = null; + if (logMessage != null) { + Object messageObject = logMessage.getMessageObject(); + + if (messageObject instanceof String) { + message = (String) messageObject; + } else { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + try { + logMessage.write(bos); + message = new String(bos.toByteArray()); + } catch (SyslogException e) { + message = null; + } + } + } + tag = null; + if (syslogMessage instanceof BsdMessage) { + BsdMessage<?> bsdMessage = (BsdMessage<?>) syslogMessage; + tag = bsdMessage.getTag(); + } + + appName = null; + messageId = null; + procId = null; + if (syslogMessage instanceof ProtocolMessage) { + ProtocolMessage<?> protocolMessage = (ProtocolMessage<?>) syslogMessage; + appName = protocolMessage.getAppName(); + messageId = protocolMessage.getMessageId(); + procId = protocolMessage.getProcId(); + } + + } + + public int getFacility() { + return facility; + } + + public String getHostName() { + return hostName; + } + + public int getSeverity() { + return severity; + } + + public String getTimestamp() { + return timestamp; + } + + public String getMessage() { + return message; + } + + public String getTag() { + return tag; + } + + public String getAppName() { + return appName; + } + + public String getMessageId() { + return messageId; + } + + public String getProcId() { + return procId; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("SyslogData [facility="); + builder.append(facility); + builder.append(", hostName="); + builder.append(hostName); + builder.append(", severity="); + builder.append(severity); + builder.append(", timestamp="); + builder.append(timestamp); + builder.append(", message="); + builder.append(message); + builder.append(", tag="); + builder.append(tag); + builder.append(", appName="); + builder.append(appName); + builder.append(", messageId="); + builder.append(messageId); + builder.append(", procId="); + builder.append(procId); + builder.append("]"); + return builder.toString(); + } + +} diff --git a/gazelle-proxy-netty/src/main/java/net/ihe/gazelle/proxy/netty/protocols/syslog/SyslogFrameDecoder.java b/gazelle-proxy-netty/src/main/java/net/ihe/gazelle/proxy/netty/protocols/syslog/SyslogFrameDecoder.java new file mode 100644 index 00000000..d952089d --- /dev/null +++ b/gazelle-proxy-netty/src/main/java/net/ihe/gazelle/proxy/netty/protocols/syslog/SyslogFrameDecoder.java @@ -0,0 +1,57 @@ +package net.ihe.gazelle.proxy.netty.protocols.syslog; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; + +import org.jboss.netty.buffer.ChannelBuffer; +import org.jboss.netty.channel.Channel; +import org.jboss.netty.channel.ChannelHandlerContext; +import org.jboss.netty.handler.codec.frame.FrameDecoder; +import org.openhealthtools.openatna.syslog.SyslogMessage; +import org.openhealthtools.openatna.syslog.SyslogMessageFactory; + +class SyslogFrameDecoder extends FrameDecoder { + + private static final int STATUS_SIZE = 0; + private static final int STATUS_READ = 1; + private static final int STATUS_END = 2; + + private int status = STATUS_SIZE; + private StringBuffer messageSizeBuffer = new StringBuffer(); + private int messageSize; + private ByteArrayOutputStream currentFrame = null; + + @Override + protected Object decode(ChannelHandlerContext ctx, Channel channel, ChannelBuffer buffer) throws Exception { + while (buffer.readableBytes() > 0) { + byte readByte = buffer.readByte(); + switch (status) { + case STATUS_SIZE: + if (readByte == 32) { + status = STATUS_READ; + messageSize = Integer.parseInt(messageSizeBuffer.toString()); + currentFrame = new ByteArrayOutputStream(); + } else { + char c = (char) readByte; + messageSizeBuffer.append(c); + } + break; + case STATUS_READ: + if (currentFrame.size() == messageSize) { + status = STATUS_END; + byte[] bytes = currentFrame.toByteArray(); + SyslogMessage<?> syslogMessage = SyslogMessageFactory.getFactory().read( + new ByteArrayInputStream(bytes)); + return new SyslogData(syslogMessage); + } + currentFrame.write(readByte); + break; + case STATUS_END: + return null; + default: + break; + } + } + return null; + } +} diff --git a/gazelle-proxy-netty/src/main/java/net/ihe/gazelle/proxy/netty/protocols/syslog/SyslogProxy.java b/gazelle-proxy-netty/src/main/java/net/ihe/gazelle/proxy/netty/protocols/syslog/SyslogProxy.java index 72aac403..efca8468 100644 --- a/gazelle-proxy-netty/src/main/java/net/ihe/gazelle/proxy/netty/protocols/syslog/SyslogProxy.java +++ b/gazelle-proxy-netty/src/main/java/net/ihe/gazelle/proxy/netty/protocols/syslog/SyslogProxy.java @@ -6,16 +6,14 @@ import java.util.List; import net.ihe.gazelle.proxy.netty.ConnectionConfig; import net.ihe.gazelle.proxy.netty.Proxy; import net.ihe.gazelle.proxy.netty.ProxyEventListener; +import net.ihe.gazelle.proxy.netty.channel.ProxyTools; import org.jboss.netty.buffer.ChannelBuffer; -import org.jboss.netty.buffer.ChannelBuffers; import org.jboss.netty.channel.ChannelHandler; -import org.jboss.netty.handler.codec.frame.DelimiterBasedFrameDecoder; -import org.jboss.netty.util.CharsetUtil; -public class SyslogProxy extends Proxy<String, String> { +public class SyslogProxy extends Proxy<SyslogData, byte[]> { - public SyslogProxy(ProxyEventListener<String, String> proxyEventListener, ConnectionConfig connectionConfig) { + public SyslogProxy(ProxyEventListener<SyslogData, byte[]> proxyEventListener, ConnectionConfig connectionConfig) { super(proxyEventListener, connectionConfig); } @@ -23,32 +21,31 @@ public class SyslogProxy extends Proxy<String, String> { List<ChannelHandler> channels = new ArrayList<ChannelHandler>(); // Decode syslog messages - channels.add(new DelimiterBasedFrameDecoder(1024 * 1024, getSyslogDelimiter())); + channels.add(new SyslogFrameDecoder()); return channels; } - public static ChannelBuffer[] getSyslogDelimiter() { - return new ChannelBuffer[] { ChannelBuffers.wrappedBuffer("auditmessage".getBytes(CharsetUtil.UTF_8)) }; - } - public List<ChannelHandler> getDecodersForResponse() { - return getDecodersForRequest(); + return new ArrayList<ChannelHandler>(); } @Override - public String handleRequest(Object message) { - if (message instanceof ChannelBuffer) { - ChannelBuffer cb = (ChannelBuffer) message; - String result = cb.toString(CharsetUtil.UTF_8); - return result; + public SyslogData handleRequest(Object message) { + if (message instanceof SyslogData) { + SyslogData syslogData = (SyslogData) message; + return syslogData; } return null; } @Override - public String handleResponse(Object message) { - return handleRequest(message); + public byte[] handleResponse(Object message) { + if (message instanceof ChannelBuffer) { + ChannelBuffer cb = (ChannelBuffer) message; + return ProxyTools.getBytes(cb); + } + return null; } } diff --git a/gazelle-proxy-netty/src/main/java/net/ihe/gazelle/proxy/netty/tools/SyslogEventListenerSimple.java b/gazelle-proxy-netty/src/main/java/net/ihe/gazelle/proxy/netty/tools/SyslogEventListenerSimple.java index 2f8e528e..8db74ac2 100644 --- a/gazelle-proxy-netty/src/main/java/net/ihe/gazelle/proxy/netty/tools/SyslogEventListenerSimple.java +++ b/gazelle-proxy-netty/src/main/java/net/ihe/gazelle/proxy/netty/tools/SyslogEventListenerSimple.java @@ -3,21 +3,22 @@ package net.ihe.gazelle.proxy.netty.tools; import java.io.PrintStream; import net.ihe.gazelle.proxy.netty.ProxyEventListenerToStream; +import net.ihe.gazelle.proxy.netty.protocols.syslog.SyslogData; -public class SyslogEventListenerSimple extends ProxyEventListenerToStream<String, String> { +public class SyslogEventListenerSimple extends ProxyEventListenerToStream<SyslogData, byte[]> { public SyslogEventListenerSimple(PrintStream printStream) { super(printStream); } @Override - protected String decodeResponse(String response) { - return response; + protected String decodeRequest(SyslogData request) { + return request.toString(); } @Override - protected String decodeRequest(String request) { - return request; + protected String decodeResponse(byte[] request) { + return new String(request); } } diff --git a/gazelle-proxy-netty/src/test/java/net/ihe/gazelle/proxy/netty/App.java b/gazelle-proxy-netty/src/test/java/net/ihe/gazelle/proxy/netty/App.java index 3649b19d..4eb227a8 100644 --- a/gazelle-proxy-netty/src/test/java/net/ihe/gazelle/proxy/netty/App.java +++ b/gazelle-proxy-netty/src/test/java/net/ihe/gazelle/proxy/netty/App.java @@ -1,6 +1,12 @@ package net.ihe.gazelle.proxy.netty; +import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; +import java.net.Socket; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; import jp.digitalsensation.ihej.transactionmonitor.dicom.messageexchange.DimseMessage; import net.ihe.gazelle.proxy.netty.basictls.ConnectionConfigSimpleTls; @@ -8,14 +14,29 @@ import net.ihe.gazelle.proxy.netty.basictls.TlsConfig; import net.ihe.gazelle.proxy.netty.basictls.TlsCredentials; import net.ihe.gazelle.proxy.netty.protocols.dicom.DicomProxy; import net.ihe.gazelle.proxy.netty.protocols.http.HttpProxy; +import net.ihe.gazelle.proxy.netty.protocols.raw.RawEventListenerSimple; +import net.ihe.gazelle.proxy.netty.protocols.raw.RawProxy; +import net.ihe.gazelle.proxy.netty.syslog.AuthSSLSocketFactory; +import net.ihe.gazelle.proxy.netty.syslog.KeystoreDetails; import net.ihe.gazelle.proxy.netty.tools.DicomEventListenerSimple; import net.ihe.gazelle.proxy.netty.tools.HttpEventListenerSimple; +import org.openhealthtools.openatna.syslog.Constants; +import org.openhealthtools.openatna.syslog.SyslogException; +import org.openhealthtools.openatna.syslog.SyslogMessage; +import org.openhealthtools.openatna.syslog.message.StringLogMessage; +import org.openhealthtools.openatna.syslog.mina.tls.TlsServer; +import org.openhealthtools.openatna.syslog.protocol.ProtocolMessage; +import org.openhealthtools.openatna.syslog.protocol.SdParam; +import org.openhealthtools.openatna.syslog.protocol.StructuredElement; +import org.openhealthtools.openatna.syslog.transport.SyslogListener; + public class App { public static void main(String[] args) throws Exception { // testHttpsConnection(); - startDicomProxyTLS(10004); + // startDicomProxyTLS(10004); + startSyslogProxyTLS(); /* startHTTPProxyBasic(10000); @@ -45,11 +66,103 @@ public class App { } + private static void startSyslogProxyTLS() { + // Syslog Client -TLS> Proxy1 -> Proxy Web app -> Proxy3 -TLS> Syslog + // Server + + // Starts a Syslog server (8443) + try { + AuthSSLSocketFactory serverSocketFactory = getSyslogServerSocketFactory(); + org.openhealthtools.openatna.syslog.mina.tls.TlsConfig serverConfig = new org.openhealthtools.openatna.syslog.mina.tls.TlsConfig(); + serverConfig.setSSLContext(serverSocketFactory.getSSLContext()); + serverConfig.setHost("localhost"); + serverConfig.setPort(8443); + TlsServer server = new TlsServer(); + server.configure(serverConfig); + server.addSyslogListener(new Listener()); + server.start(); + } catch (IOException e) { + e.printStackTrace(); + } + + RawEventListenerSimple listener = new RawEventListenerSimple(System.out); + + // Starts proxy3 + InputStream clientKeyStoreStream = listener.getClass().getResourceAsStream("/keys/clientKeyStore"); + TlsCredentials clientCredentials = new TlsCredentials(clientKeyStoreStream, "clientStorePass".toCharArray(), + "myClientCert", "password".toCharArray()); + TlsConfig tlsConfigClient = new TlsConfig(null, true, clientCredentials); + ConnectionConfig connectionConfigClient = new ConnectionConfigSimpleTls(8442, "127.0.0.1", 8443, + ChannelType.SYSLOG, tlsConfigClient); + RawProxy proxy3 = new RawProxy(listener, connectionConfigClient); + proxy3.start(); + + // Starts proxy2 + InputStream serverKeyStoreStream = listener.getClass().getResourceAsStream("/keys/serverKeyStore"); + TlsCredentials serverCredentials = new TlsCredentials(serverKeyStoreStream, "serverStorePass".toCharArray(), + "myServerCert", "password".toCharArray()); + TlsConfig tlsConfigServer = new TlsConfig(serverCredentials, false, null); + ConnectionConfig connectionConfigServer = new ConnectionConfigSimpleTls(9443, "127.0.0.1", 10000, + ChannelType.SYSLOG, tlsConfigServer); + RawProxy proxy1 = new RawProxy(listener, connectionConfigServer); + proxy1.start(); + + // Ping! + try { + AuthSSLSocketFactory clientSocketFactory = getSyslogClientSocketFactory(); + + ProtocolMessage sl = new ProtocolMessage(10, 5, "2009-08-14T14:12:23.115Z", "localhost", + new StringLogMessage("<atna></atna>"), "IHE_XDS", "ATNALOG", "1234"); + List<SdParam> params = new ArrayList<SdParam>(); + params.add(new SdParam("param1", "param value\\=1")); + params.add(new SdParam("param2", "param value] 2")); + params.add(new SdParam("param3", "param value 3")); + params.add(new SdParam("param3", "param value 4")); + StructuredElement se = new StructuredElement("exampleSDID@1234", params); + sl.addStructuredElement(se); + + Socket s = clientSocketFactory.createSecureSocket("localhost", 9443); + OutputStream out = s.getOutputStream(); + byte[] bytes = sl.toByteArray(); + for (int i = 0; i < 5; i++) { + // add message length plus space before message + out.write((String.valueOf(bytes.length) + " ").getBytes(Constants.ENC_UTF8)); + out.write(bytes); + out.flush(); + } + out.close(); + s.close(); + } catch (SyslogException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + + } + + private static AuthSSLSocketFactory getSyslogServerSocketFactory() throws IOException { + URL u = Thread.currentThread().getContextClassLoader().getResource("keys/serverKeyStore"); + KeystoreDetails key = new KeystoreDetails(u.toString(), "serverStorePass", "myServerCert", "password"); + URL uu = Thread.currentThread().getContextClassLoader().getResource("keys/clientKeyStore"); + KeystoreDetails trust = new KeystoreDetails(uu.toString(), "clientStorePass", "myClientCert"); + AuthSSLSocketFactory f = new AuthSSLSocketFactory(key, trust); + return f; + } + + private static AuthSSLSocketFactory getSyslogClientSocketFactory() throws IOException { + URL u = Thread.currentThread().getContextClassLoader().getResource("keys/serverKeyStore"); + KeystoreDetails trust = new KeystoreDetails(u.toString(), "serverStorePass", "myServerCert"); + URL uu = Thread.currentThread().getContextClassLoader().getResource("keys/clientKeyStore"); + KeystoreDetails key = new KeystoreDetails(uu.toString(), "clientStorePass", "myClientCert", "password"); + AuthSSLSocketFactory f = new AuthSSLSocketFactory(key, trust); + return f; + } + private static void startDicomProxyTLS(int port) { final ProxyEventListener<DimseMessage, DimseMessage> dicomProxyEvent = new DicomEventListenerSimple(System.out); - TlsConfig tlsConfig = new TlsConfig(new TlsCredentials(getStream("/home/glandais/435.p12"), "password".toCharArray()), false, - null); + TlsConfig tlsConfig = new TlsConfig(new TlsCredentials(getStream("/home/glandais/435.p12"), + "password".toCharArray()), false, null); ConnectionConfigSimpleTls connectionConfig = new ConnectionConfigSimpleTls(port, "kujira.irisa.fr", 10002, ChannelType.DICOM, tlsConfig); @@ -61,8 +174,8 @@ public class App { private static void startHTTPProxyTLSServer(int port) { HttpEventListenerSimple eventListener = new HttpEventListenerSimple(System.out); - TlsConfig tlsConfig = new TlsConfig(new TlsCredentials(getStream("/home/glandais/435.p12"), "password".toCharArray()), false, - null); + TlsConfig tlsConfig = new TlsConfig(new TlsCredentials(getStream("/home/glandais/435.p12"), + "password".toCharArray()), false, null); HttpProxy httpProxy = new HttpProxy(eventListener, new ConnectionConfigSimpleTls(port, "www.google.fr", 80, ChannelType.HTTP, tlsConfig)); httpProxy.start(); @@ -95,4 +208,18 @@ public class App { ChannelType.HTTP)); httpProxy.start(); } + + static class Listener implements SyslogListener { + + public void messageArrived(SyslogMessage message) { + System.out.println("serialized message:"); + System.out.println(message.toString()); + System.out.println("application message:"); + System.out.println(message.getMessage().getMessageObject()); + } + + public void exceptionThrown(SyslogException exception) { + exception.printStackTrace(); + } + } } diff --git a/gazelle-proxy-netty/src/test/java/net/ihe/gazelle/proxy/netty/syslog/AuthSSLSocketFactory.java b/gazelle-proxy-netty/src/test/java/net/ihe/gazelle/proxy/netty/syslog/AuthSSLSocketFactory.java new file mode 100644 index 00000000..01bd9320 --- /dev/null +++ b/gazelle-proxy-netty/src/test/java/net/ihe/gazelle/proxy/netty/syslog/AuthSSLSocketFactory.java @@ -0,0 +1,257 @@ +/** + * Copyright (c) 2009-2011 University of Cardiff and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + * + * Contributors: + * University of Cardiff - initial API and implementation + * - + */ + +package net.ihe.gazelle.proxy.netty.syslog; + + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.URL; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Enumeration; +import java.util.logging.Logger; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; + + +/** + * + */ +public class AuthSSLSocketFactory { + + static Logger log = Logger.getLogger("org.openhealthtools.openatna.syslog.test.tls.ssl.AuthSSLSocketFactory"); + + private KeystoreDetails details = null; + private KeystoreDetails truststore = null; + + private SSLContext sslcontext = null; + private X509TrustManager defaultTrustManager = null; + + public AuthSSLSocketFactory(KeystoreDetails details, KeystoreDetails truststore, X509TrustManager defaultTrustManager) throws IOException { + super(); + + if (details != null) { + this.details = details; + } + if (truststore != null) { + this.truststore = truststore; + } + if (defaultTrustManager == null) { + log.fine(" using sun default trust manager"); + this.defaultTrustManager = KeystoreManager.getDefaultTrustManager(); + } else { + this.defaultTrustManager = defaultTrustManager; + } + } + + public AuthSSLSocketFactory(KeystoreDetails details, KeystoreDetails truststore) throws IOException { + this(details, truststore, null); + } + + public AuthSSLSocketFactory(KeystoreDetails details, X509TrustManager defaultTrustManager) throws IOException { + this(details, null, defaultTrustManager); + } + + public AuthSSLSocketFactory(KeystoreDetails details) throws IOException { + this(details, null, null); + } + + private static KeyStore createKeyStore(KeystoreDetails details) + throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException { + if (details.getKeystoreLocation() == null) { + throw new IllegalArgumentException("Keystore location may not be null"); + } + + log.fine("Initializing key store"); + KeyStore keystore = KeyStore.getInstance(details.getKeystoreType()); + InputStream is = null; + try { + is = getKeystoreInputStream(details.getKeystoreLocation()); + if (is == null) { + throw new IOException("Could not open stream to " + details.getKeystoreLocation()); + } + String password = details.getKeystorePassword(); + keystore.load(is, password != null ? password.toCharArray() : null); + } finally { + if (is != null) { + is.close(); + } + } + return keystore; + } + + private static InputStream getKeystoreInputStream(String location) { + try { + File file = new File(location); + if (file.exists()) { + return new FileInputStream(file); + } + } catch (Exception e) { + + } + try { + URL url = new URL(location); + return url.openStream(); + } catch (Exception e) { + + } + log.fine("could not open stream to:" + location); + return null; + } + + private KeyManager[] createKeyManagers(final KeyStore keystore, KeystoreDetails details) + throws KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException { + if (keystore == null) { + throw new IllegalArgumentException("Keystore may not be null"); + } + log.fine("Initializing key manager"); + KeyManagerFactory kmfactory = KeyManagerFactory.getInstance(details.getAlgType()); + String password = details.getKeyPassword(); + kmfactory.init(keystore, (password == null || password.length() == 0) ? details.getKeystorePassword().toCharArray() : password.toCharArray()); + return kmfactory.getKeyManagers(); + + } + + private TrustManager[] createTrustManagers(KeystoreDetails truststore, final KeyStore keystore, X509TrustManager defaultTrustManager) + throws KeyStoreException, NoSuchAlgorithmException { + + if (keystore == null) { + throw new IllegalArgumentException("Keystore may not be null"); + } + TrustManagerFactory tmfactory = + TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());//TrustManagerFactory.getInstance(algorithm); + tmfactory.init(keystore); + TrustManager[] trustmanagers = tmfactory.getTrustManagers(); + for (int i = 0; i < trustmanagers.length; i++) { + + if (trustmanagers[i] instanceof X509TrustManager) { + return new TrustManager[]{ + new AuthSSLX509TrustManager((X509TrustManager) trustmanagers[i], defaultTrustManager, truststore.getAuthorizedDNs())}; + } + } + return trustmanagers; + } + + private SSLContext createSSLContext() throws IOException { + try { + KeyManager[] keymanagers = null; + TrustManager[] trustmanagers = null; + if (this.details != null) { + KeyStore keystore = createKeyStore(details); + Enumeration aliases = keystore.aliases(); + while (aliases.hasMoreElements()) { + String alias = (String) aliases.nextElement(); + Certificate[] certs = keystore.getCertificateChain(alias); + if (certs != null) { + log.fine("Certificate chain '" + alias + "':"); + for (int c = 0; c < certs.length; c++) { + if (certs[c] instanceof X509Certificate) { + X509Certificate cert = (X509Certificate) certs[c]; + log.fine(" Certificate " + (c + 1) + ":"); + log.fine(" Subject DN: " + cert.getSubjectDN()); + log.fine(" Signature Algorithm: " + cert.getSigAlgName()); + log.fine(" Valid from: " + cert.getNotBefore()); + log.fine(" Valid until: " + cert.getNotAfter()); + log.fine(" Issuer: " + cert.getIssuerDN()); + } + } + } + + } + keymanagers = createKeyManagers(keystore, details); + } + if (this.truststore != null) { + KeyStore keystore = createKeyStore(truststore); + Enumeration aliases = keystore.aliases(); + while (aliases.hasMoreElements()) { + String alias = (String) aliases.nextElement(); + log.fine("Trusted certificate '" + alias + "':"); + Certificate trustedcert = keystore.getCertificate(alias); + if (trustedcert != null && trustedcert instanceof X509Certificate) { + X509Certificate cert = (X509Certificate) trustedcert; + log.fine(" Subject DN: " + cert.getSubjectDN()); + log.fine(" Signature Algorithm: " + cert.getSigAlgName()); + log.fine(" Valid from: " + cert.getNotBefore()); + log.fine(" Valid until: " + cert.getNotAfter()); + log.fine(" Issuer: " + cert.getIssuerDN()); + } + } + trustmanagers = createTrustManagers(truststore, keystore, defaultTrustManager); + } + if (trustmanagers == null) { + log.fine(" created trustmanagers from the default..."); + trustmanagers = new TrustManager[]{defaultTrustManager}; + } + + SSLContext sslcontext = SSLContext.getInstance("SSL"); + sslcontext.init(keymanagers, trustmanagers, null); + return sslcontext; + } catch (NoSuchAlgorithmException e) { + log.warning(e.getMessage()); + throw new IOException("Unsupported algorithm exception: " + e.getMessage()); + } catch (KeyStoreException e) { + log.warning(e.getMessage()); + throw new IOException("Keystore exception: " + e.getMessage()); + } catch (GeneralSecurityException e) { + log.warning(e.getMessage()); + throw new IOException("Key management exception: " + e.getMessage()); + } catch (IOException e) { + log.warning(e.getMessage()); + throw new IOException("I/O error reading keystore/truststore file: " + e.getMessage()); + } + } + + public SSLContext getSSLContext() throws IOException { + if (this.sslcontext == null) { + this.sslcontext = createSSLContext(); + } + return this.sslcontext; + } + + public Socket createSecureSocket(String host, int port) throws IOException { + return getSSLContext().getSocketFactory().createSocket(host, port); + } + + public ServerSocket createServerSocket(int port) throws IOException { + return getSSLContext().getServerSocketFactory().createServerSocket(port); + } + + public boolean isSecured() { + return true; + } + + +} \ No newline at end of file diff --git a/gazelle-proxy-netty/src/test/java/net/ihe/gazelle/proxy/netty/syslog/AuthSSLX509TrustManager.java b/gazelle-proxy-netty/src/test/java/net/ihe/gazelle/proxy/netty/syslog/AuthSSLX509TrustManager.java new file mode 100644 index 00000000..a1056569 --- /dev/null +++ b/gazelle-proxy-netty/src/test/java/net/ihe/gazelle/proxy/netty/syslog/AuthSSLX509TrustManager.java @@ -0,0 +1,140 @@ +/** + * Copyright (c) 2009-2011 University of Cardiff and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + * + * Contributors: + * University of Cardiff - initial API and implementation + * - + */ + +package net.ihe.gazelle.proxy.netty.syslog; + +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Logger; + +import javax.net.ssl.X509TrustManager; + +/** + * <p> + * <p/> + * </p> + */ + +public class AuthSSLX509TrustManager implements X509TrustManager { + + private X509TrustManager trustManager = null; + private X509TrustManager defaultTrustManager = null; + List<String> authorizedDns = null; + /** + * Log object for this class. + */ + static Logger log = Logger.getLogger("org.openhealthtools.openatna.syslog.test.tls.ssl.AuthSSLX509TrustManager"); + + /** + * Constructor for AuthSSLX509TrustManager. + */ + public AuthSSLX509TrustManager(final X509TrustManager trustManager, final X509TrustManager defaultTrustManager, List<String> authorizedDns) { + super(); + if (trustManager == null) { + throw new IllegalArgumentException("Trust manager may not be null"); + } + this.trustManager = trustManager; + this.defaultTrustManager = defaultTrustManager; + this.authorizedDns = authorizedDns; + if (this.authorizedDns == null) { + this.authorizedDns = new ArrayList<String>(); + } + } + + /** + * @see javax.net.ssl.X509TrustManager#checkClientTrusted(X509Certificate[],String authType) + */ + public void checkClientTrusted(X509Certificate[] certificates, String authType) throws CertificateException { + if (certificates != null) { + boolean isAuthDN = false; + if (authorizedDns.size() == 0) { + isAuthDN = true; + } + for (int c = 0; c < certificates.length; c++) { + X509Certificate cert = certificates[c]; + if (isAuthDN == false) { + for (String authorizedDn : authorizedDns) { + if (authorizedDn.equals(cert.getSubjectDN())) { + isAuthDN = true; + } + } + } + log.fine(" Client certificate " + (c + 1) + ":"); + log.fine(" Subject DN: " + cert.getSubjectDN()); + log.fine(" Signature Algorithm: " + cert.getSigAlgName()); + log.fine(" Valid from: " + cert.getNotBefore()); + log.fine(" Valid until: " + cert.getNotAfter()); + log.fine(" Issuer: " + cert.getIssuerDN()); + } + if (!isAuthDN) { + throw new CertificateException("Subject DN is not authorized to perform the requested action."); + } + trustManager.checkClientTrusted(certificates, authType); + } + + } + + /** + * @see javax.net.ssl.X509TrustManager#checkServerTrusted(X509Certificate[],String authType) + */ + public void checkServerTrusted(X509Certificate[] certificates, String authType) throws CertificateException { + if (certificates != null) { + for (int c = 0; c < certificates.length; c++) { + X509Certificate cert = certificates[c]; + log.fine(" Server certificate " + (c + 1) + ":"); + log.fine(" Subject DN: " + cert.getSubjectDN()); + log.fine(" Signature Algorithm: " + cert.getSigAlgName()); + log.fine(" Valid from: " + cert.getNotBefore()); + log.fine(" Valid until: " + cert.getNotAfter()); + log.fine(" Issuer: " + cert.getIssuerDN()); + } + } + + try { + if (defaultTrustManager != null) { + defaultTrustManager.checkServerTrusted(certificates, authType); + } + } catch (CertificateException e) { + trustManager.checkServerTrusted(certificates, authType); + } + } + + /** + * @see javax.net.ssl.X509TrustManager#getAcceptedIssuers() + */ + public X509Certificate[] getAcceptedIssuers() { + X509Certificate[] certs = this.trustManager.getAcceptedIssuers(); + + if (defaultTrustManager != null) { + X509Certificate[] suncerts = this.defaultTrustManager.getAcceptedIssuers(); + X509Certificate[] all = new X509Certificate[certs.length + suncerts.length]; + System.arraycopy(certs, 0, all, 0, certs.length); + System.arraycopy(suncerts, 0, all, certs.length, suncerts.length); + certs = all; + } + if (certs == null) { + certs = new X509Certificate[0]; + } + + return certs; + } +} \ No newline at end of file diff --git a/gazelle-proxy-netty/src/test/java/net/ihe/gazelle/proxy/netty/syslog/KeystoreDetails.java b/gazelle-proxy-netty/src/test/java/net/ihe/gazelle/proxy/netty/syslog/KeystoreDetails.java new file mode 100644 index 00000000..a0a14ce2 --- /dev/null +++ b/gazelle-proxy-netty/src/test/java/net/ihe/gazelle/proxy/netty/syslog/KeystoreDetails.java @@ -0,0 +1,138 @@ +/** + * Copyright (c) 2009-2011 University of Cardiff and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + * + * Contributors: + * University of Cardiff - initial API and implementation + * - + */ + +package net.ihe.gazelle.proxy.netty.syslog; + +import java.util.ArrayList; +import java.util.List; + + +/** + * interface for classes that can retrieve details required for signing a jar or authetication. + * + * @author Andrew Harrison + * @version $Revision: 72 $ + * @created Mar 16, 2007: 3:13:50 PM + * @date $Date: 2007-03-23 09:52:50 +0000 (Fri, 23 Mar 2007) $ modified by $Author: scmabh $ + * @todo Put your notes here... + */ +public class KeystoreDetails { + + private String keystoreLocation = ""; + private String keystorePassword = ""; + private String alias = ""; + private String keyPassword = null; + private String keystoreType = "JKS"; + private String algType = "SunX509"; + private String authority = ""; + private List<String> authorizedDNs = new ArrayList<String>(); + + /** + * create a KeystoreDetails for accessing a certificate + * + * @param keystoreLocation + * @param keystorePassword + * @param alias + * @param keyPassword + */ + public KeystoreDetails(String keystoreLocation, String keystorePassword, String alias, String keyPassword) { + this.keystoreLocation = keystoreLocation; + this.keystorePassword = keystorePassword; + this.alias = alias; + this.keyPassword = keyPassword; + } + + public KeystoreDetails(String keystoreLocation, String keystorePassword, String alias) { + this.keystoreLocation = keystoreLocation; + this.keystorePassword = keystorePassword; + this.alias = alias; + this.keyPassword = keystorePassword; + } + + /** + * constructor used when loading details from file. + */ + public KeystoreDetails() { + } + + public String getKeystoreLocation() { + return keystoreLocation; + } + + public String getKeystorePassword() { + return keystorePassword; + } + + public String getAlias() { + return alias; + } + + public String getKeyPassword() { + return keyPassword; + } + + public void setKeyPassword(String keyPassword) { + this.keyPassword = keyPassword; + } + + public String getKeystoreType() { + return keystoreType; + } + + public void setKeystoreType(String keystoreType) { + this.keystoreType = keystoreType; + } + + public String getAlgType() { + return algType; + } + + public void setAlgType(String algType) { + this.algType = algType; + } + + /** + * combination of host (domain or IP) and port separated by a colon. + * + * @return + */ + public String getAuthority() { + return authority; + } + + public void setAuthority(String authority) { + this.authority = authority; + } + + public void addAuthorizedDN(String dn) { + if (!authorizedDNs.contains(dn)) { + authorizedDNs.add(dn); + } + } + + public List<String> getAuthorizedDNs() { + return authorizedDNs; + } + + public void setAuthorizedDNs(List<String> authorizedDNs) { + this.authorizedDNs = authorizedDNs; + } + +} diff --git a/gazelle-proxy-netty/src/test/java/net/ihe/gazelle/proxy/netty/syslog/KeystoreManager.java b/gazelle-proxy-netty/src/test/java/net/ihe/gazelle/proxy/netty/syslog/KeystoreManager.java new file mode 100644 index 00000000..22563e63 --- /dev/null +++ b/gazelle-proxy-netty/src/test/java/net/ihe/gazelle/proxy/netty/syslog/KeystoreManager.java @@ -0,0 +1,430 @@ +/** + * Copyright (c) 2009-2011 University of Cardiff and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + * + * Contributors: + * University of Cardiff - initial API and implementation + * - + */ + +package net.ihe.gazelle.proxy.netty.syslog; + + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.cert.CertificateException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Properties; +import java.util.logging.Logger; + +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; + +/** + * Class Description Here... + * + * @author Andrew Harrison + * @version $Revision:$ + * @created Nov 20, 2008: 7:58:07 PM + * @date $Date:$ modified by $Author:$ + * @todo Put your notes here... + */ + +public class KeystoreManager { + + static Logger log = Logger.getLogger("org.wspeer.security.KeystoreManager"); + + private static X509TrustManager sunTrustManager = null; + private KeystoreDetails defaultKeyDetails; + private HashMap<String, KeystoreDetails> allKeys = new HashMap<String, KeystoreDetails>(); + private HashMap<String, KeystoreDetails> allStores = new HashMap<String, KeystoreDetails>(); + + private File keysDir; + private File certsDir; + private String home; + + static { + loadDefaultTrustManager(); + } + + private static void loadDefaultTrustManager() { + try { + File certs; + String definedcerts = System.getProperty("javax.net.ssl.trustStore"); + String pass = System.getProperty("javax.net.ssl.trustStorePassword"); + if (definedcerts != null) { + certs = new File(definedcerts); + } else { + String common = System.getProperty("java.home") + + File.separator + + "lib" + + File.separator + + "security" + + File.separator; + String cacerts = common + "cacerts"; + String jssecacerts = common + "jssecacerts"; + certs = new File(jssecacerts); + if (!certs.exists() || certs.length() == 0) { + certs = new File(cacerts); + } + + } + if (pass == null) { + pass = "changeit"; + } + if (certs != null) { + KeyStore ks = KeyStore.getInstance("jks"); + ks.load(new FileInputStream(certs), pass.toCharArray()); + TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509", "SunJSSE"); + tmf.init(ks); + TrustManager tms[] = tmf.getTrustManagers(); + for (int i = 0; i < tms.length; i++) { + if (tms[i] instanceof X509TrustManager) { + log.info(" found default trust manager."); + sunTrustManager = (X509TrustManager) tms[i]; + break; + } + } + } + + } catch (KeyStoreException e) { + log.fine("Exception thrown trying to create default trust manager:" + e.getMessage()); + } catch (NoSuchAlgorithmException e) { + log.fine("Exception thrown trying to create default trust manager:" + e.getMessage()); + } catch (CertificateException e) { + log.fine("Exception thrown trying to create default trust manager:" + e.getMessage()); + } catch (NoSuchProviderException e) { + log.fine("Exception thrown trying to create default trust manager:" + e.getMessage()); + } catch (FileNotFoundException e) { + log.fine("Exception thrown trying to create default trust manager:" + e.getMessage()); + } catch (IOException e) { + log.fine("Exception thrown trying to create default trust manager:" + e.getMessage()); + } + } + + public KeystoreManager(String home) { + if (home != null) { + this.home = home; + loadKeys(this.home); + } + } + + private void loadKeys(String home) { + File sec = new File(home); + if (!sec.exists()) { + return; + } + + keysDir = new File(sec, "keys"); + if (!keysDir.exists()) { + keysDir.mkdir(); + } + certsDir = new File(sec, "certs"); + if (!certsDir.exists()) { + certsDir.mkdir(); + } + File[] keyfiles = keysDir.listFiles(); + if (keyfiles != null) { + for (File keyfile : keyfiles) { + try { + KeystoreDetails kd = load(new FileInputStream(keyfile)); + if (kd.getAuthority() != null && kd.getAuthority().trim().equalsIgnoreCase("default")) { + defaultKeyDetails = kd; + } + allKeys.put(keyfile.getName(), kd); + + } catch (IOException e) { + log.info(" exception thrown while loading details from " + keyfile.getAbsolutePath()); + continue; + } + } + } + keyfiles = certsDir.listFiles(); + if (keyfiles != null) { + for (File keyfile : keyfiles) { + try { + KeystoreDetails kd = load(new FileInputStream(keyfile)); + allStores.put(keyfile.getName(), kd); + + } catch (IOException e) { + log.info(" exception thrown while loading details from " + keyfile.getAbsolutePath()); + continue; + } + } + } + } + + public static X509TrustManager getDefaultTrustManager() { + return sunTrustManager; + } + + public void addKeyDetails(String fileName, KeystoreDetails details) throws IOException { + storeAsKey(details, fileName); + allKeys.put(fileName, details); + } + + public void addTrustDetails(String fileName, KeystoreDetails details) throws IOException { + storeAsCert(details, fileName); + allStores.put(fileName, details); + } + + public void deleteKeyDetails(String fileName) { + allKeys.remove(fileName); + deleteKey(fileName); + } + + public void deleteTrustDetails(String fileName) { + allStores.remove(fileName); + deleteCert(fileName); + } + + public KeystoreDetails getKeyDetails(String fileName) { + return allKeys.get(fileName); + } + + public KeystoreDetails getTrustStoreDetails(String fileName) { + return allStores.get(fileName); + } + + public void setDefaultKeystoreDetails(KeystoreDetails details) { + defaultKeyDetails = details; + } + + public KeystoreDetails getDefaultKeyDetails() { + return defaultKeyDetails; + } + + public File getKeysDirectory() { + return keysDir; + } + + public File getCertsDirectory() { + return certsDir; + } + + public KeystoreDetails getKeyFileDetails(String fileName) { + return allKeys.get(fileName); + } + + public KeystoreDetails getStoreFileDetails(String fileName) { + return allStores.get(fileName); + } + + public String[] getKeyfileNames() { + return allKeys.keySet().toArray(new String[allKeys.keySet().size()]); + } + + public String[] getTrustfileNames() { + return allStores.keySet().toArray(new String[allStores.keySet().size()]); + } + + public KeystoreDetails getKeyFileForHost(String host) { + KeystoreDetails def = null; + for (KeystoreDetails keystoreDetails : allKeys.values()) { + System.out.println("KeystoreManager.getKeyFileForHost getting next key authority:" + keystoreDetails.getAuthority()); + String auth = keystoreDetails.getAuthority(); + if (auth != null) { + if (auth.endsWith("*")) { + String s = trimPort(host); + if (s != null) { + log.fine("KeystoreManager.getKeyFileForHost trimmed port:" + s); + String a = getAnyPort(auth); + if (a != null) { + log.fine("KeystoreManager.getKeyFileForHost trimmed auth:" + a); + auth = a; + host = s; + } + } + } + if (auth.equals(host)) { + return keystoreDetails; + } else if (auth.equalsIgnoreCase("default")) { + def = keystoreDetails; + } + } + } + return def; + } + + private static String trimPort(String host) { + int colon = host.indexOf(":"); + if (colon > 0 && colon < host.length() - 1) { + try { + int port = Integer.parseInt(host.substring(colon + 1, host.length()), host.length()); + host = host.substring(0, colon); + log.fine("KeystoreManager.trimPort up to colon:" + host); + log.fine("KeystoreManager.trimPort port:" + port); + + return host; + } catch (NumberFormatException e) { + } + } + return null; + } + + private static String getAnyPort(String auth) { + int star = auth.indexOf("*"); + if (star == auth.length() - 1) { + int colon = auth.indexOf(":"); + if (colon == star - 1) { + auth = auth.substring(0, colon); + return auth; + } + } + return null; + } + + public KeystoreDetails getTrustFileForHost(String host) { + + KeystoreDetails def = null; + for (KeystoreDetails keystoreDetails : allStores.values()) { + String auth = keystoreDetails.getAuthority(); + if (auth != null) { + if (auth.endsWith("*")) { + String s = trimPort(host); + if (s != null) { + String a = getAnyPort(auth); + if (a != null) { + auth = a; + host = s; + } + } + } + if (auth.equals(host)) { + return keystoreDetails; + } else if (auth.equalsIgnoreCase("default")) { + def = keystoreDetails; + } + } + } + return def; + } + + + public KeystoreDetails load(InputStream in) throws IOException { + Properties props = new Properties(); + props.load(in); + String keystoreLocation = props.getProperty("keystoreLocation"); + if (keystoreLocation == null || keystoreLocation.length() == 0) { + throw new IOException("no location defined"); + } + String keystorePassword = props.getProperty("keystorePassword"); + if (keystorePassword == null || keystorePassword.length() == 0) { + throw new IOException("no keystore password defined"); + } + String alias = props.getProperty("alias"); + String keyPassword = props.getProperty("keyPassword"); + if (keyPassword == null || keyPassword.length() == 0) { + keyPassword = keystorePassword; + } + String keystoreType = props.getProperty("keystoreType"); + if (keystoreType == null || keystoreType.length() == 0) { + keystoreType = "JKS"; + } + String algType = props.getProperty("algType"); + if (algType == null || algType.length() == 0) { + algType = "SunX509"; + } + String authority = props.getProperty("authority"); + if (authority == null) { + authority = ""; + } + + String dns = props.getProperty("authorizedDNs"); + List<String> authorizedDNs = new ArrayList<String>(); + if (dns != null && dns.length() > 0) { + String[] dn = dns.split("&"); + for (String s : dn) { + String decoded = URLDecoder.decode(s, "UTF-8"); + if (decoded.length() > 0) { + authorizedDNs.add(decoded); + } + } + } + KeystoreDetails details = new KeystoreDetails(keystoreLocation, keystorePassword, alias, keyPassword); + details.setAlgType(algType); + details.setKeystoreType(keystoreType); + details.setAuthority(authority); + for (String authorizedDN : authorizedDNs) { + details.addAuthorizedDN(authorizedDN); + } + return details; + } + + public void storeAsKey(KeystoreDetails details, String name) throws IOException { + store(details, name, true); + } + + public void storeAsCert(KeystoreDetails details, String name) throws IOException { + store(details, name, false); + } + + public boolean deleteKey(String name) { + return delete(name, true); + } + + public boolean deleteCert(String name) { + return delete(name, false); + } + + private boolean delete(String name, boolean key) { + File f = key ? getKeysDirectory() : getCertsDirectory(); + f = new File(f, name); + return f.delete(); + } + + private void store(KeystoreDetails details, String name, boolean key) throws IOException { + Properties props = new Properties(); + props.setProperty("keystoreLocation", details.getKeystoreLocation()); + props.setProperty("keystorePassword", details.getKeystorePassword()); + props.setProperty("alias", details.getAlias()); + if (details.getKeyPassword() == null) { + details.setKeyPassword(""); + } + props.setProperty("keyPassword", details.getKeyPassword()); + props.setProperty("keystoreType", details.getKeystoreType()); + props.setProperty("algType", details.getAlgType()); + if (details.getAuthority() != null) { + props.setProperty("authority", details.getAuthority()); + } + List<String> authorizedDNs = details.getAuthorizedDNs(); + if (authorizedDNs.size() > 0) { + StringBuilder sb = new StringBuilder(); + for (String dn : authorizedDNs) { + sb.append(URLEncoder.encode(dn, "UTF-8")).append("&"); + } + props.setProperty("authorizedDNs", sb.toString()); + } + File f = key ? getKeysDirectory() : getCertsDirectory(); + f = new File(f, name); + FileOutputStream out = new FileOutputStream(f); + props.store(out, "Details for " + details.getAlias() + " keystore access."); + out.close(); + } + + +} diff --git a/gazelle-proxy-netty/src/test/resources/keys/clientKeyStore b/gazelle-proxy-netty/src/test/resources/keys/clientKeyStore new file mode 100644 index 0000000000000000000000000000000000000000..d5e6c1b6103503afc66add97c9c6cf28eaca9a65 GIT binary patch literal 1402 zcmezO_TO6u1_mY|W&~rN+{)yf%+$P+<kX@PprEqJKKrXc1$zyen06ZQv2kg$F|sgf zF$pp<vNEtVF)giEcX=AxIxDMUM)@Sof+qF7cD@-QYu@yUioH>8w>R1$THrFzE9z}Y znZ%sw-#mJ+f3UV&oig3WrlfZJ!NjB2;%6Pbn7#W=@4b>hcBSe`8G+wioU&QoG8D0S ze_PnV_O6GiaLIL@!d-K}9NqHKN=fhVf}69`+e{aId+YS!cZj0a$wXWCUl%sU%D>?k z3u6@Fwke3@sXf1Me*v4?vfef4#8@=N?))>qnZ)?4`_;cQHw<%B_q188cp4w~<-Lgh zoX@!lTgx(k%1oEs{*n7p(K*-U(~{i77Cw{NbRcZ9)ZRva%?UFzC-Z&(vd#L5+>eKO zyH1uc>}r<D3NANKVc4uwEVb=pg4z1B;x~RiHF;>U^v{`4k=;*fqZhV(ySdZM>vf`> z)^-=CGR0>HKk0Ssyd1-+&APqi_Q#BNfrJ|ozWxsa!+9bm&QYmU-=(3n$a6~Myl-EB zG_804%`|~8^sewGwUCQC-ev6_@7q}CXSGN_TlqSq#Qyx2L_trs{Q@&w&h7B-^SEgG zc#~hj*|nux6g)R4=k_h+SUNS<>$KO^SBx%aSiT$H)%6i!H!a}2S@5Vh&QeG5{^U(X z7NL=+W?i$h=KiU}?6~QEkLI22OKoi4Fl73<dl%{~W3TZ~yft6oZ*x|B=!2=f*KaxS z^B%GMmK^H7DttoMoyx-X9GotKdpGh+uL>$PeCc;LE$*M7Mw+3A{_~3Y2R%0a*x@Xy z`<tOC_BeN;NNM8bMKQVuZnP=>>xq#u663$9B9Id;krL;=%QT{U(cK%f6R*`2{h2Yf z%IeQkzX-n)Lj}?Pt(h^(etYEDUM^&uwbgx6mdp2)_O95j&D(|aI;E3s1N(jNUC79J ztnuhYhf|f`Vh*YI1uTna$jsHWUnHw59?>s2@9h<Ki<K|p-zDWdPdJ!!2b{E7BlJuS zEP-jc0GO6@44N3PGO>8|ROK1)vT<s)d9;1!Wn^SwWiY5U<Tl`BV-96u6J`qaHWUD+ zdLV~GnB6rmJtr|Q1t!8S%<flOQl6imVkm7O2~xu)ECdx%2+d1LEm8<c%_}w(HxLC$ zatrf0rj!A5R%uZxP&%{3P{cq8B*84q9aLHX%x(%!sfk4fa^k!OmIfAvmPSTEY!(IL z8X6j!L%9R(yvF&+;RP%Nm>YW;3>rI`8XFl-^{HNI*e#y-IsCznfHmu1|Nr+OWLM*b zN$(`RPHjjQD0sO3+SeG3-8l<y#w2C(b<7ZB&g%A-x+)ZNF=DMzL>Z6fVk`fRhgLJN zoVmK+;^`aaUMZ#DPO2rW>^kq&BMe_GD^Z&H`b)R_w$w8@*2~>?e*G69#<4SQdGy;I zXGLB`cK&B#W@KPQb{;S~nSt(_-)9^coA*)UmO?#8!=5nF-V0*O7T)+%5T&u^TzhDp z!$BTNqqwuRI#!GyR(}j@diklgI^3M+^xja#W&d6F3Ve8`x4LlC_g2QoHx-h@G&+u3 z+iZ86t?tj!zjsQI-T#kAF5m44U-rcIzjxoQ$6KY1*Y0E#R_U4+_?Km4k*KPQQrDm6 XEry&wo+#P;UYc{oG<d})_p^ZjR=qMM literal 0 HcmV?d00001 diff --git a/gazelle-proxy-netty/src/test/resources/keys/serverKeyStore b/gazelle-proxy-netty/src/test/resources/keys/serverKeyStore new file mode 100644 index 0000000000000000000000000000000000000000..db9b60cdde7f88c77584883ffb6210881c3d83b5 GIT binary patch literal 1367 zcmezO_TO6u1_mY|W&~rN+{)tAqO#PY<kX@PprEqJ{_e#<1-lHIn6?@4v2kg$F|sgf zF$pp<vNEtVF)h+Nn|iu?&-#TKTZ6l%Bq^!d&X38yQS|V)=e`+jrR!eKn!x3#Dj#Oh z^ZCn!Rg=?h1x8Ao+M=JjaptW(3y(P(>|3Xjx?@MrB)tjoR~OCh-m0+f-b;0b^A6Iv zjMv<Jenx(sB->xVGJHK}n>Nd0-+wymS54frbGs~a#r*3})ofy}ifxWod++g9Z~BUc z)js!cd9vnBjYzy482#mT>9!leyZ;=Q|8V|LOOMLc_wU-eKLx%l_he;>_uI2(<&_Vu zdyYO#JsPp0M>jY7?1iBBh5W65!rm4Bc=yCG<vtt7*K5;KSN@yTc}_yS?CXXvXQo~M z_0F?9^LJzV-4Dm6zrXLcS;uR6oT=K|$b;o#vbK_zcM9*Uk-V0%i|3@qZH`};vl(~J zd+jQdFI)H^)m1I|&n5e9Z(_q7KQ^XzC-<q&&JL<=R*QP5aN<+QB;K$mfhM;D!hfEx z=D$<$aTjlx%gSbho!gJwy_Z{|RGX=y;jz)>+ODOICk*%vvX|z}$jjwAeX-Pi@ATOD z0^O|QY<dq@FzvCL%<TMip7!&`H}&kt($@#DFEY5c>s`X`XN#Wvk6d)}#IKj`R>o$V z`}`k&5ttzse*NX44Wi+<OKxh-ZaQT1r{#Oh7lx+|JX0cr_xQh6c<Xh1FaPaFe~oox zz1i-(7pQ-DLzPu|-=S4s>7_W^@MG=Ns!?H9O-h%VK@wg-yk~to4#V%c~d5eo=`z ztU0%7U9n@A6YG}gXO7DL;6HIxaa!oEHm@nd0jJOPv<Mtr-FB?|xv~VK{mB#4&U?J^ z-2WvmAuIn1!`;RGH|O$C^7$*%ck6+@**#AqwJyh)fAt0%Pwh}mf8lyGSucIvPxh>% zlTFWgOtbATp0#7{hg#kEMQg&N9<~OkRX#ssD6PZ#EnWHVwFf62o;$nl(%t4$;B?Iz zp=WAf2~5ZVz=Z5$(8RcviN&j@>aYPX8>d#AN85K^Mn)D^27_EfZUas>=1>+kVWv=T zLjhot2XZ)s*<JI}a}x7XU?S|o?0%&s<@xz3hC&7cAT?aVoFN&h3L*LBsYQl-20S1! zZei8{kI3K%Loovp5T99?7o;YjC_gQ=xH!MaKu(<3z|z3N(9+1r$iUPv3dA)uG&F~D z>Fu1x`N-h`%;C(9y$lA8olK3546}0{*?6Blc&E|)!|q)5yPv1_Yuz~H_;%ra$Lles zn?K!~EWzIJyQp~Y9H;l6q$&(w&i>b&FBd=kVfX6JswVB|)ZI_cvk5<#&#ffRb8>oU z$R1bm>q>^@KT~^m+ICg%_0#(j=j&Eb_wm2mG|nV(j^3w<dmg-GvAh=QyddD;#*Xaw zOw5c7jL6OdMi(>CT>;O}#Ry&dUh(<R!Tt|DXQZ~h{&mOYWaN(}TWk1j4QBJ+5$|&g zkUq|SXvUs^HGO-!crS_DK9Jb1zqhMs{c6tA>hG3k?5aOU{a82c%i}o>8~W{BA8J}` z(}*yNzP~U<J8g@c`vWeHZ^avDO=5qo6W_S2cExAU-e9jgUtJP%ZyfBFPCNQ0B0K+& S@3DjrYd&wPTbFm|Zv_C>-${D_ literal 0 HcmV?d00001 -- GitLab