diff --git a/modules/iso-http-client/build.gradle b/modules/iso-http-client/build.gradle new file mode 100644 index 0000000000..7b759431c9 --- /dev/null +++ b/modules/iso-http-client/build.gradle @@ -0,0 +1,10 @@ +description = 'jPOS-EE :: ISO over HTTP Client Code' + +dependencies { + compile libraries.jpos + + compile "org.eclipse.jetty:jetty-client:${jettyVersion}" + compile "org.eclipse.jetty.http2:http2-client:${jettyVersion}" + compile "org.eclipse.jetty.http2:http2-http-client-transport:${jettyVersion}" +} + diff --git a/modules/iso-http-client/src/main/java/org/jpos/http/HttpClientSocketFactory.java b/modules/iso-http-client/src/main/java/org/jpos/http/HttpClientSocketFactory.java new file mode 100644 index 0000000000..c7ee6149b4 --- /dev/null +++ b/modules/iso-http-client/src/main/java/org/jpos/http/HttpClientSocketFactory.java @@ -0,0 +1,321 @@ +/* + * jPOS Project [http://jpos.org] + * Copyright (C) 2000-2018 jPOS Software SRL + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.jpos.http; + +import java.io.IOException; +import java.net.Socket; +import java.security.Provider; +import java.security.Security; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.http2.client.HTTP2Client; +import org.eclipse.jetty.http2.client.http.HttpClientTransportOverHTTP2; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.jpos.iso.ISOClientSocketFactory; +import org.jpos.iso.ISOException; +import org.jpos.core.Configurable; +import org.jpos.core.Configuration; +import org.jpos.core.ConfigurationException; +import org.jpos.core.SimpleConfiguration; +import org.jpos.util.LogEvent; +import org.jpos.util.Logger; +import org.jpos.util.SimpleLogSource; + + +/** + * HttpClientSocketFactory is used by BaseChannel + * in order to provide hooks for ISO traffic over HTTP. + * Used in conjunction with module iso-http-qrest or iso-http-servlet + * on the server side. + * + * @version $Revision$ $Date$ + * @author Ozzy Espaillat + * + */ +public class HttpClientSocketFactory extends SimpleLogSource +implements ISOClientSocketFactory, Configurable { + enum HttpType {HTTP, HTTP2}; + static final int DEFAULT_SENDER_THREADS=5; + + private static HttpClientSocketFactory DEFAULT; + + public static synchronized HttpClientSocketFactory getDefaultInstance() { + if (DEFAULT == null) { + DEFAULT = new HttpClientSocketFactory(true); + } + return DEFAULT; + } + + private Map httpClients = new HashMap(); + + private Configuration cfg; + + boolean failOnError = false; + int senderThreads = DEFAULT_SENDER_THREADS; + private int maxSendPerRequest = 1; + + String sslProviderClass = null; + private String trustStore=null; + private String trustStorePassword=null; + private String keyStore=null; + private String password=null; + private String keyPassword=null; + private boolean serverAuthNeeded=false; + private String[] enabledCipherSuites; + private String[] enabledProtocols; + + public void setKeyStore(String keyStore){ + this.keyStore=keyStore; + } + + public void setPassword(String password){ + this.password=password; + } + + public void setKeyPassword(String keyPassword){ + this.keyPassword=keyPassword; + } + + public void setServerAuthNeeded(boolean serverAuthNeeded){ + this.serverAuthNeeded=serverAuthNeeded; + } + + public int getMaxSendPerRequest() { + return maxSendPerRequest; + } + + public void setMaxSendPerRequest(int maxSendPerRequest) { + this.maxSendPerRequest = maxSendPerRequest; + } + + private synchronized HttpClient getHttpClient(HttpType type) { + HttpClient httpClient = httpClients.get(type); + if (httpClient == null) { + LogEvent evt = new LogEvent (this, "info"); + // Instantiate and configure the SslContextFactory + SslContextFactory sslContextFactory = new SslContextFactory(!serverAuthNeeded); + sslContextFactory.setExcludeCipherSuites("^.*_(MD5|SHA1)$"); + + if (keyStore != null && keyStore.length() > 0) { + sslContextFactory.setKeyStorePath(keyStore); + evt.addMessage("keystore", keyStore); + sslContextFactory.setKeyStorePassword(password); + if (keyPassword != null && keyPassword.length() > 0) { + sslContextFactory.setKeyManagerPassword(keyPassword); + } + } + if (trustStore != null && trustStore.length() > 0) { + sslContextFactory.setTrustStorePath(trustStore); + sslContextFactory.setTrustStorePassword(trustStorePassword); + } + if (enabledCipherSuites != null && enabledCipherSuites.length > 0) { + sslContextFactory.setIncludeCipherSuites(enabledCipherSuites); + evt.addMessage("cipher", Arrays.toString(enabledCipherSuites)); + } + if (enabledProtocols != null && enabledProtocols.length > 0) { + sslContextFactory.setIncludeProtocols(enabledProtocols); + evt.addMessage("protocol", Arrays.toString(enabledProtocols)); + } + + try { + if (sslProviderClass != null) { + Provider sslProviderName = (Provider) Class.forName(sslProviderClass).newInstance(); + if (Security.getProvider(sslProviderName.getName()) != null) { + Security.addProvider(sslProviderName); + } + evt.addMessage("Using " + sslProviderName.getName() + " Provider."); + sslContextFactory.setProvider(sslProviderName.getName()); + } + } catch (Throwable e) { + evt.addMessage("error", "Failed to load OpenSSLProvider, ignoring: " + e.toString()); + } + + switch (type) { + case HTTP2 : + HTTP2Client h2Client = new HTTP2Client(); + h2Client.setSelectors(1); + h2Client.addBean(sslContextFactory); + + HttpClientTransportOverHTTP2 transport = new HttpClientTransportOverHTTP2(h2Client); + + + + // Instantiate HttpClient with the SslContextFactory + httpClient = new HttpClient(transport, sslContextFactory); + evt.addMessage("Using HTTP2 transport!"); + + break; + case HTTP : + // Instantiate HttpClient with the SslContextFactory + httpClient = new HttpClient(sslContextFactory); + evt.addMessage("Using HTTP1.1 transport!"); + break; + } + + // Configure HttpClient, for example: + httpClient.setFollowRedirects(false); + + //httpClient.setMaxConnectionsPerDestination(senderThreads*2); + + // Start HttpClient + try { + httpClient.start(); + } catch (Exception e) { + evt.addMessage("error", "Could not start http client"); + evt.addMessage(e); + } + Logger.log(evt); + + httpClients.put(type, httpClient); + } else if (!httpClient.isRunning()) { + // Start HttpClient + try { + httpClient.start(); + } catch (Exception e) { + LogEvent evt = new LogEvent (this, "warn"); + evt.addMessage("error", "Could not start http client"); + evt.addMessage(e); + Logger.log(evt); + } + } + try { + while(!httpClient.isStarted() && !httpClient.isFailed()) { + Thread.sleep(100); + } + } catch (InterruptedException e) {} + + return httpClient; + } + + @Override + public void finalize() { + for(HttpClient httpClient: httpClients.values()) { + if (httpClient != null) { + try { + httpClient.stop(); + } catch (Exception e) { + } + } + } + } + + + public HttpClientSocketFactory() { + this(false); + } + + public HttpClientSocketFactory(boolean failOnError) { + this(failOnError, DEFAULT_SENDER_THREADS); + } + + public HttpClientSocketFactory(int senderThreads) { + this(false, senderThreads); + } + + public HttpClientSocketFactory(boolean failOnError, int senderThreads) { + super(); + try { + setConfiguration(new SimpleConfiguration()); + } catch (ConfigurationException e) { + } + + this.senderThreads = senderThreads; + this.failOnError = failOnError; + } + + + + @Override + public Socket createSocket(String host, int port) throws IOException, ISOException { + LogEvent evt = new LogEvent (this, "info"); + Socket s = null; + + evt.addMessage("connect"); + if (host.toLowerCase().startsWith("http")) { + HttpType type; + if (host.startsWith("http2")) { + type = HttpType.HTTP2; + host = "http" + host.substring("http2".length()); + } else { + type = HttpType.HTTP; + } + if (host.endsWith("/iso")) { + host = host + "/" + port; + } else if (host.endsWith("/iso/")) { + host = host + port; + } + evt.addMessage("type", type.name()); + evt.addMessage("host", host); + evt.addMessage("senders", Integer.toString(senderThreads)); + evt.addMessage("failOnError", Boolean.toString(failOnError)); + + HttpSocketWrapper httpSocket = new HttpSocketWrapper(this, getHttpClient(type), host, senderThreads, failOnError); + httpSocket.setMaxSendPerRequest(maxSendPerRequest); + s = httpSocket; + } else { + evt.addMessage("type", "socket"); + evt.addMessage("host", host); + evt.addMessage("port", Integer.toString(port)); + + s = new Socket(host, port); + } + Logger.log(evt); + + return s; + } + + @Override + public void setConfiguration(Configuration cfg) throws ConfigurationException { + this.cfg = cfg; + failOnError = cfg.getBoolean("failOnError", false); + maxSendPerRequest = cfg.getInt("maxSendPerRequest", 1); + senderThreads = cfg.getInt("senderThreads", DEFAULT_SENDER_THREADS); + sslProviderClass = cfg.get("sslProvider", null); + serverAuthNeeded = cfg.getBoolean("serverauth"); + + trustStore = cfg.get("truststore", System.getProperty("javax.net.ssl.trustStore")); + trustStorePassword = cfg.get("truststorepassword", System.getProperty("javax.net.ssl.trustStorePassword", "changeit")); + + keyStore = cfg.get("keystore", System.getProperty("javax.net.ssl.keyStore")); + String passwordProp = cfg.get("storepasswordprop", "javax.net.ssl.keyStorePassword"); + password = cfg.get("storepassword", System.getProperty(passwordProp)); + + String keyPaswwordProp = cfg.get("keypassword", "jpos.ssl.keypass"); + keyPassword = cfg.get("keypassword", System.getProperty(keyPaswwordProp)); + + enabledCipherSuites = cfg.getAll("addEnabledCipherSuite"); + enabledProtocols = cfg.getAll("addEnabledProtocol"); + } + + public Configuration getConfiguration() { + return cfg; + } + + public static void main(String args[]) throws Exception { + HttpClientSocketFactory f = HttpClientSocketFactory.getDefaultInstance(); + f.sslProviderClass = null; + HttpClient c = f.getHttpClient(HttpType.HTTP); + System.out.println(c.GET(args[0]).getContentAsString()); + c.stop(); + } + +} diff --git a/modules/iso-http-client/src/main/java/org/jpos/http/HttpSocketWrapper.java b/modules/iso-http-client/src/main/java/org/jpos/http/HttpSocketWrapper.java new file mode 100644 index 0000000000..627097ef67 --- /dev/null +++ b/modules/iso-http-client/src/main/java/org/jpos/http/HttpSocketWrapper.java @@ -0,0 +1,246 @@ +/* + * jPOS Project [http://jpos.org] + * Copyright (C) 2000-2018 jPOS Software SRL + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.jpos.http; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PipedInputStream; +import java.io.PipedOutputStream; +import java.net.InetAddress; +import java.net.Socket; +import java.net.SocketException; +import java.net.URI; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.util.BytesContentProvider; +import org.eclipse.jetty.client.util.FutureResponseListener; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +/** + * + * Wrapper around the Socket interface to connect via HTTP. + * + */ +public class HttpSocketWrapper extends Socket { + private static final Logger logger = LoggerFactory.getLogger(HttpSocketWrapper.class); + + HttpClientSocketFactory httpClientSocketFactory; + URI host; + private OutputStream out; + private PipedInputStream in; + private PipedOutputStream pOut; + private boolean isConnected = false; + private boolean closeOnError; + + private LinkedBlockingQueue queue; + private Thread t[]; + + private int timeout = 0; + private int maxSendPerRequest = 1; + private int clients = 5; + + + public HttpSocketWrapper(HttpClientSocketFactory httpClientSocketFactory, HttpClient http, String hostStr, int clients, boolean failOnError) throws IOException { + this.httpClientSocketFactory = httpClientSocketFactory; + this.host = URI.create(hostStr); + + this.clients = clients; + this.closeOnError = failOnError; + isConnected = true; + + in = new PipedInputStream(512*clients); + pOut = new PipedOutputStream(in); + + queue = new LinkedBlockingQueue(clients); + Runnable job = new Runnable() { + @Override + public void run() { + List list = new ArrayList(); + while (isConnected()) { + byte data[]; + try { + data = queue.poll(10, TimeUnit.SECONDS); + if (data == null || data.length == 0) { + continue; + } + list.add(data); + if (getMaxSendPerRequest() > 1) { + queue.drainTo(list, getMaxSendPerRequest()-1); + } + } catch (InterruptedException e1) { + logger.warn("Thread interrupted while waiting on queue.", e1); + } + logger.debug("Connecting to {} with {} messages.", host, list.size()); + try { + Request request = http.POST(host).content(new BytesContentProvider(list.toArray(new byte[0][0]))); + FutureResponseListener listener = new FutureResponseListener(request); + request.send(listener); // Asynchronous send + ContentResponse r = listener.get(getWaitTime(), TimeUnit.SECONDS); // Timed block + logger.debug("Received Response: {}", r); + if(r.getStatus() == 200) { + byte responseData[] = r.getContent(); + if (responseData != null) { + pOut.write(responseData); + synchronized(in) { + in.notifyAll(); + } + } + } else { + try { + if (HttpSocketWrapper.this.closeOnError) { + logger.warn("Failed to get response, clossing socket"); + HttpSocketWrapper.this.close(); + } + } catch (IOException e) { + logger.warn("Problem while trying to close", e); + } + + } + } catch (Exception e) { + logger.warn("Failed to execute request.", e); + try { + if (HttpSocketWrapper.this.closeOnError) { + logger.warn("Failed to get response, clossing socket"); + HttpSocketWrapper.this.close(); + } + } catch (IOException c) { + logger.warn("Problem while trying to close", c); + } + } + list.clear(); + } + } + }; + t = new Thread[clients]; + for (int i = 0; i < clients; i++) { + t[i] = new Thread(job, "HttpClient-" + i); + t[i].start(); + } + out = new ByteArrayOutputStream(256) { + + @Override + public void flush() throws IOException { + super.flush(); + if (this.size() > 0) { + synchronized (this) { + byte data[] = this.toByteArray(); + this.reset(); + try { + queue.put(data); + } catch (InterruptedException e) { + throw new IOException(e); + } + } + } + } + }; + } + + int getWaitTime() { + return (timeout > 0) ? timeout * 2 : 30000; + } + + @Override + public boolean isConnected() { + return isConnected; + } + + @Override + public InputStream getInputStream() { + return in; + } + + @Override + public OutputStream getOutputStream() { + return out; + } + + @Override + public InetAddress getInetAddress() { + try { + return InetAddress.getByName(host.getHost()); + } catch (Exception ignore) {} + return InetAddress.getLoopbackAddress(); + } + + @Override + public int getPort() { + return host.getPort(); + } + + @Override + public void setSoLinger(boolean on, int linger) throws SocketException { + } + + @Override + public synchronized void setSoTimeout(int timeout) throws SocketException { + this.timeout = timeout; + } + + @Override + public synchronized int getSoTimeout() { + return timeout; + } + + @Override + public void setKeepAlive(boolean on) throws SocketException { + } + + @Override + public synchronized void close() throws IOException { + this.isConnected = false; + logger.debug("Closing socket."); + for(int i = 0; i < clients; i++) { + try { + queue.offer(new byte[0], 10, TimeUnit.SECONDS); + } catch (InterruptedException ignore) {} + } + if (pOut != null) { + pOut.close(); + } + pOut = null; + if (in != null) { + in.close(); + } + in = null; + if (out != null) + out.close(); + out = null; + super.close(); + } + + public int getMaxSendPerRequest() { + return maxSendPerRequest; + } + + public void setMaxSendPerRequest(int maxSendPerRequest) { + this.maxSendPerRequest = maxSendPerRequest; + } + +} diff --git a/modules/iso-http-server/build.gradle b/modules/iso-http-server/build.gradle new file mode 100644 index 0000000000..8eded71ff2 --- /dev/null +++ b/modules/iso-http-server/build.gradle @@ -0,0 +1,7 @@ +description = 'jPOS-EE :: ISO over HTTP Client Code' + +dependencies { + compile libraries.jpos + compile "org.jdom:jdom2:2.0.6" +} + diff --git a/modules/iso-http-server/src/main/java/org/jpos/http/ClientRequestDetails.java b/modules/iso-http-server/src/main/java/org/jpos/http/ClientRequestDetails.java new file mode 100644 index 0000000000..4cf0c40d09 --- /dev/null +++ b/modules/iso-http-server/src/main/java/org/jpos/http/ClientRequestDetails.java @@ -0,0 +1,70 @@ +/* + * jPOS Project [http://jpos.org] + * Copyright (C) 2000-2018 jPOS Software SRL + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.jpos.http; + +public class ClientRequestDetails { + String host; + int port; + int length; + int timeout = 25 * 1000; + + public ClientRequestDetails(String host, int port, int length, int timeout) { + this(host, port, length); + this.timeout = timeout; + } + + public ClientRequestDetails(String host, int port, int length) { + this.host = host; + this.port = port; + this.length = length; + } + + public String getHost() { + return host; + } + + public void setHost(String host) { + this.host = host; + } + + public int getPort() { + return port; + } + + public void setPort(int port) { + this.port = port; + } + + public int getLength() { + return length; + } + + public void setLength(int length) { + this.length = length; + } + + public int getTimeout() { + return timeout; + } + + public void setTimeout(int timeout) { + this.timeout = timeout; + } + +} \ No newline at end of file diff --git a/modules/iso-http-server/src/main/java/org/jpos/http/FilterISOMsg.java b/modules/iso-http-server/src/main/java/org/jpos/http/FilterISOMsg.java new file mode 100644 index 0000000000..876c3daecd --- /dev/null +++ b/modules/iso-http-server/src/main/java/org/jpos/http/FilterISOMsg.java @@ -0,0 +1,226 @@ +/* + * jPOS Project [http://jpos.org] + * Copyright (C) 2000-2018 jPOS Software SRL + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.jpos.http; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; + +import org.jdom2.Element; +import org.jpos.core.ConfigurationException; +import org.jpos.iso.BaseChannel; +import org.jpos.iso.HttpWrapperUtil; +import org.jpos.iso.ISOChannelWrapper; +import org.jpos.iso.ISOFilter; +import org.jpos.iso.ISOMsg; +import org.jpos.iso.ISOPackager; +import org.jpos.iso.ISORequestListener; +import org.jpos.iso.ISOServer; +import org.jpos.q2.QFactory; +import org.jpos.q2.iso.ChannelAdaptor; +import org.jpos.q2.iso.QServer; +import org.jpos.util.NameRegistrar; +import org.jpos.util.NameRegistrar.NotFoundException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class FilterISOMsg { + private static final Logger logger = LoggerFactory.getLogger(FilterISOMsg.class); + + private static final int MIN_TIMEOUT = 5*000; // 5s min timeout + + Collection requestListeners = new ArrayList(); + ISOChannelWrapper channel = new ISOChannelWrapper(); + String name; + int port; + + public FilterISOMsg(String serverName) { + this.name = serverName; + } + + public FilterISOMsg(BaseChannel isoChannel) { + this(isoChannel, null); + } + + public FilterISOMsg(BaseChannel isoChannel, Collection requestListeners) { + configure(isoChannel, requestListeners); + } + + public void configure(BaseChannel isoChannel, Collection requestListeners) { + channel.setChannel(isoChannel); + if (requestListeners != null) { + this.requestListeners = requestListeners; + } + if (name == null) { + name = isoChannel.getName(); + } + if (isoChannel.getPort() > 0) { + port = isoChannel.getPort(); + } + } + + public ISOResponse process(byte[] inMsg, ClientRequestDetails requestDetails) { + requestDetails.setLength(inMsg.length); + ByteArrayInputStream in = new ByteArrayInputStream(inMsg); + return process(in, requestDetails); + } + + public ISOResponse process(InputStream in, ClientRequestDetails requestDetails) { + ISOResponse res = null; + BaseChannel c = null; + try { + initialize(); + ByteArrayOutputStream out = new ByteArrayOutputStream(512); + c = channel.getChannel(in, out, requestDetails); + List mList = new ArrayList(); + do { + ISOMsg m = c.receive(); + mList.add(m); + } while (HttpWrapperUtil.hasMoreData(c)); + logger.debug("Received: expected {} bytes with {} messages.", requestDetails.length, mList.size()); + res = new ISOResponse(c, mList, out); + for (ISOMsg m : mList) { + m.setSource(res); + processListeners(m); + } + } catch (Exception e) { + logger.error("Failed to process txn", e); + } + + res.getResponse(requestDetails.getTimeout()); + + if (c != null) { + try { + c.disconnect(); + } catch (IOException ignore) { + } + } + + return res; + } + + public void processListeners(ISOMsg request) { + for (ISORequestListener listener : requestListeners) { + if (listener.process(request.getSource(), request)) { + break; + } + } + } + + private Collection getListeners(QServer server) throws ConfigurationException { + QFactory factory = server.getFactory(); + @SuppressWarnings("rawtypes") + Iterator iter = server.getPersist().getChildren("request-listener").iterator(); + Collection listeners = new ArrayList(); + while (iter.hasNext()) { + Element l = (Element) iter.next(); + ISORequestListener listener = (ISORequestListener) factory.newInstance(l.getAttributeValue("class")); + factory.setLogger(listener, l); + factory.setConfiguration(listener, l); + listeners.add(listener); + } + return listeners; + } + + private BaseChannel getChannel(QServer server) throws ConfigurationException { + Element persist = server.getPersist(); + Element e = persist.getChild("channel"); + if (e == null) { + throw new ConfigurationException("channel element missing"); + } + + ChannelAdaptor adaptor = new ChannelAdaptor(); + BaseChannel channel = (BaseChannel) adaptor.newChannel(e, server.getFactory()); + if (channel == null) { + throw new ConfigurationException("Could not instanciate ISOChannel"); + } + return channel; + } + + public synchronized void initialize() throws ConfigurationException, NotFoundException { + if (getPackager() == null && name != null) { + Object entry = NameRegistrar.getIfExists(name); + if (entry instanceof QServer) { + QServer server = (QServer) entry; + configure(getChannel(server), getListeners(server)); + setPort(server.getPort()); + } + if (entry instanceof ISOServer) { + ISOServer server = (ISOServer) entry; + HttpWrapperUtil.configureFilter(this, server); + if (getPackager() == null) { + BaseChannel c = (BaseChannel) server.getISOChannel(":last"); + if (c != null) { + channel.setChannel((BaseChannel) c.clone()); + } + } + setPort(server.getPort()); + } + } + if (getPort() <= 0 && name != null) { + Object entry = NameRegistrar.getIfExists(name); + if (entry instanceof QServer) { + QServer server = (QServer) entry; + setPort(server.getPort()); + } + if (entry instanceof ISOServer) { + ISOServer server = (ISOServer) entry; + setPort(server.getPort()); + } + } + } + + public int getTimeout() { + return Math.max(channel.getChannel().getTimeout(), MIN_TIMEOUT); + } + + public ISOPackager getPackager() { + return channel.getPackager(); + } + + public void setPackager(ISOPackager clientPackager) { + channel.setPackager(clientPackager); + } + + public void setPort(int port) { + this.port = port; + } + + public int getPort() { + return port; + } + + public String getName() { + return name; + } + + public Collection getIncomingFilters() { + return channel.getChannel().getIncomingFilters(); + } + + public Collection getOutgoingFilters() { + return channel.getChannel().getOutgoingFilters(); + } + +} diff --git a/modules/iso-http-server/src/main/java/org/jpos/http/FilterISOMsgRegistry.java b/modules/iso-http-server/src/main/java/org/jpos/http/FilterISOMsgRegistry.java new file mode 100644 index 0000000000..9cbf107779 --- /dev/null +++ b/modules/iso-http-server/src/main/java/org/jpos/http/FilterISOMsgRegistry.java @@ -0,0 +1,157 @@ +/* + * jPOS Project [http://jpos.org] + * Copyright (C) 2000-2018 jPOS Software SRL + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.jpos.http; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.jpos.core.ConfigurationException; +import org.jpos.iso.ISOServer; +import org.jpos.q2.iso.QServer; +import org.jpos.util.NameRegistrar; +import org.jpos.util.NameRegistrar.NotFoundException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class FilterISOMsgRegistry { + private static final Logger logger = LoggerFactory.getLogger(FilterISOMsgRegistry.class); + + protected Map registry = new HashMap(); + protected Map portRegistry = new HashMap(); + + protected boolean autoRegister = false; + + public static final FilterISOMsgRegistry instance = new FilterISOMsgRegistry(); + + public boolean add(String name, boolean initialize) throws ConfigurationException, NotFoundException { + if (registry.get(name) != null) return false; + + if ("*".equals(name)) { + autoRegister = true; + addAll(initialize); + } else { + FilterISOMsg filter = new FilterISOMsg(name); + init(filter, initialize); + registry.put(name, filter); + } + + return true; + } + + @SuppressWarnings("unchecked") + private Map getNameRegistryMap() throws ConfigurationException { + // In order to support old jpos getMap and the newer getAsMap + // we have to use reflection. + Map map = null; + Method getMap = null; + try { + getMap = NameRegistrar.class.getMethod("getAsMap", new Class[0]); + } catch (Exception e) { + try { + getMap = NameRegistrar.class.getMethod("getMap", new Class[0]); + } catch (NoSuchMethodException | SecurityException e1) { + } + } + if (getMap != null) { + try { + map = (Map) getMap.invoke(NameRegistrar.class, new Object[0]); + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { + logger.warn("Error getting NameRegistrar getMap or getAsMap, posibly wrong JPOS version.", e); + throw new ConfigurationException(e); + } + } + return map; + } + + private void addAll(boolean initialize) throws ConfigurationException { + logger.debug("Adding All servers to http."); + Map r = getNameRegistryMap(); + // Look for QServers to add. Note Also look for ISOServer to + // keep compatibility with really old jpos. + for(Map.Entry entry: r.entrySet()) { + if (entry.getValue() instanceof QServer || entry.getValue() instanceof ISOServer) { + try { + add(entry.getKey(), initialize); + } catch (ConfigurationException | NotFoundException e) { + logger.warn("Problem registering server, ignoring", e); + } + } + } + + } + + public List getRegisteredNames() { + return new ArrayList(registry.keySet()); + } + + public List getRegisteredPorts() { + return new ArrayList(portRegistry.keySet()); + } + + protected void init(FilterISOMsg filter, boolean initialize) throws ConfigurationException, NotFoundException { + + if (initialize) { + filter.initialize(); + } + + if (filter.getPort() > 0) { + portRegistry.put(filter.getPort(), filter.getName()); + } + } + + public FilterISOMsg get(String name) { + FilterISOMsg filter = registry.get(name); + if (filter == null && name.matches("^\\d+$")) { + Integer port = new Integer(name); + name = portRegistry.get(port); + if (name != null) { + filter = registry.get(name); + } else { + if (autoRegister) { + try { + addAll(true); + } catch (ConfigurationException ignore) { + logger.warn("Problem with NameRegistrar posibly wrong JPOS version, ignoring.", ignore); + } + } + Map unregistered = new HashMap(); + unregistered.putAll(registry); + unregistered.keySet().removeAll(portRegistry.values()); + for (FilterISOMsg f: unregistered.values()) { + try { + init(f, true); + } catch (ConfigurationException | NotFoundException e) { + logger.warn("Problem initializing server for http, ignoring", e); + } + if (f.getPort() == port) { + filter = f; + break; + } + } + } + } + + return filter; + } + +} diff --git a/modules/iso-http-server/src/main/java/org/jpos/http/ISOResponse.java b/modules/iso-http-server/src/main/java/org/jpos/http/ISOResponse.java new file mode 100644 index 0000000000..6edd53ba05 --- /dev/null +++ b/modules/iso-http-server/src/main/java/org/jpos/http/ISOResponse.java @@ -0,0 +1,88 @@ +/* + * + /* + * jPOS Project [http://jpos.org] + * Copyright (C) 2000-2018 jPOS Software SRL + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.jpos.http; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.jpos.iso.ISOChannel; +import org.jpos.iso.ISOException; +import org.jpos.iso.ISOMsg; +import org.jpos.iso.ISOSource; + +public class ISOResponse implements ISOSource { + ISOChannel channel; + ByteArrayOutputStream out; + boolean timedout = false; + + public List request; + public List response; + + public ISOResponse(ISOChannel channel, List request, ByteArrayOutputStream out) { + super(); + this.channel = channel; + this.request = request; + this.out = out; + response = new ArrayList(request.size()); + } + + @Override + public synchronized void send(ISOMsg m) throws IOException, ISOException { + response.add(m); + channel.send(m); + notifyAll(); + } + + @Override + public boolean isConnected() { + return !timedout && !hasResponse(); + } + + public boolean hasResponse() { + return response.size() >= request.size(); + } + + public synchronized byte[] getResponse(long timeoutMs) { + long timeout = System.currentTimeMillis() + timeoutMs; + while (!hasResponse() && timeout > System.currentTimeMillis()) { + try { + wait(timeout - System.currentTimeMillis()); + } catch (Exception ignore) { + } + } + if (!hasResponse()) { + timedout = true; + } + + if (response.isEmpty()) { + return null; + } else { + return out.toByteArray(); + } + } + + public byte[] getResponse() { + return (!response.isEmpty() ? out.toByteArray() : null); + } + +} diff --git a/modules/iso-http-server/src/main/java/org/jpos/iso/HttpWrapperUtil.java b/modules/iso-http-server/src/main/java/org/jpos/iso/HttpWrapperUtil.java new file mode 100644 index 0000000000..03750b1b90 --- /dev/null +++ b/modules/iso-http-server/src/main/java/org/jpos/iso/HttpWrapperUtil.java @@ -0,0 +1,41 @@ +/* + * jPOS Project [http://jpos.org] + * Copyright (C) 2000-2018 jPOS Software SRL + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.jpos.iso; + +import java.io.IOException; + +import org.jpos.http.FilterISOMsg; + +public class HttpWrapperUtil { + + public static boolean hasMoreData(BaseChannel c) throws IOException { + return c.serverIn.available() > 0; + } + + public static void configureFilter(FilterISOMsg filter, ISOServer server) { + ISOChannel c = server.clientSideChannel != null ? server.clientSideChannel : server.getISOChannel(":last"); + c = (c != null ? (ISOChannel) c.clone() : null); + filter.configure((BaseChannel) c, server.listeners); + if (filter.getPackager() == null) { + filter.setPackager(server.clientPackager); + } + filter.setPort(server.getPort()); + } + +} diff --git a/modules/iso-http-server/src/main/java/org/jpos/iso/ISOChannelWrapper.java b/modules/iso-http-server/src/main/java/org/jpos/iso/ISOChannelWrapper.java new file mode 100644 index 0000000000..f9c19e44b3 --- /dev/null +++ b/modules/iso-http-server/src/main/java/org/jpos/iso/ISOChannelWrapper.java @@ -0,0 +1,124 @@ +/* + * jPOS Project [http://jpos.org] + * Copyright (C) 2000-2018 jPOS Software SRL + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.jpos.iso; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.InetAddress; +import java.net.Socket; +import java.net.SocketException; + +import org.jpos.http.ClientRequestDetails; + +public class ISOChannelWrapper { + private BaseChannel channel; + + public BaseChannel getChannel(InputStream in, OutputStream out, ClientRequestDetails requestDetails) + throws IOException { + BaseChannel c = (BaseChannel) channel.clone(); + FakeSocket s = new FakeSocket(in, out); + s.setRequest(requestDetails); + c.connect(s); + + return c; + } + + public BaseChannel getChannel() { + return channel; + } + + public void setChannel(BaseChannel channel) { + this.channel = channel; + } + + public void setPackager(ISOPackager p) { + channel.setPackager(p); + } + + public ISOPackager getPackager() { + return channel != null ? channel.getPackager() : null; + } + + static class FakeSocket extends Socket { + InputStream in; + OutputStream out; + boolean isConnected = false; + ClientRequestDetails requestDetails; + + public FakeSocket(InputStream in, OutputStream out) { + this.in = in; + this.out = out; + isConnected = true; + } + + public void setRequest(ClientRequestDetails requestDetails) { + this.requestDetails = requestDetails; + } + + @Override + public boolean isConnected() { + return isConnected; + } + + @Override + public InputStream getInputStream() { + return in; + } + + @Override + public OutputStream getOutputStream() { + return out; + } + + @Override + public InetAddress getInetAddress() { + try { + return (requestDetails != null && requestDetails.getHost() != null) + ? InetAddress.getByName(requestDetails.getHost()) : InetAddress.getLoopbackAddress(); + } catch (Exception ignore) { + + } + return InetAddress.getLoopbackAddress(); + } + + @Override + public int getPort() { + return (requestDetails != null && requestDetails.getHost() != null) ? requestDetails.getPort() : 0; + } + + @Override + public void setSoLinger(boolean on, int linger) throws SocketException { + } + + @Override + public synchronized void setSoTimeout(int timeout) throws SocketException { + } + + @Override + public void setKeepAlive(boolean on) throws SocketException { + } + + @Override + public synchronized void close() throws IOException { + this.isConnected = false; + super.close(); + } + } +} diff --git a/modules/iso-http-servlet/build.gradle b/modules/iso-http-servlet/build.gradle new file mode 100644 index 0000000000..56781610a4 --- /dev/null +++ b/modules/iso-http-servlet/build.gradle @@ -0,0 +1,20 @@ +description = 'jPOS-EE :: ISO over HTTP Client Code' + +apply plugin: 'war' + +dependencies { + compile libraries.jpos + compile libraries.servlet_api + compile "javax.ws.rs:javax.ws.rs-api:${jaxrsVersion}" + compile project(':modules:iso-http-server') + providedCompile project(':modules:jetty') +} + +jar { + from 'src/main/webapp' +} + +apply from: "${rootProject.projectDir}/jpos-app.gradle" + +installApp.dependsOn('war') +dist.dependsOn('war') diff --git a/modules/iso-http-servlet/src/main/java/org/jpos/http/service/HttpTransactionServlet.java b/modules/iso-http-servlet/src/main/java/org/jpos/http/service/HttpTransactionServlet.java new file mode 100644 index 0000000000..d448e319fe --- /dev/null +++ b/modules/iso-http-servlet/src/main/java/org/jpos/http/service/HttpTransactionServlet.java @@ -0,0 +1,139 @@ +package org.jpos.http.service; + +import java.io.IOException; +import java.io.OutputStream; + +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.core.MediaType; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.jpos.core.ConfigurationException; +import org.jpos.util.NameRegistrar.NotFoundException; + +import org.jpos.http.ClientRequestDetails; +import org.jpos.http.FilterISOMsg; +import org.jpos.http.FilterISOMsgRegistry; +import org.jpos.http.ISOResponse; + + +/** + * HttpTransactionServlet is used as a servlet + * in order to provide hooks for ISO traffic over HTTP using + * a servlet container like Jetty. + * Used in conjunction with module iso-http-servlet + * on the server side. + * + * @version $Revision$ $Date$ + * @author Ozzy Espaillat + * + */ +@SuppressWarnings("serial") +public class HttpTransactionServlet extends HttpServlet { + + private static final Logger logger = LoggerFactory.getLogger(HttpTransactionServlet.class); + private static final int MIN_TIMEOUT = 20*1000; + static final FilterISOMsgRegistry registry = FilterISOMsgRegistry.instance; + + @Override + public void init(ServletConfig config) throws ServletException { + try { + String initChannels = config.getInitParameter("channels"); + if (initChannels == null || initChannels.length() == 0) { + registry.add("*", false); + } else { + for (String channel: initChannels.split("[,; ]")) { + registry.add(channel, false); + } + } + } catch (ConfigurationException | NotFoundException e) { + logger.warn("Failed to configure channels:", e); + throw new ServletException(e); + } + } + + protected String getChannel(HttpServletRequest req) { + String path = req.getPathInfo(); + if (path.endsWith("/")) { + path = path.substring(0, path.length()-1); + } + return path.substring(path.lastIndexOf('/')+1); + } + + @Override + protected void doPost(HttpServletRequest request, HttpServletResponse res) + throws ServletException, IOException { + if (!MediaType.APPLICATION_OCTET_STREAM.equals(request.getContentType())) { + res.sendError(HttpServletResponse.SC_BAD_REQUEST, "Content type must be " + MediaType.APPLICATION_OCTET_STREAM); + return; + } + + String src = getChannel(request); + FilterISOMsg filter = registry.get(src); + + if (filter == null) { + res.sendError(HttpServletResponse.SC_NOT_FOUND, "Port was not found on this server. Valid sources are: " + registry.getRegisteredPorts()); + } else { + long startTime = System.currentTimeMillis(); + logger.debug("Received request on {} with filter {}", src, filter.getName()); + ClientRequestDetails clientDetails = new ClientRequestDetails(request.getRemoteAddr(), + request.getServerPort(), + request.getContentLength(), + Math.max(filter.getTimeout(), MIN_TIMEOUT)); + + ISOResponse isoRes = filter.process(request.getInputStream(), clientDetails); + byte data[] = isoRes.getResponse(); + + if (logger.isDebugEnabled()) { + logger.debug("Received: {} messages and responded with {} messges with request {} and response {} bytes in {}ms, timeout is {}", + isoRes.request.size(), isoRes.response.size(), request.getContentLength(), + (System.currentTimeMillis()-startTime), (data == null?0:data.length), + clientDetails.getTimeout()); + } + if (data != null) { + res.setStatus(HttpServletResponse.SC_OK); + + res.setContentLength(data.length); + res.setContentType(MediaType.APPLICATION_OCTET_STREAM); + + OutputStream out = res.getOutputStream(); + out.write(data); + out.close(); + } else { + res.sendError(HttpServletResponse.SC_REQUEST_TIMEOUT, "Response took longer than " + filter.getTimeout() + "ms timeout."); + } + } + } + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse res) + throws ServletException, IOException { + String src = getChannel(request); + FilterISOMsg filter = registry.get(src); + + if (filter == null) { + res.sendError(HttpServletResponse.SC_NOT_FOUND, "Port was not found on this server. Valid sources are: " + registry.getRegisteredNames() + " and ports: " + registry.getRegisteredPorts()); + } else if (filter.getPackager() == null) { + logger.debug("Packager is not yet available. " + filter.getPackager()); + try { + filter.initialize(); + } catch (Exception e) { + logger.debug("Failed configuring channel " + filter.getName(), e); + } + logger.debug("After initialized: " + filter.getPackager()); + res.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE, filter.getName() + " is not available because packager is: " + filter.getPackager()); + } else { + res.setStatus(HttpServletResponse.SC_OK); + byte msg[] = (filter.getName() + " is ready to accept transactions.").getBytes(); + res.setContentLength(msg.length); + OutputStream out = res.getOutputStream(); + out.write(msg); + out.close(); + } + } +} diff --git a/modules/iso-http-servlet/src/main/webapp/WEB-INF/jetty-web.xml b/modules/iso-http-servlet/src/main/webapp/WEB-INF/jetty-web.xml new file mode 100644 index 0000000000..8698f586ed --- /dev/null +++ b/modules/iso-http-servlet/src/main/webapp/WEB-INF/jetty-web.xml @@ -0,0 +1,9 @@ + + + + + /transaction + /webapps/@warname@ + false + + diff --git a/modules/iso-http-servlet/src/main/webapp/WEB-INF/web.xml b/modules/iso-http-servlet/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 0000000000..c7b8f4ca7e --- /dev/null +++ b/modules/iso-http-servlet/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,18 @@ + + + + ISO HTTP Interface + + + + HttpTransactionServlet + org.jpos.http.service.HttpTransactionServlet + 1 + + + + HttpTransactionServlet + /iso/* + + + \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 86856c2931..7f2ac6536d 100644 --- a/settings.gradle +++ b/settings.gradle @@ -37,7 +37,11 @@ include ':modules:core', ':modules:binlog-quartz', ':modules:cryptoservice', ':modules:qrest', - ':modules:cryptoserver' + ':modules:cryptoserver', + ':modules:iso-http-client', + ':modules:iso-http-server', + ':modules:iso-http-servlet' + rootProject.name = 'jposee'